github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/decomposedfs/upload/store.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 upload 20 21 import ( 22 "context" 23 "encoding/json" 24 "fmt" 25 iofs "io/fs" 26 "os" 27 "path/filepath" 28 "regexp" 29 "strconv" 30 "strings" 31 "syscall" 32 "time" 33 34 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 35 "github.com/cs3org/reva/v2/pkg/appctx" 36 "github.com/cs3org/reva/v2/pkg/errtypes" 37 "github.com/cs3org/reva/v2/pkg/events" 38 "github.com/cs3org/reva/v2/pkg/storage" 39 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/aspects" 40 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata" 41 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes" 42 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node" 43 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/options" 44 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/usermapper" 45 "github.com/google/uuid" 46 "github.com/pkg/errors" 47 "github.com/rogpeppe/go-internal/lockedfile" 48 "github.com/rs/zerolog" 49 tusd "github.com/tus/tusd/v2/pkg/handler" 50 ) 51 52 var _idRegexp = regexp.MustCompile(".*/([^/]+).info") 53 54 // PermissionsChecker defines an interface for checking permissions on a Node 55 type PermissionsChecker interface { 56 AssemblePermissions(ctx context.Context, n *node.Node) (ap provider.ResourcePermissions, err error) 57 } 58 59 // OcisStore manages upload sessions 60 type OcisStore struct { 61 fs storage.FS 62 lu node.PathLookup 63 tp node.Tree 64 um usermapper.Mapper 65 root string 66 pub events.Publisher 67 async bool 68 tknopts options.TokenOptions 69 disableVersioning bool 70 log *zerolog.Logger 71 } 72 73 // NewSessionStore returns a new OcisStore 74 func NewSessionStore(fs storage.FS, aspects aspects.Aspects, root string, async bool, tknopts options.TokenOptions, log *zerolog.Logger) *OcisStore { 75 return &OcisStore{ 76 fs: fs, 77 lu: aspects.Lookup, 78 tp: aspects.Tree, 79 root: root, 80 pub: aspects.EventStream, 81 async: async, 82 tknopts: tknopts, 83 disableVersioning: aspects.DisableVersioning, 84 um: aspects.UserMapper, 85 log: log, 86 } 87 } 88 89 // New returns a new upload session 90 func (store OcisStore) New(ctx context.Context) *OcisSession { 91 return &OcisSession{ 92 store: store, 93 info: tusd.FileInfo{ 94 ID: uuid.New().String(), 95 Storage: map[string]string{ 96 "Type": "OCISStore", 97 }, 98 MetaData: tusd.MetaData{}, 99 }, 100 } 101 } 102 103 // List lists all upload sessions 104 func (store OcisStore) List(ctx context.Context) ([]*OcisSession, error) { 105 uploads := []*OcisSession{} 106 infoFiles, err := filepath.Glob(filepath.Join(store.root, "uploads", "*.info")) 107 if err != nil { 108 return nil, err 109 } 110 111 for _, info := range infoFiles { 112 id := strings.TrimSuffix(filepath.Base(info), filepath.Ext(info)) 113 progress, err := store.Get(ctx, id) 114 if err != nil { 115 appctx.GetLogger(ctx).Error().Interface("path", info).Msg("Decomposedfs: could not getUploadSession") 116 continue 117 } 118 119 uploads = append(uploads, progress) 120 } 121 return uploads, nil 122 } 123 124 // Get returns the upload session for the given upload id 125 func (store OcisStore) Get(ctx context.Context, id string) (*OcisSession, error) { 126 sessionPath := sessionPath(store.root, id) 127 match := _idRegexp.FindStringSubmatch(sessionPath) 128 if match == nil || len(match) < 2 { 129 return nil, fmt.Errorf("invalid upload path") 130 } 131 132 session := OcisSession{ 133 store: store, 134 info: tusd.FileInfo{}, 135 } 136 data, err := os.ReadFile(sessionPath) 137 if err != nil { 138 // handle stale NFS file handles that can occur when the file is deleted betwenn the ATTR and FOPEN call of os.ReadFile 139 if pathErr, ok := err.(*os.PathError); ok && pathErr.Err == syscall.ESTALE { 140 appctx.GetLogger(ctx).Info().Str("session", id).Err(err).Msg("treating stale file handle as not found") 141 err = tusd.ErrNotFound 142 } 143 if errors.Is(err, iofs.ErrNotExist) { 144 // Interpret os.ErrNotExist as 404 Not Found 145 err = tusd.ErrNotFound 146 } 147 return nil, err 148 } 149 150 if err := json.Unmarshal(data, &session.info); err != nil { 151 return nil, err 152 } 153 154 stat, err := os.Stat(session.binPath()) 155 if err != nil { 156 if os.IsNotExist(err) { 157 // Interpret os.ErrNotExist as 404 Not Found 158 err = tusd.ErrNotFound 159 } 160 return nil, err 161 } 162 163 session.info.Offset = stat.Size() 164 165 return &session, nil 166 } 167 168 // Session is the interface used by the Cleanup call 169 type Session interface { 170 ID() string 171 Node(ctx context.Context) (*node.Node, error) 172 Context(ctx context.Context) context.Context 173 Cleanup(revertNodeMetadata, cleanBin, cleanInfo bool) 174 } 175 176 // Cleanup cleans upload metadata, binary data and processing status as necessary 177 func (store OcisStore) Cleanup(ctx context.Context, session Session, revertNodeMetadata, keepUpload, unmarkPostprocessing bool) { 178 ctx, span := tracer.Start(session.Context(ctx), "Cleanup") 179 defer span.End() 180 session.Cleanup(revertNodeMetadata, !keepUpload, !keepUpload) 181 182 // unset processing status 183 if unmarkPostprocessing { 184 n, err := session.Node(ctx) 185 if err != nil { 186 appctx.GetLogger(ctx).Info().Str("session", session.ID()).Err(err).Msg("could not read node") 187 return 188 } 189 // FIXME: after cleanup the node might already be deleted ... 190 if n != nil { // node can be nil when there was an error before it was created (eg. checksum-mismatch) 191 if err := n.UnmarkProcessing(ctx, session.ID()); err != nil { 192 appctx.GetLogger(ctx).Info().Str("path", n.InternalPath()).Err(err).Msg("unmarking processing failed") 193 } 194 } 195 } 196 } 197 198 // CreateNodeForUpload will create the target node for the Upload 199 // TODO move this to the node package as NodeFromUpload? 200 // should we in InitiateUpload create the node first? and then the upload? 201 func (store OcisStore) CreateNodeForUpload(ctx context.Context, session *OcisSession, initAttrs node.Attributes) (*node.Node, error) { 202 ctx, span := tracer.Start(session.Context(ctx), "CreateNodeForUpload") 203 defer span.End() 204 n := node.New( 205 session.SpaceID(), 206 session.NodeID(), 207 session.NodeParentID(), 208 session.Filename(), 209 session.Size(), 210 session.ID(), 211 provider.ResourceType_RESOURCE_TYPE_FILE, 212 nil, 213 store.lu, 214 ) 215 var err error 216 n.SpaceRoot, err = node.ReadNode(ctx, store.lu, session.SpaceID(), session.SpaceID(), false, nil, false) 217 if err != nil { 218 return nil, err 219 } 220 221 // check lock 222 if err := n.CheckLock(ctx); err != nil { 223 return nil, err 224 } 225 226 var unlock metadata.UnlockFunc 227 if session.NodeExists() { // TODO this is wrong. The node should be created when the upload starts, the revisions should be created independently of the node 228 // we do not need to propagate a change when a node is created, only when the upload is ready. 229 // that still creates problems for desktop clients because if another change causes propagation it will detects an empty file 230 // so the first upload has to point to the first revision with the expected size. The file cannot be downloaded, but it can be overwritten (which will create a new revision and make the node reflect the latest revision) 231 // any finished postprocessing will not affect the node metadata. 232 // *thinking* but then initializing an upload will lock the file until the upload has finished. That sucks. 233 // so we have to check if the node has been created meanwhile (well, only in case the upload does not know the nodeid ... or the NodeExists array that is checked by session.NodeExists()) 234 // FIXME look at the disk again to see if the file has been created in between, or just try initializing a new node and do the update existing node as a fallback. <- the latter! 235 236 unlock, err = store.updateExistingNode(ctx, session, n, session.SpaceID(), uint64(session.Size())) 237 if err != nil { 238 appctx.GetLogger(ctx).Error().Err(err).Msg("failed to update existing node") 239 } 240 } else { 241 if c, ok := store.lu.(node.IDCacher); ok { 242 err := c.CacheID(ctx, n.SpaceID, n.ID, filepath.Join(n.ParentPath(), n.Name)) 243 if err != nil { 244 appctx.GetLogger(ctx).Error().Err(err).Msg("failed to cache id") 245 } 246 } 247 248 unlock, err = store.tp.InitNewNode(ctx, n, uint64(session.Size())) 249 if err != nil { 250 appctx.GetLogger(ctx).Error().Str("path", n.InternalPath()).Err(err).Msg("failed to init new node") 251 } 252 session.info.MetaData["sizeDiff"] = strconv.FormatInt(session.Size(), 10) 253 } 254 defer func() { 255 if unlock == nil { 256 appctx.GetLogger(ctx).Info().Msg("did not get a unlockfunc, not unlocking") 257 return 258 } 259 260 if err := unlock(); err != nil { 261 appctx.GetLogger(ctx).Error().Err(err).Str("nodeid", n.ID).Str("parentid", n.ParentID).Msg("could not close lock") 262 } 263 }() 264 if err != nil { 265 return nil, err 266 } 267 268 // overwrite technical information 269 initAttrs.SetString(prefixes.IDAttr, n.ID) 270 initAttrs.SetInt64(prefixes.TypeAttr, int64(provider.ResourceType_RESOURCE_TYPE_FILE)) 271 initAttrs.SetString(prefixes.ParentidAttr, n.ParentID) 272 initAttrs.SetString(prefixes.NameAttr, n.Name) 273 initAttrs.SetString(prefixes.BlobIDAttr, n.BlobID) 274 initAttrs.SetInt64(prefixes.BlobsizeAttr, n.Blobsize) 275 initAttrs.SetString(prefixes.StatusPrefix, node.ProcessingStatus+session.ID()) 276 277 // set mtime on the new node 278 mtime := time.Now() 279 if !session.MTime().IsZero() { 280 // overwrite mtime if requested 281 mtime = session.MTime() 282 } 283 err = store.lu.TimeManager().OverrideMtime(ctx, n, &initAttrs, mtime) 284 if err != nil { 285 return nil, errors.Wrap(err, "Decomposedfs: failed to set the mtime") 286 } 287 288 // update node metadata with new blobid etc 289 err = n.SetXattrsWithContext(ctx, initAttrs, false) 290 if err != nil { 291 return nil, errors.Wrap(err, "Decomposedfs: could not write metadata") 292 } 293 294 err = store.um.RunInBaseScope(func() error { 295 return session.Persist(ctx) 296 }) 297 if err != nil { 298 return nil, err 299 } 300 301 return n, nil 302 } 303 304 func (store OcisStore) updateExistingNode(ctx context.Context, session *OcisSession, n *node.Node, spaceID string, fsize uint64) (metadata.UnlockFunc, error) { 305 _, span := tracer.Start(ctx, "updateExistingNode") 306 defer span.End() 307 targetPath := n.InternalPath() 308 309 // write lock existing node before reading any metadata 310 f, err := lockedfile.OpenFile(store.lu.MetadataBackend().LockfilePath(targetPath), os.O_RDWR|os.O_CREATE, 0600) 311 if err != nil { 312 return nil, err 313 } 314 315 unlock := func() error { 316 // NOTE: to prevent stale NFS file handles do not remove lock file! 317 return f.Close() 318 } 319 320 old, _ := node.ReadNode(ctx, store.lu, spaceID, n.ID, false, nil, false) 321 if _, err := node.CheckQuota(ctx, n.SpaceRoot, true, uint64(old.Blobsize), fsize); err != nil { 322 return unlock, err 323 } 324 325 oldNodeMtime, err := old.GetMTime(ctx) 326 if err != nil { 327 return unlock, err 328 } 329 oldNodeEtag, err := node.CalculateEtag(old.ID, oldNodeMtime) 330 if err != nil { 331 return unlock, err 332 } 333 334 // When the if-match header was set we need to check if the 335 // etag still matches before finishing the upload. 336 if session.HeaderIfMatch() != "" && session.HeaderIfMatch() != oldNodeEtag { 337 return unlock, errtypes.Aborted("etag mismatch") 338 } 339 340 // When the if-none-match header was set we need to check if any of the 341 // etags matches before finishing the upload. 342 if session.HeaderIfNoneMatch() != "" { 343 if session.HeaderIfNoneMatch() == "*" { 344 return unlock, errtypes.Aborted("etag mismatch, resource exists") 345 } 346 for _, ifNoneMatchTag := range strings.Split(session.HeaderIfNoneMatch(), ",") { 347 if ifNoneMatchTag == oldNodeEtag { 348 return unlock, errtypes.Aborted("etag mismatch") 349 } 350 } 351 } 352 353 // When the if-unmodified-since header was set we need to check if the 354 // etag still matches before finishing the upload. 355 if session.HeaderIfUnmodifiedSince() != "" { 356 ifUnmodifiedSince, err := time.Parse(time.RFC3339Nano, session.HeaderIfUnmodifiedSince()) 357 if err != nil { 358 return unlock, errtypes.InternalError(fmt.Sprintf("failed to parse if-unmodified-since time: %s", err)) 359 } 360 361 if oldNodeMtime.After(ifUnmodifiedSince) { 362 return unlock, errtypes.Aborted("if-unmodified-since mismatch") 363 } 364 } 365 366 if !store.disableVersioning { 367 versionPath := session.store.lu.InternalPath(spaceID, n.ID+node.RevisionIDDelimiter+oldNodeMtime.UTC().Format(time.RFC3339Nano)) 368 369 // create version node 370 _, err := os.OpenFile(versionPath, os.O_CREATE|os.O_EXCL, 0600) 371 if err != nil { 372 if !errors.Is(err, os.ErrExist) { 373 return unlock, err 374 } 375 376 // a revision with this mtime does already exist. 377 // If the blobs are the same we can just delete the old one 378 if err := validateChecksums(ctx, old, session, versionPath); err != nil { 379 return unlock, err 380 } 381 382 // delete old blob 383 bID, _, err := session.store.lu.ReadBlobIDAndSizeAttr(ctx, versionPath, nil) 384 if err != nil { 385 return unlock, err 386 } 387 if err := session.store.tp.DeleteBlob(&node.Node{BlobID: bID, SpaceID: n.SpaceID}); err != nil { 388 return unlock, err 389 } 390 391 // clean revision file 392 span.AddEvent("os.Create") 393 if _, err := os.Create(versionPath); err != nil { 394 return unlock, err 395 } 396 } 397 398 // copy blob metadata to version node 399 if err := store.lu.CopyMetadataWithSourceLock(ctx, targetPath, versionPath, func(attributeName string, value []byte) (newValue []byte, copy bool) { 400 return value, strings.HasPrefix(attributeName, prefixes.ChecksumPrefix) || 401 attributeName == prefixes.TypeAttr || 402 attributeName == prefixes.BlobIDAttr || 403 attributeName == prefixes.BlobsizeAttr || 404 attributeName == prefixes.MTimeAttr 405 }, f, true); err != nil { 406 return unlock, err 407 } 408 session.info.MetaData["versionsPath"] = versionPath 409 // keep mtime from previous version 410 span.AddEvent("os.Chtimes") 411 if err := os.Chtimes(session.info.MetaData["versionsPath"], oldNodeMtime, oldNodeMtime); err != nil { 412 return unlock, errtypes.InternalError(fmt.Sprintf("failed to change mtime of version node: %s", err)) 413 } 414 } 415 416 session.info.MetaData["sizeDiff"] = strconv.FormatInt((int64(fsize) - old.Blobsize), 10) 417 418 return unlock, nil 419 } 420 421 func validateChecksums(ctx context.Context, n *node.Node, session *OcisSession, versionPath string) error { 422 for _, t := range []string{"md5", "sha1", "adler32"} { 423 key := prefixes.ChecksumPrefix + t 424 425 checksum, err := n.Xattr(ctx, key) 426 if err != nil { 427 return err 428 } 429 430 revisionChecksum, err := session.store.lu.MetadataBackend().Get(ctx, versionPath, key) 431 if err != nil { 432 return err 433 } 434 435 if string(checksum) == "" || string(revisionChecksum) == "" { 436 return errors.New("checksum not found") 437 } 438 439 if string(checksum) != string(revisionChecksum) { 440 return errors.New("checksum mismatch") 441 } 442 } 443 444 return nil 445 }