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"])
|
@api_router.post("/upload-media", tags=["Media"])
|
||||||
@limiter.limit("20/minute")
|
@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.
|
Returns the relative path for markdown linking.
|
||||||
"""
|
"""
|
||||||
try:
|
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"
|
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)
|
# Save the file (reusing image save function - it works for any file)
|
||||||
file_path = save_uploaded_image(
|
file_path = save_uploaded_image(
|
||||||
config['storage']['notes_dir'],
|
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"
|
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.
|
Save uploaded media under the vault.
|
||||||
Returns the relative path to the image if successful, None otherwise.
|
|
||||||
|
|
||||||
Args:
|
Default (sibling_folder is None): store in ``_attachments`` next to the note implied by
|
||||||
notes_dir: Base notes directory
|
``note_path`` (drag/drop, paste, etc.).
|
||||||
note_path: Path of the note the image is being uploaded to
|
|
||||||
filename: Original filename
|
|
||||||
file_data: Binary file data
|
|
||||||
|
|
||||||
Returns:
|
If ``sibling_folder`` is set (including ``""`` for vault root): store ``drawing-{timestamp}.png``
|
||||||
Relative path to the saved image, or None if failed
|
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)
|
sanitized_name = sanitize_filename(filename)
|
||||||
|
|
||||||
# Get extension
|
|
||||||
ext = Path(sanitized_name).suffix
|
ext = Path(sanitized_name).suffix
|
||||||
name_without_ext = Path(sanitized_name).stem
|
name_without_ext = Path(sanitized_name).stem
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
# Add timestamp to prevent collisions
|
|
||||||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
||||||
final_filename = f"{name_without_ext}-{timestamp}{ext}"
|
final_filename = f"{name_without_ext}-{timestamp}{ext}"
|
||||||
|
|
||||||
# Get attachments directory
|
|
||||||
attachments_dir = get_attachment_dir(notes_dir, note_path)
|
attachments_dir = get_attachment_dir(notes_dir, note_path)
|
||||||
|
|
||||||
# Create directory if it doesn't exist
|
|
||||||
attachments_dir.mkdir(parents=True, exist_ok=True)
|
attachments_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Full path to save the image
|
|
||||||
full_path = attachments_dir / final_filename
|
full_path = attachments_dir / final_filename
|
||||||
|
|
||||||
# Security check
|
|
||||||
if not validate_path_security(notes_dir, full_path):
|
if not validate_path_security(notes_dir, full_path):
|
||||||
print(f"Security: Attempted to save image outside notes directory: {full_path}")
|
print(f"Security: Attempted to save image outside notes directory: {full_path}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Write the file
|
with open(full_path, "wb") as f:
|
||||||
with open(full_path, 'wb') as f:
|
|
||||||
f.write(file_data)
|
f.write(file_data)
|
||||||
|
return str(full_path.relative_to(base).as_posix())
|
||||||
# Return relative path from notes_dir
|
except OSError as e:
|
||||||
relative_path = full_path.relative_to(Path(notes_dir))
|
|
||||||
return str(relative_path.as_posix())
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error saving image: {e}")
|
print(f"Error saving image: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
// Configuration constants
|
// Configuration constants
|
||||||
const CONFIG = {
|
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
|
SEARCH_DEBOUNCE_DELAY: 500, // ms - Delay before running note search while typing
|
||||||
SAVE_INDICATOR_DURATION: 2000, // ms - How long to show "saved" indicator
|
SAVE_INDICATOR_DURATION: 2000, // ms - How long to show "saved" indicator
|
||||||
SCROLL_SYNC_DELAY: 50, // ms - Delay to prevent scroll sync interference
|
SCROLL_SYNC_DELAY: 50, // ms - Delay to prevent scroll sync interference
|
||||||
|
|
@ -504,6 +504,7 @@ function noteApp() {
|
||||||
drawingRedoStack: [],
|
drawingRedoStack: [],
|
||||||
drawingDraft: null,
|
drawingDraft: null,
|
||||||
drawingIsPointerDown: false,
|
drawingIsPointerDown: false,
|
||||||
|
_drawingAutosaveTimeout: null,
|
||||||
|
|
||||||
// DOM element cache (to avoid repeated querySelector calls)
|
// DOM element cache (to avoid repeated querySelector calls)
|
||||||
_domCache: {
|
_domCache: {
|
||||||
|
|
@ -2579,10 +2580,18 @@ function noteApp() {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Upload a media file (image, audio, video, PDF)
|
// 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();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
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 || '');
|
formData.append('note_path', notePath || '');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/upload-media', {
|
const response = await fetch('/api/upload-media', {
|
||||||
|
|
@ -2715,6 +2724,7 @@ function noteApp() {
|
||||||
viewMedia(mediaPath, mediaType = null, updateHistory = true) {
|
viewMedia(mediaPath, mediaType = null, updateHistory = true) {
|
||||||
if (this.currentMediaType === 'drawing') {
|
if (this.currentMediaType === 'drawing') {
|
||||||
this._drawingDisconnectResizeObserver();
|
this._drawingDisconnectResizeObserver();
|
||||||
|
this._drawingCancelAutosave();
|
||||||
}
|
}
|
||||||
this.showGraph = false; // Ensure graph is closed
|
this.showGraph = false; // Ensure graph is closed
|
||||||
this.currentNote = '';
|
this.currentNote = '';
|
||||||
|
|
@ -2811,12 +2821,14 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
const file = new File([blob], 'drawing.png', { type: 'image/png' });
|
const file = new File([blob], 'drawing.png', { type: 'image/png' });
|
||||||
try {
|
try {
|
||||||
const notePath = targetFolder ? `${targetFolder}/_.md` : '';
|
const path = await this.uploadMedia(file, '', {
|
||||||
const path = await this.uploadMedia(file, notePath);
|
nextToNotes: true,
|
||||||
|
contentFolder: targetFolder,
|
||||||
|
});
|
||||||
await this.loadNotes();
|
await this.loadNotes();
|
||||||
this.viewMedia(path, 'drawing');
|
this.viewMedia(path, 'drawing');
|
||||||
} catch (error) {
|
} 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). */
|
/** Size canvas to drawingCanvasWrap and redraw (fills available pane). */
|
||||||
_drawingLayoutCanvas() {
|
_drawingLayoutCanvas() {
|
||||||
const wrap = this.$refs.drawingCanvasWrap;
|
const wrap = this.$refs.drawingCanvasWrap;
|
||||||
|
|
@ -3016,6 +3062,7 @@ function noteApp() {
|
||||||
if (this.currentMediaType !== 'drawing' || e.button !== 0) return;
|
if (this.currentMediaType !== 'drawing' || e.button !== 0) return;
|
||||||
const canvas = this._drawingCanvasEl;
|
const canvas = this._drawingCanvasEl;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
this._drawingCancelAutosave();
|
||||||
canvas.setPointerCapture(e.pointerId);
|
canvas.setPointerCapture(e.pointerId);
|
||||||
this._drawingPointerId = e.pointerId;
|
this._drawingPointerId = e.pointerId;
|
||||||
const { x, y } = this._drawingCanvasCoords(e);
|
const { x, y } = this._drawingCanvasCoords(e);
|
||||||
|
|
@ -3122,22 +3169,27 @@ function noteApp() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.drawingRedraw();
|
this.drawingRedraw();
|
||||||
|
this._drawingScheduleAutosave();
|
||||||
},
|
},
|
||||||
|
|
||||||
drawingUndo() {
|
drawingUndo() {
|
||||||
if (this.drawingOps.length === 0) return;
|
if (this.drawingOps.length === 0) return;
|
||||||
this.drawingRedoStack.push(this.drawingOps.pop());
|
this.drawingRedoStack.push(this.drawingOps.pop());
|
||||||
this.drawingRedraw();
|
this.drawingRedraw();
|
||||||
|
this._drawingScheduleAutosave();
|
||||||
},
|
},
|
||||||
|
|
||||||
drawingRedo() {
|
drawingRedo() {
|
||||||
if (this.drawingRedoStack.length === 0) return;
|
if (this.drawingRedoStack.length === 0) return;
|
||||||
this.drawingOps.push(this.drawingRedoStack.pop());
|
this.drawingOps.push(this.drawingRedoStack.pop());
|
||||||
this.drawingRedraw();
|
this.drawingRedraw();
|
||||||
|
this._drawingScheduleAutosave();
|
||||||
},
|
},
|
||||||
|
|
||||||
async drawingSave() {
|
async drawingSave(options = {}) {
|
||||||
|
const { silent = false } = options;
|
||||||
if (!this.currentMedia || this.currentMediaType !== 'drawing') return;
|
if (!this.currentMedia || this.currentMediaType !== 'drawing') return;
|
||||||
|
this._drawingCancelAutosave();
|
||||||
const canvas = this._drawingCanvasEl;
|
const canvas = this._drawingCanvasEl;
|
||||||
const ctx = this._drawingCtx;
|
const ctx = this._drawingCtx;
|
||||||
if (!canvas || !ctx) return;
|
if (!canvas || !ctx) return;
|
||||||
|
|
@ -3148,6 +3200,7 @@ function noteApp() {
|
||||||
const blob = await new Promise((resolve, reject) => {
|
const blob = await new Promise((resolve, reject) => {
|
||||||
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
|
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
|
||||||
});
|
});
|
||||||
|
this.isSaving = true;
|
||||||
try {
|
try {
|
||||||
const enc = this._drawingEncodeMediaPath();
|
const enc = this._drawingEncodeMediaPath();
|
||||||
const res = await fetch(`/api/media/${enc}`, {
|
const res = await fetch(`/api/media/${enc}`, {
|
||||||
|
|
@ -3172,9 +3225,18 @@ function noteApp() {
|
||||||
this.drawingOps = [];
|
this.drawingOps = [];
|
||||||
this.drawingRedoStack = [];
|
this.drawingRedoStack = [];
|
||||||
await this.initDrawingViewer();
|
await this.initDrawingViewer();
|
||||||
|
if (!silent) {
|
||||||
this.toast(this.t('drawing.saved'), { type: 'success' });
|
this.toast(this.t('drawing.saved'), { type: 'success' });
|
||||||
|
} else {
|
||||||
|
this.lastSaved = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.lastSaved = false;
|
||||||
|
}, CONFIG.SAVE_INDICATOR_DURATION);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorHandler.handle('save drawing', error);
|
ErrorHandler.handle('save drawing', error);
|
||||||
|
} finally {
|
||||||
|
this.isSaving = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue