import asyncio import tempfile from pathlib import Path import streamlit as st from dotenv import load_dotenv from app.app_config import ( resolve_opencloud_access_token, resolve_opencloud_username, resolve_user_id, ) from app.auth.service import AuthService, clear_session from app.opencloud_token_store import OpenCloudTokenStoreError, add_token from app.pipeline.service import run_batch def main() -> None: load_dotenv() st.set_page_config(page_title="СФМ", page_icon=":lock:", layout="wide") st.title("СФМ") if not AuthService().run(): return _, center_col, _ = st.columns([1, 2, 1]) with center_col: _render_token_popover() st.caption( "Прикрепите XML-файлы и/или архивы с ними (можно несколько сразу, " "вперемешку) — отчёт, протокол и лог загрузятся в Out вашего " "личного пространства OpenCloud." ) uploaded_files = st.file_uploader( "XML-файлы и архивы", type=["xml", "zip", "tar", "gz", "tgz", "bz2", "xz"], accept_multiple_files=True, ) if st.button( "Запустить обработку xml в excel", type="primary", use_container_width=True, disabled=not uploaded_files, ): with tempfile.TemporaryDirectory() as tmp_dir: input_paths = _save_attachments(uploaded_files, Path(tmp_dir)) progress_bar = st.progress(0.0, text="Подготовка файлов...") status_placeholder = st.empty() def _on_progress(done: int, total: int, file_name: str) -> None: # Прогресс по файлам, а не один спиннер на весь батч — # иначе при большом количестве файлов (были жалобы на # батч в 230 штук) непонятно, идёт обработка или # приложение зависло, и пользователь закрывает вкладку # посреди обработки. fraction = done / total if total else 1.0 progress_bar.progress( fraction, text=f"Обработано {done} из {total}: {file_name}" ) status_placeholder.caption( f"Последний обработанный файл: {file_name}" ) launcher_name = resolve_opencloud_username() or resolve_user_id() batch_result = run_batch( input_paths=input_paths, launcher=launcher_name, opencloud_username=resolve_opencloud_username(), opencloud_access_token=resolve_opencloud_access_token(), progress_callback=_on_progress, ) progress_bar.progress(1.0, text="Готово") if batch_result.status == "success": st.success("Обработка завершена успешно.") elif batch_result.status == "partial": st.warning("Обработка завершена частично.") else: st.error("Обработка завершена с ошибками.") st.write( { "processed": batch_result.processed, "partial": batch_result.partial, "errors": batch_result.errors, "report_path": batch_result.report_path, "protocol_path": batch_result.protocol_path, "log_path": batch_result.log_path, } ) if batch_result.messages: st.caption("Последние сообщения обработки:") for message in batch_result.messages: st.write(f"- {message}") # Если обработка прошла, а загрузка в OpenCloud — нет, отдаём # результат напрямую, чтобы не терять долгую обработку. for file_name, payload in batch_result.fallback_files.items(): st.download_button( f"Скачать {file_name}", data=payload, file_name=file_name, use_container_width=True, key=f"fallback_{file_name}", ) if st.button("Выйти", type="secondary"): clear_session() st.rerun() def _save_attachments(uploaded_files, target_dir: Path) -> list[Path]: """Складывает вложения на диск, каждое в свой подкаталог. Подкаталог по индексу нужен потому, что имена вложений могут совпадать (например, два архива "Файлы.zip" из разных папок), и одно перезаписало бы другое. """ paths: list[Path] = [] for index, uploaded in enumerate(uploaded_files): holder = target_dir / str(index) holder.mkdir(parents=True, exist_ok=True) path = holder / uploaded.name path.write_bytes(uploaded.getvalue()) paths.append(path) return paths def _render_token_popover() -> None: """Самообслуживание: пользователь сам привязывает свой App Token OpenCloud к своей учётке (по preferred_username из Keycloak), без обращения к администратору с CLI-скриптом. """ with st.popover("Добавить токен"): username = resolve_opencloud_username() if not username: st.error("Не удалось определить вашу учётную запись.") return token = st.text_input( "App Token", type="password", help="Долгоживущий App Token из веб-интерфейса OpenCloud (6 слов через пробел).", key="opencloud_token_input", ) if st.button("Сохранить", key="opencloud_token_save"): if not token: st.error("Введите токен.") else: try: asyncio.run(add_token(username, token, added_by=username)) except OpenCloudTokenStoreError as exc: st.error(f"Не удалось сохранить токен: {exc}") else: st.success("Токен сохранён.") if __name__ == "__main__": main()