2025-09-23 14:01:09 -04:00
|
|
|
package data
|
2014-08-04 14:47:04 -04:00
|
|
|
|
|
|
|
|
import (
|
2017-06-04 05:16:55 -04:00
|
|
|
"context"
|
2014-08-11 16:47:24 -04:00
|
|
|
"fmt"
|
2015-04-29 17:35:02 -04:00
|
|
|
"os/user"
|
2014-09-23 16:39:12 -04:00
|
|
|
"path/filepath"
|
2020-11-28 03:32:06 -05:00
|
|
|
"sync"
|
2014-08-04 14:47:04 -04:00
|
|
|
"time"
|
2017-07-23 08:21:03 -04:00
|
|
|
|
|
|
|
|
"github.com/restic/restic/internal/debug"
|
2025-09-23 14:01:09 -04:00
|
|
|
"github.com/restic/restic/internal/restic"
|
2014-08-04 14:47:04 -04:00
|
|
|
)
|
|
|
|
|
|
2016-05-08 16:38:38 -04:00
|
|
|
// Snapshot is the state of a resource at one point in time.
|
2014-08-04 14:47:04 -04:00
|
|
|
type Snapshot struct {
|
2025-09-23 14:01:09 -04:00
|
|
|
Time time.Time `json:"time"`
|
|
|
|
|
Parent *restic.ID `json:"parent,omitempty"`
|
|
|
|
|
Tree *restic.ID `json:"tree"`
|
|
|
|
|
Paths []string `json:"paths"`
|
|
|
|
|
Hostname string `json:"hostname,omitempty"`
|
|
|
|
|
Username string `json:"username,omitempty"`
|
|
|
|
|
UID uint32 `json:"uid,omitempty"`
|
|
|
|
|
GID uint32 `json:"gid,omitempty"`
|
|
|
|
|
Excludes []string `json:"excludes,omitempty"`
|
|
|
|
|
Tags []string `json:"tags,omitempty"`
|
|
|
|
|
Original *restic.ID `json:"original,omitempty"`
|
2016-08-31 14:29:54 -04:00
|
|
|
|
2024-02-22 16:25:14 -05:00
|
|
|
ProgramVersion string `json:"program_version,omitempty"`
|
|
|
|
|
Summary *SnapshotSummary `json:"summary,omitempty"`
|
2023-06-19 13:30:41 -04:00
|
|
|
|
2025-09-23 14:01:09 -04:00
|
|
|
id *restic.ID // plaintext ID, used during restore
|
2014-08-04 14:47:04 -04:00
|
|
|
}
|
|
|
|
|
|
2024-02-22 16:25:14 -05:00
|
|
|
type SnapshotSummary struct {
|
|
|
|
|
BackupStart time.Time `json:"backup_start"`
|
|
|
|
|
BackupEnd time.Time `json:"backup_end"`
|
|
|
|
|
|
|
|
|
|
// statistics from the backup json output
|
|
|
|
|
FilesNew uint `json:"files_new"`
|
|
|
|
|
FilesChanged uint `json:"files_changed"`
|
|
|
|
|
FilesUnmodified uint `json:"files_unmodified"`
|
|
|
|
|
DirsNew uint `json:"dirs_new"`
|
|
|
|
|
DirsChanged uint `json:"dirs_changed"`
|
|
|
|
|
DirsUnmodified uint `json:"dirs_unmodified"`
|
|
|
|
|
DataBlobs int `json:"data_blobs"`
|
|
|
|
|
TreeBlobs int `json:"tree_blobs"`
|
|
|
|
|
DataAdded uint64 `json:"data_added"`
|
2024-02-25 14:40:52 -05:00
|
|
|
DataAddedPacked uint64 `json:"data_added_packed"`
|
2024-02-22 16:25:14 -05:00
|
|
|
TotalFilesProcessed uint `json:"total_files_processed"`
|
|
|
|
|
TotalBytesProcessed uint64 `json:"total_bytes_processed"`
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-08 16:38:38 -04:00
|
|
|
// NewSnapshot returns an initialized snapshot struct for the current user and
|
|
|
|
|
// time.
|
2017-08-29 12:29:46 -04:00
|
|
|
func NewSnapshot(paths []string, tags []string, hostname string, time time.Time) (*Snapshot, error) {
|
2018-01-03 16:10:20 -05:00
|
|
|
absPaths := make([]string, 0, len(paths))
|
|
|
|
|
for _, path := range paths {
|
|
|
|
|
p, err := filepath.Abs(path)
|
|
|
|
|
if err == nil {
|
|
|
|
|
absPaths = append(absPaths, p)
|
|
|
|
|
} else {
|
|
|
|
|
absPaths = append(absPaths, path)
|
2015-03-02 08:48:47 -05:00
|
|
|
}
|
2014-09-23 16:39:12 -04:00
|
|
|
}
|
|
|
|
|
|
2014-08-04 14:47:04 -04:00
|
|
|
sn := &Snapshot{
|
2018-01-03 16:10:20 -05:00
|
|
|
Paths: absPaths,
|
2017-08-29 12:29:46 -04:00
|
|
|
Time: time,
|
2017-02-10 13:37:33 -05:00
|
|
|
Tags: tags,
|
|
|
|
|
Hostname: hostname,
|
2014-08-04 14:47:04 -04:00
|
|
|
}
|
|
|
|
|
|
2017-02-10 13:37:33 -05:00
|
|
|
err := sn.fillUserInfo()
|
2015-02-03 16:04:09 -05:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2014-08-04 14:47:04 -04:00
|
|
|
}
|
|
|
|
|
|
2014-12-21 11:20:49 -05:00
|
|
|
return sn, nil
|
2014-08-04 14:47:04 -04:00
|
|
|
}
|
|
|
|
|
|
2016-05-08 16:38:38 -04:00
|
|
|
// LoadSnapshot loads the snapshot with the id and returns it.
|
2025-09-23 14:01:09 -04:00
|
|
|
func LoadSnapshot(ctx context.Context, loader restic.LoaderUnpacked, id restic.ID) (*Snapshot, error) {
|
2015-07-25 11:05:45 -04:00
|
|
|
sn := &Snapshot{id: &id}
|
2025-09-23 14:01:09 -04:00
|
|
|
err := restic.LoadJSONUnpacked(ctx, loader, restic.SnapshotFile, id, sn)
|
2014-08-04 16:46:14 -04:00
|
|
|
if err != nil {
|
2023-05-01 11:24:13 -04:00
|
|
|
return nil, fmt.Errorf("failed to load snapshot %v: %w", id.Str(), err)
|
2014-08-04 16:46:14 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sn, nil
|
2014-08-04 14:47:04 -04:00
|
|
|
}
|
2014-08-11 16:47:24 -04:00
|
|
|
|
2022-06-12 08:38:19 -04:00
|
|
|
// SaveSnapshot saves the snapshot sn and returns its ID.
|
2025-09-23 14:01:09 -04:00
|
|
|
func SaveSnapshot(ctx context.Context, repo restic.SaverUnpacked[restic.WriteableFileType], sn *Snapshot) (restic.ID, error) {
|
|
|
|
|
return restic.SaveJSONUnpacked(ctx, repo, restic.WriteableSnapshotFile, sn)
|
2022-06-12 08:38:19 -04:00
|
|
|
}
|
|
|
|
|
|
2020-11-28 03:32:06 -05:00
|
|
|
// ForAllSnapshots reads all snapshots in parallel and calls the
|
|
|
|
|
// given function. It is guaranteed that the function is not run concurrently.
|
|
|
|
|
// If the called function returns an error, this function is cancelled and
|
|
|
|
|
// also returns this error.
|
2020-11-28 02:59:12 -05:00
|
|
|
// If a snapshot ID is in excludeIDs, it will be ignored.
|
2025-09-23 14:01:09 -04:00
|
|
|
func ForAllSnapshots(ctx context.Context, be restic.Lister, loader restic.LoaderUnpacked, excludeIDs restic.IDSet, fn func(restic.ID, *Snapshot, error) error) error {
|
2020-11-28 03:32:06 -05:00
|
|
|
var m sync.Mutex
|
|
|
|
|
|
2022-10-15 11:24:47 -04:00
|
|
|
// For most snapshots decoding is nearly for free, thus just assume were only limited by IO
|
2025-09-23 14:01:09 -04:00
|
|
|
return restic.ParallelList(ctx, be, restic.SnapshotFile, loader.Connections(), func(ctx context.Context, id restic.ID, _ int64) error {
|
2022-10-15 11:24:47 -04:00
|
|
|
if excludeIDs.Has(id) {
|
2020-07-19 01:13:41 -04:00
|
|
|
return nil
|
2020-11-28 03:32:06 -05:00
|
|
|
}
|
|
|
|
|
|
2022-10-15 11:24:47 -04:00
|
|
|
sn, err := LoadSnapshot(ctx, loader, id)
|
|
|
|
|
m.Lock()
|
|
|
|
|
defer m.Unlock()
|
|
|
|
|
return fn(id, sn, err)
|
|
|
|
|
})
|
2020-11-28 02:59:12 -05:00
|
|
|
}
|
|
|
|
|
|
2014-11-24 15:12:32 -05:00
|
|
|
func (sn Snapshot) String() string {
|
2024-01-06 14:19:17 -05:00
|
|
|
return fmt.Sprintf("snapshot %s of %v at %s by %s@%s",
|
2016-08-19 14:50:52 -04:00
|
|
|
sn.id.Str(), sn.Paths, sn.Time, sn.Username, sn.Hostname)
|
2014-08-11 16:47:24 -04:00
|
|
|
}
|
2014-11-24 15:12:32 -05:00
|
|
|
|
2017-02-08 18:43:10 -05:00
|
|
|
// ID returns the snapshot's ID.
|
2025-09-23 14:01:09 -04:00
|
|
|
func (sn Snapshot) ID() *restic.ID {
|
2014-11-24 15:12:32 -05:00
|
|
|
return sn.id
|
|
|
|
|
}
|
2015-04-29 17:35:02 -04:00
|
|
|
|
|
|
|
|
func (sn *Snapshot) fillUserInfo() error {
|
|
|
|
|
usr, err := user.Current()
|
|
|
|
|
if err != nil {
|
2015-05-14 17:13:00 -04:00
|
|
|
return nil
|
2015-04-29 17:35:02 -04:00
|
|
|
}
|
|
|
|
|
sn.Username = usr.Username
|
2015-05-14 17:13:00 -04:00
|
|
|
|
2015-08-16 07:16:02 -04:00
|
|
|
// set userid and groupid
|
2025-09-23 14:01:09 -04:00
|
|
|
sn.UID, sn.GID, err = restic.UidGidInt(usr)
|
2015-08-16 07:16:02 -04:00
|
|
|
return err
|
2015-04-29 17:35:02 -04:00
|
|
|
}
|
2015-05-17 14:48:59 -04:00
|
|
|
|
2017-03-05 11:43:18 -05:00
|
|
|
// AddTags adds the given tags to the snapshots tags, preventing duplicates.
|
|
|
|
|
// It returns true if any changes were made.
|
|
|
|
|
func (sn *Snapshot) AddTags(addTags []string) (changed bool) {
|
|
|
|
|
nextTag:
|
|
|
|
|
for _, add := range addTags {
|
|
|
|
|
for _, tag := range sn.Tags {
|
|
|
|
|
if tag == add {
|
|
|
|
|
continue nextTag
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sn.Tags = append(sn.Tags, add)
|
|
|
|
|
changed = true
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RemoveTags removes the given tags from the snapshots tags and
|
|
|
|
|
// returns true if any changes were made.
|
|
|
|
|
func (sn *Snapshot) RemoveTags(removeTags []string) (changed bool) {
|
|
|
|
|
for _, remove := range removeTags {
|
|
|
|
|
for i, tag := range sn.Tags {
|
|
|
|
|
if tag == remove {
|
|
|
|
|
// https://github.com/golang/go/wiki/SliceTricks
|
|
|
|
|
sn.Tags[i] = sn.Tags[len(sn.Tags)-1]
|
|
|
|
|
sn.Tags[len(sn.Tags)-1] = ""
|
|
|
|
|
sn.Tags = sn.Tags[:len(sn.Tags)-1]
|
|
|
|
|
|
|
|
|
|
changed = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-06 21:19:06 -04:00
|
|
|
func (sn *Snapshot) hasTag(tag string) bool {
|
|
|
|
|
for _, snTag := range sn.Tags {
|
|
|
|
|
if tag == snTag {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-09 03:24:02 -04:00
|
|
|
// HasTags returns true if the snapshot has all the tags in l.
|
|
|
|
|
func (sn *Snapshot) HasTags(l []string) bool {
|
|
|
|
|
for _, tag := range l {
|
2021-07-14 21:38:15 -04:00
|
|
|
if tag == "" && len(sn.Tags) == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
2017-07-09 03:24:02 -04:00
|
|
|
if !sn.hasTag(tag) {
|
|
|
|
|
return false
|
2016-09-13 14:13:04 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-09 03:24:02 -04:00
|
|
|
return true
|
2016-09-13 14:13:04 -04:00
|
|
|
}
|
|
|
|
|
|
2020-02-26 16:17:59 -05:00
|
|
|
// HasTagList returns true if either
|
2022-08-19 13:12:26 -04:00
|
|
|
// - the snapshot satisfies at least one TagList, so there is a TagList in l
|
|
|
|
|
// for which all tags are included in sn, or
|
|
|
|
|
// - l is empty
|
2017-07-09 03:47:41 -04:00
|
|
|
func (sn *Snapshot) HasTagList(l []TagList) bool {
|
|
|
|
|
debug.Log("testing snapshot with tags %v against list: %v", sn.Tags, l)
|
|
|
|
|
|
|
|
|
|
if len(l) == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tags := range l {
|
|
|
|
|
if sn.HasTags(tags) {
|
2017-09-25 08:35:37 -04:00
|
|
|
debug.Log(" snapshot satisfies %v %v", tags, l)
|
2017-07-09 03:47:41 -04:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-09 03:24:02 -04:00
|
|
|
// HasPaths returns true if the snapshot has all of the paths.
|
2017-03-05 20:21:58 -05:00
|
|
|
func (sn *Snapshot) HasPaths(paths []string) bool {
|
2022-08-29 21:38:17 -04:00
|
|
|
m := make(map[string]struct{}, len(sn.Paths))
|
|
|
|
|
for _, snPath := range sn.Paths {
|
|
|
|
|
m[snPath] = struct{}{}
|
|
|
|
|
}
|
2017-03-05 20:21:58 -05:00
|
|
|
for _, path := range paths {
|
2022-08-29 21:38:17 -04:00
|
|
|
if _, ok := m[path]; !ok {
|
2017-07-09 03:24:02 -04:00
|
|
|
return false
|
2016-05-10 15:51:56 -04:00
|
|
|
}
|
2016-05-10 15:23:18 -04:00
|
|
|
}
|
|
|
|
|
|
2017-07-09 03:24:02 -04:00
|
|
|
return true
|
2017-03-05 20:21:58 -05:00
|
|
|
}
|
|
|
|
|
|
2020-02-26 16:17:59 -05:00
|
|
|
// HasHostname returns true if either
|
|
|
|
|
// - the snapshot hostname is in the list of the given hostnames, or
|
|
|
|
|
// - the list of given hostnames is empty
|
|
|
|
|
func (sn *Snapshot) HasHostname(hostnames []string) bool {
|
|
|
|
|
if len(hostnames) == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, hostname := range hostnames {
|
|
|
|
|
if sn.Hostname == hostname {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-18 07:05:47 -04:00
|
|
|
// Snapshots is a list of snapshots.
|
|
|
|
|
type Snapshots []*Snapshot
|
|
|
|
|
|
|
|
|
|
// Len returns the number of snapshots in sn.
|
|
|
|
|
func (sn Snapshots) Len() int {
|
|
|
|
|
return len(sn)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Less returns true iff the ith snapshot has been made after the jth.
|
|
|
|
|
func (sn Snapshots) Less(i, j int) bool {
|
|
|
|
|
return sn[i].Time.After(sn[j].Time)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Swap exchanges the two snapshots.
|
|
|
|
|
func (sn Snapshots) Swap(i, j int) {
|
|
|
|
|
sn[i], sn[j] = sn[j], sn[i]
|
|
|
|
|
}
|