diff --git a/core/file_handling/file.go b/core/file_handling/file.go index 8cbf667..3117069 100644 --- a/core/file_handling/file.go +++ b/core/file_handling/file.go @@ -25,7 +25,7 @@ type ExtractedLink struct { Col int } -func ReadFile(path string) (*File, error) { +func ReadFile(vaultRoot, path string) (*File, error) { body, err := os.ReadFile(path) if err != nil { return nil, err @@ -47,13 +47,14 @@ func ReadFile(path string) (*File, error) { return &File{ Path: path, Title: fm.Title, - Slug: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)), + Slug: SlugRelative(vaultRoot, path), FrontMatter: *fm, Content: strings.Split(string(body), "\n"), ExtractedLinks: ExtractLinks(body), }, nil } + var linkRegex = regexp.MustCompile(`\[\[([^\]|]+)(?:\|([^\]]+))?\]\]`) func ExtractLinks(body []byte) []*ExtractedLink { diff --git a/core/file_handling/slug.go b/core/file_handling/slug.go index 11676f4..346265f 100644 --- a/core/file_handling/slug.go +++ b/core/file_handling/slug.go @@ -2,6 +2,8 @@ package filehandling import ( "bytes" + "path/filepath" + "strings" ) func Slugify(s string) string { @@ -17,3 +19,9 @@ func Slugify(s string) string { } return slug.String() } + +func SlugRelative(base, path string) string { + rel := strings.TrimPrefix(path, base+string(filepath.Separator)) + rel = strings.TrimSuffix(rel, filepath.Ext(rel)) + return rel +} diff --git a/core/file_handling/template.go b/core/file_handling/template.go index e8a5ac3..adfd989 100644 --- a/core/file_handling/template.go +++ b/core/file_handling/template.go @@ -2,6 +2,7 @@ package filehandling import ( "os" + "path/filepath" "strings" "time" @@ -12,16 +13,22 @@ type Template struct { Content []byte Title string Slug string + Filename string FrontMatter *FrontMatter } -func NewTemplate(templateName string, title string) (*Template, error) { +func NewTemplate(templateName, title, directory string) (*Template, error) { var templatePath string if templateName != "" { templatePath = store.GetVaultStore().GetTemplatePath(templateName) } - slug := Slugify(title) - content, err := renderTemplate(templatePath, title, slug) + + fileNameSlug := Slugify(title) + + pathSlug := filepath.ToSlash(filepath.Join(directory, fileNameSlug)) + filename := fileNameSlug + ".md" + + content, err := renderTemplate(templatePath, title, pathSlug, filename) if err != nil { return nil, err } @@ -34,12 +41,13 @@ func NewTemplate(templateName string, title string) (*Template, error) { return &Template{ Content: content, Title: title, - Slug: slug, + Slug: pathSlug, + Filename: filename, FrontMatter: fm, }, nil } -func renderTemplate(templatePath string, title string, slug string) ([]byte, error) { +func renderTemplate(templatePath, title, slug, filename string) ([]byte, error) { template, err := readTemplate(templatePath, title) if err != nil { return nil, err @@ -49,7 +57,7 @@ func renderTemplate(templatePath string, title string, slug string) ([]byte, err "{{title}}", title, "{{date}}", time.Now().Format("2006-01-02"), "{{slug}}", slug, - "{{file}}", slug+".md", + "{{file}}", filename, ) return []byte(r.Replace(string(template))), nil diff --git a/core/handlers/note/create_note_handler.go b/core/handlers/note/create_note_handler.go index beb2f07..87787cc 100644 --- a/core/handlers/note/create_note_handler.go +++ b/core/handlers/note/create_note_handler.go @@ -39,12 +39,12 @@ func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (an return nil, err } - template, err := filehandling.NewTemplate(cmd.TemplateName, cmd.Title) + template, err := filehandling.NewTemplate(cmd.TemplateName, cmd.Title, cmd.Directory) if err != nil { return nil, err } - notePath := filepath.Join(store.GetVaultStore().Config.VaultPath(), cmd.Directory, template.Slug+".md") + notePath := filepath.Join(store.GetVaultStore().Config.VaultPath(), cmd.Directory, template.Filename) dbCtx, err := h.uow.Begin() if err != nil { @@ -53,6 +53,15 @@ func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (an defer h.uow.Rollback() + existingNote, err := h.noteRepo.GetBySlug(ctx, template.Slug) + if err != nil { + return nil, err + } + + if existingNote != nil { + return notePath, nil + } + tagModels, err := h.tagService.CreateTags(ctx, dbCtx, template.FrontMatter.Tags) if err != nil { return nil, err diff --git a/core/handlers/note/save_note_handler.go b/core/handlers/note/save_note_handler.go index c75c1c5..1156ba6 100644 --- a/core/handlers/note/save_note_handler.go +++ b/core/handlers/note/save_note_handler.go @@ -4,9 +4,10 @@ import ( "context" "encoding/json" - filehandling "github.com/KristianJBorgwarth/dendrite.daemon/core/file_handling" + "github.com/KristianJBorgwarth/dendrite.daemon/core/file_handling" "github.com/KristianJBorgwarth/dendrite.daemon/core/services" "github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories" + "github.com/KristianJBorgwarth/dendrite.daemon/persistence/store" ) type saveNoteCommand struct { @@ -38,7 +39,9 @@ func (h *SaveNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, return nil, err } - file, err := filehandling.ReadFile(cmd.Path) + vaultRoot := store.GetVaultStore().Config.VaultPath() + + file, err := filehandling.ReadFile(vaultRoot, cmd.Path) if err != nil { return nil, err } diff --git a/core/services/index_rebuilder.go b/core/services/index_rebuilder.go index 8d24c25..6649274 100644 --- a/core/services/index_rebuilder.go +++ b/core/services/index_rebuilder.go @@ -119,7 +119,7 @@ func (r *indexRebuilder) readFiles(vault string) ([]*filehandling.File, error) { return nil } - pendingFile, err := filehandling.ReadFile(path) + pendingFile, err := filehandling.ReadFile(vault, path) if err != nil { return err } diff --git a/dendrite b/dendrite index 4717851..6d6fa77 100755 Binary files a/dendrite and b/dendrite differ diff --git a/persistence/migrations/001_note.sql b/persistence/migrations/001_note.sql index 9a5214b..05ed495 100644 --- a/persistence/migrations/001_note.sql +++ b/persistence/migrations/001_note.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS note ( id TEXT PRIMARY KEY, - path TEXT UNIQUE, + path TEXT UNIQUE NOT NULL, title TEXT, slug TEXT UNIQUE NOT NULL, created_at TEXT NOT NULL, diff --git a/persistence/repositories/note_repository.go b/persistence/repositories/note_repository.go index e2f3d9d..4fd397a 100644 --- a/persistence/repositories/note_repository.go +++ b/persistence/repositories/note_repository.go @@ -33,9 +33,7 @@ func (r *noteRepository) Insert(ctx context.Context, dbContext persistence.IDbCo query := ` INSERT INTO note (id, title, path, slug, created_at, updated_at) VALUES (?, ?, ?, ?, datetime('now'), datetime('now')) - ON CONFLICT (slug) DO UPDATE - SET title = EXCLUDED.title, - path = EXCLUDED.path; + ON CONFLICT DO NOTHING; ` _, err := dbContext.ExecContext(ctx, query, note.ID(), note.Title(), note.Path(), note.Slug()) return err diff --git a/test/test_integration/create_note_handler_test.go b/test/test_integration/create_note_handler_test.go index aeeca2d..2f75939 100644 --- a/test/test_integration/create_note_handler_test.go +++ b/test/test_integration/create_note_handler_test.go @@ -79,7 +79,7 @@ func TestCreateNoteHandler_WithTemplate_CreatesNoteFileWithTagsAndReturnsPath(t assert.Equal(t, notePath, result) var noteCount int - require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "templated-note").Scan(¬eCount)) + require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "subdir/templated-note").Scan(¬eCount)) assert.Equal(t, 1, noteCount) rows, err := Fixture.DB.Query(`SELECT name FROM tag WHERE name IN ('go', 'testing')`) @@ -98,7 +98,7 @@ func TestCreateNoteHandler_WithTemplate_CreatesNoteFileWithTagsAndReturnsPath(t assert.NoError(t, statErr) } -func TestCreateNoteHandler_DuplicateSlug_Upserts(t *testing.T) { +func TestCreateNoteHandler_DuplicateSlug_ReturnsExistingPath(t *testing.T) { // Arrange handler := newCreateNoteHandler() @@ -107,29 +107,22 @@ func TestCreateNoteHandler_DuplicateSlug_Upserts(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(vaultPath, subDir), 0o755)) defer os.RemoveAll(filepath.Join(vaultPath, subDir)) - params1, _ := json.Marshal(map[string]any{"title": "Dup Note", "templateName": "", "directory": subDir}) - _, err := handler.Handle(Fixture.TestContext, params1) + params, _ := json.Marshal(map[string]any{"title": "Dup Note", "templateName": "", "directory": subDir}) + _, err := handler.Handle(Fixture.TestContext, params) require.NoError(t, err) - subDir2 := "dup-dir-moved" - require.NoError(t, os.MkdirAll(filepath.Join(vaultPath, subDir2), 0o755)) - defer os.RemoveAll(filepath.Join(vaultPath, subDir2)) - params2, _ := json.Marshal(map[string]any{"title": "Dup Note", "templateName": "", "directory": subDir2}) - - // Act - _, err = handler.Handle(Fixture.TestContext, params2) + // Act — same title and directory produces the same slug, handler returns early + result, err := handler.Handle(Fixture.TestContext, params) // Assert require.NoError(t, err) var count int - require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "dup-note").Scan(&count)) + require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "dup-dir/dup-note").Scan(&count)) assert.Equal(t, 1, count) - expectedPath := filepath.Join(vaultPath, subDir2, "dup-note.md") - var path string - require.NoError(t, Fixture.DB.QueryRow(`SELECT path FROM note WHERE slug = ?`, "dup-note").Scan(&path)) - assert.Equal(t, expectedPath, path) + expectedPath := filepath.Join(vaultPath, subDir, "dup-note.md") + assert.Equal(t, expectedPath, result) } func TestCreateNoteHandler_InvalidJSON_ReturnsError(t *testing.T) { diff --git a/test/test_integration/main_test.go b/test/test_integration/main_test.go index d4464e4..77553a6 100644 --- a/test/test_integration/main_test.go +++ b/test/test_integration/main_test.go @@ -30,7 +30,15 @@ func NewDBFixture() *DBFixture { panic(err) } - dbPath := filepath.Join(os.TempDir(), ".index", "index.db") + xdg := os.Getenv("XDG_DATA_HOME") + if xdg == "" { + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + xdg = filepath.Join(home, ".local", "share") + } + dbPath := filepath.Join(xdg, "dendrite", vaultPath, "index.db") dbContext := persistence.GetDBContext()