Compare commits

...

6 Commits

Author SHA1 Message Date
dm
19b2afcb43 added hash system 2025-09-04 11:42:33 +03:00
dm
c256f8834e moved to ftp server 2025-08-31 18:35:37 +03:00
dm
7596ce830d fix compose volume err 2025-08-31 17:09:37 +03:00
dm
45ed8f9117 added ftp support 2025-08-31 16:12:29 +03:00
dm
8c0bff24bd new ideas 2025-08-31 13:59:13 +03:00
dm
90ade2d497 Update 'README.md' 2025-08-29 08:58:54 +00:00
4 changed files with 64 additions and 33 deletions

5
.env
View File

@@ -1,4 +1,7 @@
FILES_DIR="./files"
API_KEY="aboba"
API_KEY_HASH="a6c79a27049109e472b246b5dfbe08aedff1e9e2259597e54032dbad4958d4ad"
FILES_PADDING="5"
DATABASE_NAME="files.db"
FTP_URL="ftp"
FTP_LOGIN="sfs"
FTP_PASSWORD="MySecurePass123!"

View File

@@ -2,26 +2,16 @@
simple file server
### .env example
```
FILES_DIR="./files"
API_KEY="aboba"
FILES_PADDING="5"
DATABASE_NAME="files.db"
```
Working mpv
- [x] get/post files
- [x] security from dublicats
- [x] security, using tokens
- [x] works in docker
- [ ] make it use ftp server
- [ ] make it use db
- [x] make it use ftp server
- [ ] crypt files
- [ ] make it connect to db
- [ ] multiuser system
- [ ] files ownhership, owner and groups, roles
- [ ] archive
- [ ] share link

View File

@@ -1,21 +1,30 @@
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
import hmac
load_dotenv()
FILES_DIR = os.getenv("FILES_DIR")
API_KEY = os.getenv("API_KEY")
API_KEY_HASH = os.getenv("API_KEY_HASH")
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")
api_key_hashed = hashlib.sha256(api_key.encode()).hexdigest()
if not hmac.compare_digest(api_key_hashed, API_KEY_HASH):
raise HTTPException(status_code=403, detail="Forbidden. (╥﹏╥)")
return api_key
def compute_hash(data: bytes, algorithm="sha256") -> str:
@@ -42,8 +51,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 +70,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)):

View File

@@ -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:/home/sfs
restart: unless-stopped