changed target folder, fixed bugs
This commit is contained in:
parent
cb6725f11c
commit
ef82c277c0
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue