Skip to content

Commit db89bfd

Browse files
committed
Implement file/folder creation: add "Create Note/Folder" functionality in VaultView with CreateNoteDialog, update route handling in backend, and add unit tests for API and UI components.
1 parent fa1bdf8 commit db89bfd

File tree

7 files changed

+1416
-1
lines changed

7 files changed

+1416
-1
lines changed

internal/web/handlers_files.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package web
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"net/http"
67
"os"
78
"path/filepath"
89
"strings"
10+
"time"
911

1012
"github.com/susamn/obsidian-web/internal/db"
1113
"github.com/susamn/obsidian-web/internal/logger"
@@ -801,3 +803,156 @@ func (s *Server) extractVaultIDFromPath(urlPath, prefix string) string {
801803
path = strings.TrimSuffix(path, "/")
802804
return path
803805
}
806+
807+
// CreateFileRequest represents a request to create a file or folder
808+
type CreateFileRequest struct {
809+
VaultID string `json:"vault_id"`
810+
ParentID string `json:"parent_id,omitempty"` // Optional parent folder ID
811+
Name string `json:"name"` // File or folder name
812+
IsFolder bool `json:"is_folder"` // true for folder, false for file
813+
Content string `json:"content,omitempty"` // File content (only for files)
814+
}
815+
816+
// handleCreateFile godoc
817+
// @Summary Create a new file or folder
818+
// @Description Create a new file or folder in a vault
819+
// @Tags files
820+
// @Accept json
821+
// @Produce json
822+
// @Param request body CreateFileRequest true "Create file request"
823+
// @Success 200 {object} object{message=string,id=string,path=string}
824+
// @Failure 400 {object} ErrorResponse
825+
// @Failure 404 {object} ErrorResponse
826+
// @Failure 405 {object} ErrorResponse
827+
// @Failure 503 {object} ErrorResponse
828+
// @Router /api/v1/file/create [post]
829+
func (s *Server) handleCreateFile(w http.ResponseWriter, r *http.Request) {
830+
if r.Method != http.MethodPost {
831+
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
832+
return
833+
}
834+
835+
// Parse request body
836+
var req CreateFileRequest
837+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
838+
writeError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
839+
return
840+
}
841+
842+
// Validate required fields
843+
if req.VaultID == "" {
844+
writeError(w, http.StatusBadRequest, "vault_id is required")
845+
return
846+
}
847+
if req.Name == "" {
848+
writeError(w, http.StatusBadRequest, "name is required")
849+
return
850+
}
851+
852+
// Validate vault and get services
853+
v, dbService, ok := s.validateAndGetVaultWithDB(w, req.VaultID)
854+
if !ok {
855+
return
856+
}
857+
858+
// Build the file path
859+
var targetPath string
860+
if req.ParentID != "" {
861+
// Get parent folder entry to build path
862+
parentEntry, err := dbService.GetFileEntryByID(req.ParentID)
863+
if err != nil || parentEntry == nil {
864+
writeError(w, http.StatusNotFound, "Parent folder not found")
865+
return
866+
}
867+
if !parentEntry.IsDir {
868+
writeError(w, http.StatusBadRequest, "Parent must be a directory")
869+
return
870+
}
871+
targetPath = filepath.Join(parentEntry.Path, req.Name)
872+
} else {
873+
// Create at root
874+
targetPath = req.Name
875+
}
876+
877+
// Security: prevent directory traversal
878+
if strings.Contains(targetPath, "..") {
879+
writeError(w, http.StatusBadRequest, "Invalid path: directory traversal not allowed")
880+
return
881+
}
882+
883+
// Auto-add .md extension for files if not present
884+
if !req.IsFolder && !strings.HasSuffix(strings.ToLower(req.Name), ".md") {
885+
targetPath += ".md"
886+
req.Name += ".md"
887+
}
888+
889+
// Build full filesystem path
890+
fullPath := s.buildVaultFilePath(v, targetPath)
891+
if fullPath == "" {
892+
writeError(w, http.StatusInternalServerError, "Failed to build file path")
893+
return
894+
}
895+
896+
// Check if file/folder already exists
897+
if _, err := os.Stat(fullPath); err == nil {
898+
writeError(w, http.StatusConflict, fmt.Sprintf("%s already exists", map[bool]string{true: "Folder", false: "File"}[req.IsFolder]))
899+
return
900+
}
901+
902+
// Create the file or folder
903+
if req.IsFolder {
904+
// Create directory
905+
if err := os.MkdirAll(fullPath, 0755); err != nil {
906+
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create folder: %v", err))
907+
return
908+
}
909+
} else {
910+
// Ensure parent directory exists
911+
parentDir := filepath.Dir(fullPath)
912+
if err := os.MkdirAll(parentDir, 0755); err != nil {
913+
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create parent directory: %v", err))
914+
return
915+
}
916+
917+
// Create file with content
918+
content := req.Content
919+
if content == "" {
920+
content = "# " + strings.TrimSuffix(req.Name, ".md") + "\n\n"
921+
}
922+
if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
923+
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create file: %v", err))
924+
return
925+
}
926+
}
927+
928+
// The file will be picked up by the file watcher and indexed automatically
929+
// Wait a moment for the watcher to process it
930+
time.Sleep(100 * time.Millisecond)
931+
932+
// Try to get the new file entry from database
933+
var fileID string
934+
// Try a few times to get the file entry (watcher might take a moment)
935+
for i := 0; i < 10; i++ {
936+
entry, err := dbService.GetFileEntryByPath(targetPath)
937+
if err == nil && entry != nil {
938+
fileID = entry.ID
939+
break
940+
}
941+
time.Sleep(50 * time.Millisecond)
942+
}
943+
944+
logger.WithFields(map[string]interface{}{
945+
"vault_id": req.VaultID,
946+
"path": targetPath,
947+
"is_folder": req.IsFolder,
948+
"file_id": fileID,
949+
}).Info("Created file/folder")
950+
951+
writeSuccess(w, map[string]interface{}{
952+
"message": fmt.Sprintf("%s created successfully", map[bool]string{true: "Folder", false: "File"}[req.IsFolder]),
953+
"id": fileID,
954+
"path": targetPath,
955+
"name": req.Name,
956+
"is_folder": req.IsFolder,
957+
})
958+
}

0 commit comments

Comments
 (0)