tuspyserver 2.0.0__tar.gz → 2.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,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: tuspyserver
3
+ Version: 2.2.0
4
+ Summary: TUS py protocol implementation in FastAPI
5
+ Home-page: https://github.com/edihasaj/tuspy-fast-api
6
+ Author: Edi Hasaj
7
+ Author-email: edi.hasaj@applifyer.com
8
+ License: MIT
9
+ Platform: any
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: fastapi>=0.110.0
19
+ Requires-Dist: pydantic>=2.6.2
20
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: license
27
+ Dynamic: license-file
28
+ Dynamic: platform
29
+ Dynamic: requires-dist
30
+ Dynamic: summary
31
+
32
+ # FastAPI Tus
33
+
34
+ A FastAPI extension implementing the [Tus.io](https://tus.io/) upload protocol, with optional dependency-injected hooks for post-upload processing.
35
+
36
+ ## Prerequisites
37
+
38
+ * **Python** 3.8+
39
+ * **FastAPI** 0.70+
40
+ * **Starlette** (installed as FastAPI dependency)
41
+
42
+ ## Installation
43
+
44
+ Install the latest stable release from PyPI:
45
+
46
+ ```bash
47
+ pip install tuspyserver
48
+ ```
49
+
50
+ Or install directly from source:
51
+
52
+ ```bash
53
+ git clone https://github.com/your-org/tuspyserver.git
54
+ cd tuspyserver
55
+ pip install .
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ ### Basic setup
61
+
62
+ In your `main.py`:
63
+
64
+ ```python
65
+ from fastapi import FastAPI
66
+ from starlette.middleware.cors import CORSMiddleware
67
+ from starlette.staticfiles import StaticFiles
68
+
69
+ from tusserver.tus import create_api_router
70
+
71
+ app = FastAPI()
72
+ app.add_middleware(
73
+ CORSMiddleware,
74
+ allow_origins=["*"],
75
+ allow_methods=["*"],
76
+ allow_headers=["*"],
77
+ )
78
+ app.mount("/static", StaticFiles(directory="static"), name="static")
79
+
80
+ # Optional: define a simple completion hook
81
+ def on_upload_complete(file_path: str, metadata: dict):
82
+ print("Upload complete:", file_path)
83
+ print("Metadata:", metadata)
84
+
85
+ # Include the router
86
+ app.include_router(
87
+ create_api_router(
88
+ files_dir="/tmp/uploads", # OPTIONAL: directory to store files
89
+ max_size=128_849_018_880, # OPTIONAL: max upload size in bytes (~120GB)
90
+ on_upload_complete=on_upload_complete, # OPTIONAL: callback when upload finishes
91
+ prefix="files", # OPTIONAL: URL prefix (default: 'files')
92
+ ),
93
+ )
94
+ ```
95
+
96
+ ### Dependency‑Injected Hook (Advanced)
97
+
98
+ For applications using FastAPI's dependency injection, you can supply a factory that returns a DI‑enabled callback. This factory can `Depends()` on any of your services (database session, current user, etc.).
99
+
100
+ ```python
101
+ # Define a factory dependency that injects your own services
102
+ from fastapi import Depends
103
+ from your_app.dependencies import get_db, get_current_user
104
+
105
+ def get_upload_handler(
106
+ db=Depends(get_db),
107
+ current_user=Depends(get_current_user),
108
+ ) -> Callable[[str, dict], None]:
109
+ def handler(file_path: str, metadata: dict):
110
+ # perform validation or post-processing
111
+ db.log_upload(current_user.id, metadata)
112
+ process_file(file_path)
113
+ return handler
114
+
115
+ # Include router with the DI hook
116
+ app.include_router(
117
+ create_api_router(
118
+ on_upload_complete=None, # keep default
119
+ upload_complete_dep=get_upload_handler, # factory dependency
120
+ )
121
+ )
122
+ ```
123
+
124
+ ### Features
125
+
126
+ * **Resumable uploads** via TUS protocol
127
+ * **Chunked transfer** with configurable max size
128
+ * **Metadata storage** (filename, filetype)
129
+ * **Expiration & cleanup** of old uploads (default retention: 5 days)
130
+ * **Download**, **HEAD**, **DELETE**, and **OPTIONS** endpoints
131
+ * **Optional** DI‑friendly `upload_complete_dep` for seamless integration with FastAPI
132
+
133
+ ## Scheduler (Cleanup)
134
+
135
+ Expired files are removed when `remove_expired_files()` is called. You can schedule it using your preferred background scheduler (e.g., `APScheduler`, `cron`).
136
+
137
+ ```python
138
+ from apscheduler.schedulers.background import BackgroundScheduler
139
+
140
+ scheduler = BackgroundScheduler()
141
+ scheduler.add_job(
142
+ lambda: router.remove_expired_files(),
143
+ trigger='cron',
144
+ hour=1,
145
+ )
146
+ scheduler.start()
147
+ ```
148
+
149
+ ## Versioning & Publishing
150
+
151
+ **Before releasing:**
152
+
153
+ 1. Update the version in `setup.py`:
154
+
155
+ * **Patch** for bug fixes
156
+ * **Minor** for new features
157
+ * **Major** for breaking changes
158
+ 2. Commit and push to `main`.
159
+ 3. Open a PR to merge `main` → `production`.
160
+ 4. Upon merge, CI/CD will publish to PyPI.
161
+
162
+ ## Contributing
163
+
164
+ Contributions welcome! Please open issues or PRs on [GitHub](https://github.com/your-org/tuspyserver).
165
+
166
+ *© 2025 Edi Hasaj [X](https://x.com/hasajedi)*
@@ -0,0 +1,135 @@
1
+ # FastAPI Tus
2
+
3
+ A FastAPI extension implementing the [Tus.io](https://tus.io/) upload protocol, with optional dependency-injected hooks for post-upload processing.
4
+
5
+ ## Prerequisites
6
+
7
+ * **Python** 3.8+
8
+ * **FastAPI** 0.70+
9
+ * **Starlette** (installed as FastAPI dependency)
10
+
11
+ ## Installation
12
+
13
+ Install the latest stable release from PyPI:
14
+
15
+ ```bash
16
+ pip install tuspyserver
17
+ ```
18
+
19
+ Or install directly from source:
20
+
21
+ ```bash
22
+ git clone https://github.com/your-org/tuspyserver.git
23
+ cd tuspyserver
24
+ pip install .
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Basic setup
30
+
31
+ In your `main.py`:
32
+
33
+ ```python
34
+ from fastapi import FastAPI
35
+ from starlette.middleware.cors import CORSMiddleware
36
+ from starlette.staticfiles import StaticFiles
37
+
38
+ from tusserver.tus import create_api_router
39
+
40
+ app = FastAPI()
41
+ app.add_middleware(
42
+ CORSMiddleware,
43
+ allow_origins=["*"],
44
+ allow_methods=["*"],
45
+ allow_headers=["*"],
46
+ )
47
+ app.mount("/static", StaticFiles(directory="static"), name="static")
48
+
49
+ # Optional: define a simple completion hook
50
+ def on_upload_complete(file_path: str, metadata: dict):
51
+ print("Upload complete:", file_path)
52
+ print("Metadata:", metadata)
53
+
54
+ # Include the router
55
+ app.include_router(
56
+ create_api_router(
57
+ files_dir="/tmp/uploads", # OPTIONAL: directory to store files
58
+ max_size=128_849_018_880, # OPTIONAL: max upload size in bytes (~120GB)
59
+ on_upload_complete=on_upload_complete, # OPTIONAL: callback when upload finishes
60
+ prefix="files", # OPTIONAL: URL prefix (default: 'files')
61
+ ),
62
+ )
63
+ ```
64
+
65
+ ### Dependency‑Injected Hook (Advanced)
66
+
67
+ For applications using FastAPI's dependency injection, you can supply a factory that returns a DI‑enabled callback. This factory can `Depends()` on any of your services (database session, current user, etc.).
68
+
69
+ ```python
70
+ # Define a factory dependency that injects your own services
71
+ from fastapi import Depends
72
+ from your_app.dependencies import get_db, get_current_user
73
+
74
+ def get_upload_handler(
75
+ db=Depends(get_db),
76
+ current_user=Depends(get_current_user),
77
+ ) -> Callable[[str, dict], None]:
78
+ def handler(file_path: str, metadata: dict):
79
+ # perform validation or post-processing
80
+ db.log_upload(current_user.id, metadata)
81
+ process_file(file_path)
82
+ return handler
83
+
84
+ # Include router with the DI hook
85
+ app.include_router(
86
+ create_api_router(
87
+ on_upload_complete=None, # keep default
88
+ upload_complete_dep=get_upload_handler, # factory dependency
89
+ )
90
+ )
91
+ ```
92
+
93
+ ### Features
94
+
95
+ * **Resumable uploads** via TUS protocol
96
+ * **Chunked transfer** with configurable max size
97
+ * **Metadata storage** (filename, filetype)
98
+ * **Expiration & cleanup** of old uploads (default retention: 5 days)
99
+ * **Download**, **HEAD**, **DELETE**, and **OPTIONS** endpoints
100
+ * **Optional** DI‑friendly `upload_complete_dep` for seamless integration with FastAPI
101
+
102
+ ## Scheduler (Cleanup)
103
+
104
+ Expired files are removed when `remove_expired_files()` is called. You can schedule it using your preferred background scheduler (e.g., `APScheduler`, `cron`).
105
+
106
+ ```python
107
+ from apscheduler.schedulers.background import BackgroundScheduler
108
+
109
+ scheduler = BackgroundScheduler()
110
+ scheduler.add_job(
111
+ lambda: router.remove_expired_files(),
112
+ trigger='cron',
113
+ hour=1,
114
+ )
115
+ scheduler.start()
116
+ ```
117
+
118
+ ## Versioning & Publishing
119
+
120
+ **Before releasing:**
121
+
122
+ 1. Update the version in `setup.py`:
123
+
124
+ * **Patch** for bug fixes
125
+ * **Minor** for new features
126
+ * **Major** for breaking changes
127
+ 2. Commit and push to `main`.
128
+ 3. Open a PR to merge `main` → `production`.
129
+ 4. Upon merge, CI/CD will publish to PyPI.
130
+
131
+ ## Contributing
132
+
133
+ Contributions welcome! Please open issues or PRs on [GitHub](https://github.com/your-org/tuspyserver).
134
+
135
+ *© 2025 Edi Hasaj [X](https://x.com/hasajedi)*
@@ -0,0 +1,38 @@
1
+ """
2
+ FastAPI Tus implementation
3
+ -------------
4
+ Implements the tus.io server-side file-upload protocol
5
+ visit https://tus.io for more information
6
+ """
7
+
8
+ from setuptools import find_packages, setup
9
+
10
+ with open("README.md", "r") as f:
11
+ long_description = f.read()
12
+
13
+ setup(
14
+ name="tuspyserver",
15
+ version="2.2.0",
16
+ description="TUS py protocol implementation in FastAPI",
17
+ long_description=long_description,
18
+ long_description_content_type="text/markdown",
19
+ author="Edi Hasaj",
20
+ license="MIT",
21
+ author_email="edi.hasaj@applifyer.com",
22
+ url="https://github.com/edihasaj/tuspy-fast-api",
23
+ packages=find_packages(),
24
+ platforms="any",
25
+ include_package_data=True,
26
+ install_requires=[
27
+ "fastapi>=0.110.0",
28
+ "pydantic>=2.6.2",
29
+ ],
30
+ classifiers=[
31
+ "Environment :: Web Environment",
32
+ "Intended Audience :: Developers",
33
+ "License :: OSI Approved :: MIT License",
34
+ "Operating System :: OS Independent",
35
+ "Programming Language :: Python",
36
+ "Topic :: Software Development :: Libraries :: Python Modules",
37
+ ],
38
+ )
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: tuspyserver
3
+ Version: 2.2.0
4
+ Summary: TUS py protocol implementation in FastAPI
5
+ Home-page: https://github.com/edihasaj/tuspy-fast-api
6
+ Author: Edi Hasaj
7
+ Author-email: edi.hasaj@applifyer.com
8
+ License: MIT
9
+ Platform: any
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: fastapi>=0.110.0
19
+ Requires-Dist: pydantic>=2.6.2
20
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: license
27
+ Dynamic: license-file
28
+ Dynamic: platform
29
+ Dynamic: requires-dist
30
+ Dynamic: summary
31
+
32
+ # FastAPI Tus
33
+
34
+ A FastAPI extension implementing the [Tus.io](https://tus.io/) upload protocol, with optional dependency-injected hooks for post-upload processing.
35
+
36
+ ## Prerequisites
37
+
38
+ * **Python** 3.8+
39
+ * **FastAPI** 0.70+
40
+ * **Starlette** (installed as FastAPI dependency)
41
+
42
+ ## Installation
43
+
44
+ Install the latest stable release from PyPI:
45
+
46
+ ```bash
47
+ pip install tuspyserver
48
+ ```
49
+
50
+ Or install directly from source:
51
+
52
+ ```bash
53
+ git clone https://github.com/your-org/tuspyserver.git
54
+ cd tuspyserver
55
+ pip install .
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ ### Basic setup
61
+
62
+ In your `main.py`:
63
+
64
+ ```python
65
+ from fastapi import FastAPI
66
+ from starlette.middleware.cors import CORSMiddleware
67
+ from starlette.staticfiles import StaticFiles
68
+
69
+ from tusserver.tus import create_api_router
70
+
71
+ app = FastAPI()
72
+ app.add_middleware(
73
+ CORSMiddleware,
74
+ allow_origins=["*"],
75
+ allow_methods=["*"],
76
+ allow_headers=["*"],
77
+ )
78
+ app.mount("/static", StaticFiles(directory="static"), name="static")
79
+
80
+ # Optional: define a simple completion hook
81
+ def on_upload_complete(file_path: str, metadata: dict):
82
+ print("Upload complete:", file_path)
83
+ print("Metadata:", metadata)
84
+
85
+ # Include the router
86
+ app.include_router(
87
+ create_api_router(
88
+ files_dir="/tmp/uploads", # OPTIONAL: directory to store files
89
+ max_size=128_849_018_880, # OPTIONAL: max upload size in bytes (~120GB)
90
+ on_upload_complete=on_upload_complete, # OPTIONAL: callback when upload finishes
91
+ prefix="files", # OPTIONAL: URL prefix (default: 'files')
92
+ ),
93
+ )
94
+ ```
95
+
96
+ ### Dependency‑Injected Hook (Advanced)
97
+
98
+ For applications using FastAPI's dependency injection, you can supply a factory that returns a DI‑enabled callback. This factory can `Depends()` on any of your services (database session, current user, etc.).
99
+
100
+ ```python
101
+ # Define a factory dependency that injects your own services
102
+ from fastapi import Depends
103
+ from your_app.dependencies import get_db, get_current_user
104
+
105
+ def get_upload_handler(
106
+ db=Depends(get_db),
107
+ current_user=Depends(get_current_user),
108
+ ) -> Callable[[str, dict], None]:
109
+ def handler(file_path: str, metadata: dict):
110
+ # perform validation or post-processing
111
+ db.log_upload(current_user.id, metadata)
112
+ process_file(file_path)
113
+ return handler
114
+
115
+ # Include router with the DI hook
116
+ app.include_router(
117
+ create_api_router(
118
+ on_upload_complete=None, # keep default
119
+ upload_complete_dep=get_upload_handler, # factory dependency
120
+ )
121
+ )
122
+ ```
123
+
124
+ ### Features
125
+
126
+ * **Resumable uploads** via TUS protocol
127
+ * **Chunked transfer** with configurable max size
128
+ * **Metadata storage** (filename, filetype)
129
+ * **Expiration & cleanup** of old uploads (default retention: 5 days)
130
+ * **Download**, **HEAD**, **DELETE**, and **OPTIONS** endpoints
131
+ * **Optional** DI‑friendly `upload_complete_dep` for seamless integration with FastAPI
132
+
133
+ ## Scheduler (Cleanup)
134
+
135
+ Expired files are removed when `remove_expired_files()` is called. You can schedule it using your preferred background scheduler (e.g., `APScheduler`, `cron`).
136
+
137
+ ```python
138
+ from apscheduler.schedulers.background import BackgroundScheduler
139
+
140
+ scheduler = BackgroundScheduler()
141
+ scheduler.add_job(
142
+ lambda: router.remove_expired_files(),
143
+ trigger='cron',
144
+ hour=1,
145
+ )
146
+ scheduler.start()
147
+ ```
148
+
149
+ ## Versioning & Publishing
150
+
151
+ **Before releasing:**
152
+
153
+ 1. Update the version in `setup.py`:
154
+
155
+ * **Patch** for bug fixes
156
+ * **Minor** for new features
157
+ * **Major** for breaking changes
158
+ 2. Commit and push to `main`.
159
+ 3. Open a PR to merge `main` → `production`.
160
+ 4. Upon merge, CI/CD will publish to PyPI.
161
+
162
+ ## Contributing
163
+
164
+ Contributions welcome! Please open issues or PRs on [GitHub](https://github.com/your-org/tuspyserver).
165
+
166
+ *© 2025 Edi Hasaj [X](https://x.com/hasajedi)*
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -16,13 +16,13 @@ class FileMetadata(BaseModel):
16
16
 
17
17
  @classmethod
18
18
  def from_request(
19
- cls,
20
- uid: str,
21
- metadata: dict[Any, str],
22
- size: int,
23
- created_at: str,
24
- defer_length: bool,
25
- expires: str
19
+ cls,
20
+ uid: str,
21
+ metadata: dict[Any, str],
22
+ size: int,
23
+ created_at: str,
24
+ defer_length: bool,
25
+ expires: str,
26
26
  ):
27
27
  return FileMetadata(
28
28
  uid=uid,
@@ -30,5 +30,5 @@ class FileMetadata(BaseModel):
30
30
  size=size,
31
31
  created_at=created_at,
32
32
  defer_length=defer_length,
33
- expires=expires
33
+ expires=expires,
34
34
  )
@@ -5,37 +5,66 @@ from datetime import datetime, timedelta
5
5
  from typing import Callable, Optional
6
6
  from uuid import uuid4
7
7
 
8
- from fastapi import Header, HTTPException, Response, Request, status, Depends, Path, APIRouter
8
+ from fastapi import (
9
+ APIRouter,
10
+ Depends,
11
+ Header,
12
+ HTTPException,
13
+ Path,
14
+ Request,
15
+ Response,
16
+ status,
17
+ )
9
18
  from starlette.responses import FileResponse
10
19
 
11
20
  from tusserver.metadata import FileMetadata
12
21
 
22
+
13
23
  def default_auth():
14
- pass
24
+ pass
15
25
 
16
26
 
17
27
  def create_api_router(
18
- files_dir='/tmp/files',
19
- location='http://127.0.0.1:8000/files',
20
- max_size=128849018880,
21
- on_upload_complete: Optional[Callable[[str, dict], None]] = None,
22
- auth: Optional[Callable[[], None]] = default_auth,
23
- days_to_keep: int = 5,
28
+ files_dir="/tmp/files",
29
+ max_size=128849018880,
30
+ on_upload_complete: Optional[Callable[[str, dict], None]] = None,
31
+ auth: Optional[Callable[[], None]] = default_auth,
32
+ days_to_keep: int = 5,
33
+ prefix: str = "files",
34
+ upload_complete_dep: Optional[Callable[..., Callable[[str, dict], None]]] = None,
24
35
  ):
25
- router = APIRouter()
36
+ if prefix and prefix[0] == "/":
37
+ prefix = prefix[1:]
38
+ router = APIRouter(prefix=f"/{prefix}")
39
+
40
+ tus_version = "1.0.0"
41
+ tus_extension = (
42
+ "creation,creation-defer-length,creation-with-upload,expiration,termination"
43
+ )
44
+
45
+ if upload_complete_dep is None:
26
46
 
27
- tus_version = '1.0.0'
28
- tus_extension = 'creation,creation-defer-length,creation-with-upload,expiration,termination'
47
+ async def _fallback_on_complete_dep() -> Callable[[str, dict], None]:
48
+ return on_upload_complete or (lambda *_: None)
29
49
 
30
- async def _get_request_chunk(request: Request, uuid: str = Path(...), post_request: bool = False) -> bool | None:
50
+ upload_complete_dep = _fallback_on_complete_dep
51
+
52
+ async def _get_request_chunk(
53
+ request: Request, uuid: str = Path(...), post_request: bool = False
54
+ ) -> bool | None:
31
55
  meta = _read_metadata(uuid)
32
56
  if not meta or not _file_exists(uuid):
33
57
  return False
34
58
 
59
+ # Flag to track if we processed any chunks
60
+ has_chunks = False
61
+
35
62
  with open(f"{files_dir}/{uuid}", "ab") as f:
36
63
  async for chunk in request.stream():
37
- if post_request and chunk is None or len(chunk) == 0:
38
- return None
64
+ has_chunks = True
65
+ # Skip empty chunks but continue processing
66
+ if len(chunk) == 0:
67
+ continue
39
68
 
40
69
  if _get_file_length(uuid) + len(chunk) > max_size:
41
70
  raise HTTPException(status_code=413)
@@ -48,6 +77,15 @@ def create_api_router(
48
77
 
49
78
  f.close()
50
79
 
80
+ # For empty files in a POST request, we still want to return True
81
+ # to ensure _get_and_save_the_file gets called
82
+ if post_request and not has_chunks:
83
+ # Update metadata for empty file
84
+ meta.offset = 0
85
+ meta.upload_chunk_size = 0
86
+ meta.upload_part += 1
87
+ _write_metadata(meta)
88
+
51
89
  return True
52
90
 
53
91
  @router.head("/{uuid}", status_code=status.HTTP_200_OK)
@@ -61,31 +99,36 @@ def create_api_router(
61
99
  response.headers["Upload-Length"] = str(meta.size)
62
100
  response.headers["Upload-Offset"] = str(meta.offset)
63
101
  response.headers["Cache-Control"] = "no-store"
64
- response.headers[
65
- "Upload-Metadata"] = f"filename {base64.b64encode(bytes(meta.metadata['name'], 'utf-8'))}, " \
66
- f"filetype {base64.b64encode(bytes(meta.metadata['type'], 'utf-8'))}"
102
+ response.headers["Upload-Metadata"] = (
103
+ f"filename {base64.b64encode(bytes(meta.metadata['name'], 'utf-8'))}, "
104
+ f"filetype {base64.b64encode(bytes(meta.metadata['type'], 'utf-8'))}"
105
+ )
67
106
  response.status_code = status.HTTP_200_OK
68
107
  return response
69
108
 
70
109
  @router.patch("/{uuid}", status_code=status.HTTP_204_NO_CONTENT)
71
110
  def upload_chunk(
72
- response: Response,
73
- uuid: str,
74
- content_type: str = Header(None),
75
- content_length: int = Header(None),
76
- upload_offset: int = Header(None),
77
- _=Depends(_get_request_chunk),
78
- __=Depends(auth)
111
+ response: Response,
112
+ uuid: str,
113
+ content_length: int = Header(None),
114
+ upload_offset: int = Header(None),
115
+ _=Depends(_get_request_chunk),
116
+ __=Depends(auth),
117
+ on_complete: Callable[[str, dict], None] = Depends(upload_complete_dep),
79
118
  ) -> Response:
80
- response_headers = _get_and_save_the_file(
119
+ headers = _get_and_save_the_file(
81
120
  response,
82
121
  uuid,
83
- content_type,
84
122
  content_length,
85
- upload_offset,
123
+ upload_length=upload_offset,
86
124
  )
87
125
 
88
- return response_headers
126
+ meta = _read_metadata(uuid)
127
+ if meta and meta.size == meta.offset:
128
+ file_path = os.path.join(files_dir, uuid)
129
+ on_complete(file_path, meta.metadata)
130
+
131
+ return headers
89
132
 
90
133
  @router.options("/", status_code=status.HTTP_204_NO_CONTENT)
91
134
  def options_create_upload(response: Response, __=Depends(auth)) -> Response:
@@ -99,12 +142,13 @@ def create_api_router(
99
142
 
100
143
  @router.post("/", status_code=status.HTTP_201_CREATED)
101
144
  async def create_upload(
102
- request: Request,
103
- response: Response,
104
- upload_metadata: str = Header(None),
105
- upload_length: int = Header(None),
106
- upload_defer_length: int = Header(None),
107
- _=Depends(auth)
145
+ request: Request,
146
+ response: Response,
147
+ upload_metadata: str = Header(None),
148
+ upload_length: int = Header(None),
149
+ upload_defer_length: int = Header(None),
150
+ _=Depends(auth),
151
+ on_complete: Callable[[str, dict], None] = Depends(upload_complete_dep),
108
152
  ) -> Response:
109
153
  if upload_defer_length is not None and upload_defer_length != 1:
110
154
  raise HTTPException(status_code=400, detail="Invalid Upload-Defer-Length")
@@ -113,7 +157,7 @@ def create_api_router(
113
157
 
114
158
  # Create a new upload and store the file and metadata in the mapping
115
159
  metadata = {}
116
- if upload_metadata is not None and upload_metadata != '':
160
+ if upload_metadata is not None and upload_metadata != "":
117
161
  # Decode the base64-encoded string
118
162
  for kv in upload_metadata.split(","):
119
163
  key, value = kv.rsplit(" ", 1)
@@ -132,26 +176,24 @@ def create_api_router(
132
176
  str(date_expiry.isoformat()),
133
177
  )
134
178
  _write_metadata(saved_meta_data)
135
-
136
179
  _initialize_file(uuid)
137
180
 
138
- chunk: bool | None = await _get_request_chunk(request, uuid, True)
139
- if chunk:
140
- response = _get_and_save_the_file(
141
- response,
142
- uuid,
143
- )
144
- response.headers["Location"] = f"{location}/{uuid}"
145
- return response
146
-
147
- response.headers["Location"] = f"{location}/{uuid}"
181
+ response.headers["Location"] = _build_location_url(request=request, uuid=uuid)
148
182
  response.headers["Tus-Resumable"] = tus_version
149
183
  response.headers["Content-Length"] = str(0)
150
184
  response.status_code = status.HTTP_201_CREATED
185
+
186
+ meta = _read_metadata(uuid)
187
+ if meta and meta.size == 0:
188
+ file_path = os.path.join(files_dir, uuid)
189
+ on_complete(file_path, meta.metadata)
190
+
151
191
  return response
152
192
 
153
193
  @router.options("/{uuid}", status_code=status.HTTP_204_NO_CONTENT)
154
- def options_upload_chunk(response: Response, uuid: str, _=Depends(auth)) -> Response:
194
+ def options_upload_chunk(
195
+ response: Response, uuid: str, _=Depends(auth)
196
+ ) -> Response:
155
197
  meta = _read_metadata(uuid)
156
198
  if meta is None or not _file_exists(uuid):
157
199
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@@ -176,10 +218,7 @@ def create_api_router(
176
218
  os.path.join(files_dir, uuid),
177
219
  media_type="application/octet-stream",
178
220
  filename=meta.metadata["name"],
179
- headers={
180
- "Content-Length": str(meta.offset),
181
- "Tus-Resumable": tus_version
182
- }
221
+ headers={"Content-Length": str(meta.offset), "Tus-Resumable": tus_version},
183
222
  )
184
223
 
185
224
  @router.delete("/{uuid}", status_code=status.HTTP_204_NO_CONTENT)
@@ -202,19 +241,19 @@ def create_api_router(
202
241
  if not os.path.exists(files_dir):
203
242
  os.makedirs(files_dir)
204
243
 
205
- with open(os.path.join(files_dir, f'{meta.uid}.info'), 'w') as f:
244
+ with open(os.path.join(files_dir, f"{meta.uid}.info"), "w") as f:
206
245
  f.write(json.dumps(meta, indent=4, default=lambda k: k.__dict__))
207
246
 
208
247
  def _initialize_file(uid: str) -> None:
209
248
  if not os.path.exists(files_dir):
210
249
  os.makedirs(files_dir)
211
250
 
212
- open(os.path.join(files_dir, f'{uid}'), 'a').close()
251
+ open(os.path.join(files_dir, f"{uid}"), "a").close()
213
252
 
214
253
  def _read_metadata(uid: str) -> FileMetadata | None:
215
- fpath = os.path.join(files_dir, f'{uid}.info')
254
+ fpath = os.path.join(files_dir, f"{uid}.info")
216
255
  if os.path.exists(fpath):
217
- with open(fpath, 'r') as f:
256
+ with open(fpath, "r") as f:
218
257
  return FileMetadata(**json.load(f))
219
258
 
220
259
  return None
@@ -222,7 +261,7 @@ def create_api_router(
222
261
  def _get_file(uid: str) -> bytes | None:
223
262
  fpath = os.path.join(files_dir, uid)
224
263
  if os.path.exists(fpath):
225
- with open(fpath, 'rb') as f:
264
+ with open(fpath, "rb") as f:
226
265
  return f.read()
227
266
 
228
267
  return None
@@ -243,16 +282,11 @@ def create_api_router(
243
282
  os.remove(meta_path)
244
283
 
245
284
  def _get_and_save_the_file(
246
- response: Response,
247
- uuid: str,
248
- content_type: str = Header(None),
249
- content_length: int = Header(None),
250
- upload_length: int = Header(None),
285
+ response: Response,
286
+ uuid: str,
287
+ content_length: int = Header(None),
288
+ upload_length: int = Header(None),
251
289
  ):
252
- # Check if the Content-Type header is set to "application/offset+octet-stream"
253
- if content_type != "application/offset+octet-stream":
254
- raise HTTPException(status_code=415)
255
-
256
290
  meta = _read_metadata(uuid)
257
291
  # Check if the upload ID is valid
258
292
  if not meta or uuid != meta.uid:
@@ -272,11 +306,13 @@ def create_api_router(
272
306
 
273
307
  if meta.size == meta.offset:
274
308
  response.headers["Tus-Resumable"] = tus_version
275
- response.headers["Upload-Offset"] = str(str(meta.offset) if meta.offset > 0 else str(content_length))
309
+ response.headers["Upload-Offset"] = str(
310
+ str(meta.offset) if meta.offset > 0 else str(content_length)
311
+ )
276
312
  response.headers["Upload-Expires"] = str(meta.expires)
277
313
  response.status_code = status.HTTP_204_NO_CONTENT
278
314
  if on_upload_complete:
279
- on_upload_complete(os.path.join(files_dir, f'{uuid}'), meta.metadata)
315
+ on_upload_complete(os.path.join(files_dir, f"{uuid}"), meta.metadata)
280
316
 
281
317
  return response
282
318
 
@@ -300,6 +336,19 @@ def create_api_router(
300
336
  if meta.expires and datetime.fromisoformat(meta.expires) < datetime.now():
301
337
  _delete_files(f)
302
338
 
339
+ def _get_host_and_proto(request: Request) -> tuple:
340
+ proto = "http"
341
+ host = request.headers.get("host")
342
+ if request.headers.get("X-Forwarded-Proto") is not None:
343
+ proto = request.headers.get("X-Forwarded-Proto")
344
+ if request.headers.get("X-Forwarded-Host") is not None:
345
+ host = request.headers.get("X-Forwarded-Host")
346
+ return proto, host
347
+
348
+ def _build_location_url(request: Request, uuid: str) -> str:
349
+ proto, host = _get_host_and_proto(request=request)
350
+ return f"{proto}://{host}/{prefix}/{uuid}"
351
+
303
352
  # # Create a scheduler for deleting the files
304
353
  # scheduler = sched.scheduler(time.time, time.sleep)
305
354
  # run_time = time.mktime(time.strptime("01:00", "%H:%M"))
@@ -1,78 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: tuspyserver
3
- Version: 2.0.0
4
- Summary: TUS py protocol implementation in FastAPI
5
- Home-page: https://github.com/edihasaj/tuspy-fast-api
6
- Author: Edi Hasaj
7
- Author-email: edihasaj@outlook.com
8
- License: MIT
9
- Platform: any
10
- Classifier: Environment :: Web Environment
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python
15
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
- Description-Content-Type: text/markdown
17
- License-File: LICENSE
18
- Requires-Dist: fastapi>=0.110.0
19
- Requires-Dist: pydantic>=2.6.2
20
-
21
- # FastAPI Tus
22
-
23
- FastAPI Extension implementing the Tus.io server protocol
24
-
25
- ### Prerequisites `FastAPI`
26
-
27
- ## Installation
28
-
29
- Installation from PyPi repository (recommended for latest stable release)
30
-
31
- ```
32
- pip install tuspyserver
33
- ```
34
-
35
- ## Usage
36
-
37
- ### main.py
38
-
39
- ```python
40
- from fastapi import FastAPI
41
- from starlette.middleware.cors import CORSMiddleware
42
- from starlette.staticfiles import StaticFiles
43
-
44
- from tusserver.tus import create_api_router
45
-
46
- app = FastAPI()
47
- app.add_middleware(
48
- CORSMiddleware,
49
- allow_origins=['*'],
50
- allow_methods=["*"],
51
- allow_headers=["*"],
52
- )
53
- app.mount("/static", StaticFiles(directory="static"), name="static")
54
-
55
-
56
- def on_upload_complete(file_path: str):
57
- print('Upload complete')
58
- print(file_path)
59
-
60
-
61
- app.include_router(
62
- create_api_router(
63
- files_dir='/tmp/different_dir', # OPTIONAL
64
- location='http://127.0.0.1:8000/files', # OPTIONAL
65
- max_size=128849018880, # OPTIONAL
66
- on_upload_complete=on_upload_complete # OPTIONAL
67
- ),
68
- prefix="/files"
69
- )
70
- ```
71
-
72
- This package has the ability to upload, download, delete (including a scheduler) files.
73
-
74
- ```python setup.py sdist bdist_wheel```
75
-
76
- Any contribution is welcomed.
77
-
78
- <a href="https://www.buymeacoffee.com/edihasaj" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-red.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
@@ -1,58 +0,0 @@
1
- # FastAPI Tus
2
-
3
- FastAPI Extension implementing the Tus.io server protocol
4
-
5
- ### Prerequisites `FastAPI`
6
-
7
- ## Installation
8
-
9
- Installation from PyPi repository (recommended for latest stable release)
10
-
11
- ```
12
- pip install tuspyserver
13
- ```
14
-
15
- ## Usage
16
-
17
- ### main.py
18
-
19
- ```python
20
- from fastapi import FastAPI
21
- from starlette.middleware.cors import CORSMiddleware
22
- from starlette.staticfiles import StaticFiles
23
-
24
- from tusserver.tus import create_api_router
25
-
26
- app = FastAPI()
27
- app.add_middleware(
28
- CORSMiddleware,
29
- allow_origins=['*'],
30
- allow_methods=["*"],
31
- allow_headers=["*"],
32
- )
33
- app.mount("/static", StaticFiles(directory="static"), name="static")
34
-
35
-
36
- def on_upload_complete(file_path: str):
37
- print('Upload complete')
38
- print(file_path)
39
-
40
-
41
- app.include_router(
42
- create_api_router(
43
- files_dir='/tmp/different_dir', # OPTIONAL
44
- location='http://127.0.0.1:8000/files', # OPTIONAL
45
- max_size=128849018880, # OPTIONAL
46
- on_upload_complete=on_upload_complete # OPTIONAL
47
- ),
48
- prefix="/files"
49
- )
50
- ```
51
-
52
- This package has the ability to upload, download, delete (including a scheduler) files.
53
-
54
- ```python setup.py sdist bdist_wheel```
55
-
56
- Any contribution is welcomed.
57
-
58
- <a href="https://www.buymeacoffee.com/edihasaj" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-red.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
@@ -1,38 +0,0 @@
1
- """
2
- FastAPI Tus implementation
3
- -------------
4
- Implements the tus.io server-side file-upload protocol
5
- visit https://tus.io for more information
6
- """
7
-
8
- from setuptools import setup, find_packages
9
-
10
- with open('README.md', 'r') as f:
11
- long_description = f.read()
12
-
13
- setup(
14
- name='tuspyserver',
15
- version='2.0.0',
16
- description='TUS py protocol implementation in FastAPI',
17
- long_description=long_description,
18
- long_description_content_type='text/markdown',
19
- author='Edi Hasaj',
20
- license='MIT',
21
- author_email='edihasaj@outlook.com',
22
- url='https://github.com/edihasaj/tuspy-fast-api',
23
- packages=find_packages(),
24
- platforms="any",
25
- include_package_data=True,
26
- install_requires=[
27
- 'fastapi>=0.110.0',
28
- 'pydantic>=2.6.2',
29
- ],
30
- classifiers=[
31
- 'Environment :: Web Environment',
32
- 'Intended Audience :: Developers',
33
- 'License :: OSI Approved :: MIT License',
34
- 'Operating System :: OS Independent',
35
- 'Programming Language :: Python',
36
- 'Topic :: Software Development :: Libraries :: Python Modules'
37
- ]
38
- )
@@ -1,78 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: tuspyserver
3
- Version: 2.0.0
4
- Summary: TUS py protocol implementation in FastAPI
5
- Home-page: https://github.com/edihasaj/tuspy-fast-api
6
- Author: Edi Hasaj
7
- Author-email: edihasaj@outlook.com
8
- License: MIT
9
- Platform: any
10
- Classifier: Environment :: Web Environment
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python
15
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
- Description-Content-Type: text/markdown
17
- License-File: LICENSE
18
- Requires-Dist: fastapi>=0.110.0
19
- Requires-Dist: pydantic>=2.6.2
20
-
21
- # FastAPI Tus
22
-
23
- FastAPI Extension implementing the Tus.io server protocol
24
-
25
- ### Prerequisites `FastAPI`
26
-
27
- ## Installation
28
-
29
- Installation from PyPi repository (recommended for latest stable release)
30
-
31
- ```
32
- pip install tuspyserver
33
- ```
34
-
35
- ## Usage
36
-
37
- ### main.py
38
-
39
- ```python
40
- from fastapi import FastAPI
41
- from starlette.middleware.cors import CORSMiddleware
42
- from starlette.staticfiles import StaticFiles
43
-
44
- from tusserver.tus import create_api_router
45
-
46
- app = FastAPI()
47
- app.add_middleware(
48
- CORSMiddleware,
49
- allow_origins=['*'],
50
- allow_methods=["*"],
51
- allow_headers=["*"],
52
- )
53
- app.mount("/static", StaticFiles(directory="static"), name="static")
54
-
55
-
56
- def on_upload_complete(file_path: str):
57
- print('Upload complete')
58
- print(file_path)
59
-
60
-
61
- app.include_router(
62
- create_api_router(
63
- files_dir='/tmp/different_dir', # OPTIONAL
64
- location='http://127.0.0.1:8000/files', # OPTIONAL
65
- max_size=128849018880, # OPTIONAL
66
- on_upload_complete=on_upload_complete # OPTIONAL
67
- ),
68
- prefix="/files"
69
- )
70
- ```
71
-
72
- This package has the ability to upload, download, delete (including a scheduler) files.
73
-
74
- ```python setup.py sdist bdist_wheel```
75
-
76
- Any contribution is welcomed.
77
-
78
- <a href="https://www.buymeacoffee.com/edihasaj" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-red.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
@@ -1 +0,0 @@
1
- __version__ = '1.0.0'
File without changes
File without changes