refactor and limits change
This commit is contained in:
parent
7edbc28a9c
commit
82cdd12d19
|
|
@ -626,7 +626,7 @@ async def get_media(media_path: str):
|
||||||
|
|
||||||
|
|
||||||
@api_router.put("/media/{media_path:path}", tags=["Media"])
|
@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):
|
async def put_media(media_path: str, request: Request):
|
||||||
"""
|
"""
|
||||||
Overwrite an existing media file in place (used for saving drawing PNGs).
|
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",
|
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:
|
try:
|
||||||
with open(full_path, 'wb') as f:
|
with open(full_path, 'wb') as f:
|
||||||
f.write(body)
|
f.write(body)
|
||||||
|
|
|
||||||
|
|
@ -508,6 +508,10 @@ function noteApp() {
|
||||||
drawingIsPointerDown: false,
|
drawingIsPointerDown: false,
|
||||||
/** True after the PNG from disk has been decoded into _drawingBaseImage; false after Clear. */
|
/** True after the PNG from disk has been decoded into _drawingBaseImage; false after Clear. */
|
||||||
drawingHasRasterFromFile: false,
|
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,
|
_drawingAutosaveTimeout: null,
|
||||||
|
|
||||||
// DOM element cache (to avoid repeated querySelector calls)
|
// DOM element cache (to avoid repeated querySelector calls)
|
||||||
|
|
@ -3268,43 +3272,60 @@ function noteApp() {
|
||||||
*/
|
*/
|
||||||
async drawingSave() {
|
async drawingSave() {
|
||||||
if (!this.currentMedia || this.currentMediaType !== 'drawing') return;
|
if (!this.currentMedia || this.currentMediaType !== 'drawing') return;
|
||||||
this._drawingCancelAutosave();
|
if (this._drawingSaveInFlight) {
|
||||||
const canvas = this._drawingCanvasEl;
|
this._drawingSaveQueued = true;
|
||||||
const ctx = this._drawingCtx;
|
return;
|
||||||
if (!canvas || !ctx) return;
|
}
|
||||||
this.drawingDraft = null;
|
this._drawingSaveInFlight = true;
|
||||||
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;
|
|
||||||
try {
|
try {
|
||||||
const enc = this._drawingEncodeMediaPath();
|
this._drawingCancelAutosave();
|
||||||
const res = await fetch(`/api/media/${enc}`, {
|
const canvas = this._drawingCanvasEl;
|
||||||
method: 'PUT',
|
const ctx = this._drawingCtx;
|
||||||
body: blob,
|
if (!canvas || !ctx) return;
|
||||||
headers: { 'Content-Type': 'image/png' },
|
this.drawingDraft = null;
|
||||||
credentials: 'same-origin',
|
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) {
|
this.isSaving = true;
|
||||||
const err = await res.json().catch(() => ({}));
|
try {
|
||||||
let detail = err.detail;
|
const enc = this._drawingEncodeMediaPath();
|
||||||
if (Array.isArray(detail)) {
|
const res = await fetch(`/api/media/${enc}`, {
|
||||||
detail = detail.map((d) => d.msg || d).join(', ');
|
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 {
|
} finally {
|
||||||
this.isSaving = false;
|
this._drawingSaveInFlight = false;
|
||||||
|
if (this._drawingSaveQueued) {
|
||||||
|
this._drawingSaveQueued = false;
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (this.currentMedia && this.currentMediaType === 'drawing') {
|
||||||
|
this.drawingSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue