github.com/cs3org/reva/v2@v2.27.7/pkg/storage/fs/s3ng/blobstore/blobstore.go (about) 1 // Copyright 2018-2021 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package blobstore 20 21 import ( 22 "context" 23 "fmt" 24 "io" 25 "net/url" 26 "os" 27 "path/filepath" 28 "strings" 29 30 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/lookup" 31 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node" 32 "github.com/minio/minio-go/v7" 33 "github.com/minio/minio-go/v7/pkg/credentials" 34 "github.com/pkg/errors" 35 ) 36 37 // Blobstore provides an interface to an s3 compatible blobstore 38 type Blobstore struct { 39 client *minio.Client 40 41 defaultPutOptions Options 42 43 bucket string 44 } 45 46 type Options struct { 47 DisableContentSha256 bool 48 DisableMultipart bool 49 SendContentMd5 bool 50 ConcurrentStreamParts bool 51 NumThreads uint 52 PartSize uint64 53 } 54 55 // New returns a new Blobstore 56 func New(endpoint, region, bucket, accessKey, secretKey string, defaultPutOptions Options) (*Blobstore, error) { 57 u, err := url.Parse(endpoint) 58 if err != nil { 59 return nil, errors.Wrap(err, "failed to parse s3 endpoint") 60 } 61 62 useSSL := u.Scheme != "http" 63 client, err := minio.New(u.Host, &minio.Options{ 64 Region: region, 65 Creds: credentials.NewStaticV4(accessKey, secretKey, ""), 66 Secure: useSSL, 67 }) 68 if err != nil { 69 return nil, errors.Wrap(err, "failed to setup s3 client") 70 } 71 72 return &Blobstore{ 73 client: client, 74 bucket: bucket, 75 defaultPutOptions: defaultPutOptions, 76 }, nil 77 } 78 79 // Upload stores some data in the blobstore under the given key 80 func (bs *Blobstore) Upload(node *node.Node, source string) error { 81 reader, err := os.Open(source) 82 if err != nil { 83 return errors.Wrap(err, "can not open source file to upload") 84 } 85 defer reader.Close() 86 87 _, err = bs.client.PutObject(context.Background(), bs.bucket, bs.Path(node), reader, node.Blobsize, minio.PutObjectOptions{ 88 ContentType: "application/octet-stream", 89 SendContentMd5: bs.defaultPutOptions.SendContentMd5, 90 ConcurrentStreamParts: bs.defaultPutOptions.ConcurrentStreamParts, 91 NumThreads: bs.defaultPutOptions.NumThreads, 92 PartSize: bs.defaultPutOptions.PartSize, 93 DisableMultipart: bs.defaultPutOptions.DisableMultipart, 94 DisableContentSha256: bs.defaultPutOptions.DisableContentSha256, 95 }) 96 97 if err != nil { 98 return errors.Wrapf(err, "could not store object '%s' into bucket '%s'", bs.Path(node), bs.bucket) 99 } 100 return nil 101 } 102 103 // Download retrieves a blob from the blobstore for reading 104 func (bs *Blobstore) Download(node *node.Node) (io.ReadCloser, error) { 105 reader, err := bs.client.GetObject(context.Background(), bs.bucket, bs.Path(node), minio.GetObjectOptions{}) 106 if err != nil { 107 return nil, errors.Wrapf(err, "could not download object '%s' from bucket '%s'", bs.Path(node), bs.bucket) 108 } 109 110 stat, err := reader.Stat() 111 if err != nil { 112 return nil, errors.Wrapf(err, "blob path: %s", bs.Path(node)) 113 } 114 115 if node.Blobsize != stat.Size { 116 return nil, fmt.Errorf("blob has unexpected size. %d bytes expected, got %d bytes", node.Blobsize, stat.Size) 117 } 118 119 return reader, nil 120 } 121 122 // Delete deletes a blob from the blobstore 123 func (bs *Blobstore) Delete(node *node.Node) error { 124 err := bs.client.RemoveObject(context.Background(), bs.bucket, bs.Path(node), minio.RemoveObjectOptions{}) 125 if err != nil { 126 return errors.Wrapf(err, "could not delete object '%s' from bucket '%s'", bs.Path(node), bs.bucket) 127 } 128 return nil 129 } 130 131 // List lists all blobs in the Blobstore 132 func (bs *Blobstore) List() ([]*node.Node, error) { 133 ch := bs.client.ListObjects(context.Background(), bs.bucket, minio.ListObjectsOptions{Recursive: true}) 134 135 var err error 136 ids := make([]*node.Node, 0) 137 for oi := range ch { 138 if oi.Err != nil { 139 err = oi.Err 140 continue 141 } 142 spaceid, blobid, _ := strings.Cut(oi.Key, "/") 143 ids = append(ids, &node.Node{ 144 SpaceID: strings.ReplaceAll(spaceid, "/", ""), 145 BlobID: strings.ReplaceAll(blobid, "/", ""), 146 }) 147 } 148 return ids, err 149 } 150 151 func (bs *Blobstore) Path(node *node.Node) string { 152 // https://aws.amazon.com/de/premiumsupport/knowledge-center/s3-prefix-nested-folders-difference/ 153 // Prefixes are used to partion a bucket. A prefix is everything except the filename. 154 // For a file `BucketName/foo/bar/lorem.ipsum`, `BucketName/foo/bar/` is the prefix. 155 // There are request limits per prefix, therefore you should have many prefixes. 156 // There are no limits to prefixes per bucket, so in general it's better to have more then less. 157 // 158 // Since the spaceID is always the same for a space, we don't need to pathify that, because it would 159 // not yield any performance gains 160 return filepath.Clean(filepath.Join(node.SpaceID, lookup.Pathify(node.BlobID, 4, 2))) 161 }