github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/decomposedfs/upload.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 decomposedfs 20 21 import ( 22 "context" 23 "fmt" 24 "os" 25 "path/filepath" 26 "strings" 27 "time" 28 29 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 30 "github.com/google/uuid" 31 "github.com/pkg/errors" 32 tusd "github.com/tus/tusd/v2/pkg/handler" 33 34 "github.com/cs3org/reva/v2/pkg/appctx" 35 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 36 "github.com/cs3org/reva/v2/pkg/errtypes" 37 "github.com/cs3org/reva/v2/pkg/rhttp/datatx/metrics" 38 "github.com/cs3org/reva/v2/pkg/storage" 39 "github.com/cs3org/reva/v2/pkg/storage/utils/chunking" 40 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node" 41 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/upload" 42 "github.com/cs3org/reva/v2/pkg/storagespace" 43 "github.com/cs3org/reva/v2/pkg/utils" 44 ) 45 46 // Upload uploads data to the given resource 47 // TODO Upload (and InitiateUpload) needs a way to receive the expected checksum. 48 // Maybe in metadata as 'checksum' => 'sha1 aeosvp45w5xaeoe' = lowercase, space separated? 49 func (fs *Decomposedfs) Upload(ctx context.Context, req storage.UploadRequest, uff storage.UploadFinishedFunc) (*provider.ResourceInfo, error) { 50 _, span := tracer.Start(ctx, "Upload") 51 defer span.End() 52 up, err := fs.GetUpload(ctx, req.Ref.GetPath()) 53 if err != nil { 54 return &provider.ResourceInfo{}, errors.Wrap(err, "Decomposedfs: error retrieving upload") 55 } 56 57 session := up.(*upload.OcisSession) 58 59 ctx = session.Context(ctx) 60 61 if session.Chunk() != "" { // check chunking v1 62 p, assembledFile, err := fs.chunkHandler.WriteChunk(session.Chunk(), req.Body) 63 if err != nil { 64 return &provider.ResourceInfo{}, err 65 } 66 if p == "" { 67 if err = session.Terminate(ctx); err != nil { 68 return &provider.ResourceInfo{}, errors.Wrap(err, "Decomposedfs: error removing auxiliary files") 69 } 70 return &provider.ResourceInfo{}, errtypes.PartialContent(req.Ref.String()) 71 } 72 fd, err := os.Open(assembledFile) 73 if err != nil { 74 return &provider.ResourceInfo{}, errors.Wrap(err, "Decomposedfs: error opening assembled file") 75 } 76 defer fd.Close() 77 defer os.RemoveAll(assembledFile) 78 req.Body = fd 79 80 size, err := session.WriteChunk(ctx, 0, req.Body) 81 if err != nil { 82 return &provider.ResourceInfo{}, errors.Wrap(err, "Decomposedfs: error writing to binary file") 83 } 84 session.SetSize(size) 85 } else { 86 size, err := session.WriteChunk(ctx, 0, req.Body) 87 if err != nil { 88 return &provider.ResourceInfo{}, errors.Wrap(err, "Decomposedfs: error writing to binary file") 89 } 90 if size != req.Length { 91 return &provider.ResourceInfo{}, errtypes.PartialContent("Decomposedfs: unexpected end of stream") 92 } 93 } 94 95 if err := session.FinishUploadDecomposed(ctx); err != nil { 96 return &provider.ResourceInfo{}, err 97 } 98 99 if uff != nil { 100 uploadRef := &provider.Reference{ 101 ResourceId: &provider.ResourceId{ 102 StorageId: session.ProviderID(), 103 SpaceId: session.SpaceID(), 104 OpaqueId: session.SpaceID(), 105 }, 106 Path: utils.MakeRelativePath(filepath.Join(session.Dir(), session.Filename())), 107 } 108 executant := session.Executant() 109 uff(session.SpaceOwner(), &executant, uploadRef) 110 } 111 112 ri := &provider.ResourceInfo{ 113 // fill with at least fileid, mtime and etag 114 Id: &provider.ResourceId{ 115 StorageId: session.ProviderID(), 116 SpaceId: session.SpaceID(), 117 OpaqueId: session.NodeID(), 118 }, 119 } 120 121 // add etag to metadata 122 ri.Etag, _ = node.CalculateEtag(session.NodeID(), session.MTime()) 123 124 if !session.MTime().IsZero() { 125 ri.Mtime = utils.TimeToTS(session.MTime()) 126 } 127 128 return ri, nil 129 } 130 131 // InitiateUpload returns upload ids corresponding to different protocols it supports 132 // TODO read optional content for small files in this request 133 // TODO InitiateUpload (and Upload) needs a way to receive the expected checksum. Maybe in metadata as 'checksum' => 'sha1 aeosvp45w5xaeoe' = lowercase, space separated? 134 func (fs *Decomposedfs) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error) { 135 _, span := tracer.Start(ctx, "InitiateUpload") 136 defer span.End() 137 log := appctx.GetLogger(ctx) 138 139 // remember the path from the reference 140 refpath := ref.GetPath() 141 var chunk *chunking.ChunkBLOBInfo 142 var err error 143 if chunking.IsChunked(refpath) { // check chunking v1 144 chunk, err = chunking.GetChunkBLOBInfo(refpath) 145 if err != nil { 146 return nil, errtypes.BadRequest(err.Error()) 147 } 148 ref.Path = chunk.Path 149 } 150 n, err := fs.lu.NodeFromResource(ctx, ref) 151 switch err.(type) { 152 case nil: 153 // ok 154 case errtypes.IsNotFound: 155 return nil, errtypes.PreconditionFailed(err.Error()) 156 default: 157 return nil, err 158 } 159 160 // permissions are checked in NewUpload below 161 162 relative, err := fs.lu.Path(ctx, n, node.NoCheck) 163 // TODO why do we need the path here? 164 // jfd: it is used later when emitting the UploadReady event ... 165 // AAAND refPath might be . when accessing with an id / relative reference ... which causes NodeName to become . But then dir will also always be . 166 // That is why we still have to read the path here: so that the event we emit contains a relative reference with a path relative to the space root. WTF 167 if err != nil { 168 return nil, err 169 } 170 171 lockID, _ := ctxpkg.ContextGetLockID(ctx) 172 173 session := fs.sessionStore.New(ctx) 174 session.SetMetadata("filename", n.Name) 175 session.SetStorageValue("NodeName", n.Name) 176 if chunk != nil { 177 session.SetStorageValue("Chunk", filepath.Base(refpath)) 178 } 179 session.SetMetadata("dir", filepath.Dir(relative)) 180 session.SetStorageValue("Dir", filepath.Dir(relative)) 181 session.SetMetadata("lockid", lockID) 182 183 session.SetSize(uploadLength) 184 session.SetStorageValue("SpaceRoot", n.SpaceRoot.ID) // TODO SpaceRoot -> SpaceID 185 session.SetStorageValue("SpaceOwnerOrManager", n.SpaceOwnerOrManager(ctx).GetOpaqueId()) // TODO needed for what? 186 187 spaceGID, ok := ctx.Value(CtxKeySpaceGID).(uint32) 188 if ok { 189 session.SetStorageValue("SpaceGid", fmt.Sprintf("%d", spaceGID)) 190 } 191 192 iid, _ := ctxpkg.ContextGetInitiator(ctx) 193 session.SetMetadata("initiatorid", iid) 194 195 if metadata != nil { 196 session.SetMetadata("providerID", metadata["providerID"]) 197 if mtime, ok := metadata["mtime"]; ok { 198 if mtime != "null" { 199 session.SetMetadata("mtime", metadata["mtime"]) 200 } 201 } 202 if expiration, ok := metadata["expires"]; ok { 203 if expiration != "null" { 204 session.SetMetadata("expires", metadata["expires"]) 205 } 206 } 207 if _, ok := metadata["sizedeferred"]; ok { 208 session.SetSizeIsDeferred(true) 209 } 210 if checksum, ok := metadata["checksum"]; ok { 211 parts := strings.SplitN(checksum, " ", 2) 212 if len(parts) != 2 { 213 return nil, errtypes.BadRequest("invalid checksum format. must be '[algorithm] [checksum]'") 214 } 215 switch parts[0] { 216 case "sha1", "md5", "adler32": 217 session.SetMetadata("checksum", checksum) 218 default: 219 return nil, errtypes.BadRequest("unsupported checksum algorithm: " + parts[0]) 220 } 221 } 222 223 // only check preconditions if they are not empty // TODO or is this a bad request? 224 if metadata["if-match"] != "" { 225 session.SetMetadata("if-match", metadata["if-match"]) 226 } 227 if metadata["if-none-match"] != "" { 228 session.SetMetadata("if-none-match", metadata["if-none-match"]) 229 } 230 if metadata["if-unmodified-since"] != "" { 231 session.SetMetadata("if-unmodified-since", metadata["if-unmodified-since"]) 232 } 233 } 234 235 if session.MTime().IsZero() { 236 session.SetMetadata("mtime", utils.TimeToOCMtime(time.Now())) 237 } 238 239 log.Debug().Str("uploadid", session.ID()).Str("spaceid", n.SpaceID).Str("nodeid", n.ID).Interface("metadata", metadata).Msg("Decomposedfs: resolved filename") 240 241 _, err = node.CheckQuota(ctx, n.SpaceRoot, n.Exists, uint64(n.Blobsize), uint64(session.Size())) 242 if err != nil { 243 return nil, err 244 } 245 246 if session.Filename() == "" { 247 return nil, errors.New("Decomposedfs: missing filename in metadata") 248 } 249 if session.Dir() == "" { 250 return nil, errors.New("Decomposedfs: missing dir in metadata") 251 } 252 253 // the parent owner will become the new owner 254 parent, perr := n.Parent(ctx) 255 if perr != nil { 256 return nil, errors.Wrap(perr, "Decomposedfs: error getting parent "+n.ParentID) 257 } 258 259 // check permissions 260 var ( 261 checkNode *node.Node 262 path string 263 ) 264 if n.Exists { 265 // check permissions of file to be overwritten 266 checkNode = n 267 path, _ = storagespace.FormatReference(&provider.Reference{ResourceId: &provider.ResourceId{ 268 SpaceId: checkNode.SpaceID, 269 OpaqueId: checkNode.ID, 270 }}) 271 } else { 272 // check permissions of parent 273 checkNode = parent 274 path, _ = storagespace.FormatReference(&provider.Reference{ResourceId: &provider.ResourceId{ 275 SpaceId: checkNode.SpaceID, 276 OpaqueId: checkNode.ID, 277 }, Path: n.Name}) 278 } 279 rp, err := fs.p.AssemblePermissions(ctx, checkNode) 280 switch { 281 case err != nil: 282 return nil, err 283 case !rp.InitiateFileUpload: 284 return nil, errtypes.PermissionDenied(path) 285 } 286 287 // are we trying to overwriting a folder with a file? 288 if n.Exists && n.IsDir(ctx) { 289 return nil, errtypes.PreconditionFailed("resource is not a file") 290 } 291 292 // check lock 293 if err := n.CheckLock(ctx); err != nil { 294 return nil, err 295 } 296 297 usr := ctxpkg.ContextMustGetUser(ctx) 298 299 // fill future node info 300 if n.Exists { 301 if session.HeaderIfNoneMatch() == "*" { 302 return nil, errtypes.Aborted(fmt.Sprintf("parent %s already has a child %s, id %s", n.ParentID, n.Name, n.ID)) 303 } 304 session.SetStorageValue("NodeId", n.ID) 305 session.SetStorageValue("NodeExists", "true") 306 } else { 307 session.SetStorageValue("NodeId", uuid.New().String()) 308 } 309 session.SetStorageValue("NodeParentId", n.ParentID) 310 session.SetExecutant(usr) 311 session.SetStorageValue("LogLevel", log.GetLevel().String()) 312 313 log.Debug().Interface("session", session).Msg("Decomposedfs: built session info") 314 315 err = fs.um.RunInBaseScope(func() error { 316 // Create binary file in the upload folder with no content 317 // It will be used when determining the current offset of an upload 318 err := session.TouchBin() 319 if err != nil { 320 return err 321 } 322 323 return session.Persist(ctx) 324 }) 325 if err != nil { 326 return nil, err 327 } 328 metrics.UploadSessionsInitiated.Inc() 329 330 if uploadLength == 0 { 331 // Directly finish this upload 332 err = session.FinishUploadDecomposed(ctx) 333 if err != nil { 334 return nil, err 335 } 336 } 337 338 return map[string]string{ 339 "simple": session.ID(), 340 "tus": session.ID(), 341 }, nil 342 } 343 344 // UseIn tells the tus upload middleware which extensions it supports. 345 func (fs *Decomposedfs) UseIn(composer *tusd.StoreComposer) { 346 composer.UseCore(fs) 347 composer.UseTerminater(fs) 348 composer.UseConcater(fs) 349 composer.UseLengthDeferrer(fs) 350 } 351 352 // To implement the core tus.io protocol as specified in https://tus.io/protocols/resumable-upload.html#core-protocol 353 // - the storage needs to implement NewUpload and GetUpload 354 // - the upload needs to implement the tusd.Upload interface: WriteChunk, GetInfo, GetReader and FinishUpload 355 356 // NewUpload returns a new tus Upload instance 357 func (fs *Decomposedfs) NewUpload(ctx context.Context, info tusd.FileInfo) (tusd.Upload, error) { 358 return nil, fmt.Errorf("not implemented, use InitiateUpload on the CS3 API to start a new upload") 359 } 360 361 // GetUpload returns the Upload for the given upload id 362 func (fs *Decomposedfs) GetUpload(ctx context.Context, id string) (tusd.Upload, error) { 363 var ul tusd.Upload 364 var err error 365 _ = fs.um.RunInBaseScope(func() error { 366 ul, err = fs.sessionStore.Get(ctx, id) 367 return nil 368 }) 369 return ul, err 370 } 371 372 // ListUploadSessions returns the upload sessions for the given filter 373 func (fs *Decomposedfs) ListUploadSessions(ctx context.Context, filter storage.UploadSessionFilter) ([]storage.UploadSession, error) { 374 var sessions []*upload.OcisSession 375 if filter.ID != nil && *filter.ID != "" { 376 session, err := fs.sessionStore.Get(ctx, *filter.ID) 377 if err != nil { 378 return nil, err 379 } 380 sessions = []*upload.OcisSession{session} 381 } else { 382 var err error 383 sessions, err = fs.sessionStore.List(ctx) 384 if err != nil { 385 return nil, err 386 } 387 } 388 filteredSessions := []storage.UploadSession{} 389 now := time.Now() 390 for _, session := range sessions { 391 if filter.Processing != nil && *filter.Processing != session.IsProcessing() { 392 continue 393 } 394 if filter.Expired != nil { 395 if *filter.Expired { 396 if now.Before(session.Expires()) { 397 continue 398 } 399 } else { 400 if now.After(session.Expires()) { 401 continue 402 } 403 } 404 } 405 if filter.HasVirus != nil { 406 sr, _ := session.ScanData() 407 infected := sr != "" 408 if *filter.HasVirus != infected { 409 continue 410 } 411 } 412 filteredSessions = append(filteredSessions, session) 413 } 414 return filteredSessions, nil 415 } 416 417 // AsTerminatableUpload returns a TerminatableUpload 418 // To implement the termination extension as specified in https://tus.io/protocols/resumable-upload.html#termination 419 // the storage needs to implement AsTerminatableUpload 420 func (fs *Decomposedfs) AsTerminatableUpload(up tusd.Upload) tusd.TerminatableUpload { 421 return up.(*upload.OcisSession) 422 } 423 424 // AsLengthDeclarableUpload returns a LengthDeclarableUpload 425 // To implement the creation-defer-length extension as specified in https://tus.io/protocols/resumable-upload.html#creation 426 // the storage needs to implement AsLengthDeclarableUpload 427 func (fs *Decomposedfs) AsLengthDeclarableUpload(up tusd.Upload) tusd.LengthDeclarableUpload { 428 return up.(*upload.OcisSession) 429 } 430 431 // AsConcatableUpload returns a ConcatableUpload 432 // To implement the concatenation extension as specified in https://tus.io/protocols/resumable-upload.html#concatenation 433 // the storage needs to implement AsConcatableUpload 434 func (fs *Decomposedfs) AsConcatableUpload(up tusd.Upload) tusd.ConcatableUpload { 435 return up.(*upload.OcisSession) 436 }