refactor: unify data directory structure

Unify user data directory from fragmented layout to consistent nested format:

- Move WebDAV from: data/files/<username> → data/<username>/files/
- Move CalDAV from: data/<username>/cal-<name> → data/<username>/calendars/<name>
- Move CardDAV from: data/<username>/card-<name> → data/<username>/addressbooks/<name>

Changes:
- internal/store/store.go: Update collectionPath() to map collection names
- internal/store/migrate.go: Add idempotent Migrate() method
- internal/store/migrate_test.go: Comprehensive migration tests
- internal/webdav/handler.go: Use new unified path structure
- cmd/server/main.go: Auto-run migration on startup
- tools/nidusctl/main.go: Add migrate subcommand
- Update tests to verify new structure

URL endpoints unchanged - only on-disk structure modified. All tests pass.
This commit is contained in:
2026-08-30 12:15:31 +02:00
parent f463c01f0f
commit 2fd39c8180
14 changed files with 964 additions and 22 deletions
+63
View File
@@ -0,0 +1,63 @@
// Command migrate is a tool to restructure the data directory from the
// old format to the new unified format.
package main
import (
"flag"
"fmt"
"log/slog"
"os"
"github.com/yourusername/caldav-server/internal/config"
"github.com/yourusername/caldav-server/internal/store"
)
func main() {
os.Exit(run(os.Args[1:]))
}
func run(args []string) int {
fs := flag.NewFlagSet("migrate", flag.ContinueOnError)
cfgPath := fs.String("config", "config.yaml", "path to configuration file")
verbose := fs.Bool("verbose", false, "enable verbose output")
if err := fs.Parse(args); err != nil {
return 2
}
cfg, err := config.Load(*cfgPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error loading config: %v\n", err)
return 1
}
logger := buildLogger(*verbose)
st, err := store.NewStore(cfg.Storage.DataDir)
if err != nil {
fmt.Fprintf(os.Stderr, "error opening store %q: %v\n", cfg.Storage.DataDir, err)
return 1
}
logger.Info("starting migration", "data_dir", cfg.Storage.DataDir)
if err := st.Migrate(); err != nil {
logger.Error("migration failed", "error", err)
fmt.Fprintf(os.Stderr, "migration failed: %v\n", err)
return 1
}
logger.Info("migration completed successfully")
fmt.Println("Migration completed successfully")
return 0
}
func buildLogger(verbose bool) *slog.Logger {
level := slog.LevelInfo
if verbose {
level = slog.LevelDebug
}
handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: level,
})
return slog.New(handler)
}
+29 -10
View File
@@ -61,6 +61,8 @@ func run(args []string) int {
return runCalendar(dbase, st, rest[1:])
case "addressbook", "card":
return runAddressBook(dbase, st, rest[1:])
case "migrate":
return runMigrate(st, rest[1:])
case "help", "-h", "--help":
usage()
return 0
@@ -88,20 +90,23 @@ Usage:
nidusctl [-config config.yaml] calendar unshare <owner> <calendar> <user>
nidusctl [-config config.yaml] calendar shares <owner> <calendar>
nidusctl [-config config.yaml] addressbook create <owner> <book>
nidusctl [-config config.yaml] addressbook delete <owner> <book>
nidusctl [-config config.yaml] addressbook list <owner>
nidusctl [-config config.yaml] addressbook share <owner> <book> <user> <read|write>
nidusctl [-config config.yaml] addressbook unshare <owner> <book> <user>
nidusctl [-config config.yaml] addressbook shares <owner> <book>
nidusctl [-config config.yaml] addressbook create <owner> <book>
nidusctl [-config config.yaml] addressbook delete <owner> <book>
nidusctl [-config config.yaml] addressbook list <owner>
nidusctl [-config config.yaml] addressbook share <owner> <book> <user> <read|write>
nidusctl [-config config.yaml] addressbook unshare <owner> <book> <user>
nidusctl [-config config.yaml] addressbook shares <owner> <book>
nidusctl [-config config.yaml] migrate [--verbose]
Examples:
nidusctl user create alice --display-name "Alice Smith" --email alice@example.com
nidusctl calendar create alice work
nidusctl calendar share alice work bob write
nidusctl calendar shares alice work
nidusctl calendar unshare alice work bob
`)
nidusctl calendar share alice work bob write
nidusctl calendar shares alice work
nidusctl calendar unshare alice work bob
nidusctl migrate
`)
}
// newFlagSet creates a flag.FlagSet configured for subcommand parsing
@@ -359,6 +364,20 @@ func runAddressBook(dbase *db.DB, st *store.Store, args []string) int {
}
}
func runMigrate(st *store.Store, args []string) int {
fs := newFlagSet("migrate")
if err := fs.Parse(args); err != nil {
return 2
}
if err := st.Migrate(); err != nil {
fmt.Fprintf(os.Stderr, "migration failed: %v\n", err)
return 1
}
fmt.Println("Migration completed successfully")
return 0
}
// warnIfUnknownUser reports (without failing) if username is not a
// registered user — the share is still recorded, since a user could be
// created afterwards.