mirror of
https://github.com/octoleo/restic.git
synced 2024-11-23 05:12:10 +00:00
fs: replace statT with ExtendedFileInfo
This commit is contained in:
parent
6d3a5260d3
commit
f0329bb4e6
@ -6,7 +6,6 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
@ -57,8 +56,7 @@ func nodeTypeFromFileInfo(fi os.FileInfo) restic.NodeType {
|
||||
}
|
||||
|
||||
func nodeFillExtra(node *restic.Node, path string, fi os.FileInfo, ignoreXattrListError bool) error {
|
||||
stat, ok := toStatT(fi.Sys())
|
||||
if !ok {
|
||||
if fi.Sys() == nil {
|
||||
// fill minimal info with current values for uid, gid
|
||||
node.UID = uint32(os.Getuid())
|
||||
node.GID = uint32(os.Getgid())
|
||||
@ -66,38 +64,43 @@ func nodeFillExtra(node *restic.Node, path string, fi os.FileInfo, ignoreXattrLi
|
||||
return nil
|
||||
}
|
||||
|
||||
node.Inode = uint64(stat.ino())
|
||||
node.DeviceID = uint64(stat.dev())
|
||||
stat := ExtendedStat(fi)
|
||||
|
||||
nodeFillTimes(node, stat)
|
||||
node.Inode = stat.Inode
|
||||
node.DeviceID = stat.DeviceID
|
||||
node.ChangeTime = stat.ChangeTime
|
||||
node.AccessTime = stat.AccessTime
|
||||
|
||||
nodeFillUser(node, stat)
|
||||
node.UID = stat.UID
|
||||
node.GID = stat.GID
|
||||
node.User = lookupUsername(stat.UID)
|
||||
node.Group = lookupGroup(stat.GID)
|
||||
|
||||
switch node.Type {
|
||||
case restic.NodeTypeFile:
|
||||
node.Size = uint64(stat.size())
|
||||
node.Links = uint64(stat.nlink())
|
||||
node.Size = uint64(stat.Size)
|
||||
node.Links = stat.Links
|
||||
case restic.NodeTypeDir:
|
||||
case restic.NodeTypeSymlink:
|
||||
var err error
|
||||
node.LinkTarget, err = os.Readlink(fixpath(path))
|
||||
node.Links = uint64(stat.nlink())
|
||||
node.Links = stat.Links
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
case restic.NodeTypeDev:
|
||||
node.Device = uint64(stat.rdev())
|
||||
node.Links = uint64(stat.nlink())
|
||||
node.Device = stat.Device
|
||||
node.Links = stat.Links
|
||||
case restic.NodeTypeCharDev:
|
||||
node.Device = uint64(stat.rdev())
|
||||
node.Links = uint64(stat.nlink())
|
||||
node.Device = stat.Device
|
||||
node.Links = stat.Links
|
||||
case restic.NodeTypeFifo:
|
||||
case restic.NodeTypeSocket:
|
||||
default:
|
||||
return errors.Errorf("unsupported file type %q", node.Type)
|
||||
}
|
||||
|
||||
allowExtended, err := nodeFillGenericAttributes(node, path, fi, stat)
|
||||
allowExtended, err := nodeFillGenericAttributes(node, path, &stat)
|
||||
if allowExtended {
|
||||
// Skip processing ExtendedAttributes if allowExtended is false.
|
||||
err = errors.CombineErrors(err, nodeFillExtendedAttributes(node, path, ignoreXattrListError))
|
||||
@ -105,20 +108,6 @@ func nodeFillExtra(node *restic.Node, path string, fi os.FileInfo, ignoreXattrLi
|
||||
return err
|
||||
}
|
||||
|
||||
func nodeFillTimes(node *restic.Node, stat *statT) {
|
||||
ctim := stat.ctim()
|
||||
atim := stat.atim()
|
||||
node.ChangeTime = time.Unix(ctim.Unix())
|
||||
node.AccessTime = time.Unix(atim.Unix())
|
||||
}
|
||||
|
||||
func nodeFillUser(node *restic.Node, stat *statT) {
|
||||
uid, gid := stat.uid(), stat.gid()
|
||||
node.UID, node.GID = uid, gid
|
||||
node.User = lookupUsername(uid)
|
||||
node.Group = lookupGroup(gid)
|
||||
}
|
||||
|
||||
var (
|
||||
uidLookupCache = make(map[uint32]string)
|
||||
uidLookupCacheMutex = sync.RWMutex{}
|
||||
|
@ -4,7 +4,6 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
@ -14,17 +13,6 @@ func nodeRestoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AIX has a funny timespec type in syscall, with 32-bit nanoseconds.
|
||||
// golang.org/x/sys/unix handles this cleanly, but we're stuck with syscall
|
||||
// because os.Stat returns a syscall type in its os.FileInfo.Sys().
|
||||
func toTimespec(t syscall.StTimespec_t) syscall.Timespec {
|
||||
return syscall.Timespec{Sec: t.Sec, Nsec: int64(t.Nsec)}
|
||||
}
|
||||
|
||||
func (s statT) atim() syscall.Timespec { return toTimespec(s.Atim) }
|
||||
func (s statT) mtim() syscall.Timespec { return toTimespec(s.Mtim) }
|
||||
func (s statT) ctim() syscall.Timespec { return toTimespec(s.Ctim) }
|
||||
|
||||
// nodeRestoreExtendedAttributes is a no-op on AIX.
|
||||
func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error {
|
||||
return nil
|
||||
@ -46,6 +34,6 @@ func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg str
|
||||
}
|
||||
|
||||
// nodeFillGenericAttributes is a no-op on AIX.
|
||||
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
|
||||
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ *ExtendedFileInfo) (allowExtended bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
@ -5,7 +5,3 @@ import "syscall"
|
||||
func nodeRestoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 }
|
||||
|
@ -12,7 +12,3 @@ func nodeRestoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error
|
||||
func mknod(path string, mode uint32, dev uint64) (err error) {
|
||||
return syscall.Mknod(path, mode, dev)
|
||||
}
|
||||
|
||||
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 }
|
||||
|
@ -31,7 +31,3 @@ func nodeRestoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error
|
||||
|
||||
return dir.Close()
|
||||
}
|
||||
|
||||
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 }
|
||||
|
@ -1,7 +1,6 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
@ -11,10 +10,6 @@ func nodeRestoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
// nodeRestoreExtendedAttributes is a no-op on netbsd.
|
||||
func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error {
|
||||
return nil
|
||||
@ -36,6 +31,6 @@ func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg str
|
||||
}
|
||||
|
||||
// nodeFillGenericAttributes is a no-op on netbsd.
|
||||
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
|
||||
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ *ExtendedFileInfo) (allowExtended bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
@ -11,10 +10,6 @@ func nodeRestoreSymlinkTimestamps(_ string, _ [2]syscall.Timespec) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
// nodeRestoreExtendedAttributes is a no-op on openbsd.
|
||||
func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error {
|
||||
return nil
|
||||
@ -36,6 +31,6 @@ func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg str
|
||||
}
|
||||
|
||||
// fillGenericAttributes is a no-op on openbsd.
|
||||
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
|
||||
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ *ExtendedFileInfo) (allowExtended bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
@ -5,7 +5,3 @@ import "syscall"
|
||||
func nodeRestoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 }
|
||||
|
@ -5,27 +5,8 @@ package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func lchown(name string, uid, gid int) error {
|
||||
return os.Lchown(name, uid, gid)
|
||||
}
|
||||
|
||||
type statT syscall.Stat_t
|
||||
|
||||
func toStatT(i interface{}) (*statT, bool) {
|
||||
s, ok := i.(*syscall.Stat_t)
|
||||
if ok && s != nil {
|
||||
return (*statT)(s), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (s statT) dev() uint64 { return uint64(s.Dev) }
|
||||
func (s statT) ino() uint64 { return uint64(s.Ino) }
|
||||
func (s statT) nlink() uint64 { return uint64(s.Nlink) }
|
||||
func (s statT) uid() uint32 { return uint32(s.Uid) }
|
||||
func (s statT) gid() uint32 { return uint32(s.Gid) }
|
||||
func (s statT) rdev() uint64 { return uint64(s.Rdev) }
|
||||
func (s statT) size() int64 { return int64(s.Size) }
|
||||
|
@ -4,12 +4,12 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
@ -28,8 +28,11 @@ func stat(t testing.TB, filename string) (fi os.FileInfo, ok bool) {
|
||||
return fi, true
|
||||
}
|
||||
|
||||
func checkFile(t testing.TB, stat *syscall.Stat_t, node *restic.Node) {
|
||||
func checkFile(t testing.TB, fi fs.FileInfo, node *restic.Node) {
|
||||
t.Helper()
|
||||
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
|
||||
if uint32(node.Mode.Perm()) != uint32(stat.Mode&0777) {
|
||||
t.Errorf("Mode does not match, want %v, got %v", stat.Mode&0777, node.Mode)
|
||||
}
|
||||
@ -59,29 +62,20 @@ func checkFile(t testing.TB, stat *syscall.Stat_t, node *restic.Node) {
|
||||
}
|
||||
|
||||
// use the os dependent function to compare the timestamps
|
||||
s, ok := toStatT(stat)
|
||||
if !ok {
|
||||
return
|
||||
s := ExtendedStat(fi)
|
||||
if node.ModTime != s.ModTime {
|
||||
t.Errorf("ModTime does not match, want %v, got %v", s.ModTime, node.ModTime)
|
||||
}
|
||||
|
||||
mtime := s.mtim()
|
||||
if node.ModTime != time.Unix(mtime.Unix()) {
|
||||
t.Errorf("ModTime does not match, want %v, got %v", time.Unix(mtime.Unix()), node.ModTime)
|
||||
if node.ChangeTime != s.ChangeTime {
|
||||
t.Errorf("ChangeTime does not match, want %v, got %v", s.ChangeTime, node.ChangeTime)
|
||||
}
|
||||
|
||||
ctime := s.ctim()
|
||||
if node.ChangeTime != time.Unix(ctime.Unix()) {
|
||||
t.Errorf("ChangeTime does not match, want %v, got %v", time.Unix(ctime.Unix()), node.ChangeTime)
|
||||
if node.AccessTime != s.AccessTime {
|
||||
t.Errorf("AccessTime does not match, want %v, got %v", s.AccessTime, node.AccessTime)
|
||||
}
|
||||
|
||||
atime := s.atim()
|
||||
if node.AccessTime != time.Unix(atime.Unix()) {
|
||||
t.Errorf("AccessTime does not match, want %v, got %v", time.Unix(atime.Unix()), node.AccessTime)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func checkDevice(t testing.TB, stat *syscall.Stat_t, node *restic.Node) {
|
||||
func checkDevice(t testing.TB, fi fs.FileInfo, node *restic.Node) {
|
||||
stat := fi.Sys().(*syscall.Stat_t)
|
||||
if node.Device != uint64(stat.Rdev) {
|
||||
t.Errorf("Rdev does not match, want %v, got %v", stat.Rdev, node.Device)
|
||||
}
|
||||
@ -123,12 +117,6 @@ func TestNodeFromFileInfo(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
s, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
t.Skipf("fi type is %T, not stat_t", fi.Sys())
|
||||
return
|
||||
}
|
||||
|
||||
node, err := NodeFromFileInfo(test.filename, fi, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -136,10 +124,10 @@ func TestNodeFromFileInfo(t *testing.T) {
|
||||
|
||||
switch node.Type {
|
||||
case restic.NodeTypeFile, restic.NodeTypeSymlink:
|
||||
checkFile(t, s, node)
|
||||
checkFile(t, fi, node)
|
||||
case restic.NodeTypeDev, restic.NodeTypeCharDev:
|
||||
checkFile(t, s, node)
|
||||
checkDevice(t, s, node)
|
||||
checkFile(t, fi, node)
|
||||
checkDevice(t, fi, node)
|
||||
default:
|
||||
t.Fatalf("invalid node type %q", node.Type)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package fs
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
@ -175,40 +174,6 @@ func restoreExtendedAttributes(nodeType restic.NodeType, path string, eas []exte
|
||||
return nil
|
||||
}
|
||||
|
||||
type statT syscall.Win32FileAttributeData
|
||||
|
||||
func toStatT(i interface{}) (*statT, bool) {
|
||||
s, ok := i.(*syscall.Win32FileAttributeData)
|
||||
if ok && s != nil {
|
||||
return (*statT)(s), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (s statT) dev() uint64 { return 0 }
|
||||
func (s statT) ino() uint64 { return 0 }
|
||||
func (s statT) nlink() uint64 { return 0 }
|
||||
func (s statT) uid() uint32 { return 0 }
|
||||
func (s statT) gid() uint32 { return 0 }
|
||||
func (s statT) rdev() uint64 { return 0 }
|
||||
|
||||
func (s statT) size() int64 {
|
||||
return int64(s.FileSizeLow) | (int64(s.FileSizeHigh) << 32)
|
||||
}
|
||||
|
||||
func (s statT) atim() syscall.Timespec {
|
||||
return syscall.NsecToTimespec(s.LastAccessTime.Nanoseconds())
|
||||
}
|
||||
|
||||
func (s statT) mtim() syscall.Timespec {
|
||||
return syscall.NsecToTimespec(s.LastWriteTime.Nanoseconds())
|
||||
}
|
||||
|
||||
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 s.mtim()
|
||||
}
|
||||
|
||||
// restoreGenericAttributes restores generic attributes for Windows
|
||||
func nodeRestoreGenericAttributes(node *restic.Node, path string, warn func(msg string)) (err error) {
|
||||
if len(node.GenericAttributes) == 0 {
|
||||
@ -365,7 +330,7 @@ func decryptFile(pathPointer *uint16) error {
|
||||
// Created time and Security Descriptors.
|
||||
// It also checks if the volume supports extended attributes and stores the result in a map
|
||||
// so that it does not have to be checked again for subsequent calls for paths in the same volume.
|
||||
func nodeFillGenericAttributes(node *restic.Node, path string, fi os.FileInfo, stat *statT) (allowExtended bool, err error) {
|
||||
func nodeFillGenericAttributes(node *restic.Node, path string, stat *ExtendedFileInfo) (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.
|
||||
@ -396,10 +361,13 @@ func nodeFillGenericAttributes(node *restic.Node, path string, fi os.FileInfo, s
|
||||
return allowExtended, err
|
||||
}
|
||||
}
|
||||
|
||||
winFI := stat.Sys().(*syscall.Win32FileAttributeData)
|
||||
|
||||
// Add Windows attributes
|
||||
node.GenericAttributes, err = WindowsAttrsToGenericAttributes(WindowsAttributes{
|
||||
CreationTime: getCreationTime(fi, path),
|
||||
FileAttributes: &stat.FileAttributes,
|
||||
CreationTime: &winFI.CreationTime,
|
||||
FileAttributes: &winFI.FileAttributes,
|
||||
SecurityDescriptor: sd,
|
||||
})
|
||||
return allowExtended, err
|
||||
@ -501,18 +469,3 @@ func WindowsAttrsToGenericAttributes(windowsAttributes WindowsAttributes) (attrs
|
||||
windowsAttributesValue := reflect.ValueOf(windowsAttributes)
|
||||
return restic.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
|
||||
}
|
||||
}
|
||||
|
@ -80,10 +80,10 @@ func TestRestoreCreationTime(t *testing.T) {
|
||||
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))
|
||||
attr := fi.Sys().(*syscall.Win32FileAttributeData)
|
||||
creationTimeAttribute := attr.CreationTime
|
||||
//Using the temp dir creation time as the test creation time for the test file and folder
|
||||
runGenericAttributesTest(t, path, restic.TypeCreationTime, WindowsAttributes{CreationTime: creationTimeAttribute}, false)
|
||||
runGenericAttributesTest(t, path, restic.TypeCreationTime, WindowsAttributes{CreationTime: &creationTimeAttribute}, false)
|
||||
}
|
||||
|
||||
func TestRestoreFileAttributes(t *testing.T) {
|
||||
|
@ -71,7 +71,7 @@ func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg str
|
||||
}
|
||||
|
||||
// nodeFillGenericAttributes is a no-op.
|
||||
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) {
|
||||
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ *ExtendedFileInfo) (allowExtended bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ func extendedStat(fi os.FileInfo) ExtendedFileInfo {
|
||||
|
||||
extFI := ExtendedFileInfo{
|
||||
FileInfo: fi,
|
||||
Size: int64(s.FileSizeLow) + int64(s.FileSizeHigh)<<32,
|
||||
Size: int64(s.FileSizeLow) | (int64(s.FileSizeHigh) << 32),
|
||||
}
|
||||
|
||||
atime := syscall.NsecToTimespec(s.LastAccessTime.Nanoseconds())
|
||||
@ -28,6 +28,7 @@ func extendedStat(fi os.FileInfo) ExtendedFileInfo {
|
||||
mtime := syscall.NsecToTimespec(s.LastWriteTime.Nanoseconds())
|
||||
extFI.ModTime = time.Unix(mtime.Unix())
|
||||
|
||||
// Windows does not have the concept of a "change time" in the sense Unix uses it, so we're using the LastWriteTime here.
|
||||
extFI.ChangeTime = extFI.ModTime
|
||||
|
||||
return extFI
|
||||
|
Loading…
Reference in New Issue
Block a user