From 82cdd12d191d982e44525e0ed9aba8227b86bc06 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Tue, 21 Apr 2026 18:00:50 +0200 Subject: [PATCH] refactor and limits change --- backend/main.py | 6 +++- frontend/app.js | 85 ++++++++++++++++++++++++++++++------------------- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/backend/main.py b/backend/main.py index cff914b..bcf5ff2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -626,7 +626,7 @@ async def get_media(media_path: str): @api_router.put("/media/{media_path:path}", tags=["Media"]) -@limiter.limit("30/minute") +@limiter.limit("120/minute") async def put_media(media_path: str, request: Request): """ Overwrite an existing media file in place (used for saving drawing PNGs). @@ -662,6 +662,10 @@ async def put_media(media_path: str, request: Request): detail=f"File too large. Maximum size: {UPLOAD_MAX_IMAGE_MB}MB", ) + # Reject non-PNG payloads (defense in depth; path already restricts to .png) + if len(body) < 8 or body[:8] != b"\x89PNG\r\n\x1a\n": + raise HTTPException(status_code=400, detail="Body must be a valid PNG image") + try: with open(full_path, 'wb') as f: f.write(body) diff --git a/frontend/app.js b/frontend/app.js index 81c7aa1..a70d0a9 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -508,6 +508,10 @@ function noteApp() { drawingIsPointerDown: false, /** True after the PNG from disk has been decoded into _drawingBaseImage; false after Clear. */ drawingHasRasterFromFile: false, + /** Prevents overlapping drawingSave() runs (Ctrl+S + autosave + fast retries). */ + _drawingSaveInFlight: false, + /** If true, run drawingSave again after the current one finishes (coalesce). */ + _drawingSaveQueued: false, _drawingAutosaveTimeout: null, // DOM element cache (to avoid repeated querySelector calls) @@ -3268,43 +3272,60 @@ function noteApp() { */ async drawingSave() { if (!this.currentMedia || this.currentMediaType !== 'drawing') return; - this._drawingCancelAutosave(); - const canvas = this._drawingCanvasEl; - const ctx = this._drawingCtx; - if (!canvas || !ctx) return; - this.drawingDraft = null; - this.drawingIsPointerDown = false; - this.drawingRedraw(); - await new Promise((r) => requestAnimationFrame(r)); - const blob = await new Promise((resolve, reject) => { - canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png'); - }); - this.isSaving = true; + if (this._drawingSaveInFlight) { + this._drawingSaveQueued = true; + return; + } + this._drawingSaveInFlight = true; try { - const enc = this._drawingEncodeMediaPath(); - const res = await fetch(`/api/media/${enc}`, { - method: 'PUT', - body: blob, - headers: { 'Content-Type': 'image/png' }, - credentials: 'same-origin', + this._drawingCancelAutosave(); + const canvas = this._drawingCanvasEl; + const ctx = this._drawingCtx; + if (!canvas || !ctx) return; + this.drawingDraft = null; + this.drawingIsPointerDown = false; + this.drawingRedraw(); + await new Promise((r) => requestAnimationFrame(r)); + const blob = await new Promise((resolve, reject) => { + canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png'); }); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - let detail = err.detail; - if (Array.isArray(detail)) { - detail = detail.map((d) => d.msg || d).join(', '); + this.isSaving = true; + try { + const enc = this._drawingEncodeMediaPath(); + const res = await fetch(`/api/media/${enc}`, { + method: 'PUT', + body: blob, + headers: { 'Content-Type': 'image/png' }, + credentials: 'same-origin', + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + let detail = err.detail; + if (Array.isArray(detail)) { + detail = detail.map((d) => d.msg || d).join(', '); + } + throw new Error(detail || res.statusText); } - throw new Error(detail || res.statusText); + await this.loadNotes(); + this.lastSaved = true; + setTimeout(() => { + this.lastSaved = false; + }, CONFIG.SAVE_INDICATOR_DURATION); + } catch (error) { + ErrorHandler.handle('save drawing', error); + } finally { + this.isSaving = false; } - await this.loadNotes(); - this.lastSaved = true; - setTimeout(() => { - this.lastSaved = false; - }, CONFIG.SAVE_INDICATOR_DURATION); - } catch (error) { - ErrorHandler.handle('save drawing', error); } finally { - this.isSaving = false; + this._drawingSaveInFlight = false; + if (this._drawingSaveQueued) { + this._drawingSaveQueued = false; + queueMicrotask(() => { + if (this.currentMedia && this.currentMediaType === 'drawing') { + this.drawingSave(); + } + }); + } } },