Files
Work/Backend/internal/bootstrap/project_disk_items.go
T

190 lines
7.4 KiB
Go

// Path: Backend/internal/bootstrap/project_disk_items.go
package bootstrap
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
)
func (service *Service) createProjectTreeItemOnDisk(projectSlug, parentFolderPath, name, itemType string) (string, error) {
posixRoot := strings.TrimSpace(service.posixRoot)
if posixRoot == "" {
return "", fmt.Errorf("POSIX root is not configured")
}
trimmedName := strings.TrimSpace(name)
if trimmedName == "" {
return "", fmt.Errorf("item name is required")
}
canonicalItemType := normalizeProjectTreeItemType(itemType)
containerProjectionPath := projectTreeRootPath(projectSlug)
parentDir := filepath.Join(posixRoot, filepath.FromSlash(containerProjectionPath))
if trimmedParentFolderPath := strings.TrimSpace(parentFolderPath); trimmedParentFolderPath != "" {
containerProjectionPath = trimmedParentFolderPath
parentDir = filepath.Join(posixRoot, filepath.FromSlash(containerProjectionPath))
info, err := os.Stat(parentDir)
if err != nil {
if os.IsNotExist(err) {
return "", ErrProjectFolderNotFound
}
return "", fmt.Errorf("stat parent project folder: %w", err)
}
if !info.IsDir() {
return "", ErrProjectFolderNotFound
}
}
if err := os.MkdirAll(parentDir, 0o755); err != nil {
return "", fmt.Errorf("create parent project item path: %w", err)
}
baseSlug := normalizePOSIXSlug(trimmedName)
itemDirName := slugDir("item", baseSlug)
itemDir := filepath.Join(parentDir, itemDirName)
itemSlug := baseSlug
for attempt := 2; ; attempt += 1 {
if _, err := os.Stat(itemDir); os.IsNotExist(err) {
break
} else if err != nil {
return "", fmt.Errorf("stat candidate project item: %w", err)
}
itemSlug = fmt.Sprintf("%s-%d", baseSlug, attempt)
itemDirName = slugDir("item", itemSlug)
itemDir = filepath.Join(parentDir, itemDirName)
}
if err := os.MkdirAll(itemDir, 0o755); err != nil {
return "", fmt.Errorf("create project item: %w", err)
}
itemID := uuid.NewString()
if err := writeCBORFile(filepath.Join(itemDir, posixItemFileName), map[string]any{"id": itemID, "name": trimmedName, "slug": itemSlug, "type": canonicalItemType}); err != nil {
return "", fmt.Errorf("write project %s: %w", posixItemFileName, err)
}
if err := writeJSONFile(filepath.Join(itemDir, posixSchemaFileName), defaultProjectTreeItemSchema(canonicalItemType)); err != nil {
return "", fmt.Errorf("write project %s: %w", posixSchemaFileName, err)
}
if err := writeCBORFile(filepath.Join(itemDir, posixDataFileName), defaultProjectTreeItemData(canonicalItemType, trimmedName)); err != nil {
return "", fmt.Errorf("write project %s: %w", posixDataFileName, err)
}
return filepath.ToSlash(filepath.Join(containerProjectionPath, itemDirName)), nil
}
func (service *Service) deleteProjectTreeItemOnDisk(projectSlug, itemPath string) (string, error) {
posixRoot := strings.TrimSpace(service.posixRoot)
if posixRoot == "" {
return "", fmt.Errorf("POSIX root is not configured")
}
rootProjectionPath := projectTreeRootPath(projectSlug)
itemProjectionPath := strings.TrimPrefix(filepath.ToSlash(filepath.Clean("/"+strings.TrimSpace(itemPath))), "/")
if itemProjectionPath == "." || itemProjectionPath == rootProjectionPath || !strings.HasPrefix(itemProjectionPath, rootProjectionPath+"/") {
return "", ErrProjectItemNotFound
}
itemDir := filepath.Join(posixRoot, filepath.FromSlash(itemProjectionPath))
info, err := os.Stat(itemDir)
if err != nil {
if os.IsNotExist(err) {
return "", ErrProjectItemNotFound
}
return "", fmt.Errorf("stat project item: %w", err)
}
if !info.IsDir() {
return "", ErrProjectItemNotFound
}
if err := os.RemoveAll(itemDir); err != nil {
return "", fmt.Errorf("delete project item: %w", err)
}
return itemProjectionPath, nil
}
func (service *Service) moveProjectTreeItemOnDisk(projectSlug, itemPath, parentFolderPath string) (string, string, error) {
posixRoot := strings.TrimSpace(service.posixRoot)
if posixRoot == "" {
return "", "", fmt.Errorf("POSIX root is not configured")
}
rootProjectionPath := projectTreeRootPath(projectSlug)
itemProjectionPath := strings.TrimPrefix(filepath.ToSlash(filepath.Clean("/"+strings.TrimSpace(itemPath))), "/")
if itemProjectionPath == "." || itemProjectionPath == rootProjectionPath || !strings.HasPrefix(itemProjectionPath, rootProjectionPath+"/") {
return "", "", ErrProjectItemNotFound
}
itemDir := filepath.Join(posixRoot, filepath.FromSlash(itemProjectionPath))
info, err := os.Stat(itemDir)
if err != nil {
if os.IsNotExist(err) {
return "", "", ErrProjectItemNotFound
}
return "", "", fmt.Errorf("stat project item: %w", err)
}
if !info.IsDir() {
return "", "", ErrProjectItemNotFound
}
trimmedParentFolderPath := strings.TrimSpace(parentFolderPath)
parentProjectionPath := rootProjectionPath
parentDir := filepath.Join(posixRoot, filepath.FromSlash(parentProjectionPath))
if trimmedParentFolderPath != "" {
parentProjectionPath = strings.TrimPrefix(filepath.ToSlash(filepath.Clean("/"+trimmedParentFolderPath)), "/")
if parentProjectionPath == "." || parentProjectionPath == rootProjectionPath || !strings.HasPrefix(parentProjectionPath, rootProjectionPath+"/") {
return "", "", ErrProjectFolderNotFound
}
parentDir = filepath.Join(posixRoot, filepath.FromSlash(parentProjectionPath))
}
parentInfo, err := os.Stat(parentDir)
if err != nil {
if os.IsNotExist(err) {
return "", "", ErrProjectFolderNotFound
}
return "", "", fmt.Errorf("stat project item parent: %w", err)
}
if !parentInfo.IsDir() {
return "", "", ErrProjectFolderNotFound
}
currentParentDir := filepath.Dir(itemDir)
currentBase := filepath.Base(itemDir)
itemPayload := readStructuredFileMap(filepath.Join(itemDir, posixItemFileName))
itemID, _ := itemPayload["id"].(string)
if strings.TrimSpace(itemID) == "" {
itemID = uuid.NewString()
}
itemName, _ := itemPayload["name"].(string)
if strings.TrimSpace(itemName) == "" {
itemName = fallbackItemLabel(itemProjectionPath)
}
itemType, _ := itemPayload["type"].(string)
canonicalItemType := normalizeProjectTreeItemType(itemType)
baseSlug := strings.TrimPrefix(currentBase, "item-")
if strings.TrimSpace(baseSlug) == "" {
baseSlug = normalizePOSIXSlug(itemName)
}
itemSlug := baseSlug
itemDirName := slugDir("item", itemSlug)
destinationDir := filepath.Join(parentDir, itemDirName)
for attempt := 2; ; attempt += 1 {
if samePath(destinationDir, itemDir) {
break
}
if _, err := os.Stat(destinationDir); os.IsNotExist(err) {
break
} else if err != nil {
return "", "", fmt.Errorf("stat candidate moved project item: %w", err)
}
itemSlug = fmt.Sprintf("%s-%d", baseSlug, attempt)
itemDirName = slugDir("item", itemSlug)
destinationDir = filepath.Join(parentDir, itemDirName)
}
movedProjectionPath := filepath.ToSlash(filepath.Join(parentProjectionPath, itemDirName))
if samePath(currentParentDir, parentDir) && currentBase == itemDirName {
return itemProjectionPath, itemProjectionPath, nil
}
if err := os.Rename(itemDir, destinationDir); err != nil {
return "", "", fmt.Errorf("move project item: %w", err)
}
itemPayload["id"] = itemID
itemPayload["name"] = itemName
itemPayload["slug"] = itemSlug
itemPayload["type"] = canonicalItemType
if err := writeCBORFile(filepath.Join(destinationDir, posixItemFileName), itemPayload); err != nil {
return "", "", fmt.Errorf("write moved project %s: %w", posixItemFileName, err)
}
return itemProjectionPath, movedProjectionPath, nil
}