image-upload 0.1.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.
- image_upload-0.1.0/LICENSE.txt +21 -0
- image_upload-0.1.0/PKG-INFO +225 -0
- image_upload-0.1.0/README.md +211 -0
- image_upload-0.1.0/pyproject.toml +34 -0
- image_upload-0.1.0/setup.cfg +4 -0
- image_upload-0.1.0/src/image_upload/__init__.py +8 -0
- image_upload-0.1.0/src/image_upload/add_base_url.py +12 -0
- image_upload-0.1.0/src/image_upload/save_file.py +76 -0
- image_upload-0.1.0/src/image_upload.egg-info/PKG-INFO +225 -0
- image_upload-0.1.0/src/image_upload.egg-info/SOURCES.txt +11 -0
- image_upload-0.1.0/src/image_upload.egg-info/dependency_links.txt +1 -0
- image_upload-0.1.0/src/image_upload.egg-info/requires.txt +3 -0
- image_upload-0.1.0/src/image_upload.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sahil Sheoran
|
|
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,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: image-upload
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Image Upload Package for FastAPI
|
|
5
|
+
Author-email: Sahil Sheoran <sahilsheoran24@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE.txt
|
|
10
|
+
Requires-Dist: aiofiles>=25.1.0
|
|
11
|
+
Requires-Dist: fastapi>=0.138.2
|
|
12
|
+
Requires-Dist: pydantic>=2.13.4
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# image-upload
|
|
16
|
+
|
|
17
|
+
A lightweight FastAPI package for saving uploaded files and generating absolute URLs for stored images using Pydantic.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
* Simple file upload handling
|
|
22
|
+
* Automatically stores files inside an `uploads/` directory
|
|
23
|
+
* Returns the saved file path
|
|
24
|
+
* Easily generate absolute URLs with Pydantic
|
|
25
|
+
* Asynchronous file saving using `aiofiles`
|
|
26
|
+
* File size and MIME type validation
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Python 3.12+
|
|
33
|
+
- FastAPI
|
|
34
|
+
- Pydantic v2
|
|
35
|
+
- aiofiles
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Install from PyPI:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install image-upload
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Supported File Types
|
|
50
|
+
|
|
51
|
+
By default the package accepts:
|
|
52
|
+
|
|
53
|
+
- JPEG (`image/jpeg`)
|
|
54
|
+
- PNG (`image/png`)
|
|
55
|
+
- GIF (`image/gif`)
|
|
56
|
+
- PDF (`application/pdf`)
|
|
57
|
+
- Plain Text (`text/plain`)
|
|
58
|
+
|
|
59
|
+
Maximum upload size is **50 MB**.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Saving an Uploaded File
|
|
64
|
+
|
|
65
|
+
Import `get_path` and pass an `UploadFile`.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from image_upload import get_path
|
|
69
|
+
|
|
70
|
+
data.picture = await get_path(data.picture)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
or
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
data["picture"] = await get_path(data.pop("picture"))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The function returns the relative storage path.
|
|
80
|
+
|
|
81
|
+
Example output:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
uploads/0c6d8a69-9c84-47d4-81f6-d15d11fcbf88.png
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## FastAPI Example
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from fastapi import APIRouter, Depends
|
|
93
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
94
|
+
|
|
95
|
+
from image_upload import get_path
|
|
96
|
+
|
|
97
|
+
router = APIRouter()
|
|
98
|
+
|
|
99
|
+
@router.post("/")
|
|
100
|
+
async def create(
|
|
101
|
+
data: UserCreate = Depends(UserCreate.as_form),
|
|
102
|
+
db: AsyncSession = Depends(get_db),
|
|
103
|
+
):
|
|
104
|
+
data.picture = await get_path(data.picture)
|
|
105
|
+
|
|
106
|
+
return await user(db, data.model_dump())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Save Inside a Custom Folder
|
|
112
|
+
|
|
113
|
+
You can optionally provide a folder name.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
path = await get_path(file, folder="users")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Building Absolute URLs
|
|
122
|
+
|
|
123
|
+
`AbsoluteUrl` automatically prepends the application's base URL using the Pydantic validation context.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from pydantic import BaseModel
|
|
127
|
+
from image_upload import AbsoluteUrl
|
|
128
|
+
|
|
129
|
+
class UserOut(BaseModel):
|
|
130
|
+
name: str
|
|
131
|
+
picture: AbsoluteUrl
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
When validating:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
user = UserOut.model_validate(
|
|
138
|
+
data,
|
|
139
|
+
context={
|
|
140
|
+
"base_url": "https://example.com/"
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Note: Can also use "base_url": request.base_url
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from fastapi import Request
|
|
149
|
+
|
|
150
|
+
@router.get("/")
|
|
151
|
+
async def get_user(
|
|
152
|
+
request: Request,
|
|
153
|
+
db: AsyncSession = Depends(get_db),
|
|
154
|
+
) -> UserOut:
|
|
155
|
+
users = await list_user(
|
|
156
|
+
db
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return UserOut.model_validate(
|
|
160
|
+
users,
|
|
161
|
+
context={"base_url": request.base_url},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Result:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"name": "John",
|
|
171
|
+
"picture": "https://example.com/uploads/avatar.png"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## API
|
|
178
|
+
|
|
179
|
+
### `get_path(file, folder=None)`
|
|
180
|
+
|
|
181
|
+
Stores the uploaded file and returns its storage path.
|
|
182
|
+
|
|
183
|
+
**Parameters**
|
|
184
|
+
|
|
185
|
+
| Parameter | Type | Description |
|
|
186
|
+
|-----------|-------------|-------------------------|
|
|
187
|
+
| file | UploadFile | Uploaded file |
|
|
188
|
+
| folder | str | None | Optional folder prefix |
|
|
189
|
+
|
|
190
|
+
**Returns**
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
str
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
path = await get_path(upload_file)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Security
|
|
205
|
+
|
|
206
|
+
The package performs basic validation:
|
|
207
|
+
|
|
208
|
+
- Allowed MIME type validation
|
|
209
|
+
- Maximum file size validation (50 MB)
|
|
210
|
+
- UUID-based file names to avoid collisions
|
|
211
|
+
- Asynchronous streaming to reduce memory usage
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Dependencies
|
|
216
|
+
|
|
217
|
+
- FastAPI
|
|
218
|
+
- Pydantic v2
|
|
219
|
+
- aiofiles
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT License
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# image-upload
|
|
2
|
+
|
|
3
|
+
A lightweight FastAPI package for saving uploaded files and generating absolute URLs for stored images using Pydantic.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
* Simple file upload handling
|
|
8
|
+
* Automatically stores files inside an `uploads/` directory
|
|
9
|
+
* Returns the saved file path
|
|
10
|
+
* Easily generate absolute URLs with Pydantic
|
|
11
|
+
* Asynchronous file saving using `aiofiles`
|
|
12
|
+
* File size and MIME type validation
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Python 3.12+
|
|
19
|
+
- FastAPI
|
|
20
|
+
- Pydantic v2
|
|
21
|
+
- aiofiles
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Install from PyPI:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install image-upload
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Supported File Types
|
|
36
|
+
|
|
37
|
+
By default the package accepts:
|
|
38
|
+
|
|
39
|
+
- JPEG (`image/jpeg`)
|
|
40
|
+
- PNG (`image/png`)
|
|
41
|
+
- GIF (`image/gif`)
|
|
42
|
+
- PDF (`application/pdf`)
|
|
43
|
+
- Plain Text (`text/plain`)
|
|
44
|
+
|
|
45
|
+
Maximum upload size is **50 MB**.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Saving an Uploaded File
|
|
50
|
+
|
|
51
|
+
Import `get_path` and pass an `UploadFile`.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from image_upload import get_path
|
|
55
|
+
|
|
56
|
+
data.picture = await get_path(data.picture)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
or
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
data["picture"] = await get_path(data.pop("picture"))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The function returns the relative storage path.
|
|
66
|
+
|
|
67
|
+
Example output:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
uploads/0c6d8a69-9c84-47d4-81f6-d15d11fcbf88.png
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## FastAPI Example
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from fastapi import APIRouter, Depends
|
|
79
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
80
|
+
|
|
81
|
+
from image_upload import get_path
|
|
82
|
+
|
|
83
|
+
router = APIRouter()
|
|
84
|
+
|
|
85
|
+
@router.post("/")
|
|
86
|
+
async def create(
|
|
87
|
+
data: UserCreate = Depends(UserCreate.as_form),
|
|
88
|
+
db: AsyncSession = Depends(get_db),
|
|
89
|
+
):
|
|
90
|
+
data.picture = await get_path(data.picture)
|
|
91
|
+
|
|
92
|
+
return await user(db, data.model_dump())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Save Inside a Custom Folder
|
|
98
|
+
|
|
99
|
+
You can optionally provide a folder name.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
path = await get_path(file, folder="users")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Building Absolute URLs
|
|
108
|
+
|
|
109
|
+
`AbsoluteUrl` automatically prepends the application's base URL using the Pydantic validation context.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from pydantic import BaseModel
|
|
113
|
+
from image_upload import AbsoluteUrl
|
|
114
|
+
|
|
115
|
+
class UserOut(BaseModel):
|
|
116
|
+
name: str
|
|
117
|
+
picture: AbsoluteUrl
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
When validating:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
user = UserOut.model_validate(
|
|
124
|
+
data,
|
|
125
|
+
context={
|
|
126
|
+
"base_url": "https://example.com/"
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Note: Can also use "base_url": request.base_url
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from fastapi import Request
|
|
135
|
+
|
|
136
|
+
@router.get("/")
|
|
137
|
+
async def get_user(
|
|
138
|
+
request: Request,
|
|
139
|
+
db: AsyncSession = Depends(get_db),
|
|
140
|
+
) -> UserOut:
|
|
141
|
+
users = await list_user(
|
|
142
|
+
db
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return UserOut.model_validate(
|
|
146
|
+
users,
|
|
147
|
+
context={"base_url": request.base_url},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Result:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"name": "John",
|
|
157
|
+
"picture": "https://example.com/uploads/avatar.png"
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## API
|
|
164
|
+
|
|
165
|
+
### `get_path(file, folder=None)`
|
|
166
|
+
|
|
167
|
+
Stores the uploaded file and returns its storage path.
|
|
168
|
+
|
|
169
|
+
**Parameters**
|
|
170
|
+
|
|
171
|
+
| Parameter | Type | Description |
|
|
172
|
+
|-----------|-------------|-------------------------|
|
|
173
|
+
| file | UploadFile | Uploaded file |
|
|
174
|
+
| folder | str | None | Optional folder prefix |
|
|
175
|
+
|
|
176
|
+
**Returns**
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
str
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
path = await get_path(upload_file)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Security
|
|
191
|
+
|
|
192
|
+
The package performs basic validation:
|
|
193
|
+
|
|
194
|
+
- Allowed MIME type validation
|
|
195
|
+
- Maximum file size validation (50 MB)
|
|
196
|
+
- UUID-based file names to avoid collisions
|
|
197
|
+
- Asynchronous streaming to reduce memory usage
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Dependencies
|
|
202
|
+
|
|
203
|
+
- FastAPI
|
|
204
|
+
- Pydantic v2
|
|
205
|
+
- aiofiles
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT License
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "image-upload"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Image Upload Package for FastAPI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Sahil Sheoran", email = "sahilsheoran24@gmail.com"}
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
"aiofiles>=25.1.0",
|
|
19
|
+
"fastapi>=0.138.2",
|
|
20
|
+
"pydantic>=2.13.4",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
package-dir = {"" = "src"}
|
|
25
|
+
license-files = ["LICENSE.txt"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"build>=1.5.0",
|
|
33
|
+
"twine>=6.2.0",
|
|
34
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pydantic.functional_validators import BeforeValidator
|
|
2
|
+
from pydantic import ValidationInfo
|
|
3
|
+
from typing_extensions import Annotated
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
def append_base_url(v: Optional[str], info: ValidationInfo) -> Optional[str]:
|
|
7
|
+
if v is not None and info.context:
|
|
8
|
+
base_url = info.context.get("base_url", "")
|
|
9
|
+
return f"{base_url}{v}"
|
|
10
|
+
return v
|
|
11
|
+
|
|
12
|
+
AbsoluteUrl = Annotated[Optional[str], BeforeValidator(append_base_url)]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from fastapi import FastAPI, UploadFile, HTTPException
|
|
4
|
+
|
|
5
|
+
import aiofiles
|
|
6
|
+
import uuid
|
|
7
|
+
import hashlib
|
|
8
|
+
|
|
9
|
+
class UploadConfig:
|
|
10
|
+
UPLOAD_DIR = Path("uploads")
|
|
11
|
+
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50 MB
|
|
12
|
+
ALLOWED_TYPES = {
|
|
13
|
+
"image/jpeg", "image/png", "image/gif",
|
|
14
|
+
"application/pdf", "text/plain"
|
|
15
|
+
}
|
|
16
|
+
CHUNK_SIZE = 1024 * 1024 # 1 MB
|
|
17
|
+
|
|
18
|
+
config = UploadConfig()
|
|
19
|
+
config.UPLOAD_DIR.mkdir(exist_ok=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UploadResult(BaseModel):
|
|
23
|
+
id: str
|
|
24
|
+
filename: str
|
|
25
|
+
storage_path: Path
|
|
26
|
+
size: int
|
|
27
|
+
content_type: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def process_upload(file: UploadFile, folder: str | None = None) -> UploadResult:
|
|
31
|
+
|
|
32
|
+
if file.content_type not in config.ALLOWED_TYPES:
|
|
33
|
+
raise HTTPException(
|
|
34
|
+
status_code=400,
|
|
35
|
+
detail=f"File type not allowed: {file.content_type}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
file_id = str(uuid.uuid4())
|
|
39
|
+
if folder:
|
|
40
|
+
folder_path = config.UPLOAD_DIR / Path(folder)
|
|
41
|
+
folder_path.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
extension = Path(file.filename).suffix
|
|
43
|
+
storage_path = config.UPLOAD_DIR / folder / f"{file_id}{extension}"
|
|
44
|
+
else:
|
|
45
|
+
extension = Path(file.filename).suffix
|
|
46
|
+
storage_path = config.UPLOAD_DIR / f"{file_id}{extension}"
|
|
47
|
+
|
|
48
|
+
hasher = hashlib.shake_256()
|
|
49
|
+
total_size = 0
|
|
50
|
+
|
|
51
|
+
async with aiofiles.open(storage_path, "wb") as out:
|
|
52
|
+
while chunk := await file.read(config.CHUNK_SIZE):
|
|
53
|
+
total_size +=len(chunk)
|
|
54
|
+
|
|
55
|
+
if total_size > config.MAX_FILE_SIZE:
|
|
56
|
+
storage_path.unlink(missing_ok=True)
|
|
57
|
+
raise HTTPException(
|
|
58
|
+
status_code=400,
|
|
59
|
+
detail=f"File exceeds maximum size of {config.MAX_FILE_SIZE // (1024*1024)} MB"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
await out.write(chunk)
|
|
63
|
+
hasher.update(chunk)
|
|
64
|
+
|
|
65
|
+
return UploadResult(
|
|
66
|
+
id=file_id,
|
|
67
|
+
filename=file.filename,
|
|
68
|
+
storage_path=storage_path,
|
|
69
|
+
size=total_size,
|
|
70
|
+
content_type=file.content_type
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def get_path(file: UploadFile, folder: str | None = None) -> str:
|
|
74
|
+
upload = await process_upload(file, folder)
|
|
75
|
+
|
|
76
|
+
return str(upload.storage_path)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: image-upload
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Image Upload Package for FastAPI
|
|
5
|
+
Author-email: Sahil Sheoran <sahilsheoran24@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE.txt
|
|
10
|
+
Requires-Dist: aiofiles>=25.1.0
|
|
11
|
+
Requires-Dist: fastapi>=0.138.2
|
|
12
|
+
Requires-Dist: pydantic>=2.13.4
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# image-upload
|
|
16
|
+
|
|
17
|
+
A lightweight FastAPI package for saving uploaded files and generating absolute URLs for stored images using Pydantic.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
* Simple file upload handling
|
|
22
|
+
* Automatically stores files inside an `uploads/` directory
|
|
23
|
+
* Returns the saved file path
|
|
24
|
+
* Easily generate absolute URLs with Pydantic
|
|
25
|
+
* Asynchronous file saving using `aiofiles`
|
|
26
|
+
* File size and MIME type validation
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Python 3.12+
|
|
33
|
+
- FastAPI
|
|
34
|
+
- Pydantic v2
|
|
35
|
+
- aiofiles
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Install from PyPI:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install image-upload
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Supported File Types
|
|
50
|
+
|
|
51
|
+
By default the package accepts:
|
|
52
|
+
|
|
53
|
+
- JPEG (`image/jpeg`)
|
|
54
|
+
- PNG (`image/png`)
|
|
55
|
+
- GIF (`image/gif`)
|
|
56
|
+
- PDF (`application/pdf`)
|
|
57
|
+
- Plain Text (`text/plain`)
|
|
58
|
+
|
|
59
|
+
Maximum upload size is **50 MB**.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Saving an Uploaded File
|
|
64
|
+
|
|
65
|
+
Import `get_path` and pass an `UploadFile`.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from image_upload import get_path
|
|
69
|
+
|
|
70
|
+
data.picture = await get_path(data.picture)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
or
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
data["picture"] = await get_path(data.pop("picture"))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The function returns the relative storage path.
|
|
80
|
+
|
|
81
|
+
Example output:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
uploads/0c6d8a69-9c84-47d4-81f6-d15d11fcbf88.png
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## FastAPI Example
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from fastapi import APIRouter, Depends
|
|
93
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
94
|
+
|
|
95
|
+
from image_upload import get_path
|
|
96
|
+
|
|
97
|
+
router = APIRouter()
|
|
98
|
+
|
|
99
|
+
@router.post("/")
|
|
100
|
+
async def create(
|
|
101
|
+
data: UserCreate = Depends(UserCreate.as_form),
|
|
102
|
+
db: AsyncSession = Depends(get_db),
|
|
103
|
+
):
|
|
104
|
+
data.picture = await get_path(data.picture)
|
|
105
|
+
|
|
106
|
+
return await user(db, data.model_dump())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Save Inside a Custom Folder
|
|
112
|
+
|
|
113
|
+
You can optionally provide a folder name.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
path = await get_path(file, folder="users")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Building Absolute URLs
|
|
122
|
+
|
|
123
|
+
`AbsoluteUrl` automatically prepends the application's base URL using the Pydantic validation context.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from pydantic import BaseModel
|
|
127
|
+
from image_upload import AbsoluteUrl
|
|
128
|
+
|
|
129
|
+
class UserOut(BaseModel):
|
|
130
|
+
name: str
|
|
131
|
+
picture: AbsoluteUrl
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
When validating:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
user = UserOut.model_validate(
|
|
138
|
+
data,
|
|
139
|
+
context={
|
|
140
|
+
"base_url": "https://example.com/"
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Note: Can also use "base_url": request.base_url
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from fastapi import Request
|
|
149
|
+
|
|
150
|
+
@router.get("/")
|
|
151
|
+
async def get_user(
|
|
152
|
+
request: Request,
|
|
153
|
+
db: AsyncSession = Depends(get_db),
|
|
154
|
+
) -> UserOut:
|
|
155
|
+
users = await list_user(
|
|
156
|
+
db
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return UserOut.model_validate(
|
|
160
|
+
users,
|
|
161
|
+
context={"base_url": request.base_url},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Result:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"name": "John",
|
|
171
|
+
"picture": "https://example.com/uploads/avatar.png"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## API
|
|
178
|
+
|
|
179
|
+
### `get_path(file, folder=None)`
|
|
180
|
+
|
|
181
|
+
Stores the uploaded file and returns its storage path.
|
|
182
|
+
|
|
183
|
+
**Parameters**
|
|
184
|
+
|
|
185
|
+
| Parameter | Type | Description |
|
|
186
|
+
|-----------|-------------|-------------------------|
|
|
187
|
+
| file | UploadFile | Uploaded file |
|
|
188
|
+
| folder | str | None | Optional folder prefix |
|
|
189
|
+
|
|
190
|
+
**Returns**
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
str
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
path = await get_path(upload_file)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Security
|
|
205
|
+
|
|
206
|
+
The package performs basic validation:
|
|
207
|
+
|
|
208
|
+
- Allowed MIME type validation
|
|
209
|
+
- Maximum file size validation (50 MB)
|
|
210
|
+
- UUID-based file names to avoid collisions
|
|
211
|
+
- Asynchronous streaming to reduce memory usage
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Dependencies
|
|
216
|
+
|
|
217
|
+
- FastAPI
|
|
218
|
+
- Pydantic v2
|
|
219
|
+
- aiofiles
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT License
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/image_upload/__init__.py
|
|
5
|
+
src/image_upload/add_base_url.py
|
|
6
|
+
src/image_upload/save_file.py
|
|
7
|
+
src/image_upload.egg-info/PKG-INFO
|
|
8
|
+
src/image_upload.egg-info/SOURCES.txt
|
|
9
|
+
src/image_upload.egg-info/dependency_links.txt
|
|
10
|
+
src/image_upload.egg-info/requires.txt
|
|
11
|
+
src/image_upload.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
image_upload
|