images working again, featured is broken
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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 we’re 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 don’t 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"],
|
||||||
|
Reference in New Issue
Block a user