github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/localfs/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 localfs 20 21 import ( 22 "context" 23 "encoding/json" 24 "io" 25 "os" 26 "path/filepath" 27 28 userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 29 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 30 "github.com/cs3org/reva/v2/pkg/appctx" 31 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 32 "github.com/cs3org/reva/v2/pkg/errtypes" 33 "github.com/cs3org/reva/v2/pkg/storage" 34 "github.com/cs3org/reva/v2/pkg/storage/utils/chunking" 35 "github.com/cs3org/reva/v2/pkg/utils" 36 "github.com/google/uuid" 37 "github.com/pkg/errors" 38 tusd "github.com/tus/tusd/v2/pkg/handler" 39 ) 40 41 var defaultFilePerm = os.FileMode(0664) 42 43 func (fs *localfs) Upload(ctx context.Context, req storage.UploadRequest, uff storage.UploadFinishedFunc) (*provider.ResourceInfo, error) { 44 upload, err := fs.GetUpload(ctx, req.Ref.GetPath()) 45 if err != nil { 46 return &provider.ResourceInfo{}, errors.Wrap(err, "localfs: error retrieving upload") 47 } 48 49 uploadInfo := upload.(*fileUpload) 50 51 p := uploadInfo.info.Storage["InternalDestination"] 52 if chunking.IsChunked(p) { 53 var assembledFile string 54 p, assembledFile, err = fs.chunkHandler.WriteChunk(p, req.Body) 55 if err != nil { 56 return &provider.ResourceInfo{}, err 57 } 58 if p == "" { 59 if err = uploadInfo.Terminate(ctx); err != nil { 60 return &provider.ResourceInfo{}, errors.Wrap(err, "localfs: error removing auxiliary files") 61 } 62 return &provider.ResourceInfo{}, errtypes.PartialContent(req.Ref.String()) 63 } 64 uploadInfo.info.Storage["InternalDestination"] = p 65 fd, err := os.Open(assembledFile) 66 if err != nil { 67 return &provider.ResourceInfo{}, errors.Wrap(err, "localfs: error opening assembled file") 68 } 69 defer fd.Close() 70 defer os.RemoveAll(assembledFile) 71 req.Body = fd 72 } 73 74 if _, err := uploadInfo.WriteChunk(ctx, 0, req.Body); err != nil { 75 return &provider.ResourceInfo{}, errors.Wrap(err, "localfs: error writing to binary file") 76 } 77 78 if err := uploadInfo.FinishUpload(ctx); err != nil { 79 return &provider.ResourceInfo{}, err 80 } 81 82 if uff != nil { 83 info := uploadInfo.info 84 uploadRef := &provider.Reference{ 85 ResourceId: &provider.ResourceId{ 86 StorageId: info.MetaData["providerID"], 87 SpaceId: info.Storage["SpaceRoot"], 88 OpaqueId: info.Storage["SpaceRoot"], 89 }, 90 Path: utils.MakeRelativePath(filepath.Join(info.MetaData["dir"], info.MetaData["filename"])), 91 } 92 owner, ok := ctxpkg.ContextGetUser(uploadInfo.ctx) 93 if !ok { 94 return &provider.ResourceInfo{}, errtypes.PreconditionFailed("error getting user from uploadinfo context") 95 } 96 // spaces support in localfs needs to be revisited: 97 // * info.Storage["SpaceRoot"] is never set 98 // * there is no space owner or manager that could be passed to the UploadFinishedFunc 99 uff(owner.Id, owner.Id, uploadRef) 100 } 101 102 // return id, etag and mtime 103 ri, err := fs.GetMD(ctx, req.Ref, []string{}, []string{"id", "etag", "mtime"}) 104 if err != nil { 105 return &provider.ResourceInfo{}, err 106 } 107 108 return ri, nil 109 } 110 111 // InitiateUpload returns upload ids corresponding to different protocols it supports 112 // It resolves the resource and then reuses the NewUpload function 113 // Currently requires the uploadLength to be set 114 // TODO to implement LengthDeferrerDataStore make size optional 115 // TODO read optional content for small files in this request 116 func (fs *localfs) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error) { 117 118 np, err := fs.resolve(ctx, ref) 119 if err != nil { 120 return nil, errors.Wrap(err, "localfs: error resolving reference") 121 } 122 123 info := tusd.FileInfo{ 124 MetaData: tusd.MetaData{ 125 "filename": filepath.Base(np), 126 "dir": filepath.Dir(np), 127 }, 128 Size: uploadLength, 129 } 130 131 if metadata != nil { 132 info.MetaData["providerID"] = metadata["providerID"] 133 if metadata["mtime"] != "" { 134 info.MetaData["mtime"] = metadata["mtime"] 135 } 136 if _, ok := metadata["sizedeferred"]; ok { 137 info.SizeIsDeferred = true 138 } 139 } 140 141 upload, err := fs.NewUpload(ctx, info) 142 if err != nil { 143 return nil, err 144 } 145 146 info, _ = upload.GetInfo(ctx) 147 148 return map[string]string{ 149 "simple": info.ID, 150 "tus": info.ID, 151 }, nil 152 } 153 154 // UseIn tells the tus upload middleware which extensions it supports. 155 func (fs *localfs) UseIn(composer *tusd.StoreComposer) { 156 composer.UseCore(fs) 157 composer.UseTerminater(fs) 158 // TODO composer.UseConcater(fs) 159 // TODO composer.UseLengthDeferrer(fs) 160 } 161 162 // NewUpload creates a new upload using the size as the file's length. To determine where to write the binary data 163 // the Fileinfo metadata must contain a dir and a filename. 164 // returns a unique id which is used to identify the upload. The properties Size and MetaData will be filled. 165 func (fs *localfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tusd.Upload, err error) { 166 167 log := appctx.GetLogger(ctx) 168 log.Debug().Interface("info", info).Msg("localfs: NewUpload") 169 170 fn := info.MetaData["filename"] 171 if fn == "" { 172 return nil, errors.New("localfs: missing filename in metadata") 173 } 174 info.MetaData["filename"] = filepath.Clean(info.MetaData["filename"]) 175 176 dir := info.MetaData["dir"] 177 if dir == "" { 178 return nil, errors.New("localfs: missing dir in metadata") 179 } 180 info.MetaData["dir"] = filepath.Clean(info.MetaData["dir"]) 181 182 np := fs.wrap(ctx, filepath.Join(info.MetaData["dir"], info.MetaData["filename"])) 183 184 log.Debug().Interface("info", info).Msg("localfs: resolved filename") 185 186 info.ID = uuid.New().String() 187 188 binPath, err := fs.getUploadPath(ctx, info.ID) 189 if err != nil { 190 return nil, errors.Wrap(err, "localfs: error resolving upload path") 191 } 192 usr := ctxpkg.ContextMustGetUser(ctx) 193 info.Storage = map[string]string{ 194 "Type": "LocalStore", 195 "BinPath": binPath, 196 "InternalDestination": np, 197 198 "Idp": usr.Id.Idp, 199 "UserId": usr.Id.OpaqueId, 200 "UserName": usr.Username, 201 "UserType": utils.UserTypeToString(usr.Id.Type), 202 203 "LogLevel": log.GetLevel().String(), 204 } 205 // Create binary file with no content 206 file, err := os.OpenFile(binPath, os.O_CREATE|os.O_WRONLY, defaultFilePerm) 207 if err != nil { 208 return nil, err 209 } 210 defer file.Close() 211 212 u := &fileUpload{ 213 info: info, 214 binPath: binPath, 215 infoPath: binPath + ".info", 216 fs: fs, 217 ctx: ctx, 218 } 219 220 // writeInfo creates the file by itself if necessary 221 err = u.writeInfo() 222 if err != nil { 223 return nil, err 224 } 225 226 return u, nil 227 } 228 229 func (fs *localfs) getUploadPath(ctx context.Context, uploadID string) (string, error) { 230 return filepath.Join(fs.conf.Uploads, uploadID), nil 231 } 232 233 // GetUpload returns the Upload for the given upload id 234 func (fs *localfs) GetUpload(ctx context.Context, id string) (tusd.Upload, error) { 235 binPath, err := fs.getUploadPath(ctx, id) 236 if err != nil { 237 return nil, err 238 } 239 infoPath := binPath + ".info" 240 info := tusd.FileInfo{} 241 data, err := os.ReadFile(infoPath) 242 if err != nil { 243 if os.IsNotExist(err) { 244 // Interpret os.ErrNotExist as 404 Not Found 245 err = tusd.ErrNotFound 246 } 247 return nil, err 248 } 249 if err := json.Unmarshal(data, &info); err != nil { 250 return nil, err 251 } 252 253 stat, err := os.Stat(binPath) 254 if err != nil { 255 return nil, err 256 } 257 258 info.Offset = stat.Size() 259 260 u := &userpb.User{ 261 Id: &userpb.UserId{ 262 Idp: info.Storage["Idp"], 263 OpaqueId: info.Storage["UserId"], 264 Type: utils.UserTypeMap(info.Storage["UserType"]), 265 }, 266 Username: info.Storage["UserName"], 267 } 268 269 ctx = ctxpkg.ContextSetUser(ctx, u) 270 271 return &fileUpload{ 272 info: info, 273 binPath: binPath, 274 infoPath: infoPath, 275 fs: fs, 276 ctx: ctx, 277 }, nil 278 } 279 280 type fileUpload struct { 281 // info stores the current information about the upload 282 info tusd.FileInfo 283 // infoPath is the path to the .info file 284 infoPath string 285 // binPath is the path to the binary file (which has no extension) 286 binPath string 287 // only fs knows how to handle metadata and versions 288 fs *localfs 289 // a context with a user 290 ctx context.Context 291 } 292 293 // GetInfo returns the FileInfo 294 func (upload *fileUpload) GetInfo(ctx context.Context) (tusd.FileInfo, error) { 295 return upload.info, nil 296 } 297 298 // GetReader returns an io.Reader for the upload 299 func (upload *fileUpload) GetReader(ctx context.Context) (io.ReadCloser, error) { 300 return os.Open(upload.binPath) 301 } 302 303 // WriteChunk writes the stream from the reader to the given offset of the upload 304 func (upload *fileUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (int64, error) { 305 file, err := os.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm) 306 if err != nil { 307 return 0, err 308 } 309 defer file.Close() 310 311 n, err := io.Copy(file, src) 312 313 // If the HTTP PATCH request gets interrupted in the middle (e.g. because 314 // the user wants to pause the upload), Go's net/http returns an io.ErrUnexpectedEOF. 315 // However, for OwnCloudStore it's not important whether the stream has ended 316 // on purpose or accidentally. 317 if err != nil { 318 if err != io.ErrUnexpectedEOF { 319 return n, err 320 } 321 } 322 323 upload.info.Offset += n 324 err = upload.writeInfo() 325 326 return n, err 327 } 328 329 // writeInfo updates the entire information. Everything will be overwritten. 330 func (upload *fileUpload) writeInfo() error { 331 data, err := json.Marshal(upload.info) 332 if err != nil { 333 return err 334 } 335 return os.WriteFile(upload.infoPath, data, defaultFilePerm) 336 } 337 338 // FinishUpload finishes an upload and moves the file to the internal destination 339 func (upload *fileUpload) FinishUpload(ctx context.Context) error { 340 341 np := upload.info.Storage["InternalDestination"] 342 343 // TODO check etag with If-Match header 344 // if destination exists 345 // if _, err := os.Stat(np); err == nil { 346 // the local storage does not store metadata 347 // the fileid is based on the path, so no we do not need to copy it to the new file 348 // the local storage does not track revisions 349 //} 350 351 // if destination exists 352 if _, err := os.Stat(np); err == nil { 353 // create revision 354 if err := upload.fs.archiveRevision(upload.ctx, np); err != nil { 355 return err 356 } 357 } 358 359 err := os.Rename(upload.binPath, np) 360 if err != nil { 361 return err 362 } 363 364 // only delete the upload if it was successfully written to the fs 365 if err := os.Remove(upload.infoPath); err != nil { 366 if !os.IsNotExist(err) { 367 log := appctx.GetLogger(ctx) 368 log.Err(err).Interface("info", upload.info).Msg("localfs: could not delete upload info") 369 } 370 } 371 372 // TODO: set mtime if specified in metadata 373 374 // metadata propagation is left to the storage implementation 375 return err 376 } 377 378 // To implement the termination extension as specified in https://tus.io/protocols/resumable-upload.html#termination 379 // - the storage needs to implement AsTerminatableUpload 380 // - the upload needs to implement Terminate 381 382 // AsTerminatableUpload returns a a TerminatableUpload 383 func (fs *localfs) AsTerminatableUpload(upload tusd.Upload) tusd.TerminatableUpload { 384 return upload.(*fileUpload) 385 } 386 387 // Terminate terminates the upload 388 func (upload *fileUpload) Terminate(ctx context.Context) error { 389 if err := os.Remove(upload.infoPath); err != nil { 390 return err 391 } 392 if err := os.Remove(upload.binPath); err != nil { 393 return err 394 } 395 return nil 396 }