restic — Interfaces#

Interface catalog#

Backend#

  • Package: github.com/restic/restic/internal/backend
  • File: internal/backend/backend.go:19
  • Methods:
    Properties() Properties
    Hasher() hash.Hash
    Remove(ctx context.Context, h Handle) error
    Close() error
    Save(ctx context.Context, h Handle, rd RewindReader) error
    Load(ctx context.Context, h Handle, length int, offset int64, fn func(rd io.Reader) error) error
    Stat(ctx context.Context, h Handle) (FileInfo, error)
    List(ctx context.Context, t FileType, fn func(FileInfo) error) error
    IsNotExist(err error) bool
    IsPermanentError(err error) bool
    Delete(ctx context.Context) error
    Warmup(ctx context.Context, h []Handle) ([]Handle, error)
    WarmupWait(ctx context.Context, h []Handle) error
  • Purpose: Raw object-storage contract. Abstracts all durable persistence: save a named binary blob, load it back (with offset support for partial reads), enumerate blobs by file type, remove them. The Load callback design (fn func(io.Reader) error) allows retrying reads without re-allocating — callers may be invoked multiple times.
  • Implementations: local, sftp, rest, s3, azure, gs (GCS), b2, swift, rclone storage drivers; cache.Backend, retry.Backend, logger.Backend, sema.Backend, limiter.Backend, dryrun.Backend decorator wrappers; mock.Backend, mem.Backend (testing).
  • Design quality: Well-segregated for its role. 13 methods is on the larger side, but each method corresponds to a distinct storage primitive. The Warmup/WarmupWait pair is notably forward-looking (supporting cold-to-hot storage tiering like AWS Glacier). The IsPermanentError/IsNotExist error classification methods allow the retry decorator to make intelligent decisions without inspecting implementation-specific error types. Follows ISP at the use-site by extracting smaller sub-interfaces (Lister, LoaderUnpacked) for components that only need a subset.

Repository#

  • Package: github.com/restic/restic/internal/restic
  • File: internal/restic/repository.go:18
  • Methods:
    Connections() uint
    Config() Config
    PackSize() uint
    Key() *crypto.Key
    LoadIndex(ctx context.Context, p TerminalCounterFactory) error
    LookupBlob(t BlobType, id ID) []PackedBlob
    LookupBlobSize(t BlobType, id ID) (size uint, exists bool)
    NewAssociatedBlobSet() AssociatedBlobSet
    ListBlobs(ctx context.Context, fn func(PackedBlob)) error
    ListPacksFromIndex(ctx context.Context, packs IDSet) <-chan PackBlobs
    ListPack(ctx context.Context, id ID, packSize int64) (entries []Blob, hdrSize uint32, err error)
    LoadBlob(ctx context.Context, t BlobType, id ID, buf []byte) ([]byte, error)
    LoadBlobsFromPack(ctx context.Context, packID ID, blobs []Blob, handleBlobFn func(blob BlobHandle, buf []byte, err error) error) error
    WithBlobUploader(ctx context.Context, fn func(ctx context.Context, uploader BlobSaverWithAsync) error) error
    List(ctx context.Context, t FileType, fn func(ID, int64) error) error
    LoadRaw(ctx context.Context, t FileType, id ID) (data []byte, err error)
    LoadUnpacked(ctx context.Context, t FileType, id ID) (data []byte, err error)
    SaveUnpacked(ctx context.Context, t WriteableFileType, buf []byte) (ID, error)
    RemoveUnpacked(ctx context.Context, t WriteableFileType, id ID) error
    StartWarmup(ctx context.Context, packs IDSet) (WarmupJob, error)
  • Purpose: The complete high-level repository contract: encrypted and deduplicated content-addressable storage. Defined in the domain package — this is the dependency inversion anchor. All operations (backup, restore, check, prune) program against this interface, not the concrete repository.Repository struct. Exposes blob-level operations (content-addressed chunks) and file-level operations (snapshots, indexes, keys, locks).
  • Implementations: internal/repository.Repository (the sole production implementation). Test implementations in internal/repository/testing.go.
  • Design quality: At ~20 methods, this is the most complex interface in the project. The breadth is justified — it is the central system abstraction, and splitting it further would just create artificial seams. The WithBlobUploader method is notable: it uses a callback-based API to manage lifetime of an upload session (workers are started before fn is called, stopped and index is flushed after fn returns). This prevents misuse where a caller might forget to finalize an upload session.

Unwrapper#

  • Package: github.com/restic/restic/internal/backend
  • File: internal/backend/backend.go:104
  • Methods:
    Unwrap() Backend
  • Purpose: Allows traversal of the backend decorator stack. The generic AsBackend[B Backend](b Backend) B function uses this to find a specific concrete type anywhere in the chain (e.g., to locate the cache.Backend for cache invalidation after a successful write). Without this, the decorator chain would be opaque to the composition root.
  • Implementations: Every decorator wrapper: cache.Backend, retry.Backend, logger.Backend, sema.Backend, limiter.Backend, dryrun.Backend.
  • Design quality: Elegant. A single-method interface with a companion generic function. The pattern appears at the backend layer (not the restic domain layer), keeping decorator-stack introspection out of domain concerns. The use of generics (AsBackend[B]) avoids the need for a type-switch cascade.

BlobSaver, BlobSaverAsync, BlobSaverWithAsync, BlobLoader#

  • Package: github.com/restic/restic/internal/restic
  • File: internal/restic/repository.go:160–181
  • Methods:
    // BlobSaver
    SaveBlob(ctx context.Context, tpe BlobType, buf []byte, id ID, storeDuplicate bool) (newID ID, known bool, sizeInRepo int, err error)
    
    // BlobSaverAsync
    SaveBlobAsync(ctx context.Context, tpe BlobType, buf []byte, id ID, storeDuplicate bool, cb func(newID ID, known bool, sizeInRepo int, err error))
    
    // BlobSaverWithAsync = BlobSaver + BlobSaverAsync (composite)
    
    // BlobLoader
    LoadBlob(context.Context, BlobType, ID, []byte) ([]byte, error)
  • Purpose: Narrow capability interfaces for passing to workers. archiver.FileSaver and archiver.TreeSaver receive a BlobSaverWithAsync (not the full Repository) for their upload work. This enforces that upload workers can only save blobs — they cannot query the index, modify snapshots, or call other repository methods. The async variant enables fire-and-forget uploads with a callback for completion notification, enabling pipeline parallelism in the archiver.
  • Implementations: internal/repository.Repository satisfies all three. Mock implementations in archiver tests.
  • Design quality: Excellent application of the Interface Segregation Principle. The archiver’s FileSaver goroutines genuinely do not need LoadIndex or ListBlobs. The BlobSaverAsync callback design also sidesteps channel complexity for communicating results from upload workers back to the tree assembler.

Factory#

  • Package: github.com/restic/restic/internal/backend/location
  • File: internal/backend/location/registry.go:32
  • Methods:
    Scheme() string
    ParseConfig(s string) (interface{}, error)
    StripPassword(s string) string
    Create(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter, errorLog func(string, ...interface{})) (backend.Backend, error)
    Open(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter, errorLog func(string, ...interface{})) (backend.Backend, error)
  • Purpose: Plugin factory for registering and instantiating storage backends by URL scheme. The Registry maps scheme strings ("s3", "sftp", "rest", etc.) to Factory implementations. ParseConfig converts a URL string into a driver-specific config struct; Open/Create instantiate the Backend from that config. StripPassword sanitizes URLs for logging.
  • Implementations: Each storage driver provides a factory via NewHTTPBackendFactory or NewLimitedBackendFactory generic constructors, registered via init() side-effects in internal/backend/all. The generic genericBackendFactory[C, T] struct bridges the type-erased interface with typed driver configs.
  • Design quality: The use of interface{} for the config parameter is a necessary type erasure at the registry boundary — the registry cannot know each driver’s config type. The generic constructors (NewHTTPBackendFactory[C, T]) restore type safety at the driver registration site. The interface{} leakage is minimal (only in Factory, not in any driver code directly).

FS and File#

  • Package: github.com/restic/restic/internal/fs
  • File: internal/fs/interface.go:10–52
  • Methods (FS):
    OpenFile(name string, flag int, metadataOnly bool) (File, error)
    Lstat(name string) (*ExtendedFileInfo, error)
    Join(elem ...string) string
    Separator() string
    Abs(path string) (string, error)
    Clean(path string) string
    VolumeName(path string) string
    IsAbs(path string) bool
    Dir(path string) string
    Base(path string) string
  • Methods (File):
    MakeReadable() error
    Read(p []byte) (n int, err error)  // io.Reader
    Close() error                       // io.Closer
    Readdirnames(n int) ([]string, error)
    Stat() (*ExtendedFileInfo, error)
    ToNode(ignoreXattrListError bool, warnf func(format string, args ...any)) (*data.Node, error)
  • Purpose: Filesystem abstraction enabling backup of virtual or OS filesystems. The critical feature is the metadataOnly flag on OpenFile: when set, the implementation can return a File object without actually opening/reading the file — metadata (stat, xattr) is accessible but reading data requires calling MakeReadable(). This enables efficient pre-scan passes (gathering file counts/sizes without data reads) and supports platforms where some file types cannot be opened normally.
  • Implementations: fs.OSFS (wraps the real OS), fs.VirtualFS (for testing with in-memory trees), fs.ModeFilter (wraps another FS, filtering by file mode).
  • Design quality: File embeds io.Reader and io.Closer, honoring stdlib interfaces. The ToNode() method on File is a domain-layer coupling (returns *data.Node) — it bleeds domain types into the filesystem abstraction. However, since fs is only consumed by the archiver and data.Node is the canonical file-metadata type, this is pragmatic rather than problematic.

Layout#

  • Package: github.com/restic/restic/internal/backend/layout
  • File: internal/backend/layout/layout.go:8
  • Methods:
    Filename(backend.Handle) string
    Dirname(backend.Handle) string
    Basedir(backend.FileType) (dir string, subdirs bool)
    Paths() []string
    Name() string
  • Purpose: Strategy interface for computing filesystem paths from backend handles. Allows the local and sftp backends to support multiple on-disk layouts (default restic layout, old legacy layout, S3-style flat layout). The layout is selected at repository open time and passed to the storage driver — the driver then delegates all path computation to the layout.
  • Implementations: layout.DefaultLayout (directory fan-out: <type>/<first-2-hex-chars>/<full-hash>), layout.S3LegacyLayout (flat), layout.RESTLayout.
  • Design quality: Clean strategy pattern. 5 methods, all path-computation related. No stateful behavior. Well-segregated.

ui.Terminal#

  • Package: github.com/restic/restic/internal/ui
  • File: internal/ui/terminal.go:10
  • Methods:
    Print(line string)
    Error(line string)
    SetStatus(lines []string)
    CanUpdateStatus() bool
    InputRaw() io.ReadCloser
    InputIsTerminal() bool
    ReadPassword(ctx context.Context, prompt string) (string, error)
    OutputWriter() io.Writer
    OutputRaw() io.Writer
    OutputIsTerminal() bool
  • Purpose: Abstracts all terminal I/O: status lines (in-place update), log output, error output, and interactive password prompting. Allows the same command code to work against a real termstatus.Terminal (with ANSI escape sequences) and against a plain line-oriented writer (for --json mode, non-TTY environments, and tests).
  • Implementations: termstatus.Terminal (full ANSI terminal), a plain ui.StdioTerminal used in JSON/non-interactive mode.
  • Design quality: Broad (10 methods) but cohesive — all methods relate to the terminal I/O contract. The separation of Print/Error (for immediate lines) from SetStatus (for in-place updateable lines) from OutputWriter (for raw concurrent-safe writing) covers the full range of output modes restic needs without leaking implementation details of the ANSI terminal.

Interface patterns#

  • Size distribution: Ranges from 1 method (Unwrapper) to ~20 methods (Repository). Most interfaces have 3–10 methods. The project heavily applies ISP: large interfaces like Repository are broken into small capability subsets (Lister, LoaderUnpacked, BlobSaver, BlobLoader) for use by specific callers.
  • Embedding: Extensively used for interface composition. ListerLoaderUnpacked embeds Lister + LoaderUnpacked. BlobSaverWithAsync embeds BlobSaver + BlobSaverAsync. Unpacked[FT] embeds ListerLoaderUnpacked + SaverUnpacked[FT] + RemoverUnpacked[FT]. FreezeBackend embeds Backend. File embeds io.Reader + io.Closer.
  • Generics: SaverUnpacked[FT FileTypes], RemoverUnpacked[FT FileTypes], Unpacked[FT FileTypes] use type constraints to restrict file types to FileType | WriteableFileType — a rare but effective use of Go generics to prevent calling SaveUnpacked with an incorrect file type at compile time. AsBackend[B Backend] is a generic function for type-safe decorator-stack traversal.
  • Implicit satisfaction: Interfaces are defined predominantly by consumers in the domain package (internal/restic). Drivers in internal/backend/* satisfy Backend without importing from the domain package — the dependency arrow points inward. This is a textbook application of the Go interface/dependency rule.
  • stdlib interfaces used: io.Reader and io.Closer embedded in fs.File. hash.Hash returned by Backend.Hasher(). io.ReadCloser and io.Writer in ui.Terminal. The RewindReader interface (returned by various helpers) extends io.Reader.

Key abstractions#

1. backend.Backend — The storage primitive#

The foundational contract. Every persistence operation goes through it. Its 13 methods map directly to the operations any object-store must support. Its error-classification methods (IsPermanentError, IsNotExist) are what enable the retry.Backend decorator to retry transient failures without knowing which storage system it wraps.

2. restic.Repository — The encryption and deduplication gateway#

The central domain contract. All high-level operations (archiving, restoring, checking, pruning) program against this interface. By placing it in the innermost package, restic ensures that no core business logic can accidentally bypass encryption or the content-addressed index.

3. restic.BlobSaver / BlobSaverAsync — Minimum-capability upload contract#

The narrow interfaces passed to archiver workers. Demonstrate mature ISP application: workers receive only the one capability they need, making the data flow more auditable and the test surface smaller.

4. backend.Unwrapper + AsBackend[B] — Decorator-stack introspection#

A single-method interface combined with a generic traversal function. Solves the practical problem that the composition root needs to locate specific decorators (e.g., cache) after they’ve been buried in the stack — without requiring the stack to expose a typed accessor for every possible decorator type.

5. fs.FS / fs.File — Virtual filesystem abstraction#

Enables the archiver to work against real OS filesystems, in-memory test fixtures, and filtered views using the same code. The metadataOnly flag on OpenFile is the key innovation: it allows a fast metadata-only pre-scan pass before committing to a full data read, which is important for large backup sets.


Interface-driven extensibility#

restic’s extensibility is almost entirely interface-driven:

Storage backends: Any type satisfying backend.Backend can be registered as a storage driver via the location.Factory interface + Registry. The backend/all package uses blank-import init() side-effects to auto-register all built-in drivers. Third-party backends could be added by importing additional packages.

Backend decorators: The backend.Backend + backend.Unwrapper pair defines the decorator composability contract. Any cross-cutting concern (rate limiting, caching, observability, dry-run simulation) can be implemented as a wrapping Backend without modifying the drivers. The explicit composition in global.wrapBackend means the decorator order is auditable.

Filesystem sources: The fs.FS interface allows the archiver to back up sources other than the real OS filesystem. This is used in integration tests (in-memory VirtualFS) and could support future features like container filesystem snapshotting.

Repository migrations: The internal/migrations package defines a Migration interface for applying structural changes to existing repositories. Each migration is independently implemented and registered, allowing the restic migrate command to list and apply them.

Layout strategies: The layout.Layout interface allows local and SFTP backends to support multiple on-disk directory organizations (current default, S3-legacy, REST-style flat) via strategy injection at open time.

The architecture ensures that all extension points flow through interfaces defined in the domain package (internal/restic) or the backend abstraction layer (internal/backend), never through concrete type imports. This makes the system legible: adding a new storage backend requires touching only the driver package and the all registration package.