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" FILES_DIR="./files"
API_KEY="aboba" API_KEY_HASH="a6c79a27049109e472b246b5dfbe08aedff1e9e2259597e54032dbad4958d4ad"
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,26 +2,16 @@
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
- [x] security from dublicats - [x] security from dublicats
- [x] security, using tokens - [x] security, using tokens
- [x] works in docker - [x] works in docker
- [x] make it use ftp server
- [ ] crypt files
- [ ] make it use ftp server - [ ] make it connect to db
- [ ] make it use db
- [ ] multiuser system - [ ] multiuser system
- [ ] files ownhership, owner and groups, roles - [ ] files ownhership, owner and groups, roles
- [ ] archive - [ ] archive
- [ ] share link - [ ] share link

View File

@@ -1,21 +1,30 @@
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
import hmac
load_dotenv() load_dotenv()
FILES_DIR = os.getenv("FILES_DIR") 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") 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: api_key_hashed = hashlib.sha256(api_key.encode()).hexdigest()
raise HTTPException(status_code=403, detail="Forbidden") if not hmac.compare_digest(api_key_hashed, API_KEY_HASH):
raise HTTPException(status_code=403, detail="Forbidden. (╥﹏╥)")
return api_key return api_key
def compute_hash(data: bytes, algorithm="sha256") -> str: 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) 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,25 +70,37 @@ 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}" return {"status": "error", "message": "no file like that"}
if os.path.exists(file_path):
os.remove(file_path) try:
ftp = FTP(FTP_URL)
ftp.login(FTP_LOGIN, FTP_PASSWORD)
ftp.delete(f"{FILES_DIR}/{filename}")
ftp.quit()
return {"status": "deleted"} 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/") @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)):

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