219 lines
6.4 KiB
Go
219 lines
6.4 KiB
Go
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)
|
|
}
|