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 }