Merge branch 'master' of gogs:dm/SFS
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -59,3 +59,4 @@ docs/_build/
|
|||||||
target/
|
target/
|
||||||
|
|
||||||
*.db
|
*.db
|
||||||
|
.env
|
||||||
|
|||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||||
|
&& ln -s /root/.local/bin/uv /usr/local/bin/uv
|
||||||
|
|
||||||
|
ENV XDG_CACHE_HOME=/root/.cache \
|
||||||
|
UV_CACHE_DIR=/root/.cache/uv \
|
||||||
|
UV_LINK_MODE=copy \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
RUN uv sync --frozen --no-dev
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uv", "run", "uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
41
app/api.py
41
app/api.py
@@ -1,10 +1,23 @@
|
|||||||
from fastapi import FastAPI, File, UploadFile, HTTPException
|
from fastapi import FastAPI, File, UploadFile, Depends, HTTPException, Security
|
||||||
from fastapi.responses import FileResponse, PlainTextResponse
|
from fastapi.responses import FileResponse, PlainTextResponse
|
||||||
|
from fastapi.security import APIKeyHeader
|
||||||
from sqlalchemy import exists
|
from sqlalchemy import exists
|
||||||
import hashlib
|
import hashlib
|
||||||
from . import db
|
from . import db
|
||||||
|
from dotenv import load_dotenv
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
FILES_DIR = os.getenv("FILES_DIR")
|
||||||
|
API_KEY = os.getenv("API_KEY")
|
||||||
|
api_key_header = APIKeyHeader(name="X-API-Key")
|
||||||
|
|
||||||
|
def verify_api_key(api_key: str = Security(api_key_header)):
|
||||||
|
if api_key != API_KEY:
|
||||||
|
raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
return api_key
|
||||||
|
|
||||||
def compute_hash(data: bytes, algorithm="sha256") -> str:
|
def compute_hash(data: bytes, algorithm="sha256") -> str:
|
||||||
h = hashlib.new(algorithm)
|
h = hashlib.new(algorithm)
|
||||||
h.update(data)
|
h.update(data)
|
||||||
@@ -12,14 +25,13 @@ def compute_hash(data: bytes, algorithm="sha256") -> str:
|
|||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
FILES_DIR = "./files"
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def root():
|
async def root():
|
||||||
return {"message": "hiii from sfs"}
|
return {"message": "hiii from sfs"}
|
||||||
|
|
||||||
@app.post("/file")
|
@app.post("/file")
|
||||||
async def save_file(file: UploadFile = File(...)):
|
async def save_file(file: UploadFile = File(...), api_key: str = Depends(verify_api_key)):
|
||||||
contents = await file.read()
|
contents = await file.read()
|
||||||
|
|
||||||
hash = compute_hash(contents)
|
hash = compute_hash(contents)
|
||||||
@@ -29,15 +41,18 @@ async def save_file(file: UploadFile = File(...)):
|
|||||||
if not existed_url:
|
if not existed_url:
|
||||||
file_url = db.add_file(file.filename, file.content_type, file.size, hash)
|
file_url = db.add_file(file.filename, file.content_type, file.size, hash)
|
||||||
|
|
||||||
print(f"{FILES_DIR}/{file_url}")
|
try:
|
||||||
with open(f"{FILES_DIR}/{file_url}", "wb") as f:
|
with open(f"{FILES_DIR}/{file_url}", "wb") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
return {"status": "saved", "filename": file_url}
|
return {"status": "saved", "filename": file_url}
|
||||||
|
except Exception as e:
|
||||||
|
db.remove_file(file_url)
|
||||||
|
return {"status": "error", "message": f"Could not save file {file_url}: {e}"}
|
||||||
else:
|
else:
|
||||||
return {"status": "file_exists", "filename": existed_url}
|
return {"status": "file_exists", "filename": existed_url}
|
||||||
|
|
||||||
@app.get("/file/{filename}")
|
@app.get("/file/{filename}")
|
||||||
def get_file(filename: str, raw: bool = False):
|
async def get_file(filename: str, raw: bool = False, api_key: str = Depends(verify_api_key)):
|
||||||
file_path = os.path.join(FILES_DIR, filename)
|
file_path = os.path.join(FILES_DIR, filename)
|
||||||
|
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
@@ -48,8 +63,18 @@ def get_file(filename: str, raw: bool = False):
|
|||||||
|
|
||||||
return FileResponse(file_path, filename=filename)
|
return FileResponse(file_path, filename=filename)
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/file/{filename}")
|
||||||
|
async def delete_file(filename: str, api_key: str = Depends(verify_api_key)):
|
||||||
|
if db.remove_file(filename):
|
||||||
|
file_path = f"{FILES_DIR}/{filename}"
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
return {"status": "deleted"}
|
||||||
|
return {"status": "error", "message": "no file like that"}
|
||||||
|
|
||||||
@app.get("/files/")
|
@app.get("/files/")
|
||||||
def get_list_of_files():
|
async def get_list_of_files(api_key: str = Depends(verify_api_key)):
|
||||||
files = db.get_all_files()
|
files = db.get_all_files()
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -63,5 +88,5 @@ def get_list_of_files():
|
|||||||
]
|
]
|
||||||
|
|
||||||
@app.get("/healthchecker")
|
@app.get("/healthchecker")
|
||||||
def healthchecker():
|
async def healthchecker(api_key: str = Depends(verify_api_key)):
|
||||||
return {"message": "Howdy :3"}
|
return {"message": "Howdy :3"}
|
||||||
|
|||||||
35
app/db.py
35
app/db.py
@@ -4,9 +4,13 @@ from sqlalchemy import create_engine
|
|||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
PADDING = 5
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
load_dotenv()
|
||||||
|
PADDING = int(os.getenv("FILES_PADDING"))
|
||||||
|
DATABASE_NAME = os.getenv("DATABASE_NAME")
|
||||||
|
|
||||||
engine = create_engine("sqlite:///example.db")
|
engine = create_engine(f"sqlite:///{DATABASE_NAME}.db")
|
||||||
session = Session(bind=engine)
|
session = Session(bind=engine)
|
||||||
|
|
||||||
class Base(DeclarativeBase): pass
|
class Base(DeclarativeBase): pass
|
||||||
@@ -38,6 +42,22 @@ def to_base36(n: int, width: int) -> str:
|
|||||||
result.append(chars[rem])
|
result.append(chars[rem])
|
||||||
return "".join(reversed(result)).rjust(width, "0")
|
return "".join(reversed(result)).rjust(width, "0")
|
||||||
|
|
||||||
|
|
||||||
|
def from_base36(s: str) -> int:
|
||||||
|
chars = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||||
|
char_to_val = {c: i for i, c in enumerate(chars)}
|
||||||
|
|
||||||
|
s = s.lower().lstrip("0")
|
||||||
|
if not s:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
for ch in s:
|
||||||
|
if ch not in char_to_val:
|
||||||
|
raise ValueError(f"Invalid base36 character: {ch}")
|
||||||
|
n = n * 36 + char_to_val[ch]
|
||||||
|
return n
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
def get_all_files():
|
def get_all_files():
|
||||||
@@ -83,6 +103,17 @@ def add_file(filename: str, content_type, size: int, hash):
|
|||||||
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def remove_file(file_url: str):
|
||||||
|
with Session(autoflush=False, bind=engine) as db:
|
||||||
|
file_id = from_base36(file_url.rsplit(".")[0])
|
||||||
|
file = db.get(File, file_id)
|
||||||
|
if not file:
|
||||||
|
return None
|
||||||
|
db.delete(file)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
for i in get_all_files():
|
for i in get_all_files():
|
||||||
print(f"{i.id} {i.name}.{i.extension} ({i.hash}) {i.content_type} {i.size}")
|
print(f"{i.id} {i.name}.{i.extension} ({i.hash}) {i.content_type} {i.size}")
|
||||||
|
|||||||
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: sfs
|
||||||
|
command: uv run uvicorn app.api:app --host 0.0.0.0 --port 8000
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- ./files:/app/files
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
@@ -5,6 +5,7 @@ description = "Add your description here"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"dotenv>=0.9.9",
|
||||||
"fastapi>=0.116.1",
|
"fastapi>=0.116.1",
|
||||||
"python-multipart>=0.0.20",
|
"python-multipart>=0.0.20",
|
||||||
"sqlalchemy>=2.0.43",
|
"sqlalchemy>=2.0.43",
|
||||||
|
|||||||
24
uv.lock
generated
24
uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -46,6 +46,17 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenv"
|
||||||
|
version = "0.9.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.116.1"
|
version = "0.116.1"
|
||||||
@@ -168,6 +179,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.20"
|
version = "0.0.20"
|
||||||
@@ -182,6 +202,7 @@ name = "sfs"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "dotenv" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
{ name = "sqlalchemy" },
|
{ name = "sqlalchemy" },
|
||||||
@@ -190,6 +211,7 @@ dependencies = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||||
{ name = "fastapi", specifier = ">=0.116.1" },
|
{ name = "fastapi", specifier = ">=0.116.1" },
|
||||||
{ name = "python-multipart", specifier = ">=0.0.20" },
|
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||||
{ name = "sqlalchemy", specifier = ">=2.0.43" },
|
{ name = "sqlalchemy", specifier = ">=2.0.43" },
|
||||||
|
|||||||
Reference in New Issue
Block a user