Release 1.2: bulk upload, S3/SFTP/FTP, SMTP, password reset, user groups, git deploy
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+48
-50
@@ -1,67 +1,49 @@
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
current_app,
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_from_directory,
|
||||
send_file,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user, login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app import db
|
||||
from app.auth_utils import photo_owner_or_admin
|
||||
from app.folder_utils import can_edit_folder
|
||||
from app.models import Folder, Photo
|
||||
from app.settings_service import get_settings
|
||||
from app.storage_service import delete_photo_file, get_photo_stream
|
||||
from app.upload_service import process_uploads
|
||||
|
||||
bp = Blueprint("main", __name__)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return (
|
||||
"." in filename
|
||||
and filename.rsplit(".", 1)[1].lower() in current_app.config["ALLOWED_EXTENSIONS"]
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
def index():
|
||||
photos = Photo.query.order_by(Photo.created_at.desc()).limit(24).all()
|
||||
total_photos = Photo.query.count()
|
||||
total_size = db.session.query(db.func.coalesce(db.func.sum(Photo.file_size), 0)).scalar() or 0
|
||||
settings = get_settings()
|
||||
return render_template(
|
||||
"index.html",
|
||||
photos=photos,
|
||||
total_photos=total_photos,
|
||||
total_size=int(total_size),
|
||||
max_upload_mb=current_app.config["MAX_CONTENT_LENGTH"] // (1024 * 1024),
|
||||
max_bulk_upload=settings.max_bulk_upload,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/upload", methods=["POST"])
|
||||
@login_required
|
||||
def upload():
|
||||
if "photo" not in request.files:
|
||||
flash("Файл не выбран", "error")
|
||||
return redirect(request.referrer or url_for("main.index"))
|
||||
|
||||
file = request.files["photo"]
|
||||
if file.filename == "":
|
||||
flash("Файл не выбран", "error")
|
||||
return redirect(request.referrer or url_for("main.index"))
|
||||
|
||||
if not allowed_file(file.filename):
|
||||
flash("Недопустимый формат. Разрешены: PNG, JPG, GIF, WEBP, BMP", "error")
|
||||
return redirect(request.referrer or url_for("main.index"))
|
||||
|
||||
folder_id = request.form.get("folder_id", type=int)
|
||||
folder = None
|
||||
if folder_id:
|
||||
@@ -69,31 +51,36 @@ def upload():
|
||||
if not can_edit_folder(folder):
|
||||
abort(403)
|
||||
|
||||
ext = file.filename.rsplit(".", 1)[1].lower()
|
||||
stored_name = f"{uuid.uuid4().hex}.{ext}"
|
||||
safe_original = secure_filename(file.filename) or f"photo.{ext}"
|
||||
|
||||
upload_dir = current_app.config["UPLOAD_FOLDER"]
|
||||
filepath = os.path.join(upload_dir, stored_name)
|
||||
file.save(filepath)
|
||||
file_size = os.path.getsize(filepath)
|
||||
|
||||
photo = Photo(
|
||||
filename=stored_name,
|
||||
original_name=safe_original,
|
||||
file_size=file_size,
|
||||
mime_type=file.content_type or f"image/{ext}",
|
||||
user_id=current_user.id,
|
||||
folder_id=folder.id if folder else None,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
result = process_uploads(
|
||||
request.files,
|
||||
current_user,
|
||||
folder,
|
||||
current_app.config["ALLOWED_EXTENSIONS"],
|
||||
)
|
||||
db.session.add(photo)
|
||||
db.session.commit()
|
||||
|
||||
flash("Фото успешно загружено", "success")
|
||||
if result["uploaded"] == 0 and result["errors"]:
|
||||
flash(result["errors"][0], "error")
|
||||
elif result["uploaded"] == 1:
|
||||
flash("Фото успешно загружено", "success")
|
||||
elif result["uploaded"] > 1:
|
||||
flash(f"Загружено {result['uploaded']} фото", "success")
|
||||
|
||||
for err in result["errors"]:
|
||||
if result["uploaded"] > 0:
|
||||
flash(err, "error")
|
||||
|
||||
if result["uploaded"] > 0:
|
||||
from app.email_service import send_upload_notification
|
||||
|
||||
send_upload_notification(
|
||||
current_user,
|
||||
result["uploaded"],
|
||||
folder.name if folder else None,
|
||||
)
|
||||
|
||||
if folder:
|
||||
return redirect(url_for("folders.view_folder", folder_id=folder.id))
|
||||
return redirect(url_for("cabinet.index"))
|
||||
return redirect(request.referrer or url_for("cabinet.index"))
|
||||
|
||||
|
||||
@bp.route("/api/photos")
|
||||
@@ -117,7 +104,15 @@ def api_photos():
|
||||
|
||||
@bp.route("/uploads/<path:filename>")
|
||||
def uploaded_file(filename):
|
||||
return send_from_directory(current_app.config["UPLOAD_FOLDER"], filename)
|
||||
photo = Photo.query.filter_by(filename=filename).first()
|
||||
storage_backend = photo.storage_backend if photo else "local"
|
||||
|
||||
stream = get_photo_stream(filename, storage_backend)
|
||||
if stream is None:
|
||||
abort(404)
|
||||
|
||||
mimetype = photo.mime_type if photo else "application/octet-stream"
|
||||
return send_file(stream, mimetype=mimetype)
|
||||
|
||||
|
||||
@bp.route("/delete/<int:photo_id>", methods=["POST"])
|
||||
@@ -126,9 +121,7 @@ def delete_photo(photo_id):
|
||||
photo = Photo.query.get_or_404(photo_id)
|
||||
photo_owner_or_admin(photo)
|
||||
|
||||
filepath = os.path.join(current_app.config["UPLOAD_FOLDER"], photo.filename)
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
delete_photo_file(photo.filename, photo.storage_backend)
|
||||
db.session.delete(photo)
|
||||
db.session.commit()
|
||||
flash("Фото удалено", "success")
|
||||
@@ -142,6 +135,7 @@ cabinet_bp = Blueprint("cabinet", __name__, url_prefix="/cabinet")
|
||||
@login_required
|
||||
def index():
|
||||
from app.folder_utils import process_pending_invites
|
||||
from app.quota_utils import quota_status
|
||||
|
||||
process_pending_invites(current_user)
|
||||
photos = (
|
||||
@@ -151,13 +145,17 @@ def index():
|
||||
)
|
||||
folders = Folder.query.filter_by(owner_id=current_user.id).order_by(Folder.created_at.desc()).limit(6).all()
|
||||
total_size = sum(p.file_size for p in photos)
|
||||
quota = quota_status(current_user)
|
||||
settings = get_settings()
|
||||
return render_template(
|
||||
"cabinet/index.html",
|
||||
photos=photos,
|
||||
folders=folders,
|
||||
total_photos=len(photos),
|
||||
total_size=total_size,
|
||||
quota=quota,
|
||||
max_upload_mb=current_app.config["MAX_CONTENT_LENGTH"] // (1024 * 1024),
|
||||
max_bulk_upload=settings.max_bulk_upload,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user