Progress
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import aiofiles
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from devplacepy.database import get_table, get_setting
|
||||
from devplacepy.config import STATIC_DIR
|
||||
from devplacepy.utils import generate_uid, require_user
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"}
|
||||
|
||||
|
||||
def _store_file(content: bytes, original_filename: str) -> tuple[str, str]:
|
||||
uid = generate_uid()
|
||||
ext = Path(original_filename).suffix.lower() or ""
|
||||
stored_name = f"{uid}{ext}"
|
||||
hash_str = hashlib.sha256(stored_name.encode()).hexdigest()
|
||||
subdir = f"{hash_str[:2]}/{hash_str[2:4]}"
|
||||
storage_path = f"{subdir}/{stored_name}"
|
||||
full_dir = STATIC_DIR / "uploads" / subdir
|
||||
full_dir.mkdir(parents=True, exist_ok=True)
|
||||
full_path = full_dir / stored_name
|
||||
with open(str(full_path), "wb") as f:
|
||||
f.write(content)
|
||||
return uid, storage_path
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_file(request: Request):
|
||||
user = require_user(request)
|
||||
form = await request.form()
|
||||
file = form.get("file")
|
||||
|
||||
if not file or not hasattr(file, "filename") or not file.filename:
|
||||
return JSONResponse({"error": "No file provided"}, status_code=400)
|
||||
|
||||
max_size_mb = int(get_setting("max_upload_size_mb", "10"))
|
||||
max_size_bytes = max_size_mb * 1024 * 1024
|
||||
allowed_types_raw = get_setting("allowed_file_types", "").strip()
|
||||
allowed_extensions = set()
|
||||
if allowed_types_raw:
|
||||
for ext in allowed_types_raw.split(","):
|
||||
ext = ext.strip().lower()
|
||||
if ext.startswith("."):
|
||||
allowed_extensions.add(ext)
|
||||
else:
|
||||
allowed_extensions.add(f".{ext}")
|
||||
|
||||
try:
|
||||
content = await file.read()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read uploaded file: {e}")
|
||||
return JSONResponse({"error": "Failed to read file"}, status_code=400)
|
||||
|
||||
if len(content) > max_size_bytes:
|
||||
return JSONResponse({"error": f"File exceeds {max_size_mb}MB limit"}, status_code=413)
|
||||
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
if allowed_extensions and ext not in allowed_extensions:
|
||||
return JSONResponse({"error": f"File type '{ext}' not allowed"}, status_code=415)
|
||||
|
||||
uid, storage_path = _store_file(content, file.filename)
|
||||
|
||||
attachments = get_table("attachments")
|
||||
attachments.insert({
|
||||
"uid": uid,
|
||||
"resource_uid": "",
|
||||
"resource_type": "",
|
||||
"original_filename": file.filename,
|
||||
"stored_filename": f"{uid}{ext}",
|
||||
"mime_type": file.content_type or "application/octet-stream",
|
||||
"file_size": len(content),
|
||||
"storage_path": storage_path,
|
||||
"created_at": __import__("datetime").datetime.utcnow().isoformat(),
|
||||
})
|
||||
|
||||
is_image = ext in IMAGE_EXTENSIONS
|
||||
file_url = f"/static/uploads/{storage_path}"
|
||||
|
||||
logger.info(f"File uploaded: {file.filename} ({len(content)} bytes) -> {storage_path}")
|
||||
return JSONResponse({
|
||||
"uid": uid,
|
||||
"original_filename": file.filename,
|
||||
"url": file_url,
|
||||
"mime_type": file.content_type or "application/octet-stream",
|
||||
"file_size": len(content),
|
||||
"is_image": is_image,
|
||||
}, status_code=201)
|
||||
|
||||
|
||||
@router.delete("/delete/{attachment_uid}")
|
||||
async def delete_attachment(request: Request, attachment_uid: str):
|
||||
user = require_user(request)
|
||||
attachments = get_table("attachments")
|
||||
att = attachments.find_one(uid=attachment_uid)
|
||||
if not att:
|
||||
return JSONResponse({"error": "Attachment not found"}, status_code=404)
|
||||
if att.get("resource_uid"):
|
||||
resource_type = att["resource_type"]
|
||||
if resource_type == "post":
|
||||
post = get_table("posts").find_one(uid=att["resource_uid"])
|
||||
if not post or post["user_uid"] != user["uid"]:
|
||||
return JSONResponse({"error": "Not authorized"}, status_code=403)
|
||||
elif resource_type == "comment":
|
||||
comment = get_table("comments").find_one(uid=att["resource_uid"])
|
||||
if not comment or comment["user_uid"] != user["uid"]:
|
||||
return JSONResponse({"error": "Not authorized"}, status_code=403)
|
||||
from devplacepy.database import _delete_attachment_file
|
||||
_delete_attachment_file(att.get("storage_path", ""))
|
||||
attachments.delete(id=att["id"])
|
||||
logger.info(f"Attachment {attachment_uid} deleted by {user['username']}")
|
||||
return JSONResponse({"status": "deleted"})
|
||||
Reference in New Issue
Block a user