mirror of
https://github.com/octoleo/restic.git
synced 2024-11-23 13:17:42 +00:00
Merge pull request #4611 from zmanda/windows-metadata-support
Back up and restore windows metadata like created ts, file attribs like hidden, readonly, encrypted with a common extensible mechanism
This commit is contained in:
commit
b953dc8f58
7
changelog/unreleased/pull-4611
Normal file
7
changelog/unreleased/pull-4611
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Back up windows created time and file attributes like hidden flag
|
||||||
|
|
||||||
|
Restic did not back up windows-specific meta-data like created time and file attributes like hidden flag.
|
||||||
|
Restic now backs up file created time and file attributes like hidden, readonly and encrypted flag when backing up files and folders on windows.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4611
|
||||||
|
|
@ -126,6 +126,7 @@ func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
|
|||||||
// Make the following attributes disappear
|
// Make the following attributes disappear
|
||||||
Name byte `json:"name,omitempty"`
|
Name byte `json:"name,omitempty"`
|
||||||
ExtendedAttributes byte `json:"extended_attributes,omitempty"`
|
ExtendedAttributes byte `json:"extended_attributes,omitempty"`
|
||||||
|
GenericAttributes byte `json:"generic_attributes,omitempty"`
|
||||||
Device byte `json:"device,omitempty"`
|
Device byte `json:"device,omitempty"`
|
||||||
Content byte `json:"content,omitempty"`
|
Content byte `json:"content,omitempty"`
|
||||||
Subtree byte `json:"subtree,omitempty"`
|
Subtree byte `json:"subtree,omitempty"`
|
||||||
|
@ -178,6 +178,9 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
|
|||||||
totalErrors++
|
totalErrors++
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
res.Warn = func(message string) {
|
||||||
|
msg.E("Warning: %s\n", message)
|
||||||
|
}
|
||||||
|
|
||||||
excludePatterns := filter.ParsePatterns(opts.Exclude)
|
excludePatterns := filter.ParsePatterns(opts.Exclude)
|
||||||
insensitiveExcludePatterns := filter.ParsePatterns(opts.InsensitiveExclude)
|
insensitiveExcludePatterns := filter.ParsePatterns(opts.InsensitiveExclude)
|
||||||
|
@ -487,7 +487,6 @@ particular note are:
|
|||||||
* File creation date on Unix platforms
|
* File creation date on Unix platforms
|
||||||
* Inode flags on Unix platforms
|
* Inode flags on Unix platforms
|
||||||
* File ownership and ACLs on Windows
|
* File ownership and ACLs on Windows
|
||||||
* The "hidden" flag on Windows
|
|
||||||
|
|
||||||
Reading data from a command
|
Reading data from a command
|
||||||
***************************
|
***************************
|
||||||
|
@ -2,6 +2,7 @@ package errors
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
stderrors "errors"
|
stderrors "errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -22,12 +23,42 @@ var Wrap = errors.Wrap
|
|||||||
// nil, Wrapf returns nil.
|
// nil, Wrapf returns nil.
|
||||||
var Wrapf = errors.Wrapf
|
var Wrapf = errors.Wrapf
|
||||||
|
|
||||||
|
// WithStack annotates err with a stack trace at the point WithStack was called.
|
||||||
|
// If err is nil, WithStack returns nil.
|
||||||
var WithStack = errors.WithStack
|
var WithStack = errors.WithStack
|
||||||
|
|
||||||
// Go 1.13-style error handling.
|
// Go 1.13-style error handling.
|
||||||
|
|
||||||
|
// As finds the first error in err's tree that matches target, and if one is found,
|
||||||
|
// sets target to that error value and returns true. Otherwise, it returns false.
|
||||||
func As(err error, tgt interface{}) bool { return stderrors.As(err, tgt) }
|
func As(err error, tgt interface{}) bool { return stderrors.As(err, tgt) }
|
||||||
|
|
||||||
|
// Is reports whether any error in err's tree matches target.
|
||||||
func Is(x, y error) bool { return stderrors.Is(x, y) }
|
func Is(x, y error) bool { return stderrors.Is(x, y) }
|
||||||
|
|
||||||
|
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
|
||||||
|
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
|
||||||
|
//
|
||||||
|
// Unwrap only calls a method of the form "Unwrap() error". In particular Unwrap does not
|
||||||
|
// unwrap errors returned by [Join].
|
||||||
func Unwrap(err error) error { return stderrors.Unwrap(err) }
|
func Unwrap(err error) error { return stderrors.Unwrap(err) }
|
||||||
|
|
||||||
|
// CombineErrors combines multiple errors into a single error.
|
||||||
|
func CombineErrors(errors ...error) error {
|
||||||
|
var combinedErrorMsg string
|
||||||
|
|
||||||
|
for _, err := range errors {
|
||||||
|
if err != nil {
|
||||||
|
if combinedErrorMsg != "" {
|
||||||
|
combinedErrorMsg += "; " // Separate error messages with a delimiter
|
||||||
|
}
|
||||||
|
combinedErrorMsg += err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if combinedErrorMsg == "" {
|
||||||
|
return nil // No errors, return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("multiple errors occurred: [%s]", combinedErrorMsg)
|
||||||
|
}
|
||||||
|
@ -124,3 +124,17 @@ func RemoveIfExists(filename string) error {
|
|||||||
func Chtimes(name string, atime time.Time, mtime time.Time) error {
|
func Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||||
return os.Chtimes(fixpath(name), atime, mtime)
|
return os.Chtimes(fixpath(name), atime, mtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsAccessDenied checks if the error is due to permission error.
|
||||||
|
func IsAccessDenied(err error) bool {
|
||||||
|
return os.IsPermission(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPermissions resets the permissions of the file at the specified path
|
||||||
|
func ResetPermissions(path string) error {
|
||||||
|
// Set the default file permissions
|
||||||
|
if err := os.Chmod(path, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -77,3 +77,29 @@ func TempFile(dir, prefix string) (f *os.File, err error) {
|
|||||||
func Chmod(name string, mode os.FileMode) error {
|
func Chmod(name string, mode os.FileMode) error {
|
||||||
return os.Chmod(fixpath(name), mode)
|
return os.Chmod(fixpath(name), mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearSystem removes the system attribute from the file.
|
||||||
|
func ClearSystem(path string) error {
|
||||||
|
return ClearAttribute(path, windows.FILE_ATTRIBUTE_SYSTEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAttribute removes the specified attribute from the file.
|
||||||
|
func ClearAttribute(path string, attribute uint32) error {
|
||||||
|
ptr, err := windows.UTF16PtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fileAttributes, err := windows.GetFileAttributes(ptr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fileAttributes&attribute != 0 {
|
||||||
|
// Clear the attribute
|
||||||
|
fileAttributes &= ^uint32(attribute)
|
||||||
|
err = windows.SetFileAttributes(ptr, fileAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -6,7 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@ -20,12 +22,53 @@ import (
|
|||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExtendedAttribute is a tuple storing the xattr name and value.
|
// ExtendedAttribute is a tuple storing the xattr name and value for various filesystems.
|
||||||
type ExtendedAttribute struct {
|
type ExtendedAttribute struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Value []byte `json:"value"`
|
Value []byte `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenericAttributeType can be used for OS specific functionalities by defining specific types
|
||||||
|
// in node.go to be used by the specific node_xx files.
|
||||||
|
// OS specific attribute types should follow the convention <OS>Attributes.
|
||||||
|
// GenericAttributeTypes should follow the convention <OS specific attribute type>.<attribute name>
|
||||||
|
// The attributes in OS specific attribute types must be pointers as we want to distinguish nil values
|
||||||
|
// and not create GenericAttributes for them.
|
||||||
|
type GenericAttributeType string
|
||||||
|
|
||||||
|
// OSType is the type created to represent each specific OS
|
||||||
|
type OSType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// When new GenericAttributeType are defined, they must be added in the init function as well.
|
||||||
|
|
||||||
|
// Below are windows specific attributes.
|
||||||
|
|
||||||
|
// TypeCreationTime is the GenericAttributeType used for storing creation time for windows files within the generic attributes map.
|
||||||
|
TypeCreationTime GenericAttributeType = "windows.creation_time"
|
||||||
|
// TypeFileAttributes is the GenericAttributeType used for storing file attributes for windows files within the generic attributes map.
|
||||||
|
TypeFileAttributes GenericAttributeType = "windows.file_attributes"
|
||||||
|
|
||||||
|
// Generic Attributes for other OS types should be defined here.
|
||||||
|
)
|
||||||
|
|
||||||
|
// init is called when the package is initialized. Any new GenericAttributeTypes being created must be added here as well.
|
||||||
|
func init() {
|
||||||
|
storeGenericAttributeType(TypeCreationTime, TypeFileAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// genericAttributesForOS maintains a map of known genericAttributesForOS to the OSType
|
||||||
|
var genericAttributesForOS = map[GenericAttributeType]OSType{}
|
||||||
|
|
||||||
|
// storeGenericAttributeType adds and entry in genericAttributesForOS map
|
||||||
|
func storeGenericAttributeType(attributeTypes ...GenericAttributeType) {
|
||||||
|
for _, attributeType := range attributeTypes {
|
||||||
|
// Get the OS attribute type from the GenericAttributeType
|
||||||
|
osAttributeName := strings.Split(string(attributeType), ".")[0]
|
||||||
|
genericAttributesForOS[attributeType] = OSType(osAttributeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Node is a file, directory or other item in a backup.
|
// Node is a file, directory or other item in a backup.
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -47,11 +90,12 @@ type Node struct {
|
|||||||
// This allows storing arbitrary byte-sequences, which are possible as symlink targets on unix systems,
|
// This allows storing arbitrary byte-sequences, which are possible as symlink targets on unix systems,
|
||||||
// as LinkTarget without breaking backwards-compatibility.
|
// as LinkTarget without breaking backwards-compatibility.
|
||||||
// Must only be set of the linktarget cannot be encoded as valid utf8.
|
// Must only be set of the linktarget cannot be encoded as valid utf8.
|
||||||
LinkTargetRaw []byte `json:"linktarget_raw,omitempty"`
|
LinkTargetRaw []byte `json:"linktarget_raw,omitempty"`
|
||||||
ExtendedAttributes []ExtendedAttribute `json:"extended_attributes,omitempty"`
|
ExtendedAttributes []ExtendedAttribute `json:"extended_attributes,omitempty"`
|
||||||
Device uint64 `json:"device,omitempty"` // in case of Type == "dev", stat.st_rdev
|
GenericAttributes map[GenericAttributeType]json.RawMessage `json:"generic_attributes,omitempty"`
|
||||||
Content IDs `json:"content"`
|
Device uint64 `json:"device,omitempty"` // in case of Type == "dev", stat.st_rdev
|
||||||
Subtree *ID `json:"subtree,omitempty"`
|
Content IDs `json:"content"`
|
||||||
|
Subtree *ID `json:"subtree,omitempty"`
|
||||||
|
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
|
||||||
@ -180,8 +224,8 @@ func (node *Node) CreateAt(ctx context.Context, path string, repo BlobLoader) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RestoreMetadata restores node metadata
|
// RestoreMetadata restores node metadata
|
||||||
func (node Node) RestoreMetadata(path string) error {
|
func (node Node) RestoreMetadata(path string, warn func(msg string)) error {
|
||||||
err := node.restoreMetadata(path)
|
err := node.restoreMetadata(path, warn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debug.Log("restoreMetadata(%s) error %v", path, err)
|
debug.Log("restoreMetadata(%s) error %v", path, err)
|
||||||
}
|
}
|
||||||
@ -189,7 +233,7 @@ func (node Node) RestoreMetadata(path string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node Node) restoreMetadata(path string) error {
|
func (node Node) restoreMetadata(path string, warn func(msg string)) error {
|
||||||
var firsterr error
|
var firsterr error
|
||||||
|
|
||||||
if err := lchown(path, int(node.UID), int(node.GID)); err != nil {
|
if err := lchown(path, int(node.UID), int(node.GID)); err != nil {
|
||||||
@ -203,14 +247,6 @@ func (node Node) restoreMetadata(path string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type != "symlink" {
|
|
||||||
if err := fs.Chmod(path, node.Mode); err != nil {
|
|
||||||
if firsterr != nil {
|
|
||||||
firsterr = errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := node.RestoreTimestamps(path); err != nil {
|
if err := node.RestoreTimestamps(path); err != nil {
|
||||||
debug.Log("error restoring timestamps for dir %v: %v", path, err)
|
debug.Log("error restoring timestamps for dir %v: %v", path, err)
|
||||||
if firsterr != nil {
|
if firsterr != nil {
|
||||||
@ -225,6 +261,24 @@ func (node Node) restoreMetadata(path string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := node.restoreGenericAttributes(path, warn); err != nil {
|
||||||
|
debug.Log("error restoring generic attributes for %v: %v", path, err)
|
||||||
|
if firsterr != nil {
|
||||||
|
firsterr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moving RestoreTimestamps and restoreExtendedAttributes calls above as for readonly files in windows
|
||||||
|
// calling Chmod below will no longer allow any modifications to be made on the file and the
|
||||||
|
// calls above would fail.
|
||||||
|
if node.Type != "symlink" {
|
||||||
|
if err := fs.Chmod(path, node.Mode); err != nil {
|
||||||
|
if firsterr != nil {
|
||||||
|
firsterr = errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return firsterr
|
return firsterr
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -438,6 +492,9 @@ func (node Node) Equals(other Node) bool {
|
|||||||
if !node.sameExtendedAttributes(other) {
|
if !node.sameExtendedAttributes(other) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if !node.sameGenericAttributes(other) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if node.Subtree != nil {
|
if node.Subtree != nil {
|
||||||
if other.Subtree == nil {
|
if other.Subtree == nil {
|
||||||
return false
|
return false
|
||||||
@ -480,8 +537,13 @@ func (node Node) sameContent(other Node) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (node Node) sameExtendedAttributes(other Node) bool {
|
func (node Node) sameExtendedAttributes(other Node) bool {
|
||||||
if len(node.ExtendedAttributes) != len(other.ExtendedAttributes) {
|
ln := len(node.ExtendedAttributes)
|
||||||
|
lo := len(other.ExtendedAttributes)
|
||||||
|
if ln != lo {
|
||||||
return false
|
return false
|
||||||
|
} else if ln == 0 {
|
||||||
|
// This means lo is also of length 0
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// build a set of all attributes that node has
|
// build a set of all attributes that node has
|
||||||
@ -525,6 +587,33 @@ func (node Node) sameExtendedAttributes(other Node) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (node Node) sameGenericAttributes(other Node) bool {
|
||||||
|
return deepEqual(node.GenericAttributes, other.GenericAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepEqual(map1, map2 map[GenericAttributeType]json.RawMessage) bool {
|
||||||
|
// Check if the maps have the same number of keys
|
||||||
|
if len(map1) != len(map2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over each key-value pair in map1
|
||||||
|
for key, value1 := range map1 {
|
||||||
|
// Check if the key exists in map2
|
||||||
|
value2, ok := map2[key]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the JSON.RawMessage values are equal byte by byte
|
||||||
|
if !bytes.Equal(value1, value2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (node *Node) fillUser(stat *statT) {
|
func (node *Node) fillUser(stat *statT) {
|
||||||
uid, gid := stat.uid(), stat.gid()
|
uid, gid := stat.uid(), stat.gid()
|
||||||
node.UID, node.GID = uid, gid
|
node.UID, node.GID = uid, gid
|
||||||
@ -627,7 +716,17 @@ func (node *Node) fillExtra(path string, fi os.FileInfo) error {
|
|||||||
return errors.Errorf("unsupported file type %q", node.Type)
|
return errors.Errorf("unsupported file type %q", node.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
return node.fillExtendedAttributes(path)
|
allowExtended, err := node.fillGenericAttributes(path, fi, stat)
|
||||||
|
if allowExtended {
|
||||||
|
// Skip processing ExtendedAttributes if allowExtended is false.
|
||||||
|
errEx := node.fillExtendedAttributes(path)
|
||||||
|
if err == nil {
|
||||||
|
err = errEx
|
||||||
|
} else {
|
||||||
|
debug.Log("Error filling extended attributes for %v at %v : %v", node.Name, path, errEx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *Node) fillExtendedAttributes(path string) error {
|
func (node *Node) fillExtendedAttributes(path string) error {
|
||||||
@ -665,3 +764,119 @@ func (node *Node) fillTimes(stat *statT) {
|
|||||||
node.ChangeTime = time.Unix(ctim.Unix())
|
node.ChangeTime = time.Unix(ctim.Unix())
|
||||||
node.AccessTime = time.Unix(atim.Unix())
|
node.AccessTime = time.Unix(atim.Unix())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleUnknownGenericAttributesFound is used for handling and distinguing between scenarios related to future versions and cross-OS repositories
|
||||||
|
func HandleUnknownGenericAttributesFound(unknownAttribs []GenericAttributeType, warn func(msg string)) {
|
||||||
|
for _, unknownAttrib := range unknownAttribs {
|
||||||
|
handleUnknownGenericAttributeFound(unknownAttrib, warn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUnknownGenericAttributeFound is used for handling and distinguing between scenarios related to future versions and cross-OS repositories
|
||||||
|
func handleUnknownGenericAttributeFound(genericAttributeType GenericAttributeType, warn func(msg string)) {
|
||||||
|
if checkGenericAttributeNameNotHandledAndPut(genericAttributeType) {
|
||||||
|
// Print the unique error only once for a given execution
|
||||||
|
os, exists := genericAttributesForOS[genericAttributeType]
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
// If genericAttributesForOS contains an entry but we still got here, it means the specific node_xx.go for the current OS did not handle it and the repository may have been originally created on a different OS.
|
||||||
|
// The fact that node.go knows about the attribute, means it is not a new attribute. This may be a common situation if a repo is used across OSs.
|
||||||
|
debug.Log("Ignoring a generic attribute found in the repository: %s which may not be compatible with your OS. Compatible OS: %s", genericAttributeType, os)
|
||||||
|
} else {
|
||||||
|
// If genericAttributesForOS in node.go does not know about this attribute, then the repository may have been created by a newer version which has a newer GenericAttributeType.
|
||||||
|
warn(fmt.Sprintf("Found an unrecognized generic attribute in the repository: %s. You may need to upgrade to latest version of restic.", genericAttributeType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllUnknownGenericAttributesFound performs validations for all generic attributes in the node.
|
||||||
|
// This is not used on windows currently because windows has handling for generic attributes.
|
||||||
|
// nolint:unused
|
||||||
|
func (node Node) handleAllUnknownGenericAttributesFound(warn func(msg string)) error {
|
||||||
|
for name := range node.GenericAttributes {
|
||||||
|
handleUnknownGenericAttributeFound(name, warn)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var unknownGenericAttributesHandlingHistory sync.Map
|
||||||
|
|
||||||
|
// checkGenericAttributeNameNotHandledAndPut checks if the GenericAttributeType name entry
|
||||||
|
// already exists and puts it in the map if not.
|
||||||
|
func checkGenericAttributeNameNotHandledAndPut(value GenericAttributeType) bool {
|
||||||
|
// If Key doesn't exist, put the value and return true because it is not already handled
|
||||||
|
_, exists := unknownGenericAttributesHandlingHistory.LoadOrStore(value, "")
|
||||||
|
// Key exists, then it is already handled so return false
|
||||||
|
return !exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// The functions below are common helper functions which can be used for generic attributes support
|
||||||
|
// across different OS.
|
||||||
|
|
||||||
|
// genericAttributesToOSAttrs gets the os specific attribute from the generic attribute using reflection
|
||||||
|
// nolint:unused
|
||||||
|
func genericAttributesToOSAttrs(attrs map[GenericAttributeType]json.RawMessage, attributeType reflect.Type, attributeValuePtr *reflect.Value, keyPrefix string) (unknownAttribs []GenericAttributeType, err error) {
|
||||||
|
attributeValue := *attributeValuePtr
|
||||||
|
|
||||||
|
for key, rawMsg := range attrs {
|
||||||
|
found := false
|
||||||
|
for i := 0; i < attributeType.NumField(); i++ {
|
||||||
|
if getFQKeyByIndex(attributeType, i, keyPrefix) == key {
|
||||||
|
found = true
|
||||||
|
fieldValue := attributeValue.Field(i)
|
||||||
|
// For directly supported types, use json.Unmarshal directly
|
||||||
|
if err := json.Unmarshal(rawMsg, fieldValue.Addr().Interface()); err != nil {
|
||||||
|
return unknownAttribs, errors.Wrap(err, "Unmarshal")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
unknownAttribs = append(unknownAttribs, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unknownAttribs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFQKey gets the fully qualified key for the field
|
||||||
|
// nolint:unused
|
||||||
|
func getFQKey(field reflect.StructField, keyPrefix string) GenericAttributeType {
|
||||||
|
return GenericAttributeType(fmt.Sprintf("%s.%s", keyPrefix, field.Tag.Get("generic")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFQKeyByIndex gets the fully qualified key for the field index
|
||||||
|
// nolint:unused
|
||||||
|
func getFQKeyByIndex(attributeType reflect.Type, index int, keyPrefix string) GenericAttributeType {
|
||||||
|
return getFQKey(attributeType.Field(index), keyPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// osAttrsToGenericAttributes gets the generic attribute from the os specific attribute using reflection
|
||||||
|
// nolint:unused
|
||||||
|
func osAttrsToGenericAttributes(attributeType reflect.Type, attributeValuePtr *reflect.Value, keyPrefix string) (attrs map[GenericAttributeType]json.RawMessage, err error) {
|
||||||
|
attributeValue := *attributeValuePtr
|
||||||
|
attrs = make(map[GenericAttributeType]json.RawMessage)
|
||||||
|
|
||||||
|
// Iterate over the fields of the struct
|
||||||
|
for i := 0; i < attributeType.NumField(); i++ {
|
||||||
|
field := attributeType.Field(i)
|
||||||
|
|
||||||
|
// Get the field value using reflection
|
||||||
|
fieldValue := attributeValue.FieldByName(field.Name)
|
||||||
|
|
||||||
|
// Check if the field is nil
|
||||||
|
if fieldValue.IsNil() {
|
||||||
|
// If it's nil, skip this field
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the field value into a json.RawMessage
|
||||||
|
var fieldBytes []byte
|
||||||
|
if fieldBytes, err = json.Marshal(fieldValue.Interface()); err != nil {
|
||||||
|
return attrs, errors.Wrap(err, "Marshal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the field into the map
|
||||||
|
attrs[getFQKey(field, keyPrefix)] = json.RawMessage(fieldBytes)
|
||||||
|
}
|
||||||
|
return attrs, nil
|
||||||
|
}
|
||||||
|
@ -3,9 +3,12 @@
|
|||||||
|
|
||||||
package restic
|
package restic
|
||||||
|
|
||||||
import "syscall"
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
|
func (node Node) restoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,3 +37,13 @@ func Listxattr(path string) ([]string, error) {
|
|||||||
func Setxattr(path, name string, data []byte) error {
|
func Setxattr(path, name string, data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restoreGenericAttributes is no-op on AIX.
|
||||||
|
func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error {
|
||||||
|
return node.handleAllUnknownGenericAttributesFound(warn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillGenericAttributes is a no-op on AIX.
|
||||||
|
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package restic
|
package restic
|
||||||
|
|
||||||
import "syscall"
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
|
func (node Node) restoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,18 +13,27 @@ func (s statT) atim() syscall.Timespec { return s.Atimespec }
|
|||||||
func (s statT) mtim() syscall.Timespec { return s.Mtimespec }
|
func (s statT) mtim() syscall.Timespec { return s.Mtimespec }
|
||||||
func (s statT) ctim() syscall.Timespec { return s.Ctimespec }
|
func (s statT) ctim() syscall.Timespec { return s.Ctimespec }
|
||||||
|
|
||||||
// Getxattr retrieves extended attribute data associated with path.
|
// Getxattr is a no-op on netbsd.
|
||||||
func Getxattr(path, name string) ([]byte, error) {
|
func Getxattr(path, name string) ([]byte, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listxattr retrieves a list of names of extended attributes associated with the
|
// Listxattr is a no-op on netbsd.
|
||||||
// given path in the file system.
|
|
||||||
func Listxattr(path string) ([]string, error) {
|
func Listxattr(path string) ([]string, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setxattr associates name and data together as an attribute of path.
|
// Setxattr is a no-op on netbsd.
|
||||||
func Setxattr(path, name string, data []byte) error {
|
func Setxattr(path, name string, data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restoreGenericAttributes is no-op on netbsd.
|
||||||
|
func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error {
|
||||||
|
return node.handleAllUnknownGenericAttributesFound(warn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillGenericAttributes is a no-op on netbsd.
|
||||||
|
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package restic
|
package restic
|
||||||
|
|
||||||
import "syscall"
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
|
func (node Node) restoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,18 +13,27 @@ func (s statT) atim() syscall.Timespec { return s.Atim }
|
|||||||
func (s statT) mtim() syscall.Timespec { return s.Mtim }
|
func (s statT) mtim() syscall.Timespec { return s.Mtim }
|
||||||
func (s statT) ctim() syscall.Timespec { return s.Ctim }
|
func (s statT) ctim() syscall.Timespec { return s.Ctim }
|
||||||
|
|
||||||
// Getxattr retrieves extended attribute data associated with path.
|
// Getxattr is a no-op on openbsd.
|
||||||
func Getxattr(path, name string) ([]byte, error) {
|
func Getxattr(path, name string) ([]byte, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listxattr retrieves a list of names of extended attributes associated with the
|
// Listxattr is a no-op on openbsd.
|
||||||
// given path in the file system.
|
|
||||||
func Listxattr(path string) ([]string, error) {
|
func Listxattr(path string) ([]string, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setxattr associates name and data together as an attribute of path.
|
// Setxattr is a no-op on openbsd.
|
||||||
func Setxattr(path, name string, data []byte) error {
|
func Setxattr(path, name string, data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restoreGenericAttributes is no-op on openbsd.
|
||||||
|
func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error {
|
||||||
|
return node.handleAllUnknownGenericAttributesFound(warn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillGenericAttributes is a no-op on openbsd.
|
||||||
|
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package restic_test
|
package restic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -11,7 +11,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/restic"
|
|
||||||
"github.com/restic/restic/internal/test"
|
"github.com/restic/restic/internal/test"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
@ -32,7 +31,7 @@ func BenchmarkNodeFillUser(t *testing.B) {
|
|||||||
t.ResetTimer()
|
t.ResetTimer()
|
||||||
|
|
||||||
for i := 0; i < t.N; i++ {
|
for i := 0; i < t.N; i++ {
|
||||||
_, err := restic.NodeFromFileInfo(path, fi)
|
_, err := NodeFromFileInfo(path, fi)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +55,7 @@ func BenchmarkNodeFromFileInfo(t *testing.B) {
|
|||||||
t.ResetTimer()
|
t.ResetTimer()
|
||||||
|
|
||||||
for i := 0; i < t.N; i++ {
|
for i := 0; i < t.N; i++ {
|
||||||
_, err := restic.NodeFromFileInfo(path, fi)
|
_, err := NodeFromFileInfo(path, fi)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -75,11 +74,11 @@ func parseTime(s string) time.Time {
|
|||||||
return t.Local()
|
return t.Local()
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeTests = []restic.Node{
|
var nodeTests = []Node{
|
||||||
{
|
{
|
||||||
Name: "testFile",
|
Name: "testFile",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Content: restic.IDs{},
|
Content: IDs{},
|
||||||
UID: uint32(os.Getuid()),
|
UID: uint32(os.Getuid()),
|
||||||
GID: uint32(os.Getgid()),
|
GID: uint32(os.Getgid()),
|
||||||
Mode: 0604,
|
Mode: 0604,
|
||||||
@ -90,7 +89,7 @@ var nodeTests = []restic.Node{
|
|||||||
{
|
{
|
||||||
Name: "testSuidFile",
|
Name: "testSuidFile",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Content: restic.IDs{},
|
Content: IDs{},
|
||||||
UID: uint32(os.Getuid()),
|
UID: uint32(os.Getuid()),
|
||||||
GID: uint32(os.Getgid()),
|
GID: uint32(os.Getgid()),
|
||||||
Mode: 0755 | os.ModeSetuid,
|
Mode: 0755 | os.ModeSetuid,
|
||||||
@ -101,7 +100,7 @@ var nodeTests = []restic.Node{
|
|||||||
{
|
{
|
||||||
Name: "testSuidFile2",
|
Name: "testSuidFile2",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Content: restic.IDs{},
|
Content: IDs{},
|
||||||
UID: uint32(os.Getuid()),
|
UID: uint32(os.Getuid()),
|
||||||
GID: uint32(os.Getgid()),
|
GID: uint32(os.Getgid()),
|
||||||
Mode: 0755 | os.ModeSetgid,
|
Mode: 0755 | os.ModeSetgid,
|
||||||
@ -112,7 +111,7 @@ var nodeTests = []restic.Node{
|
|||||||
{
|
{
|
||||||
Name: "testSticky",
|
Name: "testSticky",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Content: restic.IDs{},
|
Content: IDs{},
|
||||||
UID: uint32(os.Getuid()),
|
UID: uint32(os.Getuid()),
|
||||||
GID: uint32(os.Getgid()),
|
GID: uint32(os.Getgid()),
|
||||||
Mode: 0755 | os.ModeSticky,
|
Mode: 0755 | os.ModeSticky,
|
||||||
@ -148,7 +147,7 @@ var nodeTests = []restic.Node{
|
|||||||
{
|
{
|
||||||
Name: "testFile",
|
Name: "testFile",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Content: restic.IDs{},
|
Content: IDs{},
|
||||||
UID: uint32(os.Getuid()),
|
UID: uint32(os.Getuid()),
|
||||||
GID: uint32(os.Getgid()),
|
GID: uint32(os.Getgid()),
|
||||||
Mode: 0604,
|
Mode: 0604,
|
||||||
@ -170,14 +169,14 @@ var nodeTests = []restic.Node{
|
|||||||
{
|
{
|
||||||
Name: "testXattrFile",
|
Name: "testXattrFile",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Content: restic.IDs{},
|
Content: IDs{},
|
||||||
UID: uint32(os.Getuid()),
|
UID: uint32(os.Getuid()),
|
||||||
GID: uint32(os.Getgid()),
|
GID: uint32(os.Getgid()),
|
||||||
Mode: 0604,
|
Mode: 0604,
|
||||||
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
||||||
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
ExtendedAttributes: []restic.ExtendedAttribute{
|
ExtendedAttributes: []ExtendedAttribute{
|
||||||
{"user.foo", []byte("bar")},
|
{"user.foo", []byte("bar")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -191,7 +190,7 @@ var nodeTests = []restic.Node{
|
|||||||
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
||||||
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
ExtendedAttributes: []restic.ExtendedAttribute{
|
ExtendedAttributes: []ExtendedAttribute{
|
||||||
{"user.foo", []byte("bar")},
|
{"user.foo", []byte("bar")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -219,7 +218,7 @@ func TestNodeRestoreAt(t *testing.T) {
|
|||||||
nodePath = filepath.Join(tempdir, test.Name)
|
nodePath = filepath.Join(tempdir, test.Name)
|
||||||
}
|
}
|
||||||
rtest.OK(t, test.CreateAt(context.TODO(), nodePath, nil))
|
rtest.OK(t, test.CreateAt(context.TODO(), nodePath, nil))
|
||||||
rtest.OK(t, test.RestoreMetadata(nodePath))
|
rtest.OK(t, test.RestoreMetadata(nodePath, func(msg string) { rtest.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", nodePath, msg)) }))
|
||||||
|
|
||||||
if test.Type == "dir" {
|
if test.Type == "dir" {
|
||||||
rtest.OK(t, test.RestoreTimestamps(nodePath))
|
rtest.OK(t, test.RestoreTimestamps(nodePath))
|
||||||
@ -228,7 +227,7 @@ func TestNodeRestoreAt(t *testing.T) {
|
|||||||
fi, err := os.Lstat(nodePath)
|
fi, err := os.Lstat(nodePath)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
n2, err := restic.NodeFromFileInfo(nodePath, fi)
|
n2, err := NodeFromFileInfo(nodePath, fi)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
rtest.Assert(t, test.Name == n2.Name,
|
rtest.Assert(t, test.Name == n2.Name,
|
||||||
@ -330,7 +329,7 @@ func TestFixTime(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
res := restic.FixTime(test.src)
|
res := FixTime(test.src)
|
||||||
if !res.Equal(test.want) {
|
if !res.Equal(test.want) {
|
||||||
t.Fatalf("wrong result for %v, want:\n %v\ngot:\n %v", test.src, test.want, res)
|
t.Fatalf("wrong result for %v, want:\n %v\ngot:\n %v", test.src, test.want, res)
|
||||||
}
|
}
|
||||||
@ -343,12 +342,12 @@ func TestSymlinkSerialization(t *testing.T) {
|
|||||||
"válîd \t Üñi¢òde \n śẗŕinǵ",
|
"válîd \t Üñi¢òde \n śẗŕinǵ",
|
||||||
string([]byte{0, 1, 2, 0xfa, 0xfb, 0xfc}),
|
string([]byte{0, 1, 2, 0xfa, 0xfb, 0xfc}),
|
||||||
} {
|
} {
|
||||||
n := restic.Node{
|
n := Node{
|
||||||
LinkTarget: link,
|
LinkTarget: link,
|
||||||
}
|
}
|
||||||
ser, err := json.Marshal(n)
|
ser, err := json.Marshal(n)
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
var n2 restic.Node
|
var n2 Node
|
||||||
err = json.Unmarshal(ser, &n2)
|
err = json.Unmarshal(ser, &n2)
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
fmt.Println(string(ser))
|
fmt.Println(string(ser))
|
||||||
@ -365,7 +364,7 @@ func TestSymlinkSerializationFormat(t *testing.T) {
|
|||||||
{`{"linktarget":"test"}`, "test"},
|
{`{"linktarget":"test"}`, "test"},
|
||||||
{`{"linktarget":"\u0000\u0001\u0002\ufffd\ufffd\ufffd","linktarget_raw":"AAEC+vv8"}`, string([]byte{0, 1, 2, 0xfa, 0xfb, 0xfc})},
|
{`{"linktarget":"\u0000\u0001\u0002\ufffd\ufffd\ufffd","linktarget_raw":"AAEC+vv8"}`, string([]byte{0, 1, 2, 0xfa, 0xfb, 0xfc})},
|
||||||
} {
|
} {
|
||||||
var n2 restic.Node
|
var n2 Node
|
||||||
err := json.Unmarshal([]byte(d.ser), &n2)
|
err := json.Unmarshal([]byte(d.ser), &n2)
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
test.Equals(t, d.linkTarget, n2.LinkTarget)
|
test.Equals(t, d.linkTarget, n2.LinkTarget)
|
||||||
|
@ -1,21 +1,47 @@
|
|||||||
package restic
|
package restic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/fs"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WindowsAttributes are the genericAttributes for Windows OS
|
||||||
|
type WindowsAttributes struct {
|
||||||
|
// CreationTime is used for storing creation time for windows files.
|
||||||
|
CreationTime *syscall.Filetime `generic:"creation_time"`
|
||||||
|
// FileAttributes is used for storing file attributes for windows files.
|
||||||
|
FileAttributes *uint32 `generic:"file_attributes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
modAdvapi32 = syscall.NewLazyDLL("advapi32.dll")
|
||||||
|
procEncryptFile = modAdvapi32.NewProc("EncryptFileW")
|
||||||
|
procDecryptFile = modAdvapi32.NewProc("DecryptFileW")
|
||||||
)
|
)
|
||||||
|
|
||||||
// mknod is not supported on Windows.
|
// mknod is not supported on Windows.
|
||||||
func mknod(path string, mode uint32, dev uint64) (err error) {
|
func mknod(_ string, mode uint32, dev uint64) (err error) {
|
||||||
return errors.New("device nodes cannot be created on windows")
|
return errors.New("device nodes cannot be created on windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows doesn't need lchown
|
// Windows doesn't need lchown
|
||||||
func lchown(path string, uid int, gid int) (err error) {
|
func lchown(_ string, uid int, gid int) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restoreSymlinkTimestamps restores timestamps for symlinks
|
||||||
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
|
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
|
||||||
// tweaked version of UtimesNano from go/src/syscall/syscall_windows.go
|
// tweaked version of UtimesNano from go/src/syscall/syscall_windows.go
|
||||||
pathp, e := syscall.UTF16PtrFromString(path)
|
pathp, e := syscall.UTF16PtrFromString(path)
|
||||||
@ -28,7 +54,14 @@ func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespe
|
|||||||
if e != nil {
|
if e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
defer syscall.Close(h)
|
|
||||||
|
defer func() {
|
||||||
|
err := syscall.Close(h)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log("Error closing file handle for %s: %v\n", path, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
a := syscall.NsecToFiletime(syscall.TimespecToNsec(utimes[0]))
|
a := syscall.NsecToFiletime(syscall.TimespecToNsec(utimes[0]))
|
||||||
w := syscall.NsecToFiletime(syscall.TimespecToNsec(utimes[1]))
|
w := syscall.NsecToFiletime(syscall.TimespecToNsec(utimes[1]))
|
||||||
return syscall.SetFileTime(h, nil, &a, &w)
|
return syscall.SetFileTime(h, nil, &a, &w)
|
||||||
@ -83,3 +116,188 @@ func (s statT) ctim() syscall.Timespec {
|
|||||||
// Windows does not have the concept of a "change time" in the sense Unix uses it, so we're using the LastWriteTime here.
|
// Windows does not have the concept of a "change time" in the sense Unix uses it, so we're using the LastWriteTime here.
|
||||||
return syscall.NsecToTimespec(s.LastWriteTime.Nanoseconds())
|
return syscall.NsecToTimespec(s.LastWriteTime.Nanoseconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restoreGenericAttributes restores generic attributes for Windows
|
||||||
|
func (node Node) restoreGenericAttributes(path string, warn func(msg string)) (err error) {
|
||||||
|
if len(node.GenericAttributes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var errs []error
|
||||||
|
windowsAttributes, unknownAttribs, err := genericAttributesToWindowsAttrs(node.GenericAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing generic attribute for: %s : %v", path, err)
|
||||||
|
}
|
||||||
|
if windowsAttributes.CreationTime != nil {
|
||||||
|
if err := restoreCreationTime(path, windowsAttributes.CreationTime); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("error restoring creation time for: %s : %v", path, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if windowsAttributes.FileAttributes != nil {
|
||||||
|
if err := restoreFileAttributes(path, windowsAttributes.FileAttributes); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("error restoring file attributes for: %s : %v", path, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleUnknownGenericAttributesFound(unknownAttribs, warn)
|
||||||
|
return errors.CombineErrors(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// genericAttributesToWindowsAttrs converts the generic attributes map to a WindowsAttributes and also returns a string of unkown attributes that it could not convert.
|
||||||
|
func genericAttributesToWindowsAttrs(attrs map[GenericAttributeType]json.RawMessage) (windowsAttributes WindowsAttributes, unknownAttribs []GenericAttributeType, err error) {
|
||||||
|
waValue := reflect.ValueOf(&windowsAttributes).Elem()
|
||||||
|
unknownAttribs, err = genericAttributesToOSAttrs(attrs, reflect.TypeOf(windowsAttributes), &waValue, "windows")
|
||||||
|
return windowsAttributes, unknownAttribs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreCreationTime gets the creation time from the data and sets it to the file/folder at
|
||||||
|
// the specified path.
|
||||||
|
func restoreCreationTime(path string, creationTime *syscall.Filetime) (err error) {
|
||||||
|
pathPointer, err := syscall.UTF16PtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
handle, err := syscall.CreateFile(pathPointer,
|
||||||
|
syscall.FILE_WRITE_ATTRIBUTES, syscall.FILE_SHARE_WRITE, nil,
|
||||||
|
syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := syscall.Close(handle); err != nil {
|
||||||
|
debug.Log("Error closing file handle for %s: %v\n", path, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return syscall.SetFileTime(handle, creationTime, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreFileAttributes gets the File Attributes from the data and sets them to the file/folder
|
||||||
|
// at the specified path.
|
||||||
|
func restoreFileAttributes(path string, fileAttributes *uint32) (err error) {
|
||||||
|
pathPointer, err := syscall.UTF16PtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = fixEncryptionAttribute(path, fileAttributes, pathPointer)
|
||||||
|
if err != nil {
|
||||||
|
debug.Log("Could not change encryption attribute for path: %s: %v", path, err)
|
||||||
|
}
|
||||||
|
return syscall.SetFileAttributes(pathPointer, *fileAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fixEncryptionAttribute checks if a file needs to be marked encrypted and is not already encrypted, it sets
|
||||||
|
// the FILE_ATTRIBUTE_ENCRYPTED. Conversely, if the file needs to be marked unencrypted and it is already
|
||||||
|
// marked encrypted, it removes the FILE_ATTRIBUTE_ENCRYPTED.
|
||||||
|
func fixEncryptionAttribute(path string, attrs *uint32, pathPointer *uint16) (err error) {
|
||||||
|
if *attrs&windows.FILE_ATTRIBUTE_ENCRYPTED != 0 {
|
||||||
|
// File should be encrypted.
|
||||||
|
err = encryptFile(pathPointer)
|
||||||
|
if err != nil {
|
||||||
|
if fs.IsAccessDenied(err) {
|
||||||
|
// If existing file already has readonly or system flag, encrypt file call fails.
|
||||||
|
// We have already cleared readonly flag, clearing system flag if needed.
|
||||||
|
// The readonly and system flags will be set again at the end of this func if they are needed.
|
||||||
|
err = fs.ClearSystem(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt file: failed to clear system flag: %s : %v", path, err)
|
||||||
|
}
|
||||||
|
err = encryptFile(pathPointer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encrypt file: %s : %v", path, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to encrypt file: %s : %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
existingAttrs, err := windows.GetFileAttributes(pathPointer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get file attributes for existing file: %s : %v", path, err)
|
||||||
|
}
|
||||||
|
if existingAttrs&windows.FILE_ATTRIBUTE_ENCRYPTED != 0 {
|
||||||
|
// File should not be encrypted, but its already encrypted. Decrypt it.
|
||||||
|
err = decryptFile(pathPointer)
|
||||||
|
if err != nil {
|
||||||
|
if fs.IsAccessDenied(err) {
|
||||||
|
// If existing file already has readonly or system flag, decrypt file call fails.
|
||||||
|
// We have already cleared readonly flag, clearing system flag if needed.
|
||||||
|
// The readonly and system flags will be set again after this func if they are needed.
|
||||||
|
err = fs.ClearSystem(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt file: failed to clear system flag: %s : %v", path, err)
|
||||||
|
}
|
||||||
|
err = decryptFile(pathPointer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt file: %s : %v", path, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to decrypt file: %s : %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// encryptFile set the encrypted flag on the file.
|
||||||
|
func encryptFile(pathPointer *uint16) error {
|
||||||
|
// Call EncryptFile function
|
||||||
|
ret, _, err := procEncryptFile.Call(uintptr(unsafe.Pointer(pathPointer)))
|
||||||
|
if ret == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decryptFile removes the encrypted flag from the file.
|
||||||
|
func decryptFile(pathPointer *uint16) error {
|
||||||
|
// Call DecryptFile function
|
||||||
|
ret, _, err := procDecryptFile.Call(uintptr(unsafe.Pointer(pathPointer)))
|
||||||
|
if ret == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillGenericAttributes fills in the generic attributes for windows like File Attributes,
|
||||||
|
// Created time etc.
|
||||||
|
func (node *Node) fillGenericAttributes(path string, fi os.FileInfo, stat *statT) (allowExtended bool, err error) {
|
||||||
|
if strings.Contains(filepath.Base(path), ":") {
|
||||||
|
//Do not process for Alternate Data Streams in Windows
|
||||||
|
// Also do not allow processing of extended attributes for ADS.
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(filepath.Clean(path), `\`) {
|
||||||
|
// Do not process file attributes and created time for windows directories like
|
||||||
|
// C:, D:
|
||||||
|
// Filepath.Clean(path) ends with '\' for Windows root drives only.
|
||||||
|
|
||||||
|
// Add Windows attributes
|
||||||
|
node.GenericAttributes, err = WindowsAttrsToGenericAttributes(WindowsAttributes{
|
||||||
|
CreationTime: getCreationTime(fi, path),
|
||||||
|
FileAttributes: &stat.FileAttributes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// windowsAttrsToGenericAttributes converts the WindowsAttributes to a generic attributes map using reflection
|
||||||
|
func WindowsAttrsToGenericAttributes(windowsAttributes WindowsAttributes) (attrs map[GenericAttributeType]json.RawMessage, err error) {
|
||||||
|
// Get the value of the WindowsAttributes
|
||||||
|
windowsAttributesValue := reflect.ValueOf(windowsAttributes)
|
||||||
|
return osAttrsToGenericAttributes(reflect.TypeOf(windowsAttributes), &windowsAttributesValue, runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCreationTime gets the value for the WindowsAttribute CreationTime in a windows specific time format.
|
||||||
|
// The value is a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC)
|
||||||
|
// split into two 32-bit parts: the low-order DWORD and the high-order DWORD for efficiency and interoperability.
|
||||||
|
// The low-order DWORD represents the number of 100-nanosecond intervals elapsed since January 1, 1601, modulo
|
||||||
|
// 2^32. The high-order DWORD represents the number of times the low-order DWORD has overflowed.
|
||||||
|
func getCreationTime(fi os.FileInfo, path string) (creationTimeAttribute *syscall.Filetime) {
|
||||||
|
attrib, success := fi.Sys().(*syscall.Win32FileAttributeData)
|
||||||
|
if success && attrib != nil {
|
||||||
|
return &attrib.CreationTime
|
||||||
|
} else {
|
||||||
|
debug.Log("Could not get create time for path: %s", path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
210
internal/restic/node_windows_test.go
Normal file
210
internal/restic/node_windows_test.go
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package restic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/test"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRestoreCreationTime(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
path := t.TempDir()
|
||||||
|
fi, err := os.Lstat(path)
|
||||||
|
test.OK(t, errors.Wrapf(err, "Could not Lstat for path: %s", path))
|
||||||
|
creationTimeAttribute := getCreationTime(fi, path)
|
||||||
|
test.OK(t, errors.Wrapf(err, "Could not get creation time for path: %s", path))
|
||||||
|
//Using the temp dir creation time as the test creation time for the test file and folder
|
||||||
|
runGenericAttributesTest(t, path, TypeCreationTime, WindowsAttributes{CreationTime: creationTimeAttribute}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRestoreFileAttributes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
genericAttributeName := TypeFileAttributes
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
normal := uint32(syscall.FILE_ATTRIBUTE_NORMAL)
|
||||||
|
hidden := uint32(syscall.FILE_ATTRIBUTE_HIDDEN)
|
||||||
|
system := uint32(syscall.FILE_ATTRIBUTE_SYSTEM)
|
||||||
|
archive := uint32(syscall.FILE_ATTRIBUTE_ARCHIVE)
|
||||||
|
encrypted := uint32(windows.FILE_ATTRIBUTE_ENCRYPTED)
|
||||||
|
fileAttributes := []WindowsAttributes{
|
||||||
|
//normal
|
||||||
|
{FileAttributes: &normal},
|
||||||
|
//hidden
|
||||||
|
{FileAttributes: &hidden},
|
||||||
|
//system
|
||||||
|
{FileAttributes: &system},
|
||||||
|
//archive
|
||||||
|
{FileAttributes: &archive},
|
||||||
|
//encrypted
|
||||||
|
{FileAttributes: &encrypted},
|
||||||
|
}
|
||||||
|
for i, fileAttr := range fileAttributes {
|
||||||
|
genericAttrs, err := WindowsAttrsToGenericAttributes(fileAttr)
|
||||||
|
test.OK(t, err)
|
||||||
|
expectedNodes := []Node{
|
||||||
|
{
|
||||||
|
Name: fmt.Sprintf("testfile%d", i),
|
||||||
|
Type: "file",
|
||||||
|
Mode: 0655,
|
||||||
|
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
||||||
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
|
GenericAttributes: genericAttrs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
runGenericAttributesTestForNodes(t, expectedNodes, tempDir, genericAttributeName, fileAttr, false)
|
||||||
|
}
|
||||||
|
normal = uint32(syscall.FILE_ATTRIBUTE_DIRECTORY)
|
||||||
|
hidden = uint32(syscall.FILE_ATTRIBUTE_DIRECTORY | syscall.FILE_ATTRIBUTE_HIDDEN)
|
||||||
|
system = uint32(syscall.FILE_ATTRIBUTE_DIRECTORY | windows.FILE_ATTRIBUTE_SYSTEM)
|
||||||
|
archive = uint32(syscall.FILE_ATTRIBUTE_DIRECTORY | windows.FILE_ATTRIBUTE_ARCHIVE)
|
||||||
|
encrypted = uint32(syscall.FILE_ATTRIBUTE_DIRECTORY | windows.FILE_ATTRIBUTE_ENCRYPTED)
|
||||||
|
folderAttributes := []WindowsAttributes{
|
||||||
|
//normal
|
||||||
|
{FileAttributes: &normal},
|
||||||
|
//hidden
|
||||||
|
{FileAttributes: &hidden},
|
||||||
|
//system
|
||||||
|
{FileAttributes: &system},
|
||||||
|
//archive
|
||||||
|
{FileAttributes: &archive},
|
||||||
|
//encrypted
|
||||||
|
{FileAttributes: &encrypted},
|
||||||
|
}
|
||||||
|
for i, folderAttr := range folderAttributes {
|
||||||
|
genericAttrs, err := WindowsAttrsToGenericAttributes(folderAttr)
|
||||||
|
test.OK(t, err)
|
||||||
|
expectedNodes := []Node{
|
||||||
|
{
|
||||||
|
Name: fmt.Sprintf("testdirectory%d", i),
|
||||||
|
Type: "dir",
|
||||||
|
Mode: 0755,
|
||||||
|
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
||||||
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
|
GenericAttributes: genericAttrs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
runGenericAttributesTestForNodes(t, expectedNodes, tempDir, genericAttributeName, folderAttr, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGenericAttributesTest(t *testing.T, tempDir string, genericAttributeName GenericAttributeType, genericAttributeExpected WindowsAttributes, warningExpected bool) {
|
||||||
|
genericAttributes, err := WindowsAttrsToGenericAttributes(genericAttributeExpected)
|
||||||
|
test.OK(t, err)
|
||||||
|
expectedNodes := []Node{
|
||||||
|
{
|
||||||
|
Name: "testfile",
|
||||||
|
Type: "file",
|
||||||
|
Mode: 0644,
|
||||||
|
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
||||||
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
|
GenericAttributes: genericAttributes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "testdirectory",
|
||||||
|
Type: "dir",
|
||||||
|
Mode: 0755,
|
||||||
|
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
||||||
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
|
GenericAttributes: genericAttributes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
runGenericAttributesTestForNodes(t, expectedNodes, tempDir, genericAttributeName, genericAttributeExpected, warningExpected)
|
||||||
|
}
|
||||||
|
func runGenericAttributesTestForNodes(t *testing.T, expectedNodes []Node, tempDir string, genericAttr GenericAttributeType, genericAttributeExpected WindowsAttributes, warningExpected bool) {
|
||||||
|
|
||||||
|
for _, testNode := range expectedNodes {
|
||||||
|
testPath, node := restoreAndGetNode(t, tempDir, testNode, warningExpected)
|
||||||
|
rawMessage := node.GenericAttributes[genericAttr]
|
||||||
|
genericAttrsExpected, err := WindowsAttrsToGenericAttributes(genericAttributeExpected)
|
||||||
|
test.OK(t, err)
|
||||||
|
rawMessageExpected := genericAttrsExpected[genericAttr]
|
||||||
|
test.Equals(t, rawMessageExpected, rawMessage, "Generic attribute: %s got from NodeFromFileInfo not equal for path: %s", string(genericAttr), testPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreAndGetNode(t *testing.T, tempDir string, testNode Node, warningExpected bool) (string, *Node) {
|
||||||
|
testPath := filepath.Join(tempDir, "001", testNode.Name)
|
||||||
|
err := os.MkdirAll(filepath.Dir(testPath), testNode.Mode)
|
||||||
|
test.OK(t, errors.Wrapf(err, "Failed to create parent directories for: %s", testPath))
|
||||||
|
|
||||||
|
if testNode.Type == "file" {
|
||||||
|
|
||||||
|
testFile, err := os.Create(testPath)
|
||||||
|
test.OK(t, errors.Wrapf(err, "Failed to create test file: %s", testPath))
|
||||||
|
testFile.Close()
|
||||||
|
} else if testNode.Type == "dir" {
|
||||||
|
|
||||||
|
err := os.Mkdir(testPath, testNode.Mode)
|
||||||
|
test.OK(t, errors.Wrapf(err, "Failed to create test directory: %s", testPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = testNode.RestoreMetadata(testPath, func(msg string) {
|
||||||
|
if warningExpected {
|
||||||
|
test.Assert(t, warningExpected, "Warning triggered as expected: %s", msg)
|
||||||
|
} else {
|
||||||
|
// If warning is not expected, this code should not get triggered.
|
||||||
|
test.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", testPath, msg))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
test.OK(t, errors.Wrapf(err, "Failed to restore metadata for: %s", testPath))
|
||||||
|
|
||||||
|
fi, err := os.Lstat(testPath)
|
||||||
|
test.OK(t, errors.Wrapf(err, "Could not Lstat for path: %s", testPath))
|
||||||
|
|
||||||
|
nodeFromFileInfo, err := NodeFromFileInfo(testPath, fi)
|
||||||
|
test.OK(t, errors.Wrapf(err, "Could not get NodeFromFileInfo for path: %s", testPath))
|
||||||
|
|
||||||
|
return testPath, nodeFromFileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
const TypeSomeNewAttribute GenericAttributeType = "MockAttributes.SomeNewAttribute"
|
||||||
|
|
||||||
|
func TestNewGenericAttributeType(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
newGenericAttribute := map[GenericAttributeType]json.RawMessage{}
|
||||||
|
newGenericAttribute[TypeSomeNewAttribute] = []byte("any value")
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
expectedNodes := []Node{
|
||||||
|
{
|
||||||
|
Name: "testfile",
|
||||||
|
Type: "file",
|
||||||
|
Mode: 0644,
|
||||||
|
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
||||||
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
|
GenericAttributes: newGenericAttribute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "testdirectory",
|
||||||
|
Type: "dir",
|
||||||
|
Mode: 0755,
|
||||||
|
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
||||||
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
|
GenericAttributes: newGenericAttribute,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testNode := range expectedNodes {
|
||||||
|
testPath, node := restoreAndGetNode(t, tempDir, testNode, true)
|
||||||
|
_, ua, err := genericAttributesToWindowsAttrs(node.GenericAttributes)
|
||||||
|
test.OK(t, err)
|
||||||
|
// Since this GenericAttribute is unknown to this version of the software, it will not get set on the file.
|
||||||
|
test.Assert(t, len(ua) == 0, "Unkown attributes: %s found for path: %s", ua, testPath)
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@
|
|||||||
package restic
|
package restic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
@ -47,3 +48,13 @@ func handleXattrErr(err error) error {
|
|||||||
return errors.WithStack(e)
|
return errors.WithStack(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restoreGenericAttributes is no-op.
|
||||||
|
func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error {
|
||||||
|
return node.handleAllUnknownGenericAttributesFound(warn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillGenericAttributes is a no-op.
|
||||||
|
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
@ -50,16 +50,26 @@ func (w *filesWriter) writeToFile(path string, blob []byte, offset int64, create
|
|||||||
bucket.files[path].users++
|
bucket.files[path].users++
|
||||||
return wr, nil
|
return wr, nil
|
||||||
}
|
}
|
||||||
|
var f *os.File
|
||||||
var flags int
|
var err error
|
||||||
if createSize >= 0 {
|
if createSize >= 0 {
|
||||||
flags = os.O_CREATE | os.O_TRUNC | os.O_WRONLY
|
if f, err = os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600); err != nil {
|
||||||
} else {
|
if fs.IsAccessDenied(err) {
|
||||||
flags = os.O_WRONLY
|
// If file is readonly, clear the readonly flag by resetting the
|
||||||
}
|
// permissions of the file and try again
|
||||||
|
// as the metadata will be set again in the second pass and the
|
||||||
f, err := os.OpenFile(path, flags, 0600)
|
// readonly flag will be applied again if needed.
|
||||||
if err != nil {
|
if err = fs.ResetPermissions(path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if f, err = os.OpenFile(path, os.O_TRUNC|os.O_WRONLY, 0600); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if f, err = os.OpenFile(path, os.O_WRONLY, 0600); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ type Restorer struct {
|
|||||||
progress *restoreui.Progress
|
progress *restoreui.Progress
|
||||||
|
|
||||||
Error func(location string, err error) error
|
Error func(location string, err error) error
|
||||||
|
Warn func(message string)
|
||||||
SelectFilter func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool)
|
SelectFilter func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +179,7 @@ func (res *Restorer) restoreNodeTo(ctx context.Context, node *restic.Node, targe
|
|||||||
|
|
||||||
func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location string) error {
|
func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location string) error {
|
||||||
debug.Log("restoreNodeMetadata %v %v %v", node.Name, target, location)
|
debug.Log("restoreNodeMetadata %v %v %v", node.Name, target, location)
|
||||||
err := node.RestoreMetadata(target)
|
err := node.RestoreMetadata(target, res.Warn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debug.Log("node.RestoreMetadata(%s) error %v", target, err)
|
debug.Log("node.RestoreMetadata(%s) error %v", target, err)
|
||||||
}
|
}
|
||||||
@ -204,11 +205,19 @@ func (res *Restorer) restoreHardlinkAt(node *restic.Node, target, path, location
|
|||||||
|
|
||||||
func (res *Restorer) restoreEmptyFileAt(node *restic.Node, target, location string) error {
|
func (res *Restorer) restoreEmptyFileAt(node *restic.Node, target, location string) error {
|
||||||
wr, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
wr, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
||||||
if err != nil {
|
if fs.IsAccessDenied(err) {
|
||||||
return err
|
// If file is readonly, clear the readonly flag by resetting the
|
||||||
|
// permissions of the file and try again
|
||||||
|
// as the metadata will be set again in the second pass and the
|
||||||
|
// readonly flag will be applied again if needed.
|
||||||
|
if err = fs.ResetPermissions(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if wr, err = os.OpenFile(target, os.O_TRUNC|os.O_WRONLY, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = wr.Close()
|
if err = wr.Close(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package restorer
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
@ -27,17 +28,27 @@ type Snapshot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
Data string
|
Data string
|
||||||
Links uint64
|
Links uint64
|
||||||
Inode uint64
|
Inode uint64
|
||||||
Mode os.FileMode
|
Mode os.FileMode
|
||||||
ModTime time.Time
|
ModTime time.Time
|
||||||
|
attributes *FileAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dir struct {
|
type Dir struct {
|
||||||
Nodes map[string]Node
|
Nodes map[string]Node
|
||||||
Mode os.FileMode
|
Mode os.FileMode
|
||||||
ModTime time.Time
|
ModTime time.Time
|
||||||
|
attributes *FileAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileAttributes struct {
|
||||||
|
ReadOnly bool
|
||||||
|
Hidden bool
|
||||||
|
System bool
|
||||||
|
Archive bool
|
||||||
|
Encrypted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveFile(t testing.TB, repo restic.BlobSaver, node File) restic.ID {
|
func saveFile(t testing.TB, repo restic.BlobSaver, node File) restic.ID {
|
||||||
@ -52,7 +63,7 @@ func saveFile(t testing.TB, repo restic.BlobSaver, node File) restic.ID {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode uint64) restic.ID {
|
func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode uint64, getGenericAttributes func(attr *FileAttributes, isDir bool) (genericAttributes map[restic.GenericAttributeType]json.RawMessage)) restic.ID {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -78,20 +89,21 @@ func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode u
|
|||||||
mode = 0644
|
mode = 0644
|
||||||
}
|
}
|
||||||
err := tree.Insert(&restic.Node{
|
err := tree.Insert(&restic.Node{
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
ModTime: node.ModTime,
|
ModTime: node.ModTime,
|
||||||
Name: name,
|
Name: name,
|
||||||
UID: uint32(os.Getuid()),
|
UID: uint32(os.Getuid()),
|
||||||
GID: uint32(os.Getgid()),
|
GID: uint32(os.Getgid()),
|
||||||
Content: fc,
|
Content: fc,
|
||||||
Size: uint64(len(n.(File).Data)),
|
Size: uint64(len(n.(File).Data)),
|
||||||
Inode: fi,
|
Inode: fi,
|
||||||
Links: lc,
|
Links: lc,
|
||||||
|
GenericAttributes: getGenericAttributes(node.attributes, false),
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
case Dir:
|
case Dir:
|
||||||
id := saveDir(t, repo, node.Nodes, inode)
|
id := saveDir(t, repo, node.Nodes, inode, getGenericAttributes)
|
||||||
|
|
||||||
mode := node.Mode
|
mode := node.Mode
|
||||||
if mode == 0 {
|
if mode == 0 {
|
||||||
@ -99,13 +111,14 @@ func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode u
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := tree.Insert(&restic.Node{
|
err := tree.Insert(&restic.Node{
|
||||||
Type: "dir",
|
Type: "dir",
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
ModTime: node.ModTime,
|
ModTime: node.ModTime,
|
||||||
Name: name,
|
Name: name,
|
||||||
UID: uint32(os.Getuid()),
|
UID: uint32(os.Getuid()),
|
||||||
GID: uint32(os.Getgid()),
|
GID: uint32(os.Getgid()),
|
||||||
Subtree: &id,
|
Subtree: &id,
|
||||||
|
GenericAttributes: getGenericAttributes(node.attributes, false),
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
default:
|
default:
|
||||||
@ -121,13 +134,13 @@ func saveDir(t testing.TB, repo restic.BlobSaver, nodes map[string]Node, inode u
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveSnapshot(t testing.TB, repo restic.Repository, snapshot Snapshot) (*restic.Snapshot, restic.ID) {
|
func saveSnapshot(t testing.TB, repo restic.Repository, snapshot Snapshot, getGenericAttributes func(attr *FileAttributes, isDir bool) (genericAttributes map[restic.GenericAttributeType]json.RawMessage)) (*restic.Snapshot, restic.ID) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
wg, wgCtx := errgroup.WithContext(ctx)
|
wg, wgCtx := errgroup.WithContext(ctx)
|
||||||
repo.StartPackUploader(wgCtx, wg)
|
repo.StartPackUploader(wgCtx, wg)
|
||||||
treeID := saveDir(t, repo, snapshot.Nodes, 1000)
|
treeID := saveDir(t, repo, snapshot.Nodes, 1000, getGenericAttributes)
|
||||||
err := repo.Flush(ctx)
|
err := repo.Flush(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -147,6 +160,11 @@ func saveSnapshot(t testing.TB, repo restic.Repository, snapshot Snapshot) (*res
|
|||||||
return sn, id
|
return sn, id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var noopGetGenericAttributes = func(attr *FileAttributes, isDir bool) (genericAttributes map[restic.GenericAttributeType]json.RawMessage) {
|
||||||
|
// No-op
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestRestorer(t *testing.T) {
|
func TestRestorer(t *testing.T) {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
Snapshot
|
Snapshot
|
||||||
@ -322,7 +340,7 @@ func TestRestorer(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
repo := repository.TestRepository(t)
|
repo := repository.TestRepository(t)
|
||||||
sn, id := saveSnapshot(t, repo, test.Snapshot)
|
sn, id := saveSnapshot(t, repo, test.Snapshot, noopGetGenericAttributes)
|
||||||
t.Logf("snapshot saved as %v", id.Str())
|
t.Logf("snapshot saved as %v", id.Str())
|
||||||
|
|
||||||
res := NewRestorer(repo, sn, false, nil)
|
res := NewRestorer(repo, sn, false, nil)
|
||||||
@ -439,7 +457,7 @@ func TestRestorerRelative(t *testing.T) {
|
|||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
repo := repository.TestRepository(t)
|
repo := repository.TestRepository(t)
|
||||||
|
|
||||||
sn, id := saveSnapshot(t, repo, test.Snapshot)
|
sn, id := saveSnapshot(t, repo, test.Snapshot, noopGetGenericAttributes)
|
||||||
t.Logf("snapshot saved as %v", id.Str())
|
t.Logf("snapshot saved as %v", id.Str())
|
||||||
|
|
||||||
res := NewRestorer(repo, sn, false, nil)
|
res := NewRestorer(repo, sn, false, nil)
|
||||||
@ -669,7 +687,7 @@ func TestRestorerTraverseTree(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
repo := repository.TestRepository(t)
|
repo := repository.TestRepository(t)
|
||||||
sn, _ := saveSnapshot(t, repo, test.Snapshot)
|
sn, _ := saveSnapshot(t, repo, test.Snapshot, noopGetGenericAttributes)
|
||||||
|
|
||||||
res := NewRestorer(repo, sn, false, nil)
|
res := NewRestorer(repo, sn, false, nil)
|
||||||
|
|
||||||
@ -745,7 +763,7 @@ func TestRestorerConsistentTimestampsAndPermissions(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}, noopGetGenericAttributes)
|
||||||
|
|
||||||
res := NewRestorer(repo, sn, false, nil)
|
res := NewRestorer(repo, sn, false, nil)
|
||||||
|
|
||||||
@ -800,7 +818,7 @@ func TestVerifyCancel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
repo := repository.TestRepository(t)
|
repo := repository.TestRepository(t)
|
||||||
sn, _ := saveSnapshot(t, repo, snapshot)
|
sn, _ := saveSnapshot(t, repo, snapshot, noopGetGenericAttributes)
|
||||||
|
|
||||||
res := NewRestorer(repo, sn, false, nil)
|
res := NewRestorer(repo, sn, false, nil)
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ func TestRestorerRestoreEmptyHardlinkedFileds(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}, noopGetGenericAttributes)
|
||||||
|
|
||||||
res := NewRestorer(repo, sn, false, nil)
|
res := NewRestorer(repo, sn, false, nil)
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ func TestRestorerProgressBar(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"file2": File{Links: 1, Inode: 2, Data: "example"},
|
"file2": File{Links: 1, Inode: 2, Data: "example"},
|
||||||
},
|
},
|
||||||
})
|
}, noopGetGenericAttributes)
|
||||||
|
|
||||||
mock := &printerMock{}
|
mock := &printerMock{}
|
||||||
progress := restoreui.NewProgress(mock, 0)
|
progress := restoreui.NewProgress(mock, 0)
|
||||||
|
@ -4,11 +4,20 @@
|
|||||||
package restorer
|
package restorer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/repository"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"github.com/restic/restic/internal/test"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
@ -33,3 +42,500 @@ func getBlockCount(t *testing.T, filename string) int64 {
|
|||||||
|
|
||||||
return int64(math.Ceil(float64(result) / 512))
|
return int64(math.Ceil(float64(result) / 512))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DataStreamInfo struct {
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeInfo struct {
|
||||||
|
DataStreamInfo
|
||||||
|
parentDir string
|
||||||
|
attributes FileAttributes
|
||||||
|
Exists bool
|
||||||
|
IsDirectory bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileAttributeCombination(t *testing.T) {
|
||||||
|
testFileAttributeCombination(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyFileAttributeCombination(t *testing.T) {
|
||||||
|
testFileAttributeCombination(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFileAttributeCombination(t *testing.T, isEmpty bool) {
|
||||||
|
t.Parallel()
|
||||||
|
//Generate combination of 5 attributes.
|
||||||
|
attributeCombinations := generateCombinations(5, []bool{})
|
||||||
|
|
||||||
|
fileName := "TestFile.txt"
|
||||||
|
// Iterate through each attribute combination
|
||||||
|
for _, attr1 := range attributeCombinations {
|
||||||
|
|
||||||
|
//Set up the required file information
|
||||||
|
fileInfo := NodeInfo{
|
||||||
|
DataStreamInfo: getDataStreamInfo(isEmpty, fileName),
|
||||||
|
parentDir: "dir",
|
||||||
|
attributes: getFileAttributes(attr1),
|
||||||
|
Exists: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get the current test name
|
||||||
|
testName := getCombinationTestName(fileInfo, fileName, fileInfo.attributes)
|
||||||
|
|
||||||
|
//Run test
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
mainFilePath := runAttributeTests(t, fileInfo, fileInfo.attributes)
|
||||||
|
|
||||||
|
verifyFileRestores(isEmpty, mainFilePath, t, fileInfo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateCombinations(n int, prefix []bool) [][]bool {
|
||||||
|
if n == 0 {
|
||||||
|
// Return a slice containing the current permutation
|
||||||
|
return [][]bool{append([]bool{}, prefix...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate combinations with True
|
||||||
|
prefixTrue := append(prefix, true)
|
||||||
|
permsTrue := generateCombinations(n-1, prefixTrue)
|
||||||
|
|
||||||
|
// Generate combinations with False
|
||||||
|
prefixFalse := append(prefix, false)
|
||||||
|
permsFalse := generateCombinations(n-1, prefixFalse)
|
||||||
|
|
||||||
|
// Combine combinations with True and False
|
||||||
|
return append(permsTrue, permsFalse...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDataStreamInfo(isEmpty bool, fileName string) DataStreamInfo {
|
||||||
|
var dataStreamInfo DataStreamInfo
|
||||||
|
if isEmpty {
|
||||||
|
dataStreamInfo = DataStreamInfo{
|
||||||
|
name: fileName,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataStreamInfo = DataStreamInfo{
|
||||||
|
name: fileName,
|
||||||
|
data: "Main file data stream.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataStreamInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFileAttributes(values []bool) FileAttributes {
|
||||||
|
return FileAttributes{
|
||||||
|
ReadOnly: values[0],
|
||||||
|
Hidden: values[1],
|
||||||
|
System: values[2],
|
||||||
|
Archive: values[3],
|
||||||
|
Encrypted: values[4],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCombinationTestName(fi NodeInfo, fileName string, overwriteAttr FileAttributes) string {
|
||||||
|
if fi.attributes.ReadOnly {
|
||||||
|
fileName += "-ReadOnly"
|
||||||
|
}
|
||||||
|
if fi.attributes.Hidden {
|
||||||
|
fileName += "-Hidden"
|
||||||
|
}
|
||||||
|
if fi.attributes.System {
|
||||||
|
fileName += "-System"
|
||||||
|
}
|
||||||
|
if fi.attributes.Archive {
|
||||||
|
fileName += "-Archive"
|
||||||
|
}
|
||||||
|
if fi.attributes.Encrypted {
|
||||||
|
fileName += "-Encrypted"
|
||||||
|
}
|
||||||
|
if fi.Exists {
|
||||||
|
fileName += "-Overwrite"
|
||||||
|
if overwriteAttr.ReadOnly {
|
||||||
|
fileName += "-R"
|
||||||
|
}
|
||||||
|
if overwriteAttr.Hidden {
|
||||||
|
fileName += "-H"
|
||||||
|
}
|
||||||
|
if overwriteAttr.System {
|
||||||
|
fileName += "-S"
|
||||||
|
}
|
||||||
|
if overwriteAttr.Archive {
|
||||||
|
fileName += "-A"
|
||||||
|
}
|
||||||
|
if overwriteAttr.Encrypted {
|
||||||
|
fileName += "-E"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAttributeTests(t *testing.T, fileInfo NodeInfo, existingFileAttr FileAttributes) string {
|
||||||
|
testDir := t.TempDir()
|
||||||
|
res, _ := setupWithFileAttributes(t, fileInfo, testDir, existingFileAttr)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := res.RestoreTo(ctx, testDir)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
|
||||||
|
mainFilePath := path.Join(testDir, fileInfo.parentDir, fileInfo.name)
|
||||||
|
//Verify restore
|
||||||
|
verifyFileAttributes(t, mainFilePath, fileInfo.attributes)
|
||||||
|
return mainFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupWithFileAttributes(t *testing.T, nodeInfo NodeInfo, testDir string, existingFileAttr FileAttributes) (*Restorer, []int) {
|
||||||
|
t.Helper()
|
||||||
|
if nodeInfo.Exists {
|
||||||
|
if !nodeInfo.IsDirectory {
|
||||||
|
err := os.MkdirAll(path.Join(testDir, nodeInfo.parentDir), os.ModeDir)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
filepath := path.Join(testDir, nodeInfo.parentDir, nodeInfo.name)
|
||||||
|
if existingFileAttr.Encrypted {
|
||||||
|
err := createEncryptedFileWriteData(filepath, nodeInfo)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
} else {
|
||||||
|
// Write the data to the file
|
||||||
|
file, err := os.OpenFile(path.Clean(filepath), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
_, err = file.Write([]byte(nodeInfo.data))
|
||||||
|
rtest.OK(t, err)
|
||||||
|
|
||||||
|
err = file.Close()
|
||||||
|
rtest.OK(t, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := os.MkdirAll(path.Join(testDir, nodeInfo.parentDir, nodeInfo.name), os.ModeDir)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pathPointer, err := syscall.UTF16PtrFromString(path.Join(testDir, nodeInfo.parentDir, nodeInfo.name))
|
||||||
|
rtest.OK(t, err)
|
||||||
|
syscall.SetFileAttributes(pathPointer, getAttributeValue(&existingFileAttr))
|
||||||
|
}
|
||||||
|
|
||||||
|
index := 0
|
||||||
|
|
||||||
|
order := []int{}
|
||||||
|
streams := []DataStreamInfo{}
|
||||||
|
if !nodeInfo.IsDirectory {
|
||||||
|
order = append(order, index)
|
||||||
|
index++
|
||||||
|
streams = append(streams, nodeInfo.DataStreamInfo)
|
||||||
|
}
|
||||||
|
return setup(t, getNodes(nodeInfo.parentDir, nodeInfo.name, order, streams, nodeInfo.IsDirectory, &nodeInfo.attributes)), order
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEncryptedFileWriteData(filepath string, fileInfo NodeInfo) (err error) {
|
||||||
|
var ptr *uint16
|
||||||
|
if ptr, err = windows.UTF16PtrFromString(filepath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var handle windows.Handle
|
||||||
|
//Create the file with encrypted flag
|
||||||
|
if handle, err = windows.CreateFile(ptr, uint32(windows.GENERIC_READ|windows.GENERIC_WRITE), uint32(windows.FILE_SHARE_READ), nil, uint32(windows.CREATE_ALWAYS), windows.FILE_ATTRIBUTE_ENCRYPTED, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//Write data to file
|
||||||
|
if _, err = windows.Write(handle, []byte(fileInfo.data)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//Close handle
|
||||||
|
return windows.CloseHandle(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup(t *testing.T, nodesMap map[string]Node) *Restorer {
|
||||||
|
repo := repository.TestRepository(t)
|
||||||
|
getFileAttributes := func(attr *FileAttributes, isDir bool) (genericAttributes map[restic.GenericAttributeType]json.RawMessage) {
|
||||||
|
if attr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileattr := getAttributeValue(attr)
|
||||||
|
|
||||||
|
if isDir {
|
||||||
|
//If the node is a directory add FILE_ATTRIBUTE_DIRECTORY to attributes
|
||||||
|
fileattr |= windows.FILE_ATTRIBUTE_DIRECTORY
|
||||||
|
}
|
||||||
|
attrs, err := restic.WindowsAttrsToGenericAttributes(restic.WindowsAttributes{FileAttributes: &fileattr})
|
||||||
|
test.OK(t, err)
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
sn, _ := saveSnapshot(t, repo, Snapshot{
|
||||||
|
Nodes: nodesMap,
|
||||||
|
}, getFileAttributes)
|
||||||
|
res := NewRestorer(repo, sn, false, nil)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAttributeValue(attr *FileAttributes) uint32 {
|
||||||
|
var fileattr uint32
|
||||||
|
if attr.ReadOnly {
|
||||||
|
fileattr |= windows.FILE_ATTRIBUTE_READONLY
|
||||||
|
}
|
||||||
|
if attr.Hidden {
|
||||||
|
fileattr |= windows.FILE_ATTRIBUTE_HIDDEN
|
||||||
|
}
|
||||||
|
if attr.Encrypted {
|
||||||
|
fileattr |= windows.FILE_ATTRIBUTE_ENCRYPTED
|
||||||
|
}
|
||||||
|
if attr.Archive {
|
||||||
|
fileattr |= windows.FILE_ATTRIBUTE_ARCHIVE
|
||||||
|
}
|
||||||
|
if attr.System {
|
||||||
|
fileattr |= windows.FILE_ATTRIBUTE_SYSTEM
|
||||||
|
}
|
||||||
|
return fileattr
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNodes(dir string, mainNodeName string, order []int, streams []DataStreamInfo, isDirectory bool, attributes *FileAttributes) map[string]Node {
|
||||||
|
var mode os.FileMode
|
||||||
|
if isDirectory {
|
||||||
|
mode = os.FileMode(2147484159)
|
||||||
|
} else {
|
||||||
|
if attributes != nil && attributes.ReadOnly {
|
||||||
|
mode = os.FileMode(0o444)
|
||||||
|
} else {
|
||||||
|
mode = os.FileMode(0o666)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileNodes := func() map[string]Node {
|
||||||
|
nodes := map[string]Node{}
|
||||||
|
if isDirectory {
|
||||||
|
//Add a directory node at the same level as the other streams
|
||||||
|
nodes[mainNodeName] = Dir{
|
||||||
|
ModTime: time.Now(),
|
||||||
|
attributes: attributes,
|
||||||
|
Mode: mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(streams) > 0 {
|
||||||
|
for _, index := range order {
|
||||||
|
stream := streams[index]
|
||||||
|
|
||||||
|
var attr *FileAttributes = nil
|
||||||
|
if mainNodeName == stream.name {
|
||||||
|
attr = attributes
|
||||||
|
} else if attributes != nil && attributes.Encrypted {
|
||||||
|
//Set encrypted attribute
|
||||||
|
attr = &FileAttributes{Encrypted: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes[stream.name] = File{
|
||||||
|
ModTime: time.Now(),
|
||||||
|
Data: stream.data,
|
||||||
|
Mode: mode,
|
||||||
|
attributes: attr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]Node{
|
||||||
|
dir: Dir{
|
||||||
|
Mode: normalizeFileMode(0750 | mode),
|
||||||
|
ModTime: time.Now(),
|
||||||
|
Nodes: getFileNodes(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyFileAttributes(t *testing.T, mainFilePath string, attr FileAttributes) {
|
||||||
|
ptr, err := windows.UTF16PtrFromString(mainFilePath)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
//Get file attributes using syscall
|
||||||
|
fileAttributes, err := syscall.GetFileAttributes(ptr)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
//Test positive and negative scenarios
|
||||||
|
if attr.ReadOnly {
|
||||||
|
rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_READONLY != 0, "Expected read only attibute.")
|
||||||
|
} else {
|
||||||
|
rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_READONLY == 0, "Unexpected read only attibute.")
|
||||||
|
}
|
||||||
|
if attr.Hidden {
|
||||||
|
rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_HIDDEN != 0, "Expected hidden attibute.")
|
||||||
|
} else {
|
||||||
|
rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_HIDDEN == 0, "Unexpected hidden attibute.")
|
||||||
|
}
|
||||||
|
if attr.System {
|
||||||
|
rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_SYSTEM != 0, "Expected system attibute.")
|
||||||
|
} else {
|
||||||
|
rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_SYSTEM == 0, "Unexpected system attibute.")
|
||||||
|
}
|
||||||
|
if attr.Archive {
|
||||||
|
rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ARCHIVE != 0, "Expected archive attibute.")
|
||||||
|
} else {
|
||||||
|
rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ARCHIVE == 0, "Unexpected archive attibute.")
|
||||||
|
}
|
||||||
|
if attr.Encrypted {
|
||||||
|
rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ENCRYPTED != 0, "Expected encrypted attibute.")
|
||||||
|
} else {
|
||||||
|
rtest.Assert(t, fileAttributes&windows.FILE_ATTRIBUTE_ENCRYPTED == 0, "Unexpected encrypted attibute.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyFileRestores(isEmpty bool, mainFilePath string, t *testing.T, fileInfo NodeInfo) {
|
||||||
|
if isEmpty {
|
||||||
|
_, err1 := os.Stat(mainFilePath)
|
||||||
|
rtest.Assert(t, !errors.Is(err1, os.ErrNotExist), "The file "+fileInfo.name+" does not exist")
|
||||||
|
} else {
|
||||||
|
|
||||||
|
verifyMainFileRestore(t, mainFilePath, fileInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyMainFileRestore(t *testing.T, mainFilePath string, fileInfo NodeInfo) {
|
||||||
|
fi, err1 := os.Stat(mainFilePath)
|
||||||
|
rtest.Assert(t, !errors.Is(err1, os.ErrNotExist), "The file "+fileInfo.name+" does not exist")
|
||||||
|
|
||||||
|
size := fi.Size()
|
||||||
|
rtest.Assert(t, size > 0, "The file "+fileInfo.name+" exists but is empty")
|
||||||
|
|
||||||
|
content, err := os.ReadFile(mainFilePath)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
rtest.Assert(t, string(content) == fileInfo.data, "The file "+fileInfo.name+" exists but the content is not overwritten")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirAttributeCombination(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
attributeCombinations := generateCombinations(4, []bool{})
|
||||||
|
|
||||||
|
dirName := "TestDir"
|
||||||
|
// Iterate through each attribute combination
|
||||||
|
for _, attr1 := range attributeCombinations {
|
||||||
|
|
||||||
|
//Set up the required directory information
|
||||||
|
dirInfo := NodeInfo{
|
||||||
|
DataStreamInfo: DataStreamInfo{
|
||||||
|
name: dirName,
|
||||||
|
},
|
||||||
|
parentDir: "dir",
|
||||||
|
attributes: getDirFileAttributes(attr1),
|
||||||
|
Exists: false,
|
||||||
|
IsDirectory: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get the current test name
|
||||||
|
testName := getCombinationTestName(dirInfo, dirName, dirInfo.attributes)
|
||||||
|
|
||||||
|
//Run test
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
mainDirPath := runAttributeTests(t, dirInfo, dirInfo.attributes)
|
||||||
|
|
||||||
|
//Check directory exists
|
||||||
|
_, err1 := os.Stat(mainDirPath)
|
||||||
|
rtest.Assert(t, !errors.Is(err1, os.ErrNotExist), "The directory "+dirInfo.name+" does not exist")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDirFileAttributes(values []bool) FileAttributes {
|
||||||
|
return FileAttributes{
|
||||||
|
// readonly not valid for directories
|
||||||
|
Hidden: values[0],
|
||||||
|
System: values[1],
|
||||||
|
Archive: values[2],
|
||||||
|
Encrypted: values[3],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileAttributeCombinationsOverwrite(t *testing.T) {
|
||||||
|
testFileAttributeCombinationsOverwrite(t, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyFileAttributeCombinationsOverwrite(t *testing.T) {
|
||||||
|
testFileAttributeCombinationsOverwrite(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFileAttributeCombinationsOverwrite(t *testing.T, isEmpty bool) {
|
||||||
|
t.Parallel()
|
||||||
|
//Get attribute combinations
|
||||||
|
attributeCombinations := generateCombinations(5, []bool{})
|
||||||
|
//Get overwrite file attribute combinations
|
||||||
|
overwriteCombinations := generateCombinations(5, []bool{})
|
||||||
|
|
||||||
|
fileName := "TestOverwriteFile"
|
||||||
|
|
||||||
|
//Iterate through each attribute combination
|
||||||
|
for _, attr1 := range attributeCombinations {
|
||||||
|
|
||||||
|
fileInfo := NodeInfo{
|
||||||
|
DataStreamInfo: getDataStreamInfo(isEmpty, fileName),
|
||||||
|
parentDir: "dir",
|
||||||
|
attributes: getFileAttributes(attr1),
|
||||||
|
Exists: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
overwriteFileAttributes := []FileAttributes{}
|
||||||
|
|
||||||
|
for _, overwrite := range overwriteCombinations {
|
||||||
|
overwriteFileAttributes = append(overwriteFileAttributes, getFileAttributes(overwrite))
|
||||||
|
}
|
||||||
|
|
||||||
|
//Iterate through each overwrite attribute combination
|
||||||
|
for _, overwriteFileAttr := range overwriteFileAttributes {
|
||||||
|
//Get the test name
|
||||||
|
testName := getCombinationTestName(fileInfo, fileName, overwriteFileAttr)
|
||||||
|
|
||||||
|
//Run test
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
mainFilePath := runAttributeTests(t, fileInfo, overwriteFileAttr)
|
||||||
|
|
||||||
|
verifyFileRestores(isEmpty, mainFilePath, t, fileInfo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirAttributeCombinationsOverwrite(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
//Get attribute combinations
|
||||||
|
attributeCombinations := generateCombinations(4, []bool{})
|
||||||
|
//Get overwrite dir attribute combinations
|
||||||
|
overwriteCombinations := generateCombinations(4, []bool{})
|
||||||
|
|
||||||
|
dirName := "TestOverwriteDir"
|
||||||
|
|
||||||
|
//Iterate through each attribute combination
|
||||||
|
for _, attr1 := range attributeCombinations {
|
||||||
|
|
||||||
|
dirInfo := NodeInfo{
|
||||||
|
DataStreamInfo: DataStreamInfo{
|
||||||
|
name: dirName,
|
||||||
|
},
|
||||||
|
parentDir: "dir",
|
||||||
|
attributes: getDirFileAttributes(attr1),
|
||||||
|
Exists: true,
|
||||||
|
IsDirectory: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
overwriteDirFileAttributes := []FileAttributes{}
|
||||||
|
|
||||||
|
for _, overwrite := range overwriteCombinations {
|
||||||
|
overwriteDirFileAttributes = append(overwriteDirFileAttributes, getDirFileAttributes(overwrite))
|
||||||
|
}
|
||||||
|
|
||||||
|
//Iterate through each overwrite attribute combinations
|
||||||
|
for _, overwriteDirAttr := range overwriteDirFileAttributes {
|
||||||
|
//Get the test name
|
||||||
|
testName := getCombinationTestName(dirInfo, dirName, overwriteDirAttr)
|
||||||
|
|
||||||
|
//Run test
|
||||||
|
t.Run(testName, func(t *testing.T) {
|
||||||
|
mainDirPath := runAttributeTests(t, dirInfo, dirInfo.attributes)
|
||||||
|
|
||||||
|
//Check directory exists
|
||||||
|
_, err1 := os.Stat(mainDirPath)
|
||||||
|
rtest.Assert(t, !errors.Is(err1, os.ErrNotExist), "The directory "+dirInfo.name+" does not exist")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ package test
|
|||||||
import (
|
import (
|
||||||
"compress/bzip2"
|
"compress/bzip2"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -47,10 +48,22 @@ func OKs(tb testing.TB, errs []error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Equals fails the test if exp is not equal to act.
|
// Equals fails the test if exp is not equal to act.
|
||||||
func Equals(tb testing.TB, exp, act interface{}) {
|
// msg is optional message to be printed, first param being format string and rest being arguments.
|
||||||
|
func Equals(tb testing.TB, exp, act interface{}, msgs ...string) {
|
||||||
tb.Helper()
|
tb.Helper()
|
||||||
if !reflect.DeepEqual(exp, act) {
|
if !reflect.DeepEqual(exp, act) {
|
||||||
tb.Fatalf("\033[31m\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", exp, act)
|
var msgString string
|
||||||
|
length := len(msgs)
|
||||||
|
if length == 1 {
|
||||||
|
msgString = msgs[0]
|
||||||
|
} else if length > 1 {
|
||||||
|
args := make([]interface{}, length-1)
|
||||||
|
for i, msg := range msgs[1:] {
|
||||||
|
args[i] = msg
|
||||||
|
}
|
||||||
|
msgString = fmt.Sprintf(msgs[0], args...)
|
||||||
|
}
|
||||||
|
tb.Fatalf("\033[31m\n\n\t"+msgString+"\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", exp, act)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user