651 lines
18 KiB
Go
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})
|
|
}
|