mirror of
https://github.com/rclone/rclone.git
synced 2025-12-06 00:03:32 +00:00
236 lines
4.9 KiB
Go
236 lines
4.9 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
|
|
f.reader = f.transfer.Account(ctx, f.reader)
|
|
|
|
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())
|
|
},
|
|
}
|
|
|
|
}
|