added ftp support

This commit is contained in:
dm
2025-08-31 16:12:29 +03:00
parent 8c0bff24bd
commit 45ed8f9117
4 changed files with 54 additions and 24 deletions

3
.env
View File

@@ -2,3 +2,6 @@ FILES_DIR="./files"
API_KEY="aboba" API_KEY="aboba"
FILES_PADDING="5" FILES_PADDING="5"
DATABASE_NAME="files.db" DATABASE_NAME="files.db"
FTP_URL="ftp"
FTP_LOGIN="sfs"
FTP_PASSWORD="MySecurePass123!"

View File

@@ -2,14 +2,6 @@
simple file server simple file server
### .env example
```
FILES_DIR="./files"
API_KEY="aboba"
FILES_PADDING="5"
DATABASE_NAME="files.db"
```
Working mpv Working mpv
- [x] get/post files - [x] get/post files

View File

@@ -1,8 +1,10 @@
from fastapi import FastAPI, File, UploadFile, Depends, HTTPException, Security from fastapi import FastAPI, File, UploadFile, Depends, HTTPException, Security
from fastapi.responses import FileResponse, PlainTextResponse from fastapi.responses import FileResponse, PlainTextResponse, StreamingResponse
from fastapi.security import APIKeyHeader from fastapi.security import APIKeyHeader
from sqlalchemy import exists from sqlalchemy import exists
import hashlib import hashlib
from ftplib import FTP
from io import BytesIO
from . import db from . import db
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
@@ -13,6 +15,10 @@ FILES_DIR = os.getenv("FILES_DIR")
API_KEY = os.getenv("API_KEY") API_KEY = os.getenv("API_KEY")
api_key_header = APIKeyHeader(name="X-API-Key") api_key_header = APIKeyHeader(name="X-API-Key")
FTP_URL = os.getenv("FTP_URL")
FTP_LOGIN = os.getenv("FTP_LOGIN")
FTP_PASSWORD = os.getenv("FTP_PASSWORD")
def verify_api_key(api_key: str = Security(api_key_header)): def verify_api_key(api_key: str = Security(api_key_header)):
if api_key != API_KEY: if api_key != API_KEY:
raise HTTPException(status_code=403, detail="Forbidden") raise HTTPException(status_code=403, detail="Forbidden")
@@ -42,8 +48,16 @@ async def save_file(file: UploadFile = File(...), api_key: str = Depends(verify_
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)
try: try:
with open(f"{FILES_DIR}/{file_url}", "wb") as f: ftp = FTP(FTP_URL)
f.write(contents) ftp.login(FTP_LOGIN, FTP_PASSWORD)
buffer = BytesIO(contents)
try:
ftp.mkd(FILES_DIR)
except:
pass
ftp.storbinary(f"STOR {FILES_DIR}/{file_url}", buffer)
ftp.quit()
return {"status": "saved", "filename": file_url} return {"status": "saved", "filename": file_url}
except Exception as e: except Exception as e:
db.remove_file(file_url) db.remove_file(file_url)
@@ -53,26 +67,38 @@ async def save_file(file: UploadFile = File(...), api_key: str = Depends(verify_
@app.get("/file/{filename}") @app.get("/file/{filename}")
async def get_file(filename: str, raw: bool = False, api_key: str = Depends(verify_api_key)): async def get_file(filename: str, raw: bool = False, api_key: str = Depends(verify_api_key)):
file_path = os.path.join(FILES_DIR, filename) try:
ftp = FTP(FTP_URL)
ftp.login(FTP_LOGIN, FTP_PASSWORD)
buffer = BytesIO()
ftp.retrbinary(f"RETR {FILES_DIR}/{filename}", buffer.write)
ftp.quit()
buffer.seek(0)
except Exception as e:
raise HTTPException(status_code=404, detail=f"File not found: {e}")
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="File not found")
if raw: if raw:
with open(file_path, "r", encoding="utf-8", errors="ignore") as f: return PlainTextResponse(buffer.getvalue().decode("utf-8", errors="ignore"))
return PlainTextResponse(f.read())
return FileResponse(file_path, filename=filename) return StreamingResponse(buffer, media_type="application/octet-stream", headers={"Content-Disposition": f'attachment; filename="{filename}"'})
from ftplib import FTP
@app.delete("/file/{filename}") @app.delete("/file/{filename}")
async def delete_file(filename: str, api_key: str = Depends(verify_api_key)): async def delete_file(filename: str, api_key: str = Depends(verify_api_key)):
if db.remove_file(filename): if not 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"} return {"status": "error", "message": "no file like that"}
try:
ftp = FTP(FTP_URL)
ftp.login(FTP_LOGIN, FTP_PASSWORD)
ftp.delete(f"{FILES_DIR}/{filename}")
ftp.quit()
return {"status": "deleted"}
except Exception as e:
return {"status": "error", "message": f"Could not delete file {filename}: {e}"}
@app.get("/files/") @app.get("/files/")
async def get_list_of_files(api_key: str = Depends(verify_api_key)): async def get_list_of_files(api_key: str = Depends(verify_api_key)):
return db.get_all_files() return db.get_all_files()

View File

@@ -1,13 +1,22 @@
services: services:
web: web:
build: . build: .
container_name: sfs container_name: sfs.api
command: uv run uvicorn app.api:app --host 0.0.0.0 --port 8000 command: uv run uvicorn app.api:app --host 0.0.0.0 --port 8000
ports: ports:
- "8000:8000" - "8000:8000"
volumes: volumes:
- .:/app - .:/app
- ./files:/app/files
env_file: env_file:
- .env - .env
restart: unless-stopped restart: unless-stopped
ftp:
image: delfer/alpine-ftp-server
container_name: sfs.ftp
ports: []
environment:
USERS: "sfs|MySecurePass123!|/home/sfs"
ADDRESS: "ftp"
volumes:
- /opt/sfs/files:/home/sfs
restart: unless-stopped