// Path: Backend/internal/bootstrap/project_disk_folders.go package bootstrap import ( "fmt" "os" "path/filepath" "strings" "github.com/google/uuid" ) func (service *Service) createProjectFolderOnDisk(projectSlug, parentFolderID, name string, rootPathBuilder func(projectSlug string) string) (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("folder name is required") } containerProjectionPath := rootPathBuilder(projectSlug) parentDir := filepath.Join(posixRoot, filepath.FromSlash(containerProjectionPath)) if strings.TrimSpace(parentFolderID) != "" { containerProjectionPath = filepath.ToSlash(filepath.Join(strings.TrimSpace(parentFolderID), "children")) 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 folder path: %w", err) } baseSlug := normalizePOSIXSlug(trimmedName) folderName := slugDir("folder", baseSlug) folderDir := filepath.Join(parentDir, folderName) folderSlug := baseSlug for attempt := 2; ; attempt += 1 { if _, err := os.Stat(folderDir); os.IsNotExist(err) { break } else if err != nil { return "", "", fmt.Errorf("stat candidate project folder: %w", err) } folderSlug = fmt.Sprintf("%s-%d", baseSlug, attempt) folderName = slugDir("folder", folderSlug) folderDir = filepath.Join(parentDir, folderName) } if err := os.MkdirAll(filepath.Join(folderDir, "children"), 0o755); err != nil { return "", "", fmt.Errorf("create project hierarchy folder: %w", err) } folderID := uuid.NewString() if err := writeCBORFile(filepath.Join(folderDir, posixFolderFileName), map[string]any{"id": folderID, "name": trimmedName, "slug": folderSlug, "type": "folder"}); err != nil { return "", "", fmt.Errorf("write project %s: %w", posixFolderFileName, err) } if err := writeCBORFile(filepath.Join(folderDir, posixACLFileName), map[string]any{"version": 1, "inherits": true, "rules": []any{}}); err != nil { return "", "", fmt.Errorf("write project %s: %w", posixACLFileName, err) } return filepath.ToSlash(filepath.Join(containerProjectionPath, folderName)), folderSlug, nil } func (service *Service) deleteProjectFolderOnDisk(projectSlug, folderID string, rootPathBuilder func(projectSlug string) string) (string, error) { posixRoot := strings.TrimSpace(service.posixRoot) if posixRoot == "" { return "", fmt.Errorf("POSIX root is not configured") } trimmedFolderID := strings.TrimSpace(folderID) if trimmedFolderID == "" { return "", ErrProjectFolderNotFound } rootProjectionPath := rootPathBuilder(projectSlug) folderProjectionPath := strings.TrimPrefix(filepath.ToSlash(filepath.Clean("/"+trimmedFolderID)), "/") if folderProjectionPath == "." || folderProjectionPath == rootProjectionPath || !strings.HasPrefix(folderProjectionPath, rootProjectionPath+"/") { return "", ErrProjectFolderNotFound } folderDir := filepath.Join(posixRoot, filepath.FromSlash(folderProjectionPath)) info, err := os.Stat(folderDir) if err != nil { if os.IsNotExist(err) { return "", ErrProjectFolderNotFound } return "", fmt.Errorf("stat project folder: %w", err) } if !info.IsDir() { return "", ErrProjectFolderNotFound } if err := os.RemoveAll(folderDir); err != nil { return "", fmt.Errorf("delete project folder: %w", err) } return folderProjectionPath, nil } func (service *Service) renameProjectFolderOnDisk(projectSlug, folderID, name string, rootPathBuilder func(projectSlug string) string) (string, string, error) { posixRoot := strings.TrimSpace(service.posixRoot) if posixRoot == "" { return "", "", fmt.Errorf("POSIX root is not configured") } trimmedFolderID := strings.TrimSpace(folderID) if trimmedFolderID == "" { return "", "", ErrProjectFolderNotFound } trimmedName := strings.TrimSpace(name) if trimmedName == "" { return "", "", fmt.Errorf("folder name is required") } rootProjectionPath := rootPathBuilder(projectSlug) folderProjectionPath := strings.TrimPrefix(filepath.ToSlash(filepath.Clean("/"+trimmedFolderID)), "/") if folderProjectionPath == "." || folderProjectionPath == rootProjectionPath || !strings.HasPrefix(folderProjectionPath, rootProjectionPath+"/") { return "", "", ErrProjectFolderNotFound } folderDir := filepath.Join(posixRoot, filepath.FromSlash(folderProjectionPath)) info, err := os.Stat(folderDir) if err != nil { if os.IsNotExist(err) { return "", "", ErrProjectFolderNotFound } return "", "", fmt.Errorf("stat project folder: %w", err) } if !info.IsDir() { return "", "", ErrProjectFolderNotFound } parentDir := filepath.Dir(folderDir) baseSlug := normalizePOSIXSlug(trimmedName) folderName := slugDir("folder", baseSlug) folderSlug := baseSlug destinationDir := filepath.Join(parentDir, folderName) for attempt := 2; ; attempt += 1 { if destinationDir == folderDir { break } if _, err := os.Stat(destinationDir); os.IsNotExist(err) { break } else if err != nil { return "", "", fmt.Errorf("stat candidate renamed project folder: %w", err) } folderSlug = fmt.Sprintf("%s-%d", baseSlug, attempt) folderName = slugDir("folder", folderSlug) destinationDir = filepath.Join(parentDir, folderName) } renamedProjectionPath := filepath.ToSlash(filepath.Join(filepath.Dir(folderProjectionPath), folderName)) if destinationDir != folderDir { if err := os.Rename(folderDir, destinationDir); err != nil { return "", "", fmt.Errorf("rename project folder: %w", err) } } folderPayload := readStructuredFileMap(filepath.Join(destinationDir, posixFolderFileName)) folderMetadataID, _ := folderPayload["id"].(string) if strings.TrimSpace(folderMetadataID) == "" { folderMetadataID = uuid.NewString() } if err := writeCBORFile(filepath.Join(destinationDir, posixFolderFileName), map[string]any{"id": folderMetadataID, "name": trimmedName, "slug": folderSlug, "type": "folder"}); err != nil { return "", "", fmt.Errorf("write renamed project %s: %w", posixFolderFileName, err) } return folderProjectionPath, renamedProjectionPath, nil } func (service *Service) moveProjectFolderOnDisk(projectSlug, folderID, parentFolderID string, rootPathBuilder func(projectSlug string) string) (string, string, error) { posixRoot := strings.TrimSpace(service.posixRoot) if posixRoot == "" { return "", "", fmt.Errorf("POSIX root is not configured") } rootProjectionPath := rootPathBuilder(projectSlug) folderProjectionPath := strings.TrimPrefix(filepath.ToSlash(filepath.Clean("/"+strings.TrimSpace(folderID))), "/") if folderProjectionPath == "." || folderProjectionPath == rootProjectionPath || !strings.HasPrefix(folderProjectionPath, rootProjectionPath+"/") { return "", "", ErrProjectFolderNotFound } folderDir := filepath.Join(posixRoot, filepath.FromSlash(folderProjectionPath)) info, err := os.Stat(folderDir) if err != nil { if os.IsNotExist(err) { return "", "", ErrProjectFolderNotFound } return "", "", fmt.Errorf("stat project folder: %w", err) } if !info.IsDir() { return "", "", ErrProjectFolderNotFound } trimmedParentFolderID := strings.TrimSpace(parentFolderID) parentChildrenProjectionPath := rootProjectionPath parentDir := filepath.Join(posixRoot, filepath.FromSlash(rootProjectionPath)) if trimmedParentFolderID != "" { parentProjectionPath := strings.TrimPrefix(filepath.ToSlash(filepath.Clean("/"+trimmedParentFolderID)), "/") if parentProjectionPath == "." || parentProjectionPath == rootProjectionPath || !strings.HasPrefix(parentProjectionPath, rootProjectionPath+"/") { return "", "", ErrProjectFolderNotFound } if parentProjectionPath == folderProjectionPath || strings.HasPrefix(parentProjectionPath, folderProjectionPath+"/children/") { return "", "", ErrInvalidProjectFolderMove } parentChildrenProjectionPath = filepath.ToSlash(filepath.Join(parentProjectionPath, "children")) parentDir = filepath.Join(posixRoot, filepath.FromSlash(parentChildrenProjectionPath)) } parentInfo, err := os.Stat(parentDir) if err != nil { if os.IsNotExist(err) { return "", "", ErrProjectFolderNotFound } return "", "", fmt.Errorf("stat project folder parent: %w", err) } if !parentInfo.IsDir() { return "", "", ErrProjectFolderNotFound } currentParentDir := filepath.Dir(folderDir) if samePath(currentParentDir, parentDir) { return folderProjectionPath, folderProjectionPath, nil } folderPayload := readStructuredFileMap(filepath.Join(folderDir, posixFolderFileName)) folderMetadataID, _ := folderPayload["id"].(string) if strings.TrimSpace(folderMetadataID) == "" { folderMetadataID = uuid.NewString() } folderName, _ := folderPayload["name"].(string) if strings.TrimSpace(folderName) == "" { folderName = fallbackFolderLabel(folderProjectionPath) } currentBase := filepath.Base(folderDir) baseSlug := strings.TrimPrefix(currentBase, "folder-") if strings.TrimSpace(baseSlug) == "" { baseSlug = normalizePOSIXSlug(folderName) } folderSlug := baseSlug folderDirName := slugDir("folder", folderSlug) destinationDir := filepath.Join(parentDir, folderDirName) for attempt := 2; ; attempt += 1 { if _, err := os.Stat(destinationDir); os.IsNotExist(err) { break } else if err != nil { return "", "", fmt.Errorf("stat candidate moved project folder: %w", err) } folderSlug = fmt.Sprintf("%s-%d", baseSlug, attempt) folderDirName = slugDir("folder", folderSlug) destinationDir = filepath.Join(parentDir, folderDirName) } if err := os.Rename(folderDir, destinationDir); err != nil { return "", "", fmt.Errorf("move project folder: %w", err) } folderPayload["id"] = folderMetadataID folderPayload["name"] = folderName folderPayload["slug"] = folderSlug folderPayload["type"] = "folder" if err := writeCBORFile(filepath.Join(destinationDir, posixFolderFileName), folderPayload); err != nil { return "", "", fmt.Errorf("write moved project %s: %w", posixFolderFileName, err) } movedProjectionPath := filepath.ToSlash(filepath.Join(parentChildrenProjectionPath, folderDirName)) return folderProjectionPath, movedProjectionPath, nil }