From 45ed8f9117df7b968d95c702b65c27937664cf6d Mon Sep 17 00:00:00 2001 From: dm <> Date: Sun, 31 Aug 2025 16:12:29 +0300 Subject: [PATCH] added ftp support --- .env | 3 +++ README.md | 8 ------- app/api.py | 54 ++++++++++++++++++++++++++++++++++------------ docker-compose.yml | 13 +++++++++-- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/.env b/.env index ff66ebf..b1b8cba 100644 --- a/.env +++ b/.env @@ -2,3 +2,6 @@ FILES_DIR="./files" API_KEY="aboba" FILES_PADDING="5" DATABASE_NAME="files.db" +FTP_URL="ftp" +FTP_LOGIN="sfs" +FTP_PASSWORD="MySecurePass123!" diff --git a/README.md b/README.md index b6d8c1b..683e200 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,6 @@ simple file server -### .env example -``` -FILES_DIR="./files" -API_KEY="aboba" -FILES_PADDING="5" -DATABASE_NAME="files.db" -``` - Working mpv - [x] get/post files diff --git a/app/api.py b/app/api.py index 5e241a0..9bfc408 100644 --- a/app/api.py +++ b/app/api.py @@ -1,8 +1,10 @@ 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 sqlalchemy import exists import hashlib +from ftplib import FTP +from io import BytesIO from . import db from dotenv import load_dotenv import os @@ -13,6 +15,10 @@ FILES_DIR = os.getenv("FILES_DIR") API_KEY = os.getenv("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)): if api_key != API_KEY: 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) try: - with open(f"{FILES_DIR}/{file_url}", "wb") as f: - f.write(contents) + ftp = FTP(FTP_URL) + 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} except Exception as e: db.remove_file(file_url) @@ -53,25 +67,37 @@ async def save_file(file: UploadFile = File(...), api_key: str = Depends(verify_ @app.get("/file/{filename}") 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: - with open(file_path, "r", encoding="utf-8", errors="ignore") as f: - return PlainTextResponse(f.read()) + return PlainTextResponse(buffer.getvalue().decode("utf-8", errors="ignore")) - 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}") 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) + if not db.remove_file(filename): + 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"} - return {"status": "error", "message": "no file like that"} + except Exception as e: + return {"status": "error", "message": f"Could not delete file {filename}: {e}"} @app.get("/files/") async def get_list_of_files(api_key: str = Depends(verify_api_key)): diff --git a/docker-compose.yml b/docker-compose.yml index 0a8b08b..cd585e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,22 @@ services: web: build: . - container_name: sfs + container_name: sfs.api 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 + 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