Files
gobsidian/internal/handlers/handlers.go
nahakubuilde da71deb7e2 search added
2025-08-25 18:02:41 +01:00

651 lines
18 KiB
Go

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
}
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 = ""
}
c.HTML(http.StatusOK, "view_text", gin.H{
"app_name": h.config.AppName,
"file_name": filepath.Base(filePath),
"file_path": filePath,
"content": string(content),
"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})
}