package main import ( "fmt" "path/filepath" "strings" "verstak/internal/core/activity" "verstak/internal/core/files" "verstak/internal/core/nodes" syncsvc "verstak/internal/core/sync" ) func (a *App) ListFiles(nodeID string) ([]FileDTO, error) { if err := a.requireVault(); err != nil { return nil, err } records, err := a.files.ListByNode(nodeID) if err != nil { return nil, err } result := make([]FileDTO, len(records)) for i := range records { rec := &records[i] result[i] = FileDTO{ ID: rec.ID, NodeID: rec.NodeID, Name: rec.Filename, Path: rec.Path, Size: rec.Size, Mime: rec.MIME, IsDir: rec.MIME == "inode/directory", Missing: rec.Missing, } } return result, nil } func (a *App) ListItems(nodeID string) ([]FileTreeItemDTO, error) { if err := a.requireVault(); err != nil { return nil, err } children, err := a.nodes.ListChildren(nodeID, false) if err != nil { return nil, err } result := make([]FileTreeItemDTO, 0, len(children)) for i := range children { typ := children[i].Type if typ != nodes.TypeFolder && typ != nodes.TypeFile && typ != nodes.TypeNote { continue } item := FileTreeItemDTO{ ID: children[i].ID, Name: children[i].Title, Type: typ, } if typ == nodes.TypeFolder { kids, _ := a.nodes.ListChildren(children[i].ID, false) item.HasKids = len(kids) > 0 item.Mime = "inode/directory" } else if typ == nodes.TypeFile { records, _ := a.files.ListByNode(children[i].ID) if len(records) > 0 { item.FileID = records[0].ID item.Size = records[0].Size item.Mime = records[0].MIME } } else if typ == nodes.TypeNote { records, _ := a.files.ListByNode(children[i].ID) if len(records) > 0 { item.FileID = records[0].ID item.Size = records[0].Size item.Mime = "text/markdown" } } result = append(result, item) } return result, nil } func (a *App) AddPathCopy(nodeID, sourcePath string) ([]NodeDTO, error) { if err := a.requireVault(); err != nil { return nil, err } nodes, err := a.files.AddPathCopy(nodeID, sourcePath) if err != nil { return nil, err } for _, n := range nodes { _ = a.activity.Record(nodeID, activity.TargetFile, n.ID, "", activity.TypeFileAdded, n.Title, `{"source":"`+sourcePath+`"}`) _ = a.sync.RecordOp(syncsvc.EntityFile, n.ID, syncsvc.OpCreate, a.filePayload(&n)) } return toNodeDTOs(nodes), nil } func (a *App) AddPathLink(nodeID, sourcePath string) ([]NodeDTO, error) { if err := a.requireVault(); err != nil { return nil, err } nodes, err := a.files.AddPathLink(nodeID, sourcePath) if err != nil { return nil, err } for _, n := range nodes { _ = a.activity.Record(nodeID, activity.TargetFile, n.ID, "", activity.TypeFileAdded, n.Title, `{"source":"`+sourcePath+`"}`) _ = a.sync.RecordOp(syncsvc.EntityFile, n.ID, syncsvc.OpCreate, a.filePayload(&n)) } return toNodeDTOs(nodes), nil } func (a *App) DeleteFileOrFolder(nodeID string) error { if err := a.requireVault(); err != nil { return err } n, err := a.nodes.GetActive(nodeID) if err == nil { pid := "" if n.ParentID != nil { pid = *n.ParentID } evType := activity.TypeFileDeleted targetType := activity.TargetFile if n.Type == nodes.TypeFolder { evType = activity.TypeFolderDeleted targetType = activity.TargetFolder } _ = a.activity.Record(pid, targetType, nodeID, "", evType, n.Title, "") syncEntity := syncsvc.EntityFile if n.Type == nodes.TypeFolder { syncEntity = syncsvc.EntityFolder } _ = a.sync.RecordOp(syncEntity, nodeID, syncsvc.OpDelete, nil) } return a.files.DeleteNodeAndChildren(nodeID) } func (a *App) CreateEmptyFile(parentID, filename string) (*NodeDTO, error) { if err := a.requireVault(); err != nil { return nil, err } node, err := a.files.CreateEmptyFile(parentID, filename) if err != nil { return nil, err } _ = a.activity.Record(parentID, activity.TargetFile, node.ID, "", activity.TypeFileAdded, filename, "") _ = a.sync.RecordOp(syncsvc.EntityFile, node.ID, syncsvc.OpCreate, a.filePayload(node)) dto := toNodeDTO(node) return &dto, nil } func (a *App) DuplicateNode(nodeID string) (*NodeDTO, error) { if err := a.requireVault(); err != nil { return nil, err } node, err := a.files.Duplicate(nodeID) if err != nil { return nil, err } n, err2 := a.nodes.GetActive(nodeID) pid := "" if err2 == nil && n.ParentID != nil { pid = *n.ParentID } _ = a.activity.Record(pid, activity.TargetFile, node.ID, "", activity.TypeFileCopied, node.Title, "") _ = a.sync.RecordOp(syncsvc.EntityFile, node.ID, syncsvc.OpCreate, a.filePayload(node)) dto := toNodeDTO(node) return &dto, nil } func (a *App) ValidateName(name string) error { return files.ValidateName(name) } func (a *App) CheckFileAction(fileID string) (*PreflightFileAction, error) { if err := a.requireVault(); err != nil { return nil, err } fileRec, err := a.files.Get(fileID) if err != nil { return nil, fmt.Errorf("get file: %w", err) } name := strings.ToLower(fileRec.Filename) isMD := strings.HasSuffix(name, ".md") || strings.HasSuffix(name, ".markdown") if !isMD { return &PreflightFileAction{Action: "external", FileName: fileRec.Filename}, nil } // .md file — check for linked note noteRec, err := a.notes.FindByFileID(fileID) if err == nil && noteRec != nil { noteNode, nodeErr := a.nodes.Get(noteRec.NodeID) title := fileRec.Filename if nodeErr == nil && noteNode != nil { title = noteNode.Title } return &PreflightFileAction{Action: "note", NoteID: noteRec.NodeID, NoteTitle: title, FileName: fileRec.Filename}, nil } // .md inside Notes/ with no note record — auto-link pathLower := strings.ToLower(fileRec.Path) insideNotes := strings.Contains(pathLower, string(filepath.Separator)+"notes"+string(filepath.Separator)) || strings.HasPrefix(pathLower, "notes"+string(filepath.Separator)) if insideNotes { noteNode, nodeErr := a.nodes.Get(fileRec.NodeID) if nodeErr == nil && noteNode != nil { _ = a.notes.LinkFile(noteNode.ID, fileID, "markdown") return &PreflightFileAction{Action: "note", NoteID: noteNode.ID, NoteTitle: noteNode.Title, FileName: fileRec.Filename}, nil } } // .md outside Notes/ — internal preview return &PreflightFileAction{Action: "preview", FileName: fileRec.Filename}, nil } func (a *App) PreviewImport(sourcePath string) (*files.ImportSummary, error) { if err := a.requireVault(); err != nil { return nil, err } return a.files.PreviewImport(sourcePath) }