forked from retoor/devplacepy
chore: add docker infrastructure, gists feature, attachment system, and admin CLI tools
- Add .dockerignore, Dockerfile, and docker compose targets to Makefile for containerized deployment - Implement gists router with CRUD operations, database schema, and polymorphic comment/vote reuse - Create attachment upload system with thumbnail generation, MIME detection, and storage path management - Add attachments_prune CLI command to clean orphaned attachment records and files - Introduce rate limiting middleware with 60 requests per minute window - Add custom 404 and 500 error handlers with SEO-optimized template responses - Extend database initialization with gists and attachments indexes plus upload site settings defaults - Update load_comments to include attachment mapping for comment resources - Register gists and uploads routers in main application and update AGENTS.md documentation
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from devplacepy.database import get_table, load_comments, get_vote_counts, get_attachments, get_attachments_by_type, delete_attachments, resolve_by_slug
|
||||
from devplacepy.templating import templates
|
||||
from devplacepy.utils import generate_uid, get_current_user, require_user, time_ago, make_combined_slug, create_mention_notifications
|
||||
from devplacepy.seo import base_seo_context, site_url, website_schema, breadcrumb_schema, combine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
LANGUAGES = [
|
||||
("python", "Python"), ("javascript", "JavaScript"), ("typescript", "TypeScript"),
|
||||
("html", "HTML"), ("css", "CSS"), ("c", "C"), ("cpp", "C++"), ("java", "Java"),
|
||||
("go", "Go"), ("rust", "Rust"), ("sql", "SQL"), ("bash", "Bash"),
|
||||
("yaml", "YAML"), ("json", "JSON"), ("markdown", "Markdown"),
|
||||
("swift", "Swift"), ("php", "PHP"), ("ruby", "Ruby"), ("kotlin", "Kotlin"),
|
||||
("lua", "Lua"), ("perl", "Perl"), ("haskell", "Haskell"), ("elixir", "Elixir"),
|
||||
("r", "R"), ("dart", "Dart"), ("scala", "Scala"), ("plaintext", "Plain Text"),
|
||||
]
|
||||
|
||||
|
||||
def get_gists_list(user_uid=None, language=None):
|
||||
gists_table = get_table("gists")
|
||||
filters = {}
|
||||
if user_uid:
|
||||
filters["user_uid"] = user_uid
|
||||
if language:
|
||||
filters["language"] = language
|
||||
|
||||
all_gists = list(gists_table.find(**filters, order_by=["-created_at"]))
|
||||
|
||||
if not all_gists:
|
||||
return []
|
||||
|
||||
from devplacepy.database import get_users_by_uids
|
||||
uids = [g["user_uid"] for g in all_gists]
|
||||
users_map = get_users_by_uids(uids)
|
||||
|
||||
result = []
|
||||
for g in all_gists:
|
||||
author = users_map.get(g["user_uid"])
|
||||
result.append({
|
||||
"gist": g,
|
||||
"author": author,
|
||||
"time_ago": time_ago(g["created_at"]),
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{gist_slug}", response_class=HTMLResponse)
|
||||
async def gist_detail(request: Request, gist_slug: str):
|
||||
user = get_current_user(request)
|
||||
gists = get_table("gists")
|
||||
gist = resolve_by_slug(gists, gist_slug)
|
||||
if not gist:
|
||||
raise HTTPException(status_code=404, detail="Gist not found")
|
||||
|
||||
from devplacepy.database import get_users_by_uids
|
||||
users_map = get_users_by_uids([gist["user_uid"]])
|
||||
author = users_map.get(gist["user_uid"])
|
||||
|
||||
is_owner = user and user["uid"] == gist["user_uid"]
|
||||
|
||||
ups, downs = get_vote_counts([gist["uid"]])
|
||||
star_count = ups.get(gist["uid"], 0) - downs.get(gist["uid"], 0)
|
||||
|
||||
comments = load_comments("gist", gist["uid"])
|
||||
|
||||
gist_attachments = get_attachments("gist", gist["uid"])
|
||||
|
||||
base = site_url(request)
|
||||
seo_ctx = base_seo_context(
|
||||
request,
|
||||
title=gist.get("title", "Gist"),
|
||||
description=gist.get("description", "")[:160],
|
||||
breadcrumbs=[
|
||||
{"name": "Home", "url": "/feed"},
|
||||
{"name": "Gists", "url": "/gists"},
|
||||
{"name": gist.get("title", "Gist"), "url": f"/gists/{gist['slug'] or gist['uid']}"},
|
||||
],
|
||||
schemas=[website_schema(base)],
|
||||
)
|
||||
return templates.TemplateResponse("gist_detail.html", {
|
||||
**seo_ctx,
|
||||
"request": request,
|
||||
"user": user,
|
||||
"gist": gist,
|
||||
"author": author,
|
||||
"is_owner": is_owner,
|
||||
"star_count": star_count,
|
||||
"time_ago": time_ago(gist["created_at"]),
|
||||
"comments": comments,
|
||||
"languages": LANGUAGES,
|
||||
"attachments": gist_attachments,
|
||||
})
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_gist(request: Request):
|
||||
user = require_user(request)
|
||||
form = await request.form()
|
||||
title = form.get("title", "").strip()
|
||||
description = form.get("description", "").strip()
|
||||
source_code = form.get("source_code", "").strip()
|
||||
language = form.get("language", "plaintext")
|
||||
|
||||
if not title:
|
||||
return RedirectResponse(url="/gists", status_code=302)
|
||||
if len(title) > 200:
|
||||
return RedirectResponse(url="/gists", status_code=302)
|
||||
if not source_code:
|
||||
return RedirectResponse(url="/gists", status_code=302)
|
||||
if len(source_code) > 50000:
|
||||
return RedirectResponse(url="/gists", status_code=302)
|
||||
if len(description) > 5000:
|
||||
return RedirectResponse(url="/gists", status_code=302)
|
||||
|
||||
valid_languages = {l[0] for l in LANGUAGES}
|
||||
if language not in valid_languages:
|
||||
language = "plaintext"
|
||||
|
||||
gists = get_table("gists")
|
||||
uid = generate_uid()
|
||||
gist_slug = make_combined_slug(title, uid)
|
||||
gists.insert({
|
||||
"uid": uid,
|
||||
"user_uid": user["uid"],
|
||||
"title": title,
|
||||
"slug": gist_slug,
|
||||
"description": description or None,
|
||||
"source_code": source_code,
|
||||
"language": language,
|
||||
"stars": 0,
|
||||
"created_at": datetime.utcnow().isoformat(),
|
||||
})
|
||||
|
||||
attachment_uids = form.getlist("attachment_uids") if hasattr(form, "getlist") else []
|
||||
Reference in New Issue
Block a user