github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/metadata/cs3.go (about)

     1  // Copyright 2018-2022 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 metadata
    20  
    21  import (
    22  	"bytes"
    23  	"context"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"net/http"
    28  	"os"
    29  	"strconv"
    30  	"time"
    31  
    32  	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
    33  	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    34  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    35  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    36  	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    37  	"go.opentelemetry.io/otel"
    38  	"go.opentelemetry.io/otel/trace"
    39  	"google.golang.org/grpc/metadata"
    40  
    41  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net"
    42  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    43  	"github.com/cs3org/reva/v2/pkg/errtypes"
    44  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    45  	"github.com/cs3org/reva/v2/pkg/utils"
    46  )
    47  
    48  var tracer trace.Tracer
    49  
    50  func init() {
    51  	tracer = otel.Tracer("github.com/cs3org/reva/pkg/storage/utils/metadata")
    52  }
    53  
    54  // CS3 represents a metadata storage with a cs3 storage backend
    55  type CS3 struct {
    56  	SpaceRoot *provider.ResourceId
    57  
    58  	providerAddr      string
    59  	gatewayAddr       string
    60  	useSystemUser     bool
    61  	serviceUser       *user.User
    62  	machineAuthAPIKey string
    63  
    64  	dataGatewayClient *http.Client
    65  }
    66  
    67  // NewCS3 returns a new CS3 instance. Use an authenticated context and be sure to define SpaceRoot manually.
    68  func NewCS3(gwAddr, providerAddr string) (s *CS3) {
    69  	return &CS3{
    70  		providerAddr:      providerAddr,
    71  		gatewayAddr:       gwAddr,
    72  		dataGatewayClient: http.DefaultClient,
    73  	}
    74  }
    75  
    76  // NewCS3Storage returns a new cs3 storage instance. Context passed to methods is irrelevant as the service user will be used.
    77  // Be sure to call Init before using the storage.
    78  func NewCS3Storage(gwAddr, providerAddr, serviceUserID, serviceUserIDP, machineAuthAPIKey string) (s Storage, err error) {
    79  	cs3 := NewCS3(gwAddr, providerAddr)
    80  
    81  	cs3.useSystemUser = true
    82  	cs3.machineAuthAPIKey = machineAuthAPIKey
    83  	cs3.serviceUser = &user.User{
    84  		Id: &user.UserId{
    85  			OpaqueId: serviceUserID,
    86  			Idp:      serviceUserIDP,
    87  		},
    88  	}
    89  
    90  	return cs3, nil
    91  }
    92  
    93  // Backend returns the backend name of the storage
    94  func (cs3 *CS3) Backend() string {
    95  	return "cs3"
    96  }
    97  
    98  // Init creates the metadata space
    99  func (cs3 *CS3) Init(ctx context.Context, spaceid string) (err error) {
   100  	ctx, span := tracer.Start(ctx, "Init")
   101  	defer span.End()
   102  
   103  	client, err := cs3.spacesClient()
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	ctx, err = cs3.getAuthContext(ctx)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	lsRes, err := client.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{
   114  		Filters: []*provider.ListStorageSpacesRequest_Filter{
   115  			{
   116  				Type: provider.ListStorageSpacesRequest_Filter_TYPE_ID,
   117  				Term: &provider.ListStorageSpacesRequest_Filter_Id{
   118  					Id: &provider.StorageSpaceId{OpaqueId: spaceid + "!" + spaceid},
   119  				},
   120  			},
   121  		},
   122  	})
   123  	switch {
   124  	case err != nil:
   125  		return err
   126  	case lsRes.Status.Code == rpc.Code_CODE_OK && len(lsRes.StorageSpaces) > 0:
   127  		if len(lsRes.StorageSpaces) > 0 {
   128  			cs3.SpaceRoot = lsRes.StorageSpaces[0].Root
   129  			return nil
   130  		}
   131  	}
   132  
   133  	// FIXME change CS3 api to allow sending a space id
   134  	cssr, err := client.CreateStorageSpace(ctx, &provider.CreateStorageSpaceRequest{
   135  		Opaque: &types.Opaque{
   136  			Map: map[string]*types.OpaqueEntry{
   137  				"spaceid": {
   138  					Decoder: "plain",
   139  					Value:   []byte(spaceid),
   140  				},
   141  			},
   142  		},
   143  		Owner: cs3.serviceUser,
   144  		Name:  "Metadata",
   145  		Type:  "metadata",
   146  	})
   147  	switch {
   148  	case err != nil:
   149  		return err
   150  	case cssr.Status.Code == rpc.Code_CODE_OK:
   151  		cs3.SpaceRoot = cssr.StorageSpace.Root
   152  	case cssr.Status.Code == rpc.Code_CODE_ALREADY_EXISTS:
   153  		return errtypes.AlreadyExists(fmt.Sprintf("user %s does not have access to metadata space %s, but it exists", cs3.serviceUser.Id.OpaqueId, spaceid))
   154  	default:
   155  		return errtypes.NewErrtypeFromStatus(cssr.Status)
   156  	}
   157  	return nil
   158  }
   159  
   160  // SimpleUpload uploads a file to the metadata storage
   161  func (cs3 *CS3) SimpleUpload(ctx context.Context, uploadpath string, content []byte) error {
   162  	ctx, span := tracer.Start(ctx, "SimpleUpload")
   163  	defer span.End()
   164  
   165  	_, err := cs3.Upload(ctx, UploadRequest{
   166  		Path:    uploadpath,
   167  		Content: content,
   168  	})
   169  	return err
   170  }
   171  
   172  // Upload uploads a file to the metadata storage
   173  func (cs3 *CS3) Upload(ctx context.Context, req UploadRequest) (*UploadResponse, error) {
   174  	ctx, span := tracer.Start(ctx, "Upload")
   175  	defer span.End()
   176  
   177  	client, err := cs3.providerClient()
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	ctx, err = cs3.getAuthContext(ctx)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	ifuReq := &provider.InitiateFileUploadRequest{
   187  		Opaque: &types.Opaque{},
   188  		Ref: &provider.Reference{
   189  			ResourceId: cs3.SpaceRoot,
   190  			Path:       utils.MakeRelativePath(req.Path),
   191  		},
   192  	}
   193  
   194  	if req.IfMatchEtag != "" {
   195  		ifuReq.Options = &provider.InitiateFileUploadRequest_IfMatch{
   196  			IfMatch: req.IfMatchEtag,
   197  		}
   198  	}
   199  	if len(req.IfNoneMatch) > 0 {
   200  		if req.IfNoneMatch[0] == "*" {
   201  			ifuReq.Options = &provider.InitiateFileUploadRequest_IfNotExist{
   202  				IfNotExist: true,
   203  			}
   204  		}
   205  		// else {
   206  		//   the http upload will carry all if-not-match etags
   207  		// }
   208  	}
   209  	if req.IfUnmodifiedSince != (time.Time{}) {
   210  		ifuReq.Options = &provider.InitiateFileUploadRequest_IfUnmodifiedSince{
   211  			IfUnmodifiedSince: utils.TimeToTS(req.IfUnmodifiedSince),
   212  		}
   213  	}
   214  	if req.MTime != (time.Time{}) {
   215  		// The format of the X-OC-Mtime header is <epoch>.<nanoseconds>, e.g. '1691053416.934129485'
   216  		ifuReq.Opaque = utils.AppendPlainToOpaque(ifuReq.Opaque, "X-OC-Mtime", utils.TimeToOCMtime(req.MTime))
   217  	}
   218  
   219  	ifuReq.Opaque = utils.AppendPlainToOpaque(ifuReq.Opaque, net.HeaderUploadLength, strconv.FormatInt(int64(len(req.Content)), 10))
   220  
   221  	res, err := client.InitiateFileUpload(ctx, ifuReq)
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  	if res.Status.Code != rpc.Code_CODE_OK {
   226  		return nil, errtypes.NewErrtypeFromStatus(res.Status)
   227  	}
   228  
   229  	var endpoint string
   230  
   231  	for _, proto := range res.GetProtocols() {
   232  		if proto.Protocol == "simple" {
   233  			endpoint = proto.GetUploadEndpoint()
   234  			break
   235  		}
   236  	}
   237  	if endpoint == "" {
   238  		return nil, errors.New("metadata storage doesn't support the simple upload protocol")
   239  	}
   240  
   241  	httpReq, err := http.NewRequest(http.MethodPut, endpoint, bytes.NewReader(req.Content))
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	for _, etag := range req.IfNoneMatch {
   246  		httpReq.Header.Add(net.HeaderIfNoneMatch, etag)
   247  	}
   248  
   249  	md, _ := metadata.FromOutgoingContext(ctx)
   250  	httpReq.Header.Add(ctxpkg.TokenHeader, md.Get(ctxpkg.TokenHeader)[0])
   251  	resp, err := cs3.dataGatewayClient.Do(httpReq)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	defer resp.Body.Close()
   256  	if err := errtypes.NewErrtypeFromHTTPStatusCode(resp.StatusCode, httpReq.URL.Path); err != nil {
   257  		return nil, err
   258  	}
   259  	etag := resp.Header.Get("Etag")
   260  	if ocEtag := resp.Header.Get("OC-ETag"); ocEtag != "" {
   261  		etag = ocEtag
   262  	}
   263  	return &UploadResponse{
   264  		Etag:   etag,
   265  		FileID: resp.Header.Get("OC-Fileid"),
   266  	}, nil
   267  }
   268  
   269  // Stat returns the metadata for the given path
   270  func (cs3 *CS3) Stat(ctx context.Context, path string) (*provider.ResourceInfo, error) {
   271  	ctx, span := tracer.Start(ctx, "Stat")
   272  	defer span.End()
   273  
   274  	client, err := cs3.providerClient()
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	ctx, err = cs3.getAuthContext(ctx)
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  
   283  	req := provider.StatRequest{
   284  		Ref: &provider.Reference{
   285  			ResourceId: cs3.SpaceRoot,
   286  			Path:       utils.MakeRelativePath(path),
   287  		},
   288  	}
   289  
   290  	res, err := client.Stat(ctx, &req)
   291  	if err != nil {
   292  		return nil, err
   293  	}
   294  	if res.Status.Code != rpc.Code_CODE_OK {
   295  		return nil, errtypes.NewErrtypeFromStatus(res.Status)
   296  	}
   297  
   298  	return res.Info, nil
   299  }
   300  
   301  // SimpleDownload reads a file from the metadata storage
   302  func (cs3 *CS3) SimpleDownload(ctx context.Context, downloadpath string) (content []byte, err error) {
   303  	ctx, span := tracer.Start(ctx, "SimpleDownload")
   304  	defer span.End()
   305  	dres, err := cs3.Download(ctx, DownloadRequest{Path: downloadpath})
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	return dres.Content, nil
   310  }
   311  
   312  // Download reads a file from the metadata storage
   313  func (cs3 *CS3) Download(ctx context.Context, req DownloadRequest) (*DownloadResponse, error) {
   314  	ctx, span := tracer.Start(ctx, "Download")
   315  	defer span.End()
   316  
   317  	client, err := cs3.providerClient()
   318  	if err != nil {
   319  		return nil, err
   320  	}
   321  	ctx, err = cs3.getAuthContext(ctx)
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  
   326  	dreq := provider.InitiateFileDownloadRequest{
   327  		Ref: &provider.Reference{
   328  			ResourceId: cs3.SpaceRoot,
   329  			Path:       utils.MakeRelativePath(req.Path),
   330  		},
   331  	}
   332  	// FIXME add a dedicated property on the CS3 InitiateFileDownloadRequest message
   333  	// well the gateway never forwards the initiate request to the storageprovider, so we have to send it in the actual GET request
   334  	// if len(req.IfNoneMatch) > 0 {
   335  	//   dreq.Opaque = utils.AppendPlainToOpaque(dreq.Opaque, "if-none-match", strings.Join(req.IfNoneMatch, ","))
   336  	// }
   337  
   338  	res, err := client.InitiateFileDownload(ctx, &dreq)
   339  	if err != nil {
   340  		return nil, errtypes.NotFound(dreq.Ref.Path)
   341  	}
   342  
   343  	var endpoint string
   344  
   345  	for _, proto := range res.GetProtocols() {
   346  		if proto.Protocol == "spaces" {
   347  			endpoint = proto.GetDownloadEndpoint()
   348  			break
   349  		}
   350  	}
   351  	if endpoint == "" {
   352  		return nil, errors.New("metadata storage doesn't support the spaces download protocol")
   353  	}
   354  
   355  	hreq, err := http.NewRequest(http.MethodGet, endpoint, nil)
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	for _, etag := range req.IfNoneMatch {
   361  		hreq.Header.Add(net.HeaderIfNoneMatch, etag)
   362  	}
   363  
   364  	md, _ := metadata.FromOutgoingContext(ctx)
   365  	hreq.Header.Add(ctxpkg.TokenHeader, md.Get(ctxpkg.TokenHeader)[0])
   366  	resp, err := cs3.dataGatewayClient.Do(hreq)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	defer resp.Body.Close()
   371  
   372  	dres := DownloadResponse{}
   373  
   374  	dres.Etag = resp.Header.Get("etag")
   375  	dres.Etag = resp.Header.Get("oc-etag") // takes precedence
   376  
   377  	if err := errtypes.NewErrtypeFromHTTPStatusCode(resp.StatusCode, hreq.URL.Path); err != nil {
   378  		return nil, err
   379  	}
   380  
   381  	dres.Mtime, err = time.Parse(time.RFC1123Z, resp.Header.Get("last-modified"))
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  
   386  	dres.Content, err = io.ReadAll(resp.Body)
   387  	if err != nil {
   388  		return nil, err
   389  	}
   390  
   391  	return &dres, nil
   392  }
   393  
   394  // Delete deletes a path
   395  func (cs3 *CS3) Delete(ctx context.Context, path string) error {
   396  	ctx, span := tracer.Start(ctx, "Delete")
   397  	defer span.End()
   398  
   399  	client, err := cs3.providerClient()
   400  	if err != nil {
   401  		return err
   402  	}
   403  	ctx, err = cs3.getAuthContext(ctx)
   404  	if err != nil {
   405  		return err
   406  	}
   407  
   408  	res, err := client.Delete(ctx, &provider.DeleteRequest{
   409  		Ref: &provider.Reference{
   410  			ResourceId: cs3.SpaceRoot,
   411  			Path:       utils.MakeRelativePath(path),
   412  		},
   413  	})
   414  	if err != nil {
   415  		return err
   416  	}
   417  	if res.Status.Code != rpc.Code_CODE_OK {
   418  		return errtypes.NewErrtypeFromStatus(res.Status)
   419  	}
   420  
   421  	return nil
   422  }
   423  
   424  // ReadDir returns the entries in a given directory
   425  func (cs3 *CS3) ReadDir(ctx context.Context, path string) ([]string, error) {
   426  	ctx, span := tracer.Start(ctx, "ReadDir")
   427  	defer span.End()
   428  
   429  	infos, err := cs3.ListDir(ctx, path)
   430  	if err != nil {
   431  		return nil, err
   432  	}
   433  
   434  	entries := []string{}
   435  	for _, ri := range infos {
   436  		entries = append(entries, ri.Path)
   437  	}
   438  	return entries, nil
   439  }
   440  
   441  // ListDir returns a list of ResourceInfos for the entries in a given directory
   442  func (cs3 *CS3) ListDir(ctx context.Context, path string) ([]*provider.ResourceInfo, error) {
   443  	ctx, span := tracer.Start(ctx, "ListDir")
   444  	defer span.End()
   445  
   446  	client, err := cs3.providerClient()
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  	ctx, err = cs3.getAuthContext(ctx)
   451  	if err != nil {
   452  		return nil, err
   453  	}
   454  
   455  	relPath := utils.MakeRelativePath(path)
   456  	res, err := client.ListContainer(ctx, &provider.ListContainerRequest{
   457  		Ref: &provider.Reference{
   458  			ResourceId: cs3.SpaceRoot,
   459  			Path:       relPath,
   460  		},
   461  	})
   462  
   463  	if err != nil {
   464  		return nil, err
   465  	}
   466  	if res.Status.Code != rpc.Code_CODE_OK {
   467  		return nil, errtypes.NewErrtypeFromStatus(res.Status)
   468  	}
   469  
   470  	return res.Infos, nil
   471  }
   472  
   473  // MakeDirIfNotExist will create a root node in the metadata storage. Requires an authenticated context.
   474  func (cs3 *CS3) MakeDirIfNotExist(ctx context.Context, folder string) error {
   475  	ctx, span := tracer.Start(ctx, "MakeDirIfNotExist")
   476  	defer span.End()
   477  
   478  	client, err := cs3.providerClient()
   479  	if err != nil {
   480  		return err
   481  	}
   482  	ctx, err = cs3.getAuthContext(ctx)
   483  	if err != nil {
   484  		return err
   485  	}
   486  
   487  	var rootPathRef = &provider.Reference{
   488  		ResourceId: cs3.SpaceRoot,
   489  		Path:       utils.MakeRelativePath(folder),
   490  	}
   491  
   492  	resp, err := client.CreateContainer(ctx, &provider.CreateContainerRequest{
   493  		Ref: rootPathRef,
   494  	})
   495  	switch {
   496  	case err != nil:
   497  		return err
   498  	case resp.Status.Code == rpc.Code_CODE_OK:
   499  		// nothing to do in this case
   500  		return nil
   501  	case resp.Status.Code == rpc.Code_CODE_ALREADY_EXISTS:
   502  		// nothing to do in this case
   503  		return nil
   504  	default:
   505  		return errtypes.NewErrtypeFromStatus(resp.Status)
   506  	}
   507  }
   508  
   509  // CreateSymlink creates a symlink
   510  func (cs3 *CS3) CreateSymlink(ctx context.Context, oldname, newname string) error {
   511  	ctx, span := tracer.Start(ctx, "CreateSymlink")
   512  	defer span.End()
   513  
   514  	if _, err := cs3.ResolveSymlink(ctx, newname); err == nil {
   515  		return os.ErrExist
   516  	}
   517  
   518  	return cs3.SimpleUpload(ctx, newname, []byte(oldname))
   519  }
   520  
   521  // ResolveSymlink resolves a symlink
   522  func (cs3 *CS3) ResolveSymlink(ctx context.Context, name string) (string, error) {
   523  	ctx, span := tracer.Start(ctx, "ResolveSymlink")
   524  	defer span.End()
   525  
   526  	b, err := cs3.SimpleDownload(ctx, name)
   527  	if err != nil {
   528  		if errors.Is(err, errtypes.NotFound("")) {
   529  			return "", os.ErrNotExist
   530  		}
   531  		return "", err
   532  	}
   533  
   534  	return string(b), err
   535  }
   536  
   537  func (cs3 *CS3) providerClient() (provider.ProviderAPIClient, error) {
   538  	return pool.GetStorageProviderServiceClient(cs3.providerAddr)
   539  }
   540  
   541  func (cs3 *CS3) spacesClient() (provider.SpacesAPIClient, error) {
   542  	return pool.GetSpacesProviderServiceClient(cs3.providerAddr)
   543  }
   544  
   545  func (cs3 *CS3) getAuthContext(ctx context.Context) (context.Context, error) {
   546  	if !cs3.useSystemUser {
   547  		return ctx, nil
   548  	}
   549  
   550  	// we need to start a new context to get rid of an existing x-access-token in the outgoing context
   551  	authCtx := context.Background()
   552  	authCtx, span := tracer.Start(authCtx, "getAuthContext", trace.WithLinks(trace.LinkFromContext(ctx)))
   553  	defer span.End()
   554  
   555  	selector, err := pool.GatewaySelector(cs3.gatewayAddr)
   556  	if err != nil {
   557  		return nil, err
   558  	}
   559  	client, err := selector.Next()
   560  	if err != nil {
   561  		return nil, err
   562  	}
   563  
   564  	authCtx = ctxpkg.ContextSetUser(authCtx, cs3.serviceUser)
   565  	authRes, err := client.Authenticate(authCtx, &gateway.AuthenticateRequest{
   566  		Type:         "machine",
   567  		ClientId:     "userid:" + cs3.serviceUser.Id.OpaqueId,
   568  		ClientSecret: cs3.machineAuthAPIKey,
   569  	})
   570  	if err != nil {
   571  		return nil, err
   572  	}
   573  	if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
   574  		return nil, errtypes.NewErrtypeFromStatus(authRes.GetStatus())
   575  	}
   576  	authCtx = metadata.AppendToOutgoingContext(authCtx, ctxpkg.TokenHeader, authRes.Token)
   577  	return authCtx, nil
   578  }