package handlers import ( "fmt" "io" "net/http" "os" "path/filepath" "strings" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" "github.com/h2non/filetype" "gobsidian/internal/config" "gobsidian/internal/markdown" "gobsidian/internal/models" "gobsidian/internal/utils" ) type Handlers struct { config *config.Config store *sessions.CookieStore renderer *markdown.Renderer } // EditTextPageHandler renders an editor for allowed text files (json, html, xml, yaml, etc.) func (h *Handlers) EditTextPageHandler(c *gin.Context) { filePath := strings.TrimPrefix(c.Param("path"), "/") // Security check if strings.Contains(filePath, "..") { c.HTML(http.StatusBadRequest, "error", gin.H{ "error": "Invalid path", "app_name": h.config.AppName, "message": "Path traversal is not allowed", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } fullPath := filepath.Join(h.config.NotesDir, filePath) // Ensure file exists if _, err := os.Stat(fullPath); os.IsNotExist(err) { c.HTML(http.StatusNotFound, "error", gin.H{ "error": "File not found", "app_name": h.config.AppName, "message": "The requested file does not exist", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } // Only allow editing of configured text file types (not markdown here) ext := filepath.Ext(fullPath) ftype := models.GetFileType(ext, h.config.AllowedImageExtensions, h.config.AllowedFileExtensions) if ftype != models.FileTypeText { c.HTML(http.StatusForbidden, "error", gin.H{ "error": "Editing not allowed", "app_name": h.config.AppName, "message": "This file type cannot be edited here", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } // Load content data, err := os.ReadFile(fullPath) if err != nil { c.HTML(http.StatusInternalServerError, "error", gin.H{ "error": "Failed to read file", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } // Build notes tree notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config) if err != nil { c.HTML(http.StatusInternalServerError, "error", gin.H{ "error": "Failed to build notes tree", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } folderPath := filepath.Dir(filePath) if folderPath == "." { folderPath = "" } c.HTML(http.StatusOK, "edit_text", gin.H{ "app_name": h.config.AppName, "title": filepath.Base(filePath), "content": string(data), "file_path": filePath, "file_ext": strings.TrimPrefix(strings.ToLower(ext), "."), "folder_path": folderPath, "notes_tree": notesTree, "active_path": utils.GetActivePath(folderPath), "breadcrumbs": utils.GenerateBreadcrumbs(folderPath), "ContentTemplate": "edit_text_content", "ScriptsTemplate": "edit_text_scripts", "Page": "edit_text", }) } // PostEditTextHandler saves changes to an allowed text file func (h *Handlers) PostEditTextHandler(c *gin.Context) { filePath := strings.TrimPrefix(c.Param("path"), "/") if strings.Contains(filePath, "..") { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid path"}) return } fullPath := filepath.Join(h.config.NotesDir, filePath) // Enforce allowed file type ext := filepath.Ext(fullPath) ftype := models.GetFileType(ext, h.config.AllowedImageExtensions, h.config.AllowedFileExtensions) if ftype != models.FileTypeText { c.JSON(http.StatusForbidden, gin.H{"error": "This file type cannot be edited"}) return } content := c.PostForm("content") // Ensure parent directory exists if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create parent directory"}) return } if err := os.WriteFile(fullPath, []byte(content), 0o644); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) return } c.JSON(http.StatusOK, gin.H{"success": true, "redirect": "/view_text/" + filePath}) } func New(cfg *config.Config, store *sessions.CookieStore) *Handlers { return &Handlers{ config: cfg, store: store, renderer: markdown.NewRenderer(cfg), } } func (h *Handlers) IndexHandler(c *gin.Context) { fmt.Printf("DEBUG: IndexHandler called\n") folderContents, err := utils.GetFolderContents("", h.config) if err != nil { fmt.Printf("DEBUG: Error getting folder contents: %v\n", err) c.HTML(http.StatusInternalServerError, "error", gin.H{ "error": "Failed to read directory", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } fmt.Printf("DEBUG: Found %d folder contents\n", len(folderContents)) notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config) if err != nil { fmt.Printf("DEBUG: Error building tree structure: %v\n", err) c.HTML(http.StatusInternalServerError, "error", gin.H{ "error": "Failed to build tree structure", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } fmt.Printf("DEBUG: Tree structure built, app_name: %s\n", h.config.AppName) c.HTML(http.StatusOK, "folder", gin.H{ "app_name": h.config.AppName, "folder_path": "", "folder_contents": folderContents, "notes_tree": notesTree, "active_path": []string{}, "current_note": nil, "breadcrumbs": utils.GenerateBreadcrumbs(""), "allowed_image_extensions": h.config.AllowedImageExtensions, "allowed_file_extensions": h.config.AllowedFileExtensions, "ContentTemplate": "folder_content", "ScriptsTemplate": "folder_scripts", "Page": "folder", }) } func (h *Handlers) FolderHandler(c *gin.Context) { folderPath := strings.TrimPrefix(c.Param("path"), "/") // Security check - prevent path traversal if strings.Contains(folderPath, "..") { c.HTML(http.StatusBadRequest, "error", gin.H{ "error": "Invalid path", "app_name": h.config.AppName, "message": "Path traversal is not allowed", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } // Check if path is in skipped directories if utils.IsPathInSkippedDirs(folderPath, h.config.NotesDirSkip) { c.HTML(http.StatusForbidden, "error", gin.H{ "error": "Access denied", "app_name": h.config.AppName, "message": "This directory is not accessible", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } folderContents, err := utils.GetFolderContents(folderPath, h.config) if err != nil { c.HTML(http.StatusNotFound, "error", gin.H{ "error": "Folder not found", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config) if err != nil { c.HTML(http.StatusInternalServerError, "error", gin.H{ "error": "Failed to build tree structure", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } c.HTML(http.StatusOK, "folder", gin.H{ "app_name": h.config.AppName, "folder_path": folderPath, "folder_contents": folderContents, "notes_tree": notesTree, "active_path": utils.GetActivePath(folderPath), "current_note": nil, "breadcrumbs": utils.GenerateBreadcrumbs(folderPath), "allowed_image_extensions": h.config.AllowedImageExtensions, "allowed_file_extensions": h.config.AllowedFileExtensions, "ContentTemplate": "folder_content", "ScriptsTemplate": "folder_scripts", "Page": "folder", }) } func (h *Handlers) NoteHandler(c *gin.Context) { notePath := strings.TrimPrefix(c.Param("path"), "/") if !strings.HasSuffix(notePath, ".md") { c.HTML(http.StatusBadRequest, "error", gin.H{ "error": "Invalid note path", "app_name": h.config.AppName, "message": "Note path must end with .md", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } // Security check if strings.Contains(notePath, "..") { c.HTML(http.StatusBadRequest, "error", gin.H{ "error": "Invalid path", "app_name": h.config.AppName, "message": "Path traversal is not allowed", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } // Check if path is in skipped directories if utils.IsPathInSkippedDirs(notePath, h.config.NotesDirSkip) { c.HTML(http.StatusForbidden, "error", gin.H{ "error": "Access denied", "app_name": h.config.AppName, "message": "This note is not accessible", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } fullPath := filepath.Join(h.config.NotesDir, notePath) if _, err := os.Stat(fullPath); os.IsNotExist(err) { c.HTML(http.StatusNotFound, "error", gin.H{ "error": "Note not found", "app_name": h.config.AppName, "message": "The requested note does not exist", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } content, err := os.ReadFile(fullPath) if err != nil { c.HTML(http.StatusInternalServerError, "error", gin.H{ "error": "Failed to read note", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } htmlContent, err := h.renderer.RenderMarkdown(string(content), notePath) if err != nil { c.HTML(http.StatusInternalServerError, "error", gin.H{ "error": "Failed to render markdown", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config) if err != nil { c.HTML(http.StatusInternalServerError, "error", gin.H{ "error": "Failed to build tree structure", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } title := strings.TrimSuffix(filepath.Base(notePath), ".md") folderPath := filepath.Dir(notePath) if folderPath == "." { folderPath = "" } c.HTML(http.StatusOK, "note", gin.H{ "app_name": h.config.AppName, "title": title, "content": htmlContent, "note_path": notePath, "folder_path": folderPath, "notes_tree": notesTree, "active_path": utils.GetActivePath(folderPath), "current_note": notePath, "breadcrumbs": utils.GenerateBreadcrumbs(folderPath), "ContentTemplate": "note_content", "ScriptsTemplate": "note_scripts", "Page": "note", }) } func (h *Handlers) ServeAttachedImageHandler(c *gin.Context) { imagePath := strings.TrimPrefix(c.Param("path"), "/") // Security check if strings.Contains(imagePath, "..") { c.AbortWithStatus(http.StatusBadRequest) return } var fullPath string switch h.config.ImageStorageMode { case 1: // Root directory fullPath = filepath.Join(h.config.NotesDir, imagePath) case 3: // Same as note directory fullPath = filepath.Join(h.config.NotesDir, imagePath) case 4: // Subfolder of note directory fullPath = filepath.Join(h.config.NotesDir, imagePath) default: c.AbortWithStatus(http.StatusNotFound) return } // Check if file exists and is an image if _, err := os.Stat(fullPath); os.IsNotExist(err) { c.AbortWithStatus(http.StatusNotFound) return } if !models.IsImageFile(filepath.Base(imagePath), h.config.AllowedImageExtensions) { c.AbortWithStatus(http.StatusForbidden) return } c.File(fullPath) } func (h *Handlers) ServeStoredImageHandler(c *gin.Context) { filename := c.Param("filename") // Security check if strings.Contains(filename, "..") || strings.Contains(filename, "/") { c.AbortWithStatus(http.StatusBadRequest) return } if h.config.ImageStorageMode != 2 { c.AbortWithStatus(http.StatusNotFound) return } fullPath := filepath.Join(h.config.ImageStoragePath, filename) if _, err := os.Stat(fullPath); os.IsNotExist(err) { c.AbortWithStatus(http.StatusNotFound) return } if !models.IsImageFile(filename, h.config.AllowedImageExtensions) { c.AbortWithStatus(http.StatusForbidden) return } c.File(fullPath) } func (h *Handlers) DownloadHandler(c *gin.Context) { filePath := strings.TrimPrefix(c.Param("path"), "/") // Security check if strings.Contains(filePath, "..") { c.AbortWithStatus(http.StatusBadRequest) return } fullPath := filepath.Join(h.config.NotesDir, filePath) if _, err := os.Stat(fullPath); os.IsNotExist(err) { c.AbortWithStatus(http.StatusNotFound) return } filename := filepath.Base(filePath) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) c.File(fullPath) } func (h *Handlers) ViewTextHandler(c *gin.Context) { filePath := strings.TrimPrefix(c.Param("path"), "/") // Security check if strings.Contains(filePath, "..") { c.HTML(http.StatusBadRequest, "error", gin.H{ "error": "Invalid path", "app_name": h.config.AppName, "message": "Path traversal is not allowed", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } fullPath := filepath.Join(h.config.NotesDir, filePath) if _, err := os.Stat(fullPath); os.IsNotExist(err) { c.HTML(http.StatusNotFound, "error", gin.H{ "error": "File not found", "app_name": h.config.AppName, "message": "The requested file does not exist", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } // Check if file extension is allowed if !models.IsAllowedFile(filePath, h.config.AllowedFileExtensions) { c.HTML(http.StatusForbidden, "error", gin.H{ "error": "File type not allowed", "app_name": h.config.AppName, "message": "This file type cannot be viewed", "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } content, err := os.ReadFile(fullPath) if err != nil { c.HTML(http.StatusInternalServerError, "error", gin.H{ "error": "Failed to read file", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config) if err != nil { c.HTML(http.StatusInternalServerError, "error", gin.H{ "error": "Failed to build tree structure", "app_name": h.config.AppName, "message": err.Error(), "ContentTemplate": "error_content", "ScriptsTemplate": "error_scripts", "Page": "error", }) return } folderPath := filepath.Dir(filePath) if folderPath == "." { folderPath = "" } // Determine extension and whether file is editable as text ext := filepath.Ext(filePath) ftype := models.GetFileType(ext, h.config.AllowedImageExtensions, h.config.AllowedFileExtensions) isEditable := ftype == models.FileTypeText c.HTML(http.StatusOK, "view_text", gin.H{ "app_name": h.config.AppName, "file_name": filepath.Base(filePath), "file_path": filePath, "content": string(content), "file_ext": strings.TrimPrefix(strings.ToLower(ext), "."), "is_editable": isEditable, "folder_path": folderPath, "notes_tree": notesTree, "active_path": utils.GetActivePath(folderPath), "breadcrumbs": utils.GenerateBreadcrumbs(folderPath), "ContentTemplate": "view_text_content", "ScriptsTemplate": "view_text_scripts", "Page": "view_text", }) } func (h *Handlers) UploadHandler(c *gin.Context) { // Parse multipart form if err := c.Request.ParseMultipartForm(h.config.MaxContentLength); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "File too large or invalid form data"}) return } // Get the upload path uploadPath := c.PostForm("path") if uploadPath == "" { uploadPath = "" } // Security check if strings.Contains(uploadPath, "..") { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid upload path"}) return } file, header, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"}) return } defer file.Close() // Validate file type buffer := make([]byte, 512) if _, err := file.Read(buffer); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read file"}) return } file.Seek(0, 0) // Reset file pointer kind, _ := filetype.Match(buffer) if kind == filetype.Unknown { // Allow text files and other allowed extensions if !models.IsAllowedFile(header.Filename, append(h.config.AllowedImageExtensions, h.config.AllowedFileExtensions...)) { c.JSON(http.StatusBadRequest, gin.H{"error": "File type not allowed"}) return } } else { // Check if detected type is allowed isImageType := strings.HasPrefix(kind.MIME.Value, "image/") if !isImageType && !models.IsAllowedFile(header.Filename, h.config.AllowedFileExtensions) { c.JSON(http.StatusBadRequest, gin.H{"error": "File type not allowed"}) return } } // Determine upload directory var uploadDir string isImage := models.IsImageFile(header.Filename, h.config.AllowedImageExtensions) if isImage { // For images, use the configured storage mode storageInfo := utils.GetImageStorageInfo(uploadPath, h.config) uploadDir = storageInfo.StorageDir } else { // For other files, upload to the current folder uploadDir = filepath.Join(h.config.NotesDir, uploadPath) } // Ensure upload directory exists if err := utils.EnsureDir(uploadDir); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create upload directory"}) return } // Create destination file destPath := filepath.Join(uploadDir, header.Filename) dest, err := os.Create(destPath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create destination file"}) return } defer dest.Close() // Copy file content if _, err := io.Copy(dest, file); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "File uploaded successfully", "filename": header.Filename, "size": header.Size, }) } func (h *Handlers) TreeAPIHandler(c *gin.Context) { notesTree, err := utils.BuildTreeStructure(h.config.NotesDir, h.config.NotesDirHideSidepane, h.config) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, notesTree) } // SearchHandler performs a simple full-text search across markdown and allowed text files // within the notes directory, honoring skipped directories. // GET /api/search?q=term func (h *Handlers) SearchHandler(c *gin.Context) { query := strings.TrimSpace(c.Query("q")) if query == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing query"}) return } // Case-insensitive search qLower := strings.ToLower(query) type Snippet struct { Line int `json:"line"` Preview string `json:"preview"` } type Result struct { Path string `json:"path"` Type string `json:"type"` // "md" or "text" Snippets []Snippet `json:"snippets"` } results := make([]Result, 0, 32) // Walk the notes directory err := filepath.Walk(h.config.NotesDir, func(path string, info os.FileInfo, err error) error { if err != nil { return nil // skip on error } if info.IsDir() { // Compute relative dir path to check skip list rel, _ := filepath.Rel(h.config.NotesDir, path) rel = filepath.ToSlash(rel) if rel == "." { rel = "" } if utils.IsPathInSkippedDirs(rel, h.config.NotesDirSkip) { return filepath.SkipDir } return nil } // Compute relative file path relPath, _ := filepath.Rel(h.config.NotesDir, path) relPath = filepath.ToSlash(relPath) // Skip disallowed files ext := strings.ToLower(filepath.Ext(relPath)) isMD := ext == ".md" if !isMD && !models.IsAllowedFile(relPath, h.config.AllowedFileExtensions) { return nil } // Read file content (limit size to prevent huge memory usage) data, readErr := os.ReadFile(path) if readErr != nil { return nil } content := string(data) contentLower := strings.ToLower(content) if !strings.Contains(contentLower, qLower) { return nil } // Build snippets: show up to 3 matches with 2 lines of context lines := strings.Split(content, "\n") linesLower := strings.Split(contentLower, "\n") snippets := make([]Snippet, 0, 3) for i := 0; i < len(linesLower) && len(snippets) < 3; i++ { if strings.Contains(linesLower[i], qLower) { start := i - 2 if start < 0 { start = 0 } end := i + 2 if end >= len(lines) { end = len(lines) - 1 } // Join preview lines preview := strings.Join(lines[start:end+1], "\n") snippets = append(snippets, Snippet{Line: i + 1, Preview: preview}) } } rtype := "text" if isMD { rtype = "md" } results = append(results, Result{Path: relPath, Type: rtype, Snippets: snippets}) return nil }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"query": query, "results": results}) }