images working again, featured is broken

This commit is contained in:
2025-06-23 05:16:22 -05:00
parent 23fee50a77
commit 289f2f0ca1
4 changed files with 108 additions and 50 deletions

2
.gitignore vendored
View File

@ -9,6 +9,7 @@ __pycache__/
# Flask/Migrations # Flask/Migrations
#migrations/ #migrations/
instance/ instance/
mysql_data/
.env .env
#.env.* #.env.*
@ -21,6 +22,7 @@ logs/
# Uploads # Uploads
app/static/uploads/ app/static/uploads/
static/uploads/
# OS-generated files # OS-generated files
.DS_Store .DS_Store

BIN
nip.zip

Binary file not shown.

View File

@ -57,20 +57,17 @@ class Media(db.Model):
# If they passed plant_id or growlog_id in kwargs, pick one: # If they passed plant_id or growlog_id in kwargs, pick one:
if self.plant_id: if self.plant_id:
self.plugin = "plants" self.plugin = "plant"
self.related_id = self.plant_id self.related_id = self.plant_id
elif self.growlog_id: elif self.growlog_id:
self.plugin = "grow_logs" self.plugin = "growlog"
self.related_id = self.growlog_id self.related_id = self.growlog_id
else: else:
# fallback (you might choose to raise instead) # fallback (you might choose to raise instead)
self.plugin = kwargs.get("plugin", "") self.plugin = kwargs.get("plugin", "")
self.related_id = kwargs.get("related_id", 0) self.related_id = kwargs.get("related_id", 0)
# They must also supply `filename` before commit. self.file_url = f"{self.plugin}/{self.related_id}/{self.filename}"
# Build `file_url` in the same format your property used to:
date_path = self.uploaded_at.strftime("%Y/%m/%d")
self.file_url = f"{self.plugin}/{self.related_id}/{date_path}/{self.filename}"
@property @property
def url(self): def url(self):

View File

@ -2,16 +2,20 @@
import os import os
import uuid import uuid
from werkzeug.utils import secure_filename import io
import traceback
import tempfile
import logging
from datetime import datetime from datetime import datetime
from PIL import Image, ExifTags from werkzeug.utils import secure_filename
from werkzeug.datastructures import FileStorage
from flask import ( from flask import (
Blueprint, request, redirect, url_for, Blueprint, request, redirect, url_for,
flash, send_from_directory, current_app, flash, send_from_directory, current_app,
jsonify, abort jsonify, abort
) )
from flask_login import login_required, current_user from flask_login import login_required, current_user
from PIL import Image, ExifTags
from app import db from app import db
from .models import Media, ImageHeart, FeaturedImage from .models import Media, ImageHeart, FeaturedImage
@ -42,13 +46,11 @@ def allowed_file(filename):
def get_upload_path(plugin: str, related_id: int): def get_upload_path(plugin: str, related_id: int):
""" """
Build and return (absolute_dir, subdir) where uploads are stored: Return (absolute_dir, subdir) where uploads are stored:
{UPLOAD_FOLDER}/{plugin}s/{related_id}/YYYY/MM/DD/ <UPLOAD_FOLDER>/<plugin>/<related_id>/
""" """
now = datetime.utcnow()
date_path = f"{now.year}/{now.month:02}/{now.day:02}"
subdir = f"{plugin}s/{related_id}/{date_path}"
base = current_app.config["UPLOAD_FOLDER"] base = current_app.config["UPLOAD_FOLDER"]
subdir = os.path.join(plugin, str(related_id))
abs_dir = os.path.join(base, subdir) abs_dir = os.path.join(base, subdir)
os.makedirs(abs_dir, exist_ok=True) os.makedirs(abs_dir, exist_ok=True)
return abs_dir, subdir return abs_dir, subdir
@ -74,28 +76,37 @@ def _strip_exif(image: Image.Image) -> Image.Image:
return image return image
def _process_upload_file(file, uploader_id, plugin='', related_id=0, plant_id=None, growlog_id=None, caption=None): def _process_upload_file(
"""Handles saving an uploaded file and creating the Media record.""" file: FileStorage,
uploader_id: int,
plugin: str = '',
related_id: int = 0,
plant_id=None,
growlog_id=None,
caption=None
) -> Media:
"""
Handles saving an uploaded file under:
<UPLOAD_FOLDER>/<plugin>/<related_id>/<uuid>.<ext>
and returns a new Media object (not yet committed).
"""
# 1) Unique filename (uuid + original extension)
uid = str(uuid.uuid4()).replace('-', '')
_, ext = os.path.splitext(file.filename)
filename = f"{uid}{ext.lower()}"
# Generate a unique filename # 2) Build the folder
now = datetime.utcnow() abs_dir, subdir = get_upload_path(plugin, related_id)
unique_id = str(uuid.uuid4()).replace("-", "")
secure_name = secure_filename(file.filename)
filename = f"{unique_id}_{secure_name}"
# Construct the save path # 3) Save to disk
storage_path = os.path.join( full_path = os.path.join(abs_dir, filename)
current_app.config['UPLOAD_FOLDER'],
str(uploader_id),
now.strftime('%Y/%m/%d')
)
os.makedirs(storage_path, exist_ok=True)
full_path = os.path.join(storage_path, filename)
file.save(full_path) file.save(full_path)
file_url = f"/{uploader_id}/{now.strftime('%Y/%m/%d')}/{filename}" # 4) Record the relative URL fragment (for lookup)
file_url = f"{subdir}/{filename}"
# 5) Build the Media row
now = datetime.utcnow()
media = Media( media = Media(
plugin=plugin, plugin=plugin,
related_id=related_id, related_id=related_id,
@ -117,40 +128,91 @@ def save_media_file(file, user_id, **ctx):
def delete_media_file(media: Media): def delete_media_file(media: Media):
"""
Remove file from disk and delete DB record.
"""
base = current_app.config["UPLOAD_FOLDER"] base = current_app.config["UPLOAD_FOLDER"]
path = os.path.join(base, media.file_url) full = os.path.normpath(os.path.join(base, media.file_url))
if os.path.exists(path): if os.path.exists(full):
os.remove(path) os.remove(full)
db.session.delete(media) db.session.delete(media)
db.session.commit() db.session.commit()
def rotate_media_file(media: Media): def rotate_media_file(media: Media):
"""
Rotate the image on disk by -90° and save.
"""
base = current_app.config["UPLOAD_FOLDER"] base = current_app.config["UPLOAD_FOLDER"]
path = os.path.join(base, media.file_url) full = os.path.normpath(os.path.join(base, media.file_url))
with Image.open(path) as img: with Image.open(full) as img:
img.rotate(-90, expand=True).save(path) img.rotate(-90, expand=True).save(full)
db.session.commit() db.session.commit()
def generate_image_url(media: Media): def generate_image_url(media: Media):
""" """
Given a Media instance (or None), return its public URL Given a Media instance (or None), return its public URL
or a placeholder if no media. under our new schema, or a placeholder if no media.
""" """
if media and media.file_url: if media and media.file_url:
return url_for("media.media_file", filename=media.file_url) # use singular context
return url_for(
"media.serve_context_media",
context=media.plugin,
context_id=media.related_id,
filename=media.filename
)
# fallback
w, h = current_app.config.get("STANDARD_IMG_SIZE", (300, 200)) w, h = current_app.config.get("STANDARD_IMG_SIZE", (300, 200))
return f"https://placehold.co/{w}x{h}" return f"https://placehold.co/{w}x{h}"
# ─── Routes ──────────────────────────────────────────────────────────────────── @bp.route("/<context>/<int:context_id>/<filename>")
def serve_context_media(context, context_id, filename):
"""
Serve files saved under:
<UPLOAD_FOLDER>/<plugin>/<context_id>/<filename>
Accepts both singular and trailing-'s' contexts:
/media/plant/1/foo.jpg OR /media/plants/1/foo.jpg
"""
# — determine plugin name (always singular) —
valid = {"user", "plant", "growlog", "vendor"}
if context in valid:
plugin = context
elif context.endswith("s") and context[:-1] in valid:
plugin = context[:-1]
else:
logging.debug(f"Invalid context '{context}' in URL")
abort(404)
# — build filesystem path —
base = current_app.config["UPLOAD_FOLDER"]
directory = os.path.join(base, plugin, str(context_id))
full_path = os.path.join(directory, filename)
# — Debug log what were about to do —
logging.debug(f"[serve_context_media] plugin={plugin!r}, "
f"context_id={context_id!r}, filename={filename!r}")
logging.debug(f"[serve_context_media] checking DB for media row…")
logging.debug(f"[serve_context_media] filesystem path = {full_path!r}, exists? {os.path.exists(full_path)}")
# — Check the DB row (but dont abort if missing) —
media = Media.query.filter_by(
plugin=plugin,
related_id=context_id,
filename=filename
).first()
if not media:
logging.warning(f"[serve_context_media] no Media DB row for "
f"{plugin}/{context_id}/{filename!r}, "
"will try serving from disk anyway")
# — If the file exists on disk, serve it — otherwise 404 —
if os.path.exists(full_path):
return send_from_directory(directory, filename)
logging.error(f"[serve_context_media] file not found on disk: {full_path!r}")
abort(404)
# ─── Legacy / Other Routes (you can leave these for backward compatibility) ────
@bp.route("/", methods=["GET"]) @bp.route("/", methods=["GET"])
def media_index(): def media_index():
return redirect(url_for("core_ui.home")) return redirect(url_for("core_ui.home"))
@ -158,11 +220,8 @@ def media_index():
@bp.route("/<plugin>/<filename>") @bp.route("/<plugin>/<filename>")
def serve(plugin, filename): def serve(plugin, filename):
""" # optional legacy support
Stream uploaded media by plugin & filename, enforcing Media lookup.
"""
m = Media.query.filter_by(file_url=f"{plugin}s/%/{filename}").first_or_404() m = Media.query.filter_by(file_url=f"{plugin}s/%/{filename}").first_or_404()
# reconstruct disk path
date_path = m.uploaded_at.strftime("%Y/%m/%d") date_path = m.uploaded_at.strftime("%Y/%m/%d")
disk_dir = os.path.join( disk_dir = os.path.join(
current_app.config["UPLOAD_FOLDER"], current_app.config["UPLOAD_FOLDER"],