Feat: add persisted folder rename
This commit is contained in:
@@ -197,6 +197,12 @@ type DeleteProjectFolderInput struct {
|
||||
FolderID string
|
||||
}
|
||||
|
||||
type RenameProjectFolderInput struct {
|
||||
ProjectID string
|
||||
FolderID string
|
||||
Name string
|
||||
}
|
||||
|
||||
type CreateProjectFolderResult struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
CreatedFolder ProjectHierarchyFolderRecord `json:"createdFolder"`
|
||||
@@ -209,6 +215,13 @@ type DeleteProjectFolderResult struct {
|
||||
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
||||
}
|
||||
|
||||
type RenameProjectFolderResult struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
PreviousFolderID string `json:"previousFolderId"`
|
||||
RenamedFolder ProjectHierarchyFolderRecord `json:"renamedFolder"`
|
||||
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
||||
}
|
||||
|
||||
type projectHierarchyFolderRow struct {
|
||||
Path string
|
||||
ParentPath string
|
||||
@@ -917,6 +930,14 @@ func (service *Service) DeleteProjectTreeFolder(ctx context.Context, input Delet
|
||||
return service.deleteProjectHierarchyFolder(ctx, input, projectTreeRootPath, service.deleteProjectTreeFolderOnDisk)
|
||||
}
|
||||
|
||||
func (service *Service) RenameProjectFolder(ctx context.Context, input RenameProjectFolderInput) (RenameProjectFolderResult, error) {
|
||||
return service.renameProjectHierarchyFolder(ctx, input, projectHierarchyRootPath, service.renameProjectHierarchyFolderOnDisk)
|
||||
}
|
||||
|
||||
func (service *Service) RenameProjectTreeFolder(ctx context.Context, input RenameProjectFolderInput) (RenameProjectFolderResult, error) {
|
||||
return service.renameProjectHierarchyFolder(ctx, input, projectTreeRootPath, service.renameProjectTreeFolderOnDisk)
|
||||
}
|
||||
|
||||
func (service *Service) createProjectHierarchyFolder(
|
||||
ctx context.Context,
|
||||
input CreateProjectFolderInput,
|
||||
@@ -990,6 +1011,48 @@ func (service *Service) deleteProjectHierarchyFolder(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) renameProjectHierarchyFolder(
|
||||
ctx context.Context,
|
||||
input RenameProjectFolderInput,
|
||||
rootPath func(projectSlug string) string,
|
||||
renameOnDisk func(projectSlug, folderID, name string) (string, string, error),
|
||||
) (RenameProjectFolderResult, error) {
|
||||
project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID))
|
||||
if err != nil {
|
||||
return RenameProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
previousFolderID, renamedFolderID, err := renameOnDisk(project.Slug, input.FolderID, input.Name)
|
||||
if err != nil {
|
||||
return RenameProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
if err := posixproj.NewProjector(service.db, service.posixRoot).Rebuild(ctx); err != nil {
|
||||
return RenameProjectFolderResult{}, fmt.Errorf("rebuild POSIX projection: %w", err)
|
||||
}
|
||||
|
||||
folders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath)
|
||||
if err != nil {
|
||||
return RenameProjectFolderResult{}, err
|
||||
}
|
||||
|
||||
renamedFolder, found := findProjectHierarchyFolder(folders, renamedFolderID)
|
||||
if !found {
|
||||
return RenameProjectFolderResult{}, fmt.Errorf("renamed project folder missing from projection")
|
||||
}
|
||||
|
||||
if _, found := findProjectHierarchyFolder(folders, previousFolderID); found {
|
||||
return RenameProjectFolderResult{}, fmt.Errorf("previous project folder path still present in projection")
|
||||
}
|
||||
|
||||
return RenameProjectFolderResult{
|
||||
ProjectID: project.ID,
|
||||
PreviousFolderID: previousFolderID,
|
||||
RenamedFolder: renamedFolder,
|
||||
Folders: folders,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) listWorkspaces(ctx context.Context) ([]WorkspaceRecord, error) {
|
||||
rows, err := service.db.Pool.Query(ctx, `
|
||||
SELECT id::text, organization_id::text, name, slug, kind::text, department_id::text, team_id::text, project_id::text
|
||||
@@ -1281,6 +1344,14 @@ func (service *Service) deleteProjectTreeFolderOnDisk(projectSlug, folderID stri
|
||||
return service.deleteProjectFolderOnDisk(projectSlug, folderID, projectTreeRootPath)
|
||||
}
|
||||
|
||||
func (service *Service) renameProjectHierarchyFolderOnDisk(projectSlug, folderID, name string) (string, string, error) {
|
||||
return service.renameProjectFolderOnDisk(projectSlug, folderID, name, projectHierarchyRootPath)
|
||||
}
|
||||
|
||||
func (service *Service) renameProjectTreeFolderOnDisk(projectSlug, folderID, name string) (string, string, error) {
|
||||
return service.renameProjectFolderOnDisk(projectSlug, folderID, name, projectTreeRootPath)
|
||||
}
|
||||
|
||||
func (service *Service) createProjectFolderOnDisk(
|
||||
projectSlug, parentFolderID, name string,
|
||||
rootPathBuilder func(projectSlug string) string,
|
||||
@@ -1396,6 +1467,83 @@ func (service *Service) deleteProjectFolderOnDisk(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeJSONFile(filepath.Join(destinationDir, "folder.json"), map[string]any{
|
||||
"name": trimmedName,
|
||||
"slug": folderSlug,
|
||||
"type": "folder",
|
||||
}); err != nil {
|
||||
return "", "", fmt.Errorf("write renamed project folder.json: %w", err)
|
||||
}
|
||||
|
||||
return folderProjectionPath, renamedProjectionPath, nil
|
||||
}
|
||||
|
||||
func buildProjectHierarchyFolderTree(rows []projectHierarchyFolderRow, rootParentPath string) []ProjectHierarchyFolderRecord {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user