Skip to content

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

@arcker

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.

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