z230
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user