diff --git a/changelog/unreleased/issue-4585 b/changelog/unreleased/issue-4585 new file mode 100644 index 000000000..bcea9c83d --- /dev/null +++ b/changelog/unreleased/issue-4585 @@ -0,0 +1,29 @@ +Enhancement: Limit repository size to a predefined maximum size during vackup + +Restic backup can now limit the repository size to a given maximum value. The +repository size limit can be specified as option `--max-repo-size`, with +the usual meaning for size related options. + +During backup, the current size is monitored by calculating its actual repository +size plus the size of any new blob added during the backup process. Once the +defined limit is exceeded, backup is prevented from creating new backup entries +and the 'in progress' files are finalized, without adding large amounts of new data. + +The size limit is a rough limit and cannot be taken as a precise limit, +since indexes and snapshot file have to be finalized. +Due to the highly parallel processing of a backup process +the limit can be overshot by `packfile size` multiple times. +With a current default of 16m for a packfile, +you might experience differences of multiple megabytes between the expected limit +and the actual size when the backup process terminates. + +The new packfiles are created by the current backup process and a new snapshot +will be created. + +Currently the implemenation is incomplete. There are currently two indications +that there is a incomplete backup: +field `PartialSnapshot` exist and is set to true and +the snapshot is tagged as `partial-snapshot`. + +https://github.com/restic/restic/issues/4583 +https://github.com/restic/restic/pull/5215 diff --git a/internal/repository/repository.go b/internal/repository/repository.go index aee0db103..c1243d5ef 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -51,6 +51,8 @@ type Repository struct { allocDec sync.Once enc *zstd.Encoder dec *zstd.Decoder + + MaxRepoCapReached bool } // internalRepository allows using SaveUnpacked and RemoveUnpacked with all FileTypes @@ -62,6 +64,8 @@ type Options struct { Compression CompressionMode PackSize uint NoExtraVerify bool + RepoSizeMax uint64 + repoCurSize uint64 } // CompressionMode configures if data should be compressed. @@ -391,7 +395,60 @@ func (r *Repository) saveAndEncrypt(ctx context.Context, t restic.BlobType, data panic(fmt.Sprintf("invalid type: %v", t)) } - return pm.SaveBlob(ctx, t, id, ciphertext, uncompressedLength) + length, err := pm.SaveBlob(ctx, t, id, ciphertext, uncompressedLength) + + var m sync.Mutex + + // maximum repository capacity exceeded? + m.Lock() + defer m.Unlock() + if r.opts.RepoSizeMax > 0 { + r.opts.repoCurSize += uint64(length) + if r.opts.repoCurSize > r.opts.RepoSizeMax { + r.MaxRepoCapReached = true + debug.Log("MaxCapacityExceeded") + return length, errors.New("MaxCapacityExceeded") + } + } + return length, err +} + +// CurrentRepositorySize counts the sizes of the filetypes snapshot, index and packs +func (r *Repository) CurrentRepositorySize(ctx context.Context) (uint64, error) { + curSize := uint64(0) + if r.opts.RepoSizeMax > 0 { + for _, ft := range []restic.FileType{restic.SnapshotFile, restic.IndexFile, restic.PackFile} { + err := r.List(ctx, ft, func(_ restic.ID, size int64) error { + curSize += uint64(size) + return nil + }) + if err != nil { + return 0, err + } + } + r.opts.repoCurSize = curSize + return curSize, nil + } + + return 0, errors.New("repository maximum size has not been set") +} + +// MaxCapacityExceeded reports if repository has a limit and if it is exceeded +func (r *Repository) MaxCapacityExceeded() bool { + if r.opts.RepoSizeMax == 0 { + return false + } + return r.MaxRepoCapReached +} + +func (r *Repository) IsRepositoryLimitActive() bool { + return r.opts.RepoSizeMax > 0 +} + +// CapacityChecker has to satisfy restic.Repository interface needs +type CapacityChecker interface { + MaxCapacityExceeded() bool + IsRepositoryLimitActive() bool } func (r *Repository) verifyCiphertext(buf []byte, uncompressedLength int, id restic.ID) error {