Soumik Bose commited on
Commit
402ef6a
·
1 Parent(s): fd049b7

clean: remove all space contents

Browse files
Files changed (42) hide show
  1. .gitignore +0 -1
  2. .vscode/settings.json +0 -3
  3. Dockerfile +0 -30
  4. README.md +0 -9
  5. app/__pycache__/main.cpython-311.pyc +0 -0
  6. app/banner.py +0 -20
  7. app/core/__pycache__/config.cpython-311.pyc +0 -0
  8. app/core/__pycache__/logger.cpython-311.pyc +0 -0
  9. app/core/config.py +0 -41
  10. app/core/constants.py +0 -1
  11. app/core/exceptions.py +0 -5
  12. app/core/logger.py +0 -26
  13. app/db/__pycache__/session.cpython-311.pyc +0 -0
  14. app/db/session.py +0 -59
  15. app/main.py +0 -118
  16. app/middleware/__pycache__/allowed_methods.cpython-311.pyc +0 -0
  17. app/middleware/__pycache__/auth.cpython-311.pyc +0 -0
  18. app/middleware/__pycache__/ip_filter.cpython-311.pyc +0 -0
  19. app/middleware/__pycache__/logging.cpython-311.pyc +0 -0
  20. app/middleware/__pycache__/rate_limit.cpython-311.pyc +0 -0
  21. app/middleware/__pycache__/request_id.cpython-311.pyc +0 -0
  22. app/middleware/__pycache__/request_size.cpython-311.pyc +0 -0
  23. app/middleware/__pycache__/security_headers.cpython-311.pyc +0 -0
  24. app/middleware/allowed_methods.py +0 -15
  25. app/middleware/auth.py +0 -21
  26. app/middleware/ip_filter.py +0 -23
  27. app/middleware/logging.py +0 -33
  28. app/middleware/rate_limit.py +0 -93
  29. app/middleware/request_id.py +0 -13
  30. app/middleware/request_size.py +0 -22
  31. app/middleware/security_headers.py +0 -27
  32. app/models/__pycache__/schemas.cpython-311.pyc +0 -0
  33. app/models/schemas.py +0 -32
  34. app/routers/__pycache__/files.cpython-311.pyc +0 -0
  35. app/routers/__pycache__/ping.cpython-311.pyc +0 -0
  36. app/routers/files.py +0 -83
  37. app/routers/ping.py +0 -22
  38. app/services/__pycache__/file_service.cpython-311.pyc +0 -0
  39. app/services/file_service.py +0 -179
  40. db/file_service.db +0 -0
  41. requirements.txt +0 -13
  42. start.sh +0 -9
.gitignore DELETED
@@ -1 +0,0 @@
1
- .env
 
 
.vscode/settings.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "python-envs.defaultEnvManager": "ms-python.python:system"
3
- }
 
 
 
 
Dockerfile DELETED
@@ -1,30 +0,0 @@
1
- FROM python:3.11-slim
2
-
3
- ENV PYTHONDONTWRITEBYTECODE=1 \
4
- PYTHONUNBUFFERED=1 \
5
- PIP_NO_CACHE_DIR=1 \
6
- PIP_DISABLE_PIP_VERSION_CHECK=1
7
-
8
- WORKDIR /app
9
-
10
- RUN apt-get update && apt-get install -y --no-install-recommends \
11
- curl \
12
- && rm -rf /var/lib/apt/lists/*
13
-
14
- RUN useradd -m -u 1000 user
15
-
16
- COPY --chown=user:user requirements.txt .
17
-
18
- RUN pip install --no-cache-dir -r requirements.txt
19
-
20
- COPY --chown=user:user . .
21
-
22
- RUN mkdir -p /app/documents/pdfs /app/documents/images /app/files /app/db \
23
- && chown -R user:user /app/documents /app/files /app/db \
24
- && chmod +x /app/start.sh
25
-
26
- USER user
27
-
28
- EXPOSE 7860
29
-
30
- CMD ["./start.sh"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md DELETED
@@ -1,9 +0,0 @@
1
- ---
2
- title: reconciliation-file-service-backup
3
- emoji: 📁
4
- colorFrom: red
5
- colorTo: pink
6
- sdk: docker
7
- app_file: main.py
8
- pinned: false
9
- ---
 
 
 
 
 
 
 
 
 
 
app/__pycache__/main.cpython-311.pyc DELETED
Binary file (5.6 kB)
 
app/banner.py DELETED
@@ -1,20 +0,0 @@
1
- import pyfiglet
2
- from rich.console import Console
3
- from rich.rule import Rule
4
- from rich.text import Text
5
-
6
- from app.core.config import settings
7
-
8
- _console = Console()
9
-
10
-
11
- def print_banner() -> None:
12
-
13
- art = pyfiglet.figlet_format("ValidOps", font="slant")
14
-
15
- _console.print(Text(art, style="bold cyan"))
16
- _console.print(f" [bold white]{'Service:':<14}[/bold white] [cyan]{settings.APP_NAME}[/cyan]")
17
- _console.print(f" [bold white]{'Version:':<14}[/bold white] [cyan]v{settings.APP_VERSION}[/cyan]")
18
- _console.print(f" [bold white]{'Environment:':<14}[/bold white] [cyan]{settings.ENVIRONMENT}[/cyan]")
19
- _console.print(Rule(style="dim cyan"))
20
- _console.print()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/core/__pycache__/config.cpython-311.pyc DELETED
Binary file (1.9 kB)
 
app/core/__pycache__/logger.cpython-311.pyc DELETED
Binary file (1.43 kB)
 
app/core/config.py DELETED
@@ -1,41 +0,0 @@
1
- from pathlib import Path
2
- from typing import Optional
3
- from pydantic_settings import BaseSettings
4
-
5
-
6
- class Settings(BaseSettings):
7
- APP_BASE_URL: str = "https://validops-east-3-instance-2.hf.space"
8
- APP_NAME: str = "reconciliation-backup-file-service"
9
- APP_VERSION: str = "1.0.0"
10
- ENVIRONMENT: str = "production"
11
-
12
- BASE_DIR: Path = Path("documents")
13
- FILES_DIR: Path = Path("/app/files")
14
- DATABASE_URL: str = "sqlite:////app/db/file_service.db"
15
-
16
- MAX_FILE_SIZE_MB: int = 50
17
-
18
- VALID_API_KEYS: set[str] = set()
19
-
20
- ALLOWED_ORIGINS: list[str] = ["https://validops-east-1-instance-1.hf.space", "https://validops-east-1-instance-2.hf.space", "https://validops-ai.vercel.app"]
21
- ALLOWED_METHODS: set[str] = {"GET", "POST", "PUT", "DELETE", "OPTIONS"}
22
-
23
- IP_WHITELIST: Optional[set[str]] = None
24
- IP_BLACKLIST: set[str] = set()
25
-
26
- REDIS_URL: str
27
- RATE_LIMIT_PER_MINUTE: int = 100
28
- RATE_LIMIT_WINDOW_SECONDS: int = 60
29
- RATE_LIMIT_KEY_PREFIX: str = "rl"
30
-
31
- MAX_REQUEST_SIZE_MB: int = 50
32
- TRUSTED_PROXIES: set[str] = {"127.0.0.1"}
33
-
34
- REQUEST_ID_HEADER: str = "X-Request-ID"
35
-
36
- class Config:
37
- env_file = ".env"
38
- extra = "ignore"
39
-
40
-
41
- settings = Settings()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/core/constants.py DELETED
@@ -1 +0,0 @@
1
- # No specific constants required for the current implementation.
 
 
app/core/exceptions.py DELETED
@@ -1,5 +0,0 @@
1
- from fastapi import HTTPException, status
2
-
3
- class FileServiceException(HTTPException):
4
- def __init__(self, status_code: int, detail: str):
5
- super().__init__(status_code=status_code, detail=detail)
 
 
 
 
 
 
app/core/logger.py DELETED
@@ -1,26 +0,0 @@
1
- import logging
2
- import sys
3
- from functools import lru_cache
4
-
5
-
6
- LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
7
- DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
8
-
9
-
10
- @lru_cache(maxsize=None)
11
- def get_logger(name: str) -> logging.Logger:
12
- logger = logging.getLogger(name)
13
-
14
- if logger.handlers:
15
- return logger
16
-
17
- logger.setLevel(logging.DEBUG)
18
-
19
- handler = logging.StreamHandler(sys.stdout)
20
- handler.setLevel(logging.DEBUG)
21
- handler.setFormatter(logging.Formatter(fmt=LOG_FORMAT, datefmt=DATE_FORMAT))
22
-
23
- logger.addHandler(handler)
24
- logger.propagate = False
25
-
26
- return logger
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/db/__pycache__/session.cpython-311.pyc DELETED
Binary file (2.22 kB)
 
app/db/session.py DELETED
@@ -1,59 +0,0 @@
1
- from datetime import datetime, timezone
2
- from typing import AsyncGenerator
3
-
4
- from sqlalchemy import Column, DateTime, Integer, String
5
- from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
6
- from sqlalchemy.orm import DeclarativeBase
7
-
8
- from app.core.config import settings
9
-
10
- _async_url = settings.DATABASE_URL.replace("sqlite:///", "sqlite+aiosqlite:///")
11
-
12
- _engine = create_async_engine(
13
- _async_url,
14
- connect_args={"check_same_thread": False},
15
- pool_pre_ping=True,
16
- echo=False,
17
- )
18
-
19
- _AsyncSessionLocal = async_sessionmaker(
20
- bind=_engine,
21
- class_=AsyncSession,
22
- expire_on_commit=False,
23
- autoflush=False,
24
- autocommit=False,
25
- )
26
-
27
-
28
- class _Base(DeclarativeBase):
29
- pass
30
-
31
-
32
- class FileRecord(_Base):
33
- __tablename__ = "files"
34
-
35
- id = Column(Integer, primary_key=True, index=True, autoincrement=True)
36
- fileid = Column(String, unique=True, index=True, nullable=False)
37
- filename = Column(String, nullable=False)
38
- file_url = Column(String, nullable=False)
39
- createdat = Column(
40
- DateTime(timezone=True),
41
- default=lambda: datetime.now(timezone.utc),
42
- nullable=False,
43
- )
44
- updatedat = Column(
45
- DateTime(timezone=True),
46
- default=lambda: datetime.now(timezone.utc),
47
- onupdate=lambda: datetime.now(timezone.utc),
48
- nullable=False,
49
- )
50
-
51
-
52
- async def init_db() -> None:
53
- async with _engine.begin() as conn:
54
- await conn.run_sync(_Base.metadata.create_all)
55
-
56
-
57
- async def get_db() -> AsyncGenerator[AsyncSession, None]:
58
- async with _AsyncSessionLocal() as session:
59
- yield session
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/main.py DELETED
@@ -1,118 +0,0 @@
1
- import shutil
2
- from contextlib import asynccontextmanager
3
-
4
- from fastapi import Depends, FastAPI, HTTPException, Query, Request
5
- from fastapi.middleware.cors import CORSMiddleware
6
- from fastapi.middleware.trustedhost import TrustedHostMiddleware
7
- from fastapi.responses import FileResponse, Response
8
- from sqlalchemy import select
9
- from sqlalchemy.ext.asyncio import AsyncSession
10
-
11
- from app.core.config import settings
12
- from app.core.logger import get_logger
13
- from app.db.session import FileRecord, get_db, init_db
14
- from app.middleware.allowed_methods import AllowedMethodsMiddleware
15
- from app.middleware.auth import AuthMiddleware
16
- from app.middleware.ip_filter import IPFilterMiddleware
17
- from app.middleware.logging import LoggingMiddleware
18
- from app.middleware.rate_limit import RateLimitMiddleware
19
- from app.middleware.request_id import RequestIDMiddleware
20
- from app.middleware.request_size import RequestSizeLimitMiddleware
21
- from app.middleware.security_headers import SecurityHeadersMiddleware
22
- from app.routers import files, ping
23
- from app.services.file_service import file_service
24
-
25
- logger = get_logger(__name__)
26
-
27
-
28
- def _get_storage_info() -> dict:
29
- total, used, free = shutil.disk_usage("/")
30
- gb = 2**30
31
- return {
32
- "total_gb": round(total / gb, 2),
33
- "used_gb": round(used / gb, 2),
34
- "free_gb": round(free / gb, 2),
35
- "percent_used": f"{(used / total) * 100:.2f}%",
36
- }
37
-
38
-
39
- @asynccontextmanager
40
- async def lifespan(app: FastAPI):
41
- await init_db()
42
- storage = _get_storage_info()
43
- logger.info(
44
- "Storage: %sGB free of %sGB (%s used)",
45
- storage["free_gb"],
46
- storage["total_gb"],
47
- storage["percent_used"],
48
- )
49
- yield
50
-
51
-
52
- app = FastAPI(
53
- title=settings.APP_NAME,
54
- version=settings.APP_VERSION,
55
- lifespan=lifespan,
56
- )
57
-
58
- app.add_middleware(RequestIDMiddleware)
59
- app.add_middleware(LoggingMiddleware)
60
- app.add_middleware(SecurityHeadersMiddleware)
61
- app.add_middleware(IPFilterMiddleware)
62
- app.add_middleware(RateLimitMiddleware)
63
- app.add_middleware(RequestSizeLimitMiddleware)
64
- app.add_middleware(AllowedMethodsMiddleware)
65
- app.add_middleware(AuthMiddleware)
66
-
67
- app.add_middleware(
68
- CORSMiddleware,
69
- allow_origins=settings.ALLOWED_ORIGINS,
70
- allow_credentials=True,
71
- allow_methods=list(settings.ALLOWED_METHODS),
72
- allow_headers=["Authorization", "X-API-Key", "Content-Type", "X-Request-ID"],
73
- expose_headers=["X-Request-ID", "Content-Length", "Content-Type", "Accept-Ranges"],
74
- )
75
-
76
- app.add_middleware(
77
- TrustedHostMiddleware,
78
- allowed_hosts=["*"],
79
- )
80
-
81
- app.include_router(ping.router, tags=["ping"])
82
- app.include_router(files.router, prefix="/api/v1/files", tags=["files"])
83
-
84
-
85
- @app.api_route("/v1/getfile", methods=["GET", "HEAD"])
86
- async def serve_file(
87
- request: Request,
88
- fileid: str = Query(..., description="UUID of the file to serve"),
89
- db: AsyncSession = Depends(get_db),
90
- ):
91
- result = await db.execute(select(FileRecord).where(FileRecord.fileid == fileid))
92
- db_record = result.scalar_one_or_none()
93
- if not db_record:
94
- logger.warning("File not found in database: %s", fileid)
95
- raise HTTPException(status_code=404, detail="File not found")
96
-
97
- file_path = file_service.get_file_path(fileid)
98
- mime_type = file_service.get_mime_type(db_record.filename) or "application/octet-stream"
99
- file_size = file_path.stat().st_size
100
-
101
- base_headers = {
102
- "Content-Disposition": f'inline; filename="{db_record.filename}"',
103
- "Content-Length": str(file_size),
104
- "Accept-Ranges": "bytes",
105
- }
106
-
107
- if request.method == "HEAD":
108
- return Response(
109
- status_code=200,
110
- headers={**base_headers, "Content-Type": mime_type},
111
- )
112
-
113
- logger.info("Serving file: %s (%s)", db_record.filename, fileid)
114
- return FileResponse(
115
- path=file_path,
116
- media_type=mime_type,
117
- headers=base_headers,
118
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/middleware/__pycache__/allowed_methods.cpython-311.pyc DELETED
Binary file (1.41 kB)
 
app/middleware/__pycache__/auth.cpython-311.pyc DELETED
Binary file (1.88 kB)
 
app/middleware/__pycache__/ip_filter.cpython-311.pyc DELETED
Binary file (2.06 kB)
 
app/middleware/__pycache__/logging.cpython-311.pyc DELETED
Binary file (1.92 kB)
 
app/middleware/__pycache__/rate_limit.cpython-311.pyc DELETED
Binary file (3.47 kB)
 
app/middleware/__pycache__/request_id.cpython-311.pyc DELETED
Binary file (1.31 kB)
 
app/middleware/__pycache__/request_size.cpython-311.pyc DELETED
Binary file (1.56 kB)
 
app/middleware/__pycache__/security_headers.cpython-311.pyc DELETED
Binary file (1.72 kB)
 
app/middleware/allowed_methods.py DELETED
@@ -1,15 +0,0 @@
1
- from starlette.middleware.base import BaseHTTPMiddleware
2
- from starlette.requests import Request
3
- from starlette.responses import JSONResponse
4
- from app.core.config import settings
5
-
6
-
7
- class AllowedMethodsMiddleware(BaseHTTPMiddleware):
8
- async def dispatch(self, request: Request, call_next):
9
- if request.method not in settings.ALLOWED_METHODS:
10
- return JSONResponse(
11
- status_code=405,
12
- content={"detail": f"Method '{request.method}' is not allowed."},
13
- headers={"Allow": ", ".join(settings.ALLOWED_METHODS)},
14
- )
15
- return await call_next(request)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/middleware/auth.py DELETED
@@ -1,21 +0,0 @@
1
- from starlette.middleware.base import BaseHTTPMiddleware
2
- from starlette.requests import Request
3
- from starlette.responses import JSONResponse
4
- from app.core.config import settings
5
- from app.core.logger import get_logger
6
-
7
- logger = get_logger(__name__)
8
-
9
- class AuthMiddleware(BaseHTTPMiddleware):
10
- async def dispatch(self, request: Request, call_next):
11
- if request.method == "POST" and ("/api/v1/files/upload" in request.url.path or "/api/v1/files/bulk-upload" in request.url.path):
12
- api_key = request.headers.get("X-API-Key")
13
- if not api_key or api_key not in settings.VALID_API_KEYS:
14
- logger.warning(f"Unauthorized access attempt to {request.url.path}")
15
- return JSONResponse(
16
- status_code=401,
17
- content={"detail": "Invalid or missing API key."},
18
- )
19
- logger.info(f"Authorized access to {request.url.path}")
20
-
21
- return await call_next(request)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/middleware/ip_filter.py DELETED
@@ -1,23 +0,0 @@
1
- from starlette.middleware.base import BaseHTTPMiddleware
2
- from starlette.requests import Request
3
- from starlette.responses import JSONResponse
4
- from app.core.config import settings
5
-
6
-
7
- class IPFilterMiddleware(BaseHTTPMiddleware):
8
- async def dispatch(self, request: Request, call_next):
9
- client_ip = self._resolve_client_ip(request)
10
-
11
- if settings.IP_BLACKLIST and client_ip in settings.IP_BLACKLIST:
12
- return JSONResponse(status_code=403, content={"detail": "Access denied."})
13
-
14
- if settings.IP_WHITELIST and client_ip not in settings.IP_WHITELIST:
15
- return JSONResponse(status_code=403, content={"detail": "Access denied."})
16
-
17
- return await call_next(request)
18
-
19
- def _resolve_client_ip(self, request: Request) -> str:
20
- forwarded_for = request.headers.get("X-Forwarded-For")
21
- if forwarded_for:
22
- return forwarded_for.split(",")[0].strip()
23
- return request.client.host if request.client else "unknown"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/middleware/logging.py DELETED
@@ -1,33 +0,0 @@
1
- import time
2
- import logging
3
- from starlette.middleware.base import BaseHTTPMiddleware
4
- from starlette.requests import Request
5
- from starlette.responses import Response
6
-
7
- logger = logging.getLogger("fileserver.access")
8
-
9
-
10
- class LoggingMiddleware(BaseHTTPMiddleware):
11
- async def dispatch(self, request: Request, call_next) -> Response:
12
- start = time.perf_counter()
13
- response = await call_next(request)
14
- duration_ms = (time.perf_counter() - start) * 1000
15
-
16
- client_ip = getattr(request.state, "client_ip", None) or (
17
- request.client.host if request.client else "unknown"
18
- )
19
- request_id = getattr(request.state, "request_id", "-")
20
- auth_method = getattr(request.state, "auth_method", "none")
21
-
22
- logger.info(
23
- "%s %s %s | ip=%s | req_id=%s | auth=%s | %.2fms",
24
- request.method,
25
- request.url.path,
26
- response.status_code,
27
- client_ip,
28
- request_id,
29
- auth_method,
30
- duration_ms,
31
- )
32
-
33
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/middleware/rate_limit.py DELETED
@@ -1,93 +0,0 @@
1
- import logging
2
- from starlette.middleware.base import BaseHTTPMiddleware
3
- from starlette.requests import Request
4
- from starlette.responses import JSONResponse
5
- import redis.asyncio as aioredis
6
- from app.core.config import settings
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
- SLIDING_WINDOW_LUA = """
11
- local key = KEYS[1]
12
- local now = tonumber(ARGV[1])
13
- local window_ms = tonumber(ARGV[2])
14
- local limit = tonumber(ARGV[3])
15
- local cutoff = now - window_ms
16
-
17
- redis.call("ZREMRANGEBYSCORE", key, "-inf", cutoff)
18
-
19
- local count = redis.call("ZCARD", key)
20
-
21
- if count >= limit then
22
- local oldest = redis.call("ZRANGE", key, 0, 0, "WITHSCORES")
23
- local retry_at = 0
24
- if oldest and oldest[2] then
25
- retry_at = math.ceil((tonumber(oldest[2]) + window_ms - now) / 1000)
26
- end
27
- return {0, retry_at}
28
- end
29
-
30
- redis.call("ZADD", key, now, now .. "-" .. math.random(1, 1000000))
31
- redis.call("PEXPIRE", key, window_ms)
32
-
33
- return {1, 0}
34
- """
35
-
36
- EXEMPT_PATHS = {"/ping", "/api/v1/files/get"}
37
-
38
-
39
- class RateLimitMiddleware(BaseHTTPMiddleware):
40
- def __init__(self, app):
41
- super().__init__(app)
42
- self._limit = settings.RATE_LIMIT_PER_MINUTE
43
- self._window_ms = settings.RATE_LIMIT_WINDOW_SECONDS * 1000
44
- self._key_prefix = settings.RATE_LIMIT_KEY_PREFIX
45
- self._redis: aioredis.Redis = aioredis.from_url(
46
- settings.REDIS_URL,
47
- encoding="utf-8",
48
- decode_responses=True,
49
- socket_connect_timeout=2,
50
- socket_timeout=2,
51
- )
52
- self._script = self._redis.register_script(SLIDING_WINDOW_LUA)
53
-
54
- async def dispatch(self, request: Request, call_next):
55
- if request.url.path in EXEMPT_PATHS:
56
- return await call_next(request)
57
-
58
- client_ip = (
59
- getattr(request.state, "client_ip", None)
60
- or request.headers.get("x-real-ip")
61
- or (request.headers.get("x-forwarded-for", "").split(",")[0].strip() or None)
62
- or (request.client.host if request.client else "unknown")
63
- )
64
-
65
- allowed, retry_after = await self._check(client_ip)
66
-
67
- if not allowed:
68
- return JSONResponse(
69
- status_code=429,
70
- content={"detail": "Too many requests. Please slow down."},
71
- headers={"Retry-After": str(max(1, retry_after))},
72
- )
73
-
74
- return await call_next(request)
75
-
76
- async def _check(self, client_ip: str) -> tuple[bool, int]:
77
- import time
78
-
79
- key = f"{self._key_prefix}:{client_ip}"
80
- now_ms = int(time.time() * 1000)
81
-
82
- try:
83
- result = await self._script(
84
- keys=[key],
85
- args=[now_ms, self._window_ms, self._limit],
86
- )
87
- return bool(result[0]), int(result[1])
88
- except Exception as exc:
89
- logger.error("Rate limiter Redis error — failing open: %s", exc)
90
- return True, 0
91
-
92
- async def close(self):
93
- await self._redis.aclose()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/middleware/request_id.py DELETED
@@ -1,13 +0,0 @@
1
- import uuid
2
- from starlette.middleware.base import BaseHTTPMiddleware
3
- from starlette.requests import Request
4
- from starlette.responses import Response
5
-
6
-
7
- class RequestIDMiddleware(BaseHTTPMiddleware):
8
- async def dispatch(self, request: Request, call_next) -> Response:
9
- request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
10
- request.state.request_id = request_id
11
- response = await call_next(request)
12
- response.headers["X-Request-ID"] = request_id
13
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/middleware/request_size.py DELETED
@@ -1,22 +0,0 @@
1
- from starlette.middleware.base import BaseHTTPMiddleware
2
- from starlette.requests import Request
3
- from starlette.responses import JSONResponse
4
- from app.core.config import settings
5
-
6
-
7
- class RequestSizeLimitMiddleware(BaseHTTPMiddleware):
8
- async def dispatch(self, request: Request, call_next):
9
- content_length = request.headers.get("Content-Length")
10
-
11
- if content_length:
12
- size_bytes = int(content_length)
13
- max_bytes = settings.MAX_FILE_SIZE_MB * 1024 * 1024
14
- if size_bytes > max_bytes:
15
- return JSONResponse(
16
- status_code=413,
17
- content={
18
- "detail": f"Request body exceeds maximum allowed size of {settings.MAX_FILE_SIZE_MB}MB."
19
- },
20
- )
21
-
22
- return await call_next(request)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/middleware/security_headers.py DELETED
@@ -1,27 +0,0 @@
1
- from starlette.middleware.base import BaseHTTPMiddleware
2
- from starlette.requests import Request
3
- from starlette.responses import Response
4
-
5
-
6
- class SecurityHeadersMiddleware(BaseHTTPMiddleware):
7
- async def dispatch(self, request: Request, call_next) -> Response:
8
- response = await call_next(request)
9
- response.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains; preload"
10
- response.headers["X-Content-Type-Options"] = "nosniff"
11
- # response.headers["X-Frame-Options"] = "DENY"
12
- response.headers["X-XSS-Protection"] = "1; mode=block"
13
- response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
14
- response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
15
- # Relax CSP for file rendering (e.g., PDFs and images)
16
- response.headers["Content-Security-Policy"] = (
17
- "default-src 'self'; "
18
- "script-src 'self'; "
19
- "style-src 'self'; "
20
- "img-src 'self' data: *; "
21
- "object-src 'self' *;"
22
- )
23
- # For file serving, we might want to allow caching, but for now we just ensure it doesn't force download
24
- if not request.url.path.startswith("/v1/getfile"):
25
- response.headers["Cache-Control"] = "no-store"
26
- response.headers["Pragma"] = "no-cache"
27
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/models/__pycache__/schemas.cpython-311.pyc DELETED
Binary file (1.98 kB)
 
app/models/schemas.py DELETED
@@ -1,32 +0,0 @@
1
- from datetime import datetime
2
- from typing import List, Optional
3
- from pydantic import BaseModel
4
-
5
-
6
- class FileInfo(BaseModel):
7
- id: int
8
- fileid: str
9
- filename: str
10
- file_url: str
11
- createdat: datetime
12
- updatedat: datetime
13
-
14
- class Config:
15
- from_attributes = True
16
-
17
-
18
- class UploadResponse(BaseModel):
19
- success: bool
20
- data: Optional[FileInfo] = None
21
-
22
-
23
- class FileListResponse(BaseModel):
24
- success: bool
25
- total: int
26
- items: List[FileInfo]
27
-
28
-
29
- class DeleteResponse(BaseModel):
30
- success: bool
31
- message: str
32
- fileid: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/routers/__pycache__/files.cpython-311.pyc DELETED
Binary file (3.73 kB)
 
app/routers/__pycache__/ping.cpython-311.pyc DELETED
Binary file (1.14 kB)
 
app/routers/files.py DELETED
@@ -1,83 +0,0 @@
1
- from typing import List
2
-
3
- from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile
4
- from fastapi.responses import FileResponse, Response
5
- from sqlalchemy import select
6
- from sqlalchemy.ext.asyncio import AsyncSession
7
-
8
- from app.db.session import FileRecord, get_db
9
- from app.models.schemas import DeleteResponse, FileInfo, FileListResponse, UploadResponse
10
- from app.services.file_service import file_service
11
-
12
- router = APIRouter()
13
-
14
-
15
- @router.post("/upload", response_model=UploadResponse, status_code=201)
16
- async def upload_file(
17
- file: UploadFile = File(...),
18
- db: AsyncSession = Depends(get_db),
19
- ) -> UploadResponse:
20
- return await file_service.upload_file(db, file)
21
-
22
-
23
- @router.post("/bulk-upload", response_model=List[UploadResponse], status_code=201)
24
- async def bulk_upload_files(
25
- files: List[UploadFile] = File(...),
26
- db: AsyncSession = Depends(get_db),
27
- ) -> List[UploadResponse]:
28
- return await file_service.bulk_upload_files(db, files)
29
-
30
-
31
- @router.get("/all", response_model=FileListResponse)
32
- async def list_files(db: AsyncSession = Depends(get_db)) -> FileListResponse:
33
- return await file_service.list_files(db)
34
-
35
-
36
- @router.get("/get", response_model=FileInfo)
37
- async def get_file_info(
38
- fileid: str = Query(..., description="UUID of the uploaded file"),
39
- db: AsyncSession = Depends(get_db),
40
- ) -> FileInfo:
41
- return await file_service.get_file_info(db, fileid)
42
-
43
-
44
- @router.delete("/", response_model=DeleteResponse)
45
- async def delete_file(
46
- fileid: str = Query(..., description="UUID of the file to delete"),
47
- db: AsyncSession = Depends(get_db),
48
- ) -> DeleteResponse:
49
- return await file_service.delete_file(db, fileid)
50
-
51
-
52
- @router.api_route("/getfile", methods=["GET", "HEAD"])
53
- async def serve_file(
54
- request: Request,
55
- fileid: str = Query(..., description="UUID of the file to serve"),
56
- db: AsyncSession = Depends(get_db),
57
- ):
58
- result = await db.execute(select(FileRecord).where(FileRecord.fileid == fileid))
59
- db_record = result.scalar_one_or_none()
60
- if not db_record:
61
- raise HTTPException(status_code=404, detail="File not found")
62
-
63
- file_path = file_service.get_file_path(fileid)
64
- mime_type = file_service.get_mime_type(db_record.filename) or "application/octet-stream"
65
- file_size = file_path.stat().st_size
66
-
67
- base_headers = {
68
- "Content-Disposition": f'inline; filename="{db_record.filename}"',
69
- "Content-Length": str(file_size),
70
- "Accept-Ranges": "bytes",
71
- }
72
-
73
- if request.method == "HEAD":
74
- return Response(
75
- status_code=200,
76
- headers={**base_headers, "Content-Type": mime_type},
77
- )
78
-
79
- return FileResponse(
80
- path=file_path,
81
- media_type=mime_type,
82
- headers=base_headers,
83
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/routers/ping.py DELETED
@@ -1,22 +0,0 @@
1
- from fastapi import APIRouter
2
- from fastapi.responses import JSONResponse
3
- from app.core.config import settings
4
-
5
- router = APIRouter()
6
-
7
-
8
- @router.get("/", tags=["root"])
9
- async def root():
10
- return JSONResponse(
11
- content={
12
- "service": settings.APP_NAME,
13
- "version": settings.APP_VERSION,
14
- "status": "online",
15
- "docs": "/docs"
16
- }
17
- )
18
-
19
-
20
- @router.get("/ping", tags=["ping"])
21
- async def ping():
22
- return {"message": f"{settings.APP_NAME} is running..."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/services/__pycache__/file_service.cpython-311.pyc DELETED
Binary file (8.32 kB)
 
app/services/file_service.py DELETED
@@ -1,179 +0,0 @@
1
- import uuid
2
- import mimetypes
3
- from pathlib import Path
4
-
5
- import aiofiles
6
- import aiofiles.os
7
- from fastapi import HTTPException, UploadFile
8
- from sqlalchemy import select
9
- from sqlalchemy.ext.asyncio import AsyncSession
10
-
11
- from app.core.config import settings
12
- from app.core.logger import get_logger
13
- from app.db.session import FileRecord
14
- from app.models.schemas import DeleteResponse, FileInfo, FileListResponse, UploadResponse
15
-
16
- logger = get_logger(__name__)
17
-
18
-
19
- class FileService:
20
- RENDERABLE_MIME_TYPES: dict[str, str] = {
21
- ".png": "image/png",
22
- ".jpg": "image/jpeg",
23
- ".jpeg": "image/jpeg",
24
- ".gif": "image/gif",
25
- ".webp": "image/webp",
26
- ".svg": "image/svg+xml",
27
- ".apng": "image/apng",
28
- ".avif": "image/avif",
29
- ".bmp": "image/bmp",
30
- ".ico": "image/vnd.microsoft.icon",
31
- ".tif": "image/tiff",
32
- ".tiff": "image/tiff",
33
- ".aac": "audio/aac",
34
- ".mp3": "audio/mpeg",
35
- ".oga": "audio/ogg",
36
- ".ogg": "audio/ogg",
37
- ".wav": "audio/wav",
38
- ".weba": "audio/webm",
39
- ".midi": "audio/midi",
40
- ".mid": "audio/midi",
41
- ".mp4": "video/mp4",
42
- ".mpeg": "video/mpeg",
43
- ".ogv": "video/ogg",
44
- ".webm": "video/webm",
45
- ".ts": "video/mp2t",
46
- ".pdf": "application/pdf",
47
- ".txt": "text/plain",
48
- ".html": "text/html",
49
- ".htm": "text/html",
50
- ".css": "text/css",
51
- ".js": "text/javascript",
52
- ".json": "application/json",
53
- ".jsonld": "application/ld+json",
54
- ".md": "text/markdown",
55
- ".xml": "application/xml",
56
- ".xhtml": "application/xhtml+xml",
57
- ".csv": "text/csv",
58
- ".ics": "text/calendar",
59
- ".otf": "font/otf",
60
- ".ttf": "font/ttf",
61
- ".woff": "font/woff",
62
- ".woff2": "font/woff2",
63
- }
64
-
65
- def __init__(self) -> None:
66
- self.files_dir = Path(settings.FILES_DIR)
67
- self.files_dir.mkdir(parents=True, exist_ok=True)
68
-
69
- def _generate_file_id(self) -> str:
70
- return f"temp-{uuid.uuid4()}"
71
-
72
- def _get_file_url(self, fileid: str) -> str:
73
- return f"{settings.APP_BASE_URL}/v1/getfile?fileid={fileid}"
74
-
75
- async def upload_file(self, db: AsyncSession, file: UploadFile) -> UploadResponse:
76
- fileid = self._generate_file_id()
77
- filename = file.filename
78
- file_path = self.files_dir / fileid
79
-
80
- logger.info("Starting file upload: %s", filename)
81
-
82
- try:
83
- async with aiofiles.open(file_path, "wb") as buffer:
84
- while chunk := await file.read(1024 * 1024):
85
- await buffer.write(chunk)
86
-
87
- logger.info("File saved to disk: %s", file_path)
88
-
89
- file_url = self._get_file_url(fileid)
90
- db_record = FileRecord(fileid=fileid, filename=filename, file_url=file_url)
91
- db.add(db_record)
92
- await db.commit()
93
- await db.refresh(db_record)
94
-
95
- logger.info("File record saved to database: %s", fileid)
96
-
97
- return UploadResponse(success=True, data=FileInfo.from_orm(db_record))
98
-
99
- except Exception as exc:
100
- logger.error("Error during file upload: %s", str(exc))
101
- if await aiofiles.os.path.exists(file_path):
102
- await aiofiles.os.remove(file_path)
103
- await db.rollback()
104
- raise HTTPException(status_code=500, detail="Internal server error during file upload")
105
-
106
- async def bulk_upload_files(self, db: AsyncSession, files: list[UploadFile]) -> list[UploadResponse]:
107
- responses: list[UploadResponse] = []
108
- for file in files:
109
- try:
110
- response = await self.upload_file(db, file)
111
- responses.append(response)
112
- except HTTPException as exc:
113
- logger.error("Failed to upload file %s: %s", file.filename, exc.detail)
114
- responses.append(UploadResponse(success=False, data=None))
115
- except Exception as exc:
116
- logger.error("Unexpected error during bulk upload for file %s: %s", file.filename, str(exc))
117
- responses.append(UploadResponse(success=False, data=None))
118
- return responses
119
-
120
- async def get_file_info(self, db: AsyncSession, fileid: str) -> FileInfo:
121
- logger.info("Fetching file info: %s", fileid)
122
- result = await db.execute(select(FileRecord).where(FileRecord.fileid == fileid))
123
- db_record = result.scalar_one_or_none()
124
- if not db_record:
125
- logger.warning("File not found in database: %s", fileid)
126
- raise HTTPException(status_code=404, detail="File not found")
127
- return FileInfo.from_orm(db_record)
128
-
129
- async def list_files(self, db: AsyncSession) -> FileListResponse:
130
- logger.info("Listing all files")
131
- result = await db.execute(select(FileRecord))
132
- records = result.scalars().all()
133
- items = [FileInfo.from_orm(r) for r in records]
134
- return FileListResponse(success=True, total=len(items), items=items)
135
-
136
- async def delete_file(self, db: AsyncSession, fileid: str) -> DeleteResponse:
137
- logger.info("Starting file deletion: %s", fileid)
138
- result = await db.execute(select(FileRecord).where(FileRecord.fileid == fileid))
139
- db_record = result.scalar_one_or_none()
140
- if not db_record:
141
- logger.warning("File not found for deletion: %s", fileid)
142
- raise HTTPException(status_code=404, detail="File not found")
143
-
144
- file_path = self.files_dir / fileid
145
- try:
146
- if await aiofiles.os.path.exists(file_path):
147
- await aiofiles.os.remove(file_path)
148
- logger.info("File removed from disk: %s", file_path)
149
-
150
- await db.delete(db_record)
151
- await db.commit()
152
- logger.info("File record removed from database: %s", fileid)
153
-
154
- return DeleteResponse(success=True, message="File deleted successfully", fileid=fileid)
155
-
156
- except Exception as exc:
157
- logger.error("Error during file deletion: %s", str(exc))
158
- await db.rollback()
159
- raise HTTPException(status_code=500, detail="Internal server error during file deletion")
160
-
161
- def get_file_path(self, fileid: str) -> Path:
162
- file_path = self.files_dir / fileid
163
- if not file_path.exists():
164
- logger.warning("File path does not exist: %s", file_path)
165
- raise HTTPException(status_code=404, detail="File not found on disk")
166
- return file_path
167
-
168
- def get_mime_type(self, filename: str) -> str:
169
- extension = Path(filename).suffix.lower()
170
- if extension in self.RENDERABLE_MIME_TYPES:
171
- return self.RENDERABLE_MIME_TYPES[extension]
172
- mime_type, _ = mimetypes.guess_type(filename)
173
- if mime_type:
174
- return mime_type
175
- return "application/octet-stream"
176
-
177
-
178
- file_service = FileService()
179
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db/file_service.db DELETED
Binary file (16.4 kB)
 
requirements.txt DELETED
@@ -1,13 +0,0 @@
1
- fastapi==0.115.0
2
- uvicorn[standard]==0.30.6
3
- python-multipart==0.0.9
4
- pydantic-settings==2.5.2
5
- aiofiles==24.1.0
6
- pyJWT==2.10.1
7
- sqlalchemy==2.0.25
8
- aiosqlite==0.20.0
9
- python-dotenv==1.0.0
10
- httpx>=0.27.0
11
- redis[hiredis]==6.2.0
12
- pyfiglet==1.0.2
13
- rich==14.1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
start.sh DELETED
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -e
3
-
4
- python - <<EOF
5
- from app.banner import print_banner
6
- print_banner()
7
- EOF
8
-
9
- exec uvicorn app.main:app --host 0.0.0.0 --port 7860 --workers 4