server.py
← Back to explorer
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
import os
import base64
import mimetypes
from urllib.parse import quote
from flask import Flask, render_template, abort, request, redirect, url_for, session, flash, jsonify
from PIL import Image
from PIL.ExifTags import TAGS
from werkzeug.utils import secure_filename
from uuid import uuid4
import requests
import markdown as md
import bleach
from utils.metas import projects, github_projects
from utils.utils import header, fetch_github_contents, get_project_metadata, get_file_summary, load_activity_data
from utils.blog import (
ensure_blog_schema,
get_blog_db_connection,
slugify,
client_anon_hash,
utcnow,
)
app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY") or os.urandom(32)
UPLOAD_FOLDER = os.path.join("static", "uploads")
PHOTOS_FOLDER = os.path.join("static", "photos")
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "webp", "gif"}
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
app.config["PHOTOS_FOLDER"] = PHOTOS_FOLDER
def _ensure_upload_dir():
os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)
def _ensure_photos_dir():
os.makedirs(app.config["PHOTOS_FOLDER"], exist_ok=True)
def _allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
def _save_upload(file_storage):
if not file_storage or not getattr(file_storage, "filename", ""):
return None
filename = secure_filename(file_storage.filename)
if not filename or not _allowed_file(filename):
return None
ext = filename.rsplit(".", 1)[1].lower()
stored = f"{uuid4().hex}.{ext}"
_ensure_upload_dir()
file_storage.save(os.path.join(app.config["UPLOAD_FOLDER"], stored))
return stored
def _save_photo(file_storage):
if not file_storage or not getattr(file_storage, "filename", ""):
return None
filename = secure_filename(file_storage.filename)
if not filename or not _allowed_file(filename):
return None
ext = filename.rsplit(".", 1)[1].lower()
stored = f"{uuid4().hex}.{ext}"
_ensure_photos_dir()
file_storage.save(os.path.join(app.config["PHOTOS_FOLDER"], stored))
return stored
def _rational_str(val):
try:
if hasattr(val, 'numerator'):
n, d = val.numerator, val.denominator
elif isinstance(val, tuple) and len(val) == 2:
n, d = val
else:
return str(val)
if d == 0:
return str(n)
return str(n // d) if n % d == 0 else f"{n}/{d}"
except Exception:
return str(val)
def extract_photo_exif(filepath):
try:
img = Image.open(filepath)
raw_exif = img._getexif()
except Exception:
return {}
if not raw_exif:
return {}
raw = {TAGS.get(tid, str(tid)): v for tid, v in raw_exif.items()}
result = {}
make = str(raw.get('Make', '') or '').strip()
model = str(raw.get('Model', '') or '').strip()
if make and model and not model.startswith(make):
result['camera'] = f"{make} {model}"
elif model:
result['camera'] = model
lens = raw.get('LensModel') or raw.get('LensSpecification')
if isinstance(lens, (list, tuple)):
lens = ' '.join(_rational_str(x) for x in lens)
if lens:
result['lens'] = str(lens).strip()
fl = raw.get('FocalLength')
if fl is not None:
result['focal_length'] = _rational_str(fl) + 'mm'
fnum = raw.get('FNumber')
if fnum is not None:
result['aperture'] = 'f/' + _rational_str(fnum)
exp = raw.get('ExposureTime')
if exp is not None:
result['shutter_speed'] = _rational_str(exp) + 's'
iso = raw.get('ISOSpeedRatings') or raw.get('PhotographicSensitivity')
if iso is not None:
result['iso'] = str(iso)
dt = raw.get('DateTimeOriginal') or raw.get('DateTime')
if dt:
result['date_taken'] = str(dt).replace(':', '-', 2).split(' ')[0]
return result
def ensure_photos_schema():
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS photos (
id SERIAL PRIMARY KEY,
filename TEXT NOT NULL,
original_url TEXT,
title TEXT,
camera TEXT,
lens TEXT,
focal_length TEXT,
aperture TEXT,
shutter_speed TEXT,
iso TEXT,
date_taken TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
""")
conn.commit()
finally:
conn.close()
@app.before_request
def _ensure_schemas_once():
if not getattr(app, "_schemas_ready", False):
ensure_blog_schema()
ensure_photos_schema()
app._schemas_ready = True
def _is_admin_logged_in():
return bool(session.get("is_admin"))
def _require_admin():
if not _is_admin_logged_in():
return redirect(url_for("admin_login", next=request.path))
return None
def _render_markdown_safe(markdown_text):
html = md.markdown(
markdown_text or "",
extensions=["fenced_code", "tables", "codehilite"],
output_format="html5",
)
allowed_tags = set(bleach.sanitizer.ALLOWED_TAGS).union({
"p","pre","code","h1","h2","h3","h4","h5","h6",
"img","blockquote","hr","br","span","div",
"table","thead","tbody","tr","th","td",
})
allowed_attrs = {
**bleach.sanitizer.ALLOWED_ATTRIBUTES,
"a": ["href","title","rel","target"],
"img": ["src","alt","title"],
"*": ["class"],
}
return bleach.clean(html, tags=allowed_tags, attributes=allowed_attrs, strip=True)
@app.route("/")
def home():
return render_template("main.html")
@app.route("/activity")
def activity():
days = load_activity_data()
latest_three = days[-3:]
return render_template("activity.html", days=days, latest_three=latest_three)
@app.route("/activity/raw")
def activity_raw():
days = load_activity_data()
return render_template("activity_raw.html", days=days)
@app.route("/projects")
def projects_view():
return render_template("projects.html", projects=projects)
@app.route("/blog")
def blog_index():
q = (request.args.get("q") or "").strip()
tag = (request.args.get("tag") or "").strip()
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
if q and tag:
like = f"%{q}%"
cur.execute("""
SELECT p.id,p.slug,p.title,p.excerpt,p.published_at,p.cover_image,p.tags,COUNT(l.id)
FROM blog_posts p LEFT JOIN blog_likes l ON p.id=l.post_id
WHERE p.is_published=TRUE AND (%s=ANY(p.tags))
AND (p.title ILIKE %s OR p.excerpt ILIKE %s OR p.content_md ILIKE %s)
GROUP BY p.id ORDER BY p.published_at DESC NULLS LAST,p.created_at DESC LIMIT 50
""",(tag,like,like,like))
elif q:
like = f"%{q}%"
cur.execute("""
SELECT p.id,p.slug,p.title,p.excerpt,p.published_at,p.cover_image,p.tags,COUNT(l.id)
FROM blog_posts p LEFT JOIN blog_likes l ON p.id=l.post_id
WHERE p.is_published=TRUE
AND (p.title ILIKE %s OR p.excerpt ILIKE %s OR p.content_md ILIKE %s)
GROUP BY p.id ORDER BY p.published_at DESC NULLS LAST,p.created_at DESC LIMIT 50
""",(like,like,like))
elif tag:
cur.execute("""
SELECT p.id,p.slug,p.title,p.excerpt,p.published_at,p.cover_image,p.tags,COUNT(l.id)
FROM blog_posts p LEFT JOIN blog_likes l ON p.id=l.post_id
WHERE p.is_published=TRUE AND (%s=ANY(p.tags))
GROUP BY p.id ORDER BY p.published_at DESC NULLS LAST,p.created_at DESC LIMIT 50
""",(tag,))
else:
cur.execute("""
SELECT p.id,p.slug,p.title,p.excerpt,p.published_at,p.cover_image,p.tags,COUNT(l.id)
FROM blog_posts p LEFT JOIN blog_likes l ON p.id=l.post_id
WHERE p.is_published=TRUE
GROUP BY p.id ORDER BY p.published_at DESC NULLS LAST,p.created_at DESC LIMIT 50
""")
rows = cur.fetchall()
finally:
conn.close()
posts = [{"id":r[0],"slug":r[1],"title":r[2],"excerpt":r[3],"published_at":r[4],"cover_image":r[5],"tags":r[6] or [],"like_count":r[7]} for r in rows]
return render_template("blog_index.html", posts=posts, q=q, tag=tag)
@app.route("/blog/<slug>")
def blog_post(slug):
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("""
SELECT id,slug,title,excerpt,content_html,published_at,cover_image,tags
FROM blog_posts WHERE slug=%s AND is_published=TRUE
""",(slug,))
row = cur.fetchone()
if not row: abort(404)
post_id = row[0]
cur.execute("SELECT COUNT(*) FROM blog_likes WHERE post_id=%s",(post_id,))
like_count = int(cur.fetchone()[0])
cur.execute("""
SELECT COALESCE(name,display_name,'Anonymous'),content,created_at
FROM blog_comments WHERE post_id=%s AND is_hidden=FALSE
ORDER BY created_at DESC LIMIT 200
""",(post_id,))
comments = [{"display_name":c[0],"content":c[1],"created_at":c[2]} for c in cur.fetchall()]
cur.execute("""
SELECT filename FROM blog_images WHERE post_id=%s ORDER BY sort_order ASC,id ASC
""",(post_id,))
images = [{"filename":r[0]} for r in cur.fetchall()]
anon = client_anon_hash(request)
cur.execute("SELECT 1 FROM blog_likes WHERE post_id=%s AND anon_hash=%s",(post_id,anon))
has_liked = bool(cur.fetchone())
finally:
conn.close()
post = {"id":row[0],"slug":row[1],"title":row[2],"excerpt":row[3],"content_html":row[4],"published_at":row[5],"cover_image":row[6],"tags":row[7] or []}
return render_template("blog_post.html", post=post, like_count=like_count, has_liked=has_liked, comments=comments, images=images)
@app.route("/api/blog/<int:post_id>/like", methods=["POST"])
def blog_like(post_id):
anon = client_anon_hash(request)
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("SELECT id FROM blog_likes WHERE post_id=%s AND anon_hash=%s",(post_id,anon))
existing = cur.fetchone()
if existing:
cur.execute("DELETE FROM blog_likes WHERE id=%s",(existing[0],))
liked = False
else:
cur.execute("INSERT INTO blog_likes (post_id,anon_hash) VALUES (%s,%s) ON CONFLICT DO NOTHING",(post_id,anon))
liked = True
cur.execute("SELECT COUNT(*) FROM blog_likes WHERE post_id=%s",(post_id,))
count = int(cur.fetchone()[0])
conn.commit()
finally:
conn.close()
return jsonify({"ok":True,"liked":liked,"count":count})
@app.route("/api/blog/<int:post_id>/comment", methods=["POST"])
def blog_comment(post_id):
data = request.get_json(silent=True) or {}
content = (data.get("content") or "").strip()
name = (data.get("display_name") or data.get("name") or "Anonymous").strip() or "Anonymous"
if not content or len(content) > 2000:
return jsonify({"ok":False,"error":"Comment is empty or too long."}), 400
if len(name) > 40: name = name[:40]
anon = client_anon_hash(request)
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("INSERT INTO blog_comments (post_id,anon_hash,name,content) VALUES (%s,%s,%s,%s)",(post_id,anon,name,content))
conn.commit()
finally:
conn.close()
return jsonify({"ok":True})
@app.route("/admin/login", methods=["GET","POST"])
def admin_login():
if request.method == "POST":
username = (request.form.get("username") or "").strip()
password = (request.form.get("password") or "").strip()
expected_user = os.getenv("ADMIN_USERNAME") or "admin"
expected_pass = os.getenv("ADMIN_PASSWORD") or "admin"
if username == expected_user and password == expected_pass:
session["is_admin"] = True
nxt = request.args.get("next") or url_for("admin_photos")
return redirect(nxt)
flash("Invalid credentials.")
return render_template("admin_login.html")
@app.route("/admin/logout")
def admin_logout():
session.clear()
return redirect(url_for("home"))
@app.route("/admin")
@app.route("/admin/blog")
def admin_posts():
guard = _require_admin()
if guard: return guard
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("SELECT id,slug,title,is_published,created_at,published_at FROM blog_posts ORDER BY created_at DESC LIMIT 200")
rows = cur.fetchall()
finally:
conn.close()
posts = [{"id":r[0],"slug":r[1],"title":r[2],"is_published":r[3],"created_at":r[4],"published_at":r[5]} for r in rows]
return render_template("admin_posts.html", posts=posts)
@app.route("/admin/photos")
def admin_photos():
guard = _require_admin()
if guard: return guard
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("""
SELECT id,filename,original_url,title,camera,lens,
focal_length,aperture,shutter_speed,iso,date_taken,sort_order
FROM photos ORDER BY sort_order ASC,created_at DESC
""")
rows = cur.fetchall()
finally:
conn.close()
photos = [{"id":r[0],"filename":r[1],"original_url":r[2],"title":r[3],"camera":r[4],"lens":r[5],"focal_length":r[6],"aperture":r[7],"shutter_speed":r[8],"iso":r[9],"date_taken":r[10],"sort_order":r[11]} for r in rows]
return render_template("admin_photos.html", photos=photos)
@app.route("/admin/photos/upload", methods=["POST"])
def admin_photos_upload():
guard = _require_admin()
if guard: return guard
file = request.files.get("photo")
if not file:
flash("No file selected.")
return redirect(url_for("admin_photos"))
stored = _save_photo(file)
if not stored:
flash("Invalid file type. Use webp, jpg, png, gif.")
return redirect(url_for("admin_photos"))
saved_path = os.path.join(app.config["PHOTOS_FOLDER"], stored)
auto = extract_photo_exif(saved_path)
def field(name):
v = (request.form.get(name) or "").strip()
return v or auto.get(name) or None
original_url = (request.form.get("original_url") or "").strip() or None
title = field("title")
camera = field("camera")
lens = field("lens")
focal_length = field("focal_length")
aperture = field("aperture")
shutter_speed = field("shutter_speed")
iso = field("iso")
date_taken = field("date_taken")
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("SELECT COALESCE(MAX(sort_order),-1) FROM photos")
max_order = cur.fetchone()[0]
cur.execute("""
INSERT INTO photos (filename,original_url,title,camera,lens,focal_length,aperture,shutter_speed,iso,date_taken,sort_order)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
""",(stored,original_url,title,camera,lens,focal_length,aperture,shutter_speed,iso,date_taken,max_order+1))
conn.commit()
finally:
conn.close()
flash("Photo uploaded successfully.")
return redirect(url_for("admin_photos"))
@app.route("/admin/photos/<int:photo_id>/delete", methods=["POST"])
def admin_photos_delete(photo_id):
guard = _require_admin()
if guard: return guard
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("SELECT filename FROM photos WHERE id=%s",(photo_id,))
row = cur.fetchone()
if row:
filepath = os.path.join(app.config["PHOTOS_FOLDER"], row[0])
if os.path.exists(filepath):
os.remove(filepath)
cur.execute("DELETE FROM photos WHERE id=%s",(photo_id,))
conn.commit()
finally:
conn.close()
flash("Photo deleted.")
return redirect(url_for("admin_photos"))
@app.route("/admin/blog/new", methods=["GET","POST"])
def admin_new_post():
guard = _require_admin()
if guard: return guard
if request.method == "POST":
title = (request.form.get("title") or "").strip()
excerpt = (request.form.get("excerpt") or "").strip()
tags_raw = (request.form.get("tags") or "").strip()
slug = slugify((request.form.get("slug") or "").strip() or title)
cover_image = _save_upload(request.files.get("cover_image"))
uploaded_images = request.files.getlist("post_images")
image_files = [fn for fn in (_save_upload(f) for f in uploaded_images) if fn]
content_md = (request.form.get("content_md") or "").strip()
is_published = bool(request.form.get("is_published"))
if not title or not content_md:
flash("Title and content are required.")
return render_template("admin_edit.html", post=None)
tags = [t.strip() for t in tags_raw.replace(";",",").split(",") if t.strip()]
seen = set()
tags = [t for t in tags if not (t.lower() in seen or seen.add(t.lower()))]
content_html = _render_markdown_safe(content_md)
published_at = utcnow() if is_published else None
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO blog_posts (slug,title,excerpt,tags,cover_image,content_md,content_html,is_published,created_at,updated_at,published_at)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,NOW(),NOW(),%s) RETURNING id
""",(slug,title,excerpt,tags,cover_image,content_md,content_html,is_published,published_at))
new_id = cur.fetchone()[0]
if image_files:
for i,fn in enumerate(image_files):
cur.execute("INSERT INTO blog_images (post_id,filename,sort_order) VALUES (%s,%s,%s)",(new_id,fn,i))
conn.commit()
finally:
conn.close()
return redirect(url_for("admin_edit_post", post_id=new_id))
return render_template("admin_edit.html", post=None)
@app.route("/admin/blog/<int:post_id>/edit", methods=["GET","POST"])
def admin_edit_post(post_id):
guard = _require_admin()
if guard: return guard
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("SELECT id,slug,title,excerpt,tags,cover_image,content_md,is_published FROM blog_posts WHERE id=%s",(post_id,))
row = cur.fetchone()
if not row: abort(404)
cur.execute("SELECT filename FROM blog_images WHERE post_id=%s ORDER BY sort_order ASC,id ASC",(post_id,))
existing_images = [r[0] for r in cur.fetchall()]
if request.method == "POST":
title = (request.form.get("title") or "").strip()
excerpt = (request.form.get("excerpt") or "").strip()
tags_raw = (request.form.get("tags") or "").strip()
remove_cover = bool(request.form.get("remove_cover"))
remove_gallery = bool(request.form.get("remove_gallery"))
new_cover = _save_upload(request.files.get("cover_image"))
content_md = (request.form.get("content_md") or "").strip()
is_published = bool(request.form.get("is_published"))
slug = slugify(request.form.get("slug") or title)
uploaded_images = request.files.getlist("post_images")
new_images = [fn for fn in (_save_upload(f) for f in uploaded_images) if fn]
if not title or not content_md:
flash("Title and content are required.")
else:
tags = [t.strip() for t in tags_raw.replace(";",",").split(",") if t.strip()]
seen = set()
tags = [t for t in tags if not (t.lower() in seen or seen.add(t.lower()))]
cur.execute("SELECT cover_image FROM blog_posts WHERE id=%s",(post_id,))
current_cover = cur.fetchone()[0]
cover_value = None if remove_cover else (new_cover or current_cover)
content_html = _render_markdown_safe(content_md)
now = utcnow()
if is_published:
cur.execute("SELECT published_at FROM blog_posts WHERE id=%s",(post_id,))
existing_pub = cur.fetchone()[0]
published_at = existing_pub or now
else:
published_at = None
cur.execute("""
UPDATE blog_posts SET slug=%s,title=%s,excerpt=%s,tags=%s,cover_image=%s,
content_md=%s,content_html=%s,is_published=%s,updated_at=NOW(),published_at=%s
WHERE id=%s
""",(slug,title,excerpt,tags,cover_value,content_md,content_html,is_published,published_at,post_id))
if remove_gallery:
cur.execute("DELETE FROM blog_images WHERE post_id=%s",(post_id,))
if new_images:
cur.execute("SELECT COALESCE(MAX(sort_order),-1) FROM blog_images WHERE post_id=%s",(post_id,))
start_order = (cur.fetchone()[0] or -1) + 1
for i,fn in enumerate(new_images):
cur.execute("INSERT INTO blog_images (post_id,filename,sort_order) VALUES (%s,%s,%s)",(post_id,fn,start_order+i))
conn.commit()
flash("Saved.")
post = {"id":row[0],"slug":row[1],"title":row[2],"excerpt":row[3],"tags":row[4] or [],"cover_image":row[5],"content_md":row[6],"is_published":row[7],"images":existing_images}
cur.execute("SELECT slug,title,excerpt,tags,cover_image,content_md,is_published FROM blog_posts WHERE id=%s",(post_id,))
r2 = cur.fetchone()
post.update({"slug":r2[0],"title":r2[1],"excerpt":r2[2],"tags":r2[3] or [],"cover_image":r2[4],"content_md":r2[5],"is_published":r2[6]})
cur.execute("SELECT filename FROM blog_images WHERE post_id=%s ORDER BY sort_order ASC,id ASC",(post_id,))
post["images"] = [r[0] for r in cur.fetchall()]
finally:
conn.close()
return render_template("admin_edit.html", post=post)
@app.route("/admin/blog/<int:post_id>/delete", methods=["POST"])
def admin_delete_post(post_id):
guard = _require_admin()
if guard: return guard
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("DELETE FROM blog_posts WHERE id=%s",(post_id,))
conn.commit()
finally:
conn.close()
return redirect(url_for("admin_posts"))
@app.route("/projects/<project_name>/", defaults={"folder_path":""})
@app.route("/projects/<project_name>/<path:folder_path>")
def project_detail(project_name, folder_path):
repository_name = github_projects.get(project_name)
if not repository_name: abort(404)
structure = fetch_github_contents(repository_name, path=folder_path)
if structure is None:
return render_template("project_detail.html",project_name=project_name,project_display_name=project_name.replace("-"," ").title(),structure=[],folder_path=folder_path,metadata=None,file_summary=None,error="GitHub API error or rate limit exceeded.")
metadata = get_project_metadata(repository_name, os.getenv("TOKEN"))
file_summary = get_file_summary(repository_name, os.getenv("TOKEN"))
return render_template("project_detail.html",project_name=project_name,project_display_name=project_name.replace("-"," ").title(),structure=structure,folder_path=folder_path,metadata=metadata,file_summary=file_summary,error=None)
@app.route("/projects/<project_name>/file/<path:file_path>")
def view_file(project_name, file_path):
repository_name = github_projects.get(project_name)
if not repository_name: abort(404)
url = f"https://api.github.com/repos/{repository_name}/contents/{file_path}"
response = requests.get(url, headers=header)
if response.status_code != 200: abort(404)
content_json = response.json()
file_content = None
image_data_url = None
if content_json.get("encoding") == "base64":
decoded_bytes = base64.b64decode(content_json["content"])
file_ext = file_path.split(".")[-1].lower() if "." in file_path else ""
if file_ext in ["png","jpg","jpeg","gif","webp","svg"]:
mime_type = mimetypes.guess_type(file_path)[0] or "image/png"
base64_data = base64.b64encode(decoded_bytes).decode("utf-8")
image_data_url = f"data:{mime_type};base64,{base64_data}"
else:
file_content = decoded_bytes.decode("utf-8", errors="replace")
else:
file_content = "Cannot display content."
file_name = file_path.split("/")[-1]
file_ext = file_name.split(".")[-1] if "." in file_name else ""
return render_template("file_viewer.html",project_name=project_name,file_path=file_path,file_name=file_name,file_content=file_content,file_ext=file_ext,image_data_url=image_data_url)
@app.route("/photos")
def photos():
conn = get_blog_db_connection()
try:
with conn.cursor() as cur:
cur.execute("""
SELECT id,filename,original_url,title,camera,lens,focal_length,aperture,shutter_speed,iso,date_taken
FROM photos ORDER BY sort_order ASC,created_at DESC
""")
rows = cur.fetchall()
finally:
conn.close()
photos_list = [{"id":r[0],"filename":r[1],"original_url":r[2],"title":r[3],"camera":r[4],"lens":r[5],"focal_length":r[6],"aperture":r[7],"shutter_speed":r[8],"iso":r[9],"date_taken":r[10],"url":url_for("static",filename=f"photos/{r[1]}")} for r in rows]
return render_template("photos.html", photos=photos_list)
@app.route("/contacts")
def contacts():
links = [
{"name":"GitHub","href":"https://github.com/AalbatrossGuy","icon":"GitHub.svg","subtitle":"github.com/AalbatrossGuy"},
{"name":"LinkedIn","href":"https://linkedin.com/in/kishaloy-roy","icon":"LinkedIn.svg","subtitle":"linkedin.com/KishaloyRoy"},
{"name":"Reddit","href":"https://reddit.com/user/AalbatrossGuy","icon":"RedditIcon.svg","subtitle":"reddit.com/AalbatrossGuy"},
{"name":"Email","href":"mailto:[email protected]","icon":"email.png","subtitle":"[email protected]"},
{"name":"Instagram","href":"https://instagram.com/aalbatrossguy","icon":"InstagramIcon.svg","subtitle":"instagram.com/aalbatrossguy"},
{"name":"Monkeytype","href":"https://monkeytype.com/profile/AalbatrossGuy","icon":"MonkeyTypeIcon.svg","subtitle":"monkeytype.com/AalbatrossGuy"},
{"name":"Stack Overflow","href":"https://stackoverflow.com/users/13810518/aag","icon":"Stack_Overflow.svg","subtitle":"stackoverflow.com/AalbatrossGuy"},
{"name":"Matrix","href":"https://matrix.to/#/@aalbatrossguyy:matrix.org","icon":"MatrixIcon.svg","subtitle":"@AalbatrossGuy"},
{"name":"Discord","href":"https://discord.com/users/676414187131371520","icon":"DiscordIcon.svg","subtitle":"@aalbatrossguy"},
]
return render_template("contacts.html", links=links)
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)