9c731d6e67
Build and Push Docker Container / build-and-push (push) Failing after 51s
- Register quart_common wide-event logging during app setup so every HTTP request emits one canonical structured event. - Replace the inline security middleware with reusable quart_common security middleware wiring and move skip path configuration into app constants. - Add NanoShare-specific wide-event context for health checks, auth/error handlers, file list/edit/delete/serve flows and upload outcomes. - Rename runtime logging/project metadata from simple-picoshare to nanoshare where it is emitted in service context. - Update my_helpers and quart_common submodules for Convex/wide-event integration and reusable security middleware support. - Add NanoShare middleware tests covering safe user context, client IP enrichment, missing Convex handling and Convex security lookup failures.
180 lines
6.5 KiB
Python
180 lines
6.5 KiB
Python
from my_modules.decoratory.header import login_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_common.web.wide_event import add_wide_event_context
|
|
|
|
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
|
|
|
|
def build_share_url(file_id: str) -> str:
|
|
scheme = (request.headers.get("X-Forwarded-Proto") or request.scheme or "http").split(",")[0].strip()
|
|
host = (request.headers.get("X-Forwarded-Host") or request.host).split(",")[0].strip()
|
|
root_path = request.root_path.rstrip("/")
|
|
return f"{scheme}://{host}{root_path}/-{file_id}"
|
|
|
|
@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):
|
|
add_wide_event_context(nanoshare={"operation": "access_list"})
|
|
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):
|
|
add_wide_event_context(nanoshare={"operation": "files_list"})
|
|
files_data = await current_app.convex.get_files(user_id=user['sub'])
|
|
return await render_template("views/webpage/files/list.htm",
|
|
files=files_data
|
|
)
|
|
|
|
@side_main_bp.route('/files/<path:file_id>/info')
|
|
@login_required
|
|
async def file_info(file_id, user):
|
|
add_wide_event_context(nanoshare={"operation": "file_info", "file_id": file_id})
|
|
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 = build_share_url(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/<path:file_id>/edit")
|
|
@login_required
|
|
async def file_edit(file_id, user):
|
|
add_wide_event_context(nanoshare={"operation": "file_edit", "file_id": file_id})
|
|
file_data = await current_app.convex.get_file_informations(file_id=file_id, user_id=user["sub"])
|
|
if not file_data:
|
|
abort(404)
|
|
|
|
share_url = build_share_url(file_id)
|
|
return await render_template(
|
|
"views/webpage/files/edit.htm", file=file_data, share_url=share_url, file_id=file_id
|
|
)
|
|
|
|
@side_main_bp.put("/api/file/<path:file_id>")
|
|
@login_required
|
|
async def file_edit_api(file_id, user):
|
|
add_wide_event_context(nanoshare={"operation": "file_update", "file_id": file_id})
|
|
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.delete("/api/file/<path:file_id>")
|
|
@login_required
|
|
async def file_delete_api(file_id, user):
|
|
add_wide_event_context(nanoshare={"operation": "file_delete", "file_id": file_id})
|
|
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("/-<file_id>")
|
|
@LIMITER.limit("10 per minute;500 per hour;")
|
|
async def serve_file(file_id: str):
|
|
add_wide_event_context(nanoshare={"operation": "serve_file", "file_id": file_id})
|
|
file_data = await current_app.convex.get_file(file_id=file_id)
|
|
disable_logging = False
|
|
|
|
if not file_data:
|
|
add_wide_event_context(nanoshare={"operation_status": "not_found"})
|
|
abort(404)
|
|
|
|
user = session.get('user')
|
|
if user and user['sub'] == file_data['user_id']:
|
|
disable_logging = True
|
|
|
|
if file_data.get("expired", None):
|
|
add_wide_event_context(nanoshare={"operation_status": "expired", "owner_request": disable_logging})
|
|
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):
|
|
add_wide_event_context(nanoshare={"operation_status": "missing_storage", "owner_request": disable_logging})
|
|
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)
|
|
|
|
add_wide_event_context(nanoshare={"operation_status": "served", "owner_request": disable_logging, "content_type": content_type, "download": force_download})
|
|
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
|
|
)
|