Compare commits
6 Commits
1295a8fdc1
...
security
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19b2afcb43 | ||
|
|
c256f8834e | ||
|
|
7596ce830d | ||
|
|
45ed8f9117 | ||
|
|
8c0bff24bd | ||
| 90ade2d497 |
5
.env
5
.env
@@ -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!"
|
||||
|
||||
16
README.md
16
README.md
@@ -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
|
||||
|
||||
|
||||
63
app/api.py
63
app/api.py
@@ -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,26 +70,38 @@ 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)
|
||||
return {"status": "deleted"}
|
||||
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"}
|
||||
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)):
|
||||
return db.get_all_files()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user