This commit is contained in:
2026-06-10 20:16:38 +02:00
parent 7b2f69ad85
commit bff5cc4cac
20 changed files with 4037 additions and 19 deletions
+51 -17
View File
@@ -1,20 +1,24 @@
# app.py | v2.0 | 2026-06-08
# app.py | v2.3 | 2026-06-10
# FastAPI server pro příjem .msg a .db souborů, upload do Dropboxu a import do Graph API.
# Endpointy: /upload (.msg → /msgs + Graph import), /upload-db (.db → /msgs/db),
# Endpointy: /upload (.msg/.emsg → /msgs + Graph import),
# /upload-db (.db NEBO .db.xz.enc → Fernet desifruj + lzma rozbal → /msgs/db),
# /upload-dropbox (→ Dropbox /!!!Days/Downloads Z230),
# /message-delete, /message-update (sync: smazání, přečtení, přesun složky),
# /mirror-plan (diff manifestu z JNJ vůči schránce → smaže přebytky, vrátí to_add),
# /status (seznam souborů k odeslání na JNJ — jména zašifrována Fernetem),
# /item/{enc_filename} (stažení souboru — enc_filename je Fernet token).
# /item/{enc_filename} (stažení souboru — enc_filename je Fernet token;
# Accept: application/json → {"data": fernet_b64}, jinak binárka).
from fastapi import FastAPI, UploadFile, File, Form, Header, HTTPException, Response
from fastapi import FastAPI, Request, UploadFile, File, Form, Header, HTTPException, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel
import shutil
import base64
import hashlib
import logging
import lzma
from pathlib import Path
from typing import Optional
from urllib.parse import quote
import os
import dropbox
import msal
@@ -372,14 +376,27 @@ async def upload_db(
):
if authorization != f"Bearer {TOKEN}":
raise HTTPException(status_code=401, detail="Unauthorized")
if not file.filename.endswith(".db"):
raise HTTPException(status_code=400, detail="Only .db files accepted")
fn = file.filename or ""
is_enc = fn.endswith(".db.xz.enc") # jnj_mailbox_sync >= v1.2
if not (is_enc or fn.endswith(".db")):
raise HTTPException(status_code=400, detail="Only .db or .db.xz.enc files accepted")
content = await file.read()
if is_enc:
# Fernet desifra -> lzma rozbal -> plain .db (jako .emsg -> .msg u /upload)
content = lzma.decompress(_FERNET.decrypt(content))
db_filename = fn[: -len(".xz.enc")] # jnjemails_<ts>.db
else:
db_filename = fn
# Smazat stare AZ po uspesnem desifrovani/rozbaleni — pri chybe stara DB zustane.
for old in DB_DIR.glob("*.db"):
old.unlink()
dest = DB_DIR / file.filename
dest = DB_DIR / db_filename
with dest.open("wb") as f:
shutil.copyfileobj(file.file, f)
return {"status": "saved", "file": file.filename}
f.write(content)
return {"status": "saved", "file": db_filename, "bytes": len(content), "encrypted": is_enc}
class MessageDeleteRequest(BaseModel):
@@ -547,7 +564,7 @@ async def pending_files(authorization: str = Header(None)):
@app.get("/item/{filename:path}")
async def download_file(filename: str, authorization: str = Header(None)):
async def download_file(filename: str, request: Request, authorization: str = Header(None)):
if authorization != f"Bearer {TOKEN}":
raise HTTPException(status_code=401, detail="Unauthorized")
# filename je Fernet token (zašifrované původní jméno souboru)
@@ -570,7 +587,28 @@ async def download_file(filename: str, authorization: str = Header(None)):
encrypted = _FERNET.encrypt(raw)
# Přesun do Sent
if "application/json" in (request.headers.get("accept") or ""):
# v2.3: klient >= v1.2 — obsah jako JSON, ne binární příloha. Korporátní
# filtr (Zscaler/SiteMinder) pak nevidí "stahování souboru" a nespouští
# AV sandbox, který binární odpovědi blokoval (403 + ?_sm_nck=1).
# Fernet token je sám o sobě urlsafe-base64 text → rovnou do JSON.
resp = JSONResponse(content={"data": encrypted.decode()})
else:
# Starý klient (<= v1.1) — binární odpověď jako dřív.
# HTTP hlavičky jsou latin-1 — jméno s ne-ASCII znaky (např. ▲▲) by shodilo
# Response na UnicodeEncodeError (500). ASCII fallback + RFC 5987 filename*.
# Klient hlavičku stejně nečte (jméno zná z dešifrovaného tokenu).
fname = f"{orig_filename}.enc"
ascii_fallback = fname.encode("ascii", "ignore").decode().replace('"', "") or "file.enc"
resp = Response(
content=encrypted,
media_type="application/octet-stream",
headers={"Content-Disposition":
f"attachment; filename=\"{ascii_fallback}\"; filename*=UTF-8''{quote(fname)}"},
)
# Přesun do Sent — až PO úspěšném sestavení odpovědi, aby případný pád
# neodstranil soubor z fronty UploadToJNJ dřív, než ho klient dostane.
sent_path = f"{DROPBOX_UPLOAD_TO_JNJ}/##Trash/{orig_filename}"
try:
dbx.files_move_v2(dropbox_path, sent_path, autorename=True)
@@ -578,8 +616,4 @@ async def download_file(filename: str, authorization: str = Header(None)):
except Exception as e:
log.warning("download-file: nelze přesunout %s do Sent: %s", orig_filename, e)
return Response(
content=encrypted,
media_type="application/octet-stream",
headers={"Content-Disposition": f'attachment; filename="{orig_filename}.enc"'},
)
return resp