// 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 }