fix(workspace): dedicated reconnecting websocket, restore editing lock blocking & amber banner, broadcast live doc/folder moves
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m1s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m1s
This commit is contained in:
1 parent
ed673b0707
commit
e088ef6974
5 files changed
+323
-108
No files matched your search
@@ -2807,6 +2807,138 @@ func (s *Store) MoveDocument(userID, docID, targetFolderPath string) (*domain.Do
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// MoveFolder moves a folder (and all its documents/subfolders) to a new target parent path.
|
||||
func (s *Store) MoveFolder(userID, sourcePath, targetParentPath string) (string, []string, error) {
|
||||
cleanSource := filepath.ToSlash(filepath.Clean(strings.TrimSpace(sourcePath)))
|
||||
if cleanSource == "" || cleanSource == "." || strings.HasPrefix(cleanSource, "/") || strings.Contains(cleanSource, "..") {
|
||||
return "", nil, fmt.Errorf("ungültiger Quell-Ordnerpfad")
|
||||
}
|
||||
cleanTarget := filepath.ToSlash(filepath.Clean(strings.TrimSpace(targetParentPath)))
|
||||
if cleanTarget == "." {
|
||||
cleanTarget = ""
|
||||
}
|
||||
if cleanTarget != "" && (cleanTarget == cleanSource || strings.HasPrefix(cleanTarget, cleanSource+"/")) {
|
||||
return "", nil, fmt.Errorf("ein Ordner kann nicht in sich selbst verschoben werden")
|
||||
}
|
||||
|
||||
folderName := filepath.Base(cleanSource)
|
||||
var newFolderPath string
|
||||
if cleanTarget == "" {
|
||||
newFolderPath = folderName
|
||||
} else {
|
||||
newFolderPath = cleanTarget + "/" + folderName
|
||||
}
|
||||
|
||||
if newFolderPath == cleanSource {
|
||||
return cleanSource, []string{}, nil
|
||||
}
|
||||
|
||||
// 1. Check if sourcePath is the root mount of a shared folder
|
||||
mounts, _, err := s.getUserFolderMounts(userID)
|
||||
if err == nil {
|
||||
for sfID, mPath := range mounts {
|
||||
if mPath == cleanSource {
|
||||
err := s.UpdateFolderMount(userID, sfID, newFolderPath)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return newFolderPath, []string{}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Move all affected documents
|
||||
docs, err := s.GetDocuments(userID)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
var movedDocIDs []string
|
||||
for _, doc := range docs {
|
||||
if doc.FolderPath == cleanSource || strings.HasPrefix(doc.FolderPath, cleanSource+"/") {
|
||||
var nextDocFolder string
|
||||
if doc.FolderPath == cleanSource {
|
||||
nextDocFolder = newFolderPath
|
||||
} else {
|
||||
suffix := strings.TrimPrefix(doc.FolderPath, cleanSource)
|
||||
nextDocFolder = newFolderPath + suffix
|
||||
}
|
||||
_, moveErr := s.MoveDocument(userID, doc.ID, nextDocFolder)
|
||||
if moveErr == nil {
|
||||
movedDocIDs = append(movedDocIDs, doc.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Re-map workspace_folders entries
|
||||
targetSFID, targetSub, isTargetSF := matchMountPath(newFolderPath, mounts)
|
||||
sourceSFID, sourceSub, isSourceSF := matchMountPath(cleanSource, mounts)
|
||||
|
||||
wfRows, qErr := s.db.Query(`SELECT id, user_id, shared_folder_id, path, sub_path FROM workspace_folders`)
|
||||
if qErr == nil {
|
||||
type wfRecord struct {
|
||||
id, uid, sfID, path, subPath string
|
||||
}
|
||||
var records []wfRecord
|
||||
for wfRows.Next() {
|
||||
var r wfRecord
|
||||
if err := wfRows.Scan(&r.id, &r.uid, &r.sfID, &r.path, &r.subPath); err == nil {
|
||||
records = append(records, r)
|
||||
}
|
||||
}
|
||||
_ = wfRows.Close()
|
||||
|
||||
for _, r := range records {
|
||||
if isSourceSF && r.sfID == sourceSFID {
|
||||
matches := false
|
||||
var suffix string
|
||||
if sourceSub == "" {
|
||||
matches = true
|
||||
suffix = "/" + r.subPath
|
||||
} else if r.subPath == sourceSub {
|
||||
matches = true
|
||||
suffix = ""
|
||||
} else if strings.HasPrefix(r.subPath, sourceSub+"/") {
|
||||
matches = true
|
||||
suffix = strings.TrimPrefix(r.subPath, sourceSub)
|
||||
}
|
||||
if matches {
|
||||
var newSub string
|
||||
if targetSub == "" {
|
||||
newSub = strings.TrimPrefix(folderName+suffix, "/")
|
||||
} else {
|
||||
newSub = targetSub + "/" + folderName + suffix
|
||||
}
|
||||
if isTargetSF {
|
||||
_, _ = s.db.Exec(`UPDATE workspace_folders SET shared_folder_id = ?, sub_path = ? WHERE id = ?`, targetSFID, newSub, r.id)
|
||||
} else {
|
||||
newP := newFolderPath + suffix
|
||||
_, _ = s.db.Exec(`UPDATE workspace_folders SET shared_folder_id = '', sub_path = '', path = ? WHERE id = ?`, newP, r.id)
|
||||
}
|
||||
}
|
||||
} else if r.path == cleanSource || strings.HasPrefix(r.path, cleanSource+"/") {
|
||||
suffix := strings.TrimPrefix(r.path, cleanSource)
|
||||
newP := newFolderPath + suffix
|
||||
if isTargetSF {
|
||||
var newSub string
|
||||
if targetSub == "" {
|
||||
newSub = strings.TrimPrefix(folderName+suffix, "/")
|
||||
} else {
|
||||
newSub = targetSub + "/" + folderName + suffix
|
||||
}
|
||||
_, _ = s.db.Exec(`UPDATE workspace_folders SET shared_folder_id = ?, sub_path = ?, path = ? WHERE id = ?`, targetSFID, newSub, newP, r.id)
|
||||
} else {
|
||||
_, _ = s.db.Exec(`UPDATE workspace_folders SET path = ? WHERE id = ?`, newP, r.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = s.CreateFolder(userID, newFolderPath)
|
||||
|
||||
return newFolderPath, movedDocIDs, nil
|
||||
}
|
||||
|
||||
// syncSharedFolderContents ensures all documents and subfolders under cleanMount owned by ownerID are linked to folderID.
|
||||
func (s *Store) syncSharedFolderContents(ownerID, folderID, cleanMount string) {
|
||||
if ownerID == "" || folderID == "" || cleanMount == "" {
|
||||
|
||||
@@ -665,5 +665,29 @@ func TestDatabaseFirstWorkspaceArchitecture(t *testing.T) {
|
||||
if !foundSubfolderForB {
|
||||
t.Fatalf("expected User B to see empty shared subfolder 'ExterneProjekte/TeamProjekt/Konzepte', got folders: %v", foldersB)
|
||||
}
|
||||
|
||||
// 3. MoveFolder inside a shared folder
|
||||
newPath, _, err := store.MoveFolder(userA.ID, "TeamProjekt/Konzepte", "TeamProjekt/Archiv")
|
||||
if err != nil {
|
||||
t.Fatalf("MoveFolder failed: %v", err)
|
||||
}
|
||||
if newPath != "TeamProjekt/Archiv/Konzepte" {
|
||||
t.Fatalf("expected newPath 'TeamProjekt/Archiv/Konzepte', got %q", newPath)
|
||||
}
|
||||
|
||||
// User B must now see "ExterneProjekte/TeamProjekt/Archiv/Konzepte"
|
||||
foldersBAfterMove, err := store.GetFolders(userB.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetFolders after MoveFolder failed: %v", err)
|
||||
}
|
||||
foundMovedForB := false
|
||||
for _, f := range foldersBAfterMove {
|
||||
if f == "ExterneProjekte/TeamProjekt/Archiv/Konzepte" {
|
||||
foundMovedForB = true
|
||||
}
|
||||
}
|
||||
if !foundMovedForB {
|
||||
t.Fatalf("expected User B to see moved shared subfolder 'ExterneProjekte/TeamProjekt/Archiv/Konzepte', got %v", foldersBAfterMove)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user