changed target folder, fixed bugs

This commit is contained in:
Gamosoft 2026-04-21 16:00:58 +02:00
parent cb6725f11c
commit ef82c277c0
3 changed files with 152 additions and 44 deletions

View File

@ -677,9 +677,16 @@ async def put_media(media_path: str, request: Request):
@api_router.post("/upload-media", tags=["Media"])
@limiter.limit("20/minute")
async def upload_media(request: Request, file: UploadFile = File(...), note_path: str = Form("")):
async def upload_media(
request: Request,
file: UploadFile = File(...),
note_path: str = Form(""),
content_folder: str = Form(""),
next_to_notes: str = Form(""),
):
"""
Upload a media file (image, audio, video, PDF) and save it to the attachments directory.
Upload a media file (image, audio, video, PDF) and save it to the attachments directory,
or (when next_to_notes=1) save a new drawing PNG next to markdown notes in content_folder.
Returns the relative path for markdown linking.
"""
try:
@ -727,6 +734,32 @@ async def upload_media(request: Request, file: UploadFile = File(...), note_path
detail=f"File too large. Maximum size for {media_type or 'this type'}: {max_size // (1024*1024)}MB. Uploaded: {len(file_data) / 1024 / 1024:.2f}MB"
)
if (next_to_notes or "").strip() == "1":
is_png = file.content_type in ("image/png",) or (file.filename and file.filename.lower().endswith(".png"))
if not is_png:
raise HTTPException(
status_code=400,
detail="next_to_notes requires a PNG file",
)
file_path = save_uploaded_image(
config["storage"]["notes_dir"],
"",
file.filename or "drawing.png",
file_data,
sibling_folder=content_folder or "",
)
if not file_path:
raise HTTPException(status_code=500, detail="Failed to save drawing")
out_name = Path(file_path).name
media_type = get_media_type(out_name) or "drawing"
return {
"success": True,
"path": file_path,
"filename": out_name,
"type": media_type,
"message": "Drawing created",
}
# Save the file (reusing image save function - it works for any file)
file_path = save_uploaded_image(
config['storage']['notes_dir'],

View File

@ -604,54 +604,67 @@ def get_attachment_dir(notes_dir: str, note_path: str) -> Path:
return Path(notes_dir) / folder / "_attachments"
def save_uploaded_image(notes_dir: str, note_path: str, filename: str, file_data: bytes) -> Optional[str]:
def save_uploaded_image(
notes_dir: str,
note_path: str,
filename: str,
file_data: bytes,
*,
sibling_folder: Optional[str] = None,
) -> Optional[str]:
"""
Save an uploaded image to the appropriate attachments directory.
Returns the relative path to the image if successful, None otherwise.
Save uploaded media under the vault.
Args:
notes_dir: Base notes directory
note_path: Path of the note the image is being uploaded to
filename: Original filename
file_data: Binary file data
Default (sibling_folder is None): store in ``_attachments`` next to the note implied by
``note_path`` (drag/drop, paste, etc.).
Returns:
Relative path to the saved image, or None if failed
If ``sibling_folder`` is set (including ``""`` for vault root): store ``drawing-{timestamp}.png``
in that folder next to ``.md`` files used for new drawings from the + menu.
Returns a relative path from ``notes_dir``, or None on failure.
"""
# Sanitize filename
base = Path(notes_dir)
if sibling_folder is not None:
cf = (sibling_folder or "").strip().replace("\\", "/")
segments = [p for p in cf.split("/") if p and p != "."]
if any(p == ".." for p in segments):
return None
dest_dir = base.joinpath(*segments) if segments else base
if not validate_path_security(notes_dir, dest_dir / ".nd_probe"):
return None
try:
dest_dir.mkdir(parents=True, exist_ok=True)
except OSError:
return None
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
full_path = dest_dir / f"drawing-{timestamp}.png"
if not validate_path_security(notes_dir, full_path):
return None
try:
with open(full_path, "wb") as f:
f.write(file_data)
return str(full_path.relative_to(base).as_posix())
except OSError as e:
print(f"Error saving image: {e}")
return None
sanitized_name = sanitize_filename(filename)
# Get extension
ext = Path(sanitized_name).suffix
name_without_ext = Path(sanitized_name).stem
# Add timestamp to prevent collisions
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
final_filename = f"{name_without_ext}-{timestamp}{ext}"
# Get attachments directory
attachments_dir = get_attachment_dir(notes_dir, note_path)
# Create directory if it doesn't exist
attachments_dir.mkdir(parents=True, exist_ok=True)
# Full path to save the image
full_path = attachments_dir / final_filename
# Security check
if not validate_path_security(notes_dir, full_path):
print(f"Security: Attempted to save image outside notes directory: {full_path}")
return None
try:
# Write the file
with open(full_path, 'wb') as f:
with open(full_path, "wb") as f:
f.write(file_data)
# Return relative path from notes_dir
relative_path = full_path.relative_to(Path(notes_dir))
return str(relative_path.as_posix())
except Exception as e:
return str(full_path.relative_to(base).as_posix())
except OSError as e:
print(f"Error saving image: {e}")
return None

View File

@ -2,7 +2,7 @@
// Configuration constants
const CONFIG = {
AUTOSAVE_DELAY: 1000, // ms - Delay before triggering autosave
AUTOSAVE_DELAY: 1000, // ms - Debounce before note save (autoSave) and drawing PNG autosave (_drawingScheduleAutosave)
SEARCH_DEBOUNCE_DELAY: 500, // ms - Delay before running note search while typing
SAVE_INDICATOR_DURATION: 2000, // ms - How long to show "saved" indicator
SCROLL_SYNC_DELAY: 50, // ms - Delay to prevent scroll sync interference
@ -504,6 +504,7 @@ function noteApp() {
drawingRedoStack: [],
drawingDraft: null,
drawingIsPointerDown: false,
_drawingAutosaveTimeout: null,
// DOM element cache (to avoid repeated querySelector calls)
_domCache: {
@ -2579,10 +2580,18 @@ function noteApp() {
},
// Upload a media file (image, audio, video, PDF)
async uploadMedia(file, notePath) {
// Use options.nextToNotes + options.contentFolder to save a new drawing PNG next to .md files (not in _attachments).
async uploadMedia(file, notePath, options = {}) {
const { nextToNotes, contentFolder } = options;
const formData = new FormData();
formData.append('file', file);
formData.append('note_path', notePath || '');
if (nextToNotes) {
formData.append('next_to_notes', '1');
formData.append('content_folder', contentFolder != null ? contentFolder : '');
formData.append('note_path', '');
} else {
formData.append('note_path', notePath || '');
}
try {
const response = await fetch('/api/upload-media', {
@ -2715,6 +2724,7 @@ function noteApp() {
viewMedia(mediaPath, mediaType = null, updateHistory = true) {
if (this.currentMediaType === 'drawing') {
this._drawingDisconnectResizeObserver();
this._drawingCancelAutosave();
}
this.showGraph = false; // Ensure graph is closed
this.currentNote = '';
@ -2811,12 +2821,14 @@ function noteApp() {
}
const file = new File([blob], 'drawing.png', { type: 'image/png' });
try {
const notePath = targetFolder ? `${targetFolder}/_.md` : '';
const path = await this.uploadMedia(file, notePath);
const path = await this.uploadMedia(file, '', {
nextToNotes: true,
contentFolder: targetFolder,
});
await this.loadNotes();
this.viewMedia(path, 'drawing');
} catch (error) {
ErrorHandler.handle('upload drawing', error);
ErrorHandler.handle('create drawing', error);
}
},
@ -2924,6 +2936,40 @@ function noteApp() {
}
},
_drawingCancelAutosave() {
if (this._drawingAutosaveTimeout) {
clearTimeout(this._drawingAutosaveTimeout);
this._drawingAutosaveTimeout = null;
}
},
/**
* Debounced PNG save (same delay as notes). Never runs while the primary button is held
* (active stroke/shape); pending timers are cleared when a new stroke starts.
*/
_drawingScheduleAutosave() {
if (this.currentMediaType !== 'drawing' || !this.currentMedia) return;
if (this._drawingAutosaveTimeout) {
clearTimeout(this._drawingAutosaveTimeout);
this._drawingAutosaveTimeout = null;
}
const path = this.currentMedia;
const pollMs = 150;
const attemptSave = () => {
if (this.currentMedia !== path || this.currentMediaType !== 'drawing') {
this._drawingAutosaveTimeout = null;
return;
}
if (this.drawingIsPointerDown) {
this._drawingAutosaveTimeout = setTimeout(attemptSave, pollMs);
return;
}
this._drawingAutosaveTimeout = null;
this.drawingSave({ silent: true });
};
this._drawingAutosaveTimeout = setTimeout(attemptSave, CONFIG.AUTOSAVE_DELAY);
},
/** Size canvas to drawingCanvasWrap and redraw (fills available pane). */
_drawingLayoutCanvas() {
const wrap = this.$refs.drawingCanvasWrap;
@ -3016,6 +3062,7 @@ function noteApp() {
if (this.currentMediaType !== 'drawing' || e.button !== 0) return;
const canvas = this._drawingCanvasEl;
if (!canvas) return;
this._drawingCancelAutosave();
canvas.setPointerCapture(e.pointerId);
this._drawingPointerId = e.pointerId;
const { x, y } = this._drawingCanvasCoords(e);
@ -3122,22 +3169,27 @@ function noteApp() {
});
}
this.drawingRedraw();
this._drawingScheduleAutosave();
},
drawingUndo() {
if (this.drawingOps.length === 0) return;
this.drawingRedoStack.push(this.drawingOps.pop());
this.drawingRedraw();
this._drawingScheduleAutosave();
},
drawingRedo() {
if (this.drawingRedoStack.length === 0) return;
this.drawingOps.push(this.drawingRedoStack.pop());
this.drawingRedraw();
this._drawingScheduleAutosave();
},
async drawingSave() {
async drawingSave(options = {}) {
const { silent = false } = options;
if (!this.currentMedia || this.currentMediaType !== 'drawing') return;
this._drawingCancelAutosave();
const canvas = this._drawingCanvasEl;
const ctx = this._drawingCtx;
if (!canvas || !ctx) return;
@ -3148,6 +3200,7 @@ function noteApp() {
const blob = await new Promise((resolve, reject) => {
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
});
this.isSaving = true;
try {
const enc = this._drawingEncodeMediaPath();
const res = await fetch(`/api/media/${enc}`, {
@ -3172,9 +3225,18 @@ function noteApp() {
this.drawingOps = [];
this.drawingRedoStack = [];
await this.initDrawingViewer();
this.toast(this.t('drawing.saved'), { type: 'success' });
if (!silent) {
this.toast(this.t('drawing.saved'), { type: 'success' });
} else {
this.lastSaved = true;
setTimeout(() => {
this.lastSaved = false;
}, CONFIG.SAVE_INDICATOR_DURATION);
}
} catch (error) {
ErrorHandler.handle('save drawing', error);
} finally {
this.isSaving = false;
}
},