Bug Description
GetSnapshot() in src/server/backup/rustic_local.go attempts to unmarshal rustic snapshots --json output into a flat []SnapshotInfo, but rustic outputs a grouped format since recent versions. This causes the snapshot ID field to always be empty, which makes CanDownload() return false, and the download endpoint returns HTTP 400.
Root Cause
rustic snapshots --json [id] outputs grouped format:
[
{
"group_key": { ... },
"snapshots": [
{ "id": "e38a66292dc4...", "time": "...", "paths": [...], ... }
]
}
]
But GetSnapshot() tries to unmarshal into []SnapshotInfo (flat format):
var snapshots []SnapshotInfo
if err := json.Unmarshal(output, &snapshots); err != nil {
return nil, errors.Wrap(err, "failed to parse snapshot info")
}
Because Go's json.Unmarshal silently ignores unknown fields, this does not return an error — it simply produces a slice where every field is the zero value. snapshots[0].ID is "", so CanDownload() returns false, and the download handler aborts with HTTP 400:
The requested backup cannot be downloaded.
The Inconsistency
ListSnapshots() in the same file correctly handles grouped format:
var results []struct {
GroupKey json.RawMessage `json:"group_key"`
Snapshots []SnapshotInfo `json:"snapshots"`
}
if err := json.Unmarshal(output, &results); err != nil {
return nil, errors.Wrap(err, "failed to parse snapshot list")
}
GetSnapshot() should be updated to use the same approach.
Suggested Fix
In src/server/backup/rustic_local.go, change GetSnapshot() to handle grouped format, mirroring ListSnapshots():
func (r *LocalRepository) GetSnapshot(ctx context.Context, id string) (*Snapshot, error) {
// ... existing code to run rustic ...
// Handle grouped format (same as ListSnapshots)
var grouped []struct {
GroupKey json.RawMessage `json:"group_key"`
Snapshots []SnapshotInfo `json:"snapshots"`
}
if err := json.Unmarshal(output, &grouped); err != nil {
return nil, errors.Wrap(err, "failed to parse snapshot info")
}
var info SnapshotInfo
found := false
for _, group := range grouped {
for _, s := range group.Snapshots {
if strings.HasPrefix(s.ID, id) {
info = s
found = true
break
}
}
if found {
break
}
}
if !found {
return nil, errors.New("snapshot not found")
}
// ... rest of existing code ...
}
Impact
- All rustic backup downloads fail with HTTP 400 for users on recent rustic versions that output grouped snapshot format.
ListSnapshots() works correctly (backups appear in the panel list), so the issue is not immediately obvious — backups look fine until you try to download them.
Workaround
Wrap the rustic binary with a script that flattens grouped output using jq:
if [[ "$@" == *"snapshots"* && "$@" == *"--json"* ]]; then
FLAT=$(echo "$OUTPUT" | jq 'if (type == "array") and (length > 0) and (.[0] | has("snapshots")) then [.[].snapshots[]] else . end')
printf '%s' "$FLAT"
fi
Environment
- Elytra v1.4.0
- rustic (recent version outputting grouped snapshot JSON)
- Panel: Pyrodactyl (Pterodactyl fork)
- Disk type:
rustic_local
Bug Description
GetSnapshot()insrc/server/backup/rustic_local.goattempts to unmarshalrustic snapshots --jsonoutput into a flat[]SnapshotInfo, but rustic outputs a grouped format since recent versions. This causes the snapshotIDfield to always be empty, which makesCanDownload()returnfalse, and the download endpoint returns HTTP 400.Root Cause
rustic snapshots --json [id]outputs grouped format:[ { "group_key": { ... }, "snapshots": [ { "id": "e38a66292dc4...", "time": "...", "paths": [...], ... } ] } ]But
GetSnapshot()tries to unmarshal into[]SnapshotInfo(flat format):Because Go's
json.Unmarshalsilently ignores unknown fields, this does not return an error — it simply produces a slice where every field is the zero value.snapshots[0].IDis"", soCanDownload()returnsfalse, and the download handler aborts with HTTP 400:The Inconsistency
ListSnapshots()in the same file correctly handles grouped format:GetSnapshot()should be updated to use the same approach.Suggested Fix
In
src/server/backup/rustic_local.go, changeGetSnapshot()to handle grouped format, mirroringListSnapshots():Impact
ListSnapshots()works correctly (backups appear in the panel list), so the issue is not immediately obvious — backups look fine until you try to download them.Workaround
Wrap the
rusticbinary with a script that flattens grouped output usingjq:Environment
rustic_local