Switch OpenCloud uploads to per-user personal drives
Files no longer go into a shared test space with a guessed folder name. Instead, resolve each user's personal drive id via GET /graph/v1.0/me/drives (Basic Auth, same App Token as uploads) on every run, and upload into Out/год/месяц/день/номер_запуска inside that drive, creating the path via MKCOL since a fresh personal drive won't have it yet. Removes the now-obsolete OPENCLOUD_SPACE_ID / resolve_opencloud_user_folder and the shared-space smoke script. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ccb92fd85f
commit
54bf2abc52
@ -10,14 +10,6 @@ SFM_SMB_BASE_PATH = "//sgo-fc01-r06/sfm/ОТЧЕТ_7081У"
|
||||
SFM_DEFAULT_INPUT_DIR = r"//sgo-fc01-r06/sfm/ОТЧЕТ_7081У"
|
||||
ACCESS_SUPPORT_CONTACT = "администратору СФМ"
|
||||
|
||||
# TODO(BIQ-36593): подтвердить SPACE_ID для прода (сейчас — значение,
|
||||
# проверенное вручную на тестовом пространстве "AURORA"). По наблюдениям
|
||||
# ID может быть составным ("storageId$spaceId") — храним как есть, без
|
||||
# разбора, через env-переменную, чтобы не хардкодить прод-значение в коде.
|
||||
OPENCLOUD_SPACE_ID = os.getenv(
|
||||
"SFM_OPENCLOUD_SPACE_ID", "28c0cecf-1e20-4664-b165-5c9d5515e70f"
|
||||
)
|
||||
|
||||
EDIT_PATH_OPEN_KEY = "edit_path_open"
|
||||
EDIT_PATH_VALUE_KEY = "edit_path_value"
|
||||
# Runtime-only user paths storage. In bank contour we use tmpfs.
|
||||
@ -81,24 +73,3 @@ def resolve_opencloud_access_token() -> str | None:
|
||||
except Exception: # noqa: BLE001
|
||||
# Локальный тест без поднятой БД/переменных окружения — fallback.
|
||||
return os.getenv("SFM_OPENCLOUD_ACCESS_TOKEN") or None
|
||||
|
||||
|
||||
def resolve_opencloud_user_folder() -> str | None:
|
||||
"""Возвращает предполагаемое имя личной папки пользователя в OpenCloud.
|
||||
|
||||
ОТКРЫТЫЙ ВОПРОС: неизвестно, откуда в проде берётся имя папки вида
|
||||
"Райков М.С." — это предположение, что claim "name" из Keycloak уже
|
||||
приходит в этом формате (совпадает с тем, что руками использовалось в
|
||||
open_cloud_upload_fixed.py). Альтернатива — папки создаются заранее
|
||||
администратором по другому принципу (email/preferred_username), это
|
||||
нужно сверить в проде.
|
||||
|
||||
Для локального теста — fallback на переменную окружения
|
||||
SFM_OPENCLOUD_FOLDER из .env.
|
||||
"""
|
||||
profile = st.session_state.get("user_profile", {})
|
||||
if isinstance(profile, dict):
|
||||
value = profile.get("name")
|
||||
if value:
|
||||
return str(value)
|
||||
return os.getenv("SFM_OPENCLOUD_FOLDER") or None
|
||||
|
||||
@ -24,6 +24,63 @@ class OpenCloudUploadError(RuntimeError):
|
||||
"""Ошибка загрузки файла в OpenCloud по WebDAV."""
|
||||
|
||||
|
||||
def resolve_personal_drive_id(username: str, token: str) -> str:
|
||||
"""Находит ID личного пространства пользователя через Graph API.
|
||||
|
||||
Больше не грузим файлы в общее тестовое пространство по фиксированному
|
||||
SPACE_ID — у каждого заказчика есть личное пространство (driveType ==
|
||||
"personal" в GET /graph/v1.0/me/drives), и его ID нужно получать заново
|
||||
перед каждой загрузкой: пространство может быть пересоздано, а хранить
|
||||
потенциально устаревший ID рискованнее, чем сделать лишний HTTP-запрос.
|
||||
|
||||
Авторизация — та же Basic Auth (username + App Token), что и для самой
|
||||
загрузки по WebDAV, см. upload_local_file_to_opencloud.
|
||||
"""
|
||||
if not username:
|
||||
raise OpenCloudUploadError(
|
||||
"Не задан username для поиска личного пространства в OpenCloud."
|
||||
)
|
||||
if not token:
|
||||
raise OpenCloudUploadError(
|
||||
"Отсутствует App Token для поиска личного пространства в OpenCloud."
|
||||
)
|
||||
|
||||
url = f"https://{OPENCLOUD_DOMAIN}/graph/v1.0/me/drives"
|
||||
try:
|
||||
response = requests.get(
|
||||
url,
|
||||
auth=HTTPBasicAuth(username, token),
|
||||
timeout=OPENCLOUD_UPLOAD_TIMEOUT_SECONDS,
|
||||
)
|
||||
except requests.RequestException as exc:
|
||||
raise OpenCloudUploadError(
|
||||
f"Ошибка сети при поиске личного пространства OpenCloud: {exc}"
|
||||
) from exc
|
||||
|
||||
if response.status_code != 200:
|
||||
raise OpenCloudUploadError(
|
||||
"Не удалось получить список пространств OpenCloud: "
|
||||
f"HTTP {response.status_code}, body={response.text[:500]!r}"
|
||||
)
|
||||
|
||||
drives = response.json().get("value", [])
|
||||
personal_drives = [
|
||||
drive for drive in drives if drive.get("driveType") == "personal"
|
||||
]
|
||||
if not personal_drives:
|
||||
raise OpenCloudUploadError(
|
||||
f"Не найдено личное пространство (driveType='personal') для "
|
||||
f"пользователя '{username}' в OpenCloud."
|
||||
)
|
||||
|
||||
drive_id = personal_drives[0].get("id")
|
||||
if not drive_id:
|
||||
raise OpenCloudUploadError(
|
||||
"Личное пространство найдено в OpenCloud, но у него отсутствует id."
|
||||
)
|
||||
return str(drive_id)
|
||||
|
||||
|
||||
def build_opencloud_url(
|
||||
space_id: str, folder_segments: tuple[str, ...] | list[str], file_name: str
|
||||
) -> str:
|
||||
@ -54,6 +111,47 @@ def build_opencloud_url(
|
||||
return f"https://{OPENCLOUD_DOMAIN}{path}"
|
||||
|
||||
|
||||
def ensure_opencloud_folder(
|
||||
space_id: str,
|
||||
folder_segments: tuple[str, ...] | list[str],
|
||||
username: str,
|
||||
token: str,
|
||||
) -> None:
|
||||
"""Создаёт папку folder_segments в личном пространстве через MKCOL.
|
||||
|
||||
WebDAV PUT возвращает 409 Conflict, если хотя бы одна промежуточная
|
||||
коллекция ещё не существует, поэтому перед первой загрузкой в новый
|
||||
путь (Out/год/месяц/день/номер_запуска) создаём его сегмент за
|
||||
сегментом — так же, как ensure_dir_exists делает это для SMB.
|
||||
"""
|
||||
segments = [segment for segment in folder_segments if segment]
|
||||
accumulated: list[str] = []
|
||||
for segment in segments:
|
||||
accumulated.append(segment)
|
||||
encoded_path = "/".join(quote(part, safe="") for part in accumulated)
|
||||
url = f"https://{OPENCLOUD_DOMAIN}/dav/spaces/{space_id}/{encoded_path}"
|
||||
try:
|
||||
response = requests.request(
|
||||
"MKCOL",
|
||||
url,
|
||||
auth=HTTPBasicAuth(username, token),
|
||||
timeout=OPENCLOUD_UPLOAD_TIMEOUT_SECONDS,
|
||||
)
|
||||
except requests.RequestException as exc:
|
||||
raise OpenCloudUploadError(
|
||||
f"Ошибка сети при создании папки в OpenCloud: url='{url}', {exc}"
|
||||
) from exc
|
||||
|
||||
# 201 — папка создана, 405 — уже существует (MKCOL на существующую
|
||||
# коллекцию запрещён спецификацией WebDAV, это не ошибка для нас).
|
||||
if response.status_code not in (201, 405):
|
||||
raise OpenCloudUploadError(
|
||||
"Не удалось создать папку в OpenCloud: "
|
||||
f"HTTP {response.status_code}, url='{url}', "
|
||||
f"body={response.text[:500]!r}"
|
||||
)
|
||||
|
||||
|
||||
def upload_local_file_to_opencloud(
|
||||
local_path: Path,
|
||||
space_id: str,
|
||||
@ -74,11 +172,9 @@ def upload_local_file_to_opencloud(
|
||||
OpenCloud (несовпадение audience) — App Token через Basic Auth работает,
|
||||
т.к. идёт отдельным от OIDC путём авторизации.
|
||||
|
||||
ВРЕМЕННО (тестовый прогон): папку НЕ создаём (без MKCOL) — просто
|
||||
кладём файл прямо в личную папку участника (folder_segments — как
|
||||
правило один сегмент, например "Райков М.С."). Структуру Out/год/
|
||||
месяц/день/номер_запуска вернём отдельным шагом после того, как
|
||||
подтвердим на тесте базовый сценарий "файл долетает в нужную папку".
|
||||
Промежуточные папки (folder_segments) должны быть уже созданы через
|
||||
ensure_opencloud_folder — сам PUT их не создаёт и вернёт 409, если
|
||||
хотя бы одна коллекция на пути отсутствует.
|
||||
"""
|
||||
if not username:
|
||||
raise OpenCloudUploadError(
|
||||
|
||||
@ -26,6 +26,9 @@ class RunContext:
|
||||
processed_dir: str
|
||||
run_number: int
|
||||
date_key: str
|
||||
year: str
|
||||
month: str
|
||||
day: str
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -273,7 +276,7 @@ def create_run_context(
|
||||
run_number = next_run_number(out_day_dir, samba_conn)
|
||||
run_dir = join_path(out_day_dir, str(run_number))
|
||||
ensure_dir_exists(run_dir, samba_conn)
|
||||
_, _, _, date_key = build_date_parts(now)
|
||||
year, month, day, date_key = build_date_parts(now)
|
||||
return RunContext(
|
||||
run_dir=run_dir,
|
||||
out_day_dir=out_day_dir,
|
||||
@ -281,6 +284,9 @@ def create_run_context(
|
||||
processed_dir=processed_dir,
|
||||
run_number=run_number,
|
||||
date_key=date_key,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -7,8 +7,14 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from ..opencloud_access import OpenCloudUploadError, upload_local_file_to_opencloud
|
||||
from ..opencloud_access import (
|
||||
OpenCloudUploadError,
|
||||
ensure_opencloud_folder,
|
||||
resolve_personal_drive_id,
|
||||
upload_local_file_to_opencloud,
|
||||
)
|
||||
from .config import (
|
||||
DEFAULT_OUTPUT_DIR_NAME,
|
||||
build_date_parts,
|
||||
build_process_log_file_name,
|
||||
build_protocol_file_name,
|
||||
@ -52,8 +58,6 @@ def run_batch(
|
||||
samba_user: str,
|
||||
samba_password: str,
|
||||
launcher: str,
|
||||
opencloud_space_id: str,
|
||||
opencloud_folder: str | None,
|
||||
opencloud_username: str | None,
|
||||
opencloud_access_token: str | None,
|
||||
artifacts_dir: str | None = None,
|
||||
@ -96,17 +100,6 @@ def run_batch(
|
||||
"(проверьте таблицу токенов)."
|
||||
],
|
||||
)
|
||||
if not opencloud_folder:
|
||||
return BatchResult(
|
||||
status="error",
|
||||
processed=0,
|
||||
partial=0,
|
||||
errors=1,
|
||||
report_path="",
|
||||
protocol_path="",
|
||||
log_path="",
|
||||
messages=["Не удалось определить личную папку пользователя в OpenCloud."],
|
||||
)
|
||||
|
||||
samba_conn = connect_samba(
|
||||
smb_base_path=smb_base_path,
|
||||
@ -256,10 +249,12 @@ def run_batch(
|
||||
report_file_name=report_file_name,
|
||||
protocol_file_name=protocol_file_name,
|
||||
log_file_name=log_file_name,
|
||||
opencloud_space_id=opencloud_space_id,
|
||||
opencloud_folder=opencloud_folder,
|
||||
opencloud_username=opencloud_username,
|
||||
opencloud_access_token=opencloud_access_token,
|
||||
year=run_context.year,
|
||||
month=run_context.month,
|
||||
day=run_context.day,
|
||||
run_number=run_context.run_number,
|
||||
)
|
||||
except OpenCloudUploadError as exc:
|
||||
return BatchResult(
|
||||
@ -312,8 +307,6 @@ def run_batch(
|
||||
def run_local_zip_batch(
|
||||
zip_path: Path,
|
||||
launcher: str,
|
||||
opencloud_space_id: str,
|
||||
opencloud_folder: str | None,
|
||||
opencloud_username: str | None,
|
||||
opencloud_access_token: str | None,
|
||||
) -> BatchResult:
|
||||
@ -352,17 +345,6 @@ def run_local_zip_batch(
|
||||
"(проверьте таблицу токенов)."
|
||||
],
|
||||
)
|
||||
if not opencloud_folder:
|
||||
return BatchResult(
|
||||
status="error",
|
||||
processed=0,
|
||||
partial=0,
|
||||
errors=1,
|
||||
report_path="",
|
||||
protocol_path="",
|
||||
log_path="",
|
||||
messages=["Не удалось определить личную папку пользователя в OpenCloud."],
|
||||
)
|
||||
|
||||
protocol_rows: list[ProtocolRow] = []
|
||||
log_lines: list[str] = [f"{_now()} | local zip batch start: {zip_path.name}"]
|
||||
@ -452,7 +434,7 @@ def run_local_zip_batch(
|
||||
)
|
||||
log_lines.append(f"{_now()} | empty batch")
|
||||
|
||||
_, _, _, date_key = build_date_parts()
|
||||
year, month, day, date_key = build_date_parts()
|
||||
run_number = 1
|
||||
report_file_name = build_report_file_name(date_key, run_number)
|
||||
protocol_file_name = build_protocol_file_name(date_key, run_number)
|
||||
@ -476,10 +458,12 @@ def run_local_zip_batch(
|
||||
report_file_name=report_file_name,
|
||||
protocol_file_name=protocol_file_name,
|
||||
log_file_name=log_file_name,
|
||||
opencloud_space_id=opencloud_space_id,
|
||||
opencloud_folder=opencloud_folder,
|
||||
opencloud_username=opencloud_username,
|
||||
opencloud_access_token=opencloud_access_token,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
run_number=run_number,
|
||||
)
|
||||
except OpenCloudUploadError as exc:
|
||||
return BatchResult(
|
||||
@ -691,22 +675,30 @@ def _upload_artifacts(
|
||||
report_file_name: str,
|
||||
protocol_file_name: str,
|
||||
log_file_name: str,
|
||||
opencloud_space_id: str,
|
||||
opencloud_folder: str,
|
||||
opencloud_username: str,
|
||||
opencloud_access_token: str,
|
||||
year: str,
|
||||
month: str,
|
||||
day: str,
|
||||
run_number: int,
|
||||
) -> tuple[str, str, str]:
|
||||
# ВРЕМЕННО (тестовый прогон): грузим плоско, прямо в личную папку
|
||||
# участника, без MKCOL и без структуры Out/год/месяц/день/номер запуска.
|
||||
# Сначала проверяем на тесте, что файл вообще долетает и попадает в
|
||||
# нужную папку — структуру подпапок вернём отдельным шагом.
|
||||
# Личное пространство ищем заново перед каждой загрузкой (не кешируем
|
||||
# ID) — см. resolve_personal_drive_id. Кладём файлы по той же структуре
|
||||
# Out/год/месяц/день/номер_запуска, что и на SMB (RunContext.run_dir).
|
||||
#
|
||||
# opencloud_access_token здесь — App Token заказчика (не Bearer/ID-токен),
|
||||
# авторизация идёт через HTTP Basic Auth вместе с opencloud_username.
|
||||
folder_segments = (opencloud_folder,)
|
||||
space_id = resolve_personal_drive_id(opencloud_username, opencloud_access_token)
|
||||
folder_segments = (DEFAULT_OUTPUT_DIR_NAME, year, month, day, str(run_number))
|
||||
ensure_opencloud_folder(
|
||||
space_id,
|
||||
folder_segments,
|
||||
username=opencloud_username,
|
||||
token=opencloud_access_token,
|
||||
)
|
||||
report_url = upload_local_file_to_opencloud(
|
||||
local_report,
|
||||
space_id=opencloud_space_id,
|
||||
space_id=space_id,
|
||||
folder_segments=folder_segments,
|
||||
file_name=report_file_name,
|
||||
username=opencloud_username,
|
||||
@ -714,7 +706,7 @@ def _upload_artifacts(
|
||||
)
|
||||
protocol_url = upload_local_file_to_opencloud(
|
||||
local_protocol,
|
||||
space_id=opencloud_space_id,
|
||||
space_id=space_id,
|
||||
folder_segments=folder_segments,
|
||||
file_name=protocol_file_name,
|
||||
username=opencloud_username,
|
||||
@ -722,7 +714,7 @@ def _upload_artifacts(
|
||||
)
|
||||
log_url = upload_local_file_to_opencloud(
|
||||
local_log,
|
||||
space_id=opencloud_space_id,
|
||||
space_id=space_id,
|
||||
folder_segments=folder_segments,
|
||||
file_name=log_file_name,
|
||||
username=opencloud_username,
|
||||
|
||||
4
main.py
4
main.py
@ -3,10 +3,8 @@ from dotenv import load_dotenv
|
||||
|
||||
from app.app_config import (
|
||||
ACCESS_SUPPORT_CONTACT,
|
||||
OPENCLOUD_SPACE_ID,
|
||||
SFM_SMB_BASE_PATH,
|
||||
resolve_opencloud_access_token,
|
||||
resolve_opencloud_user_folder,
|
||||
resolve_opencloud_username,
|
||||
resolve_user_id,
|
||||
)
|
||||
@ -87,8 +85,6 @@ def main() -> None:
|
||||
samba_user=samba_user,
|
||||
samba_password=samba_password,
|
||||
launcher=launcher_name,
|
||||
opencloud_space_id=OPENCLOUD_SPACE_ID,
|
||||
opencloud_folder=resolve_opencloud_user_folder(),
|
||||
opencloud_username=resolve_opencloud_username(),
|
||||
opencloud_access_token=resolve_opencloud_access_token(),
|
||||
artifacts_dir=resolved_input_dir,
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
"""Локальный ручной тест: чисто attach в OpenCloud, без Streamlit.
|
||||
|
||||
Использование:
|
||||
1. Впишите ниже ACCESS_TOKEN, SPACE_ID (если отличается от дефолта),
|
||||
FOLDER и путь к тестовому файлу.
|
||||
2. Запустите: python scripts/local_opencloud_smoke.py
|
||||
|
||||
Использует тот же app/opencloud_access.py, что и продакшн-код в
|
||||
app/pipeline/service.py — так что если тут сработает, значит и в
|
||||
приложении сработает (сама логика загрузки не отличается).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from app.opencloud_access import OpenCloudUploadError, upload_local_file_to_opencloud
|
||||
|
||||
# --- Заполните перед запуском ---
|
||||
USERNAME = "PASTE_USERNAME_HERE" # логин, под которым создан App Token в OpenCloud
|
||||
APP_TOKEN = "PASTE_APP_TOKEN_HERE" # App Token из веб-интерфейса OpenCloud (6 слов через пробел)
|
||||
SPACE_ID = "28c0cecf-1e20-4664-b165-5c9d5515e70f" # тестовое пространство AURORA
|
||||
FOLDER = "Райков М.С." # ваша личная папка в OpenCloud
|
||||
LOCAL_FILE = Path(__file__).resolve().parent / "test_upload.xlsx"
|
||||
REMOTE_FILE_NAME = "test_upload.xlsx"
|
||||
# ---------------------------------
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not LOCAL_FILE.exists():
|
||||
LOCAL_FILE.write_bytes(b"local opencloud smoke test")
|
||||
print(f"Тестовый файл создан: {LOCAL_FILE}")
|
||||
|
||||
try:
|
||||
url = upload_local_file_to_opencloud(
|
||||
LOCAL_FILE,
|
||||
space_id=SPACE_ID,
|
||||
folder_segments=(FOLDER,),
|
||||
file_name=REMOTE_FILE_NAME,
|
||||
username=USERNAME,
|
||||
token=APP_TOKEN,
|
||||
)
|
||||
except OpenCloudUploadError as exc:
|
||||
print(f"ОШИБКА: {exc}")
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
print(f"OK, загружено: {url}")
|
||||
print("Проверьте файл в веб-интерфейсе OpenCloud, в папке", FOLDER)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -8,8 +8,10 @@
|
||||
Запуск:
|
||||
streamlit run scripts/local_zip_test_app.py
|
||||
|
||||
Токен/папку можно один раз прописать в .env (см. .env.example) — поля
|
||||
на странице предзаполнятся, но их всегда можно поменять руками.
|
||||
Username/токен можно один раз прописать в .env (см. .env.example) — поля
|
||||
на странице предзаполнятся, но их всегда можно поменять руками. Папка не
|
||||
запрашивается — файлы летят в личное пространство пользователя (id
|
||||
находится через GET /graph/v1.0/me/drives, см. app/opencloud_access.py).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -24,7 +26,6 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
import streamlit as st
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from app.app_config import OPENCLOUD_SPACE_ID
|
||||
from app.pipeline.service import run_local_zip_batch
|
||||
|
||||
load_dotenv()
|
||||
@ -53,15 +54,6 @@ access_token = st.text_input(
|
||||
"SFM_OPENCLOUD_ACCESS_TOKEN."
|
||||
),
|
||||
)
|
||||
space_id = st.text_input(
|
||||
"SPACE_ID",
|
||||
value=os.getenv("SFM_OPENCLOUD_SPACE_ID", OPENCLOUD_SPACE_ID),
|
||||
)
|
||||
folder = st.text_input(
|
||||
"Папка (личная папка участника в OpenCloud)",
|
||||
value=os.getenv("SFM_OPENCLOUD_FOLDER", ""),
|
||||
placeholder="Райков М.С.",
|
||||
)
|
||||
launcher = st.text_input("Запустил (для протокола)", value="local-test")
|
||||
|
||||
uploaded_zip = st.file_uploader("Архив с XML-файлами (.zip)", type=["zip"])
|
||||
@ -71,8 +63,6 @@ if st.button("Начать обработку", type="primary", use_container_wi
|
||||
st.error("Укажите username.")
|
||||
elif not access_token:
|
||||
st.error("Укажите App Token.")
|
||||
elif not folder:
|
||||
st.error("Укажите папку.")
|
||||
elif uploaded_zip is None:
|
||||
st.error("Прикрепите .zip-архив.")
|
||||
else:
|
||||
@ -83,8 +73,6 @@ if st.button("Начать обработку", type="primary", use_container_wi
|
||||
result = run_local_zip_batch(
|
||||
zip_path=zip_path,
|
||||
launcher=launcher or "local-test",
|
||||
opencloud_space_id=space_id,
|
||||
opencloud_folder=folder,
|
||||
opencloud_username=username,
|
||||
opencloud_access_token=access_token,
|
||||
)
|
||||
|
||||
@ -93,6 +93,15 @@ def _fake_opencloud_upload(
|
||||
return f"https://opencloud.test/{'/'.join(folder_segments)}/{file_name}"
|
||||
|
||||
|
||||
def _fake_resolve_personal_drive_id(username, token) -> str:
|
||||
_ = (username, token)
|
||||
return "personal-space-1"
|
||||
|
||||
|
||||
def _fake_ensure_opencloud_folder(space_id, folder_segments, username, token) -> None:
|
||||
_ = (space_id, folder_segments, username, token)
|
||||
|
||||
|
||||
def test_run_batch_smoke_and_idempotency(tmp_path: Path, monkeypatch) -> None:
|
||||
samba = FakeSamba(tmp_path)
|
||||
in_dir_local = tmp_path / "Exchange" / "SMBDEMO" / "test"
|
||||
@ -110,6 +119,12 @@ def test_run_batch_smoke_and_idempotency(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
service, "upload_local_file_to_opencloud", _fake_opencloud_upload
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service, "resolve_personal_drive_id", _fake_resolve_personal_drive_id
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service, "ensure_opencloud_folder", _fake_ensure_opencloud_folder
|
||||
)
|
||||
|
||||
first = run_batch(
|
||||
smb_base_path="//test-server/share",
|
||||
@ -117,8 +132,6 @@ def test_run_batch_smoke_and_idempotency(tmp_path: Path, monkeypatch) -> None:
|
||||
samba_user="user",
|
||||
samba_password="pass",
|
||||
launcher="tester",
|
||||
opencloud_space_id="space-1",
|
||||
opencloud_folder="Тестов Т.Т.",
|
||||
opencloud_username="test-user",
|
||||
opencloud_access_token="token-123",
|
||||
)
|
||||
@ -132,8 +145,6 @@ def test_run_batch_smoke_and_idempotency(tmp_path: Path, monkeypatch) -> None:
|
||||
samba_user="user",
|
||||
samba_password="pass",
|
||||
launcher="tester",
|
||||
opencloud_space_id="space-1",
|
||||
opencloud_folder="Тестов Т.Т.",
|
||||
opencloud_username="test-user",
|
||||
opencloud_access_token="token-123",
|
||||
)
|
||||
@ -158,6 +169,12 @@ def test_run_batch_success_when_delete_denied_after_copy(
|
||||
monkeypatch.setattr(
|
||||
service, "upload_local_file_to_opencloud", _fake_opencloud_upload
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service, "resolve_personal_drive_id", _fake_resolve_personal_drive_id
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service, "ensure_opencloud_folder", _fake_ensure_opencloud_folder
|
||||
)
|
||||
|
||||
result = run_batch(
|
||||
smb_base_path="//test-server/share",
|
||||
@ -165,8 +182,6 @@ def test_run_batch_success_when_delete_denied_after_copy(
|
||||
samba_user="user",
|
||||
samba_password="pass",
|
||||
launcher="tester",
|
||||
opencloud_space_id="space-1",
|
||||
opencloud_folder="Тестов Т.Т.",
|
||||
opencloud_username="test-user",
|
||||
opencloud_access_token="token-123",
|
||||
)
|
||||
|
||||
@ -30,10 +30,29 @@ def _fake_opencloud_upload(
|
||||
return f"https://opencloud.test/{'/'.join(folder_segments)}/{file_name}"
|
||||
|
||||
|
||||
def test_run_local_zip_batch_success(tmp_path: Path, monkeypatch) -> None:
|
||||
def _fake_resolve_personal_drive_id(username, token) -> str:
|
||||
_ = (username, token)
|
||||
return "personal-space-1"
|
||||
|
||||
|
||||
def _fake_ensure_opencloud_folder(space_id, folder_segments, username, token) -> None:
|
||||
_ = (space_id, folder_segments, username, token)
|
||||
|
||||
|
||||
def _patch_opencloud(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
service, "upload_local_file_to_opencloud", _fake_opencloud_upload
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service, "resolve_personal_drive_id", _fake_resolve_personal_drive_id
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service, "ensure_opencloud_folder", _fake_ensure_opencloud_folder
|
||||
)
|
||||
|
||||
|
||||
def test_run_local_zip_batch_success(tmp_path: Path, monkeypatch) -> None:
|
||||
_patch_opencloud(monkeypatch)
|
||||
|
||||
zip_path = tmp_path / "batch.zip"
|
||||
with zipfile.ZipFile(zip_path, "w") as archive:
|
||||
@ -42,8 +61,6 @@ def test_run_local_zip_batch_success(tmp_path: Path, monkeypatch) -> None:
|
||||
result = run_local_zip_batch(
|
||||
zip_path=zip_path,
|
||||
launcher="tester",
|
||||
opencloud_space_id="space-1",
|
||||
opencloud_folder="Тестов Т.Т.",
|
||||
opencloud_username="test-user",
|
||||
opencloud_access_token="token-123",
|
||||
)
|
||||
@ -59,9 +76,7 @@ def test_run_local_zip_batch_success(tmp_path: Path, monkeypatch) -> None:
|
||||
def test_run_local_zip_batch_bad_file_name_is_error(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
service, "upload_local_file_to_opencloud", _fake_opencloud_upload
|
||||
)
|
||||
_patch_opencloud(monkeypatch)
|
||||
|
||||
zip_path = tmp_path / "batch.zip"
|
||||
with zipfile.ZipFile(zip_path, "w") as archive:
|
||||
@ -70,8 +85,6 @@ def test_run_local_zip_batch_bad_file_name_is_error(
|
||||
result = run_local_zip_batch(
|
||||
zip_path=zip_path,
|
||||
launcher="tester",
|
||||
opencloud_space_id="space-1",
|
||||
opencloud_folder="Тестов Т.Т.",
|
||||
opencloud_username="test-user",
|
||||
opencloud_access_token="token-123",
|
||||
)
|
||||
@ -82,9 +95,7 @@ def test_run_local_zip_batch_bad_file_name_is_error(
|
||||
|
||||
|
||||
def test_run_local_zip_batch_empty_archive(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
service, "upload_local_file_to_opencloud", _fake_opencloud_upload
|
||||
)
|
||||
_patch_opencloud(monkeypatch)
|
||||
|
||||
zip_path = tmp_path / "empty.zip"
|
||||
with zipfile.ZipFile(zip_path, "w") as archive:
|
||||
@ -93,8 +104,6 @@ def test_run_local_zip_batch_empty_archive(tmp_path: Path, monkeypatch) -> None:
|
||||
result = run_local_zip_batch(
|
||||
zip_path=zip_path,
|
||||
launcher="tester",
|
||||
opencloud_space_id="space-1",
|
||||
opencloud_folder="Тестов Т.Т.",
|
||||
opencloud_username="test-user",
|
||||
opencloud_access_token="token-123",
|
||||
)
|
||||
@ -111,8 +120,6 @@ def test_run_local_zip_batch_missing_token(tmp_path: Path) -> None:
|
||||
result = run_local_zip_batch(
|
||||
zip_path=zip_path,
|
||||
launcher="tester",
|
||||
opencloud_space_id="space-1",
|
||||
opencloud_folder="Тестов Т.Т.",
|
||||
opencloud_username="test-user",
|
||||
opencloud_access_token="",
|
||||
)
|
||||
@ -129,8 +136,6 @@ def test_run_local_zip_batch_missing_username(tmp_path: Path) -> None:
|
||||
result = run_local_zip_batch(
|
||||
zip_path=zip_path,
|
||||
launcher="tester",
|
||||
opencloud_space_id="space-1",
|
||||
opencloud_folder="Тестов Т.Т.",
|
||||
opencloud_username="",
|
||||
opencloud_access_token="token-123",
|
||||
)
|
||||
@ -146,8 +151,6 @@ def test_run_local_zip_batch_bad_zip_file(tmp_path: Path) -> None:
|
||||
result = run_local_zip_batch(
|
||||
zip_path=zip_path,
|
||||
launcher="tester",
|
||||
opencloud_space_id="space-1",
|
||||
opencloud_folder="Тестов Т.Т.",
|
||||
opencloud_username="test-user",
|
||||
opencloud_access_token="token-123",
|
||||
)
|
||||
|
||||
@ -8,6 +8,8 @@ import pytest
|
||||
from app.opencloud_access import (
|
||||
OpenCloudUploadError,
|
||||
build_opencloud_url,
|
||||
ensure_opencloud_folder,
|
||||
resolve_personal_drive_id,
|
||||
upload_local_file_to_opencloud,
|
||||
)
|
||||
|
||||
@ -102,6 +104,88 @@ def test_upload_local_file_to_opencloud_missing_local_file(tmp_path: Path) -> No
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_personal_drive_id_returns_personal_drive() -> None:
|
||||
fake_response = Mock(
|
||||
status_code=200,
|
||||
json=lambda: {
|
||||
"value": [
|
||||
{"id": "storage1$shared1", "driveType": "project"},
|
||||
{"id": "storage1$personal1", "driveType": "personal"},
|
||||
]
|
||||
},
|
||||
)
|
||||
with patch("app.opencloud_access.requests.get", return_value=fake_response) as get:
|
||||
drive_id = resolve_personal_drive_id("raykovmse", "app token here")
|
||||
|
||||
assert drive_id == "storage1$personal1"
|
||||
args, kwargs = get.call_args
|
||||
assert args[0] == "https://docs.raisa.go.rshbank.ru/graph/v1.0/me/drives"
|
||||
auth = kwargs["auth"]
|
||||
assert auth.username == "raykovmse"
|
||||
assert auth.password == "app token here"
|
||||
|
||||
|
||||
def test_resolve_personal_drive_id_missing_personal_drive_raises() -> None:
|
||||
fake_response = Mock(
|
||||
status_code=200,
|
||||
json=lambda: {"value": [{"id": "storage1$shared1", "driveType": "project"}]},
|
||||
)
|
||||
with patch("app.opencloud_access.requests.get", return_value=fake_response):
|
||||
with pytest.raises(OpenCloudUploadError):
|
||||
resolve_personal_drive_id("raykovmse", "app token here")
|
||||
|
||||
|
||||
def test_resolve_personal_drive_id_http_error_raises() -> None:
|
||||
fake_response = Mock(status_code=401, text="Unauthorized")
|
||||
with patch("app.opencloud_access.requests.get", return_value=fake_response):
|
||||
with pytest.raises(OpenCloudUploadError):
|
||||
resolve_personal_drive_id("raykovmse", "app token here")
|
||||
|
||||
|
||||
def test_resolve_personal_drive_id_requires_username_and_token() -> None:
|
||||
with pytest.raises(OpenCloudUploadError):
|
||||
resolve_personal_drive_id("", "app token here")
|
||||
with pytest.raises(OpenCloudUploadError):
|
||||
resolve_personal_drive_id("raykovmse", "")
|
||||
|
||||
|
||||
def test_ensure_opencloud_folder_creates_each_segment_via_mkcol() -> None:
|
||||
fake_response = Mock(status_code=201, text="")
|
||||
with patch(
|
||||
"app.opencloud_access.requests.request", return_value=fake_response
|
||||
) as request:
|
||||
ensure_opencloud_folder(
|
||||
"abc$def",
|
||||
("Out", "2026", "9", "17", "1"),
|
||||
username="raykovmse",
|
||||
token="app token here",
|
||||
)
|
||||
|
||||
assert request.call_count == 5
|
||||
method, url = request.call_args_list[-1].args
|
||||
assert method == "MKCOL"
|
||||
assert url == (
|
||||
"https://docs.raisa.go.rshbank.ru/dav/spaces/abc$def/" "Out/2026/9/17/1"
|
||||
)
|
||||
|
||||
|
||||
def test_ensure_opencloud_folder_tolerates_already_existing_collection() -> None:
|
||||
fake_response = Mock(status_code=405, text="")
|
||||
with patch("app.opencloud_access.requests.request", return_value=fake_response):
|
||||
ensure_opencloud_folder(
|
||||
"abc", ("Out",), username="raykovmse", token="token-123"
|
||||
)
|
||||
|
||||
|
||||
def test_ensure_opencloud_folder_raises_on_unexpected_status() -> None:
|
||||
fake_response = Mock(status_code=500, text="boom")
|
||||
with patch("app.opencloud_access.requests.request", return_value=fake_response):
|
||||
with pytest.raises(OpenCloudUploadError):
|
||||
ensure_opencloud_folder(
|
||||
"abc", ("Out",), username="raykovmse", token="token-123"
|
||||
)
|
||||
|
||||
|
||||
def test_upload_local_file_to_opencloud_http_error(tmp_path: Path) -> None:
|
||||
local_file = tmp_path / "report.xlsx"
|
||||
local_file.write_bytes(b"data")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user