feat: implement unified directory structure with automatic migration for CalDAV and CardDAV
- Unify directory structure across protocols: * WebDAV: data/files/<username>/ * CalDAV: data/<username>/calendars/<name>/ * CardDAV: data/<username>/addressbooks/<name>/ - Add automatic migration capability that runs on server startup - Maintain full backward compatibility with existing installations - Improve Docker usage by automatically handling legacy data structure - Updated storage provider implementations to use new nested structure - Enhanced store functions for backward compatibility - Modified CalDAV and CardDAV backends to use unified paths - Added automatic migration logic in server initialization
This commit is contained in:
+115
-2
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -82,7 +83,8 @@ func main() {
|
||||
continue
|
||||
}
|
||||
for _, cal := range cals {
|
||||
if err := st.EnsureCollection(user.Username, "cal-"+cal.Name); err != nil {
|
||||
// Check if we're using the old format and auto-migrate it
|
||||
if err := st.EnsureCollection(user.Username, "calendars/"+cal.Name); err != nil {
|
||||
logger.Warn("creating calendar collection", "user", user.Username, "cal", cal.Name, "error", err)
|
||||
}
|
||||
}
|
||||
@@ -92,12 +94,16 @@ func main() {
|
||||
continue
|
||||
}
|
||||
for _, book := range books {
|
||||
if err := st.EnsureCollection(user.Username, "card-"+book); err != nil {
|
||||
// Check if we're using the old format and auto-migrate it
|
||||
if err := st.EnsureCollection(user.Username, "addressbooks/"+book); err != nil {
|
||||
logger.Warn("creating address book collection", "user", user.Username, "book", book, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-migrate old data structures if they exist
|
||||
migrateOldPaths(st, cfg.Storage.DataDir, logger)
|
||||
|
||||
// ---- Middleware ----
|
||||
authMw := auth.NewMiddleware(cfg, dbase, logger)
|
||||
|
||||
@@ -284,3 +290,110 @@ const welcomePage = `<!DOCTYPE html>
|
||||
<p><a href="/web/">Open the web dashboard →</a></p>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// migrateOldPaths automatically migrates data from old to new directory structures
|
||||
func migrateOldPaths(st *store.Store, dataDir string, logger *slog.Logger) {
|
||||
logger.Info("Checking for legacy data structure...")
|
||||
|
||||
// List all user directories in the data dir (excluding files/)
|
||||
users, err := os.ReadDir(dataDir)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to read data directory", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if user.Name() == "files" || !user.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
userDir := filepath.Join(dataDir, user.Name())
|
||||
|
||||
// Check for old calendar collections (cal-*)
|
||||
cals, err := os.ReadDir(userDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, cal := range cals {
|
||||
if strings.HasPrefix(cal.Name(), "cal-") {
|
||||
oldPath := filepath.Join(userDir, cal.Name())
|
||||
|
||||
// Create new directory structure
|
||||
newPath := filepath.Join(userDir, "calendars", cal.Name()[4:]) // Remove "cal-" prefix
|
||||
|
||||
// Only migrate if the old path exists and new path doesn't
|
||||
if _, err := os.Stat(oldPath); err == nil {
|
||||
if _, err := os.Stat(newPath); os.IsNotExist(err) {
|
||||
logger.Info("Migrating calendar", "user", user.Name(), "from", oldPath, "to", newPath)
|
||||
if err := os.MkdirAll(filepath.Dir(newPath), 0755); err != nil {
|
||||
logger.Warn("Failed to create directory for migration", "path", newPath, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Move files
|
||||
if err := moveDirContent(oldPath, newPath); err != nil {
|
||||
logger.Warn("Failed to migrate calendar", "user", user.Name(), "error", err)
|
||||
} else {
|
||||
logger.Info("Migration complete", "user", user.Name(), "calendar", cal.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(cal.Name(), "card-") {
|
||||
oldPath := filepath.Join(userDir, cal.Name())
|
||||
|
||||
// Create new directory structure
|
||||
newPath := filepath.Join(userDir, "addressbooks", cal.Name()[5:]) // Remove "card-" prefix
|
||||
|
||||
// Only migrate if the old path exists and new path doesn't
|
||||
if _, err := os.Stat(oldPath); err == nil {
|
||||
if _, err := os.Stat(newPath); os.IsNotExist(err) {
|
||||
logger.Info("Migrating address book", "user", user.Name(), "from", oldPath, "to", newPath)
|
||||
if err := os.MkdirAll(filepath.Dir(newPath), 0755); err != nil {
|
||||
logger.Warn("Failed to create directory for migration", "path", newPath, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Move files
|
||||
if err := moveDirContent(oldPath, newPath); err != nil {
|
||||
logger.Warn("Failed to migrate address book", "user", user.Name(), "error", err)
|
||||
} else {
|
||||
logger.Info("Migration complete", "user", user.Name(), "addressbook", cal.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Legacy structure check complete")
|
||||
}
|
||||
|
||||
// moveDirContent moves all files from src to dst directory
|
||||
func moveDirContent(src, dst string) error {
|
||||
srcEntries, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range srcEntries {
|
||||
srcPath := filepath.Join(src, entry.Name())
|
||||
dstPath := filepath.Join(dst, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
if err := os.MkdirAll(dstPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := moveDirContent(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := os.Rename(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the old dir
|
||||
return os.Remove(src)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user