refactor: split sqlite.go and server.go by domain (FLAW-20)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m31s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m31s
Pure moves, no behaviour change: the storage layer now has users, apikeys, thoughts, tasklists, documents, folders, tags, lists, teams and system files next to sqlite.go (store, schema, migrations); the API has handlers_*.go per area plus realtime.go and alarms.go next to server.go (server, router, middleware). Declarations were moved verbatim with an AST-based tool; imports pruned with goimports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7bba174454
commit
74c394efaa
26 files changed
+7702
-7467
No files matched your search
@@ -0,0 +1,862 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sandrodach/mindsift/internal/domain"
|
||||
)
|
||||
|
||||
func (s *Store) GetFolders(userID string) ([]string, error) {
|
||||
folderSet := make(map[string]bool)
|
||||
|
||||
// 1. Collect from resolved documents for this user
|
||||
docs, err := s.GetDocuments(userID)
|
||||
if err == nil {
|
||||
for _, doc := range docs {
|
||||
if doc.FolderPath != "" {
|
||||
addFolderSegments(doc.FolderPath, folderSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Add mounted shared folders (so even empty shared folders appear in the tree)
|
||||
mounts, _, err := s.getUserFolderMounts(userID)
|
||||
if err == nil {
|
||||
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. 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()
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(folderSet))
|
||||
for f := range folderSet {
|
||||
result = append(result, f)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func addFolderSegments(fp string, set map[string]bool) {
|
||||
clean := filepath.ToSlash(filepath.Clean(strings.TrimSpace(fp)))
|
||||
if clean == "." || clean == "" {
|
||||
return
|
||||
}
|
||||
parts := strings.Split(clean, "/")
|
||||
var cur string
|
||||
for _, p := range parts {
|
||||
if p != "" && p != "." {
|
||||
if cur == "" {
|
||||
cur = p
|
||||
} else {
|
||||
cur = cur + "/" + p
|
||||
}
|
||||
set[cur] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateFolder(userID, folderPath string) (string, error) {
|
||||
clean := filepath.ToSlash(filepath.Clean(strings.TrimSpace(folderPath)))
|
||||
if clean == "" || clean == "." || strings.HasPrefix(clean, "/") || strings.Contains(clean, "..") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return clean, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteFolder(userID, folderPath string) ([]string, error) {
|
||||
clean := filepath.ToSlash(filepath.Clean(strings.TrimSpace(folderPath)))
|
||||
if clean == "" || clean == "." || strings.HasPrefix(clean, "/") || strings.Contains(clean, "..") {
|
||||
return nil, fmt.Errorf("ungültiger Ordnerpfad")
|
||||
}
|
||||
|
||||
var deletedDocIDs []string
|
||||
seenDocID := make(map[string]bool)
|
||||
|
||||
mounts, _, err := s.getUserFolderMounts(userID)
|
||||
if err == nil {
|
||||
sfID, subPath, isMatch := matchMountPath(clean, mounts)
|
||||
if isMatch && sfID != "" {
|
||||
if subPath == "" {
|
||||
// Root of a shared folder
|
||||
shFolder, sfErr := s.GetSharedFolderByID(userID, sfID)
|
||||
if sfErr == nil && !shFolder.IsOwner {
|
||||
// Collaborator removing mount/share
|
||||
_ = s.DeleteSharedFolder(userID, sfID)
|
||||
return []string{}, nil
|
||||
}
|
||||
rows, qErr := s.db.Query(`SELECT id FROM documents WHERE shared_folder_id = ?`, sfID)
|
||||
if qErr == nil {
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err == nil && !seenDocID[id] {
|
||||
seenDocID[id] = true
|
||||
deletedDocIDs = append(deletedDocIDs, id)
|
||||
}
|
||||
}
|
||||
_ = rows.Close()
|
||||
}
|
||||
_, _ = s.db.Exec(`DELETE FROM workspace_folders WHERE shared_folder_id = ?`, sfID)
|
||||
} else {
|
||||
// Subfolder inside a shared folder
|
||||
rows, qErr := s.db.Query(
|
||||
`SELECT id FROM documents WHERE shared_folder_id = ? AND (sub_folder_path = ? OR sub_folder_path LIKE ?)`,
|
||||
sfID, subPath, subPath+"/%",
|
||||
)
|
||||
if qErr == nil {
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err == nil && !seenDocID[id] {
|
||||
seenDocID[id] = true
|
||||
deletedDocIDs = append(deletedDocIDs, id)
|
||||
}
|
||||
}
|
||||
_ = rows.Close()
|
||||
}
|
||||
_, _ = s.db.Exec(`DELETE FROM workspace_folders WHERE shared_folder_id = ? AND (sub_path = ? OR sub_path LIKE ?)`,
|
||||
sfID, subPath, subPath+"/%")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all personal documents owned by this user in this folder and subfolders
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id FROM documents
|
||||
WHERE user_id = ? AND (shared_folder_id = '' OR shared_folder_id IS NULL) AND (folder_path = ? OR folder_path LIKE ?)`,
|
||||
userID, clean, clean+"/%",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err == nil && !seenDocID[id] {
|
||||
seenDocID[id] = true
|
||||
deletedDocIDs = append(deletedDocIDs, id)
|
||||
}
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
// The notes of the folder go to the trash; restoring one creates its folder again
|
||||
for _, id := range deletedDocIDs {
|
||||
_ = s.trashDocument(userID, id)
|
||||
}
|
||||
|
||||
// Also clear any personal folder placements for shared notes in this folder
|
||||
_, _ = s.db.Exec(`UPDATE doc_user_folders SET folder_path = '' WHERE user_id = ? AND (folder_path = ? OR folder_path LIKE ?)`, userID, clean, clean+"/%")
|
||||
_, _ = s.db.Exec(`DELETE FROM workspace_folders WHERE user_id = ? AND (path = ? OR path LIKE ?)`, userID, clean, clean+"/%")
|
||||
|
||||
return deletedDocIDs, nil
|
||||
}
|
||||
|
||||
func (s *Store) MoveDocument(userID, docID, targetFolderPath string) (*domain.Document, error) {
|
||||
doc, err := s.GetDocumentByID(userID, docID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if doc.IsArchived {
|
||||
return nil, ErrDocInTrash
|
||||
}
|
||||
|
||||
cleanTarget := filepath.ToSlash(filepath.Clean(strings.TrimSpace(targetFolderPath)))
|
||||
if cleanTarget == "." {
|
||||
cleanTarget = ""
|
||||
}
|
||||
|
||||
mounts, _, err := s.getUserFolderMounts(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := domain.NowISO()
|
||||
|
||||
// Check if target folder falls within any mounted shared folder
|
||||
targetFolderID, subPath, isTargetShared := matchMountPath(cleanTarget, mounts)
|
||||
|
||||
if isTargetShared {
|
||||
// Moving into / within a shared folder
|
||||
shFolder, err := s.GetSharedFolderByID(userID, targetFolderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !shFolder.IsOwner {
|
||||
return nil, fmt.Errorf("nur der Eigentümer dieses geteilten Ordners kann Dateien dorthin verschieben")
|
||||
}
|
||||
|
||||
// Update document to belong to targetSharedFolderID
|
||||
_, err = s.db.Exec(`UPDATE documents SET shared_folder_id = ?, sub_folder_path = ?, updated_at = ? WHERE id = ?`,
|
||||
targetFolderID, subPath, now, docID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remove personal override if any
|
||||
_, _ = s.db.Exec(`DELETE FROM doc_user_folders WHERE user_id = ? AND doc_id = ?`, userID, docID)
|
||||
|
||||
doc.SharedFolderID = targetFolderID
|
||||
doc.SubFolderPath = subPath
|
||||
doc.IsSharedFolder = true
|
||||
doc.FolderPath = cleanTarget
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// Target is NOT inside a shared folder (personal / standalone)
|
||||
if doc.SharedFolderID != "" {
|
||||
// Moving OUT of a shared folder into a personal folder
|
||||
if !doc.IsOwner {
|
||||
return nil, fmt.Errorf("keine Berechtigung, die Notiz aus dem geteilten Ordner zu verschieben")
|
||||
}
|
||||
_, err = s.db.Exec(`UPDATE documents SET shared_folder_id = '', sub_folder_path = '', folder_path = ?, updated_at = ? WHERE id = ?`,
|
||||
cleanTarget, now, docID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc.SharedFolderID = ""
|
||||
doc.SubFolderPath = ""
|
||||
doc.IsSharedFolder = false
|
||||
doc.FolderPath = cleanTarget
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// Standalone note
|
||||
if doc.IsOwner {
|
||||
// Owner moves their own document -> updates documents.folder_path
|
||||
_, err = s.db.Exec(`UPDATE documents SET folder_path = ?, updated_at = ? WHERE id = ?`, cleanTarget, now, docID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc.FolderPath = cleanTarget
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// Recipient moves an individually shared note -> updates doc_user_folders ONLY!
|
||||
_, err = s.db.Exec(`
|
||||
INSERT INTO doc_user_folders (user_id, doc_id, folder_path, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, doc_id) DO UPDATE SET folder_path = excluded.folder_path, updated_at = excluded.updated_at
|
||||
`, userID, docID, cleanTarget, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
doc.FolderPath = cleanTarget
|
||||
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 == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Link all documents owned by ownerID in cleanMount to this shared folder
|
||||
rows, err := s.db.Query(`SELECT id, folder_path FROM documents WHERE user_id = ? AND is_archived = 0 AND (folder_path = ? OR folder_path LIKE ?)`,
|
||||
ownerID, cleanMount, cleanMount+"/%")
|
||||
if err == nil {
|
||||
type docEntry struct{ id, fp string }
|
||||
var entries []docEntry
|
||||
for rows.Next() {
|
||||
var de docEntry
|
||||
if err := rows.Scan(&de.id, &de.fp); err == nil {
|
||||
entries = append(entries, de)
|
||||
}
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
for _, de := range entries {
|
||||
var subPath string
|
||||
if de.fp == cleanMount {
|
||||
subPath = ""
|
||||
} else if strings.HasPrefix(de.fp, cleanMount+"/") {
|
||||
subPath = strings.TrimPrefix(de.fp, cleanMount+"/")
|
||||
}
|
||||
_, _ = s.db.Exec(`UPDATE documents SET shared_folder_id = ?, sub_folder_path = ? WHERE id = ?`,
|
||||
folderID, subPath, de.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Link any workspace_folders created by ownerID in cleanMount
|
||||
fRows, err := s.db.Query(`SELECT id, path FROM workspace_folders WHERE user_id = ? AND (path = ? OR path LIKE ?)`,
|
||||
ownerID, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateSharedFolder(userID, folderName, initialMountPath string) (*domain.SharedFolder, error) {
|
||||
cleanName := strings.TrimSpace(folderName)
|
||||
if cleanName == "" {
|
||||
return nil, fmt.Errorf("ordnername darf nicht leer sein")
|
||||
}
|
||||
cleanMount := filepath.ToSlash(filepath.Clean(strings.TrimSpace(initialMountPath)))
|
||||
if cleanMount == "." || cleanMount == "" {
|
||||
cleanMount = cleanName
|
||||
}
|
||||
|
||||
// Check if already shared by this user at this mount path
|
||||
var existingID string
|
||||
err := s.db.QueryRow(`
|
||||
SELECT f.id FROM workspace_shared_folders f
|
||||
JOIN workspace_folder_mounts m ON m.folder_id = f.id AND m.user_id = ?
|
||||
WHERE f.owner_id = ? AND (m.mount_path = ? OR f.name = ?)
|
||||
`, userID, userID, cleanMount, cleanName).Scan(&existingID)
|
||||
if err == nil && existingID != "" {
|
||||
s.syncSharedFolderContents(userID, existingID, cleanMount)
|
||||
return s.GetSharedFolderByID(userID, existingID)
|
||||
}
|
||||
|
||||
now := domain.NowISO()
|
||||
folderID := domain.NewID("fld")
|
||||
|
||||
_, err = s.db.Exec(`
|
||||
INSERT INTO workspace_shared_folders (id, owner_id, name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, folderID, userID, cleanName, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = s.db.Exec(`
|
||||
INSERT INTO workspace_folder_mounts (user_id, folder_id, mount_path, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, userID, folderID, cleanMount, now, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.syncSharedFolderContents(userID, folderID, cleanMount)
|
||||
|
||||
var ownerName string
|
||||
_ = s.db.QueryRow(`SELECT display_name FROM users WHERE id = ?`, userID).Scan(&ownerName)
|
||||
|
||||
return &domain.SharedFolder{
|
||||
ID: folderID,
|
||||
OwnerID: userID,
|
||||
OwnerName: ownerName,
|
||||
Name: cleanName,
|
||||
MountPath: cleanMount,
|
||||
IsOwner: true,
|
||||
Permission: "owner",
|
||||
Shares: []domain.FolderShare{},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSharedFolders(userID string) ([]domain.SharedFolder, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT f.id, f.owner_id, u.display_name, f.name,
|
||||
COALESCE(m.mount_path, f.name) as mount_path,
|
||||
CASE
|
||||
WHEN f.owner_id = ? THEN 1
|
||||
ELSE 0
|
||||
END as is_owner,
|
||||
CASE
|
||||
WHEN f.owner_id = ? THEN 'owner'
|
||||
WHEN MAX(CASE WHEN s.permission = 'write' THEN 1 ELSE 0 END) = 1 THEN 'write'
|
||||
ELSE 'read'
|
||||
END as permission,
|
||||
f.created_at, f.updated_at
|
||||
FROM workspace_shared_folders f
|
||||
JOIN users u ON f.owner_id = u.id
|
||||
LEFT JOIN workspace_folder_mounts m ON m.folder_id = f.id AND m.user_id = ?
|
||||
LEFT JOIN workspace_folder_shares s ON s.folder_id = f.id AND (
|
||||
(s.target_type = 'user' AND s.target_id = ?) OR
|
||||
(s.target_type = 'team' AND s.target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||
)
|
||||
WHERE f.owner_id = ? OR s.id IS NOT NULL
|
||||
GROUP BY f.id
|
||||
ORDER BY f.created_at ASC
|
||||
`, userID, userID, userID, userID, userID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
folders := make([]domain.SharedFolder, 0)
|
||||
for rows.Next() {
|
||||
var f domain.SharedFolder
|
||||
var isOwner int
|
||||
if err := rows.Scan(&f.ID, &f.OwnerID, &f.OwnerName, &f.Name, &f.MountPath, &isOwner, &f.Permission, &f.CreatedAt, &f.UpdatedAt); err == nil {
|
||||
f.IsOwner = isOwner != 0
|
||||
if f.IsOwner {
|
||||
if shares, err := s.GetFolderShares(f.ID); err == nil {
|
||||
f.Shares = shares
|
||||
}
|
||||
}
|
||||
folders = append(folders, f)
|
||||
}
|
||||
}
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSharedFolderByID(userID, folderID string) (*domain.SharedFolder, error) {
|
||||
row := s.db.QueryRow(`
|
||||
SELECT f.id, f.owner_id, u.display_name, f.name,
|
||||
COALESCE(m.mount_path, f.name) as mount_path,
|
||||
CASE
|
||||
WHEN f.owner_id = ? THEN 1
|
||||
ELSE 0
|
||||
END as is_owner,
|
||||
CASE
|
||||
WHEN f.owner_id = ? THEN 'owner'
|
||||
WHEN MAX(CASE WHEN s.permission = 'write' THEN 1 ELSE 0 END) = 1 THEN 'write'
|
||||
ELSE 'read'
|
||||
END as permission,
|
||||
f.created_at, f.updated_at
|
||||
FROM workspace_shared_folders f
|
||||
JOIN users u ON f.owner_id = u.id
|
||||
LEFT JOIN workspace_folder_mounts m ON m.folder_id = f.id AND m.user_id = ?
|
||||
LEFT JOIN workspace_folder_shares s ON s.folder_id = f.id AND (
|
||||
(s.target_type = 'user' AND s.target_id = ?) OR
|
||||
(s.target_type = 'team' AND s.target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
|
||||
)
|
||||
WHERE f.id = ? AND (f.owner_id = ? OR s.id IS NOT NULL)
|
||||
GROUP BY f.id
|
||||
`, userID, userID, userID, userID, userID, folderID, userID)
|
||||
|
||||
var f domain.SharedFolder
|
||||
var isOwner int
|
||||
if err := row.Scan(&f.ID, &f.OwnerID, &f.OwnerName, &f.Name, &f.MountPath, &isOwner, &f.Permission, &f.CreatedAt, &f.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("geteilter Ordner nicht gefunden")
|
||||
}
|
||||
f.IsOwner = isOwner != 0
|
||||
if f.IsOwner {
|
||||
if shares, err := s.GetFolderShares(f.ID); err == nil {
|
||||
f.Shares = shares
|
||||
}
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateFolderMount(userID, folderID, mountPath string) error {
|
||||
cleanMount := filepath.ToSlash(filepath.Clean(strings.TrimSpace(mountPath)))
|
||||
if cleanMount == "." || cleanMount == "" {
|
||||
return fmt.Errorf("ungültiger Mount-Pfad")
|
||||
}
|
||||
now := domain.NowISO()
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO workspace_folder_mounts (user_id, folder_id, mount_path, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, folder_id) DO UPDATE SET mount_path = excluded.mount_path, updated_at = excluded.updated_at
|
||||
`, userID, folderID, cleanMount, now, now)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetFolderShares(folderID string) ([]domain.FolderShare, error) {
|
||||
shares := make([]domain.FolderShare, 0)
|
||||
rows, err := s.db.Query(`
|
||||
SELECT s.id, s.folder_id, s.target_type, s.target_id,
|
||||
CASE
|
||||
WHEN s.target_type = 'user' THEN u.display_name
|
||||
WHEN s.target_type = 'team' THEN t.name
|
||||
ELSE ''
|
||||
END as target_name,
|
||||
COALESCE(u.email, '') as email,
|
||||
COALESCE(u.avatar_url, '') as avatar_url,
|
||||
s.permission, s.created_by, s.created_at
|
||||
FROM workspace_folder_shares s
|
||||
LEFT JOIN users u ON s.target_type = 'user' AND s.target_id = u.id
|
||||
LEFT JOIN teams t ON s.target_type = 'team' AND s.target_id = t.id
|
||||
WHERE s.folder_id = ?
|
||||
ORDER BY s.created_at ASC
|
||||
`, folderID)
|
||||
if err != nil {
|
||||
return shares, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var sh domain.FolderShare
|
||||
if err := rows.Scan(&sh.ID, &sh.FolderID, &sh.TargetType, &sh.TargetID, &sh.TargetName, &sh.Email, &sh.AvatarURL, &sh.Permission, &sh.CreatedBy, &sh.CreatedAt); err == nil {
|
||||
shares = append(shares, sh)
|
||||
}
|
||||
}
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateFolderShare(currentUserID, folderID, targetType, targetIdentifier, permission string) (*domain.FolderShare, error) {
|
||||
f, err := s.GetSharedFolderByID(currentUserID, folderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !f.IsOwner {
|
||||
return nil, fmt.Errorf("nur der Eigentümer kann Ordner-Freigaben verwalten")
|
||||
}
|
||||
|
||||
cleanType := strings.TrimSpace(targetType)
|
||||
cleanTarget := strings.TrimSpace(targetIdentifier)
|
||||
if cleanType == "" {
|
||||
if strings.HasPrefix(cleanTarget, "team_") {
|
||||
cleanType = "team"
|
||||
} else {
|
||||
cleanType = "user"
|
||||
}
|
||||
}
|
||||
|
||||
permission = normalizeSharePermission(permission)
|
||||
|
||||
var targetID, targetName, email, avatarURL string
|
||||
if cleanType == "team" {
|
||||
err = s.db.QueryRow(`SELECT id, name FROM teams WHERE id = ?`, cleanTarget).Scan(&targetID, &targetName)
|
||||
if err != nil {
|
||||
return nil, ErrTargetNotFound
|
||||
}
|
||||
} else {
|
||||
var u *domain.User
|
||||
if strings.HasPrefix(cleanTarget, "usr_") {
|
||||
u, _ = s.GetUserByID(cleanTarget)
|
||||
}
|
||||
if u == nil {
|
||||
u, _ = s.GetUserByLogin(cleanTarget)
|
||||
}
|
||||
if u == nil {
|
||||
u, err = s.GetUserByID(cleanTarget)
|
||||
}
|
||||
if err != nil || u == nil {
|
||||
if s.db.QueryRow(`SELECT id, name FROM teams WHERE id = ?`, cleanTarget).Scan(&targetID, &targetName) == nil {
|
||||
cleanType = "team"
|
||||
} else {
|
||||
return nil, ErrTargetNotFound
|
||||
}
|
||||
} else {
|
||||
if u.ID == f.OwnerID {
|
||||
return nil, fmt.Errorf("ordner gehört bereits diesem Benutzer")
|
||||
}
|
||||
targetID = u.ID
|
||||
targetName = u.DisplayName
|
||||
email = u.Email
|
||||
avatarURL = u.AvatarURL
|
||||
}
|
||||
}
|
||||
|
||||
if !s.CanShareWith(currentUserID, cleanType, targetID) {
|
||||
return nil, ErrTargetNotFound
|
||||
}
|
||||
|
||||
now := domain.NowISO()
|
||||
shareID := domain.NewID("fsh")
|
||||
|
||||
_, err = s.db.Exec(`
|
||||
INSERT INTO workspace_folder_shares (id, folder_id, target_type, target_id, permission, created_by, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(folder_id, target_type, target_id) DO UPDATE SET permission = excluded.permission
|
||||
`, shareID, folderID, cleanType, targetID, permission, currentUserID, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ownerMount string
|
||||
_ = s.db.QueryRow(`SELECT COALESCE(m.mount_path, f.name) FROM workspace_shared_folders f LEFT JOIN workspace_folder_mounts m ON m.folder_id = f.id AND m.user_id = f.owner_id WHERE f.id = ?`, folderID).Scan(&ownerMount)
|
||||
if ownerMount == "" {
|
||||
ownerMount = f.Name
|
||||
}
|
||||
s.syncSharedFolderContents(f.OwnerID, folderID, ownerMount)
|
||||
|
||||
if cleanType == "user" {
|
||||
_, _ = s.db.Exec(`
|
||||
INSERT OR IGNORE INTO workspace_folder_mounts (user_id, folder_id, mount_path, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, targetID, folderID, f.Name, now, now)
|
||||
} else if cleanType == "team" {
|
||||
mRows, err := s.db.Query(`SELECT user_id FROM team_members WHERE team_id = ?`, targetID)
|
||||
if err == nil {
|
||||
var memberIDs []string
|
||||
for mRows.Next() {
|
||||
var uid string
|
||||
if err := mRows.Scan(&uid); err == nil && uid != "" {
|
||||
memberIDs = append(memberIDs, uid)
|
||||
}
|
||||
}
|
||||
_ = mRows.Close()
|
||||
for _, uid := range memberIDs {
|
||||
_, _ = s.db.Exec(`
|
||||
INSERT OR IGNORE INTO workspace_folder_mounts (user_id, folder_id, mount_path, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, uid, folderID, f.Name, now, now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &domain.FolderShare{
|
||||
ID: shareID,
|
||||
FolderID: folderID,
|
||||
TargetType: cleanType,
|
||||
TargetID: targetID,
|
||||
TargetName: targetName,
|
||||
Email: email,
|
||||
AvatarURL: avatarURL,
|
||||
Permission: permission,
|
||||
CreatedBy: currentUserID,
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteFolderShare(currentUserID, folderID, shareID string) error {
|
||||
f, err := s.GetSharedFolderByID(currentUserID, folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cleanID := strings.TrimSpace(shareID)
|
||||
|
||||
// Only the owner manages shares; everyone else can only leave the folder
|
||||
if !f.IsOwner {
|
||||
_, err = s.db.Exec(`DELETE FROM workspace_folder_shares WHERE folder_id = ? AND target_type = 'user' AND target_id = ? AND (id = ? OR target_id = ?)`,
|
||||
folderID, currentUserID, cleanID, cleanID)
|
||||
_, _ = s.db.Exec(`DELETE FROM workspace_folder_mounts WHERE folder_id = ? AND user_id = ?`, folderID, currentUserID)
|
||||
return err
|
||||
}
|
||||
|
||||
var targetType, targetID string
|
||||
_ = s.db.QueryRow(`SELECT target_type, target_id FROM workspace_folder_shares WHERE folder_id = ? AND (id = ? OR target_id = ?)`,
|
||||
folderID, cleanID, cleanID).Scan(&targetType, &targetID)
|
||||
|
||||
_, err = s.db.Exec(`DELETE FROM workspace_folder_shares WHERE folder_id = ? AND (id = ? OR target_id = ?)`, folderID, cleanID, cleanID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if targetType == "user" && targetID != "" {
|
||||
_, _ = s.db.Exec(`DELETE FROM workspace_folder_mounts WHERE folder_id = ? AND user_id = ?`, folderID, targetID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSharedFolder(currentUserID, folderID string) error {
|
||||
f, err := s.GetSharedFolderByID(currentUserID, folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !f.IsOwner {
|
||||
return s.DeleteFolderShare(currentUserID, folderID, currentUserID)
|
||||
}
|
||||
|
||||
_, _ = 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
|
||||
}
|
||||
|
||||
// GetSharedFolderDocIDs returns all non-archived document IDs belonging to a shared folder.
|
||||
func (s *Store) GetSharedFolderDocIDs(folderID string) ([]string, error) {
|
||||
rows, err := s.db.Query(`SELECT id FROM documents WHERE shared_folder_id = ? AND is_archived = 0`, folderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var docIDs []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err == nil && id != "" {
|
||||
docIDs = append(docIDs, id)
|
||||
}
|
||||
}
|
||||
return docIDs, nil
|
||||
}
|
||||
Reference in new issue
Block a user