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  }