Compare commits
8 Commits
f093d41f60
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d2d17f1297 | |||
| faa7e841bf | |||
| fb4c23c206 | |||
| 82284f7aab | |||
| 8ee7ff9227 | |||
| 0fd3862c56 | |||
| 01dfe37642 | |||
| 9bd3272846 |
@@ -1,7 +1,6 @@
|
|||||||
.git
|
.git
|
||||||
.postgres
|
.postgres
|
||||||
postgres
|
postgres
|
||||||
incoming
|
|
||||||
backend/uploads
|
backend/uploads
|
||||||
backend/logs
|
backend/logs
|
||||||
backend/.venv
|
backend/.venv
|
||||||
|
|||||||
@@ -11,6 +11,3 @@ POSTGRES_DB=mail_summary
|
|||||||
# Windows: ./backend/uploads
|
# Windows: ./backend/uploads
|
||||||
# Linux (Synology): /volume1/09_container_manager/13_mail_summary/backend/uploads
|
# Linux (Synology): /volume1/09_container_manager/13_mail_summary/backend/uploads
|
||||||
UPLOAD_HOST=./backend/uploads
|
UPLOAD_HOST=./backend/uploads
|
||||||
|
|
||||||
# Host folder mounted into backend as /app/incoming (for multi-GB zips).
|
|
||||||
INCOMING_HOST=C:/Users/Administrator/Downloads/20260719
|
|
||||||
|
|||||||
@@ -19,6 +19,3 @@
|
|||||||
|
|
||||||
# postgres
|
# postgres
|
||||||
/postgres
|
/postgres
|
||||||
|
|
||||||
# large mailbox drops (optional local folder)
|
|
||||||
/incoming
|
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# Mail Summary
|
||||||
|
|
||||||
|
로컬 LLM으로 한 주 치 메일 백업(`.eml` zip)을 분석해 **주간 업무보고(금주 / 차주)** 를 자동 정리하는 온프레미스 도구입니다.
|
||||||
|
|
||||||
|
메일은 외부 AI로 전송하지 않고, 지정한 Ollama 엔드포인트에서만 요약합니다.
|
||||||
|
|
||||||
|
**버전:** 1.0.0
|
||||||
|
**프로덕션 URL:** https://ms.takits.me
|
||||||
|
**로컬 URL:** http://localhost:4888
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주요 기능
|
||||||
|
|
||||||
|
- 받은함 / 보낸함 `.zip` 업로드 (대용량 zip 지원, nginx 최대 약 20GB)
|
||||||
|
- 기간·메일함 범위 지정 후 분석 작업 생성
|
||||||
|
- 진행 상태 폴링 (준비 → 스캔·필터 → LLM 요약 → 완료)
|
||||||
|
- 고객(은행/거래처) 단위 **금주 / 차주** 업무보고 UI
|
||||||
|
- 보고서 원클릭 복사
|
||||||
|
- DB / LLM 상태 표시 (`LLM(on-prem · 30B)`, `v1.0.0`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 아키텍처
|
||||||
|
|
||||||
|
```text
|
||||||
|
Browser
|
||||||
|
└─ frontend (nginx :4888)
|
||||||
|
├─ static React build
|
||||||
|
└─ /api/* → backend (FastAPI)
|
||||||
|
├─ Postgres 15.6
|
||||||
|
└─ Ollama (LLM_BASE_URL)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 구성 | 기술 |
|
||||||
|
|------|------|
|
||||||
|
| Frontend | React + Vite, nginx |
|
||||||
|
| Backend | FastAPI, Python 3.12 |
|
||||||
|
| DB | PostgreSQL 15.6 |
|
||||||
|
| LLM | Ollama (예: `qwen3:30b-a3b-instruct-2507-q4_K_M`) |
|
||||||
|
| 배포 | Docker Compose |
|
||||||
|
|
||||||
|
### 분석 파이프라인
|
||||||
|
|
||||||
|
1. zip에서 `.eml` 추출
|
||||||
|
2. 기간·노이즈 필터 (프로모션/불필요 메일 등)
|
||||||
|
3. 스레드 단위 LLM 요약
|
||||||
|
4. 주간 보고 JSON 생성 (`this_week` / `next_week` — 고객별 항목)
|
||||||
|
5. 본문 정리(purge) 후 보고 결과만 유지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 빠른 시작
|
||||||
|
|
||||||
|
### 요구사항
|
||||||
|
|
||||||
|
- Docker / Docker Compose
|
||||||
|
- 접근 가능한 Ollama 서버와 사용할 모델
|
||||||
|
|
||||||
|
### 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mail_summary
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
- 웹 UI: http://localhost:4888
|
||||||
|
- Postgres (호스트): `localhost:5437`
|
||||||
|
|
||||||
|
### 자주 쓰는 명령
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 전체 재빌드
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# 프론트만 재빌드
|
||||||
|
docker compose up -d --build frontend
|
||||||
|
|
||||||
|
# 백엔드만 재시작 (env 변경 후)
|
||||||
|
docker compose up -d --force-recreate backend
|
||||||
|
|
||||||
|
# 로그
|
||||||
|
docker compose logs -f backend
|
||||||
|
docker compose logs -f frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 환경 변수
|
||||||
|
|
||||||
|
### 루트 `.env` (Compose)
|
||||||
|
|
||||||
|
| 변수 | 설명 | 예시 |
|
||||||
|
|------|------|------|
|
||||||
|
| `APP_NAME` | 컨테이너 이름 prefix | `mail_summary` |
|
||||||
|
| `FRONTEND_PORT` | 웹 UI 포트 | `4888` |
|
||||||
|
| `POSTGRES_PORT` | DB 호스트 포트 | `5437` |
|
||||||
|
| `POSTGRES_USER` / `PASSWORD` / `DB` | DB 접속 정보 | — |
|
||||||
|
| `UPLOAD_HOST` | 업로드 저장 호스트 경로 | `./backend/uploads` |
|
||||||
|
|
||||||
|
Synology 등에서는 `UPLOAD_HOST`를 실제 호스트 경로로 지정합니다.
|
||||||
|
|
||||||
|
### `backend/.env`
|
||||||
|
|
||||||
|
| 변수 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `CORS_ORIGINS` | 허용 Origin (쉼표 구분) |
|
||||||
|
| `LLM_BASE_URL` | Ollama base URL |
|
||||||
|
| `LLM_MODEL` | 사용할 모델명 |
|
||||||
|
| `LLM_TIMEOUT_SECONDS` | LLM 타임아웃 |
|
||||||
|
| `LLM_MAX_MAILS` | 분석에 넣을 최대 메일 수 |
|
||||||
|
| `LLM_BODY_CHARS` | 메일 본문 전달 최대 글자 수 |
|
||||||
|
| `UPLOAD_DIR` | 컨테이너 내 업로드 경로 (`/app/uploads`) |
|
||||||
|
|
||||||
|
`DATABASE_URL`은 Compose가 Postgres 서비스로 주입합니다.
|
||||||
|
|
||||||
|
### `frontend/.env` (빌드 시점)
|
||||||
|
|
||||||
|
| 변수 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `VITE_APP_NAME` | UI 앱 이름 |
|
||||||
|
| `VITE_APP_VERSION` | UI 버전 표시 |
|
||||||
|
| `VITE_SITE_URL` | OG 메타 URL (예: `https://ms.takits.me`) |
|
||||||
|
|
||||||
|
프론트 env를 바꾼 뒤에는 **frontend 재빌드**가 필요합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 사용 방법
|
||||||
|
|
||||||
|
1. **01 업로드** — 받은함 / 보낸함 zip 업로드 (둘 중 하나만도 가능)
|
||||||
|
2. **02 조건** — 시작일·종료일·분석 범위 선택 후 **분석 시작**
|
||||||
|
3. **03 결과** — 진행률 확인 → 금주/차주 보고 확인 → 복사 / 조건으로 돌아가기 / 새 분석
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 개요
|
||||||
|
|
||||||
|
| Method | Path | 설명 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `GET` | `/api/health` | DB·LLM 상태 |
|
||||||
|
| `POST` | `/api/uploads` | mailbox zip 업로드 |
|
||||||
|
| `DELETE` | `/api/uploads/{upload_id}` | 스테이징 삭제 |
|
||||||
|
| `POST` | `/api/jobs` | 분석 작업 생성 |
|
||||||
|
| `GET` | `/api/jobs/{id}` | 작업 상태 |
|
||||||
|
| `GET` | `/api/jobs/{id}/report` | 주간 보고 |
|
||||||
|
| `GET` | `/api/jobs` | 작업 목록 |
|
||||||
|
|
||||||
|
프론트 nginx가 `/api/*`를 backend로 프록시합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DB 테이블
|
||||||
|
|
||||||
|
- `analysis_jobs` — 작업 상태·기간·진행률
|
||||||
|
- `mails` — 필터된 메일·스레드 요약 (본문은 분석 후 purge 가능)
|
||||||
|
- `weekly_reports` — 최종 주간 보고 JSON / markdown
|
||||||
|
|
||||||
|
스키마: `db/init/01_schema.sql` (컨테이너 최초 기동 시 적용). 백엔드도 기동 시 idempotent `CREATE TABLE IF NOT EXISTS`를 수행합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 디렉터리 구조
|
||||||
|
|
||||||
|
```text
|
||||||
|
mail_summary/
|
||||||
|
├── compose.yaml
|
||||||
|
├── .env # Compose / 포트 / 업로드 경로
|
||||||
|
├── db/init/ # Postgres init SQL
|
||||||
|
├── backend/
|
||||||
|
│ ├── .env # LLM, CORS 등
|
||||||
|
│ ├── app.py # FastAPI
|
||||||
|
│ ├── pipeline.py # 분석·보고 파이프라인
|
||||||
|
│ ├── mail_parser.py
|
||||||
|
│ ├── llm_client.py
|
||||||
|
│ └── uploads/ # zip 스테이징·작업 파일
|
||||||
|
└── frontend/
|
||||||
|
├── .env # VITE_*
|
||||||
|
├── nginx.conf
|
||||||
|
└── src/ # React UI
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 도메인 배포 메모
|
||||||
|
|
||||||
|
앱 설정:
|
||||||
|
|
||||||
|
- `frontend/.env` → `VITE_SITE_URL=https://ms.takits.me`
|
||||||
|
- `backend/.env` → `CORS_ORIGINS`에 `https://ms.takits.me` 포함
|
||||||
|
|
||||||
|
인프라:
|
||||||
|
|
||||||
|
- DNS: `ms.takits.me` → 서버
|
||||||
|
- 리버스 프록시에서 HTTPS 종료 후 `FRONTEND_PORT`(기본 4888)로 프록시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 보안·운영 참고
|
||||||
|
|
||||||
|
- 메일 원문은 분석용으로만 쓰고, 보고 생성 후 본문은 purge하는 흐름입니다.
|
||||||
|
- zip·업로드 데이터는 `UPLOAD_HOST` 경로에 남습니다. 운영 환경에서는 디스크·권한을 관리하세요.
|
||||||
|
- `.env`의 DB 비밀번호·LLM URL은 저장소에 올리지 않는 것을 권장합니다.
|
||||||
|
)
|
||||||
+1
-2
@@ -4,10 +4,9 @@ CORS_ORIGINS=https://ms.takits.me,http://localhost:4888,http://localhost:5173
|
|||||||
|
|
||||||
# Local LLM (Ollama on Mac).
|
# Local LLM (Ollama on Mac).
|
||||||
LLM_BASE_URL=https://mac-ollama.takits.me
|
LLM_BASE_URL=https://mac-ollama.takits.me
|
||||||
LLM_MODEL=gemma4:31b-mlx
|
LLM_MODEL=qwen3:30b-a3b-instruct-2507-q4_K_M
|
||||||
LLM_TIMEOUT_SECONDS=300
|
LLM_TIMEOUT_SECONDS=300
|
||||||
LLM_MAX_MAILS=80
|
LLM_MAX_MAILS=80
|
||||||
LLM_BODY_CHARS=3000
|
LLM_BODY_CHARS=3000
|
||||||
|
|
||||||
UPLOAD_DIR=/app/uploads
|
UPLOAD_DIR=/app/uploads
|
||||||
INCOMING_DIR=/app/incoming
|
|
||||||
|
|||||||
+1
-54
@@ -16,7 +16,7 @@ from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from config import CORS_ORIGINS, INCOMING_DIR, LLM_MODEL, UPLOAD_DIR
|
from config import CORS_ORIGINS, LLM_MODEL, UPLOAD_DIR
|
||||||
from db.pool import close_pool, init_pool, pool_enabled
|
from db.pool import close_pool, init_pool, pool_enabled
|
||||||
from job_repository import create_job, ensure_tables, get_job, get_report, list_jobs
|
from job_repository import create_job, ensure_tables, get_job, get_report, list_jobs
|
||||||
from llm_client import ping_llm
|
from llm_client import ping_llm
|
||||||
@@ -35,7 +35,6 @@ STAGING_DIR = UPLOAD_DIR / "staging"
|
|||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
STAGING_DIR.mkdir(parents=True, exist_ok=True)
|
STAGING_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
INCOMING_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
await init_pool()
|
await init_pool()
|
||||||
await ensure_tables()
|
await ensure_tables()
|
||||||
yield
|
yield
|
||||||
@@ -69,7 +68,6 @@ class UploadResponse(BaseModel):
|
|||||||
mailbox: str
|
mailbox: str
|
||||||
filename: str
|
filename: str
|
||||||
size_bytes: int
|
size_bytes: int
|
||||||
source: str # upload | incoming
|
|
||||||
|
|
||||||
|
|
||||||
def _staging_path(upload_id: str) -> Path:
|
def _staging_path(upload_id: str) -> Path:
|
||||||
@@ -91,29 +89,6 @@ def _read_staging_meta(upload_id: str) -> dict:
|
|||||||
return json.loads(meta_path.read_text(encoding="utf-8"))
|
return json.loads(meta_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def _attach_source(src: Path, stage_dir: Path, mailbox: str, filename: str) -> Path:
|
|
||||||
dest = stage_dir / f"{mailbox}_{filename}"
|
|
||||||
if dest.exists() or dest.is_symlink():
|
|
||||||
dest.unlink()
|
|
||||||
try:
|
|
||||||
dest.symlink_to(src)
|
|
||||||
except OSError:
|
|
||||||
shutil.copy2(src, dest)
|
|
||||||
return dest
|
|
||||||
|
|
||||||
|
|
||||||
def _find_incoming_match(filename: str, size_bytes: int) -> Path | None:
|
|
||||||
candidate = INCOMING_DIR / Path(filename).name
|
|
||||||
if not candidate.is_file():
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
if candidate.stat().st_size != size_bytes:
|
|
||||||
return None
|
|
||||||
except OSError:
|
|
||||||
return None
|
|
||||||
return candidate.resolve()
|
|
||||||
|
|
||||||
|
|
||||||
def _attach_staging_to_job(upload_id: str, job_dir: Path, mailbox: str) -> str:
|
def _attach_staging_to_job(upload_id: str, job_dir: Path, mailbox: str) -> str:
|
||||||
"""Copy/link staged zip into a job dir, keeping staging for re-analysis."""
|
"""Copy/link staged zip into a job dir, keeping staging for re-analysis."""
|
||||||
meta = _read_staging_meta(upload_id)
|
meta = _read_staging_meta(upload_id)
|
||||||
@@ -147,33 +122,6 @@ async def health() -> HealthResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/uploads/resolve", response_model=UploadResponse)
|
|
||||||
async def api_resolve_upload(
|
|
||||||
mailbox: Mailbox = Form(...),
|
|
||||||
filename: str = Form(...),
|
|
||||||
size_bytes: int = Form(...),
|
|
||||||
) -> UploadResponse:
|
|
||||||
"""If the selected file already exists on the incoming mount, stage it without re-upload."""
|
|
||||||
match = _find_incoming_match(filename, size_bytes)
|
|
||||||
if not match:
|
|
||||||
raise HTTPException(status_code=404, detail="incoming_match_not_found")
|
|
||||||
|
|
||||||
upload_id = uuid.uuid4().hex
|
|
||||||
stage_dir = _staging_path(upload_id)
|
|
||||||
stage_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
safe_name = Path(filename).name
|
|
||||||
_attach_source(match, stage_dir, mailbox, safe_name)
|
|
||||||
meta = {
|
|
||||||
"upload_id": upload_id,
|
|
||||||
"mailbox": mailbox,
|
|
||||||
"filename": safe_name,
|
|
||||||
"size_bytes": size_bytes,
|
|
||||||
"source": "incoming",
|
|
||||||
}
|
|
||||||
_write_staging_meta(stage_dir, meta)
|
|
||||||
return UploadResponse(**meta)
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/uploads", response_model=UploadResponse)
|
@app.post("/api/uploads", response_model=UploadResponse)
|
||||||
async def api_upload_zip(
|
async def api_upload_zip(
|
||||||
mailbox: Mailbox = Form(...),
|
mailbox: Mailbox = Form(...),
|
||||||
@@ -207,7 +155,6 @@ async def api_upload_zip(
|
|||||||
"mailbox": mailbox,
|
"mailbox": mailbox,
|
||||||
"filename": safe_name,
|
"filename": safe_name,
|
||||||
"size_bytes": size,
|
"size_bytes": size,
|
||||||
"source": "upload",
|
|
||||||
}
|
}
|
||||||
_write_staging_meta(stage_dir, meta)
|
_write_staging_meta(stage_dir, meta)
|
||||||
return UploadResponse(**meta)
|
return UploadResponse(**meta)
|
||||||
|
|||||||
+1
-4
@@ -27,13 +27,10 @@ _cors = os.getenv("CORS_ORIGINS", "http://localhost:5173,http://localhost:4888")
|
|||||||
CORS_ORIGINS = [origin.strip() for origin in _cors.split(",") if origin.strip()]
|
CORS_ORIGINS = [origin.strip() for origin in _cors.split(",") if origin.strip()]
|
||||||
|
|
||||||
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "http://host.docker.internal:11434").rstrip("/")
|
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "http://host.docker.internal:11434").rstrip("/")
|
||||||
LLM_MODEL = os.getenv("LLM_MODEL", "gemma4:31b-mlx").strip()
|
LLM_MODEL = os.getenv("LLM_MODEL", "qwen3:30b-a3b-instruct-2507-q4_K_M").strip()
|
||||||
LLM_TIMEOUT_SECONDS = float(os.getenv("LLM_TIMEOUT_SECONDS", "300"))
|
LLM_TIMEOUT_SECONDS = float(os.getenv("LLM_TIMEOUT_SECONDS", "300"))
|
||||||
LLM_MAX_MAILS = int(os.getenv("LLM_MAX_MAILS", "80"))
|
LLM_MAX_MAILS = int(os.getenv("LLM_MAX_MAILS", "80"))
|
||||||
LLM_BODY_CHARS = int(os.getenv("LLM_BODY_CHARS", "3000"))
|
LLM_BODY_CHARS = int(os.getenv("LLM_BODY_CHARS", "3000"))
|
||||||
|
|
||||||
UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", str(BASE_DIR / "uploads"))).resolve()
|
UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", str(BASE_DIR / "uploads"))).resolve()
|
||||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
INCOMING_DIR = Path(os.getenv("INCOMING_DIR", str(BASE_DIR.parent / "incoming"))).resolve()
|
|
||||||
INCOMING_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ services:
|
|||||||
- ./backend/logs:/app/logs
|
- ./backend/logs:/app/logs
|
||||||
# Host upload dir (Windows: ./backend/uploads, Linux Synology: set UPLOAD_HOST in .env).
|
# Host upload dir (Windows: ./backend/uploads, Linux Synology: set UPLOAD_HOST in .env).
|
||||||
- ${UPLOAD_HOST:-./backend/uploads}:/app/uploads
|
- ${UPLOAD_HOST:-./backend/uploads}:/app/uploads
|
||||||
# Drop multi-GB mailbox zips here (or point INCOMING_HOST at another folder).
|
|
||||||
- ${INCOMING_HOST:-./incoming}:/app/incoming:ro
|
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
+2
-38
@@ -28,10 +28,6 @@ async function request(path, options = {}) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHealth() {
|
export function getHealth() {
|
||||||
return request("/api/health");
|
return request("/api/health");
|
||||||
}
|
}
|
||||||
@@ -51,25 +47,6 @@ export function createJob(formData) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Try to stage a file that already exists on the server incoming mount. */
|
|
||||||
export async function resolveUpload(mailbox, filename, sizeBytes) {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append("mailbox", mailbox);
|
|
||||||
form.append("filename", filename);
|
|
||||||
form.append("size_bytes", String(sizeBytes));
|
|
||||||
const response = await fetch(`${API_BASE}/api/uploads/resolve`, {
|
|
||||||
method: "POST",
|
|
||||||
body: form,
|
|
||||||
});
|
|
||||||
if (response.status === 404) return null;
|
|
||||||
const text = await response.text();
|
|
||||||
const data = text ? JSON.parse(text) : {};
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.detail || "resolve_failed");
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Upload zip with progress via XHR. */
|
/** Upload zip with progress via XHR. */
|
||||||
export function uploadZip(file, mailbox, onProgress) {
|
export function uploadZip(file, mailbox, onProgress) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -109,21 +86,8 @@ export function uploadZip(file, mailbox, onProgress) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Stage a mailbox zip by uploading it to the server. */
|
||||||
* Stage a mailbox zip:
|
export function stageMailboxZip(file, mailbox, onProgress) {
|
||||||
* 1) resolve against server incoming mount (no re-upload)
|
|
||||||
* 2) otherwise upload with real progress
|
|
||||||
*/
|
|
||||||
export async function stageMailboxZip(file, mailbox, onProgress) {
|
|
||||||
onProgress?.(2);
|
|
||||||
const resolved = await resolveUpload(mailbox, file.name, file.size);
|
|
||||||
if (resolved) {
|
|
||||||
for (const pct of [25, 55, 80, 100]) {
|
|
||||||
onProgress?.(pct);
|
|
||||||
await sleep(70);
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
return uploadZip(file, mailbox, onProgress);
|
return uploadZip(file, mailbox, onProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user