mirror of
https://github.com/rclone/rclone.git
synced 2026-05-18 22:03:21 -04:00
The files.com integration tests for rcat/copyurl were failing because
fs/account.Account was declaring a ReadAt method when the underlying
handle did not support it. The files.com SDK decided to use the ReadAt
method to speed transfers up which failed.
ReadAt and Seek methods were added in this commit to support the
archive command:
409dc75328 accounting: add io.Seeker/io.ReaderAt support to accounting.Account
This fixes the problem by adding new methods to the Account object
WithSeeker/WithReaderAt/WithReadAtSeeker which produce an object with
the desired methods or errors if it isn't possible.
This stops Account advertising things it can't do which is bad Go
practice.
241 lines
5.0 KiB
Go
241 lines
5.0 KiB
Go
// Package files implements io/fs objects
|
|
package files
|
|
|
|
import (
|
|
"archive/tar"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
stdfs "io/fs"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mholt/archives"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/accounting"
|
|
"github.com/rclone/rclone/fs/operations"
|
|
)
|
|
|
|
// fill tar.Header with metadata if available (too bad username/groupname is not available)
|
|
func metadataToHeader(metadata fs.Metadata, header *tar.Header) {
|
|
var val string
|
|
var ok bool
|
|
var err error
|
|
var mode, uid, gid int64
|
|
var atime, ctime time.Time
|
|
var uname, gname string
|
|
// check if metadata is valid
|
|
if metadata != nil {
|
|
// mode
|
|
val, ok = metadata["mode"]
|
|
if !ok {
|
|
mode = 0644
|
|
} else {
|
|
mode, err = strconv.ParseInt(val, 8, 64)
|
|
if err != nil {
|
|
mode = 0664
|
|
}
|
|
}
|
|
// uid
|
|
val, ok = metadata["uid"]
|
|
if !ok {
|
|
uid = 0
|
|
} else {
|
|
uid, err = strconv.ParseInt(val, 10, 32)
|
|
if err != nil {
|
|
uid = 0
|
|
}
|
|
}
|
|
// gid
|
|
val, ok = metadata["gid"]
|
|
if !ok {
|
|
gid = 0
|
|
} else {
|
|
gid, err = strconv.ParseInt(val, 10, 32)
|
|
if err != nil {
|
|
gid = 0
|
|
}
|
|
}
|
|
// access time
|
|
val, ok := metadata["atime"]
|
|
if !ok {
|
|
atime = time.Unix(0, 0)
|
|
} else {
|
|
atime, err = time.Parse(time.RFC3339Nano, val)
|
|
if err != nil {
|
|
atime = time.Unix(0, 0)
|
|
}
|
|
}
|
|
// set uname/gname
|
|
if uid == 0 {
|
|
uname = "root"
|
|
} else {
|
|
uname = strconv.FormatInt(uid, 10)
|
|
}
|
|
if gid == 0 {
|
|
gname = "root"
|
|
} else {
|
|
gname = strconv.FormatInt(gid, 10)
|
|
}
|
|
} else {
|
|
mode = 0644
|
|
uid = 0
|
|
gid = 0
|
|
uname = "root"
|
|
gname = "root"
|
|
atime = header.ModTime
|
|
ctime = header.ModTime
|
|
}
|
|
// set values
|
|
header.Mode = mode
|
|
header.Uid = int(uid)
|
|
header.Gid = int(gid)
|
|
header.Uname = uname
|
|
header.Gname = gname
|
|
header.AccessTime = atime
|
|
header.ChangeTime = ctime
|
|
}
|
|
|
|
// structs for fs.FileInfo,fs.File,SeekableFile
|
|
|
|
type fileInfoImpl struct {
|
|
header *tar.Header
|
|
}
|
|
|
|
type fileImpl struct {
|
|
entry stdfs.FileInfo
|
|
ctx context.Context
|
|
reader io.ReadSeekCloser
|
|
transfer *accounting.Transfer
|
|
err error
|
|
}
|
|
|
|
func newFileInfo(ctx context.Context, entry fs.DirEntry, prefix string, metadata fs.Metadata) stdfs.FileInfo {
|
|
var fi = new(fileInfoImpl)
|
|
|
|
fi.header = new(tar.Header)
|
|
if prefix != "" {
|
|
fi.header.Name = path.Join(strings.TrimPrefix(prefix, "/"), entry.Remote())
|
|
} else {
|
|
fi.header.Name = entry.Remote()
|
|
}
|
|
fi.header.Size = entry.Size()
|
|
fi.header.ModTime = entry.ModTime(ctx)
|
|
// set metadata
|
|
metadataToHeader(metadata, fi.header)
|
|
// flag if directory
|
|
_, isDir := entry.(fs.Directory)
|
|
if isDir {
|
|
fi.header.Mode = int64(stdfs.ModeDir) | fi.header.Mode
|
|
}
|
|
|
|
return fi
|
|
}
|
|
|
|
func (a *fileInfoImpl) Name() string {
|
|
return a.header.Name
|
|
}
|
|
|
|
func (a *fileInfoImpl) Size() int64 {
|
|
return a.header.Size
|
|
}
|
|
|
|
func (a *fileInfoImpl) Mode() stdfs.FileMode {
|
|
return stdfs.FileMode(a.header.Mode)
|
|
}
|
|
|
|
func (a *fileInfoImpl) ModTime() time.Time {
|
|
return a.header.ModTime
|
|
}
|
|
|
|
func (a *fileInfoImpl) IsDir() bool {
|
|
return (a.header.Mode & int64(stdfs.ModeDir)) != 0
|
|
}
|
|
|
|
func (a *fileInfoImpl) Sys() any {
|
|
return a.header
|
|
}
|
|
|
|
func (a *fileInfoImpl) String() string {
|
|
return fmt.Sprintf("Name=%v Size=%v IsDir=%v UID=%v GID=%v", a.Name(), a.Size(), a.IsDir(), a.header.Uid, a.header.Gid)
|
|
}
|
|
|
|
// create a fs.File compatible struct
|
|
func newFile(ctx context.Context, obj fs.Object, fi stdfs.FileInfo) (stdfs.File, error) {
|
|
var f = new(fileImpl)
|
|
// create stdfs.File
|
|
f.entry = fi
|
|
f.ctx = ctx
|
|
f.err = nil
|
|
// create transfer
|
|
f.transfer = accounting.Stats(ctx).NewTransfer(obj, nil)
|
|
// get open options
|
|
var options []fs.OpenOption
|
|
for _, option := range fs.GetConfig(ctx).DownloadHeaders {
|
|
options = append(options, option)
|
|
}
|
|
// open file
|
|
f.reader, f.err = operations.Open(ctx, obj, options...)
|
|
if f.err != nil {
|
|
defer f.transfer.Done(ctx, f.err)
|
|
return nil, f.err
|
|
}
|
|
// Account the transfer
|
|
acc := f.transfer.Account(ctx, f.reader)
|
|
h, err := acc.WithReadAtSeeker()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f.reader = h
|
|
|
|
return f, f.err
|
|
}
|
|
|
|
func (a *fileImpl) Stat() (stdfs.FileInfo, error) {
|
|
return a.entry, nil
|
|
}
|
|
|
|
func (a *fileImpl) Read(data []byte) (int, error) {
|
|
if a.reader == nil {
|
|
a.err = fmt.Errorf("file %s not open", a.entry.Name())
|
|
return 0, a.err
|
|
}
|
|
i, err := a.reader.Read(data)
|
|
a.err = err
|
|
return i, a.err
|
|
}
|
|
|
|
func (a *fileImpl) Close() error {
|
|
// close file
|
|
if a.reader == nil {
|
|
a.err = fmt.Errorf("file %s not open", a.entry.Name())
|
|
} else {
|
|
a.err = a.reader.Close()
|
|
}
|
|
// close transfer
|
|
a.transfer.Done(a.ctx, a.err)
|
|
|
|
return a.err
|
|
}
|
|
|
|
// NewArchiveFileInfo will take a fs.DirEntry and return a archives.Fileinfo
|
|
func NewArchiveFileInfo(ctx context.Context, entry fs.DirEntry, prefix string, metadata fs.Metadata) archives.FileInfo {
|
|
fi := newFileInfo(ctx, entry, prefix, metadata)
|
|
|
|
return archives.FileInfo{
|
|
FileInfo: fi,
|
|
NameInArchive: fi.Name(),
|
|
LinkTarget: "",
|
|
Open: func() (stdfs.File, error) {
|
|
obj, isObject := entry.(fs.Object)
|
|
if isObject {
|
|
return newFile(ctx, obj, fi)
|
|
}
|
|
return nil, fmt.Errorf("%s is not a file", fi.Name())
|
|
},
|
|
}
|
|
|
|
}
|