diff --git a/backend/main.py b/backend/main.py index 544317a..fbdc288 100644 --- a/backend/main.py +++ b/backend/main.py @@ -80,10 +80,6 @@ plugin_manager.run_hook('on_app_startup') static_path = Path(__file__).parent.parent / "frontend" app.mount("/static", StaticFiles(directory=static_path), name="static") -# Mount data directory for serving images -data_path = Path(config['storage']['notes_dir']) -app.mount("/data", StaticFiles(directory=data_path), name="data") - # ============================================================================ # Custom Exception Handlers @@ -438,6 +434,36 @@ async def create_new_folder(data: dict): raise HTTPException(status_code=500, detail=str(e)) +@api_router.get("/images/{image_path:path}") +async def get_image(image_path: str): + """ + Serve an image file with authentication protection. + """ + try: + notes_dir = config['storage']['notes_dir'] + full_path = Path(notes_dir) / image_path + + # Security: Validate path is within notes directory + if not validate_path_security(notes_dir, full_path): + raise HTTPException(status_code=403, detail="Access denied") + + # Check file exists and is an image + if not full_path.exists() or not full_path.is_file(): + raise HTTPException(status_code=404, detail="Image not found") + + # Validate it's an image file + allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'} + if full_path.suffix.lower() not in allowed_extensions: + raise HTTPException(status_code=400, detail="Not an image file") + + # Return the file + return FileResponse(full_path) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + @api_router.post("/upload-image") async def upload_image(file: UploadFile = File(...), note_path: str = Form(...)): """ diff --git a/documentation/API.md b/documentation/API.md index 08a0689..96385bd 100644 --- a/documentation/API.md +++ b/documentation/API.md @@ -81,6 +81,67 @@ Content-Type: application/json } ``` +## 🖼️ Images + +### Get Image +```http +GET /api/images/{image_path} +``` +Retrieve an image file with authentication protection. + +**Example:** +```bash +curl http://localhost:8000/api/images/folder/_attachments/image-20240417093343.png +``` + +**Security Note:** This endpoint requires authentication and validates that: +- The image path is within the notes directory (prevents directory traversal) +- The file exists and is a valid image format +- The requesting user is authenticated (if auth is enabled) + +### Upload Image +```http +POST /api/upload-image +Content-Type: multipart/form-data + +file: +note_path: +``` + +Upload an image file to the `_attachments` directory. Images are automatically organized per-folder and named with timestamps to prevent conflicts. + +**Supported formats:** JPG, JPEG, PNG, GIF, WEBP +**Maximum size:** 10MB + +**Response:** +```json +{ + "success": true, + "path": "folder/_attachments/image-20240417093343.png", + "filename": "image-20240417093343.png", + "message": "Image uploaded successfully" +} +``` + +**Example (using curl):** +```bash +curl -X POST http://localhost:8000/api/upload-image \ + -F "file=@/path/to/image.png" \ + -F "note_path=folder/mynote.md" +``` + +**Windows PowerShell:** +```powershell +curl.exe -X POST http://localhost:8000/api/upload-image -F "file=@C:\path\to\image.png" -F "note_path=folder/mynote.md" +``` + +**Notes:** +- Images are stored in `_attachments` folders relative to the note's location +- Filenames are automatically timestamped (e.g., `image-20240417093343.png`) +- Images appear in the sidebar navigation and can be viewed/deleted directly +- Drag & drop images into the editor automatically uploads and inserts markdown +- All image access requires authentication when security is enabled + ## 📁 Folders ### Create Folder @@ -93,6 +154,17 @@ Content-Type: application/json } ``` +### Delete Folder +```http +DELETE /api/folders/{folder_path} +``` +Deletes a folder and all its contents. + +**Example:** +```bash +curl -X DELETE http://localhost:8000/api/folders/Projects/Archive +``` + ### Move Folder ```http POST /api/folders/move diff --git a/frontend/app.js b/frontend/app.js index c0a7eaf..b55c0a4 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -787,7 +787,7 @@ function noteApp() { const filename = notePath.split('/').pop().replace(/\.[^/.]+$/, ''); // Remove extension // URL-encode the path to handle spaces and special characters const encodedPath = notePath.split('/').map(segment => encodeURIComponent(segment)).join('/'); - link = `![${filename}](/data/${encodedPath})`; + link = `![${filename}](/api/images/${encodedPath})`; } else { // For notes, insert note link const noteName = notePath.split('/').pop().replace('.md', ''); @@ -879,7 +879,7 @@ function noteApp() { const filename = altText.replace(/\.[^/.]+$/, ''); // Remove extension // URL-encode the path to handle spaces and special characters const encodedPath = imagePath.split('/').map(segment => encodeURIComponent(segment)).join('/'); - const markdown = `![${filename}](/data/${encodedPath})`; + const markdown = `![${filename}](/api/images/${encodedPath})`; const textBefore = this.noteContent.substring(0, cursorPos); const textAfter = this.noteContent.substring(cursorPos); @@ -1242,7 +1242,7 @@ function noteApp() { const path = window.location.pathname; // Skip if root path or static assets - if (path === '/' || path.startsWith('/static/') || path.startsWith('/api/') || path.startsWith('/data/')) { + if (path === '/' || path.startsWith('/static/') || path.startsWith('/api/')) { return; } @@ -1855,24 +1855,8 @@ function noteApp() { async deleteCurrentNote() { if (!this.currentNote) return; - if (!confirm(`Delete "${this.currentNoteName}"?`)) return; - - try { - const response = await fetch(`/api/notes/${this.currentNote}`, { - method: 'DELETE' - }); - - if (response.ok) { - this.currentNote = ''; - this.noteContent = ''; - this.currentNoteName = ''; - await this.loadNotes(); - } else { - ErrorHandler.handle('delete note', new Error('Server returned error')); - } - } catch (error) { - ErrorHandler.handle('delete note', error); - } + // Just call deleteNote with current note details + await this.deleteNote(this.currentNote, this.currentNoteName); }, // Delete any note from sidebar diff --git a/frontend/index.html b/frontend/index.html index 699806e..8a4b366 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1197,7 +1197,7 @@ style="background-color: var(--bg-primary); min-height: 0;" >