253 lines
10 KiB
Go
253 lines
10 KiB
Go
// 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
|
|
}
|