refactor: reorganize project structure and update configurations

- Move migrations/ to database/schema/ for clarity on database schema definitions
- Move sqlc.yaml to internal/database/ to group with database code
- Move static/ to cmd/server/static/ to co-locate with server
- Update all configuration files and documentation
- Follow Go project conventions for better organization
This commit is contained in:
2026-01-24 23:40:31 -05:00
parent 8a951fb242
commit 08e80ae84b
34 changed files with 532 additions and 104 deletions
+52 -3
View File
@@ -4,11 +4,14 @@ import (
"bookmann/internal/database"
"fmt"
"net/http"
"path/filepath"
"strings"
"time"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
@@ -370,6 +373,31 @@ type AddEbookFolderRequest struct {
FolderPath string `json:"folder_path" validate:"required"`
}
// normalizePath cleans and normalizes folder paths for consistent storage and comparison
func normalizePath(path string) string {
fmt.Printf("normalizePath input: '%s'\n", path)
var cleaned string
// Handle home directory expansion (~)
if strings.HasPrefix(path, "~/") {
// Keep the original path for ~ to preserve user's formatting
// Just normalize separators and that's it
cleaned = strings.ReplaceAll(path, "\\", "/")
} else {
// Clean the path to remove redundant separators, ., .. etc.
cleaned = filepath.Clean(path)
// Convert to consistent path separators (use forward slashes for storage)
cleaned = strings.ReplaceAll(cleaned, "\\", "/")
// Remove trailing slash unless it's root path
if len(cleaned) > 1 && strings.HasSuffix(cleaned, "/") {
cleaned = strings.TrimSuffix(cleaned, "/")
}
}
fmt.Printf("normalizePath output: '%s'\n", cleaned)
return cleaned
}
type EbookFolderResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
@@ -397,9 +425,12 @@ func (h *AuthHandler) AddEbookFolder(c echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
// Normalize the folder path before storing
normalizedPath := normalizePath(req.FolderPath)
folder, err := h.db.AddUserEbookFolder(c.Request().Context(), database.AddUserEbookFolderParams{
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
FolderPath: req.FolderPath,
FolderPath: normalizedPath,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -426,6 +457,12 @@ func (h *AuthHandler) GetEbookFolders(c echo.Context) error {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
fmt.Printf("GetEbookFolders: user_id='%s'\n", userID)
for _, folder := range folders {
fmt.Printf(" Folder in DB: id='%s', path='%s'\n",
uuid.UUID(folder.ID.Bytes).String(), folder.FolderPath)
}
var response []EbookFolderResponse
for _, folder := range folders {
response = append(response, EbookFolderResponse{
@@ -455,14 +492,26 @@ func (h *AuthHandler) DeleteEbookFolder(c echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
err = h.db.DeleteUserEbookFolder(c.Request().Context(), database.DeleteUserEbookFolderParams{
// Normalize the folder path before deletion
normalizedPath := normalizePath(req.FolderPath)
// Debug logging - remove in production
fmt.Printf("DeleteEbookFolder: original path='%s', normalized path='%s', user_id='%s'\n",
req.FolderPath, normalizedPath, userID)
deletedFolder, err := h.db.DeleteUserEbookFolder(c.Request().Context(), database.DeleteUserEbookFolderParams{
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
FolderPath: req.FolderPath,
FolderPath: normalizedPath,
})
if err != nil {
fmt.Printf("DeleteEbookFolder failed: %v\n", err)
if err == pgx.ErrNoRows {
return c.JSON(http.StatusNotFound, map[string]string{"error": "ebook folder not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
fmt.Printf("DeleteEbookFolder succeeded: deleted folder with path '%s'\n", deletedFolder.FolderPath)
return c.JSON(http.StatusOK, map[string]string{"message": "ebook folder deleted successfully"})
}
+32 -7
View File
@@ -426,7 +426,7 @@ func (h *Handler) GetEbookRatings(c echo.Context) error {
// ScanEbooksRequest represents the request for scanning ebooks
type ScanEbooksRequest struct {
FolderPaths []string `json:"folder_paths" validate:"required,min=1"`
FolderPaths []string `json:"folder_paths,omitempty"`
}
// ScanEbooks handles POST /api/scanner/scan
@@ -435,12 +435,40 @@ func (h *Handler) ScanEbooks(c echo.Context) error {
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
// Get user ID from JWT token
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
}
var folderPaths []string
// If folder paths provided in request, use them
// Otherwise, use user's saved folders
if len(req.FolderPaths) > 0 {
folderPaths = req.FolderPaths
} else {
// Get user's configured ebook folders
folders, err := h.db.GetUserEbookFolders(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get user folders: " + err.Error()})
}
if len(folders) == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "no folders configured for user"})
}
// Convert to folder paths
folderPaths = make([]string, len(folders))
for i, folder := range folders {
folderPaths[i] = folder.FolderPath
}
}
// Set the folder paths for scanning
if err := h.scanner.SetFolders(req.FolderPaths); err != nil {
if err := h.scanner.SetFolders(folderPaths); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid folder paths: " + err.Error()})
}
@@ -461,9 +489,6 @@ func (h *Handler) StartScanner(c echo.Context) error {
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
// Set the folder paths
if err := h.scanner.SetFolders(req.FolderPaths); err != nil {