Roudrigus commited on
Commit
296b115
·
verified ·
1 Parent(s): fbbbeb0

Update repositorio_load.py

Browse files
Files changed (1) hide show
  1. repositorio_load.py +729 -697
repositorio_load.py CHANGED
@@ -1,697 +1,729 @@
1
-
2
- # -*- coding: utf-8 -*-
3
- import os
4
- import base64
5
- import hashlib
6
- import mimetypes
7
- from datetime import datetime
8
-
9
- import streamlit as st
10
- import pandas as pd
11
-
12
- from banco import engine, Base, SessionLocal
13
- from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float, Text
14
-
15
- from string import Template # ✅ Evita conflitos com { } em HTML/JS
16
-
17
- # ==============================
18
- # Modelo de dados — repositório
19
- # ==============================
20
- class RepoArquivo(Base):
21
- __tablename__ = "repo_arquivo"
22
-
23
- id = Column(Integer, primary_key=True, autoincrement=True)
24
- ambiente = Column(String(16), nullable=False) # prod/test/etc
25
- storage_path = Column(String(512), nullable=False) # caminho no disco
26
- original_name = Column(String(256), nullable=False)
27
- title = Column(String(256), nullable=True)
28
- description = Column(Text, nullable=True)
29
- tags = Column(String(256), nullable=True) # separado por vírgulas
30
- mime = Column(String(128), nullable=False)
31
- size_kb = Column(Float, nullable=True)
32
- uploaded_by = Column(String(128), nullable=False)
33
- uploaded_at = Column(DateTime, nullable=False, default=datetime.now)
34
- is_public = Column(Boolean, nullable=False, default=True)
35
-
36
- # Cria a tabela se não existir
37
- Base.metadata.create_all(bind=engine)
38
-
39
- # ==============================
40
- # Helpers paths e segurança
41
- # ==============================
42
- def _current_env_label():
43
- try:
44
- from db_router import current_db_choice
45
- env = current_db_choice()
46
- return env or "prod"
47
- except Exception:
48
- return "prod"
49
-
50
- def _repo_root():
51
- """Pasta raiz do repositório por ambiente."""
52
- env = _current_env_label()
53
- root = os.path.join("data_repo", env)
54
- os.makedirs(root, exist_ok=True)
55
- return root
56
-
57
- def _hash_bytes(b: bytes) -> str:
58
- return hashlib.sha256(b).hexdigest()
59
-
60
- # MIME por extensão (fallback)
61
- _EXT_MIME = {
62
- ".pdf": "application/pdf",
63
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
64
- ".xls": "application/vnd.ms-excel",
65
- }
66
- def _detect_mime_by_name(original_name: str) -> str:
67
- guessed = mimetypes.guess_type(original_name)[0]
68
- if guessed:
69
- return guessed
70
- ext = os.path.splitext(original_name)[1].lower()
71
- return _EXT_MIME.get(ext, "application/octet-stream")
72
-
73
- def _save_file_to_repo(file_obj, original_name: str) -> tuple[str, float, str]:
74
- """Salva o arquivo e retorna (storage_path, size_kb, mime)."""
75
- content = file_obj.read()
76
- size_kb = round(len(content) / 1024.0, 2)
77
- mime = _detect_mime_by_name(original_name)
78
- root = _repo_root()
79
- ext = os.path.splitext(original_name)[1].lower()
80
- uid = _hash_bytes(content)[:16]
81
- storage_name = f"{uid}{ext}"
82
- storage_path = os.path.join(root, storage_name)
83
- with open(storage_path, "wb") as f:
84
- f.write(content)
85
- return storage_path, size_kb, mime
86
-
87
- # ==============================
88
- # Preview — PDF e Excel
89
- # ==============================
90
- def _pdf_b64_cached(storage_path: str) -> str:
91
- """Cache leve do PDF em base64 na sessão (chaveada por path)."""
92
- key = f"__repo_pdf_b64__::{storage_path}"
93
- if key in st.session_state:
94
- return st.session_state[key]
95
- with open(storage_path, "rb") as f:
96
- b64 = base64.b64encode(f.read()).decode("utf-8")
97
- st.session_state[key] = b64
98
- return b64
99
-
100
- def _is_too_big_for_inline(storage_path: str, max_mb: int = 15) -> bool:
101
- """Evita tentar embed de arquivos muito grandes (limite ajustável)."""
102
- try:
103
- return os.path.getsize(storage_path) > max_mb * 1024 * 1024
104
- except Exception:
105
- return False
106
-
107
- def _button_open_new_tab_blob(storage_path: str, key_prefix: str, label: str = "🧭 Abrir em nova aba"):
108
- """
109
- Renderiza um botão/link que abre o PDF em nova aba usando blob: (sem embed).
110
- Mais robusto em ambientes com bloqueio de iframe/object.
111
- """
112
- try:
113
- b64 = _pdf_b64_cached(storage_path)
114
- html_tpl = Template(r"""
115
- <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
116
- <button id="${K}_btn" style="padding:6px 10px;cursor:pointer;">${LABEL}</button>
117
- <span id="${K}_msg" style="font:12px system-ui;color:#475569;"></span>
118
- </div>
119
- <script>
120
- (function(){
121
- const btn = document.getElementById("${K}_btn");
122
- const msg = document.getElementById("${K}_msg");
123
- try {
124
- const b64 = "${B64}";
125
- const raw = atob(b64);
126
- const len = raw.length;
127
- const bytes = new Uint8Array(len);
128
- for (let i=0;i<len;i++) bytes[i] = raw.charCodeAt(i);
129
- const blob = new Blob([bytes], { type: "application/pdf" });
130
-
131
- btn.addEventListener("click", function(){
132
- try {
133
- const url = URL.createObjectURL(blob);
134
- const w = window.open(url, "_blank");
135
- if (!w) {
136
- msg.textContent = "Bloqueado por pop-up. Permita pop-ups para este site e clique novamente.";
137
- msg.style.color = "#b45309";
138
- } else {
139
- msg.textContent = "Aberto em nova aba.";
140
- msg.style.color = "#0f766e";
141
- setTimeout(() => URL.revokeObjectURL(url), 30000);
142
- }
143
- } catch(e) {
144
- msg.textContent = "Falha ao abrir em nova aba: " + e;
145
- msg.style.color = "#ef4444";
146
- }
147
- });
148
- } catch(e) {
149
- msg.textContent = "Falha ao preparar visualização: " + e;
150
- msg.style.color = "#ef4444";
151
- }
152
- })();
153
- </script>
154
- """)
155
- html = html_tpl.substitute(K=key_prefix, B64=b64, LABEL=label)
156
- st.components.v1.html(html, height=40)
157
- except Exception as e:
158
- st.warning(f"Não foi possível preparar abertura em nova aba: {e}")
159
-
160
- def _embed_pdf_blob(storage_path: str, height: int = 650, mode: str = "iframe"):
161
- """
162
- Tenta exibir inline via blob: (iframe ou object). Se não carregar em 3,5s,
163
- mostra status amigável para o usuário.
164
- """
165
- try:
166
- b64 = _pdf_b64_cached(storage_path)
167
- viewer_iframe = '<iframe id="pdfifr" src="" title="Pré-visualização PDF"></iframe>'
168
- viewer_object = '<object id="pdfobj" type="application/pdf" data="" width="100%" height="${H}px"><embed id="pdfemb" type="application/pdf" src="" width="100%" height="${H}px" /></object>'
169
- viewer = viewer_object if mode == "object" else viewer_iframe
170
- if mode == "object":
171
- viewer = Template(viewer).substitute(H=int(height))
172
-
173
- inject = (
174
- 'var o=document.getElementById("pdfobj"); var e=document.getElementById("pdfemb"); '
175
- 'if(o){o.setAttribute("data", url); viewerEl=o;} else if(e){e.setAttribute("src", url); viewerEl=e;}'
176
- if mode == "object"
177
- else 'var f=document.getElementById("pdfifr"); if(f){f.setAttribute("src", url); viewerEl=f;}'
178
- )
179
-
180
- html_tpl = Template(r"""
181
- <!DOCTYPE html>
182
- <html>
183
- <head>
184
- <meta charset="utf-8" />
185
- <style>
186
- body { margin:0; padding:0; }
187
- .wrap { width:100%; height:${H}px; }
188
- iframe, object, embed { width:100%; height:${H}px; border:0; }
189
- #err { color:#ef4444; font:12px system-ui; padding:6px; }
190
- .toolbar { padding:6px; font:12px system-ui; background:#f1f5f9; color:#0f172a; display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
191
- .toolbar button { padding:6px 10px; }
192
- .status-ok { color:#0f766e; }
193
- .status-warn { color:#b45309; }
194
- </style>
195
- </head>
196
- <body>
197
- <div class="toolbar">
198
- <span id="status" class="status-warn">carregando visualizador ${MODE}…</span>
199
- </div>
200
- <div class="wrap">
201
- ${VIEWER}
202
- </div>
203
- <div id="err"></div>
204
- <script>
205
- (function() {
206
- const err = (m) => { document.getElementById('err').textContent = String(m||'Falha ao exibir o PDF.'); };
207
- const status = document.getElementById('status');
208
- try {
209
- const b64 = "${B64}";
210
- const raw = atob(b64);
211
- const len = raw.length;
212
- const bytes = new Uint8Array(len);
213
- for (let i=0;i<len;i++) bytes[i] = raw.charCodeAt(i);
214
- const blob = new Blob([bytes], { type: "application/pdf" });
215
- const url = URL.createObjectURL(blob);
216
- status.textContent = "visualizador: ${MODE} (blob)";
217
- status.className = "status-warn";
218
-
219
- let loaded = false;
220
- let viewerEl = null;
221
-
222
- ${INJECT}
223
-
224
- try {
225
- if (viewerEl && viewerEl.addEventListener) {
226
- viewerEl.addEventListener("load", function(){
227
- loaded = true;
228
- status.textContent = "visualizador carregado";
229
- status.className = "status-ok";
230
- });
231
- }
232
- } catch(e){}
233
-
234
- setTimeout(function(){
235
- if (!loaded) {
236
- status.textContent = "preview pode estar bloqueado no navegador (iframe/object). Use 'Abrir em nova aba' no app.";
237
- status.className = "status-warn";
238
- }
239
- }, 3500);
240
- } catch(e) {
241
- err(e);
242
- }
243
- })();
244
- </script>
245
- </body>
246
- </html>
247
- """)
248
- html = html_tpl.substitute(
249
- H=int(height),
250
- VIEWER=viewer,
251
- B64=b64,
252
- MODE=mode,
253
- INJECT=inject
254
- )
255
- st.components.v1.html(html, height=height + 46, scrolling=False)
256
- except Exception as e:
257
- st.warning(f"Não foi possível exibir o PDF (blob/{mode}): {e}")
258
-
259
- def _embed_pdf_pdfjs(storage_path: str, height: int = 650, theme: str = "dark"):
260
- """PDF.js via CDN (alternativo) — com navegação, zoom e abrir em nova aba (Blob)."""
261
- try:
262
- b64 = _pdf_b64_cached(storage_path)
263
- bg = "#0b1220" if theme == "dark" else "#f8fafc"
264
- fg = "#e2e8f0" if theme == "dark" else "#0f172a"
265
- btn = "#1e293b" if theme == "dark" else "#e2e8f0"
266
-
267
- html_tpl = Template(r"""
268
- <!DOCTYPE html>
269
- <html>
270
- <head>
271
- <meta charset="utf-8" />
272
- <style>
273
- body {
274
- margin: 0; padding: 8px;
275
- background: ${BG}; color: ${FG};
276
- font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
277
- }
278
- .toolbar {
279
- display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap;
280
- }
281
- .toolbar button {
282
- background: ${BTN}; color: ${FG}; border: 1px solid rgba(0,0,0,.1); border-radius: 6px;
283
- padding: 6px 10px; cursor: pointer;
284
- }
285
- .toolbar span { padding: 0 6px; }
286
- .viewer {
287
- width: 100%; height: ${VH}px; overflow: auto;
288
- background: white; border-radius: 6px; box-shadow: 0 1px 4px rgba(0,0,0,.15);
289
- display: flex; justify-content: center; align-items: flex-start; padding: 12px;
290
- }
291
- #pdf-canvas { background: #fff; }
292
- #err { color: #ef4444; font-size: 12px; margin-top: 6px; }
293
- </style>
294
- </head>
295
- <body>
296
- <div class="toolbar">
297
- <button id="prev">⬅️ Página anterior</button>
298
- <button id="next">➡️ Próxima página</button>
299
- <span id="page_info">Página 1 / ?</span>
300
- <button id="zoom_out">➖ Zoom</button>
301
- <button id="zoom_in">➕ Zoom</button>
302
- <button id="fitw">↔️ Ajustar largura</button>
303
- <button id="fitp">↕️ Ajustar página</button>
304
- <button id="open_new">🧭 Abrir em nova aba</button>
305
- </div>
306
- <div class="viewer">
307
- <canvas id="pdf-canvas"></canvas>
308
- </div>
309
- <div id="err"></div>
310
-
311
- https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js</script>
312
- <script>
313
- (function() {
314
- const errBox = document.getElementById('err');
315
- function showErr(msg) { errBox.textContent = String(msg || 'Falha ao renderizar o PDF.'); }
316
- window.onerror = function(msg) { showErr(msg); };
317
-
318
- try {
319
- const b64 = "${B64}";
320
- const raw = atob(b64);
321
- const len = raw.length;
322
- const bytes = new Uint8Array(len);
323
- for (let i = 0; i < len; i++) bytes[i] = raw.charCodeAt(i);
324
-
325
- const canvas = document.getElementById('pdf-canvas');
326
- const ctx = canvas.getContext('2d');
327
- const pageInfo = document.getElementById('page_info');
328
- const prevBtn = document.getElementById('prev');
329
- const nextBtn = document.getElementById('next');
330
- const zoomIn = document.getElementById('zoom_in');
331
- const zoomOut = document.getElementById('zoom_out');
332
- const fitW = document.getElementById('fitw');
333
- const fitP = document.getElementById('fitp');
334
- const openNew = document.getElementById('open_new');
335
-
336
- pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
337
-
338
- let pdfDoc = null;
339
- let currentPage = 1;
340
- let scale = 1.2;
341
- let fitMode = "width";
342
-
343
- function renderPage(num) {
344
- pdfDoc.getPage(num).then(function(page) {
345
- const viewport = page.getViewport({ scale: scale });
346
- canvas.width = viewport.width;
347
- canvas.height = viewport.height;
348
- const renderCtx = { canvasContext: ctx, viewport: viewport };
349
- page.render(renderCtx).promise.then(function() {
350
- pageInfo.textContent = "Página " + num + " / " + pdfDoc.numPages;
351
- });
352
- }).catch(showErr);
353
- }
354
-
355
- function fitWidth() {
356
- fitMode = "width";
357
- pdfDoc.getPage(currentPage).then(function(page) {
358
- const container = canvas.parentElement;
359
- const margins = 24;
360
- const w = container.clientWidth - margins;
361
- const viewport = page.getViewport({ scale: 1 });
362
- scale = Math.max(0.2, w / viewport.width);
363
- renderPage(currentPage);
364
- }).catch(showErr);
365
- }
366
-
367
- function fitPage() {
368
- fitMode = "page";
369
- pdfDoc.getPage(currentPage).then(function(page) {
370
- const container = canvas.parentElement;
371
- const margins = 24;
372
- const w = container.clientWidth - margins;
373
- const h = container.clientHeight - margins;
374
- const viewport = page.getViewport({ scale: 1 });
375
- const scaleW = w / viewport.width;
376
- const scaleH = h / viewport.height;
377
- scale = Math.max(0.2, Math.min(scaleW, scaleH));
378
- renderPage(currentPage);
379
- }).catch(showErr);
380
- }
381
-
382
- pdfjsLib.getDocument({ data: bytes }).promise.then(function(pdf) {
383
- pdfDoc = pdf;
384
- if (fitMode === "width") fitWidth(); else fitPage();
385
- }).catch(showErr);
386
-
387
- prevBtn.addEventListener('click', function() {
388
- if (!pdfDoc) return;
389
- if (currentPage <= 1) return;
390
- currentPage--;
391
- if (fitMode === "width") fitWidth(); else fitPage();
392
- });
393
- nextBtn.addEventListener('click', function() {
394
- if (!pdfDoc) return;
395
- if (currentPage >= pdfDoc.numPages) return;
396
- currentPage++;
397
- if (fitMode === "width") fitWidth(); else fitPage();
398
- });
399
- zoomIn.addEventListener('click', function() { scale = Math.min(scale + 0.1, 4.0); renderPage(currentPage); });
400
- zoomOut.addEventListener('click', function() { scale = Math.max(scale - 0.1, 0.2); renderPage(currentPage); });
401
- fitW.addEventListener('click', fitWidth);
402
- fitP.addEventListener('click', fitPage);
403
- openNew.addEventListener('click', function() {
404
- try {
405
- const blob = new Blob([bytes], { type: 'application/pdf' });
406
- const url = URL.createObjectURL(blob);
407
- window.open(url, '_blank');
408
- setTimeout(() => URL.revokeObjectURL(url), 30000);
409
- } catch (e) { showErr(e); }
410
- });
411
-
412
- window.addEventListener('resize', function() {
413
- if (!pdfDoc) return;
414
- if (fitMode === "width") fitWidth(); else fitPage();
415
- });
416
- } catch (e) { showErr(e); }
417
- })();
418
- </script>
419
- </body>
420
- </html>
421
- """)
422
- html = html_tpl.substitute(
423
- B64=b64,
424
- BG=bg,
425
- FG=fg,
426
- BTN=btn,
427
- VH=max(100, int(height - 54))
428
- )
429
- st.components.v1.html(html, height=height + 16, scrolling=False)
430
- except Exception as e:
431
- st.warning(f"Não foi possível exibir o PDF (PDF.js): {e}")
432
-
433
- def _preview_excel(storage_path: str):
434
- """Exibe primeiras linhas de um Excel."""
435
- try:
436
- path = storage_path.lower()
437
- if path.endswith(".xlsx"):
438
- df = pd.read_excel(storage_path, engine="openpyxl")
439
- elif path.endswith(".xls"):
440
- df = pd.read_excel(storage_path, engine="xlrd")
441
- else:
442
- st.info("Arquivo não reconhecido como Excel.")
443
- return
444
- st.dataframe(df.head(200), use_container_width=True)
445
- except Exception as e:
446
- st.warning(f"Não foi possível ler o Excel: {e}")
447
-
448
- def _render_pdf_preview(storage_path: str, height: int, key_prefix: str, default: str = "PDF.js (alternativo)"):
449
- """
450
- Viewer por arquivo:
451
- - Botão "Abrir em nova aba" (SEM embed) sempre visível.
452
- - Inline (experimental): PDF.js, Iframe (Blob) ou OBJECT/EMBED (Blob).
453
- - Arquivo grande: bypass para nova aba.
454
- """
455
- # 1) Sempre oferecer "Abrir em nova aba" (mais confiável)
456
- _button_open_new_tab_blob(storage_path, key_prefix=f"{key_prefix}_open", label="🧭 Abrir em nova aba")
457
-
458
- # 2) Bypass para PDFs muito grandes
459
- if _is_too_big_for_inline(storage_path, max_mb=15):
460
- st.info("📄 PDF grande — recomendado abrir em nova aba (acima).")
461
- return
462
-
463
- # 3) Pré-visualização inline (experimental)
464
- with st.expander("🔬 Pré‑visualização inline (experimental)", expanded=False):
465
- viewer_opt = st.radio(
466
- "Visualizador de PDF:",
467
- options=["PDF.js (alternativo)", "Iframe (Blob)", "OBJECT/EMBED (Blob compat.)"],
468
- index={"PDF.js (alternativo)": 0, "Iframe (Blob)": 1, "OBJECT/EMBED (Blob compat.)": 2}.get(default, 0),
469
- key=f"{key_prefix}_viewer"
470
- )
471
- if viewer_opt == "PDF.js (alternativo)":
472
- _embed_pdf_pdfjs(storage_path, height=height, theme="dark")
473
- elif viewer_opt == "Iframe (Blob)":
474
- _embed_pdf_blob(storage_path, height=height, mode="iframe")
475
- else:
476
- _embed_pdf_blob(storage_path, height=height, mode="object")
477
-
478
- # ==============================
479
- # UImódulo
480
- # ==============================
481
- def main():
482
- st.title("📦 Repositório Load")
483
- st.caption("Admin importa arquivos (Excel/PDF). Usuários consultam online e baixam.")
484
-
485
- perfil = st.session_state.get("perfil", "usuario")
486
- usuario = st.session_state.get("usuario") or "anon"
487
- env = _current_env_label()
488
- db = SessionLocal()
489
-
490
- # ---------------------------
491
- # ADMIN — Upload e gestão
492
- # ---------------------------
493
- if perfil == "admin":
494
- st.subheader("🛠️ Administração do Repositório")
495
- with st.form("form_upload_repo"):
496
- files = st.file_uploader(
497
- "Selecione arquivos (Excel: .xlsx/.xls, PDF: .pdf)",
498
- type=["xlsx", "xls", "pdf"],
499
- accept_multiple_files=True
500
- )
501
- default_is_public = st.checkbox("Disponibilizar como público", value=True)
502
- tags = st.text_input("Tags (opcional, separadas por vírgula) ex.: 'contas, janeiro, portaria'")
503
- enviar = st.form_submit_button("📤 Enviar para repositório")
504
-
505
- if enviar and files:
506
- ok_count = 0
507
- for f in files:
508
- try:
509
- storage_path, size_kb, mime = _save_file_to_repo(f, f.name)
510
- registro = RepoArquivo(
511
- ambiente=env,
512
- storage_path=storage_path,
513
- original_name=f.name,
514
- title=os.path.splitext(f.name)[0],
515
- description=None,
516
- tags=(tags or "").strip() or None,
517
- mime=mime,
518
- size_kb=size_kb,
519
- uploaded_by=usuario,
520
- uploaded_at=datetime.now(),
521
- is_public=default_is_public
522
- )
523
- db.add(registro)
524
- db.commit()
525
- ok_count += 1
526
- except Exception as e:
527
- db.rollback()
528
- st.error(f"Erro ao salvar '{f.name}': {e}")
529
- if ok_count > 0:
530
- st.success(f"✅ {ok_count} arquivo(s) enviado(s) para o repositório.")
531
- st.rerun()
532
-
533
- with st.expander("📋 Arquivos do repositório (Admin)", expanded=True):
534
- col_s1, col_s2, col_s3 = st.columns([1.8, 1, 1])
535
- termo = col_s1.text_input("Buscar por título/nome/tags:")
536
- somente_publicos = col_s2.selectbox("Visibilidade", ["todos", "públicos", "privados"], index=0)
537
- tipo_arquivo = col_s3.selectbox("Tipo", ["todos", "pdf", "excel"], index=0)
538
-
539
- q = db.query(RepoArquivo).filter(RepoArquivo.ambiente == env)
540
- if termo.strip():
541
- like = f"%{termo.strip()}%"
542
- q = q.filter(
543
- (RepoArquivo.title.ilike(like)) |
544
- (RepoArquivo.original_name.ilike(like)) |
545
- (RepoArquivo.tags.ilike(like))
546
- )
547
- if somente_publicos != "todos":
548
- q = q.filter(RepoArquivo.is_public == (somente_publicos == "públicos"))
549
- if tipo_arquivo == "pdf":
550
- q = q.filter((RepoArquivo.mime.ilike("%pdf%")) | (RepoArquivo.original_name.ilike("%.pdf")))
551
- elif tipo_arquivo == "excel":
552
- q = q.filter(
553
- (RepoArquivo.original_name.ilike("%.xlsx")) |
554
- (RepoArquivo.original_name.ilike("%.xls")) |
555
- (RepoArquivo.mime.ilike("%spreadsheet%")) |
556
- (RepoArquivo.mime.ilike("%excel%"))
557
- )
558
-
559
- itens = q.order_by(RepoArquivo.uploaded_at.desc()).all()
560
- if not itens:
561
- st.info("Nenhum arquivo encontrado para os filtros aplicados.")
562
- else:
563
- for item in itens:
564
- with st.expander(f"📄 {item.title or item.original_name} — {item.mime} — {item.size_kb:.1f} KB"):
565
- st.caption(
566
- f"ID: {item.id} | Enviado por: {item.uploaded_by} | Em: {item.uploaded_at.strftime('%d/%m/%Y %H:%M')} | Visível: {'Sim' if item.is_public else 'Não'}"
567
- )
568
- col_e1, col_e2 = st.columns([1.2, 1])
569
- novo_titulo = col_e1.text_input("Título", value=item.title or "", key=f"t_{item.id}")
570
- nova_desc = col_e1.text_area("Descrição", value=item.description or "", key=f"d_{item.id}", height=80)
571
- novas_tags = col_e1.text_input("Tags (vírgulas)", value=item.tags or "", key=f"g_{item.id}")
572
- novo_publico = col_e2.checkbox("Público?", value=item.is_public, key=f"p_{item.id}")
573
- btn_save = col_e2.button("💾 Salvar alterações", key=f"save_{item.id}")
574
- btn_del = col_e2.button("🗑️ Excluir", key=f"del_{item.id}")
575
-
576
- st.markdown("---")
577
- if "pdf" in (item.mime or "").lower() or item.original_name.lower().endswith(".pdf"):
578
- _render_pdf_preview(item.storage_path, height=500, key_prefix=f"adm_{item.id}", default="PDF.js (alternativo)")
579
- elif item.original_name.lower().endswith((".xlsx", ".xls")):
580
- _preview_excel(item.storage_path)
581
- else:
582
- st.info("Preview não disponível para este tipo de arquivo.")
583
-
584
- # Download
585
- try:
586
- with open(item.storage_path, "rb") as f:
587
- st.download_button(
588
- "⬇️ Baixar arquivo",
589
- data=f.read(),
590
- file_name=item.original_name,
591
- mime=item.mime,
592
- key=f"dl_admin_{item.id}"
593
- )
594
- except Exception as e:
595
- st.warning(f"Falha ao preparar download: {e}")
596
-
597
- if btn_save:
598
- try:
599
- item.title = (novo_titulo or "").strip() or None
600
- item.description = (nova_desc or "").strip() or None
601
- item.tags = (novas_tags or "").strip() or None
602
- item.is_public = bool(novo_publico)
603
- db.add(item)
604
- db.commit()
605
- st.success("Alterações salvas.")
606
- st.rerun()
607
- except Exception as e:
608
- db.rollback()
609
- st.error(f"Erro ao salvar: {e}")
610
-
611
- if btn_del:
612
- try:
613
- try:
614
- if os.path.exists(item.storage_path):
615
- os.remove(item.storage_path)
616
- except Exception:
617
- pass
618
- db.delete(item)
619
- db.commit()
620
- st.info("Arquivo removido do repositório.")
621
- st.rerun()
622
- except Exception as e:
623
- db.rollback()
624
- st.error(f"Erro ao excluir: {e}")
625
-
626
- # ---------------------------
627
- # USUÁRIO — Consulta/Download
628
- # ---------------------------
629
- st.subheader("🔎 Consulta e Download")
630
- with st.form("form_busca_repo_user"):
631
- col_u1, col_u2, col_u3 = st.columns([1.8, 1, 1])
632
- termo_u = col_u1.text_input("Buscar por título/nome/tags:")
633
- tipo_u = col_u2.selectbox("Tipo", ["todos", "pdf", "excel"], index=0)
634
- vis_user = "públicos" if perfil != "admin" else col_u3.selectbox("Visibilidade", ["públicos", "todos"], index=0)
635
- buscar = st.form_submit_button("🔍 Buscar")
636
-
637
- q_user = db.query(RepoArquivo).filter(RepoArquivo.ambiente == env)
638
- if perfil != "admin" or vis_user == "públicos":
639
- q_user = q_user.filter(RepoArquivo.is_public == True)
640
- if termo_u.strip():
641
- like_u = f"%{termo_u.strip()}%"
642
- q_user = q_user.filter(
643
- (RepoArquivo.title.ilike(like_u)) |
644
- (RepoArquivo.original_name.ilike(like_u)) |
645
- (RepoArquivo.tags.ilike(like_u))
646
- )
647
- if tipo_u == "pdf":
648
- q_user = q_user.filter((RepoArquivo.mime.ilike("%pdf%")) | (RepoArquivo.original_name.ilike("%.pdf")))
649
- elif tipo_u == "excel":
650
- q_user = q_user.filter(
651
- (RepoArquivo.original_name.ilike("%.xlsx")) |
652
- (RepoArquivo.original_name.ilike("%.xls")) |
653
- (RepoArquivo.mime.ilike("%spreadsheet%")) |
654
- (RepoArquivo.mime.ilike("%excel%"))
655
- )
656
-
657
- itens_user = q_user.order_by(RepoArquivo.uploaded_at.desc()).all()
658
- if not itens_user:
659
- st.info("Nenhum arquivo encontrado.")
660
- else:
661
- for item in itens_user:
662
- with st.expander(f"📄 {item.title or item.original_name} — {item.mime} — {item.size_kb:.1f} KB"):
663
- st.caption(f"ID: {item.id} | Enviado por: {item.uploaded_by} | Em: {item.uploaded_at.strftime('%d/%m/%Y %H:%M')}")
664
- if "pdf" in (item.mime or "").lower() or item.original_name.lower().endswith(".pdf"):
665
- _render_pdf_preview(item.storage_path, height=450, key_prefix=f"user_{item.id}", default="PDF.js (alternativo)")
666
- elif item.original_name.lower().endswith((".xlsx", ".xls")):
667
- _preview_excel(item.storage_path)
668
- else:
669
- st.info("Preview não disponível para este tipo.")
670
-
671
- # Download
672
- try:
673
- with open(item.storage_path, "rb") as f:
674
- st.download_button(
675
- "⬇️ Baixar arquivo",
676
- data=f.read(),
677
- file_name=item.original_name,
678
- mime=item.mime,
679
- key=f"dl_user_{item.id}"
680
- )
681
- except Exception as e:
682
- st.warning(f"Falha ao preparar download: {e}")
683
-
684
- # Rodapé
685
- st.caption(f"Ambiente: **{env}** Pasta: `{_repo_root()}` Perfil: **{perfil}**")
686
-
687
- # Auditoria (opcional)
688
- try:
689
- from utils_auditoria import registrar_log
690
- registrar_log(
691
- usuario=usuario,
692
- acao=f"Acesso ao Repositório Load (perfil={perfil})",
693
- tabela="repo_arquivo",
694
- registro_id=None
695
- )
696
- except Exception:
697
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ repositorio_load.py
4
+ Repositório de arquivos (PDF/Excel) por ambiente (prod/test/treinamento), com:
5
+ - Upload (admin), listagem, edição de metadados e exclusão
6
+ - Preview resiliente de PDF (blob em nova aba + inline opcional) e Excel
7
+ - Diretório raiz configurável por REPO_DIR (Secrets); fallback data_repo/<ambiente>
8
+
9
+ Compatível com Linux (Hugging Face Spaces) e local/Windows.
10
+ """
11
+ import os
12
+ import base64
13
+ import hashlib
14
+ import mimetypes
15
+ from datetime import datetime
16
+
17
+ import streamlit as st
18
+ import pandas as pd
19
+
20
+ from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float, Text
21
+ from banco import engine, Base, SessionLocal
22
+
23
+ from string import Template # evita conflitos com {} em HTML/JS
24
+
25
+ # ==============================
26
+ # Modelo de dados — repositório
27
+ # ==============================
28
+ class RepoArquivo(Base):
29
+ __tablename__ = "repo_arquivo"
30
+
31
+ id = Column(Integer, primary_key=True, autoincrement=True)
32
+ ambiente = Column(String(16), nullable=False) # prod/test/treinamento
33
+ storage_path = Column(String(512), nullable=False) # caminho no disco
34
+ original_name = Column(String(256), nullable=False)
35
+ title = Column(String(256), nullable=True)
36
+ description = Column(Text, nullable=True)
37
+ tags = Column(String(256), nullable=True) # separado por vírgulas
38
+ mime = Column(String(128), nullable=False)
39
+ size_kb = Column(Float, nullable=True)
40
+ uploaded_by = Column(String(128), nullable=False)
41
+ uploaded_at = Column(DateTime, nullable=False, default=datetime.now)
42
+ is_public = Column(Boolean, nullable=False, default=True)
43
+
44
+ # Garante a tabela
45
+ Base.metadata.create_all(bind=engine)
46
+
47
+ # ==============================
48
+ # Helpers — ambiente/paths/segurança
49
+ # ==============================
50
+ def _current_env_label() -> str:
51
+ try:
52
+ from db_router import current_db_choice
53
+ env = current_db_choice()
54
+ return env or "prod"
55
+ except Exception:
56
+ return "prod"
57
+
58
+ def _repo_root() -> str:
59
+ """
60
+ Raiz do repositório para o ambiente atual.
61
+ Permite override via Secret/ENV REPO_DIR; caso contrário: ./data_repo/<ambiente>
62
+ """
63
+ env = _current_env_label()
64
+ base = os.getenv("REPO_DIR") # ex.: /app/data_repo
65
+ if not base:
66
+ base = os.path.join(os.path.abspath(os.getcwd()), "data_repo")
67
+ root = os.path.join(os.path.abspath(base), env)
68
+ os.makedirs(root, exist_ok=True)
69
+ return root
70
+
71
+ def _hash_bytes(b: bytes) -> str:
72
+ return hashlib.sha256(b).hexdigest()
73
+
74
+ # MIME por extensão (fallback)
75
+ _EXT_MIME = {
76
+ ".pdf": "application/pdf",
77
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
78
+ ".xls": "application/vnd.ms-excel",
79
+ }
80
+ def _detect_mime_by_name(original_name: str) -> str:
81
+ guessed = mimetypes.guess_type(original_name)[0]
82
+ if guessed:
83
+ return guessed
84
+ ext = os.path.splitext(original_name)[1].lower()
85
+ return _EXT_MIME.get(ext, "application/octet-stream")
86
+
87
+ def _save_file_to_repo(file_obj, original_name: str) -> tuple[str, float, str]:
88
+ """
89
+ Salva o arquivo no repositório e retorna (storage_path, size_kb, mime).
90
+ Usa hash do conteúdo para nome (evita colisão por nome original).
91
+ """
92
+ content = file_obj.read()
93
+ size_kb = round(len(content) / 1024.0, 2)
94
+ mime = _detect_mime_by_name(original_name)
95
+ root = _repo_root()
96
+ ext = os.path.splitext(original_name)[1].lower()
97
+ uid = _hash_bytes(content)[:16]
98
+ storage_name = f"{uid}{ext}"
99
+ storage_path = os.path.join(root, storage_name)
100
+ with open(storage_path, "wb") as f:
101
+ f.write(content)
102
+ return storage_path, size_kb, mime
103
+
104
+ # ==============================
105
+ # Preview — PDF e Excel
106
+ # ==============================
107
+ def _pdf_b64_cached(storage_path: str) -> str:
108
+ """Cache leve do PDF em base64 na sessão (chaveada por path)."""
109
+ key = f"__repo_pdf_b64__::{storage_path}"
110
+ if key in st.session_state:
111
+ return st.session_state[key]
112
+ with open(storage_path, "rb") as f:
113
+ b64 = base64.b64encode(f.read()).decode("utf-8")
114
+ st.session_state[key] = b64
115
+ return b64
116
+
117
+ def _is_too_big_for_inline(storage_path: str, max_mb: int = 15) -> bool:
118
+ """Evita tentar embed de arquivos muito grandes (limite ajustável)."""
119
+ try:
120
+ return os.path.getsize(storage_path) > max_mb * 1024 * 1024
121
+ except Exception:
122
+ return False
123
+
124
+ def _button_open_new_tab_blob(storage_path: str, key_prefix: str, label: str = "🧭 Abrir em nova aba"):
125
+ """
126
+ Botão que abre o PDF em nova aba via blob: (sem iframe/object).
127
+ Mais confiável em navegadores que bloqueiam iframes.
128
+ """
129
+ try:
130
+ b64 = _pdf_b64_cached(storage_path)
131
+ html_tpl = Template(r"""
132
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
133
+ <button id="${K}_btn" style="padding:6px 10px;cursor:pointer;">${LABEL}</button>
134
+ <span id="${K}_msg" style="font:12px system-ui;color:#475569;"></span>
135
+ </div>
136
+ <script>
137
+ (function(){
138
+ const btn = document.getElementById("${K}_btn");
139
+ const msg = document.getElementById("${K}_msg");
140
+ try {
141
+ const b64 = "${B64}";
142
+ const raw = atob(b64);
143
+ const len = raw.length;
144
+ const bytes = new Uint8Array(len);
145
+ for (let i=0;i<len;i++) bytes[i] = raw.charCodeAt(i);
146
+ const blob = new Blob([bytes], { type: "application/pdf" });
147
+
148
+ btn.addEventListener("click", function(){
149
+ try {
150
+ const url = URL.createObjectURL(blob);
151
+ const w = window.open(url, "_blank");
152
+ if (!w) {
153
+ msg.textContent = "Bloqueado por pop-up. Permita pop-ups para este site e clique novamente.";
154
+ msg.style.color = "#b45309";
155
+ } else {
156
+ msg.textContent = "Aberto em nova aba.";
157
+ msg.style.color = "#0f766e";
158
+ setTimeout(() => URL.revokeObjectURL(url), 30000);
159
+ }
160
+ } catch(e) {
161
+ msg.textContent = "Falha ao abrir em nova aba: " + e;
162
+ msg.style.color = "#ef4444";
163
+ }
164
+ });
165
+ } catch(e) {
166
+ msg.textContent = "Falha ao preparar visualização: " + e;
167
+ msg.style.color = "#ef4444";
168
+ }
169
+ })();
170
+ </script>
171
+ """)
172
+ html = html_tpl.substitute(K=key_prefix, B64=b64, LABEL=label)
173
+ st.components.v1.html(html, height=40)
174
+ except Exception as e:
175
+ st.warning(f"Não foi possível preparar abertura em nova aba: {e}")
176
+
177
+ def _embed_pdf_blob(storage_path: str, height: int = 650, mode: str = "iframe"):
178
+ """
179
+ Exibe inline via blob: em iframe ou object/embed. Se não carregar em 3,5s,
180
+ mostra status amigável orientando usar "Abrir em nova aba".
181
+ """
182
+ try:
183
+ b64 = _pdf_b64_cached(storage_path)
184
+ viewer_iframe = '<iframe id="pdfifr" src="" title="Pré-visualização PDF"></iframe>'
185
+ viewer_object = '<object id="pdfobj" type="application/pdf" data="" width="100%" height="${H}px"><embed id="pdfemb" type="application/pdf" src="" width="100%" height="${H}px" /></object>'
186
+ viewer = viewer_object if mode == "object" else viewer_iframe
187
+ if mode == "object":
188
+ viewer = Template(viewer).substitute(H=int(height))
189
+
190
+ inject = (
191
+ 'var o=document.getElementById("pdfobj"); var e=document.getElementById("pdfemb"); '
192
+ 'if(o){o.setAttribute("data", url); viewerEl=o;} else if(e){e.setAttribute("src", url); viewerEl=e;}'
193
+ if mode == "object"
194
+ else 'var f=document.getElementById("pdfifr"); if(f){f.setAttribute("src", url); viewerEl=f;}'
195
+ )
196
+
197
+ html_tpl = Template(r"""
198
+ <!DOCTYPE html>
199
+ <html>
200
+ <head>
201
+ <meta charset="utf-8" />
202
+ <style>
203
+ body { margin:0; padding:0; }
204
+ .wrap { width:100%; height:${H}px; }
205
+ iframe, object, embed { width:100%; height:${H}px; border:0; }
206
+ #err { color:#ef4444; font:12px system-ui; padding:6px; }
207
+ .toolbar { padding:6px; font:12px system-ui; background:#f1f5f9; color:#0f172a; display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
208
+ .toolbar span { padding: 0 6px; }
209
+ .status-ok { color:#0f766e; }
210
+ .status-warn { color:#b45309; }
211
+ </style>
212
+ </head>
213
+ <body>
214
+ <div class="toolbar">
215
+ <span id="status" class="status-warn">carregando visualizador ${MODE}…</span>
216
+ </div>
217
+ <div class="wrap">
218
+ ${VIEWER}
219
+ </div>
220
+ <div id="err"></div>
221
+ <script>
222
+ (function() {
223
+ const err = (m) => { document.getElementById('err').textContent = String(m||'Falha ao exibir o PDF.'); };
224
+ const status = document.getElementById('status');
225
+ try {
226
+ const b64 = "${B64}";
227
+ const raw = atob(b64);
228
+ const len = raw.length;
229
+ const bytes = new Uint8Array(len);
230
+ for (let i=0;i<len;i++) bytes[i] = raw.charCodeAt(i);
231
+ const blob = new Blob([bytes], { type: "application/pdf" });
232
+ const url = URL.createObjectURL(blob);
233
+ status.textContent = "visualizador: ${MODE} (blob)";
234
+ status.className = "status-warn";
235
+
236
+ let loaded = false;
237
+ let viewerEl = null;
238
+
239
+ ${INJECT}
240
+
241
+ try {
242
+ if (viewerEl && viewerEl.addEventListener) {
243
+ viewerEl.addEventListener("load", function(){
244
+ loaded = true;
245
+ status.textContent = "visualizador carregado";
246
+ status.className = "status-ok";
247
+ });
248
+ }
249
+ } catch(e){}
250
+
251
+ setTimeout(function(){
252
+ if (!loaded) {
253
+ status.textContent = "preview pode estar bloqueado no navegador (iframe/object). Use 'Abrir em nova aba' no app.";
254
+ status.className = "status-warn";
255
+ }
256
+ }, 3500);
257
+ } catch(e) {
258
+ err(e);
259
+ }
260
+ })();
261
+ </script>
262
+ </body>
263
+ </html>
264
+ """)
265
+ html = html_tpl.substitute(
266
+ H=int(height),
267
+ VIEWER=viewer,
268
+ B64=b64,
269
+ MODE=mode,
270
+ INJECT=inject
271
+ )
272
+ st.components.v1.html(html, height=height + 46, scrolling=False)
273
+ except Exception as e:
274
+ st.warning(f"Não foi possível exibir o PDF (blob/{mode}): {e}")
275
+
276
+ def _embed_pdf_pdfjs(storage_path: str, height: int = 650, theme: str = "dark"):
277
+ """
278
+ Visualização usando PDF.js (CDN) — com navegação e zoom.
279
+ """
280
+ try:
281
+ b64 = _pdf_b64_cached(storage_path)
282
+ bg = "#0b1220" if theme == "dark" else "#f8fafc"
283
+ fg = "#e2e8f0" if theme == "dark" else "#0f172a"
284
+ btn = "#1e293b" if theme == "dark" else "#e2e8f0"
285
+
286
+ html_tpl = Template(r"""
287
+ <!DOCTYPE html>
288
+ <html>
289
+ <head>
290
+ <meta charset="utf-8" />
291
+ <style>
292
+ body {
293
+ margin: 0; padding: 8px;
294
+ background: ${BG}; color: ${FG};
295
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
296
+ }
297
+ .toolbar {
298
+ display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap;
299
+ }
300
+ .toolbar button {
301
+ background: ${BTN}; color: ${FG}; border: 1px solid rgba(0,0,0,.1); border-radius: 6px;
302
+ padding: 6px 10px; cursor: pointer;
303
+ }
304
+ .toolbar span { padding: 0 6px; }
305
+ .viewer {
306
+ width: 100%; height: ${VH}px; overflow: auto;
307
+ background: white; border-radius: 6px; box-shadow: 0 1px 4px rgba(0,0,0,.15);
308
+ display: flex; justify-content: center; align-items: flex-start; padding: 12px;
309
+ }
310
+ #pdf-canvas { background: #fff; }
311
+ #err { color: #ef4444; font-size: 12px; margin-top: 6px; }
312
+ </style>
313
+ </head>
314
+ <body>
315
+ <div class="toolbar">
316
+ <button id="prev">⬅️ Página anterior</button>
317
+ <button id="next">➡️ Próxima página</button>
318
+ <span id="page_info">Página 1 / ?</span>
319
+ <button id="zoom_out">➖ Zoom</button>
320
+ <button id="zoom_in">➕ Zoom</button>
321
+ <button id="fitw">↔️ Ajustar largura</button>
322
+ <button id="fitp">↕️ Ajustar página</button>
323
+ <button id="open_new">🧭 Abrir em nova aba</button>
324
+ </div>
325
+ <div class="viewer">
326
+ <canvas id="pdf-canvas"></canvas>
327
+ </div>
328
+ <div id="err"></div>
329
+
330
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
331
+ <script>
332
+ (function() {
333
+ const errBox = document.getElementById('err');
334
+ function showErr(msg) { errBox.textContent = String(msg || 'Falha ao renderizar o PDF.'); }
335
+ window.onerror = function(msg) { showErr(msg); };
336
+
337
+ try {
338
+ const b64 = "${B64}";
339
+ const raw = atob(b64);
340
+ const len = raw.length;
341
+ const bytes = new Uint8Array(len);
342
+ for (let i = 0; i < len; i++) bytes[i] = raw.charCodeAt(i);
343
+
344
+ const canvas = document.getElementById('pdf-canvas');
345
+ const ctx = canvas.getContext('2d');
346
+ const pageInfo = document.getElementById('page_info');
347
+ const prevBtn = document.getElementById('prev');
348
+ const nextBtn = document.getElementById('next');
349
+ const zoomIn = document.getElementById('zoom_in');
350
+ const zoomOut = document.getElementById('zoom_out');
351
+ const fitW = document.getElementById('fitw');
352
+ const fitP = document.getElementById('fitp');
353
+ const openNew = document.getElementById('open_new');
354
+
355
+ pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
356
+
357
+ let pdfDoc = null;
358
+ let currentPage = 1;
359
+ let scale = 1.2;
360
+ let fitMode = "width";
361
+
362
+ function renderPage(num) {
363
+ pdfDoc.getPage(num).then(function(page) {
364
+ const viewport = page.getViewport({ scale: scale });
365
+ canvas.width = viewport.width;
366
+ canvas.height = viewport.height;
367
+ const renderCtx = { canvasContext: ctx, viewport: viewport };
368
+ page.render(renderCtx).promise.then(function() {
369
+ pageInfo.textContent = "Página " + num + " / " + pdfDoc.numPages;
370
+ });
371
+ }).catch(showErr);
372
+ }
373
+
374
+ function fitWidth() {
375
+ fitMode = "width";
376
+ pdfDoc.getPage(currentPage).then(function(page) {
377
+ const container = canvas.parentElement;
378
+ const margins = 24;
379
+ const w = container.clientWidth - margins;
380
+ const viewport = page.getViewport({ scale: 1 });
381
+ scale = Math.max(0.2, w / viewport.width);
382
+ renderPage(currentPage);
383
+ }).catch(showErr);
384
+ }
385
+
386
+ function fitPage() {
387
+ fitMode = "page";
388
+ pdfDoc.getPage(currentPage).then(function(page) {
389
+ const container = canvas.parentElement;
390
+ const margins = 24;
391
+ const w = container.clientWidth - margins;
392
+ const h = container.clientHeight - margins;
393
+ const viewport = page.getViewport({ scale: 1 });
394
+ const scaleW = w / viewport.width;
395
+ const scaleH = h / viewport.height;
396
+ scale = Math.max(0.2, Math.min(scaleW, scaleH));
397
+ renderPage(currentPage);
398
+ }).catch(showErr);
399
+ }
400
+
401
+ pdfjsLib.getDocument({ data: bytes }).promise.then(function(pdf) {
402
+ pdfDoc = pdf;
403
+ if (fitMode === "width") fitWidth(); else fitPage();
404
+ }).catch(showErr);
405
+
406
+ prevBtn.addEventListener('click', function() {
407
+ if (!pdfDoc) return;
408
+ if (currentPage <= 1) return;
409
+ currentPage--;
410
+ if (fitMode === "width") fitWidth(); else fitPage();
411
+ });
412
+ nextBtn.addEventListener('click', function() {
413
+ if (!pdfDoc) return;
414
+ if (currentPage >= pdfDoc.numPages) return;
415
+ currentPage++;
416
+ if (fitMode === "width") fitWidth(); else fitPage();
417
+ });
418
+ zoomIn.addEventListener('click', function() { scale = Math.min(scale + 0.1, 4.0); renderPage(currentPage); });
419
+ zoomOut.addEventListener('click', function() { scale = Math.max(scale - 0.1, 0.2); renderPage(currentPage); });
420
+ fitW.addEventListener('click', fitWidth);
421
+ fitP.addEventListener('click', fitPage);
422
+ openNew.addEventListener('click', function() {
423
+ try {
424
+ const blob = new Blob([bytes], { type: 'application/pdf' });
425
+ const url = URL.createObjectURL(blob);
426
+ window.open(url, '_blank');
427
+ setTimeout(() => URL.revokeObjectURL(url), 30000);
428
+ } catch (e) { showErr(e); }
429
+ });
430
+
431
+ window.addEventListener('resize', function() {
432
+ if (!pdfDoc) return;
433
+ if (fitMode === "width") fitWidth(); else fitPage();
434
+ });
435
+ } catch (e) { showErr(e); }
436
+ })();
437
+ </script>
438
+ </body>
439
+ </html>
440
+ """)
441
+ html = html_tpl.substitute(
442
+ B64=b64,
443
+ BG=bg,
444
+ FG=fg,
445
+ BTN=btn,
446
+ VH=max(100, int(height - 54))
447
+ )
448
+ st.components.v1.html(html, height=height + 16, scrolling=False)
449
+ except Exception as e:
450
+ st.warning(f"Não foi possível exibir o PDF (PDF.js): {e}")
451
+
452
+ def _preview_excel(storage_path: str):
453
+ """Exibe primeiras linhas de um Excel (até ~200 linhas)."""
454
+ try:
455
+ path = storage_path.lower()
456
+ if path.endswith(".xlsx"):
457
+ df = pd.read_excel(storage_path, engine="openpyxl")
458
+ elif path.endswith(".xls"):
459
+ df = pd.read_excel(storage_path, engine="xlrd")
460
+ else:
461
+ st.info("Arquivo não reconhecido como Excel.")
462
+ return
463
+ st.dataframe(df.head(200), use_container_width=True)
464
+ except Exception as e:
465
+ st.warning(f"Não foi possível ler o Excel: {e}")
466
+
467
+ def _render_pdf_preview(storage_path: str, height: int, key_prefix: str, default: str = "PDF.js (alternativo)"):
468
+ """
469
+ Viewer por arquivo:
470
+ - Botão "Abrir em nova aba" (SEM embed) sempre visível.
471
+ - Inline (opcional): PDF.js, Iframe (Blob) ou OBJECT/EMBED (Blob).
472
+ - Arquivo grande: recomenda abrir em nova aba.
473
+ """
474
+ # 1) Sempre oferecer "Abrir em nova aba" (mais confiável)
475
+ _button_open_new_tab_blob(storage_path, key_prefix=f"{key_prefix}_open", label="🧭 Abrir em nova aba")
476
+
477
+ # 2) Bypass para PDFs muito grandes
478
+ if _is_too_big_for_inline(storage_path, max_mb=15):
479
+ st.info("📄 PDF grande recomendado abrir em nova aba (botão acima).")
480
+ return
481
+
482
+ # 3) Pré-visualização inline (opcional)
483
+ with st.expander("🔬 Pré‑visualização inline (opcional)", expanded=False):
484
+ viewer_opt = st.radio(
485
+ "Visualizador de PDF:",
486
+ options=["PDF.js (alternativo)", "Iframe (Blob)", "OBJECT/EMBED (Blob compat.)"],
487
+ index={"PDF.js (alternativo)": 0, "Iframe (Blob)": 1, "OBJECT/EMBED (Blob compat.)": 2}.get(default, 0),
488
+ key=f"{key_prefix}_viewer"
489
+ )
490
+ if viewer_opt == "PDF.js (alternativo)":
491
+ _embed_pdf_pdfjs(storage_path, height=height, theme="dark")
492
+ elif viewer_opt == "Iframe (Blob)":
493
+ _embed_pdf_blob(storage_path, height=height, mode="iframe")
494
+ else:
495
+ _embed_pdf_blob(storage_path, height=height, mode="object")
496
+
497
+ # ==============================
498
+ # UI — módulo
499
+ # ==============================
500
+ def main():
501
+ st.title("📦 Repositório Load")
502
+ st.caption("Admin importa arquivos (Excel/PDF). Usuários consultam online e baixam.")
503
+
504
+ perfil = (st.session_state.get("perfil") or "usuario").strip().lower()
505
+ usuario = st.session_state.get("usuario") or "anon"
506
+ env = _current_env_label()
507
+ db = SessionLocal()
508
+
509
+ try:
510
+ # ---------------------------
511
+ # ADMIN — Upload e gestão
512
+ # ---------------------------
513
+ if perfil == "admin":
514
+ st.subheader("🛠️ Administração do Repositório")
515
+ with st.form("form_upload_repo"):
516
+ files = st.file_uploader(
517
+ "Selecione arquivos (Excel: .xlsx/.xls, PDF: .pdf)",
518
+ type=["xlsx", "xls", "pdf"],
519
+ accept_multiple_files=True
520
+ )
521
+ default_is_public = st.checkbox("Disponibilizar como público", value=True)
522
+ tags = st.text_input("Tags (opcional, separadas por vírgula) ex.: 'contas, janeiro, portaria'")
523
+ enviar = st.form_submit_button("📤 Enviar para repositório")
524
+
525
+ if enviar and files:
526
+ ok_count = 0
527
+ for f in files:
528
+ try:
529
+ storage_path, size_kb, mime = _save_file_to_repo(f, f.name)
530
+ registro = RepoArquivo(
531
+ ambiente=env,
532
+ storage_path=storage_path,
533
+ original_name=f.name,
534
+ title=os.path.splitext(f.name)[0],
535
+ description=None,
536
+ tags=(tags or "").strip() or None,
537
+ mime=mime,
538
+ size_kb=size_kb,
539
+ uploaded_by=usuario,
540
+ uploaded_at=datetime.now(),
541
+ is_public=default_is_public
542
+ )
543
+ db.add(registro)
544
+ db.commit()
545
+ ok_count += 1
546
+ except Exception as e:
547
+ db.rollback()
548
+ st.error(f"Erro ao salvar '{f.name}': {e}")
549
+ if ok_count > 0:
550
+ st.success(f"✅ {ok_count} arquivo(s) enviado(s) para o repositório.")
551
+ st.rerun()
552
+
553
+ with st.expander("📋 Arquivos do repositório (Admin)", expanded=True):
554
+ col_s1, col_s2, col_s3 = st.columns([1.8, 1, 1])
555
+ termo = col_s1.text_input("Buscar por título/nome/tags:")
556
+ somente_publicos = col_s2.selectbox("Visibilidade", ["todos", "públicos", "privados"], index=0)
557
+ tipo_arquivo = col_s3.selectbox("Tipo", ["todos", "pdf", "excel"], index=0)
558
+
559
+ q = db.query(RepoArquivo).filter(RepoArquivo.ambiente == env)
560
+ if termo.strip():
561
+ like = f"%{termo.strip()}%"
562
+ q = q.filter(
563
+ (RepoArquivo.title.ilike(like)) |
564
+ (RepoArquivo.original_name.ilike(like)) |
565
+ (RepoArquivo.tags.ilike(like))
566
+ )
567
+ if somente_publicos != "todos":
568
+ q = q.filter(RepoArquivo.is_public == (somente_publicos == "públicos"))
569
+ if tipo_arquivo == "pdf":
570
+ q = q.filter((RepoArquivo.mime.ilike("%pdf%")) | (RepoArquivo.original_name.ilike("%.pdf")))
571
+ elif tipo_arquivo == "excel":
572
+ q = q.filter(
573
+ (RepoArquivo.original_name.ilike("%.xlsx")) |
574
+ (RepoArquivo.original_name.ilike("%.xls")) |
575
+ (RepoArquivo.mime.ilike("%spreadsheet%")) |
576
+ (RepoArquivo.mime.ilike("%excel%"))
577
+ )
578
+
579
+ itens = q.order_by(RepoArquivo.uploaded_at.desc()).all()
580
+ if not itens:
581
+ st.info("Nenhum arquivo encontrado para os filtros aplicados.")
582
+ else:
583
+ for item in itens:
584
+ with st.expander(f"📄 {item.title or item.original_name} — {item.mime} — {item.size_kb:.1f} KB"):
585
+ st.caption(
586
+ f"ID: {item.id} | Enviado por: {item.uploaded_by} | Em: {item.uploaded_at.strftime('%d/%m/%Y %H:%M')} | Visível: {'Sim' if item.is_public else 'Não'}"
587
+ )
588
+ col_e1, col_e2 = st.columns([1.2, 1])
589
+ novo_titulo = col_e1.text_input("Título", value=item.title or "", key=f"t_{item.id}")
590
+ nova_desc = col_e1.text_area("Descrição", value=item.description or "", key=f"d_{item.id}", height=80)
591
+ novas_tags = col_e1.text_input("Tags (vírgulas)", value=item.tags or "", key=f"g_{item.id}")
592
+ novo_publico = col_e2.checkbox("Público?", value=item.is_public, key=f"p_{item.id}")
593
+ btn_save = col_e2.button("💾 Salvar alterações", key=f"save_{item.id}")
594
+ btn_del = col_e2.button("🗑️ Excluir", key=f"del_{item.id}")
595
+
596
+ st.markdown("---")
597
+ try:
598
+ if "pdf" in (item.mime or "").lower() or item.original_name.lower().endswith(".pdf"):
599
+ _render_pdf_preview(item.storage_path, height=500, key_prefix=f"adm_{item.id}", default="PDF.js (alternativo)")
600
+ elif item.original_name.lower().endswith((".xlsx", ".xls")):
601
+ _preview_excel(item.storage_path)
602
+ else:
603
+ st.info("Preview não disponível para este tipo de arquivo.")
604
+ except Exception as e:
605
+ st.warning(f"Preview indisponível: {e}")
606
+
607
+ # Download
608
+ try:
609
+ with open(item.storage_path, "rb") as f:
610
+ st.download_button(
611
+ "⬇️ Baixar arquivo",
612
+ data=f.read(),
613
+ file_name=item.original_name,
614
+ mime=item.mime,
615
+ key=f"dl_admin_{item.id}"
616
+ )
617
+ except Exception as e:
618
+ st.warning(f"Falha ao preparar download: {e}")
619
+
620
+ if btn_save:
621
+ try:
622
+ item.title = (novo_titulo or "").strip() or None
623
+ item.description = (nova_desc or "").strip() or None
624
+ item.tags = (novas_tags or "").strip() or None
625
+ item.is_public = bool(novo_publico)
626
+ db.add(item)
627
+ db.commit()
628
+ st.success("Alterações salvas.")
629
+ st.rerun()
630
+ except Exception as e:
631
+ db.rollback()
632
+ st.error(f"Erro ao salvar: {e}")
633
+
634
+ if btn_del:
635
+ try:
636
+ try:
637
+ if os.path.exists(item.storage_path):
638
+ os.remove(item.storage_path)
639
+ except Exception:
640
+ pass
641
+ db.delete(item)
642
+ db.commit()
643
+ st.info("Arquivo removido do repositório.")
644
+ st.rerun()
645
+ except Exception as e:
646
+ db.rollback()
647
+ st.error(f"Erro ao excluir: {e}")
648
+
649
+ # ---------------------------
650
+ # USUÁRIO — Consulta/Download
651
+ # ---------------------------
652
+ st.subheader("🔎 Consulta e Download")
653
+ with st.form("form_busca_repo_user"):
654
+ col_u1, col_u2, col_u3 = st.columns([1.8, 1, 1])
655
+ termo_u = col_u1.text_input("Buscar por título/nome/tags:")
656
+ tipo_u = col_u2.selectbox("Tipo", ["todos", "pdf", "excel"], index=0)
657
+ vis_user = "públicos" if perfil != "admin" else col_u3.selectbox("Visibilidade", ["públicos", "todos"], index=0)
658
+ buscar = st.form_submit_button("🔍 Buscar")
659
+
660
+ q_user = db.query(RepoArquivo).filter(RepoArquivo.ambiente == env)
661
+ if perfil != "admin" or vis_user == "públicos":
662
+ q_user = q_user.filter(RepoArquivo.is_public == True)
663
+ if termo_u.strip():
664
+ like_u = f"%{termo_u.strip()}%"
665
+ q_user = q_user.filter(
666
+ (RepoArquivo.title.ilike(like_u)) |
667
+ (RepoArquivo.original_name.ilike(like_u)) |
668
+ (RepoArquivo.tags.ilike(like_u))
669
+ )
670
+ if tipo_u == "pdf":
671
+ q_user = q_user.filter((RepoArquivo.mime.ilike("%pdf%")) | (RepoArquivo.original_name.ilike("%.pdf")))
672
+ elif tipo_u == "excel":
673
+ q_user = q_user.filter(
674
+ (RepoArquivo.original_name.ilike("%.xlsx")) |
675
+ (RepoArquivo.original_name.ilike("%.xls")) |
676
+ (RepoArquivo.mime.ilike("%spreadsheet%")) |
677
+ (RepoArquivo.mime.ilike("%excel%"))
678
+ )
679
+
680
+ itens_user = q_user.order_by(RepoArquivo.uploaded_at.desc()).all()
681
+ if not itens_user:
682
+ st.info("Nenhum arquivo encontrado.")
683
+ else:
684
+ for item in itens_user:
685
+ with st.expander(f"📄 {item.title or item.original_name} {item.mime} {item.size_kb:.1f} KB"):
686
+ st.caption(f"ID: {item.id} | Enviado por: {item.uploaded_by} | Em: {item.uploaded_at.strftime('%d/%m/%Y %H:%M')}")
687
+ try:
688
+ if "pdf" in (item.mime or "").lower() or item.original_name.lower().endswith(".pdf"):
689
+ _render_pdf_preview(item.storage_path, height=450, key_prefix=f"user_{item.id}", default="PDF.js (alternativo)")
690
+ elif item.original_name.lower().endswith((".xlsx", ".xls")):
691
+ _preview_excel(item.storage_path)
692
+ else:
693
+ st.info("Preview não disponível para este tipo.")
694
+ except Exception as e:
695
+ st.warning(f"Preview indisponível: {e}")
696
+
697
+ # Download
698
+ try:
699
+ with open(item.storage_path, "rb") as f:
700
+ st.download_button(
701
+ "⬇️ Baixar arquivo",
702
+ data=f.read(),
703
+ file_name=item.original_name,
704
+ mime=item.mime,
705
+ key=f"dl_user_{item.id}"
706
+ )
707
+ except Exception as e:
708
+ st.warning(f"Falha ao preparar download: {e}")
709
+
710
+ # Rodapé
711
+ st.caption(f"Ambiente: **{env}** • Pasta: `{_repo_root()}` • Perfil: **{perfil}**")
712
+
713
+ # Auditoria (opcional)
714
+ try:
715
+ from utils_auditoria import registrar_log as _reg
716
+ _reg(
717
+ usuario=usuario,
718
+ acao=f"Acesso ao Repositório Load (perfil={perfil})",
719
+ tabela="repo_arquivo",
720
+ registro_id=None
721
+ )
722
+ except Exception:
723
+ pass
724
+
725
+ finally:
726
+ try:
727
+ db.close()
728
+ except Exception:
729
+ pass