mfup-fastapi 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ node_modules/
2
+ dist/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .vite/
6
+ uploads/
7
+ repomix-output.md
8
+ *.url
9
+ repomix.sh
10
+ # e2e artifacts
11
+ e2e/.tmp/
12
+ e2e/test-results/
13
+ e2e/playwright-report/
14
+ e2e/webtop/testdata/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 n0isy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: mfup-fastapi
3
+ Version: 0.2.0
4
+ Summary: FastAPI integration for the MFUP/2 upload engine: MfupEngine + APIRouter you mount into your own app, or a ready-to-run standalone server.
5
+ Project-URL: Homepage, https://github.com/n0isy/mfup
6
+ Project-URL: Repository, https://github.com/n0isy/mfup
7
+ Project-URL: Issues, https://github.com/n0isy/mfup/issues
8
+ Author: n0isy
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: fastapi,mfup,multi-file,resumable,upload
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: fastapi>=0.115.0
23
+ Requires-Dist: mfup-core==0.2.0
24
+ Requires-Dist: uvicorn[standard]>=0.30.0
25
+ Requires-Dist: websockets>=13.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: httpx; extra == 'dev'
28
+ Requires-Dist: pytest; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # mfup-fastapi
33
+
34
+ FastAPI integration for the MFUP/2 resumable multi-file upload engine
35
+ ([`mfup-core`](https://pypi.org/project/mfup-core/)).
36
+
37
+ ## Embed into your app
38
+
39
+ ```python
40
+ from pathlib import Path
41
+ from fastapi import FastAPI
42
+ from mfup_fastapi import MfupConfig, MfupEngine
43
+ from mfup_core import AuthRequest, AuthResult
44
+
45
+ async def authorize(req: AuthRequest) -> AuthResult | None:
46
+ user = await my_auth(req.headers) # cookies / Authorization
47
+ if user is None:
48
+ return None # → SESSION_ABORT(auth_failed)
49
+ return AuthResult(
50
+ base_dir=f"/srv/homes/{user.id}", # per-user home
51
+ max_total_bytes=10 * 2**30, # 10 GiB quota
52
+ context={"user_id": user.id},
53
+ )
54
+
55
+ engine = MfupEngine(MfupConfig(
56
+ base_dir=Path("/srv/uploads"),
57
+ redis_url="redis://localhost:6379/0",
58
+ authorize=authorize, # a callable — or "pkg.mod:func"
59
+ ))
60
+
61
+ app = FastAPI(lifespan=engine.lifespan)
62
+ app.include_router(engine.router, prefix="/api/uploads")
63
+ ```
64
+
65
+ Point the browser SDK (`@mfup/client` on npm) at the same prefix:
66
+ `new MfupSession({ serverUrl: "https://host/api/uploads" })`.
67
+
68
+ ## Run standalone
69
+
70
+ ```bash
71
+ pip install mfup-fastapi
72
+ MFUP_BASE_DIR=/srv/uploads REDIS_URL=redis://localhost:6379/0 \
73
+ python -m mfup_fastapi
74
+ ```
75
+
76
+ All `MfupConfig` fields map 1:1 to `MFUP_*` environment variables
77
+ (`MfupConfig.from_env()`); hooks are dotted paths there
78
+ (`MFUP_AUTHORIZE=myapp.uploads:authorize`).
79
+
80
+ ## Hooks
81
+
82
+ | Hook | When | Controls |
83
+ |---|---|---|
84
+ | `authorize` | HELLO, before anything is created | allow/deny, per-user `base_dir`, target mapping, byte/file quotas, `context` |
85
+ | `map_file` | publish, per file | final layout (by type/scope/anything) |
86
+ | `on_committed` | after COMMIT_OK | notification; return `"publish"` to publish server-side (scan/moderation/billing flows). Or call `engine.publish(session_id)` yourself later. |
87
+
88
+ Requires Redis (expiry index) and a POSIX filesystem. One worker per engine
89
+ instance; resume across workers is handled via lazy recovery from the Redis
90
+ index. Full contract: `docs/EXTENDING.md` in the repository.
91
+
92
+ Docs and source: <https://github.com/n0isy/mfup>
@@ -0,0 +1,61 @@
1
+ # mfup-fastapi
2
+
3
+ FastAPI integration for the MFUP/2 resumable multi-file upload engine
4
+ ([`mfup-core`](https://pypi.org/project/mfup-core/)).
5
+
6
+ ## Embed into your app
7
+
8
+ ```python
9
+ from pathlib import Path
10
+ from fastapi import FastAPI
11
+ from mfup_fastapi import MfupConfig, MfupEngine
12
+ from mfup_core import AuthRequest, AuthResult
13
+
14
+ async def authorize(req: AuthRequest) -> AuthResult | None:
15
+ user = await my_auth(req.headers) # cookies / Authorization
16
+ if user is None:
17
+ return None # → SESSION_ABORT(auth_failed)
18
+ return AuthResult(
19
+ base_dir=f"/srv/homes/{user.id}", # per-user home
20
+ max_total_bytes=10 * 2**30, # 10 GiB quota
21
+ context={"user_id": user.id},
22
+ )
23
+
24
+ engine = MfupEngine(MfupConfig(
25
+ base_dir=Path("/srv/uploads"),
26
+ redis_url="redis://localhost:6379/0",
27
+ authorize=authorize, # a callable — or "pkg.mod:func"
28
+ ))
29
+
30
+ app = FastAPI(lifespan=engine.lifespan)
31
+ app.include_router(engine.router, prefix="/api/uploads")
32
+ ```
33
+
34
+ Point the browser SDK (`@mfup/client` on npm) at the same prefix:
35
+ `new MfupSession({ serverUrl: "https://host/api/uploads" })`.
36
+
37
+ ## Run standalone
38
+
39
+ ```bash
40
+ pip install mfup-fastapi
41
+ MFUP_BASE_DIR=/srv/uploads REDIS_URL=redis://localhost:6379/0 \
42
+ python -m mfup_fastapi
43
+ ```
44
+
45
+ All `MfupConfig` fields map 1:1 to `MFUP_*` environment variables
46
+ (`MfupConfig.from_env()`); hooks are dotted paths there
47
+ (`MFUP_AUTHORIZE=myapp.uploads:authorize`).
48
+
49
+ ## Hooks
50
+
51
+ | Hook | When | Controls |
52
+ |---|---|---|
53
+ | `authorize` | HELLO, before anything is created | allow/deny, per-user `base_dir`, target mapping, byte/file quotas, `context` |
54
+ | `map_file` | publish, per file | final layout (by type/scope/anything) |
55
+ | `on_committed` | after COMMIT_OK | notification; return `"publish"` to publish server-side (scan/moderation/billing flows). Or call `engine.publish(session_id)` yourself later. |
56
+
57
+ Requires Redis (expiry index) and a POSIX filesystem. One worker per engine
58
+ instance; resume across workers is handled via lazy recovery from the Redis
59
+ index. Full contract: `docs/EXTENDING.md` in the repository.
60
+
61
+ Docs and source: <https://github.com/n0isy/mfup>
@@ -0,0 +1,40 @@
1
+ """mfup-fastapi — FastAPI integration for the MFUP/2 upload engine.
2
+
3
+ from mfup_fastapi import MfupConfig, MfupEngine, create_app
4
+ """
5
+
6
+ from .config import MfupConfig
7
+ from .engine import (
8
+ MapFileHookError,
9
+ MfupEngine,
10
+ NotCommitted,
11
+ PublishError,
12
+ SessionNotFound,
13
+ TargetEscapes,
14
+ reconcile_orphans,
15
+ )
16
+
17
+
18
+ def create_app(config: "MfupConfig | None" = None):
19
+ """Lazy re-export: builds the standalone app without importing
20
+ mfup_fastapi.app (which reads the environment at import time)."""
21
+ from fastapi import FastAPI
22
+
23
+ engine = MfupEngine(config or MfupConfig.from_env())
24
+ app = FastAPI(title="MFUP/2 Server", lifespan=engine.lifespan)
25
+ app.include_router(engine.router)
26
+ app.state.mfup_engine = engine
27
+ return app
28
+
29
+
30
+ __all__ = [
31
+ "create_app",
32
+ "MapFileHookError",
33
+ "MfupConfig",
34
+ "MfupEngine",
35
+ "NotCommitted",
36
+ "PublishError",
37
+ "reconcile_orphans",
38
+ "SessionNotFound",
39
+ "TargetEscapes",
40
+ ]
@@ -0,0 +1,13 @@
1
+ """Run the standalone MFUP/2 server: python -m mfup_fastapi"""
2
+
3
+ import os
4
+
5
+ import uvicorn
6
+
7
+ if __name__ == "__main__":
8
+ uvicorn.run(
9
+ "mfup_fastapi.app:app",
10
+ host=os.environ.get("MFUP_HOST", "0.0.0.0"),
11
+ port=int(os.environ.get("MFUP_PORT", "8070")),
12
+ log_level="info",
13
+ )
@@ -0,0 +1,18 @@
1
+ """Standalone MFUP/2 server — config from environment.
2
+
3
+ uvicorn mfup_fastapi.app:app
4
+
5
+ This module is the ONLY place that builds an app at import time
6
+ (``app = create_app()`` for uvicorn's module:attr convention). Library
7
+ consumers use MfupEngine/MfupConfig directly and never import this module.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+
14
+ from . import create_app
15
+
16
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
17
+
18
+ app = create_app()
@@ -0,0 +1,78 @@
1
+ """MfupConfig — every knob of the FastAPI integration in one dataclass.
2
+
3
+ Two construction paths:
4
+ - ``MfupConfig(...)`` — library embedding, explicit values,
5
+ hooks as CALLABLES;
6
+ - ``MfupConfig.from_env()`` — standalone/container deployment, values
7
+ from MFUP_* env vars, hooks as dotted
8
+ paths ("pkg.module:callable").
9
+
10
+ No environment variable is read at import time anywhere in this package —
11
+ only from_env() touches os.environ.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Mapping, Optional, Union
20
+
21
+ #: A hook is either the callable itself or a dotted import path.
22
+ HookRef = Union[str, Callable[..., Any]]
23
+
24
+
25
+ @dataclass
26
+ class MfupConfig:
27
+ #: Global base directory for staging + publish (per-session base_dir from
28
+ #: the authorize hook overrides it per session).
29
+ base_dir: Path = Path("/tmp/mfup-uploads")
30
+ redis_url: str = "redis://redis:6379/0"
31
+ session_resume_ttl: int = 3600
32
+ leg_idle_timeout: int = 60
33
+ max_chunk_bytes: int = 262144
34
+ max_open_files: int = 1
35
+ max_pending_files: int = 64
36
+ sweep_interval: int = 300
37
+ staging_prefix: str = ".incoming"
38
+ #: Run the filesystem-orphan reconciliation every Nth sweep.
39
+ reconcile_every: int = 4
40
+ #: Minimum age before a staging dir may be reconciled as an orphan.
41
+ orphan_grace_seconds: int = 600
42
+ #: Buffered-body threshold for atomic batch POSTs.
43
+ max_buffered_body: int = 16 * 1024 * 1024
44
+ #: Cap on the JSON size of HELLO.meta.
45
+ max_meta_bytes: int = 16384
46
+ #: Bearer token for admin/debug routes. Empty → routes disabled.
47
+ admin_token: str = ""
48
+ #: Consumer hooks — callables or dotted paths (see mfup_core.hooks).
49
+ authorize: Optional[HookRef] = None
50
+ map_file: Optional[HookRef] = None
51
+ on_committed: Optional[HookRef] = None
52
+
53
+ @classmethod
54
+ def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "MfupConfig":
55
+ e: Mapping[str, str] = os.environ if env is None else env
56
+
57
+ def _int(name: str, default: int) -> int:
58
+ return int(e.get(name, str(default)))
59
+
60
+ return cls(
61
+ base_dir=Path(e.get("MFUP_BASE_DIR", "/tmp/mfup-uploads")),
62
+ redis_url=e.get("REDIS_URL", "redis://redis:6379/0"),
63
+ session_resume_ttl=_int("MFUP_SESSION_RESUME_TTL", 3600),
64
+ leg_idle_timeout=_int("MFUP_LEG_IDLE_TIMEOUT", 60),
65
+ max_chunk_bytes=_int("MFUP_MAX_CHUNK_BYTES", 262144),
66
+ max_open_files=_int("MFUP_MAX_OPEN_FILES", 1),
67
+ max_pending_files=_int("MFUP_MAX_PENDING_FILES", 64),
68
+ sweep_interval=_int("MFUP_SWEEP_INTERVAL", 300),
69
+ staging_prefix=e.get("MFUP_STAGING_PREFIX", ".incoming"),
70
+ reconcile_every=_int("MFUP_RECONCILE_EVERY", 4),
71
+ orphan_grace_seconds=_int("MFUP_ORPHAN_GRACE", 600),
72
+ max_buffered_body=_int("MFUP_MAX_BUFFERED_BODY", 16 * 1024 * 1024),
73
+ max_meta_bytes=_int("MFUP_MAX_META_BYTES", 16384),
74
+ admin_token=e.get("MFUP_ADMIN_TOKEN", ""),
75
+ authorize=e.get("MFUP_AUTHORIZE") or None,
76
+ map_file=e.get("MFUP_MAP_FILE") or None,
77
+ on_committed=e.get("MFUP_ON_COMMITTED") or None,
78
+ )