restic/restic · error

internal error

Error message

internal error

What it means

removeUnexpectedFiles implements restic's --delete behavior: it removes files in the restore target that are not part of the snapshot. The function asserts its own precondition with panic("internal error") when res.opts.Delete is false, because it must only run when the user explicitly asked for deletion. The only in-tree call site (restorer.go:467-468) is already wrapped in 'if res.opts.Delete', so through public APIs this panic is unreachable; it exists as a defense-in-depth invariant against internal misuse or future refactors.

Source

Thrown at internal/restorer/restorer.go:489

			}

			if node == nil {
				return nil
			}

			err := res.restoreNodeMetadataTo(node, target, location)
			if err == nil {
				res.opts.Progress.AddProgress(location, ActionDirRestored, 0, 0)
			}
			return err
		},
	})
	return restoredFileCount, err
}

func (res *Restorer) removeUnexpectedFiles(ctx context.Context, target, location string, expectedFilenames []string) error {
	if !res.opts.Delete {
		panic("internal error")
	}

	entries, err := fs.Readdirnames(fs.NewLocal(), target, fs.O_NOFOLLOW)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	} else if err != nil {
		return err
	}

	keep := map[string]struct{}{}
	for _, name := range expectedFilenames {
		keep[toComparableFilename(name)] = struct{}{}
	}

	for _, entry := range entries {
		if ctx.Err() != nil {
			return ctx.Err()
		}

View on GitHub (pinned to a80be1478a)

Solutions

  1. If you use the public API and want deletion, construct restorer.NewRestorer(repo, sn, restorer.Options{Delete: true}) so the guarded call path is legitimate
  2. If maintaining the internal code, keep the 'if res.opts.Delete' guard at every call site and treat the panic as a CI signal that the invariant broke
  3. Turn the precondition into a returned error in your fork instead of a panic if untrusted callers can reach it

Example fix

// before (internal/fork misuse)
res := restorer.NewRestorer(repo, sn, restorer.Options{Delete: false})
res.removeUnexpectedFiles(ctx, target, location, names) // panic: internal error

// after
res := restorer.NewRestorer(repo, sn, restorer.Options{Delete: true})
// deletion now happens via the normal RestoreTo flow
Defensive patterns

Strategy: validation

Validate before calling

// Public-API users: pass the option instead of calling internals
opts := restorer.Options{Delete: true}
res := restorer.NewRestorer(repo, sn, opts)

// Fork maintainers, before an internal call:
if !res.opts.Delete {
    return errors.New("refusing to delete unexpected files: Delete option not set")
}

Prevention

When it happens

Trigger: Calling removeUnexpectedFiles (unexported, so only from within package restorer or a fork) on a Restorer built with Options{Delete: false}; a refactor that removes or bypasses the 'if res.opts.Delete' guard at the restoreNominalDir call site; a fork adding a dry-run or filtered path that reaches the function without the flag.

Common situations: Maintaining a restic fork that restructures restore traversal; backporting the delete feature onto an older code path; adding a new restore mode that forgets to check Delete before cleanup. End users running released restic binaries should never see it.

Related errors


AI-assisted analysis of restic/restic@a80be1478a (2026-08-15). Data as JSON: /api/errors/357c2402783d1b45. Report an issue: GitHub.