feat(workspace): live synchronisation, verbindungsindikator, leere ordner persistenz und sperrkonfliktschutz
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m0s

This commit is contained in:
sandrodach committed 2026-10-26 10:18:02 +01:00
1 parent 35fd682fcd
commit 2f92e1f49a
4 files changed
+234 -14

No files matched your search

+2 -2
View File
@@ -1226,7 +1226,7 @@ func (s *Server) handleLockDoc(w http.ResponseWriter, r *http.Request) {
userName = user.DisplayName
}
info, acquired, currentLock := s.wsHub.AcquireDocLock(docID, userID, userName, 12*time.Second)
info, acquired, currentLock := s.wsHub.AcquireDocLock(docID, userID, userName, 20*time.Second)
if !acquired {
w.WriteHeader(http.StatusLocked)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
@@ -1249,7 +1249,7 @@ func (s *Server) handleLockDoc(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleHeartbeatDocLock(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
docID := chi.URLParam(r, "id")
ok := s.wsHub.HeartbeatDocLock(docID, userID, 12*time.Second)
ok := s.wsHub.HeartbeatDocLock(docID, userID, 20*time.Second)
writeJSON(w, http.StatusOK, map[string]bool{"extended": ok})
}
+94 -2
View File
@@ -362,6 +362,10 @@ func (s *Store) migrate() error {
"CREATE TABLE IF NOT EXISTS doc_user_folders (user_id TEXT NOT NULL, doc_id TEXT NOT NULL, folder_path TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(user_id, doc_id));",
"CREATE INDEX IF NOT EXISTS idx_doc_user_folders_user ON doc_user_folders(user_id);",
"CREATE TABLE IF NOT EXISTS workspace_folders (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, shared_folder_id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL, sub_path TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL);",
"CREATE INDEX IF NOT EXISTS idx_workspace_folders_user ON workspace_folders(user_id);",
"CREATE INDEX IF NOT EXISTS idx_workspace_folders_sf ON workspace_folders(shared_folder_id);",
}
for _, q := range columnMigrations {
_, _ = s.db.Exec(q)
@@ -2101,14 +2105,39 @@ func (s *Store) GetFolders(userID string) ([]string, error) {
// 2. Add mounted shared folders (so even empty shared folders appear in the tree)
mounts, _, err := s.getUserFolderMounts(userID)
if err == nil {
for _, mPath := range mounts {
for sfID, mPath := range mounts {
if mPath != "" {
addFolderSegments(mPath, folderSet)
// 2b. Add all subfolders created in this shared folder
sRows, err := s.db.Query(`SELECT sub_path FROM workspace_folders WHERE shared_folder_id = ?`, sfID)
if err == nil {
for sRows.Next() {
var sp string
if err := sRows.Scan(&sp); err == nil && sp != "" {
fullP := mPath + "/" + sp
addFolderSegments(fullP, folderSet)
}
}
_ = sRows.Close()
}
}
}
}
// 3. Scan physical directories on disk in docDir/userID
// 3. Add personal empty folders created by this user
pRows, err := s.db.Query(`SELECT path FROM workspace_folders WHERE user_id = ? AND (shared_folder_id = '' OR shared_folder_id IS NULL)`, userID)
if err == nil {
for pRows.Next() {
var p string
if err := pRows.Scan(&p); err == nil && p != "" {
addFolderSegments(p, folderSet)
}
}
_ = pRows.Close()
}
// 4. Scan physical directories on disk in docDir/userID
if s.docDir != "" {
userDir := filepath.Join(s.docDir, userID)
if fi, err := os.Stat(userDir); err == nil && fi.IsDir() {
@@ -2158,6 +2187,31 @@ func (s *Store) CreateFolder(userID, folderPath string) (string, error) {
return "", fmt.Errorf("ungültiger Ordnerpfad")
}
mounts, _, err := s.getUserFolderMounts(userID)
var sfID, subPath string
if err == nil {
sfID, subPath, _ = matchMountPath(clean, mounts)
}
now := time.Now().UTC().Format(time.RFC3339)
id := domain.NewID("wfld")
if sfID != "" {
var existsID string
err := s.db.QueryRow(`SELECT id FROM workspace_folders WHERE shared_folder_id = ? AND sub_path = ?`, sfID, subPath).Scan(&existsID)
if err == sql.ErrNoRows {
_, _ = s.db.Exec(`INSERT INTO workspace_folders (id, user_id, shared_folder_id, path, sub_path, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
id, userID, sfID, clean, subPath, now)
}
} else {
var existsID string
err := s.db.QueryRow(`SELECT id FROM workspace_folders WHERE user_id = ? AND path = ? AND (shared_folder_id = '' OR shared_folder_id IS NULL)`, userID, clean).Scan(&existsID)
if err == sql.ErrNoRows {
_, _ = s.db.Exec(`INSERT INTO workspace_folders (id, user_id, shared_folder_id, path, sub_path, created_at) VALUES (?, ?, '', ?, '', ?)`,
id, userID, clean, now)
}
}
if s.docDir != "" {
targetDir := filepath.Join(s.docDir, userID, clean)
if err := os.MkdirAll(targetDir, 0755); err != nil {
@@ -2201,6 +2255,21 @@ func (s *Store) DeleteFolder(userID, folderPath string) ([]string, error) {
_, _ = s.db.Exec(`DELETE FROM documents WHERE id = ? AND user_id = ?`, id, userID)
}
// Clean up from workspace_folders
mounts, _, err := s.getUserFolderMounts(userID)
if err == nil {
sfID, subPath, isMatch := matchMountPath(clean, mounts)
if isMatch && sfID != "" {
if subPath == "" {
_, _ = s.db.Exec(`DELETE FROM workspace_folders WHERE shared_folder_id = ?`, sfID)
} else {
_, _ = s.db.Exec(`DELETE FROM workspace_folders WHERE shared_folder_id = ? AND (sub_path = ? OR sub_path LIKE ?)`,
sfID, subPath, subPath+"/%")
}
}
}
_, _ = s.db.Exec(`DELETE FROM workspace_folders WHERE user_id = ? AND (path = ? OR path LIKE ?)`, userID, clean, clean+"/%")
// Remove physical folder and subdirectories from disk
if s.docDir != "" {
targetDir := filepath.Join(s.docDir, userID, clean)
@@ -2367,6 +2436,28 @@ func (s *Store) CreateSharedFolder(userID, folderName, initialMountPath string)
}
}
// Migrate any existing workspace_folders created by this user in cleanMount
fRows, err := s.db.Query(`SELECT id, path FROM workspace_folders WHERE user_id = ? AND (path = ? OR path LIKE ?)`, userID, cleanMount, cleanMount+"/%")
if err == nil {
type wfEntry struct{ id, path string }
var wfEntries []wfEntry
for fRows.Next() {
var wfe wfEntry
if err := fRows.Scan(&wfe.id, &wfe.path); err == nil {
wfEntries = append(wfEntries, wfe)
}
}
_ = fRows.Close()
for _, wfe := range wfEntries {
subP := ""
if strings.HasPrefix(wfe.path, cleanMount+"/") {
subP = strings.TrimPrefix(wfe.path, cleanMount+"/")
}
_, _ = s.db.Exec(`UPDATE workspace_folders SET shared_folder_id = ?, sub_path = ? WHERE id = ?`, folderID, subP, wfe.id)
}
}
var ownerName string
_ = s.db.QueryRow(`SELECT display_name FROM users WHERE id = ?`, userID).Scan(&ownerName)
@@ -2646,6 +2737,7 @@ func (s *Store) DeleteSharedFolder(currentUserID, folderID string) error {
_, _ = s.db.Exec(`UPDATE documents SET shared_folder_id = '', sub_folder_path = '' WHERE shared_folder_id = ?`, folderID)
_, _ = s.db.Exec(`DELETE FROM workspace_folder_mounts WHERE folder_id = ?`, folderID)
_, _ = s.db.Exec(`DELETE FROM workspace_folder_shares WHERE folder_id = ?`, folderID)
_, _ = s.db.Exec(`DELETE FROM workspace_folders WHERE shared_folder_id = ?`, folderID)
_, err = s.db.Exec(`DELETE FROM workspace_shared_folders WHERE id = ?`, folderID)
return err
}