github.com/cs3org/reva/v2@v2.27.7/pkg/storage/fs/owncloudsql/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 owncloudsql 20 21 import ( 22 "context" 23 "encoding/json" 24 "fmt" 25 "io" 26 iofs "io/fs" 27 "os" 28 "path/filepath" 29 "strconv" 30 "time" 31 32 userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 33 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 34 "github.com/cs3org/reva/v2/pkg/appctx" 35 "github.com/cs3org/reva/v2/pkg/conversions" 36 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 37 "github.com/cs3org/reva/v2/pkg/errtypes" 38 "github.com/cs3org/reva/v2/pkg/logger" 39 "github.com/cs3org/reva/v2/pkg/mime" 40 "github.com/cs3org/reva/v2/pkg/storage" 41 "github.com/cs3org/reva/v2/pkg/storage/utils/chunking" 42 "github.com/cs3org/reva/v2/pkg/storage/utils/templates" 43 "github.com/cs3org/reva/v2/pkg/utils" 44 "github.com/google/uuid" 45 "github.com/pkg/errors" 46 "github.com/rs/zerolog/log" 47 tusd "github.com/tus/tusd/v2/pkg/handler" 48 ) 49 50 var defaultFilePerm = os.FileMode(0664) 51 52 func (fs *owncloudsqlfs) Upload(ctx context.Context, req storage.UploadRequest, uff storage.UploadFinishedFunc) (*provider.ResourceInfo, error) { 53 upload, err := fs.GetUpload(ctx, req.Ref.GetPath()) 54 if err != nil { 55 return &provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error retrieving upload") 56 } 57 58 uploadInfo := upload.(*fileUpload) 59 60 p := uploadInfo.info.Storage["InternalDestination"] 61 if chunking.IsChunked(p) { 62 var assembledFile string 63 p, assembledFile, err = fs.chunkHandler.WriteChunk(p, req.Body) 64 if err != nil { 65 return &provider.ResourceInfo{}, err 66 } 67 if p == "" { 68 if err = uploadInfo.Terminate(ctx); err != nil { 69 return &provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error removing auxiliary files") 70 } 71 return &provider.ResourceInfo{}, errtypes.PartialContent(req.Ref.String()) 72 } 73 uploadInfo.info.Storage["InternalDestination"] = p 74 fd, err := os.Open(assembledFile) 75 if err != nil { 76 return &provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error opening assembled file") 77 } 78 defer fd.Close() 79 defer os.RemoveAll(assembledFile) 80 req.Body = fd 81 } 82 83 if _, err := uploadInfo.WriteChunk(ctx, 0, req.Body); err != nil { 84 return &provider.ResourceInfo{}, errors.Wrap(err, "owncloudsql: error writing to binary file") 85 } 86 87 if err := uploadInfo.FinishUpload(ctx); err != nil { 88 return &provider.ResourceInfo{}, err 89 } 90 91 if uff != nil { 92 info := uploadInfo.info 93 uploadRef := &provider.Reference{ 94 ResourceId: &provider.ResourceId{ 95 StorageId: info.MetaData["providerID"], 96 SpaceId: info.Storage["SpaceRoot"], 97 OpaqueId: info.Storage["SpaceRoot"], 98 }, 99 Path: utils.MakeRelativePath(filepath.Join(info.MetaData["dir"], info.MetaData["filename"])), 100 } 101 owner, ok := ctxpkg.ContextGetUser(uploadInfo.ctx) 102 if !ok { 103 return &provider.ResourceInfo{}, errtypes.PreconditionFailed("error getting user from uploadinfo context") 104 } 105 // spaces support in localfs needs to be revisited: 106 // * info.Storage["SpaceRoot"] is never set 107 // * there is no space owner or manager that could be passed to the UploadFinishedFunc 108 uff(owner.Id, owner.Id, uploadRef) 109 } 110 111 ri := &provider.ResourceInfo{ 112 // fill with at least fileid, mtime and etag 113 Id: &provider.ResourceId{ 114 StorageId: uploadInfo.info.MetaData["providerID"], 115 SpaceId: uploadInfo.info.Storage["StorageId"], 116 OpaqueId: uploadInfo.info.Storage["fileid"], 117 }, 118 Etag: uploadInfo.info.MetaData["etag"], 119 } 120 121 if mtime, err := utils.MTimeToTS(uploadInfo.info.MetaData["mtime"]); err == nil { 122 ri.Mtime = &mtime 123 } 124 125 return ri, nil 126 } 127 128 // InitiateUpload returns upload ids corresponding to different protocols it supports 129 // TODO read optional content for small files in this request 130 func (fs *owncloudsqlfs) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error) { 131 ip, err := fs.resolve(ctx, ref) 132 if err != nil { 133 return nil, errors.Wrap(err, "owncloudsql: error resolving reference") 134 } 135 136 // permissions are checked in NewUpload below 137 138 p := fs.toStoragePath(ctx, ip) 139 140 info := tusd.FileInfo{ 141 MetaData: tusd.MetaData{ 142 "filename": filepath.Base(p), 143 "dir": filepath.Dir(p), 144 "mtime": strconv.FormatInt(time.Now().Unix(), 10), 145 }, 146 Size: uploadLength, 147 } 148 149 if metadata != nil { 150 info.MetaData["providerID"] = metadata["providerID"] 151 if metadata["mtime"] != "" { 152 info.MetaData["mtime"] = metadata["mtime"] 153 } 154 if _, ok := metadata["sizedeferred"]; ok { 155 info.SizeIsDeferred = true 156 } 157 } 158 159 upload, err := fs.NewUpload(ctx, info) 160 if err != nil { 161 return nil, err 162 } 163 164 info, _ = upload.GetInfo(ctx) 165 166 return map[string]string{ 167 "simple": info.ID, 168 "tus": info.ID, 169 }, nil 170 } 171 172 // UseIn tells the tus upload middleware which extensions it supports. 173 func (fs *owncloudsqlfs) UseIn(composer *tusd.StoreComposer) { 174 composer.UseCore(fs) 175 composer.UseTerminater(fs) 176 composer.UseConcater(fs) 177 composer.UseLengthDeferrer(fs) 178 } 179 180 // To implement the core tus.io protocol as specified in https://tus.io/protocols/resumable-upload.html#core-protocol 181 // - the storage needs to implement NewUpload and GetUpload 182 // - the upload needs to implement the tusd.Upload interface: WriteChunk, GetInfo, GetReader and FinishUpload 183 184 func (fs *owncloudsqlfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tusd.Upload, err error) { 185 186 log := appctx.GetLogger(ctx) 187 log.Debug().Interface("info", info).Msg("owncloudsql: NewUpload") 188 189 if info.MetaData["filename"] == "" { 190 return nil, errors.New("owncloudsql: missing filename in metadata") 191 } 192 info.MetaData["filename"] = filepath.Clean(info.MetaData["filename"]) 193 194 dir := info.MetaData["dir"] 195 if dir == "" { 196 return nil, errors.New("owncloudsql: missing dir in metadata") 197 } 198 info.MetaData["dir"] = filepath.Clean(info.MetaData["dir"]) 199 200 ip := fs.toInternalPath(ctx, filepath.Join(info.MetaData["dir"], info.MetaData["filename"])) 201 202 // check permissions 203 var perm *provider.ResourcePermissions 204 var perr error 205 var fsInfo iofs.FileInfo 206 // if destination exists 207 if fsInfo, err = os.Stat(ip); err == nil { 208 // check permissions of file to be overwritten 209 perm, perr = fs.readPermissions(ctx, ip) 210 } else { 211 // check permissions of parent folder 212 perm, perr = fs.readPermissions(ctx, filepath.Dir(ip)) 213 } 214 if perr == nil { 215 if !perm.InitiateFileUpload { 216 return nil, errtypes.PermissionDenied("") 217 } 218 } else { 219 if os.IsNotExist(err) { 220 return nil, errtypes.NotFound(fs.toStoragePath(ctx, filepath.Dir(ip))) 221 } 222 return nil, errors.Wrap(err, "owncloudsql: error reading permissions") 223 } 224 225 // if we are trying to overwriting a folder with a file 226 if fsInfo != nil && fsInfo.IsDir() { 227 return nil, errtypes.PreconditionFailed("resource is not a file") 228 } 229 230 log.Debug().Interface("info", info).Msg("owncloudsql: resolved filename") 231 232 info.ID = uuid.New().String() 233 234 binPath, err := fs.getUploadPath(ctx, info.ID) 235 if err != nil { 236 return nil, errors.Wrap(err, "owncloudsql: error resolving upload path") 237 } 238 usr := ctxpkg.ContextMustGetUser(ctx) 239 storageID, err := fs.getStorage(ctx, ip) 240 if err != nil { 241 return nil, err 242 } 243 info.Storage = map[string]string{ 244 "Type": "OwnCloudStore", 245 "BinPath": binPath, 246 "InternalDestination": ip, 247 "Permissions": strconv.Itoa((int)(conversions.RoleFromResourcePermissions(perm, false).OCSPermissions())), 248 249 "Idp": usr.Id.Idp, 250 "UserId": usr.Id.OpaqueId, 251 "UserName": usr.Username, 252 253 "LogLevel": log.GetLevel().String(), 254 255 "StorageId": strconv.Itoa(storageID), 256 } 257 // Create binary file in the upload folder with no content 258 log.Debug().Interface("info", info).Msg("owncloudsql: built storage info") 259 file, err := os.OpenFile(binPath, os.O_CREATE|os.O_WRONLY, defaultFilePerm) 260 if err != nil { 261 return nil, err 262 } 263 defer file.Close() 264 265 u := &fileUpload{ 266 info: info, 267 binPath: binPath, 268 infoPath: filepath.Join(fs.c.UploadInfoDir, info.ID+".info"), 269 fs: fs, 270 ctx: ctx, 271 } 272 273 // writeInfo creates the file by itself if necessary 274 err = u.writeInfo() 275 if err != nil { 276 return nil, err 277 } 278 279 return u, nil 280 } 281 282 func (fs *owncloudsqlfs) getUploadPath(ctx context.Context, uploadID string) (string, error) { 283 u, ok := ctxpkg.ContextGetUser(ctx) 284 if !ok { 285 err := errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx") 286 return "", err 287 } 288 layout := templates.WithUser(u, fs.c.UserLayout) 289 return filepath.Join(fs.c.DataDirectory, layout, "uploads", uploadID), nil 290 } 291 292 // GetUpload returns the Upload for the given upload id 293 func (fs *owncloudsqlfs) GetUpload(ctx context.Context, id string) (tusd.Upload, error) { 294 infoPath := filepath.Join(fs.c.UploadInfoDir, id+".info") 295 296 info := tusd.FileInfo{} 297 data, err := os.ReadFile(infoPath) 298 if err != nil { 299 if os.IsNotExist(err) { 300 // Interpret os.ErrNotExist as 404 Not Found 301 err = tusd.ErrNotFound 302 } 303 return nil, err 304 } 305 if err := json.Unmarshal(data, &info); err != nil { 306 return nil, err 307 } 308 309 stat, err := os.Stat(info.Storage["BinPath"]) 310 if err != nil { 311 return nil, err 312 } 313 314 info.Offset = stat.Size() 315 316 u := &userpb.User{ 317 Id: &userpb.UserId{ 318 Idp: info.Storage["Idp"], 319 OpaqueId: info.Storage["UserId"], 320 }, 321 Username: info.Storage["UserName"], 322 } 323 324 ctx = ctxpkg.ContextSetUser(ctx, u) 325 // TODO configure the logger the same way ... store and add traceid in file info 326 327 var opts []logger.Option 328 opts = append(opts, logger.WithLevel(info.Storage["LogLevel"])) 329 opts = append(opts, logger.WithWriter(os.Stderr, logger.ConsoleMode)) 330 l := logger.New(opts...) 331 332 sub := l.With().Int("pid", os.Getpid()).Logger() 333 334 ctx = appctx.WithLogger(ctx, &sub) 335 336 return &fileUpload{ 337 info: info, 338 binPath: info.Storage["BinPath"], 339 infoPath: infoPath, 340 fs: fs, 341 ctx: ctx, 342 }, nil 343 } 344 345 type fileUpload struct { 346 // info stores the current information about the upload 347 info tusd.FileInfo 348 // infoPath is the path to the .info file 349 infoPath string 350 // binPath is the path to the binary file (which has no extension) 351 binPath string 352 // only fs knows how to handle metadata and versions 353 fs *owncloudsqlfs 354 // a context with a user 355 // TODO add logger as well? 356 ctx context.Context 357 } 358 359 // GetInfo returns the FileInfo 360 func (upload *fileUpload) GetInfo(ctx context.Context) (tusd.FileInfo, error) { 361 return upload.info, nil 362 } 363 364 // WriteChunk writes the stream from the reader to the given offset of the upload 365 func (upload *fileUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) { 366 file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm) 367 if err != nil { 368 return 0, err 369 } 370 defer file.Close() 371 372 n, err := io.Copy(file, src) 373 374 // If the HTTP PATCH request gets interrupted in the middle (e.g. because 375 // the user wants to pause the upload), Go's net/http returns an io.ErrUnexpectedEOF. 376 // However, for OwnCloudStore it's not important whether the stream has ended 377 // on purpose or accidentally. 378 if err != nil { 379 if err != io.ErrUnexpectedEOF { 380 return n, err 381 } 382 } 383 384 upload.info.Offset += n 385 err = upload.writeInfo() // TODO info is written here ... we need to truncate in DiscardChunk 386 387 return n, err 388 } 389 390 // GetReader returns an io.Reader for the upload 391 func (upload *fileUpload) GetReader(ctx context.Context) (io.ReadCloser, error) { 392 return os.Open(upload.binPath) 393 } 394 395 // writeInfo updates the entire information. Everything will be overwritten. 396 func (upload *fileUpload) writeInfo() error { 397 log.Debug().Str("path", upload.infoPath).Msg("Writing info file") 398 data, err := json.Marshal(upload.info) 399 if err != nil { 400 return err 401 } 402 return os.WriteFile(upload.infoPath, data, defaultFilePerm) 403 } 404 405 // FinishUpload finishes an upload and moves the file to the internal destination 406 func (upload *fileUpload) FinishUpload(ctx context.Context) error { 407 408 ip := upload.info.Storage["InternalDestination"] 409 410 // if destination exists 411 // TODO check etag with If-Match header 412 if _, err := os.Stat(ip); err == nil { 413 // create revision 414 if err := upload.fs.archiveRevision(upload.ctx, upload.fs.getVersionsPath(upload.ctx, ip), ip); err != nil { 415 return err 416 } 417 } 418 419 sha1h, md5h, adler32h, err := upload.fs.HashFile(upload.binPath) 420 if err != nil { 421 log.Err(err).Msg("owncloudsql: could not open file for checksumming") 422 } 423 424 err = os.Rename(upload.binPath, ip) 425 if err != nil { 426 log.Err(err).Interface("info", upload.info). 427 Str("binPath", upload.binPath). 428 Str("ipath", ip). 429 Msg("owncloudsql: could not rename") 430 return err 431 } 432 433 var fi os.FileInfo 434 fi, err = os.Stat(ip) 435 if err != nil { 436 return err 437 } 438 439 perms, err := strconv.Atoi(upload.info.Storage["Permissions"]) 440 if err != nil { 441 return err 442 } 443 444 if upload.info.MetaData["mtime"] == "" { 445 upload.info.MetaData["mtime"] = fmt.Sprintf("%d", fi.ModTime().Unix()) 446 } 447 if upload.info.MetaData["etag"] == "" { 448 upload.info.MetaData["etag"] = calcEtag(upload.ctx, fi) 449 } 450 451 data := map[string]interface{}{ 452 "path": upload.fs.toDatabasePath(ip), 453 "checksum": fmt.Sprintf("SHA1:%032x MD5:%032x ADLER32:%032x", sha1h, md5h, adler32h), 454 "etag": upload.info.MetaData["etag"], 455 "size": upload.info.Size, 456 "mimetype": mime.Detect(false, ip), 457 "permissions": perms, 458 "mtime": upload.info.MetaData["mtime"], 459 "storage_mtime": upload.info.MetaData["mtime"], 460 } 461 var fileid int 462 fileid, err = upload.fs.filecache.InsertOrUpdate(ctx, upload.info.Storage["StorageId"], data, false) 463 if err != nil { 464 return err 465 } 466 upload.info.Storage["fileid"] = fmt.Sprintf("%d", fileid) 467 468 // only delete the upload if it was successfully written to the storage 469 if err := os.Remove(upload.infoPath); err != nil { 470 if !os.IsNotExist(err) { 471 log.Err(err).Interface("info", upload.info).Msg("owncloudsql: could not delete upload info") 472 return err 473 } 474 } 475 476 return upload.fs.propagate(upload.ctx, ip) 477 } 478 479 // To implement the termination extension as specified in https://tus.io/protocols/resumable-upload.html#termination 480 // - the storage needs to implement AsTerminatableUpload 481 // - the upload needs to implement Terminate 482 483 // AsTerminatableUpload returns a TerminatableUpload 484 func (fs *owncloudsqlfs) AsTerminatableUpload(upload tusd.Upload) tusd.TerminatableUpload { 485 return upload.(*fileUpload) 486 } 487 488 // Terminate terminates the upload 489 func (upload *fileUpload) Terminate(ctx context.Context) error { 490 if err := os.Remove(upload.infoPath); err != nil { 491 if !os.IsNotExist(err) { 492 return err 493 } 494 } 495 if err := os.Remove(upload.binPath); err != nil { 496 if !os.IsNotExist(err) { 497 return err 498 } 499 } 500 return nil 501 } 502 503 // To implement the creation-defer-length extension as specified in https://tus.io/protocols/resumable-upload.html#creation 504 // - the storage needs to implement AsLengthDeclarableUpload 505 // - the upload needs to implement DeclareLength 506 507 // AsLengthDeclarableUpload returns a LengthDeclarableUpload 508 func (fs *owncloudsqlfs) AsLengthDeclarableUpload(upload tusd.Upload) tusd.LengthDeclarableUpload { 509 return upload.(*fileUpload) 510 } 511 512 // DeclareLength updates the upload length information 513 func (upload *fileUpload) DeclareLength(ctx context.Context, length int64) error { 514 upload.info.Size = length 515 upload.info.SizeIsDeferred = false 516 return upload.writeInfo() 517 } 518 519 // To implement the concatenation extension as specified in https://tus.io/protocols/resumable-upload.html#concatenation 520 // - the storage needs to implement AsConcatableUpload 521 // - the upload needs to implement ConcatUploads 522 523 // AsConcatableUpload returns a ConcatableUpload 524 func (fs *owncloudsqlfs) AsConcatableUpload(upload tusd.Upload) tusd.ConcatableUpload { 525 return upload.(*fileUpload) 526 } 527 528 // ConcatUploads concatenates multiple uploads 529 func (upload *fileUpload) ConcatUploads(ctx context.Context, uploads []tusd.Upload) (err error) { 530 file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm) 531 if err != nil { 532 return err 533 } 534 defer file.Close() 535 536 for _, partialUpload := range uploads { 537 fileUpload := partialUpload.(*fileUpload) 538 539 src, err := os.Open(fileUpload.binPath) 540 if err != nil { 541 return err 542 } 543 544 if _, err := io.Copy(file, src); err != nil { 545 return err 546 } 547 } 548 549 return 550 }