|
|
|
|
|
package s3 |
|
|
|
import ( |
|
"context" |
|
"encoding/hex" |
|
"fmt" |
|
"io" |
|
"path" |
|
"strings" |
|
"sync" |
|
"time" |
|
|
|
"github.com/Mikubill/gofakes3" |
|
"github.com/alist-org/alist/v3/internal/errs" |
|
"github.com/alist-org/alist/v3/internal/fs" |
|
"github.com/alist-org/alist/v3/internal/model" |
|
"github.com/alist-org/alist/v3/internal/op" |
|
"github.com/alist-org/alist/v3/internal/stream" |
|
"github.com/alist-org/alist/v3/pkg/http_range" |
|
"github.com/alist-org/alist/v3/pkg/utils" |
|
"github.com/ncw/swift/v2" |
|
) |
|
|
|
var ( |
|
emptyPrefix = &gofakes3.Prefix{} |
|
timeFormat = "Mon, 2 Jan 2006 15:04:05.999999999 GMT" |
|
) |
|
|
|
|
|
|
|
type s3Backend struct { |
|
meta *sync.Map |
|
} |
|
|
|
|
|
func newBackend() gofakes3.Backend { |
|
return &s3Backend{ |
|
meta: new(sync.Map), |
|
} |
|
} |
|
|
|
|
|
func (b *s3Backend) ListBuckets() ([]gofakes3.BucketInfo, error) { |
|
buckets, err := getAndParseBuckets() |
|
if err != nil { |
|
return nil, err |
|
} |
|
var response []gofakes3.BucketInfo |
|
ctx := context.Background() |
|
for _, b := range buckets { |
|
node, _ := fs.Get(ctx, b.Path, &fs.GetArgs{}) |
|
response = append(response, gofakes3.BucketInfo{ |
|
|
|
Name: b.Name, |
|
CreationDate: gofakes3.NewContentTime(node.ModTime()), |
|
}) |
|
} |
|
return response, nil |
|
} |
|
|
|
|
|
func (b *s3Backend) ListBucket(bucketName string, prefix *gofakes3.Prefix, page gofakes3.ListBucketPage) (*gofakes3.ObjectList, error) { |
|
bucket, err := getBucketByName(bucketName) |
|
if err != nil { |
|
return nil, err |
|
} |
|
bucketPath := bucket.Path |
|
|
|
if prefix == nil { |
|
prefix = emptyPrefix |
|
} |
|
|
|
|
|
if strings.TrimSpace(prefix.Prefix) == "" { |
|
prefix.HasPrefix = false |
|
} |
|
if strings.TrimSpace(prefix.Delimiter) == "" { |
|
prefix.HasDelimiter = false |
|
} |
|
|
|
response := gofakes3.NewObjectList() |
|
path, remaining := prefixParser(prefix) |
|
|
|
err = b.entryListR(bucketPath, path, remaining, prefix.HasDelimiter, response) |
|
if err == gofakes3.ErrNoSuchKey { |
|
|
|
response = gofakes3.NewObjectList() |
|
} else if err != nil { |
|
return nil, err |
|
} |
|
|
|
return b.pager(response, page) |
|
} |
|
|
|
|
|
|
|
|
|
func (b *s3Backend) HeadObject(bucketName, objectName string) (*gofakes3.Object, error) { |
|
ctx := context.Background() |
|
bucket, err := getBucketByName(bucketName) |
|
if err != nil { |
|
return nil, err |
|
} |
|
bucketPath := bucket.Path |
|
|
|
fp := path.Join(bucketPath, objectName) |
|
fmeta, _ := op.GetNearestMeta(fp) |
|
node, err := fs.Get(context.WithValue(ctx, "meta", fmeta), fp, &fs.GetArgs{}) |
|
if err != nil { |
|
return nil, gofakes3.KeyNotFound(objectName) |
|
} |
|
|
|
if node.IsDir() { |
|
return nil, gofakes3.KeyNotFound(objectName) |
|
} |
|
|
|
size := node.GetSize() |
|
|
|
|
|
meta := map[string]string{ |
|
"Last-Modified": node.ModTime().Format(timeFormat), |
|
"Content-Type": utils.GetMimeType(fp), |
|
} |
|
|
|
if val, ok := b.meta.Load(fp); ok { |
|
metaMap := val.(map[string]string) |
|
for k, v := range metaMap { |
|
meta[k] = v |
|
} |
|
} |
|
|
|
return &gofakes3.Object{ |
|
Name: objectName, |
|
|
|
Metadata: meta, |
|
Size: size, |
|
Contents: noOpReadCloser{}, |
|
}, nil |
|
} |
|
|
|
|
|
func (b *s3Backend) GetObject(bucketName, objectName string, rangeRequest *gofakes3.ObjectRangeRequest) (obj *gofakes3.Object, err error) { |
|
ctx := context.Background() |
|
bucket, err := getBucketByName(bucketName) |
|
if err != nil { |
|
return nil, err |
|
} |
|
bucketPath := bucket.Path |
|
|
|
fp := path.Join(bucketPath, objectName) |
|
fmeta, _ := op.GetNearestMeta(fp) |
|
node, err := fs.Get(context.WithValue(ctx, "meta", fmeta), fp, &fs.GetArgs{}) |
|
if err != nil { |
|
return nil, gofakes3.KeyNotFound(objectName) |
|
} |
|
|
|
if node.IsDir() { |
|
return nil, gofakes3.KeyNotFound(objectName) |
|
} |
|
|
|
link, file, err := fs.Link(ctx, fp, model.LinkArgs{}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
size := file.GetSize() |
|
rnge, err := rangeRequest.Range(size) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
if link.RangeReadCloser == nil && link.MFile == nil && len(link.URL) == 0 { |
|
return nil, fmt.Errorf("the remote storage driver need to be enhanced to support s3") |
|
} |
|
remoteFileSize := file.GetSize() |
|
remoteClosers := utils.EmptyClosers() |
|
rangeReaderFunc := func(ctx context.Context, start, length int64) (io.ReadCloser, error) { |
|
if length >= 0 && start+length >= remoteFileSize { |
|
length = -1 |
|
} |
|
rrc := link.RangeReadCloser |
|
if len(link.URL) > 0 { |
|
|
|
rangedRemoteLink := &model.Link{ |
|
URL: link.URL, |
|
Header: link.Header, |
|
} |
|
var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, rangedRemoteLink) |
|
if err != nil { |
|
return nil, err |
|
} |
|
rrc = converted |
|
} |
|
if rrc != nil { |
|
remoteReader, err := rrc.RangeRead(ctx, http_range.Range{Start: start, Length: length}) |
|
remoteClosers.AddClosers(rrc.GetClosers()) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return remoteReader, nil |
|
} |
|
if link.MFile != nil { |
|
_, err := link.MFile.Seek(start, io.SeekStart) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
|
|
remoteClosers.Add(link.MFile) |
|
return io.NopCloser(link.MFile), nil |
|
} |
|
return nil, errs.NotSupport |
|
} |
|
|
|
var rdr io.ReadCloser |
|
if rnge != nil { |
|
rdr, err = rangeReaderFunc(ctx, rnge.Start, rnge.Length) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} else { |
|
rdr, err = rangeReaderFunc(ctx, 0, -1) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
|
|
meta := map[string]string{ |
|
"Last-Modified": node.ModTime().Format(timeFormat), |
|
"Content-Type": utils.GetMimeType(fp), |
|
} |
|
|
|
if val, ok := b.meta.Load(fp); ok { |
|
metaMap := val.(map[string]string) |
|
for k, v := range metaMap { |
|
meta[k] = v |
|
} |
|
} |
|
|
|
return &gofakes3.Object{ |
|
|
|
Name: objectName, |
|
|
|
Metadata: meta, |
|
Size: size, |
|
Range: rnge, |
|
Contents: rdr, |
|
}, nil |
|
} |
|
|
|
|
|
func (b *s3Backend) TouchObject(fp string, meta map[string]string) (result gofakes3.PutObjectResult, err error) { |
|
|
|
return result, gofakes3.ErrNotImplemented |
|
} |
|
|
|
|
|
func (b *s3Backend) PutObject( |
|
bucketName, objectName string, |
|
meta map[string]string, |
|
input io.Reader, size int64, |
|
) (result gofakes3.PutObjectResult, err error) { |
|
ctx := context.Background() |
|
bucket, err := getBucketByName(bucketName) |
|
if err != nil { |
|
return result, err |
|
} |
|
bucketPath := bucket.Path |
|
|
|
fp := path.Join(bucketPath, objectName) |
|
reqPath := path.Dir(fp) |
|
fmeta, _ := op.GetNearestMeta(fp) |
|
_, err = fs.Get(context.WithValue(ctx, "meta", fmeta), reqPath, &fs.GetArgs{}) |
|
if err != nil { |
|
return result, gofakes3.KeyNotFound(objectName) |
|
} |
|
|
|
var ti time.Time |
|
|
|
if val, ok := meta["X-Amz-Meta-Mtime"]; ok { |
|
ti, _ = swift.FloatStringToTime(val) |
|
} |
|
|
|
if val, ok := meta["mtime"]; ok { |
|
ti, _ = swift.FloatStringToTime(val) |
|
} |
|
|
|
obj := model.Object{ |
|
Name: path.Base(fp), |
|
Size: size, |
|
Modified: ti, |
|
Ctime: time.Now(), |
|
} |
|
stream := &stream.FileStream{ |
|
Obj: &obj, |
|
Reader: input, |
|
Mimetype: meta["Content-Type"], |
|
} |
|
|
|
err = fs.PutDirectly(ctx, reqPath, stream) |
|
if err != nil { |
|
return result, err |
|
} |
|
|
|
if err := stream.Close(); err != nil { |
|
|
|
_ = fs.Remove(ctx, fp) |
|
return result, err |
|
} |
|
|
|
b.meta.Store(fp, meta) |
|
|
|
return result, nil |
|
} |
|
|
|
|
|
func (b *s3Backend) DeleteMulti(bucketName string, objects ...string) (result gofakes3.MultiDeleteResult, rerr error) { |
|
for _, object := range objects { |
|
if err := b.deleteObject(bucketName, object); err != nil { |
|
utils.Log.Errorf("serve s3", "delete object failed: %v", err) |
|
result.Error = append(result.Error, gofakes3.ErrorResult{ |
|
Code: gofakes3.ErrInternal, |
|
Message: gofakes3.ErrInternal.Message(), |
|
Key: object, |
|
}) |
|
} else { |
|
result.Deleted = append(result.Deleted, gofakes3.ObjectID{ |
|
Key: object, |
|
}) |
|
} |
|
} |
|
|
|
return result, nil |
|
} |
|
|
|
|
|
func (b *s3Backend) DeleteObject(bucketName, objectName string) (result gofakes3.ObjectDeleteResult, rerr error) { |
|
return result, b.deleteObject(bucketName, objectName) |
|
} |
|
|
|
|
|
func (b *s3Backend) deleteObject(bucketName, objectName string) error { |
|
ctx := context.Background() |
|
bucket, err := getBucketByName(bucketName) |
|
if err != nil { |
|
return err |
|
} |
|
bucketPath := bucket.Path |
|
|
|
fp := path.Join(bucketPath, objectName) |
|
fmeta, _ := op.GetNearestMeta(fp) |
|
|
|
|
|
if _, err := fs.Get(context.WithValue(ctx, "meta", fmeta), fp, &fs.GetArgs{}); err != nil && !errs.IsObjectNotFound(err) { |
|
return err |
|
} |
|
|
|
fs.Remove(ctx, fp) |
|
return nil |
|
} |
|
|
|
|
|
func (b *s3Backend) CreateBucket(name string) error { |
|
return gofakes3.ErrNotImplemented |
|
} |
|
|
|
|
|
func (b *s3Backend) DeleteBucket(name string) error { |
|
return gofakes3.ErrNotImplemented |
|
} |
|
|
|
|
|
func (b *s3Backend) BucketExists(name string) (exists bool, err error) { |
|
buckets, err := getAndParseBuckets() |
|
if err != nil { |
|
return false, err |
|
} |
|
for _, b := range buckets { |
|
if b.Name == name { |
|
return true, nil |
|
} |
|
} |
|
return false, nil |
|
} |
|
|
|
|
|
func (b *s3Backend) CopyObject(srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (result gofakes3.CopyObjectResult, err error) { |
|
if srcBucket == dstBucket && srcKey == dstKey { |
|
|
|
return result, nil |
|
} |
|
|
|
ctx := context.Background() |
|
srcB, err := getBucketByName(srcBucket) |
|
if err != nil { |
|
return result, err |
|
} |
|
srcBucketPath := srcB.Path |
|
|
|
srcFp := path.Join(srcBucketPath, srcKey) |
|
fmeta, _ := op.GetNearestMeta(srcFp) |
|
srcNode, err := fs.Get(context.WithValue(ctx, "meta", fmeta), srcFp, &fs.GetArgs{}) |
|
|
|
c, err := b.GetObject(srcBucket, srcKey, nil) |
|
if err != nil { |
|
return |
|
} |
|
defer func() { |
|
_ = c.Contents.Close() |
|
}() |
|
|
|
for k, v := range c.Metadata { |
|
if _, found := meta[k]; !found && k != "X-Amz-Acl" { |
|
meta[k] = v |
|
} |
|
} |
|
if _, ok := meta["mtime"]; !ok { |
|
meta["mtime"] = swift.TimeToFloatString(srcNode.ModTime()) |
|
} |
|
|
|
_, err = b.PutObject(dstBucket, dstKey, meta, c.Contents, c.Size) |
|
if err != nil { |
|
return |
|
} |
|
|
|
return gofakes3.CopyObjectResult{ |
|
ETag: `"` + hex.EncodeToString(c.Hash) + `"`, |
|
LastModified: gofakes3.NewContentTime(srcNode.ModTime()), |
|
}, nil |
|
} |
|
|