feat: add project file system with CRUD, upload, inline editing, and video attachment support

- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
This commit is contained in:
2026-06-08 20:51:09 +00:00
parent e0535bb7c5
commit 97eb58fc19
110 changed files with 5534 additions and 603 deletions
+28 -8
View File
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
from pathlib import Path
from PIL import Image
from io import BytesIO
from devplacepy.database import get_table, db
from devplacepy.database import get_table, db, get_setting
from devplacepy.config import STATIC_DIR
from devplacepy.utils import generate_uid
@@ -28,6 +28,10 @@ ALLOWED_UPLOAD_TYPES = {
".pdf": "application/pdf",
".zip": "application/zip",
".mp4": "video/mp4",
".webm": "video/webm",
".ogv": "video/ogg",
".mov": "video/quicktime",
".m4v": "video/x-m4v",
".mp3": "audio/mpeg",
".txt": "text/plain",
".py": "text/x-python",
@@ -44,6 +48,10 @@ FILE_ICONS = {
".rar": "\U0001F4E6",
".7z": "\U0001F4E6",
".mp4": "\U0001F3AC",
".webm": "\U0001F3AC",
".ogv": "\U0001F3AC",
".mov": "\U0001F3AC",
".m4v": "\U0001F3AC",
".mp3": "\U0001F3B5",
".py": "\U0001F4BB",
".js": "\U0001F4BB",
@@ -64,13 +72,23 @@ FILE_ICONS = {
DEFAULT_FILE_ICON = "\U0001F4CE"
def _get_setting(key, default):
row = get_table("site_settings").find_one(key=key)
return row["value"] if row else default
def _get_max_upload_bytes():
return int(_get_setting("max_upload_size_mb", "10")) * 1024 * 1024
return int(get_setting("max_upload_size_mb", "10")) * 1024 * 1024
def allowed_extensions():
raw = get_setting("allowed_file_types", "").strip()
if raw:
return {
ext if ext.startswith(".") else f".{ext}"
for ext in (part.strip().lower() for part in raw.split(","))
if ext
}
return set(ALLOWED_UPLOAD_TYPES)
def is_extension_allowed(ext):
return ext in allowed_extensions()
def _directory_for(uid):
@@ -136,7 +154,7 @@ def store_attachment(file_bytes, original_filename, user_uid):
if len(file_bytes) > _get_max_upload_bytes():
return None
ext = Path(original_filename).suffix.lower()
if ext not in ALLOWED_UPLOAD_TYPES:
if not is_extension_allowed(ext):
return None
uid = generate_uid()
@@ -180,6 +198,7 @@ def store_attachment(file_bytes, original_filename, user_uid):
"thumbnail_url": f"/static/uploads/attachments/{directory}/{thumbnail}" if thumbnail else None,
"has_thumbnail": thumbnail is not None,
"is_image": is_image,
"is_video": mime.startswith("video/"),
}
@@ -290,6 +309,7 @@ def _row_to_attachment(row):
"thumbnail_url": f"/static/uploads/attachments/{directory}/{thumb_name}" if thumb_name else None,
"has_thumbnail": bool(row.get("has_thumbnail")),
"is_image": row.get("mime_type", "").startswith("image/"),
"is_video": row.get("mime_type", "").startswith("video/"),
}