from my_modules.decoratory.header import login_required, feature_flag_required from my_modules.functions import get_ip from my_modules.app.setup import LIMITER from my_modules.app.logger import logger from my_modules.expiry import parse_expires from quart import ( Blueprint, request, session, Response, send_file, render_template, abort, current_app, jsonify, ) side_main_bp = Blueprint("side_main", __name__) def find_file(files: list[dict], file_id: str): for file_data in files: if file_data.get("file_id") == file_id: return file_data return None @side_main_bp.route('/') @LIMITER.limit("10 per minute;50 per hour") async def index(): if session.get("user") is not None: return await render_template("views/webpage/files/upload.htm") return await render_template("views/webpage/index.htm") @side_main_bp.route('/access') @login_required async def access_list(user): access_data = await current_app.convex.get_all_access(user_id=user['sub']) return await render_template("views/webpage/access/list.htm", access_logs=access_data) @side_main_bp.route('/files') @login_required async def files_list(user): files_data = await current_app.convex.get_files(user_id=user['sub']) info_enabled = await current_app.convex.is_feature_enabled(key='nanoshare_files-info', fallback=False) edit_enabled = await current_app.convex.is_feature_enabled(key='nanoshare_files-edit', fallback=False) return await render_template("views/webpage/files/list.htm", files=files_data, file_info_enabled=info_enabled, file_edit_enabled=edit_enabled, ) @side_main_bp.route('/files//info') @login_required @feature_flag_required("nanoshare_files-info", fallback=False, status_code=404) async def file_info(file_id, user): files_data = await current_app.convex.get_files(user_id=user["sub"]) file_data = find_file(files_data, file_id) if not file_data: abort(404) access_data = await current_app.convex.get_file_access(file_id=file_id, user_id=user["sub"]) or [] share_url = request.url_root.rstrip("/") + f"/-{file_id}" return await render_template( "views/webpage/files/info.htm", file=file_data, accesses=access_data, share_url=share_url, ) @side_main_bp.route("/files//edit") @login_required @feature_flag_required("nanoshare_files-edit", fallback=False, status_code=404) async def file_edit(file_id, user): files_data = await current_app.convex.get_files(user_id=user["sub"]) file_data = find_file(files_data, file_id) if not file_data: abort(404) share_url = request.url_root.rstrip("/") + f"/-{file_id}" return await render_template( "views/webpage/files/edit.htm", file=file_data, share_url=share_url ) @side_main_bp.post("/api/files//edit") @login_required @feature_flag_required("nanoshare_files-edit", fallback=False, status_code=404) async def file_edit_api(file_id, user): files_data = await current_app.convex.get_files(user_id=user["sub"]) if not find_file(files_data, file_id): return jsonify({"ok": False, "error": "File not found"}), 404 payload = await request.get_json(silent=True) if payload is None: payload = await request.form file_name = str(payload.get("file_name", "")).strip() note = str(payload.get("note", "")).strip() expires_raw = str(payload.get("expires", "")).strip() if not file_name: return jsonify({"ok": False, "error": "Filename is required"}), 400 expires_at = parse_expires(expires_raw) if expires_raw and expires_raw != "never" and expires_at is None: return jsonify({"ok": False, "error": "Invalid expiration value"}), 400 await current_app.convex.update_file( file_id=file_id, file_name=file_name, note=note, expires_at=expires_at, user_id=user["sub"], ) return jsonify({"ok": True}) @side_main_bp.post("/api/files//delete") @login_required @feature_flag_required("nanoshare_files-edit", fallback=False, status_code=404) async def file_delete_api(file_id, user): files_data = await current_app.convex.get_files(user_id=user["sub"]) if not find_file(files_data, file_id): return jsonify({"ok": False, "error": "File not found"}), 404 await current_app.convex.delete_file(file_id=file_id, user_id=user["sub"]) return jsonify({"ok": True}) @side_main_bp.route("/-") @LIMITER.limit("10 per minute;500 per hour;") async def serve_file(file_id: str): file_data = await current_app.convex.get_file(file_id=file_id) disable_logging = False if not file_data: abort(404) user = session.get('user') if user and user['sub'] == file_data['user_id']: disable_logging = True if file_data.get("expired", None): if not disable_logging: await current_app.convex.add_file_access(file_id=file_id, ip_address=get_ip(), user_agent=request.user_agent, status="expired") return Response("This file has expired.", status=410, headers={ "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", }) file_name = file_data.get("file_name") content_type = file_data.get("content_type") or "application/octet-stream" force_download = request.args.get("download") in {"1", "true", "yes"} if not file_data.get('db_image_url', None): if not disable_logging: await current_app.convex.add_file_access(file_id=file_id, ip_address=get_ip(), user_agent=request.user_agent, status="error") abort(404) if not disable_logging: await current_app.convex.add_file_access(file_id=file_id, ip_address=get_ip(), user_agent=request.user_agent, status="ok") return await send_file( filename_or_io=await current_app.convex.get_from_storage(file_data.get('db_image_url')), mimetype=content_type, as_attachment=force_download, attachment_filename=file_name, conditional=True, cache_timeout=60, last_modified=int(file_data['uploaded_at']) / 1000 )