mirror of
https://github.com/kopia/kopia.git
synced 2026-03-28 11:03:44 -04:00
* policy: introduced OptionalBool - refactoring * policy: added logging policy * testing: added support for symlinks and modtime to mockfs * logging: exposed NullLogger instance * upload: emit debug logs according to logging policies * cli: logging policy support
210 lines
6.3 KiB
Go
210 lines
6.3 KiB
Go
package snapshotfs
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/kopia/kopia/fs"
|
|
"github.com/kopia/kopia/repo"
|
|
"github.com/kopia/kopia/repo/manifest"
|
|
"github.com/kopia/kopia/repo/object"
|
|
"github.com/kopia/kopia/snapshot"
|
|
)
|
|
|
|
// ParseObjectIDWithPath interprets the given ID string (which could be an object ID optionally followed by
|
|
// nested path specification) and returns corresponding object.ID.
|
|
func ParseObjectIDWithPath(ctx context.Context, rep repo.Repository, objectIDWithPath string) (object.ID, error) {
|
|
parts := strings.Split(objectIDWithPath, "/")
|
|
|
|
oid, err := object.ParseID(parts[0])
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "can't parse object ID %v", objectIDWithPath)
|
|
}
|
|
|
|
if len(parts) == 1 {
|
|
return oid, nil
|
|
}
|
|
|
|
return parseNestedObjectID(ctx, AutoDetectEntryFromObjectID(ctx, rep, oid, ""), parts[1:])
|
|
}
|
|
|
|
// GetNestedEntry returns nested entry with a given name path.
|
|
func GetNestedEntry(ctx context.Context, startingDir fs.Entry, pathElements []string) (fs.Entry, error) {
|
|
current := startingDir
|
|
|
|
for _, part := range pathElements {
|
|
if part == "" {
|
|
continue
|
|
}
|
|
|
|
dir, ok := current.(fs.Directory)
|
|
if !ok {
|
|
return nil, errors.Errorf("entry not found %q: parent is not a directory", part)
|
|
}
|
|
|
|
entries, err := dir.Readdir(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error reading directory")
|
|
}
|
|
|
|
e := entries.FindByName(part)
|
|
if e == nil {
|
|
return nil, errors.Errorf("entry not found: %q", part)
|
|
}
|
|
|
|
current = e
|
|
}
|
|
|
|
return current, nil
|
|
}
|
|
|
|
func parseNestedObjectID(ctx context.Context, startingDir fs.Entry, parts []string) (object.ID, error) {
|
|
e, err := GetNestedEntry(ctx, startingDir, parts)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return e.(object.HasObjectID).ObjectID(), nil
|
|
}
|
|
|
|
// findSnapshotByRootObjectIDOrManifestID returns the list of matching snapshots for a given rootID.
|
|
// which can be either snapshot manifst ID (which matches 0 or 1 snapshots)
|
|
// or the root object ID (which can match arbitrary number of snapshots).
|
|
// If multiple snapshots match and they don't agree on root object attributes and consistentAttributes==true
|
|
// the function fails, otherwise it returns the latest of the snapshots.
|
|
func findSnapshotByRootObjectIDOrManifestID(ctx context.Context, rep repo.Repository, rootID string, consistentAttributes bool) (*snapshot.Manifest, error) {
|
|
m, err := snapshot.LoadSnapshot(ctx, rep, manifest.ID(rootID))
|
|
if err == nil {
|
|
return m, nil
|
|
}
|
|
|
|
mans, err := snapshot.FindSnapshotsByRootObjectID(ctx, rep, object.ID(rootID))
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unable to find shapshots by ID %v", rootID)
|
|
}
|
|
|
|
// no matching snapshots.
|
|
if len(mans) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// all snapshots have consistent metadata, pick any.
|
|
if areSnapshotsConsistent(mans) {
|
|
return mans[0], nil
|
|
}
|
|
|
|
// at this point we found multiple snapshots with the same root ID which don't agree on other
|
|
// metadata (the attributes, ACLs, ownership, etc. of the root)
|
|
if consistentAttributes {
|
|
return nil, errors.Errorf("found multiple snapshots matching %v with inconsistent root attributes.", rootID)
|
|
}
|
|
|
|
repoFSLog(ctx).Debugf("Found multiple snapshots matching %v with inconsistent root attributes. Picking latest one.", rootID)
|
|
|
|
return latestManifest(mans), nil
|
|
}
|
|
|
|
func areSnapshotsConsistent(mans []*snapshot.Manifest) bool {
|
|
for _, m := range mans {
|
|
if !consistentSnapshotMetadata(m, mans[0]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func latestManifest(mans []*snapshot.Manifest) *snapshot.Manifest {
|
|
latest := mans[0]
|
|
|
|
for _, m := range mans {
|
|
if m.StartTime.After(latest.StartTime) {
|
|
latest = m
|
|
}
|
|
}
|
|
|
|
return latest
|
|
}
|
|
|
|
// FilesystemEntryFromIDWithPath returns a filesystem entry for the provided object ID, which
|
|
// can be a snapshot manifest ID or an object ID with path.
|
|
// If multiple snapshots match and they don't agree on root object attributes and consistentAttributes==true
|
|
// the function fails, otherwise it returns the latest of the snapshots.
|
|
func FilesystemEntryFromIDWithPath(ctx context.Context, rep repo.Repository, rootID string, consistentAttributes bool) (fs.Entry, error) {
|
|
pathElements := strings.Split(rootID, "/")
|
|
|
|
if len(pathElements) > 1 {
|
|
// if a path is provided, consistentAttributes is meaningless since descending into nested path is
|
|
// always unambiguous because parent always has full attributes.
|
|
consistentAttributes = false
|
|
}
|
|
|
|
var startingEntry fs.Entry
|
|
|
|
man, err := findSnapshotByRootObjectIDOrManifestID(ctx, rep, pathElements[0], consistentAttributes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if man != nil {
|
|
// ID was unambiguously resolved to a snapshot, which means we have data about the root directory itself.
|
|
startingEntry, err = SnapshotRoot(rep, man)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
oid, err := object.ParseID(pathElements[0])
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "can't parse object ID %v", rootID)
|
|
}
|
|
|
|
startingEntry = AutoDetectEntryFromObjectID(ctx, rep, oid, "")
|
|
}
|
|
|
|
return GetNestedEntry(ctx, startingEntry, pathElements[1:])
|
|
}
|
|
|
|
// FilesystemDirectoryFromIDWithPath returns a filesystem directory entry for the provided object ID, which
|
|
// can be a snapshot manifest ID or an object ID with path.
|
|
func FilesystemDirectoryFromIDWithPath(ctx context.Context, rep repo.Repository, rootID string, consistentAttributes bool) (fs.Directory, error) {
|
|
e, err := FilesystemEntryFromIDWithPath(ctx, rep, rootID, consistentAttributes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if dir, ok := e.(fs.Directory); ok {
|
|
return dir, nil
|
|
}
|
|
|
|
return nil, errors.Errorf("%v is not a directory object", rootID)
|
|
}
|
|
|
|
func consistentSnapshotMetadata(m1, m2 *snapshot.Manifest) bool {
|
|
if m1.RootEntry == nil || m2.RootEntry == nil {
|
|
return false
|
|
}
|
|
|
|
return toJSON(m1.RootEntry) == toJSON(m2.RootEntry)
|
|
}
|
|
|
|
func toJSON(v interface{}) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
|
|
// GetEntryFromPlaceholder returns a fs.Entry for shallow placeholder
|
|
// defp referencing a real Entry in Repository r.
|
|
func GetEntryFromPlaceholder(ctx context.Context, r repo.Repository, defp snapshot.HasDirEntryOrNil) (fs.Entry, error) {
|
|
de, err := defp.DirEntryOrNil(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "unable to get direntry from placeholder")
|
|
}
|
|
|
|
repoFSLog(ctx).Debugf("GetDirEntryFromPlaceholder %v %v ", r, de)
|
|
|
|
return EntryFromDirEntry(r, de), nil
|
|
}
|