Support windows metadata using generic attribs

Add new generic_attributes attribute in Node.
Use the generic attributes to add support for creation time and file attributes like hidden, readonly, encrypted in windows. Handle permission errors for readonly files in windows.
Handle backup and restore of encrypted attributes using windows system calls.
This commit is contained in:
Aneesh Nireshwalia 2024-02-22 17:31:20 -07:00
parent 62a8a599f1
commit 0962917974
No known key found for this signature in database
GPG Key ID: 6F5A52831C046F44
11 changed files with 591 additions and 60 deletions

View File

@ -126,6 +126,7 @@ func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
// Make the following attributes disappear
Name byte `json:"name,omitempty"`
ExtendedAttributes byte `json:"extended_attributes,omitempty"`
GenericAttributes byte `json:"generic_attributes,omitempty"`
Device byte `json:"device,omitempty"`
Content byte `json:"content,omitempty"`
Subtree byte `json:"subtree,omitempty"`

View File

@ -124,3 +124,17 @@ func RemoveIfExists(filename string) error {
func Chtimes(name string, atime time.Time, mtime time.Time) error {
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
}

View File

@ -77,3 +77,29 @@ func TempFile(dir, prefix string) (f *os.File, err error) {
func Chmod(name string, mode os.FileMode) error {
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
}

View File

@ -6,7 +6,9 @@ import (
"fmt"
"os"
"os/user"
"reflect"
"strconv"
"strings"
"sync"
"syscall"
"time"
@ -20,12 +22,53 @@ import (
"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 {
Name string `json:"name"`
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.
type Node struct {
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,
// as LinkTarget without breaking backwards-compatibility.
// Must only be set of the linktarget cannot be encoded as valid utf8.
LinkTargetRaw []byte `json:"linktarget_raw,omitempty"`
ExtendedAttributes []ExtendedAttribute `json:"extended_attributes,omitempty"`
Device uint64 `json:"device,omitempty"` // in case of Type == "dev", stat.st_rdev
Content IDs `json:"content"`
Subtree *ID `json:"subtree,omitempty"`
LinkTargetRaw []byte `json:"linktarget_raw,omitempty"`
ExtendedAttributes []ExtendedAttribute `json:"extended_attributes,omitempty"`
GenericAttributes map[GenericAttributeType]json.RawMessage `json:"generic_attributes,omitempty"`
Device uint64 `json:"device,omitempty"` // in case of Type == "dev", stat.st_rdev
Content IDs `json:"content"`
Subtree *ID `json:"subtree,omitempty"`
Error string `json:"error,omitempty"`
@ -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 {
debug.Log("error restoring timestamps for dir %v: %v", path, err)
if firsterr != nil {
@ -225,6 +261,24 @@ func (node Node) restoreMetadata(path string) error {
}
}
if err := node.restoreGenericAttributes(path); 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
}
@ -438,6 +492,9 @@ func (node Node) Equals(other Node) bool {
if !node.sameExtendedAttributes(other) {
return false
}
if !node.sameGenericAttributes(other) {
return false
}
if node.Subtree != nil {
if other.Subtree == nil {
return false
@ -480,8 +537,13 @@ func (node Node) sameContent(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
} else if ln == 0 {
// This means lo is also of length 0
return true
}
// build a set of all attributes that node has
@ -525,6 +587,33 @@ func (node Node) sameExtendedAttributes(other Node) bool {
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) {
uid, gid := stat.uid(), stat.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 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 {
@ -665,3 +764,119 @@ func (node *Node) fillTimes(stat *statT) {
node.ChangeTime = time.Unix(ctim.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) {
for _, unknownAttrib := range unknownAttribs {
handleUnknownGenericAttributeFound(unknownAttrib)
}
}
// handleUnknownGenericAttributeFound is used for handling and distinguing between scenarios related to future versions and cross-OS repositories
func handleUnknownGenericAttributeFound(genericAttributeType GenericAttributeType) {
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.
debug.Log("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() error {
for name := range node.GenericAttributes {
handleUnknownGenericAttributeFound(name)
}
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
}

View File

@ -3,9 +3,12 @@
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
}
@ -34,3 +37,13 @@ func Listxattr(path string) ([]string, error) {
func Setxattr(path, name string, data []byte) error {
return nil
}
// restoreGenericAttributes is no-op on AIX.
func (node *Node) restoreGenericAttributes(_ string) error {
return node.handleAllUnknownGenericAttributesFound()
}
// fillGenericAttributes is a no-op on AIX.
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil
}

View File

@ -1,8 +1,11 @@
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
}
@ -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) 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) {
return nil, nil
}
// Listxattr retrieves a list of names of extended attributes associated with the
// given path in the file system.
// Listxattr is a no-op on netbsd.
func Listxattr(path string) ([]string, error) {
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 {
return nil
}
// restoreGenericAttributes is no-op on netbsd.
func (node *Node) restoreGenericAttributes(_ string) error {
return node.handleAllUnknownGenericAttributesFound()
}
// fillGenericAttributes is a no-op on netbsd.
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil
}

View File

@ -1,8 +1,11 @@
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
}
@ -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) 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) {
return nil, nil
}
// Listxattr retrieves a list of names of extended attributes associated with the
// given path in the file system.
// Listxattr is a no-op on openbsd.
func Listxattr(path string) ([]string, error) {
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 {
return nil
}
// restoreGenericAttributes is no-op on openbsd.
func (node *Node) restoreGenericAttributes(_ string) error {
return node.handleAllUnknownGenericAttributesFound()
}
// fillGenericAttributes is a no-op on openbsd.
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil
}

View File

@ -1,4 +1,4 @@
package restic_test
package restic
import (
"context"
@ -11,7 +11,6 @@ import (
"testing"
"time"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/test"
rtest "github.com/restic/restic/internal/test"
)
@ -32,7 +31,7 @@ func BenchmarkNodeFillUser(t *testing.B) {
t.ResetTimer()
for i := 0; i < t.N; i++ {
_, err := restic.NodeFromFileInfo(path, fi)
_, err := NodeFromFileInfo(path, fi)
rtest.OK(t, err)
}
@ -56,7 +55,7 @@ func BenchmarkNodeFromFileInfo(t *testing.B) {
t.ResetTimer()
for i := 0; i < t.N; i++ {
_, err := restic.NodeFromFileInfo(path, fi)
_, err := NodeFromFileInfo(path, fi)
if err != nil {
t.Fatal(err)
}
@ -75,11 +74,11 @@ func parseTime(s string) time.Time {
return t.Local()
}
var nodeTests = []restic.Node{
var nodeTests = []Node{
{
Name: "testFile",
Type: "file",
Content: restic.IDs{},
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
@ -90,7 +89,7 @@ var nodeTests = []restic.Node{
{
Name: "testSuidFile",
Type: "file",
Content: restic.IDs{},
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0755 | os.ModeSetuid,
@ -101,7 +100,7 @@ var nodeTests = []restic.Node{
{
Name: "testSuidFile2",
Type: "file",
Content: restic.IDs{},
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0755 | os.ModeSetgid,
@ -112,7 +111,7 @@ var nodeTests = []restic.Node{
{
Name: "testSticky",
Type: "file",
Content: restic.IDs{},
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0755 | os.ModeSticky,
@ -148,7 +147,7 @@ var nodeTests = []restic.Node{
{
Name: "testFile",
Type: "file",
Content: restic.IDs{},
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
@ -170,14 +169,14 @@ var nodeTests = []restic.Node{
{
Name: "testXattrFile",
Type: "file",
Content: restic.IDs{},
Content: IDs{},
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Mode: 0604,
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"),
ExtendedAttributes: []restic.ExtendedAttribute{
ExtendedAttributes: []ExtendedAttribute{
{"user.foo", []byte("bar")},
},
},
@ -191,7 +190,7 @@ var nodeTests = []restic.Node{
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"),
ExtendedAttributes: []restic.ExtendedAttribute{
ExtendedAttributes: []ExtendedAttribute{
{"user.foo", []byte("bar")},
},
},
@ -228,7 +227,7 @@ func TestNodeRestoreAt(t *testing.T) {
fi, err := os.Lstat(nodePath)
rtest.OK(t, err)
n2, err := restic.NodeFromFileInfo(nodePath, fi)
n2, err := NodeFromFileInfo(nodePath, fi)
rtest.OK(t, err)
rtest.Assert(t, test.Name == n2.Name,
@ -330,7 +329,7 @@ func TestFixTime(t *testing.T) {
for _, test := range tests {
t.Run("", func(t *testing.T) {
res := restic.FixTime(test.src)
res := FixTime(test.src)
if !res.Equal(test.want) {
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ǵ",
string([]byte{0, 1, 2, 0xfa, 0xfb, 0xfc}),
} {
n := restic.Node{
n := Node{
LinkTarget: link,
}
ser, err := json.Marshal(n)
test.OK(t, err)
var n2 restic.Node
var n2 Node
err = json.Unmarshal(ser, &n2)
test.OK(t, err)
fmt.Println(string(ser))
@ -365,7 +364,7 @@ func TestSymlinkSerializationFormat(t *testing.T) {
{`{"linktarget":"test"}`, "test"},
{`{"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)
test.OK(t, err)
test.Equals(t, d.linkTarget, n2.LinkTarget)

View File

@ -1,21 +1,47 @@
package restic
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"syscall"
"unsafe"
"github.com/restic/restic/internal/debug"
"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.
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")
}
// 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
}
// restoreSymlinkTimestamps restores timestamps for symlinks
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
// tweaked version of UtimesNano from go/src/syscall/syscall_windows.go
pathp, e := syscall.UTF16PtrFromString(path)
@ -28,7 +54,14 @@ func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespe
if e != nil {
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]))
w := syscall.NsecToFiletime(syscall.TimespecToNsec(utimes[1]))
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.
return syscall.NsecToTimespec(s.LastWriteTime.Nanoseconds())
}
// restoreGenericAttributes restores generic attributes for Windows
func (node Node) restoreGenericAttributes(path 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)
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
}
}

View File

@ -4,6 +4,7 @@
package restic
import (
"os"
"syscall"
"github.com/restic/restic/internal/errors"
@ -47,3 +48,13 @@ func handleXattrErr(err error) error {
return errors.WithStack(e)
}
}
// restoreGenericAttributes is no-op.
func (node *Node) restoreGenericAttributes(_ string) error {
return node.handleAllUnknownGenericAttributesFound()
}
// fillGenericAttributes is a no-op.
func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
return true, nil
}

View File

@ -50,16 +50,26 @@ func (w *filesWriter) writeToFile(path string, blob []byte, offset int64, create
bucket.files[path].users++
return wr, nil
}
var flags int
var f *os.File
var err error
if createSize >= 0 {
flags = os.O_CREATE | os.O_TRUNC | os.O_WRONLY
} else {
flags = os.O_WRONLY
}
f, err := os.OpenFile(path, flags, 0600)
if err != nil {
if f, err = os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600); err != nil {
if fs.IsAccessDenied(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(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
}