Files
kopia/snapshot/snapshotfs/objref.go
Jarek Kowalski dcff6c285d Added support for logging policies (#1472)
* 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
2021-11-06 10:06:05 -07:00

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
}