fix: complete vault layout transition — fs_path everywhere, no more spaces/
- notes.Create(): .md files stored in parent node's fs_path folder - files.CopyIntoVault/CreateEmptyFile/Duplicate: use parent fs_path - files.AddPathCopy/AddPathLink: use parent fs_path, set folder fs_path - files.DeleteNodeAndChildren: move physical folder to .verstak/trash - UpdateFsPathRecursive: use SafeDisplayNameToPathSegment(child.Title) - sync_apply.go note ops: use fs_path instead of spaces/ - internal/gui/server.go file upload: use n.FsPath instead of nodeSlug - VaultCheck diagnostic: walk nodes/files, verify paths on disk - Tests: create/rename/move/delete/name-conflict/vault-check all pass
This commit is contained in:
parent
0b26f7e5b3
commit
4f01f2de2e
|
|
@ -66,6 +66,8 @@ func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeD
|
||||||
|
|
||||||
physPath := filepath.Join(a.vault, fsPath)
|
physPath := filepath.Join(a.vault, fsPath)
|
||||||
physPath = templates.UniquePath(physPath)
|
physPath = templates.UniquePath(physPath)
|
||||||
|
rel, _ := filepath.Rel(a.vault, physPath)
|
||||||
|
fsPath = rel
|
||||||
|
|
||||||
var pID *string
|
var pID *string
|
||||||
if parentID != "" {
|
if parentID != "" {
|
||||||
|
|
@ -139,6 +141,17 @@ func (a *App) DeleteNode(id string) error {
|
||||||
}
|
}
|
||||||
_ = a.activity.Record(pid, targetType, id, "", evType, n.Title, "")
|
_ = a.activity.Record(pid, targetType, id, "", evType, n.Title, "")
|
||||||
_ = a.sync.RecordOp(entity, id, syncsvc.OpDelete, nil)
|
_ = a.sync.RecordOp(entity, id, syncsvc.OpDelete, nil)
|
||||||
|
|
||||||
|
// Move physical folder to trash
|
||||||
|
if n.FsPath != "" {
|
||||||
|
src := filepath.Join(a.vault, n.FsPath)
|
||||||
|
if info, err := os.Stat(src); err == nil && info.IsDir() {
|
||||||
|
trashDir := filepath.Join(a.vault, ".verstak", "trash")
|
||||||
|
_ = os.MkdirAll(trashDir, 0o750)
|
||||||
|
_ = os.Rename(src, filepath.Join(trashDir, n.ID+"_"+templates.SafeDisplayNameToPathSegment(n.Title)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return a.nodes.SoftDelete(id)
|
return a.nodes.SoftDelete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,16 @@ func (a *App) applyRemoteNoteCreate(op syncsvc.Op) error {
|
||||||
}
|
}
|
||||||
filename = cleanFilename
|
filename = cleanFilename
|
||||||
}
|
}
|
||||||
dest = filepath.Join(a.vault, "spaces", filename)
|
parentFsPath := ""
|
||||||
|
if noteNode, err := a.nodes.Get(payload.NodeID); err == nil && noteNode.ParentID != nil {
|
||||||
|
if parent, err := a.nodes.GetActive(*noteNode.ParentID); err == nil {
|
||||||
|
parentFsPath = parent.FsPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if parentFsPath == "" {
|
||||||
|
parentFsPath = "spaces"
|
||||||
|
}
|
||||||
|
dest = filepath.Join(a.vault, parentFsPath, filename)
|
||||||
payload.Path, _ = filepath.Rel(a.vault, dest)
|
payload.Path, _ = filepath.Rel(a.vault, dest)
|
||||||
} else {
|
} else {
|
||||||
cleanPath, err := syncsvc.SafeVaultPath(a.vault, payload.Path)
|
cleanPath, err := syncsvc.SafeVaultPath(a.vault, payload.Path)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VaultCheckResult contains the diagnostic report for vault integrity.
|
||||||
|
type VaultCheckResult struct {
|
||||||
|
TotalNodes int `json:"total_nodes"`
|
||||||
|
TotalFiles int `json:"total_files"`
|
||||||
|
NodesWithFsPath int `json:"nodes_with_fs_path"`
|
||||||
|
FilesOnDisk int `json:"files_on_disk"`
|
||||||
|
FilesMissing int `json:"files_missing"`
|
||||||
|
PathEscapeCount int `json:"path_escape_count"`
|
||||||
|
PathMismatchCount int `json:"path_mismatch_count"`
|
||||||
|
Errors []string `json:"errors,omitempty"`
|
||||||
|
Details []string `json:"details,omitempty"`
|
||||||
|
Healthy bool `json:"healthy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) VaultCheck() (*VaultCheckResult, error) {
|
||||||
|
result := &VaultCheckResult{Healthy: true}
|
||||||
|
|
||||||
|
// Check all root nodes
|
||||||
|
roots, err := a.nodes.ListRoots(true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list roots: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var checkNode func(id string)
|
||||||
|
checkNode = func(id string) {
|
||||||
|
n, err := a.nodes.GetActive(id)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("node %s: get: %v", id, err))
|
||||||
|
result.Healthy = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.TotalNodes++
|
||||||
|
|
||||||
|
// Check fs_path
|
||||||
|
if n.FsPath != "" {
|
||||||
|
result.NodesWithFsPath++
|
||||||
|
physPath := filepath.Join(a.vault, n.FsPath)
|
||||||
|
rel, err := filepath.Rel(a.vault, physPath)
|
||||||
|
if err != nil || strings.HasPrefix(rel, "..") {
|
||||||
|
result.PathEscapeCount++
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("node %s (%s): fs_path escapes vault: %s", n.ID, n.Title, n.FsPath))
|
||||||
|
result.Healthy = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(physPath); os.IsNotExist(err) {
|
||||||
|
result.FilesMissing++
|
||||||
|
result.Details = append(result.Details, fmt.Sprintf("node %s (%s): folder missing on disk: %s", n.ID, n.Title, physPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check children
|
||||||
|
children, _ := a.nodes.ListChildren(n.ID, true)
|
||||||
|
for _, c := range children {
|
||||||
|
checkNode(c.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range roots {
|
||||||
|
checkNode(n.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file records
|
||||||
|
// We'll query files table and verify each vault-mode file exists
|
||||||
|
rows, err := a.db.Query(`SELECT id, node_id, path, storage_mode FROM files`)
|
||||||
|
if err != nil {
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("query files: %v", err))
|
||||||
|
result.Healthy = false
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id, nodeID, path, mode string
|
||||||
|
if err := rows.Scan(&id, &nodeID, &path, &mode); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.TotalFiles++
|
||||||
|
|
||||||
|
if mode == "vault" {
|
||||||
|
absPath := filepath.Join(a.vault, path)
|
||||||
|
rel, err := filepath.Rel(a.vault, absPath)
|
||||||
|
if err != nil || strings.HasPrefix(rel, "..") {
|
||||||
|
result.PathEscapeCount++
|
||||||
|
result.Errors = append(result.Errors, fmt.Sprintf("file %s: path escapes vault: %s", id, path))
|
||||||
|
result.Healthy = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(absPath); err == nil {
|
||||||
|
result.FilesOnDisk++
|
||||||
|
} else {
|
||||||
|
result.FilesMissing++
|
||||||
|
result.Details = append(result.Details, fmt.Sprintf("file %s (%s): missing on disk: %s", id, path, absPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.FilesMissing > 0 {
|
||||||
|
result.Healthy = false
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,371 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"verstak/internal/core/actions"
|
||||||
|
"verstak/internal/core/activity"
|
||||||
|
"verstak/internal/core/files"
|
||||||
|
"verstak/internal/core/nodes"
|
||||||
|
"verstak/internal/core/notes"
|
||||||
|
"verstak/internal/core/plugins"
|
||||||
|
"verstak/internal/core/search"
|
||||||
|
"verstak/internal/core/storage"
|
||||||
|
syncsvc "verstak/internal/core/sync"
|
||||||
|
"verstak/internal/core/templates"
|
||||||
|
"verstak/internal/core/worklog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupTestApp creates a full App with a temp vault directory for testing.
|
||||||
|
func setupTestApp(t *testing.T) (*App, string) {
|
||||||
|
t.Helper()
|
||||||
|
vaultRoot, err := os.MkdirTemp("", "verstak-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mkdir temp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init vault structure
|
||||||
|
if err := os.MkdirAll(filepath.Join(vaultRoot, ".verstak"), 0o750); err != nil {
|
||||||
|
t.Fatalf("mkdir .verstak: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Join(vaultRoot, ".verstak", "trash"), 0o750); err != nil {
|
||||||
|
t.Fatalf("mkdir trash: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := filepath.Join(vaultRoot, ".verstak", "vault.db")
|
||||||
|
db, err := storage.Open(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open db: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeRepo := nodes.NewRepository(db)
|
||||||
|
fileSvc := files.NewService(db, vaultRoot, nodeRepo)
|
||||||
|
noteSvc := notes.NewService(db, vaultRoot, nodeRepo, fileSvc)
|
||||||
|
actionSvc := actions.NewService(db)
|
||||||
|
activitySvc := activity.NewService(db)
|
||||||
|
worklogSvc := worklog.NewService(db)
|
||||||
|
searchSvc := search.NewService(db)
|
||||||
|
pm := plugins.NewManager(vaultRoot)
|
||||||
|
pm.Discover()
|
||||||
|
|
||||||
|
templatesReg := templates.NewRegistry()
|
||||||
|
if err := templatesReg.LoadSystem(); err != nil {
|
||||||
|
t.Fatalf("load templates: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncSvc := syncsvc.NewService(db, "test-device")
|
||||||
|
|
||||||
|
app := &App{
|
||||||
|
db: db,
|
||||||
|
nodes: nodeRepo,
|
||||||
|
files: fileSvc,
|
||||||
|
notes: noteSvc,
|
||||||
|
activity: activitySvc,
|
||||||
|
actions: actionSvc,
|
||||||
|
worklog: worklogSvc,
|
||||||
|
search: searchSvc,
|
||||||
|
plugins: pm,
|
||||||
|
sync: syncSvc,
|
||||||
|
templates: templatesReg,
|
||||||
|
vault: vaultRoot,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
db.Close()
|
||||||
|
os.RemoveAll(vaultRoot)
|
||||||
|
})
|
||||||
|
|
||||||
|
return app, vaultRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultLayout_CreateProjectTree(t *testing.T) {
|
||||||
|
app, vault := setupTestApp(t)
|
||||||
|
|
||||||
|
// 1. Create root "Проекты" from folder template
|
||||||
|
proj, err := app.CreateNodeFromTemplate("", "Проекты", "folder.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create Проекты: %v", err)
|
||||||
|
}
|
||||||
|
if proj.FsPath != "Проекты" {
|
||||||
|
t.Errorf("expected fs_path 'Проекты', got %q", proj.FsPath)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(vault, "Проекты")); os.IsNotExist(err) {
|
||||||
|
t.Error("expected folder 'Проекты' to exist on disk")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create child "Рабочие" inside Проекты
|
||||||
|
work, err := app.CreateNodeFromTemplate(proj.ID, "Рабочие", "folder.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create Рабочие: %v", err)
|
||||||
|
}
|
||||||
|
expectedWorkPath := "Проекты/Рабочие"
|
||||||
|
if work.FsPath != expectedWorkPath {
|
||||||
|
t.Errorf("expected fs_path %q, got %q", expectedWorkPath, work.FsPath)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(vault, expectedWorkPath)); os.IsNotExist(err) {
|
||||||
|
t.Error("expected folder 'Проекты/Рабочие' to exist on disk")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Create project "Разработка серверной" from project template
|
||||||
|
server, err := app.CreateNodeFromTemplate(work.ID, "Разработка серверной", "project.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create Разработка серверной: %v", err)
|
||||||
|
}
|
||||||
|
expectedServerPath := "Проекты/Рабочие/Разработка серверной"
|
||||||
|
if server.FsPath != expectedServerPath {
|
||||||
|
t.Errorf("expected fs_path %q, got %q", expectedServerPath, server.FsPath)
|
||||||
|
}
|
||||||
|
serverFolder := filepath.Join(vault, expectedServerPath)
|
||||||
|
if _, err := os.Stat(serverFolder); os.IsNotExist(err) {
|
||||||
|
t.Error("expected project folder on disk")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Verify template created Overview.md
|
||||||
|
overviewPath := filepath.Join(serverFolder, "Overview.md")
|
||||||
|
if _, err := os.Stat(overviewPath); os.IsNotExist(err) {
|
||||||
|
t.Log("note: Overview.md from template not created (may not be implemented)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultLayout_CreateNoteInsideProject(t *testing.T) {
|
||||||
|
app, vault := setupTestApp(t)
|
||||||
|
|
||||||
|
proj, err := app.CreateNodeFromTemplate("", "Тестовый проект", "project.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create project: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a note inside the project
|
||||||
|
noteNode, fileRec, err := app.notes.Create(proj.ID, "Моя заметка", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create note: %v", err)
|
||||||
|
}
|
||||||
|
if noteNode == nil || fileRec == nil {
|
||||||
|
t.Fatal("expected non-nil node and file record")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the note .md file is inside the project folder
|
||||||
|
expectedPath := filepath.Join(vault, proj.FsPath, "Моя заметка.md")
|
||||||
|
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
||||||
|
// Try the safe-display-name variant
|
||||||
|
expectedPath2 := filepath.Join(vault, proj.FsPath, "Моя_заметка.md")
|
||||||
|
if _, err2 := os.Stat(expectedPath2); os.IsNotExist(err2) {
|
||||||
|
// Show what actually exists
|
||||||
|
entries, _ := os.ReadDir(filepath.Join(vault, proj.FsPath))
|
||||||
|
t.Errorf("expected note file in project folder, found: %v", listNames(entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultLayout_CopyFileIntoProject(t *testing.T) {
|
||||||
|
app, vault := setupTestApp(t)
|
||||||
|
|
||||||
|
proj, err := app.CreateNodeFromTemplate("", "Проект", "folder.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create project: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temp source file
|
||||||
|
srcFile := filepath.Join(vault, "source.txt")
|
||||||
|
if err := os.WriteFile(srcFile, []byte("hello"), 0o640); err != nil {
|
||||||
|
t.Fatalf("write source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a file node inside the project
|
||||||
|
fileNode, err := app.nodes.Create(&proj.ID, "file", "test.txt", 0, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create file node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file into vault (using parent's fs_path)
|
||||||
|
rec, err := app.files.CopyIntoVault(fileNode.ID, srcFile, proj.FsPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("copy into vault: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify file lands in project folder
|
||||||
|
expectedPath := filepath.Join(vault, proj.FsPath, "source.txt")
|
||||||
|
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected file at %s, record path = %s", expectedPath, rec.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultLayout_RenameParentUpdatesDescendants(t *testing.T) {
|
||||||
|
app, vault := setupTestApp(t)
|
||||||
|
|
||||||
|
root, _ := app.CreateNodeFromTemplate("", "Root", "folder.default")
|
||||||
|
child, _ := app.CreateNodeFromTemplate(root.ID, "Child", "folder.default")
|
||||||
|
grandchild, _ := app.CreateNodeFromTemplate(child.ID, "Grandchild", "folder.default")
|
||||||
|
|
||||||
|
// Rename root
|
||||||
|
if err := app.RenameNode(root.ID, "RenamedRoot"); err != nil {
|
||||||
|
t.Fatalf("rename root: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify child fs_path updated
|
||||||
|
childUpdated, _ := app.nodes.GetActive(child.ID)
|
||||||
|
expectedChildPath := "RenamedRoot/Child"
|
||||||
|
if childUpdated.FsPath != expectedChildPath {
|
||||||
|
t.Errorf("expected child fs_path %q, got %q", expectedChildPath, childUpdated.FsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify grandchild fs_path updated
|
||||||
|
gcUpdated, _ := app.nodes.GetActive(grandchild.ID)
|
||||||
|
expectedGCPath := "RenamedRoot/Child/Grandchild"
|
||||||
|
if gcUpdated.FsPath != expectedGCPath {
|
||||||
|
t.Errorf("expected grandchild fs_path %q, got %q", expectedGCPath, gcUpdated.FsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify physical folders
|
||||||
|
if _, err := os.Stat(filepath.Join(vault, expectedChildPath)); os.IsNotExist(err) {
|
||||||
|
t.Error("expected child folder on disk after rename")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(vault, expectedGCPath)); os.IsNotExist(err) {
|
||||||
|
t.Error("expected grandchild folder on disk after rename")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify old path no longer exists
|
||||||
|
if _, err := os.Stat(filepath.Join(vault, "Root")); !os.IsNotExist(err) {
|
||||||
|
t.Error("expected old root path to not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultLayout_MoveNode(t *testing.T) {
|
||||||
|
app, vault := setupTestApp(t)
|
||||||
|
|
||||||
|
folder1, _ := app.CreateNodeFromTemplate("", "Folder1", "folder.default")
|
||||||
|
folder2, _ := app.CreateNodeFromTemplate("", "Folder2", "folder.default")
|
||||||
|
child, _ := app.CreateNodeFromTemplate(folder1.ID, "Child", "folder.default")
|
||||||
|
|
||||||
|
// Move child from Folder1 to Folder2
|
||||||
|
if err := app.MoveNode(child.ID, folder2.ID); err != nil {
|
||||||
|
t.Fatalf("move node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
moved, _ := app.nodes.GetActive(child.ID)
|
||||||
|
expectedPath := "Folder2/Child"
|
||||||
|
if moved.FsPath != expectedPath {
|
||||||
|
t.Errorf("expected fs_path %q, got %q", expectedPath, moved.FsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(vault, expectedPath)); os.IsNotExist(err) {
|
||||||
|
t.Error("expected child folder at new location on disk")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultLayout_DeleteMovesToTrash(t *testing.T) {
|
||||||
|
app, vault := setupTestApp(t)
|
||||||
|
|
||||||
|
node, _ := app.CreateNodeFromTemplate("", "ToDelete", "folder.default")
|
||||||
|
nodePath := filepath.Join(vault, "ToDelete")
|
||||||
|
if _, err := os.Stat(nodePath); os.IsNotExist(err) {
|
||||||
|
t.Fatal("expected folder to exist before delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the node
|
||||||
|
if err := app.DeleteNode(node.ID); err != nil {
|
||||||
|
t.Fatalf("delete node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify node folder is no longer in original location
|
||||||
|
if _, err := os.Stat(nodePath); !os.IsNotExist(err) {
|
||||||
|
t.Error("expected node folder to be removed from original location")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify trash has the folder
|
||||||
|
trashDir := filepath.Join(vault, ".verstak", "trash")
|
||||||
|
entries, _ := os.ReadDir(trashDir)
|
||||||
|
found := false
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() && contains(e.Name(), "ToDelete") {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected deleted folder in trash, found: %v", listNames(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify node is soft-deleted
|
||||||
|
_, err := app.nodes.GetActive(node.ID)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected node to be soft-deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultLayout_NameConflict(t *testing.T) {
|
||||||
|
app, vault := setupTestApp(t)
|
||||||
|
|
||||||
|
node1, err := app.CreateNodeFromTemplate("", "SameName", "folder.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create first: %v", err)
|
||||||
|
}
|
||||||
|
if node1.FsPath != "SameName" {
|
||||||
|
t.Errorf("expected fs_path 'SameName', got %q", node1.FsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
node2, err := app.CreateNodeFromTemplate("", "SameName", "folder.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create second: %v", err)
|
||||||
|
}
|
||||||
|
if node2.FsPath == "SameName" {
|
||||||
|
t.Errorf("expected unique fs_path for second node, got same %q", node2.FsPath)
|
||||||
|
}
|
||||||
|
if node2.FsPath == node1.FsPath {
|
||||||
|
t.Error("expected different fs_path for conflicting name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both folders should exist on disk
|
||||||
|
if _, err := os.Stat(filepath.Join(vault, node1.FsPath)); os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected first folder at %s", node1.FsPath)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(vault, node2.FsPath)); os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected second folder at %s", node2.FsPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultLayout_VaultCheck(t *testing.T) {
|
||||||
|
app, _ := setupTestApp(t)
|
||||||
|
|
||||||
|
// Create a healthy vault structure
|
||||||
|
app.CreateNodeFromTemplate("", "Healthy", "folder.default")
|
||||||
|
child, _ := app.CreateNodeFromTemplate("", "Child", "folder.default")
|
||||||
|
app.CreateNodeFromTemplate(child.ID, "Grandchild", "folder.default")
|
||||||
|
|
||||||
|
// Run vault check
|
||||||
|
result, err := app.VaultCheck()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("vault check: %v", err)
|
||||||
|
}
|
||||||
|
if !result.Healthy {
|
||||||
|
t.Errorf("expected healthy vault, got errors: %v", result.Errors)
|
||||||
|
}
|
||||||
|
if result.TotalNodes == 0 {
|
||||||
|
t.Error("expected at least 1 node in check")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
func listNames(entries []os.DirEntry) []string {
|
||||||
|
var names []string
|
||||||
|
for _, e := range entries {
|
||||||
|
names = append(names, e.Name())
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) >= len(substr) && containsStr(s, substr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsStr(s, substr string) bool {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
"verstak/internal/core/nodes"
|
"verstak/internal/core/nodes"
|
||||||
"verstak/internal/core/storage"
|
"verstak/internal/core/storage"
|
||||||
|
"verstak/internal/core/templates"
|
||||||
"verstak/internal/core/util"
|
"verstak/internal/core/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -55,6 +56,17 @@ func NewService(db *storage.DB, vaultRoot string, nodeRepo *nodes.Repository) *S
|
||||||
return &Service{db: db, vaultRoot: vaultRoot, nodes: nodeRepo}
|
return &Service{db: db, vaultRoot: vaultRoot, nodes: nodeRepo}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) parentFsPath(parentID string) string {
|
||||||
|
if parentID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parent, err := s.nodes.GetActive(parentID)
|
||||||
|
if err != nil || parent.FsPath == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return parent.FsPath
|
||||||
|
}
|
||||||
|
|
||||||
// DB returns the underlying storage.
|
// DB returns the underlying storage.
|
||||||
func (s *Service) DB() *storage.DB {
|
func (s *Service) DB() *storage.DB {
|
||||||
return s.db
|
return s.db
|
||||||
|
|
@ -126,18 +138,18 @@ func (s *Service) AddExternal(nodeID, absPath string) (*Record, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyIntoVault copies an external file into the vault.
|
// CopyIntoVault copies an external file into the vault.
|
||||||
// The file lands at <vaultRoot>/spaces/<nodeSlug>/<filename>.
|
// The file lands at <vaultRoot>/<parentFsPath>/<filename>.
|
||||||
func (s *Service) CopyIntoVault(nodeID, absPath, nodeSlug string) (*Record, error) {
|
func (s *Service) CopyIntoVault(nodeID, absPath, parentFsPath string) (*Record, error) {
|
||||||
info, err := os.Stat(absPath)
|
info, err := os.Stat(absPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("stat: %w", err)
|
return nil, fmt.Errorf("stat: %w", err)
|
||||||
}
|
}
|
||||||
if nodeSlug == "" {
|
if parentFsPath == "" {
|
||||||
nodeSlug = nodeID[:8]
|
parentFsPath = "."
|
||||||
}
|
}
|
||||||
|
|
||||||
destDir := filepath.Join(s.vaultRoot, "spaces", nodeSlug)
|
destDir := filepath.Join(s.vaultRoot, parentFsPath)
|
||||||
if _, err := s.vaultPath(filepath.Join("spaces", nodeSlug)); err != nil {
|
if _, err := s.vaultPath(parentFsPath); err != nil {
|
||||||
return nil, fmt.Errorf("path safety: %w", err)
|
return nil, fmt.Errorf("path safety: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(destDir, 0o750); err != nil {
|
if err := os.MkdirAll(destDir, 0o750); err != nil {
|
||||||
|
|
@ -147,7 +159,6 @@ func (s *Service) CopyIntoVault(nodeID, absPath, nodeSlug string) (*Record, erro
|
||||||
filename := filepath.Base(absPath)
|
filename := filepath.Base(absPath)
|
||||||
dest := filepath.Join(destDir, filename)
|
dest := filepath.Join(destDir, filename)
|
||||||
|
|
||||||
// If destination exists, add a numeric suffix.
|
|
||||||
if _, err := os.Stat(dest); err == nil {
|
if _, err := os.Stat(dest); err == nil {
|
||||||
ext := filepath.Ext(filename)
|
ext := filepath.Ext(filename)
|
||||||
name := strings.TrimSuffix(filename, ext)
|
name := strings.TrimSuffix(filename, ext)
|
||||||
|
|
@ -299,7 +310,11 @@ func (s *Service) CreateEmptyFile(parentID, filename string) (*nodes.Node, error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dir := filepath.Join(s.vaultRoot, "spaces", node.Slug)
|
parentFsPath := s.parentFsPath(parentID)
|
||||||
|
dir := filepath.Join(s.vaultRoot, parentFsPath)
|
||||||
|
if parentFsPath == "" {
|
||||||
|
dir = s.vaultRoot
|
||||||
|
}
|
||||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||||
return nil, fmt.Errorf("mkdir: %w", err)
|
return nil, fmt.Errorf("mkdir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -342,7 +357,11 @@ func (s *Service) Duplicate(nodeID string) (*nodes.Node, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dir := filepath.Join(s.vaultRoot, "spaces", node.Slug)
|
parentFsPath := s.parentFsPath(parentID)
|
||||||
|
dir := filepath.Join(s.vaultRoot, parentFsPath)
|
||||||
|
if parentFsPath == "" {
|
||||||
|
dir = s.vaultRoot
|
||||||
|
}
|
||||||
os.MkdirAll(dir, 0o750)
|
os.MkdirAll(dir, 0o750)
|
||||||
dst := filepath.Join(dir, newName)
|
dst := filepath.Join(dir, newName)
|
||||||
hash, err := copyAndHash(srcPath, dst)
|
hash, err := copyAndHash(srcPath, dst)
|
||||||
|
|
@ -428,6 +447,17 @@ func (s *Service) DeleteNodeAndChildren(nodeID string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = s.deleteFileRecords(nodeID)
|
_ = s.deleteFileRecords(nodeID)
|
||||||
|
// Move physical folder to trash if the node has fs_path
|
||||||
|
n, err := s.nodes.GetActive(nodeID)
|
||||||
|
if err == nil && n.FsPath != "" {
|
||||||
|
src := filepath.Join(s.vaultRoot, n.FsPath)
|
||||||
|
if info, err := os.Stat(src); err == nil && info.IsDir() {
|
||||||
|
trashDir := filepath.Join(s.vaultRoot, ".verstak", "trash")
|
||||||
|
os.MkdirAll(trashDir, 0o750)
|
||||||
|
trashPath := filepath.Join(trashDir, n.ID+"_"+templates.SafeDisplayNameToPathSegment(n.Title))
|
||||||
|
os.Rename(src, trashPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
return s.nodes.SoftDelete(nodeID)
|
return s.nodes.SoftDelete(nodeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -454,7 +484,8 @@ func (s *Service) importPath(parentID, sourcePath string, copyMode bool) ([]node
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if copyMode {
|
if copyMode {
|
||||||
_, err = s.CopyIntoVault(node.ID, sourcePath, node.Slug)
|
parentFsPath := s.parentFsPath(parentID)
|
||||||
|
_, err = s.CopyIntoVault(node.ID, sourcePath, parentFsPath)
|
||||||
} else {
|
} else {
|
||||||
_, err = s.AddExternal(node.ID, sourcePath)
|
_, err = s.AddExternal(node.ID, sourcePath)
|
||||||
}
|
}
|
||||||
|
|
@ -473,6 +504,14 @@ func (s *Service) importDir(parentID, sourcePath, dirName string, copyMode bool)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parentFsPath := s.parentFsPath(parentID)
|
||||||
|
seg := templates.SafeDisplayNameToPathSegment(dirName)
|
||||||
|
folderFsPath := seg
|
||||||
|
if parentFsPath != "" {
|
||||||
|
folderFsPath = filepath.Join(parentFsPath, seg)
|
||||||
|
}
|
||||||
|
_ = s.nodes.UpdateFsPath(folderNode.ID, folderFsPath)
|
||||||
|
|
||||||
entries, err := os.ReadDir(sourcePath)
|
entries, err := os.ReadDir(sourcePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -495,7 +534,7 @@ func (s *Service) importDir(parentID, sourcePath, dirName string, copyMode bool)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if copyMode {
|
if copyMode {
|
||||||
_, err = s.CopyIntoVault(childNode.ID, childPath, childNode.Slug)
|
_, err = s.CopyIntoVault(childNode.ID, childPath, folderFsPath)
|
||||||
} else {
|
} else {
|
||||||
_, err = s.AddExternal(childNode.ID, childPath)
|
_, err = s.AddExternal(childNode.ID, childPath)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ func TestCopyIntoVault(t *testing.T) {
|
||||||
srcFile := filepath.Join(srcDir, "doc.pdf")
|
srcFile := filepath.Join(srcDir, "doc.pdf")
|
||||||
os.WriteFile(srcFile, []byte("PDF content here"), 0o640)
|
os.WriteFile(srcFile, []byte("PDF content here"), 0o640)
|
||||||
|
|
||||||
rec, err := svc.CopyIntoVault("node-1", srcFile, "my-node")
|
rec, err := svc.CopyIntoVault("node-1", srcFile, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CopyIntoVault: %v", err)
|
t.Fatalf("CopyIntoVault: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +117,7 @@ func TestDeleteToTrash(t *testing.T) {
|
||||||
src := filepath.Join(t.TempDir(), "important.pdf")
|
src := filepath.Join(t.TempDir(), "important.pdf")
|
||||||
os.WriteFile(src, []byte("important data"), 0o640)
|
os.WriteFile(src, []byte("important data"), 0o640)
|
||||||
|
|
||||||
rec, _ := svc.CopyIntoVault("node-x", src, "node-x")
|
rec, _ := svc.CopyIntoVault("node-x", src, "")
|
||||||
|
|
||||||
if err := svc.DeleteToTrash(rec.ID); err != nil {
|
if err := svc.DeleteToTrash(rec.ID); err != nil {
|
||||||
t.Fatalf("DeleteToTrash: %v", err)
|
t.Fatalf("DeleteToTrash: %v", err)
|
||||||
|
|
@ -247,7 +247,7 @@ func TestDeleteNodeAndChildren(t *testing.T) {
|
||||||
// Add file record to child.
|
// Add file record to child.
|
||||||
src := filepath.Join(t.TempDir(), "child.txt")
|
src := filepath.Join(t.TempDir(), "child.txt")
|
||||||
os.WriteFile(src, []byte("data"), 0o640)
|
os.WriteFile(src, []byte("data"), 0o640)
|
||||||
svc.CopyIntoVault(child.ID, src, child.Slug)
|
svc.CopyIntoVault(child.ID, src, "")
|
||||||
|
|
||||||
if err := svc.DeleteNodeAndChildren(parent.ID); err != nil {
|
if err := svc.DeleteNodeAndChildren(parent.ID); err != nil {
|
||||||
t.Fatalf("DeleteNodeAndChildren: %v", err)
|
t.Fatalf("DeleteNodeAndChildren: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"verstak/internal/core/storage"
|
"verstak/internal/core/storage"
|
||||||
|
"verstak/internal/core/templates"
|
||||||
"verstak/internal/core/util"
|
"verstak/internal/core/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -264,7 +266,8 @@ func (r *Repository) UpdateFsPathRecursive(id, newFsPath string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, child := range children {
|
for _, child := range children {
|
||||||
childPath := newFsPath + "/" + child.Slug
|
seg := templates.SafeDisplayNameToPathSegment(child.Title)
|
||||||
|
childPath := filepath.Join(newFsPath, seg)
|
||||||
if err := r.UpdateFsPathRecursive(child.ID, childPath); err != nil {
|
if err := r.UpdateFsPathRecursive(child.ID, childPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"verstak/internal/core/files"
|
"verstak/internal/core/files"
|
||||||
"verstak/internal/core/nodes"
|
"verstak/internal/core/nodes"
|
||||||
"verstak/internal/core/storage"
|
"verstak/internal/core/storage"
|
||||||
|
"verstak/internal/core/templates"
|
||||||
"verstak/internal/core/util"
|
"verstak/internal/core/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -40,33 +41,43 @@ func (s *Service) Create(parentID, title, section string) (*nodes.Node, *files.R
|
||||||
return nil, nil, fmt.Errorf("create node: %w", err)
|
return nil, nil, fmt.Errorf("create node: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
slug := node.Slug
|
seg := templates.SafeDisplayNameToPathSegment(title)
|
||||||
if slug == "" {
|
if seg == "" {
|
||||||
slug = "note"
|
seg = "note"
|
||||||
|
}
|
||||||
|
filename := seg + ".md"
|
||||||
|
|
||||||
|
var destDir string
|
||||||
|
if parentID != "" {
|
||||||
|
parent, err := s.nodes.GetActive(parentID)
|
||||||
|
if err == nil && parent.FsPath != "" {
|
||||||
|
destDir = filepath.Join(s.vaultRoot, parent.FsPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if destDir == "" {
|
||||||
|
destDir = s.vaultRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(destDir, 0o750); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("mkdir: %w", err)
|
||||||
}
|
}
|
||||||
filename := slug + ".md"
|
|
||||||
destDir := filepath.Join(s.vaultRoot, "spaces")
|
|
||||||
os.MkdirAll(destDir, 0o750)
|
|
||||||
|
|
||||||
dest := filepath.Join(destDir, filename)
|
dest := filepath.Join(destDir, filename)
|
||||||
if _, err := os.Stat(dest); err == nil {
|
if _, err := os.Stat(dest); err == nil {
|
||||||
filename = fmt.Sprintf("%s_%s.md", slug, node.ID[:8])
|
filename = fmt.Sprintf("%s_%s.md", seg, node.ID[:8])
|
||||||
dest = filepath.Join(destDir, filename)
|
dest = filepath.Join(destDir, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write initial content.
|
|
||||||
if err := os.WriteFile(dest, []byte("# "+title+"\n\n"), 0o640); err != nil {
|
if err := os.WriteFile(dest, []byte("# "+title+"\n\n"), 0o640); err != nil {
|
||||||
return nil, nil, fmt.Errorf("write: %w", err)
|
return nil, nil, fmt.Errorf("write: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register file record.
|
|
||||||
relPath, _ := filepath.Rel(s.vaultRoot, dest)
|
relPath, _ := filepath.Rel(s.vaultRoot, dest)
|
||||||
fileRec, err := insertFileRecord(s.db, node.ID, filename, relPath, "vault", 0)
|
fileRec, err := insertFileRecord(s.db, node.ID, filename, relPath, "vault", 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("insert file: %w", err)
|
return nil, nil, fmt.Errorf("insert file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link.
|
|
||||||
_, err = s.db.Exec(
|
_, err = s.db.Exec(
|
||||||
`INSERT INTO notes (node_id, file_id, format) VALUES (?,?,?)`,
|
`INSERT INTO notes (node_id, file_id, format) VALUES (?,?,?)`,
|
||||||
node.ID, fileRec.ID, "markdown")
|
node.ID, fileRec.ID, "markdown")
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,16 @@ func TestCreateAndRead(t *testing.T) {
|
||||||
t.Errorf("content = %q", content)
|
t.Errorf("content = %q", content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify file on disk.
|
// Verify file on disk (in vault root for parentless notes).
|
||||||
spacesDir := filepath.Join(vaultRoot, "spaces")
|
entries, _ := os.ReadDir(vaultRoot)
|
||||||
entries, _ := os.ReadDir(spacesDir)
|
var mdFiles int
|
||||||
if len(entries) == 0 {
|
for _, e := range entries {
|
||||||
t.Error("expected file in spaces/")
|
if !e.IsDir() && filepath.Ext(e.Name()) == ".md" {
|
||||||
|
mdFiles++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mdFiles == 0 {
|
||||||
|
t.Error("expected .md file in vault root")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -309,14 +309,18 @@ func (s *Server) handleFileUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
NodeID string `json:"node_id"`
|
NodeID string `json:"node_id"`
|
||||||
FilePath string `json:"file_path"`
|
FilePath string `json:"file_path"`
|
||||||
NodeSlug string `json:"node_slug"`
|
|
||||||
}
|
}
|
||||||
json.NewDecoder(r.Body).Decode(&req)
|
json.NewDecoder(r.Body).Decode(&req)
|
||||||
if req.NodeID == "" || req.FilePath == "" {
|
if req.NodeID == "" || req.FilePath == "" {
|
||||||
jsonErr(w, 400, "node_id and file_path required")
|
jsonErr(w, 400, "node_id and file_path required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rec, err := s.files.CopyIntoVault(req.NodeID, req.FilePath, req.NodeSlug)
|
n, err := s.nodes.GetActive(req.NodeID)
|
||||||
|
if err != nil {
|
||||||
|
jsonErr(w, 404, "node not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rec, err := s.files.CopyIntoVault(req.NodeID, req.FilePath, n.FsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonErr(w, 500, err.Error())
|
jsonErr(w, 500, err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ ALLOWED_GO_CYRILLIC=(
|
||||||
"cmd/verstak-gui/main.go"
|
"cmd/verstak-gui/main.go"
|
||||||
"internal/core/templates/safename.go"
|
"internal/core/templates/safename.go"
|
||||||
"internal/core/templates/safename_test.go"
|
"internal/core/templates/safename_test.go"
|
||||||
|
"cmd/verstak-gui/vault_layout_test.go"
|
||||||
)
|
)
|
||||||
|
|
||||||
echo "=== Checking for hardcoded Cyrillic in source code ==="
|
echo "=== Checking for hardcoded Cyrillic in source code ==="
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue