diff --git a/pkg/cli/context/files.go b/pkg/cli/context/files.go
new file mode 100644
index 00000000..098b2cd5
--- /dev/null
+++ b/pkg/cli/context/files.go
@@ -0,0 +1,51 @@
+/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Dnote. If not, see .
+ */
+
+package context
+
+import (
+ "path/filepath"
+
+ "github.com/dnote/dnote/pkg/cli/consts"
+ "github.com/dnote/dnote/pkg/cli/utils"
+ "github.com/pkg/errors"
+)
+
+// InitDnoteDirs creates the dnote directories if they don't already exist.
+func InitDnoteDirs(paths Paths) error {
+ if paths.Config != "" {
+ configDir := filepath.Join(paths.Config, consts.DnoteDirName)
+ if err := utils.EnsureDir(configDir); err != nil {
+ return errors.Wrap(err, "initializing config dir")
+ }
+ }
+ if paths.Data != "" {
+ dataDir := filepath.Join(paths.Data, consts.DnoteDirName)
+ if err := utils.EnsureDir(dataDir); err != nil {
+ return errors.Wrap(err, "initializing data dir")
+ }
+ }
+ if paths.Cache != "" {
+ cacheDir := filepath.Join(paths.Cache, consts.DnoteDirName)
+ if err := utils.EnsureDir(cacheDir); err != nil {
+ return errors.Wrap(err, "initializing cache dir")
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/cli/context/files_test.go b/pkg/cli/context/files_test.go
new file mode 100644
index 00000000..2422a795
--- /dev/null
+++ b/pkg/cli/context/files_test.go
@@ -0,0 +1,65 @@
+/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Dnote. If not, see .
+ */
+
+package context
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/dnote/dnote/pkg/assert"
+ "github.com/dnote/dnote/pkg/cli/consts"
+)
+
+func assertDirsExist(t *testing.T, paths Paths) {
+ configDir := filepath.Join(paths.Config, consts.DnoteDirName)
+ info, err := os.Stat(configDir)
+ assert.Equal(t, err, nil, "config dir should exist")
+ assert.Equal(t, info.IsDir(), true, "config should be a directory")
+
+ dataDir := filepath.Join(paths.Data, consts.DnoteDirName)
+ info, err = os.Stat(dataDir)
+ assert.Equal(t, err, nil, "data dir should exist")
+ assert.Equal(t, info.IsDir(), true, "data should be a directory")
+
+ cacheDir := filepath.Join(paths.Cache, consts.DnoteDirName)
+ info, err = os.Stat(cacheDir)
+ assert.Equal(t, err, nil, "cache dir should exist")
+ assert.Equal(t, info.IsDir(), true, "cache should be a directory")
+}
+
+func TestInitDnoteDirs(t *testing.T) {
+ tmpDir := t.TempDir()
+
+ paths := Paths{
+ Config: filepath.Join(tmpDir, "config"),
+ Data: filepath.Join(tmpDir, "data"),
+ Cache: filepath.Join(tmpDir, "cache"),
+ }
+
+ // Initialize directories
+ err := InitDnoteDirs(paths)
+ assert.Equal(t, err, nil, "InitDnoteDirs should succeed")
+ assertDirsExist(t, paths)
+
+ // Call again - should be idempotent
+ err = InitDnoteDirs(paths)
+ assert.Equal(t, err, nil, "InitDnoteDirs should succeed when dirs already exist")
+ assertDirsExist(t, paths)
+}
diff --git a/pkg/cli/context/testutils.go b/pkg/cli/context/testutils.go
index 17d11847..62fed833 100644
--- a/pkg/cli/context/testutils.go
+++ b/pkg/cli/context/testutils.go
@@ -19,7 +19,6 @@
package context
import (
- "os"
"path/filepath"
"testing"
@@ -40,34 +39,16 @@ func getDefaultTestPaths(t *testing.T) Paths {
}
}
-// createTestDirectories creates test directories for the given paths
-func createTestDirectories(t *testing.T, paths Paths) {
- if paths.Config != "" {
- configDir := filepath.Join(paths.Config, consts.DnoteDirName)
- if err := os.MkdirAll(configDir, 0755); err != nil {
- t.Fatal(errors.Wrap(err, "creating test config directory"))
- }
- }
- if paths.Data != "" {
- dataDir := filepath.Join(paths.Data, consts.DnoteDirName)
- if err := os.MkdirAll(dataDir, 0755); err != nil {
- t.Fatal(errors.Wrap(err, "creating test data directory"))
- }
- }
- if paths.Cache != "" {
- cacheDir := filepath.Join(paths.Cache, consts.DnoteDirName)
- if err := os.MkdirAll(cacheDir, 0755); err != nil {
- t.Fatal(errors.Wrap(err, "creating test cache directory"))
- }
- }
-}
// InitTestCtx initializes a test context with an in-memory database
// and a temporary directory for all paths
func InitTestCtx(t *testing.T) DnoteCtx {
paths := getDefaultTestPaths(t)
db := database.InitTestMemoryDB(t)
- createTestDirectories(t, paths)
+
+ if err := InitDnoteDirs(paths); err != nil {
+ t.Fatal(errors.Wrap(err, "creating test directories"))
+ }
return DnoteCtx{
DB: db,
@@ -81,7 +62,10 @@ func InitTestCtx(t *testing.T) DnoteCtx {
// Used when you need full control over database initialization (e.g. migration tests).
func InitTestCtxWithDB(t *testing.T, db *database.DB) DnoteCtx {
paths := getDefaultTestPaths(t)
- createTestDirectories(t, paths)
+
+ if err := InitDnoteDirs(paths); err != nil {
+ t.Fatal(errors.Wrap(err, "creating test directories"))
+ }
return DnoteCtx{
DB: db,
@@ -94,7 +78,10 @@ func InitTestCtxWithDB(t *testing.T, db *database.DB) DnoteCtx {
// at the expected path.
func InitTestCtxWithFileDB(t *testing.T) DnoteCtx {
paths := getDefaultTestPaths(t)
- createTestDirectories(t, paths)
+
+ if err := InitDnoteDirs(paths); err != nil {
+ t.Fatal(errors.Wrap(err, "creating test directories"))
+ }
dbPath := filepath.Join(paths.Data, consts.DnoteDirName, consts.DnoteDBFileName)
db, err := database.Open(dbPath)
@@ -106,7 +93,6 @@ func InitTestCtxWithFileDB(t *testing.T) DnoteCtx {
t.Fatal(errors.Wrap(err, "running schema sql"))
}
- database.MarkMigrationComplete(t, db)
t.Cleanup(func() { db.Close() })
return DnoteCtx{
diff --git a/pkg/cli/database/models_test.go b/pkg/cli/database/models_test.go
index 28bc2212..53565d7b 100644
--- a/pkg/cli/database/models_test.go
+++ b/pkg/cli/database/models_test.go
@@ -110,7 +110,6 @@ func TestNoteInsert(t *testing.T) {
func() {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
n := Note{
UUID: tc.uuid,
@@ -244,7 +243,6 @@ func TestNoteUpdate(t *testing.T) {
func() {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
n1 := Note{
UUID: tc.uuid,
@@ -267,8 +265,8 @@ func TestNoteUpdate(t *testing.T) {
Dirty: false,
}
- MustExec(t, fmt.Sprintf("inserting n1 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n1.UUID, n1.BookUUID, n1.USN, n1.AddedOn, n1.EditedOn, n1.Body, n1.Deleted, n1.Dirty)
- MustExec(t, fmt.Sprintf("inserting n2 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n2.UUID, n2.BookUUID, n2.USN, n2.AddedOn, n2.EditedOn, n2.Body, n2.Deleted, n2.Dirty)
+ MustExec(t, fmt.Sprintf("inserting n1 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n1.UUID, n1.BookUUID, n1.USN, n1.AddedOn, n1.EditedOn, n1.Body, n1.Deleted, n1.Dirty)
+ MustExec(t, fmt.Sprintf("inserting n2 for test case %d", idx), db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n2.UUID, n2.BookUUID, n2.USN, n2.AddedOn, n2.EditedOn, n2.Body, n2.Deleted, n2.Dirty)
// execute
tx, err := db.Begin()
@@ -336,7 +334,6 @@ func TestNoteUpdateUUID(t *testing.T) {
t.Run(fmt.Sprintf("testCase%d", idx), func(t *testing.T) {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
n1 := Note{
UUID: "n1-uuid",
@@ -391,7 +388,6 @@ func TestNoteUpdateUUID(t *testing.T) {
func TestNoteExpunge(t *testing.T) {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
n1 := Note{
UUID: "n1-uuid",
@@ -414,8 +410,8 @@ func TestNoteExpunge(t *testing.T) {
Dirty: false,
}
- MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n1.UUID, n1.BookUUID, n1.USN, n1.AddedOn, n1.EditedOn, n1.Body, n1.Deleted, n1.Dirty)
- MustExec(t, "inserting n2", db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n2.UUID, n2.BookUUID, n2.USN, n2.AddedOn, n2.EditedOn, n2.Body, n2.Deleted, n2.Dirty)
+ MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n1.UUID, n1.BookUUID, n1.USN, n1.AddedOn, n1.EditedOn, n1.Body, n1.Deleted, n1.Dirty)
+ MustExec(t, "inserting n2", db, "INSERT INTO notes (uuid, book_uuid, usn, added_on, edited_on, body, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n2.UUID, n2.BookUUID, n2.USN, n2.AddedOn, n2.EditedOn, n2.Body, n2.Deleted, n2.Dirty)
// execute
tx, err := db.Begin()
@@ -514,7 +510,6 @@ func TestBookInsert(t *testing.T) {
func() {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
b := Book{
UUID: tc.uuid,
@@ -595,7 +590,6 @@ func TestBookUpdate(t *testing.T) {
func() {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
b1 := Book{
UUID: "b1-uuid",
@@ -674,7 +668,6 @@ func TestBookUpdateUUID(t *testing.T) {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
b1 := Book{
UUID: "b1-uuid",
@@ -725,7 +718,6 @@ func TestBookUpdateUUID(t *testing.T) {
func TestBookExpunge(t *testing.T) {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
b1 := Book{
UUID: "b1-uuid",
@@ -780,7 +772,6 @@ func TestBookExpunge(t *testing.T) {
func TestNoteFTS(t *testing.T) {
// set up
db := InitTestMemoryDB(t)
- defer db.Close()
// execute - insert
n := Note{
diff --git a/pkg/cli/database/queries_test.go b/pkg/cli/database/queries_test.go
index d3c2793e..cccc697f 100644
--- a/pkg/cli/database/queries_test.go
+++ b/pkg/cli/database/queries_test.go
@@ -48,7 +48,6 @@ func TestInsertSystem(t *testing.T) {
t.Run(fmt.Sprintf("insert %s %s", tc.key, tc.val), func(t *testing.T) {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
// execute
tx, err := db.Begin()
@@ -96,7 +95,6 @@ func TestUpsertSystem(t *testing.T) {
t.Run(fmt.Sprintf("insert %s %s", tc.key, tc.val), func(t *testing.T) {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
MustExec(t, "inserting a system configuration", db, "INSERT INTO system (key, value) VALUES (?, ?)", "baz", "quz")
@@ -135,7 +133,6 @@ func TestGetSystem(t *testing.T) {
t.Run(fmt.Sprintf("get string value"), func(t *testing.T) {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
// execute
MustExec(t, "inserting a system configuration", db, "INSERT INTO system (key, value) VALUES (?, ?)", "foo", "bar")
@@ -158,7 +155,6 @@ func TestGetSystem(t *testing.T) {
t.Run(fmt.Sprintf("get int64 value"), func(t *testing.T) {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
// execute
MustExec(t, "inserting a system configuration", db, "INSERT INTO system (key, value) VALUES (?, ?)", "foo", 1234)
@@ -199,7 +195,6 @@ func TestUpdateSystem(t *testing.T) {
t.Run(fmt.Sprintf("update %s %s", tc.key, tc.val), func(t *testing.T) {
// Setup
db := InitTestMemoryDB(t)
- defer db.Close()
MustExec(t, "inserting a system configuration", db, "INSERT INTO system (key, value) VALUES (?, ?)", "foo", "fuz")
MustExec(t, "inserting a system configuration", db, "INSERT INTO system (key, value) VALUES (?, ?)", "baz", "quz")
@@ -239,7 +234,6 @@ func TestGetActiveNote(t *testing.T) {
t.Run("not deleted", func(t *testing.T) {
// set up
db := InitTestMemoryDB(t)
- defer db.Close()
n1UUID := "n1-uuid"
MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, body, added_on, edited_on, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n1UUID, "b1-uuid", "n1 content", 1542058875, 1542058876, 1, false, true)
@@ -268,7 +262,6 @@ func TestGetActiveNote(t *testing.T) {
t.Run("deleted", func(t *testing.T) {
// set up
db := InitTestMemoryDB(t)
- defer db.Close()
n1UUID := "n1-uuid"
MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, body, added_on, edited_on, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", n1UUID, "b1-uuid", "n1 content", 1542058875, 1542058876, 1, true, true)
@@ -292,7 +285,6 @@ func TestGetActiveNote(t *testing.T) {
func TestUpdateNoteContent(t *testing.T) {
// set up
db := InitTestMemoryDB(t)
- defer db.Close()
uuid := "n1-uuid"
MustExec(t, "inserting n1", db, "INSERT INTO notes (uuid, book_uuid, body, added_on, edited_on, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", uuid, "b1-uuid", "n1 content", 1542058875, 0, 1, false, false)
@@ -324,7 +316,6 @@ func TestUpdateNoteContent(t *testing.T) {
func TestUpdateNoteBook(t *testing.T) {
// set up
db := InitTestMemoryDB(t)
- defer db.Close()
b1UUID := "b1-uuid"
b2UUID := "b2-uuid"
@@ -361,7 +352,6 @@ func TestUpdateNoteBook(t *testing.T) {
func TestUpdateBookName(t *testing.T) {
// set up
db := InitTestMemoryDB(t)
- defer db.Close()
b1UUID := "b1-uuid"
MustExec(t, "inserting b1", db, "INSERT INTO books (uuid, label, usn, deleted, dirty) VALUES (?, ?, ?, ?, ?)", b1UUID, "b1-label", 8, false, false)
diff --git a/pkg/cli/database/schema.sql b/pkg/cli/database/schema.sql
index 655148c8..9a9de094 100644
--- a/pkg/cli/database/schema.sql
+++ b/pkg/cli/database/schema.sql
@@ -34,3 +34,7 @@ CREATE TRIGGER notes_after_update AFTER UPDATE ON notes BEGIN
INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
END;
+
+-- Migration version data.
+INSERT INTO system (key, value) VALUES ('schema', 14);
+INSERT INTO system (key, value) VALUES ('remote_schema', 1);
diff --git a/pkg/cli/database/schema/main.go b/pkg/cli/database/schema/main.go
index e4e219de..2596c84a 100644
--- a/pkg/cli/database/schema/main.go
+++ b/pkg/cli/database/schema/main.go
@@ -26,6 +26,7 @@ import (
"strings"
"github.com/dnote/dnote/pkg/cli/config"
+ "github.com/dnote/dnote/pkg/cli/consts"
"github.com/dnote/dnote/pkg/cli/context"
"github.com/dnote/dnote/pkg/cli/database"
"github.com/dnote/dnote/pkg/cli/infra"
@@ -118,7 +119,12 @@ func generateSchema(tmpDir string) (string, error) {
return "", fmt.Errorf("extracting schema: %w", err)
}
- return schema, nil
+ // Add INSERT statements for migration versions.
+ systemData := "\n-- Migration version data.\n"
+ systemData += fmt.Sprintf("INSERT INTO system (key, value) VALUES ('%s', %d);\n", consts.SystemSchema, len(migrate.LocalSequence))
+ systemData += fmt.Sprintf("INSERT INTO system (key, value) VALUES ('%s', %d);\n", consts.SystemRemoteSchema, len(migrate.RemoteSequence))
+
+ return schema + systemData, nil
}
// extractSchema extracts the complete schema by querying sqlite_master
diff --git a/pkg/cli/database/schema/main_test.go b/pkg/cli/database/schema/main_test.go
index acc46654..24bd9fc0 100644
--- a/pkg/cli/database/schema/main_test.go
+++ b/pkg/cli/database/schema/main_test.go
@@ -26,6 +26,7 @@ import (
"testing"
"github.com/dnote/dnote/pkg/assert"
+ "github.com/dnote/dnote/pkg/cli/consts"
)
func TestRun(t *testing.T) {
@@ -73,4 +74,11 @@ func TestRun(t *testing.T) {
// Verify schema does not contain sqlite internal tables
assert.Equal(t, strings.Contains(schema, "sqlite_sequence"), false, "schema should not contain sqlite_sequence")
+
+ // Verify system key-value pairs for schema versions are present
+ expectedSchemaKey := fmt.Sprintf("INSERT INTO system (key, value) VALUES ('%s',", consts.SystemSchema)
+ assert.Equal(t, strings.Contains(schema, expectedSchemaKey), true, "schema should contain schema version INSERT statement")
+
+ expectedRemoteSchemaKey := fmt.Sprintf("INSERT INTO system (key, value) VALUES ('%s',", consts.SystemRemoteSchema)
+ assert.Equal(t, strings.Contains(schema, expectedRemoteSchemaKey), true, "schema should contain remote_schema version INSERT statement")
}
diff --git a/pkg/cli/database/test_embed.go b/pkg/cli/database/test_embed.go
deleted file mode 100644
index c2fe08e9..00000000
--- a/pkg/cli/database/test_embed.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package database
-
-import (
- "testing"
-)
-
-func TestSchemaEmbed(t *testing.T) {
- db := InitTestMemoryDB(t)
- defer db.Close()
-
- // Try to insert a book to verify schema is loaded
- _, err := db.Exec("INSERT INTO books (uuid, label) VALUES (?, ?)", "test-uuid", "test-label")
- if err != nil {
- t.Fatalf("Failed to insert into books: %v", err)
- }
-
- // Verify it was inserted
- var label string
- err = db.QueryRow("SELECT label FROM books WHERE uuid = ?", "test-uuid").Scan(&label)
- if err != nil {
- t.Fatalf("Failed to query books: %v", err)
- }
- if label != "test-label" {
- t.Fatalf("Expected label 'test-label', got '%s'", label)
- }
- t.Log("Schema embed test passed!")
-}
diff --git a/pkg/cli/database/testutils.go b/pkg/cli/database/testutils.go
index 602a48ba..16685686 100644
--- a/pkg/cli/database/testutils.go
+++ b/pkg/cli/database/testutils.go
@@ -58,9 +58,7 @@ func MustExec(t *testing.T, message string, db *DB, query string, args ...interf
// InitTestMemoryDB initializes an in-memory test database with the default schema.
func InitTestMemoryDB(t *testing.T) *DB {
- db := InitTestMemoryDBRaw(t, "")
- MarkMigrationComplete(t, db)
- return db
+ return InitTestMemoryDBRaw(t, "")
}
// InitTestFileDB initializes a file-based test database with the default schema.
@@ -81,8 +79,6 @@ func InitTestFileDBRaw(t *testing.T, dbPath string) *DB {
t.Fatal(errors.Wrap(err, "running schema sql"))
}
- MarkMigrationComplete(t, db)
-
t.Cleanup(func() { db.Close() })
return db
}
@@ -125,16 +121,6 @@ func OpenTestDB(t *testing.T, dnoteDir string) *DB {
return db
}
-// MarkMigrationComplete marks all migrations as complete in the database
-func MarkMigrationComplete(t *testing.T, db *DB) {
- if _, err := db.Exec("INSERT INTO system (key, value) VALUES (? , ?);", consts.SystemSchema, 14); err != nil {
- t.Fatal(errors.Wrap(err, "inserting schema"))
- }
- if _, err := db.Exec("INSERT INTO system (key, value) VALUES (? , ?);", consts.SystemRemoteSchema, 1); err != nil {
- t.Fatal(errors.Wrap(err, "inserting remote schema"))
- }
-}
-
// mustGenerateTestUUID generates a UUID for test databases and fails the test on error
func mustGenerateTestUUID(t *testing.T) string {
uuid, err := utils.GenerateUUID()
diff --git a/pkg/cli/infra/init.go b/pkg/cli/infra/init.go
index fbff4267..1e572cf7 100644
--- a/pkg/cli/infra/init.go
+++ b/pkg/cli/infra/init.go
@@ -24,7 +24,6 @@ import (
"database/sql"
"fmt"
"os"
- "path/filepath"
"strconv"
"time"
@@ -329,37 +328,6 @@ func getEditorCommand() string {
return ret
}
-// initDir creates a directory if it doesn't exist
-func initDir(path string) error {
- ok, err := utils.FileExists(path)
- if err != nil {
- return errors.Wrapf(err, "checking if dir exists at %s", path)
- }
- if ok {
- return nil
- }
-
- if err := os.MkdirAll(path, 0755); err != nil {
- return errors.Wrapf(err, "creating a directory at %s", path)
- }
-
- return nil
-}
-
-// initDnoteDir initializes missing directories that Dnote uses
-func initDnoteDir(ctx context.DnoteCtx) error {
- if err := initDir(filepath.Join(ctx.Paths.Config, consts.DnoteDirName)); err != nil {
- return errors.Wrap(err, "initializing config dir")
- }
- if err := initDir(filepath.Join(ctx.Paths.Data, consts.DnoteDirName)); err != nil {
- return errors.Wrap(err, "initializing data dir")
- }
- if err := initDir(filepath.Join(ctx.Paths.Cache, consts.DnoteDirName)); err != nil {
- return errors.Wrap(err, "initializing cache dir")
- }
-
- return nil
-}
// initConfigFile populates a new config file if it does not exist yet
func initConfigFile(ctx context.DnoteCtx, apiEndpoint string) error {
@@ -395,7 +363,7 @@ func initConfigFile(ctx context.DnoteCtx, apiEndpoint string) error {
// initFiles creates, if necessary, the dnote directory and files inside
func initFiles(ctx context.DnoteCtx, apiEndpoint string) error {
- if err := initDnoteDir(ctx); err != nil {
+ if err := context.InitDnoteDirs(ctx.Paths); err != nil {
return errors.Wrap(err, "creating the dnote dir")
}
if err := initConfigFile(ctx, apiEndpoint); err != nil {
diff --git a/pkg/cli/infra/init_test.go b/pkg/cli/infra/init_test.go
index 401294d5..6ac0d5cf 100644
--- a/pkg/cli/infra/init_test.go
+++ b/pkg/cli/infra/init_test.go
@@ -33,7 +33,6 @@ import (
func TestInitSystemKV(t *testing.T) {
// Setup
db := database.InitTestMemoryDB(t)
- defer db.Close()
var originalCount int
database.MustScan(t, "counting system configs", db.QueryRow("SELECT count(*) FROM system"), &originalCount)
@@ -65,7 +64,6 @@ func TestInitSystemKV(t *testing.T) {
func TestInitSystemKV_existing(t *testing.T) {
// Setup
db := database.InitTestMemoryDB(t)
- defer db.Close()
database.MustExec(t, "inserting a system config", db, "INSERT INTO system (key, value) VALUES (?, ?)", "testKey", "testVal")
diff --git a/pkg/cli/migrate/migrate_test.go b/pkg/cli/migrate/migrate_test.go
index 4bb66057..591f1800 100644
--- a/pkg/cli/migrate/migrate_test.go
+++ b/pkg/cli/migrate/migrate_test.go
@@ -38,6 +38,14 @@ import (
"github.com/pkg/errors"
)
+// initTestDBNoMigration initializes a test database with schema.sql but removes
+// migration version data so tests can control the migration state themselves.
+func initTestDBNoMigration(t *testing.T) *database.DB {
+ db := database.InitTestMemoryDBRaw(t, "")
+ // Remove migration versions from schema.sql so tests can set their own
+ database.MustExec(t, "clearing schema versions", db, "DELETE FROM system WHERE key IN (?, ?)", consts.SystemSchema, consts.SystemRemoteSchema)
+ return db
+}
func TestExecute_bump_schema(t *testing.T) {
testCases := []struct {
@@ -54,7 +62,7 @@ func TestExecute_bump_schema(t *testing.T) {
for _, tc := range testCases {
func() {
// set up
- db := database.InitTestMemoryDBRaw(t, "")
+ db := initTestDBNoMigration(t)
ctx := context.InitTestCtxWithDB(t, db)
database.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", tc.schemaKey, 8)
@@ -108,7 +116,7 @@ func TestRun_nonfresh(t *testing.T) {
for _, tc := range testCases {
func() {
// set up
- db := database.InitTestMemoryDBRaw(t, "")
+ db := initTestDBNoMigration(t)
ctx := context.InitTestCtxWithDB(t, db)
database.MustExec(t, "inserting a schema", db, "INSERT INTO system (key, value) VALUES (?, ?)", tc.schemaKey, 2)
database.MustExec(t, "creating a temporary table for testing", db,
@@ -185,7 +193,7 @@ func TestRun_fresh(t *testing.T) {
for _, tc := range testCases {
func() {
// set up
- db := database.InitTestMemoryDBRaw(t, "")
+ db := initTestDBNoMigration(t)
ctx := context.InitTestCtxWithDB(t, db)
database.MustExec(t, "creating a temporary table for testing", db,
@@ -256,7 +264,7 @@ func TestRun_up_to_date(t *testing.T) {
for _, tc := range testCases {
func() {
// set up
- db := database.InitTestMemoryDBRaw(t, "")
+ db := initTestDBNoMigration(t)
ctx := context.InitTestCtxWithDB(t, db)
database.MustExec(t, "creating a temporary table for testing", db,
diff --git a/pkg/cli/utils/files.go b/pkg/cli/utils/files.go
index b4b2d1df..00e898ec 100644
--- a/pkg/cli/utils/files.go
+++ b/pkg/cli/utils/files.go
@@ -55,6 +55,24 @@ func FileExists(filepath string) (bool, error) {
return false, errors.Wrap(err, "getting file info")
}
+// EnsureDir creates a directory if it doesn't exist.
+// Returns nil if the directory already exists or was successfully created.
+func EnsureDir(path string) error {
+ ok, err := FileExists(path)
+ if err != nil {
+ return errors.Wrapf(err, "checking if dir exists at %s", path)
+ }
+ if ok {
+ return nil
+ }
+
+ if err := os.MkdirAll(path, 0755); err != nil {
+ return errors.Wrapf(err, "creating directory at %s", path)
+ }
+
+ return nil
+}
+
// CopyDir copies a directory from src to dest, recursively copying nested
// directories
func CopyDir(src, dest string) error {
diff --git a/pkg/cli/utils/files_test.go b/pkg/cli/utils/files_test.go
new file mode 100644
index 00000000..3c7c92bd
--- /dev/null
+++ b/pkg/cli/utils/files_test.go
@@ -0,0 +1,45 @@
+/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
+ *
+ * This file is part of Dnote.
+ *
+ * Dnote is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Dnote is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Dnote. If not, see .
+ */
+
+package utils
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/dnote/dnote/pkg/assert"
+)
+
+func TestEnsureDir(t *testing.T) {
+ tmpDir := t.TempDir()
+ testPath := filepath.Join(tmpDir, "test", "nested", "dir")
+
+ // Create directory
+ err := EnsureDir(testPath)
+ assert.Equal(t, err, nil, "EnsureDir should succeed")
+
+ // Verify it exists
+ info, err := os.Stat(testPath)
+ assert.Equal(t, err, nil, "directory should exist")
+ assert.Equal(t, info.IsDir(), true, "should be a directory")
+
+ // Call again on existing directory - should not error
+ err = EnsureDir(testPath)
+ assert.Equal(t, err, nil, "EnsureDir should succeed on existing directory")
+}