190 lines
7.4 KiB
Go
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
|
|
}
|