mirror of
https://github.com/rclone/rclone.git
synced 2025-07-09 08:47:11 +02:00
260 lines
6.1 KiB
Go
260 lines
6.1 KiB
Go
package filelu
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/hash"
|
|
)
|
|
|
|
// Object describes a FileLu object
|
|
type Object struct {
|
|
fs *Fs
|
|
remote string
|
|
size int64
|
|
modTime time.Time
|
|
}
|
|
|
|
// NewObject creates a new Object for the given remote path
|
|
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|
var filePath string
|
|
filePath = path.Join(f.root, remote)
|
|
filePath = "/" + strings.Trim(filePath, "/")
|
|
|
|
// Get File code
|
|
fileCode, err := f.getFileCode(ctx, filePath)
|
|
if err != nil {
|
|
return nil, fs.ErrorObjectNotFound
|
|
}
|
|
|
|
// Get File info
|
|
fileInfos, err := f.getFileInfo(ctx, fileCode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get file info: %w", err)
|
|
}
|
|
|
|
fileInfo := fileInfos.Result[0]
|
|
size, _ := strconv.ParseInt(fileInfo.Size, 10, 64)
|
|
|
|
returnedRemote := remote
|
|
return &Object{
|
|
fs: f,
|
|
remote: returnedRemote,
|
|
size: size,
|
|
modTime: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// Open opens the object for reading
|
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
|
|
filePath := path.Join(o.fs.root, o.remote)
|
|
// Get direct link
|
|
directLink, size, err := o.fs.getDirectLink(ctx, filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get direct link: %w", err)
|
|
}
|
|
|
|
o.size = size
|
|
|
|
// Offset and Count for range download
|
|
var offset int64
|
|
var count int64
|
|
fs.FixRangeOption(options, o.size)
|
|
for _, option := range options {
|
|
switch x := option.(type) {
|
|
case *fs.RangeOption:
|
|
offset, count = x.Decode(o.size)
|
|
if count < 0 {
|
|
count = o.size - offset
|
|
}
|
|
case *fs.SeekOption:
|
|
offset = x.Offset
|
|
count = o.size
|
|
default:
|
|
if option.Mandatory() {
|
|
fs.Logf(o, "Unsupported mandatory option: %v", option)
|
|
}
|
|
}
|
|
}
|
|
|
|
var reader io.ReadCloser
|
|
err = o.fs.pacer.Call(func() (bool, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", directLink, nil)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to create download request: %w", err)
|
|
}
|
|
|
|
resp, err := o.fs.client.Do(req)
|
|
if err != nil {
|
|
return shouldRetry(err), fmt.Errorf("failed to download file: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
|
}
|
|
}()
|
|
return false, fmt.Errorf("failed to download file: HTTP %d", resp.StatusCode)
|
|
}
|
|
|
|
// Wrap the response body to handle offset and count
|
|
currentContents, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
if offset > 0 {
|
|
if offset > int64(len(currentContents)) {
|
|
return false, fmt.Errorf("offset %d exceeds file size %d", offset, len(currentContents))
|
|
}
|
|
currentContents = currentContents[offset:]
|
|
}
|
|
if count > 0 && count < int64(len(currentContents)) {
|
|
currentContents = currentContents[:count]
|
|
}
|
|
reader = io.NopCloser(bytes.NewReader(currentContents))
|
|
|
|
return false, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return reader, nil
|
|
}
|
|
|
|
// Update updates the object with new data
|
|
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
|
if src.Size() <= 0 {
|
|
return fs.ErrorCantUploadEmptyFiles
|
|
}
|
|
|
|
err := o.fs.uploadFile(ctx, in, o.remote)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to upload file: %w", err)
|
|
}
|
|
o.size = src.Size()
|
|
return nil
|
|
}
|
|
|
|
// Remove deletes the object from FileLu
|
|
func (o *Object) Remove(ctx context.Context) error {
|
|
fullPath := "/" + strings.Trim(path.Join(o.fs.root, o.remote), "/")
|
|
|
|
err := o.fs.deleteFile(ctx, fullPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fs.Infof(o.fs, "Successfully deleted file: %s", fullPath)
|
|
return nil
|
|
}
|
|
|
|
// Hash returns the MD5 hash of an object
|
|
func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
|
|
if t != hash.MD5 {
|
|
return "", hash.ErrUnsupported
|
|
}
|
|
|
|
var fileCode string
|
|
if isFileCode(o.fs.root) {
|
|
fileCode = o.fs.root
|
|
} else {
|
|
matches := regexp.MustCompile(`\((.*?)\)`).FindAllStringSubmatch(o.remote, -1)
|
|
for _, match := range matches {
|
|
if len(match) > 1 && len(match[1]) == 12 {
|
|
fileCode = match[1]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if fileCode == "" {
|
|
return "", fmt.Errorf("no valid file code found in the remote path")
|
|
}
|
|
|
|
apiURL := fmt.Sprintf("%s/file/info?file_code=%s&key=%s",
|
|
o.fs.endpoint, url.QueryEscape(fileCode), url.QueryEscape(o.fs.opt.Key))
|
|
|
|
var result struct {
|
|
Status int `json:"status"`
|
|
Msg string `json:"msg"`
|
|
Result []struct {
|
|
Hash string `json:"hash"`
|
|
} `json:"result"`
|
|
}
|
|
err := o.fs.pacer.Call(func() (bool, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
resp, err := o.fs.client.Do(req)
|
|
if err != nil {
|
|
return shouldRetry(err), err
|
|
}
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
|
}
|
|
}()
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return false, err
|
|
}
|
|
return shouldRetryHTTP(resp.StatusCode), nil
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if result.Status != 200 || len(result.Result) == 0 {
|
|
return "", fmt.Errorf("error: unable to fetch hash: %s", result.Msg)
|
|
}
|
|
|
|
return result.Result[0].Hash, nil
|
|
}
|
|
|
|
// String returns a string representation of the object
|
|
func (o *Object) String() string {
|
|
return o.remote
|
|
}
|
|
|
|
// Fs returns the parent Fs
|
|
func (o *Object) Fs() fs.Info {
|
|
return o.fs
|
|
}
|
|
|
|
// Remote returns the remote path
|
|
func (o *Object) Remote() string {
|
|
return o.remote
|
|
}
|
|
|
|
// Size returns the size of the object
|
|
func (o *Object) Size() int64 {
|
|
return o.size
|
|
}
|
|
|
|
// ModTime returns the modification time of the object
|
|
func (o *Object) ModTime(ctx context.Context) time.Time {
|
|
return o.modTime
|
|
}
|
|
|
|
// SetModTime sets the modification time of the object
|
|
func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
|
return fs.ErrorCantSetModTime
|
|
}
|
|
|
|
// Storable indicates whether the object is storable
|
|
func (o *Object) Storable() bool {
|
|
return true
|
|
}
|