Skip to content

BackupOptions.excludes.globs uses whitelist semantics — bare patterns silently produce empty snapshots #503

Description

@arcker

Summary

BackupOptions::excludes.globs is named like an exclude list but uses whitelist semantics inherited from ignore::overrides::OverrideBuilder. A bare pattern means "include only files matching this; exclude everything else", not "exclude files matching this".

This is a footgun because it fails silently — the backup completes without error, the snapshot exists, but it contains zero files.

Minimal reproducer

let mut opts = BackupOptions::default();
opts.excludes.globs = vec!["**/*.tmp".into()];

// open repo, indexed_ids, backup ...
let snap = repository.backup(&opts, &pathlist, snapshot_opts)?;
// snap.summary.total_files_processed == 0
// despite the source containing file1.txt, file2.txt, etc.

The user expected **/*.tmp to exclude .tmp files. Instead, all non-.tmp files were silently dropped.

Working form (current API)

To actually exclude .tmp files, prefix the pattern with !:

opts.excludes.globs = vec!["!**/*.tmp".into()];

Why it's harmful

  1. Field name implies the wrong meaning. Excludes::globs reads as "patterns to exclude", not "patterns to include exclusively".
  2. Silent failure. No error is raised; the snapshot is created but empty. Detection only happens via rustic ls <snapshot> or restore.
  3. Mismatch with restic conventions. restic uses --exclude with the inverse semantics (bare = exclude, ! = re-include). Users coming from restic fall straight into this.
  4. Underdocumented. The doc comment on Excludes::globs is /// Glob pattern to exclude/include without flagging that the default is include-only.

Suggestions (least to most invasive)

  1. Doc: explicitly state the whitelist semantics in the doc comment, with an example showing the ! prefix to actually exclude.
  2. Add a parallel field, e.g. Excludes::globs_blacklist: Vec<String>, where entries are auto-prefixed with ! in as_override().
  3. Invert the semantics to match the field name: bare = exclude, !pattern = include. This is a breaking change but matches user mental model and restic CLI.

Workaround

Downstream consumers can prefix ! to every user-provided pattern before assigning to excludes.globs. That's what we ended up doing in our project.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    S-triageStatus: Waiting for a maintainer to triage this issue/PR

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions