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.
198 lines
6.0 KiB
Python
198 lines
6.0 KiB
Python
from my_modules.decoratory.header import login_required
|
|
from my_modules.expiry import parse_expires, ensure_utc
|
|
from my_modules.file_meta import iso_stamp_filename, format_size
|
|
from quart_common.web.wide_event import add_wide_event_context
|
|
|
|
from quart import Blueprint, request, jsonify, current_app
|
|
import asyncio, hashlib
|
|
|
|
upload_bp = Blueprint('upload_bp', __name__)
|
|
|
|
# --- Helpers -----------------------------------------------------
|
|
|
|
async def read_all(uploaded) -> bytes:
|
|
"""Read all bytes from an uploaded file, handling sync or async .read()."""
|
|
reader = getattr(uploaded, 'read', None)
|
|
if reader is None:
|
|
return b''
|
|
if asyncio.iscoroutinefunction(reader):
|
|
return await reader()
|
|
|
|
data = reader()
|
|
if asyncio.iscoroutine(data):
|
|
return await data
|
|
return data
|
|
|
|
|
|
async def fingerprint_stream(stream, chunk_size:int=1024 * 1024) -> tuple[str|None, int|None]:
|
|
if not hasattr(stream, 'seek') or not hasattr(stream, 'tell'):
|
|
return None, None
|
|
|
|
try:
|
|
stream.seek(0)
|
|
except Exception:
|
|
return None, None
|
|
|
|
digest = hashlib.sha256()
|
|
size_bytes = 0
|
|
|
|
while True:
|
|
chunk = await asyncio.to_thread(stream.read, chunk_size)
|
|
if not chunk:
|
|
break
|
|
size_bytes += len(chunk)
|
|
digest.update(chunk)
|
|
|
|
try:
|
|
stream.seek(0)
|
|
except Exception:
|
|
return None, None
|
|
|
|
return digest.hexdigest(), size_bytes
|
|
|
|
|
|
def fingerprint_bytes(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
# --- Routes ------------------------------------------------------
|
|
|
|
@upload_bp.post('/api/upload')
|
|
@login_required
|
|
async def api_upload(user):
|
|
"""
|
|
POST /upload/api/upload
|
|
Accepts:
|
|
- multipart form with 'file' or 'text'
|
|
- 'expires' can be '1h', '7d', or ISO timestamp
|
|
- 'note' optional
|
|
"""
|
|
form = await request.form
|
|
files = await request.files
|
|
note = form.get('note', '')
|
|
expires_raw = form.get('expires', '')
|
|
text = form.get('text', '')
|
|
orphan_registry = getattr(current_app, 'orphan_storage_registry', None)
|
|
|
|
add_wide_event_context(nanoshare={"operation": "upload", "has_file": bool(files.get('file')), "has_text": bool(text.strip())})
|
|
|
|
uploaded = files.get('file')
|
|
expires_at_dt = ensure_utc(parse_expires(expires_raw))
|
|
|
|
if not uploaded and not text.strip():
|
|
add_wide_event_context(nanoshare={"operation_status": "missing_content"})
|
|
return jsonify({'ok': False, 'error': 'No content provided'}), 400
|
|
|
|
content_type = None
|
|
|
|
# --- binary upload path ---
|
|
if uploaded:
|
|
fname = uploaded.filename or ''
|
|
add_wide_event_context(nanoshare={"upload_type": "file", "filename_present": bool(fname)})
|
|
ctype = uploaded.mimetype or 'application/octet-stream'
|
|
content_type = ctype
|
|
storage_id = None
|
|
size_bytes = 0
|
|
fingerprint = None
|
|
reused_orphan_storage_id = False
|
|
|
|
# generate filename if missing/placeholder
|
|
if not fname or fname.lower() in {'blob', 'file'}:
|
|
ext = {
|
|
'image/png': 'png',
|
|
'image/jpeg': 'jpg',
|
|
'image/gif': 'gif',
|
|
'image/webp': 'webp',
|
|
'application/pdf': 'pdf',
|
|
'text/plain': 'txt',
|
|
}.get(ctype, 'bin')
|
|
fname = iso_stamp_filename('pasted', ext)
|
|
|
|
stream = getattr(uploaded, 'stream', None)
|
|
|
|
if stream is not None:
|
|
fingerprint, detected_size = await fingerprint_stream(stream)
|
|
size_bytes = detected_size or 0
|
|
storage_id = (
|
|
await orphan_registry.pop_recent(user['sub'], fingerprint)
|
|
if orphan_registry
|
|
else None
|
|
)
|
|
if storage_id:
|
|
reused_orphan_storage_id = True
|
|
add_wide_event_context(nanoshare={"reused_orphan_storage_id": True})
|
|
else:
|
|
storage_id, sent_size = await current_app.convex.send_stream_to_storage(stream=stream,content_type=content_type)
|
|
size_bytes = sent_size
|
|
else:
|
|
data = await read_all(uploaded)
|
|
fingerprint = fingerprint_bytes(data)
|
|
storage_id = (
|
|
await orphan_registry.pop_recent(user['sub'], fingerprint)
|
|
if orphan_registry
|
|
else None
|
|
)
|
|
if storage_id:
|
|
reused_orphan_storage_id = True
|
|
add_wide_event_context(nanoshare={"reused_orphan_storage_id": True})
|
|
else:
|
|
storage_id = await current_app.convex.send_to_storage(data=data, content_type=content_type)
|
|
size_bytes = len(data)
|
|
|
|
file_size_pretty = format_size(size_bytes)
|
|
|
|
try:
|
|
await current_app.convex.add_file(
|
|
file_name=fname,
|
|
file_size=file_size_pretty,
|
|
note=note,
|
|
content_type=content_type,
|
|
expires_at=expires_at_dt,
|
|
storage_id=storage_id,
|
|
user_id=user['sub'],
|
|
)
|
|
except Exception:
|
|
if storage_id and not reused_orphan_storage_id and orphan_registry:
|
|
await orphan_registry.remember(user['sub'], fingerprint, storage_id)
|
|
raise
|
|
|
|
# --- text upload path ---
|
|
elif text.strip():
|
|
add_wide_event_context(nanoshare={"upload_type": "text"})
|
|
data = text.encode('utf-8')
|
|
fname = iso_stamp_filename('pasted', 'txt')
|
|
fingerprint = fingerprint_bytes(data)
|
|
storage_id = (
|
|
await orphan_registry.pop_recent(user['sub'], fingerprint)
|
|
if orphan_registry
|
|
else None
|
|
)
|
|
reused_orphan_storage_id = bool(storage_id)
|
|
if reused_orphan_storage_id:
|
|
add_wide_event_context(nanoshare={"reused_orphan_storage_id": True})
|
|
if not storage_id:
|
|
storage_id = await current_app.convex.send_to_storage(
|
|
data=data, content_type='text/plain'
|
|
)
|
|
|
|
size_bytes = len(data)
|
|
file_size_pretty = format_size(size_bytes)
|
|
|
|
try:
|
|
await current_app.convex.add_file(
|
|
file_name=fname,
|
|
file_size=file_size_pretty,
|
|
note=note,
|
|
content_type='text/plain',
|
|
expires_at=expires_at_dt,
|
|
storage_id=storage_id,
|
|
user_id=user['sub'],
|
|
)
|
|
except Exception:
|
|
if not reused_orphan_storage_id and orphan_registry:
|
|
await orphan_registry.remember(user['sub'], fingerprint, storage_id)
|
|
raise
|
|
|
|
add_wide_event_context(nanoshare={"operation_status": "uploaded", "content_type": content_type})
|
|
return jsonify({'ok': True})
|