release / web (push) Failing after 15s
release / govulncheck (push) Successful in 16s
release / go (push) Failing after 41s
release / integration (push) Failing after 3m29s
release / android (push) Successful in 4m7s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / Build signed APK (push) Canceled after 5m34s
Admin › Backups shows a verdict first (off, not set up, waiting, backed up, overdue, failing since when), Back up now, Test destination and the latest runs, with the settings beside them. - backup.Service: one run at a time, bounded at 30 minutes; the schedule ticks every 5 minutes, catches up a slot missed while the server was down, and retries a failure after an hour and then after as long as it has been failing. Runs are recorded in backup_runs, pruned after 90 days; a run cut off by a shutdown is marked failed at the next start. - A folder destination writes database/minstrel-<UTC stamp>.tar and refuses a folder that isn't there. An S3 destination stages the file, hashes it, and uploads it under database/ through a throttled reader (4 MiB/s by default, 0 for no limit). - Test destination checks the form as typed and only lists. - The bucket secret is sealed like every stored credential; backup_settings and backup_runs are left out of the backup itself. - The panel polls only while a run is going, capped at the run limit plus five minutes. The music switch named in the task is left to #6774, so no control ships that does nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
7.9 KiB
Go
197 lines
7.9 KiB
Go
// Package backup reads Minstrel's irreplaceable data out of Postgres as JSON
|
|
// Lines, one table at a time, and loads it back into an empty instance.
|
|
//
|
|
// What goes in is everything a person made or decided: accounts, likes, play
|
|
// and skip history, playlists, follows, wants and requests, download history,
|
|
// admin decisions, and settings. What stays out is everything the restored
|
|
// server rebuilds by itself: what it computes from the music files, what it
|
|
// fetches again from MusicBrainz, ListenBrainz and the other providers, and
|
|
// logs of past runs. Excluded says why for each, and the coverage test fails
|
|
// on a table that is in neither list.
|
|
package backup
|
|
|
|
// Table is one table a backup holds.
|
|
//
|
|
// Omit names columns left out of the file. They restore to their defaults:
|
|
// - a credential Minstrel stores sealed, which only the server holding the
|
|
// stream secret could open, so the restored server asks for it again;
|
|
// - a marker saying work on an excluded table is done (a tag source, an art
|
|
// source), so the restored server does that work again rather than
|
|
// believing it has the result.
|
|
type Table struct {
|
|
Name string
|
|
Omit []string
|
|
}
|
|
|
|
// Tables is every table a backup holds, in the order a restore loads them:
|
|
// each after every table it references. There are no self-referencing
|
|
// tables, so no table needs its rows loaded in depth order.
|
|
var Tables = []Table{
|
|
{Name: "users", Omit: []string{"subsonic_password", "listenbrainz_token"}},
|
|
|
|
// The library's identity. Every like, play and playlist entry hangs off a
|
|
// track id, so the rows travel even though a scan could rebuild most of
|
|
// their columns; the scan after a restore finds each file at its path and
|
|
// keeps its id.
|
|
{Name: "artists", Omit: []string{"artist_thumb_path", "artist_fanart_path", "artist_art_source", "artist_art_sources_version"}},
|
|
{Name: "albums", Omit: []string{"cover_art_path", "cover_art_source", "cover_art_sources_version"}},
|
|
{Name: "tracks", Omit: []string{"tag_source", "tag_sources_version"}},
|
|
|
|
// Settings.
|
|
{Name: "library_settings"},
|
|
{Name: "network_settings"},
|
|
{Name: "registration_settings"},
|
|
{Name: "smtp_config", Omit: []string{"password"}},
|
|
{Name: "notification_email_settings"},
|
|
{Name: "acoustid_settings", Omit: []string{"api_key"}},
|
|
{Name: "fingerprint_settings"},
|
|
{Name: "loudness_settings"},
|
|
{Name: "cover_art_provider_settings", Omit: []string{"api_key"}},
|
|
{Name: "cover_art_sources_meta"},
|
|
{Name: "tag_provider_settings", Omit: []string{"api_key"}},
|
|
{Name: "tag_sources_meta"},
|
|
{Name: "acquisition_services", Omit: []string{"api_key", "password"}},
|
|
{Name: "acquisition_settings"},
|
|
{Name: "reacquisition_settings"},
|
|
{Name: "discover_tuning"},
|
|
{Name: "taste_tuning"},
|
|
{Name: "recommendation_weight_profiles"},
|
|
{Name: "recommendation_tuning_audit"},
|
|
|
|
// Follows, wants, requests and downloads.
|
|
{Name: "release_type_profiles"},
|
|
{Name: "follows"},
|
|
{Name: "follow_seen_groups"},
|
|
{Name: "wants"},
|
|
{Name: "grabs"},
|
|
{Name: "download_interventions"},
|
|
{Name: "release_blocks"},
|
|
{Name: "requests"},
|
|
|
|
// Listening history and likes.
|
|
{Name: "play_sessions"},
|
|
{Name: "play_events"},
|
|
{Name: "skip_events"},
|
|
{Name: "scrobble_queue"},
|
|
{Name: "contextual_likes"},
|
|
{Name: "general_likes"},
|
|
{Name: "general_likes_albums"},
|
|
{Name: "general_likes_artists"},
|
|
|
|
// Playlists and what is built from history. The taste profile and the
|
|
// "you might like" rows are rebuilt nightly from the history above; they
|
|
// travel so a restored Home is full before that first nightly pass.
|
|
{Name: "playlists"},
|
|
{Name: "playlist_tracks"},
|
|
{Name: "system_playlist_runs"},
|
|
{Name: "system_playlist_rotation_state"},
|
|
{Name: "taste_profile_artists"},
|
|
{Name: "taste_profile_tags"},
|
|
{Name: "taste_profile_eras"},
|
|
{Name: "taste_profile_moods"},
|
|
{Name: "you_might_like_albums"},
|
|
{Name: "you_might_like_artists"},
|
|
|
|
// Per-user preferences and notifications.
|
|
{Name: "user_normalization_prefs"},
|
|
{Name: "user_notification_prefs"},
|
|
{Name: "user_notification_email_state"},
|
|
{Name: "user_notifications"},
|
|
{Name: "suggestion_snoozes"},
|
|
{Name: "quarantine"},
|
|
{Name: "quarantine_actions"},
|
|
|
|
// Admin decisions: an album taken off the wanted listing stays off, and a
|
|
// dismissed duplicate group stays dismissed when the sweep finds it again.
|
|
{Name: "library_album_matches"},
|
|
{Name: "duplicate_sweeps"},
|
|
{Name: "duplicate_groups"},
|
|
{Name: "duplicate_group_members"},
|
|
|
|
// The library change feed is how the apps sync, including a first sync,
|
|
// so a restored server carries it on from where the backup left it.
|
|
{Name: "library_changes"},
|
|
{Name: "audit_log"},
|
|
{Name: "playback_errors"},
|
|
}
|
|
|
|
// Excluded is every table a backup leaves out, with the reason.
|
|
var Excluded = map[string]string{
|
|
"schema_migrations": "the restored server's own migrations write it; the backup's schema version is in its manifest",
|
|
"sessions": "sign-ins; everyone signs in again on the restored server",
|
|
"password_resets": "one-time reset links, which expire within hours",
|
|
"user_invites": "open sign-up links; an admin sends an invite again from the restored server",
|
|
|
|
"track_fingerprints": "computed from the music files",
|
|
"track_loudness": "computed from the music files",
|
|
"album_loudness": "computed from the music files",
|
|
"track_acoustid_lookups": "looked up again from each track's fingerprint",
|
|
"track_tags": "fetched again from the tag providers",
|
|
"candidate_artist_tags": "fetched again from the tag providers",
|
|
"candidate_artist_tag_state": "fetched again from the tag providers",
|
|
|
|
"mb_artists": "MusicBrainz cache, fetched again",
|
|
"mb_release_groups": "MusicBrainz cache, fetched again",
|
|
"mb_releases": "MusicBrainz cache, fetched again",
|
|
"mb_release_tracks": "MusicBrainz cache, fetched again",
|
|
"mb_artist_release_groups": "MusicBrainz cache, fetched again",
|
|
|
|
"listenbrainz_similar_recordings": "similarity, fetched again from ListenBrainz",
|
|
"track_similarity": "similarity, fetched again from the similarity providers",
|
|
"track_similarity_fetches": "similarity, fetched again from the similarity providers",
|
|
"artist_similarity": "similarity, fetched again from the similarity providers",
|
|
"artist_similarity_fetches": "similarity, fetched again from the similarity providers",
|
|
"artist_similarity_unmatched": "similarity, fetched again from the similarity providers",
|
|
|
|
"want_candidates": "the last search's results; the next search finds them again",
|
|
"scan_runs": "a log of past library scans",
|
|
"diagnostic_events": "debug-mode diagnostics",
|
|
|
|
"backup_settings": "holds the bucket's secret key; point the restored server at its destination again",
|
|
"backup_runs": "this server's own backup history",
|
|
}
|
|
|
|
// carried reports whether a table is in Tables.
|
|
func carried(name string) bool {
|
|
for _, t := range Tables {
|
|
if t.Name == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Coverage checks a schema's tables against Tables and Excluded. It returns
|
|
// one line per problem: a table in neither list, in both, or listed twice,
|
|
// and a listed table the schema doesn't have.
|
|
func Coverage(schema []string) []string {
|
|
var problems []string
|
|
inSchema := map[string]bool{}
|
|
for _, name := range schema {
|
|
inSchema[name] = true
|
|
_, excluded := Excluded[name]
|
|
switch {
|
|
case carried(name) && excluded:
|
|
problems = append(problems, name+": both backed up and excluded")
|
|
case !carried(name) && !excluded:
|
|
problems = append(problems, name+": neither backed up nor excluded; add it to Tables, or to Excluded with the reason")
|
|
}
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, t := range Tables {
|
|
if seen[t.Name] {
|
|
problems = append(problems, t.Name+": listed twice in Tables")
|
|
}
|
|
seen[t.Name] = true
|
|
if !inSchema[t.Name] {
|
|
problems = append(problems, t.Name+": in Tables but not in the schema")
|
|
}
|
|
}
|
|
for name := range Excluded {
|
|
if !inSchema[name] {
|
|
problems = append(problems, name+": in Excluded but not in the schema")
|
|
}
|
|
}
|
|
return problems
|
|
}
|