Files
jcoffey-dev 12ec0c3fd4 Rename the module to the Coffey-Labs organisation
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.
2026-08-30 15:24:18 -07:00

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
}