fix: migrate CI branch references from master to main and add comments created_at index

This commit is contained in:
2026-05-13 19:17:57 +00:00
parent aa88f18c03
commit 4958c23c0d
33 changed files with 770 additions and 203 deletions
+17 -75
View File
@@ -1,33 +1,14 @@
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
from devplacepy.utils import require_user
from devplacepy.attachments import store_attachment, delete_attachment as _delete_attachment
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):
@@ -38,17 +19,16 @@ async def upload_file(request: Request):
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}")
allowed_extensions.add(ext if ext.startswith(".") else f".{ext}")
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)
try:
content = await file.read()
@@ -56,61 +36,23 @@ async def upload_file(request: Request):
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:
result = store_attachment(content, file.filename, user["uid"])
if result is None:
max_size_mb = int(get_setting("max_upload_size_mb", "10"))
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)
logger.info(f"File uploaded: {file.filename} ({len(content)} bytes) -> {result['url']}")
return JSONResponse(result, status_code=201)
@router.delete("/delete/{attachment_uid}")
async def delete_attachment(request: Request, attachment_uid: str):
async def delete_attachment_route(request: Request, attachment_uid: str):
user = require_user(request)
attachments = get_table("attachments")
att = attachments.find_one(uid=attachment_uid)
att = get_table("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"])
if att.get("user_uid") and att["user_uid"] != user["uid"]:
return JSONResponse({"error": "Not authorized"}, status_code=403)
_delete_attachment(attachment_uid)
logger.info(f"Attachment {attachment_uid} deleted by {user['username']}")
return JSONResponse({"status": "deleted"})