The repositories moved off LINUXexpert-org. Here that is not a documentation change: the old organisation was the module path, so it is declared in go.mod and repeated in every internal import. Leaving it would have been worse than a stale link. GitHub redirects the repository, but a go.mod whose module line disagrees with the path it was fetched from is an error rather than a redirect, so `go get` on the new address would have failed against the old declaration. go.mod, 34 files of imports, and the repository links in README and ARCHITECTURE. go mod tidy leaves go.sum untouched -- no dependency moved, only our own path.
72 lines
2.6 KiB
Go
72 lines
2.6 KiB
Go
// SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"github.com/Coffey-Labs/stalwart-migrator/internal/backup"
|
|
"github.com/Coffey-Labs/stalwart-migrator/internal/checkpoint"
|
|
)
|
|
|
|
// preservePlan lifts the run's irreplaceable inputs out of the scratch
|
|
// directory into the run's state directory, which is never cleaned up, and
|
|
// records each as a checkpoint artifact.
|
|
//
|
|
// These four are not intermediate files. The settings and principals dumps
|
|
// can only be taken from a live pre-migration instance, and the apply plan
|
|
// and its supplement are what was actually replayed into the store - so
|
|
// after cutover there is no way to produce any of them again. Until this
|
|
// existed they lived only in --work-dir, which a successful run deletes,
|
|
// and `rehearse` kept more of its conclusions than `run` did: the
|
|
// read-only command preserved the plan and the destructive one threw it
|
|
// away.
|
|
//
|
|
// What made that concrete: an operator who booted recovery mode again
|
|
// after a completed migration, for an unrelated reason, and found Domain
|
|
// and Account queries coming back empty. Re-applying the original run's
|
|
// export.json and supplement.json against a fresh recovery boot is what
|
|
// got their server back, twice, on two different machines - and they had
|
|
// them only because they had passed --keep-artifacts. Nobody should need
|
|
// to have guessed that in advance. Reported by @kaya-eu in #1.
|
|
//
|
|
// A file that isn't there is skipped rather than failing the step: a patch
|
|
// bump converts nothing, and a supplement that couldn't be generated is
|
|
// already a warning of its own.
|
|
func preservePlan(stateDir string, files map[string]string, rs *checkpoint.RunState) ([]string, error) {
|
|
if stateDir == "" {
|
|
return nil, fmt.Errorf("no state directory to preserve the run's plan in")
|
|
}
|
|
names := make([]string, 0, len(files))
|
|
for name := range files {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
var kept []string
|
|
for _, name := range names {
|
|
src := files[name]
|
|
if _, err := os.Stat(src); err != nil {
|
|
continue
|
|
}
|
|
dst := filepath.Join(stateDir, filepath.Base(src))
|
|
if err := copyFile(src, dst); err != nil {
|
|
return nil, fmt.Errorf("preserve %s as %s: %w", src, dst, err)
|
|
}
|
|
sum, size, err := backup.HashFile(dst)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hash %s: %w", dst, err)
|
|
}
|
|
rs.RecordArtifact(name, checkpoint.Artifact{Path: dst, SHA256: sum, SizeBytes: size})
|
|
kept = append(kept, filepath.Base(dst))
|
|
}
|
|
if len(kept) == 0 {
|
|
return nil, fmt.Errorf("none of the run's plan files exist to preserve - the conversion produced nothing")
|
|
}
|
|
return kept, nil
|
|
}
|