| |
| |
| 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 |
| } |
|
|