Skip to content

Commit 34cf394

Browse files
committed
feat: add local mock storage service for development
* Uses a mounted folder to store uploads in the `.data` directory * This removes the dependency on S3 locally
1 parent 74b8e8c commit 34cf394

8 files changed

Lines changed: 107 additions & 19 deletions

File tree

backend/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from backend.api.routes import router as api_router
1111
from common.database.postgres_database import init_cleanup_scheduler
12+
from common.services.storage_services.local import mock_storage_app
1213
from common.settings import get_settings
1314

1415
settings = get_settings()
@@ -58,5 +59,8 @@ async def lifespan(app_: FastAPI): # noqa: ARG001
5859

5960
app.include_router(api_router)
6061

62+
if settings.STORAGE_SERVICE_NAME == "local":
63+
app.mount("/mock_storage", mock_storage_app)
64+
6165
if __name__ == "__main__":
6266
uvicorn.run("main:app", host="0.0.0.0", port=8080) # noqa: S104

common/services/queue_services/sqs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ def get_sqs_client():
1515
if settings.USE_LOCALSTACK and settings.ENVIRONMENT == "local":
1616
return boto3.client(
1717
"sqs",
18+
aws_access_key_id="YOUR_ACCESS_KEY_ID",
19+
aws_secret_access_key="YOUR_SECRET_ACCESS_KEY", # noqa: S106
20+
region_name="eu-west-2",
1821
endpoint_url=settings.LOCALSTACK_URL,
1922
)
2023

common/services/storage_services/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
from .azure_blob import AzureBlobStorageService
22
from .base import StorageService
3+
from .local import LocalStorageService
34
from .s3 import S3StorageService
45

5-
storage_services = {S3StorageService.name: S3StorageService, AzureBlobStorageService.name: AzureBlobStorageService}
6+
storage_services = {
7+
S3StorageService.name: S3StorageService,
8+
AzureBlobStorageService.name: AzureBlobStorageService,
9+
LocalStorageService.name: LocalStorageService,
10+
}
611

712

813
def get_storage_service(storage_service_name: str) -> StorageService:
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import shutil
2+
from contextlib import suppress
3+
from pathlib import Path
4+
5+
from fastapi import FastAPI, Request
6+
from fastapi.staticfiles import StaticFiles
7+
8+
from common.services.storage_services.base import StorageService
9+
from common.settings import get_settings
10+
11+
settings = get_settings()
12+
13+
14+
class LocalStorageService(StorageService):
15+
name = "local"
16+
17+
@classmethod
18+
async def upload(cls, key: str, path: Path) -> None:
19+
storage_path = Path(settings.LOCAL_STORAGE_PATH) / key
20+
shutil.copy2(path, storage_path)
21+
22+
@classmethod
23+
async def download(cls, key: str, path: Path) -> None:
24+
storage_path = Path(settings.LOCAL_STORAGE_PATH) / key
25+
shutil.copy2(storage_path, path)
26+
27+
@classmethod
28+
async def generate_presigned_url_put_object(cls, key: str, expiry_seconds: int) -> str: # noqa: ARG003
29+
return f"/api/proxy/mock_storage/uploadfile/{key}"
30+
31+
@classmethod
32+
async def generate_presigned_url_get_object(cls, key: str, filename: str, expiry_seconds: int) -> str: # noqa: ARG003
33+
return f"/api/proxy/mock_storage/static/{key}"
34+
35+
@classmethod
36+
async def check_object_exists(cls, key: str) -> bool:
37+
storage_path = Path(settings.LOCAL_STORAGE_PATH) / key
38+
return storage_path.exists()
39+
40+
@classmethod
41+
async def delete(cls, key: str) -> None:
42+
with suppress(FileNotFoundError):
43+
storage_path = Path(settings.LOCAL_STORAGE_PATH) / key
44+
storage_path.unlink()
45+
46+
47+
mock_storage_app = FastAPI(title="Mock storage service")
48+
49+
mock_storage_app.mount("/static", StaticFiles(directory=settings.LOCAL_STORAGE_PATH), name="static")
50+
51+
52+
@mock_storage_app.put("/uploadfile/{file_path:path}")
53+
async def upload_file_to_mock_storage(file_path: str, request: Request):
54+
storage_path = Path(settings.LOCAL_STORAGE_PATH) / file_path
55+
storage_path.parent.mkdir(parents=True, exist_ok=True)
56+
storage_path.write_bytes(await request.body())

common/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ class Settings(BaseSettings):
147147
),
148148
)
149149

150+
LOCAL_STORAGE_PATH: str = Field(
151+
default="", description="The folder where the data directory is mounted for the local storage service."
152+
)
153+
150154
# use a dotenv file for local development
151155
if dotenv_detected:
152156
model_config = SettingsConfigDict(env_file=DOT_ENV_PATH, extra="ignore")

docker-compose.yaml

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ services:
88
env_file:
99
- .env
1010
environment:
11-
- AWS_ACCESS_KEY_ID
12-
- AWS_SECRET_ACCESS_KEY
13-
- AWS_SESSION_TOKEN
14-
- AWS_DEFAULT_REGION=eu-west-2
1511
- POSTGRES_HOST=db
1612
- LOCALSTACK_URL=http://localstack:4566
13+
- STORAGE_SERVICE_NAME=local
14+
- LOCAL_STORAGE_PATH=/static
1715
depends_on:
1816
db:
1917
condition: service_healthy
2018
localstack:
2119
condition: service_started
20+
volumes:
21+
- type: bind
22+
source: ./.data
23+
target: /static
2224
develop:
2325
watch:
2426
- path: ./backend
@@ -45,14 +47,15 @@ services:
4547
- "8265:8265"
4648
volumes:
4749
- /tmp:/tmp
50+
- type: bind
51+
source: ./.data
52+
target: /static
4853
environment:
49-
- AWS_ACCESS_KEY_ID
50-
- AWS_SECRET_ACCESS_KEY
51-
- AWS_SESSION_TOKEN
52-
- AWS_DEFAULT_REGION=eu-west-2
5354
- POSTGRES_HOST=db
5455
- RAY_DASHBOARD_HOST=0.0.0.0
5556
- LOCALSTACK_URL=http://localstack:4566
57+
- STORAGE_SERVICE_NAME=local
58+
- LOCAL_STORAGE_PATH=/static
5659
stop_grace_period: 10s
5760
depends_on:
5861
db:

poetry.lock

Lines changed: 21 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,14 @@ mistune = "^3.1.3"
5454
httpx = "^0.28.1"
5555
psycopg2-binary = "^2.9.9"
5656
boto3 = "^1.35.3"
57+
fastapi = ">=0.112.0"
5758
sentry-sdk = {extras = ["fastapi"], version = "^2.18.0"}
5859
sqlalchemy = {extras = ["asyncio"], version = "^2.0.42"}
5960
asyncpg = "^0.30.0"
6061
markdownify = "^1.2.0"
62+
python-multipart = "^0.0.20"
6163

6264
[tool.poetry.group.backend.dependencies]
63-
fastapi = ">=0.112.0"
6465
uvicorn = "^0.31.0"
6566
pyjwt = "^2.9.0"
6667
cryptography = "^44.0.2"

0 commit comments

Comments
 (0)