package store import ( "fmt" "os" "path/filepath" "strings" ) // Migrate restructures the data directory from the old format to the new unified format. // It is idempotent and can be run multiple times safely. // // Old structure: // // data/files// // data//cal-/ // data//card-/ // // New structure: // // data//files/ // data//calendars// // data//addressbooks// func (s *Store) Migrate() error { users, err := s.listUserDirectories() if err != nil { return fmt.Errorf("listing user directories: %w", err) } for _, user := range users { if err := s.migrateUser(user); err != nil { return fmt.Errorf("migrating user %q: %w", user, err) } } return nil } // listUserDirectories returns all user directories in the data directory. // It looks for directories that are NOT the old "files" directory, // and also checks inside the old "files" directory for users who need migrating. func (s *Store) listUserDirectories() ([]string, error) { entries, err := os.ReadDir(s.rootDir) if err != nil { return nil, err } usersMap := make(map[string]bool) for _, e := range entries { if !e.IsDir() { continue } name := e.Name() // Skip the old files directory (will handle users inside it separately) if name == "files" { continue } usersMap[name] = true } // Also check the old files directory for users filesDir := filepath.Join(s.rootDir, "files") if filesEntries, err := os.ReadDir(filesDir); err == nil { for _, e := range filesEntries { if e.IsDir() { usersMap[e.Name()] = true } } } var users []string for user := range usersMap { users = append(users, user) } return users, nil } // migrateUser migrates a single user's data from old to new structure. func (s *Store) migrateUser(user string) error { userDir := filepath.Join(s.rootDir, user) // Create user directory if it doesn't exist (needed for WebDAV migration) if err := os.MkdirAll(userDir, 0o755); err != nil { return fmt.Errorf("creating user directory %q: %w", userDir, err) } // Migrate WebDAV files: files/ -> /files if err := s.migrateWebDAV(user); err != nil { return err } // Migrate CalDAV calendars: cal- -> calendars/ if err := s.migrateCalendars(user, userDir); err != nil { return err } // Migrate CardDAV address books: card- -> addressbooks/ if err := s.migrateAddressBooks(user, userDir); err != nil { return err } return nil } // migrateWebDAV moves the old files/ directory to /files. func (s *Store) migrateWebDAV(user string) error { oldPath := filepath.Join(s.rootDir, "files", user) newPath := filepath.Join(s.rootDir, user, "files") // Check if old directory exists and new doesn't if _, err := os.Stat(oldPath); os.IsNotExist(err) { return nil } if _, err := os.Stat(newPath); err == nil { return nil // Already migrated } // Create parent directory if needed if err := os.MkdirAll(filepath.Dir(newPath), 0o755); err != nil { return fmt.Errorf("creating directory %q: %w", filepath.Dir(newPath), err) } // Move the directory if err := os.Rename(oldPath, newPath); err != nil { return fmt.Errorf("moving %q to %q: %w", oldPath, newPath, err) } return nil } // migrateCalendars moves old cal- directories to calendars/. func (s *Store) migrateCalendars(user string, userDir string) error { entries, err := os.ReadDir(userDir) if err != nil { return fmt.Errorf("reading user directory %q: %w", userDir, err) } var calendars []string for _, e := range entries { if e.IsDir() && strings.HasPrefix(e.Name(), "cal-") { calendars = append(calendars, e.Name()) } } if len(calendars) == 0 { return nil } calendarsDir := filepath.Join(userDir, "calendars") if err := os.MkdirAll(calendarsDir, 0o755); err != nil { return fmt.Errorf("creating calendars directory %q: %w", calendarsDir, err) } for _, calName := range calendars { oldPath := filepath.Join(userDir, calName) newPath := filepath.Join(calendarsDir, strings.TrimPrefix(calName, "cal-")) if err := os.Rename(oldPath, newPath); err != nil { return fmt.Errorf("moving calendar %q: %w", calName, err) } } return nil } // migrateAddressBooks moves old card- directories to addressbooks/. func (s *Store) migrateAddressBooks(user string, userDir string) error { entries, err := os.ReadDir(userDir) if err != nil { return fmt.Errorf("reading user directory %q: %w", userDir, err) } var addressBooks []string for _, e := range entries { if e.IsDir() && strings.HasPrefix(e.Name(), "card-") { addressBooks = append(addressBooks, e.Name()) } } if len(addressBooks) == 0 { return nil } addressBooksDir := filepath.Join(userDir, "addressbooks") if err := os.MkdirAll(addressBooksDir, 0o755); err != nil { return fmt.Errorf("creating addressbooks directory %q: %w", addressBooksDir, err) } for _, bookName := range addressBooks { oldPath := filepath.Join(userDir, bookName) newPath := filepath.Join(addressBooksDir, strings.TrimPrefix(bookName, "card-")) if err := os.Rename(oldPath, newPath); err != nil { return fmt.Errorf("moving address book %q: %w", bookName, err) } } return nil }