package server import ( "context" "errors" "fmt" "io/fs" "os" "path/filepath" "time" "cc.tiredsysadmin.keeporg/internal/secret" "cc.tiredsysadmin.keeporg/internal/sqldb" "cc.tiredsysadmin.keeporg/internal/store" ) // OpenStore opens the configured database, proves the master key is the one // it was written with, and loads the metadata. keeporgd -reset-admin uses it // too, so a reset goes through the same checks as the server. func OpenStore(cfg Config, kr *secret.Keyring) (*store.Store, *sqldb.DB, error) { cfg.Defaults() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() db, err := sqldb.Open(ctx, cfg.Database) if err != nil { return nil, nil, err } if err := db.CheckKey(ctx, kr.Derive("check")); err != nil { db.Close() return nil, nil, fmt.Errorf("database %s: %w", db.Where, err) } st, err := store.Open(ctx, db, kr.Derive("records")) if err != nil { db.Close() return nil, nil, fmt.Errorf("database %s: %w", db.Where, err) } return st, db, nil } // Close stops the store's watcher and closes the database. func (s *Server) Close() { s.st.Close() s.db.Close() } // legacyState and legacyBlobs are where servers before 0.11 kept everything, // in the data directory. const ( legacyState = "state.json" legacyBlobs = "blobs" ) // importLegacy moves the data of a server from before 0.11 into the // database, once: when the database is empty and the old files are there. // The payloads go first and the metadata last, in one transaction, so a // crash part-way leaves an empty store and the import simply runs again. // // The old files are renamed rather than deleted, so nothing is lost if the // import turns out wrong. They are not encrypted the way the database is — // state.json holds session tokens in the clear — so the log asks for them to // be deleted once the server is known to work. func (s *Server) importLegacy() error { statePath := filepath.Join(s.cfg.DataDir, legacyState) if _, err := os.Stat(statePath); errors.Is(err, fs.ErrNotExist) { return nil } if !s.st.Empty() { s.log.Printf("WARNING: %s is still there, but the database already holds data, "+ "so it was not imported again. Delete it, and %s, once you no longer need them.", statePath, filepath.Join(s.cfg.DataDir, legacyBlobs)) return nil } old, err := store.ReadLegacy(statePath) if err != nil { return err } n, err := s.blobs.ImportDir(filepath.Join(s.cfg.DataDir, legacyBlobs)) if err != nil { return err } if err := s.st.Replace(old); err != nil { return err } stamp := time.Now().UTC().Format("20060102-150405") var renamed []string for _, name := range []string{legacyState, legacyBlobs} { from := filepath.Join(s.cfg.DataDir, name) to := from + ".imported-" + stamp if err := os.Rename(from, to); err == nil { renamed = append(renamed, to) } } s.log.Printf("imported %d accounts, %d databases and %d stored versions from %s into %s", len(old.Users), len(old.Databases), n, s.cfg.DataDir, s.db.Where) s.log.Printf("the old files were kept as %v. They are not encrypted like the database "+ "(state.json holds session tokens): delete them once this server works", renamed) return nil } // DatabaseWhere is the database's location without any password, for logs. func (s *Server) DatabaseWhere() string { return s.db.Where }