Skip to content

Commit 9c24fcb

Browse files
authored
Merge pull request #51 from fastapi-startkit/feature/storage
Feature/storage
2 parents 422c4aa + 678aa5a commit 9c24fcb

57 files changed

Lines changed: 1621 additions & 124 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ services:
1313
interval: 5s
1414
timeout: 5s
1515
retries: 10
16-
postgres:
16+
db:
1717
image: postgres:17
1818
environment:
1919
POSTGRES_DB: database_app_test

example/config-app/uv.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/database-app/app/students/controllers/registration.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import hashlib
22

3-
from fastapi import HTTPException
3+
from fastapi.exceptions import RequestValidationError
44

55
from app.http.schemas.auth import StudentRegistrationRequest
66
from app.models import User, Profile
@@ -9,7 +9,14 @@
99
async def register(request: StudentRegistrationRequest):
1010
existing_user = await User.where("email", request.email).first()
1111
if existing_user:
12-
raise HTTPException(status_code=400, detail="Email already registered")
12+
raise RequestValidationError(
13+
errors=[{
14+
"loc": ("body", "email"),
15+
"msg": "Email already registered",
16+
"type": "value_error",
17+
"input": request.email,
18+
}]
19+
)
1320

1421
password = hashlib.md5(request.password.encode()).hexdigest()
1522
user = User(

example/database-app/tests/features/students/test_register.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,6 @@ async def test_user_cannot_register_with_duplicate_email(self):
6161
await self.post("/students/register", json=payload)
6262

6363
response = await self.post("/students/register", json=payload)
64-
assert response.status_code == 400
65-
assert response.json()["detail"] == "Email already registered"
64+
assert response.status_code == 422
65+
errors = response.json()["errors"]
66+
assert "email" in errors

example/database-app/uv.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
APP_NAME="Inertia Tickets"
2+
APP_ENV=local
3+
APP_URL=http://localhost:8000
4+
APP_DEBUG=true
5+
6+
DB_CONNECTION=postgres
7+
DB_HOST=127.0.0.1
8+
DB_PORT=5432
9+
DB_DATABASE=database_app_test
10+
DB_USERNAME=app
11+
DB_PASSWORD=secret
12+
13+
AWS_ENDPOINT=http://localhost:9000
14+
AWS_ACCESS_KEY_ID=minioadmin
15+
AWS_SECRET_ACCESS_KEY=minioadmin
16+
AWS_BUCKET=pingcrm
17+
AWS_DEFAULT_REGION=us-east-1
18+
AWS_URL=http://localhost:9000/uploads
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
from fastapi import Request
2-
from fastapi.responses import JSONResponse
3-
from fastapi_startkit.inertia import Inertia
4-
5-
async def show(request: Request):
6-
return JSONResponse(content={'message': 'images_controller.py@show'})
1+
from fastapi_startkit.storage import Storage
72

3+
async def stream(path: str):
4+
"""Stream a file from S3 back to the client."""
5+
return Storage.disk("s3").stream(path)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import uuid
2+
from pathlib import Path
3+
from typing import Optional
4+
5+
6+
from fastapi import Request, Depends, UploadFile, File
7+
from fastapi.responses import RedirectResponse
8+
from fastapi_startkit.inertia import Inertia
9+
from fastapi_startkit.storage import Storage
10+
from app.models.User import User
11+
from app.http.requests.profile import ProfileUpdateRequest
12+
13+
14+
async def save_photo(photo: Optional[UploadFile]) -> Optional[str]:
15+
if photo is None or not photo.filename:
16+
return None
17+
if not photo.content_type or not photo.content_type.startswith("image/"):
18+
return None
19+
ext = Path(photo.filename).suffix.lower() or ".jpg"
20+
filename = f"photos/{uuid.uuid4().hex}{ext}"
21+
Storage.disk("s3").put(filename, await photo.read())
22+
return filename
23+
24+
25+
async def edit(request: Request):
26+
user = await User.find(request.state.user["id"])
27+
photo_url = f"/images/{user.photo_path}" if user.photo_path else None
28+
return Inertia.render('Profile/Edit', {
29+
'user': {
30+
'id': user.id,
31+
'first_name': user.first_name,
32+
'last_name': user.last_name,
33+
'email': user.email,
34+
'photo': photo_url,
35+
'password': '',
36+
}
37+
})
38+
39+
40+
async def update(
41+
request: Request,
42+
form: ProfileUpdateRequest = Depends(),
43+
photo: Optional[UploadFile] = File(default=None),
44+
):
45+
user = await User.find(request.state.user["id"])
46+
47+
photo_path = await save_photo(photo)
48+
49+
update_data = form.validated()
50+
if photo_path:
51+
update_data['photo_path'] = photo_path
52+
53+
await user.update(update_data)
54+
return RedirectResponse(url="/profile", status_code=303)

example/inertia-pingcrm-app/app/http/controllers/users_controller.py

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
1-
from fastapi import Request
1+
import uuid
2+
from pathlib import Path
3+
from typing import Optional
4+
5+
from fastapi import Request, UploadFile, File, Form
26
from fastapi.responses import RedirectResponse
37
from fastapi_startkit.inertia import Inertia
8+
from fastapi_startkit.storage import Storage
49
from app.models.User import User
510

611

12+
async def _save_photo(photo: Optional[UploadFile]) -> Optional[str]:
13+
"""Save an UploadFile to the public disk and return its public URL path, or None."""
14+
if photo is None or not photo.filename:
15+
return None
16+
if not photo.content_type or not photo.content_type.startswith("image/"):
17+
return None
18+
19+
ext = Path(photo.filename).suffix.lower() or ".jpg"
20+
filename = f"{uuid.uuid4().hex}{ext}"
21+
22+
Storage.disk("public").put(filename, await photo.read())
23+
24+
return f"/storage/{filename}"
25+
26+
727
async def index():
828
users = await User.query().limit(10).get()
929
return Inertia.render('Users/Index', {
@@ -32,13 +52,32 @@ async def create():
3252
return Inertia.render('Users/Create', {})
3353

3454

35-
async def store(request: Request):
36-
form = await request.json()
37-
await User.create(form)
55+
async def store(
56+
request: Request,
57+
first_name: str = Form(...),
58+
last_name: str = Form(...),
59+
email: str = Form(...),
60+
password: str = Form(default=''),
61+
owner: str = Form(default='0'),
62+
photo: Optional[UploadFile] = File(default=None),
63+
):
64+
photo_path = await _save_photo(photo)
65+
66+
user_data = {
67+
'first_name': first_name,
68+
'last_name': last_name,
69+
'email': email,
70+
'password': password,
71+
'owner': owner == '1',
72+
}
73+
if photo_path:
74+
user_data['photo_path'] = photo_path
75+
76+
await User.create(user_data)
3877
return RedirectResponse(url="/users", status_code=303)
3978

4079

41-
async def edit(user: str):
80+
async def edit(user: int):
4281
u = await User.find(user)
4382
return Inertia.render('Users/Edit', {
4483
'user': {
@@ -54,16 +93,38 @@ async def edit(user: str):
5493
})
5594

5695

57-
async def update(request: Request, user: str):
96+
async def update(
97+
request: Request,
98+
user: int,
99+
first_name: str = Form(...),
100+
last_name: str = Form(...),
101+
email: str = Form(...),
102+
password: str = Form(default=''),
103+
owner: str = Form(default='0'),
104+
photo: Optional[UploadFile] = File(default=None),
105+
):
58106
u = await User.find(user)
59-
form = await request.json()
60-
await u.update(form)
107+
108+
photo_path = await _save_photo(photo)
109+
110+
update_data = {
111+
'first_name': first_name,
112+
'last_name': last_name,
113+
'email': email,
114+
'owner': owner == '1',
115+
}
116+
if password:
117+
update_data['password'] = password
118+
if photo_path:
119+
update_data['photo_path'] = photo_path
120+
121+
await u.update(update_data)
61122
return RedirectResponse(url=f"/users/{user}/edit", status_code=303)
62123

63124

64-
async def destroy(user: str):
125+
async def destroy(user: int):
65126
return RedirectResponse(url="/users", status_code=303)
66127

67128

68-
async def restore(user: str):
129+
async def restore(user: int):
69130
return RedirectResponse(url="/users", status_code=303)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)