github.com/cs3org/reva/v2@v2.27.7/pkg/storage/fs/posix/tree/assimilation.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 tree
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"io/fs"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  	"sync"
    29  	"syscall"
    30  	"time"
    31  
    32  	"github.com/google/uuid"
    33  	"github.com/pkg/errors"
    34  	"github.com/rs/zerolog/log"
    35  
    36  	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    37  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    38  	"github.com/cs3org/reva/v2/pkg/events"
    39  	"github.com/cs3org/reva/v2/pkg/storage/fs/posix/lookup"
    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/utils"
    44  )
    45  
    46  type ScanDebouncer struct {
    47  	after      time.Duration
    48  	f          func(item scanItem)
    49  	pending    sync.Map
    50  	inProgress sync.Map
    51  
    52  	mutex sync.Mutex
    53  }
    54  
    55  type EventAction int
    56  
    57  const (
    58  	ActionCreate EventAction = iota
    59  	ActionUpdate
    60  	ActionMove
    61  	ActionDelete
    62  	ActionMoveFrom
    63  )
    64  
    65  type queueItem struct {
    66  	item  scanItem
    67  	timer *time.Timer
    68  }
    69  
    70  const dirtyFlag = "user.ocis.dirty"
    71  
    72  // NewScanDebouncer returns a new SpaceDebouncer instance
    73  func NewScanDebouncer(d time.Duration, f func(item scanItem)) *ScanDebouncer {
    74  	return &ScanDebouncer{
    75  		after:      d,
    76  		f:          f,
    77  		pending:    sync.Map{},
    78  		inProgress: sync.Map{},
    79  	}
    80  }
    81  
    82  // Debounce restarts the debounce timer for the given space
    83  func (d *ScanDebouncer) Debounce(item scanItem) {
    84  	if d.after == 0 {
    85  		d.f(item)
    86  		return
    87  	}
    88  
    89  	d.mutex.Lock()
    90  	defer d.mutex.Unlock()
    91  
    92  	path := item.Path
    93  	force := item.ForceRescan
    94  	recurse := item.Recurse
    95  	if i, ok := d.pending.Load(item.Path); ok {
    96  		queueItem := i.(*queueItem)
    97  		force = force || queueItem.item.ForceRescan
    98  		recurse = recurse || queueItem.item.Recurse
    99  		queueItem.timer.Stop()
   100  	}
   101  
   102  	d.pending.Store(item.Path, &queueItem{
   103  		item: item,
   104  		timer: time.AfterFunc(d.after, func() {
   105  			if _, ok := d.inProgress.Load(path); ok {
   106  				// Reschedule this run for when the previous run has finished
   107  				d.mutex.Lock()
   108  				if i, ok := d.pending.Load(path); ok {
   109  					i.(*queueItem).timer.Reset(d.after)
   110  				}
   111  
   112  				d.mutex.Unlock()
   113  				return
   114  			}
   115  
   116  			d.pending.Delete(path)
   117  			d.inProgress.Store(path, true)
   118  			defer d.inProgress.Delete(path)
   119  			d.f(scanItem{
   120  				Path:        path,
   121  				ForceRescan: force,
   122  				Recurse:     recurse,
   123  			})
   124  		}),
   125  	})
   126  }
   127  
   128  // InProgress returns true if the given path is currently being processed
   129  func (d *ScanDebouncer) InProgress(path string) bool {
   130  	d.mutex.Lock()
   131  	defer d.mutex.Unlock()
   132  	if _, ok := d.pending.Load(path); ok {
   133  		return true
   134  	}
   135  
   136  	_, ok := d.inProgress.Load(path)
   137  	return ok
   138  }
   139  
   140  func (t *Tree) workScanQueue() {
   141  	for i := 0; i < t.options.MaxConcurrency; i++ {
   142  		go func() {
   143  			for {
   144  				item := <-t.scanQueue
   145  
   146  				err := t.assimilate(item)
   147  				if err != nil {
   148  					log.Error().Err(err).Str("path", item.Path).Msg("failed to assimilate item")
   149  					continue
   150  				}
   151  
   152  				if item.Recurse {
   153  					err = t.WarmupIDCache(item.Path, true, false)
   154  					if err != nil {
   155  						log.Error().Err(err).Str("path", item.Path).Msg("failed to warmup id cache")
   156  					}
   157  				}
   158  			}
   159  		}()
   160  	}
   161  }
   162  
   163  // Scan scans the given path and updates the id chache
   164  func (t *Tree) Scan(path string, action EventAction, isDir bool) error {
   165  	// cases:
   166  	switch action {
   167  	case ActionCreate:
   168  		t.log.Debug().Str("path", path).Bool("isDir", isDir).Msg("scanning path (ActionCreate)")
   169  		if !isDir {
   170  			// 1. New file (could be emitted as part of a new directory)
   171  			//	 -> assimilate file
   172  			//   -> scan parent directory recursively to update tree size and catch nodes that weren't covered by an event
   173  			if !t.scanDebouncer.InProgress(filepath.Dir(path)) {
   174  				t.scanDebouncer.Debounce(scanItem{
   175  					Path:        path,
   176  					ForceRescan: false,
   177  				})
   178  			}
   179  			if err := t.setDirty(filepath.Dir(path), true); err != nil {
   180  				return err
   181  			}
   182  			t.scanDebouncer.Debounce(scanItem{
   183  				Path:        filepath.Dir(path),
   184  				ForceRescan: true,
   185  				Recurse:     true,
   186  			})
   187  		} else {
   188  			// 2. New directory
   189  			//  -> scan directory
   190  			if err := t.setDirty(path, true); err != nil {
   191  				return err
   192  			}
   193  			t.scanDebouncer.Debounce(scanItem{
   194  				Path:        path,
   195  				ForceRescan: true,
   196  				Recurse:     true,
   197  			})
   198  		}
   199  
   200  	case ActionUpdate:
   201  		t.log.Debug().Str("path", path).Bool("isDir", isDir).Msg("scanning path (ActionUpdate)")
   202  		// 3. Updated file
   203  		//   -> update file unless parent directory is being rescanned
   204  		if !t.scanDebouncer.InProgress(filepath.Dir(path)) {
   205  			t.scanDebouncer.Debounce(scanItem{
   206  				Path:        path,
   207  				ForceRescan: true,
   208  			})
   209  		}
   210  
   211  	case ActionMove:
   212  		t.log.Debug().Str("path", path).Bool("isDir", isDir).Msg("scanning path (ActionMove)")
   213  		// 4. Moved file
   214  		//   -> update file
   215  		// 5. Moved directory
   216  		//   -> update directory and all children
   217  		t.scanDebouncer.Debounce(scanItem{
   218  			Path:        path,
   219  			ForceRescan: isDir,
   220  			Recurse:     isDir,
   221  		})
   222  
   223  	case ActionMoveFrom:
   224  		t.log.Debug().Str("path", path).Bool("isDir", isDir).Msg("scanning path (ActionMoveFrom)")
   225  		// 6. file/directory moved out of the watched directory
   226  		//   -> update directory
   227  		if err := t.setDirty(filepath.Dir(path), true); err != nil {
   228  			return err
   229  		}
   230  
   231  		go func() { _ = t.WarmupIDCache(filepath.Dir(path), false, true) }()
   232  
   233  	case ActionDelete:
   234  		t.log.Debug().Str("path", path).Bool("isDir", isDir).Msg("handling deleted item")
   235  
   236  		// 7. Deleted file or directory
   237  		//   -> update parent and all children
   238  
   239  		err := t.HandleFileDelete(path)
   240  		if err != nil {
   241  			return err
   242  		}
   243  
   244  		t.scanDebouncer.Debounce(scanItem{
   245  			Path:        filepath.Dir(path),
   246  			ForceRescan: true,
   247  			Recurse:     true,
   248  		})
   249  	}
   250  
   251  	return nil
   252  }
   253  
   254  func (t *Tree) HandleFileDelete(path string) error {
   255  	// purge metadata
   256  	if err := t.lookup.(*lookup.Lookup).IDCache.DeleteByPath(context.Background(), path); err != nil {
   257  		t.log.Error().Err(err).Str("path", path).Msg("could not delete id cache entry by path")
   258  	}
   259  	if err := t.lookup.MetadataBackend().Purge(context.Background(), path); err != nil {
   260  		t.log.Error().Err(err).Str("path", path).Msg("could not purge metadata")
   261  	}
   262  
   263  	// send event
   264  	owner, spaceID, nodeID, parentID, err := t.getOwnerAndIDs(filepath.Dir(path))
   265  	if err != nil {
   266  		return err
   267  	}
   268  
   269  	t.PublishEvent(events.ItemTrashed{
   270  		Owner:     owner,
   271  		Executant: owner,
   272  		Ref: &provider.Reference{
   273  			ResourceId: &provider.ResourceId{
   274  				StorageId: t.options.MountID,
   275  				SpaceId:   spaceID,
   276  				OpaqueId:  parentID,
   277  			},
   278  			Path: filepath.Base(path),
   279  		},
   280  		ID: &provider.ResourceId{
   281  			StorageId: t.options.MountID,
   282  			SpaceId:   spaceID,
   283  			OpaqueId:  nodeID,
   284  		},
   285  		Timestamp: utils.TSNow(),
   286  	})
   287  
   288  	return nil
   289  }
   290  
   291  func (t *Tree) getOwnerAndIDs(path string) (*userv1beta1.UserId, string, string, string, error) {
   292  	lu := t.lookup.(*lookup.Lookup)
   293  
   294  	spaceID, nodeID, err := lu.IDsForPath(context.Background(), path)
   295  	if err != nil {
   296  		return nil, "", "", "", err
   297  	}
   298  
   299  	attrs, err := t.lookup.MetadataBackend().All(context.Background(), path)
   300  	if err != nil {
   301  		return nil, "", "", "", err
   302  	}
   303  
   304  	parentID := string(attrs[prefixes.ParentidAttr])
   305  
   306  	spacePath, ok := lu.GetCachedID(context.Background(), spaceID, spaceID)
   307  	if !ok {
   308  		return nil, "", "", "", fmt.Errorf("could not find space root for path %s", path)
   309  	}
   310  
   311  	spaceAttrs, err := t.lookup.MetadataBackend().All(context.Background(), spacePath)
   312  	if err != nil {
   313  		return nil, "", "", "", err
   314  	}
   315  
   316  	owner := &userv1beta1.UserId{
   317  		Idp:      string(spaceAttrs[prefixes.OwnerIDPAttr]),
   318  		OpaqueId: string(spaceAttrs[prefixes.OwnerIDAttr]),
   319  	}
   320  
   321  	return owner, nodeID, spaceID, parentID, nil
   322  }
   323  
   324  func (t *Tree) findSpaceId(path string) (string, node.Attributes, error) {
   325  	// find the space id, scope by the according user
   326  	spaceCandidate := path
   327  	spaceAttrs := node.Attributes{}
   328  	for strings.HasPrefix(spaceCandidate, t.options.Root) {
   329  		spaceAttrs, err := t.lookup.MetadataBackend().All(context.Background(), spaceCandidate)
   330  		spaceID := spaceAttrs[prefixes.SpaceIDAttr]
   331  		if err == nil && len(spaceID) > 0 {
   332  			if t.options.UseSpaceGroups {
   333  				// set the uid and gid for the space
   334  				fi, err := os.Stat(spaceCandidate)
   335  				if err != nil {
   336  					return "", spaceAttrs, err
   337  				}
   338  				sys := fi.Sys().(*syscall.Stat_t)
   339  				gid := int(sys.Gid)
   340  				_, err = t.userMapper.ScopeUserByIds(-1, gid)
   341  				if err != nil {
   342  					return "", spaceAttrs, err
   343  				}
   344  			}
   345  
   346  			return string(spaceID), spaceAttrs, nil
   347  		}
   348  		spaceCandidate = filepath.Dir(spaceCandidate)
   349  	}
   350  	return "", spaceAttrs, fmt.Errorf("could not find space for path %s", path)
   351  }
   352  
   353  func (t *Tree) assimilate(item scanItem) error {
   354  	var id []byte
   355  	var err error
   356  
   357  	// First find the space id
   358  	spaceID, spaceAttrs, err := t.findSpaceId(item.Path)
   359  	if err != nil {
   360  		return err
   361  	}
   362  
   363  	// lock the file for assimilation
   364  	unlock, err := t.lookup.MetadataBackend().Lock(item.Path)
   365  	if err != nil {
   366  		return errors.Wrap(err, "failed to lock item for assimilation")
   367  	}
   368  	defer func() {
   369  		_ = unlock()
   370  	}()
   371  
   372  	user := &userv1beta1.UserId{
   373  		Idp:      string(spaceAttrs[prefixes.OwnerIDPAttr]),
   374  		OpaqueId: string(spaceAttrs[prefixes.OwnerIDAttr]),
   375  	}
   376  
   377  	// check for the id attribute again after grabbing the lock, maybe the file was assimilated/created by us in the meantime
   378  	id, err = t.lookup.MetadataBackend().Get(context.Background(), item.Path, prefixes.IDAttr)
   379  	if err == nil {
   380  		previousPath, ok := t.lookup.(*lookup.Lookup).GetCachedID(context.Background(), spaceID, string(id))
   381  		previousParentID, _ := t.lookup.MetadataBackend().Get(context.Background(), item.Path, prefixes.ParentidAttr)
   382  
   383  		// was it moved or copied/restored with a clashing id?
   384  		if ok && len(previousParentID) > 0 && previousPath != item.Path {
   385  			_, err := os.Stat(previousPath)
   386  			if err == nil {
   387  				// this id clashes with an existing item -> clear metadata and re-assimilate
   388  				t.log.Debug().Str("path", item.Path).Msg("ID clash detected, purging metadata and re-assimilating")
   389  
   390  				if err := t.lookup.MetadataBackend().Purge(context.Background(), item.Path); err != nil {
   391  					t.log.Error().Err(err).Str("path", item.Path).Msg("could not purge metadata")
   392  				}
   393  				go func() {
   394  					if err := t.assimilate(scanItem{Path: item.Path, ForceRescan: true}); err != nil {
   395  						t.log.Error().Err(err).Str("path", item.Path).Msg("could not re-assimilate")
   396  					}
   397  				}()
   398  			} else {
   399  				// this is a move
   400  				t.log.Debug().Str("path", item.Path).Msg("move detected")
   401  
   402  				if err := t.lookup.(*lookup.Lookup).CacheID(context.Background(), spaceID, string(id), item.Path); err != nil {
   403  					t.log.Error().Err(err).Str("spaceID", spaceID).Str("id", string(id)).Str("path", item.Path).Msg("could not cache id")
   404  				}
   405  				_, err := t.updateFile(item.Path, string(id), spaceID)
   406  				if err != nil {
   407  					return err
   408  				}
   409  
   410  				// purge original metadata. Only delete the path entry using DeletePath(reverse lookup), not the whole entry pair.
   411  				if err := t.lookup.(*lookup.Lookup).IDCache.DeletePath(context.Background(), previousPath); err != nil {
   412  					t.log.Error().Err(err).Str("path", previousPath).Msg("could not delete id cache entry by path")
   413  				}
   414  				if err := t.lookup.MetadataBackend().Purge(context.Background(), previousPath); err != nil {
   415  					t.log.Error().Err(err).Str("path", previousPath).Msg("could not purge metadata")
   416  				}
   417  
   418  				fi, err := os.Stat(item.Path)
   419  				if err != nil {
   420  					return err
   421  				}
   422  				if fi.IsDir() {
   423  					// if it was moved and it is a directory we need to propagate the move
   424  					go func() {
   425  						if err := t.WarmupIDCache(item.Path, false, true); err != nil {
   426  							t.log.Error().Err(err).Str("path", item.Path).Msg("could not warmup id cache")
   427  						}
   428  					}()
   429  				}
   430  
   431  				parentID, err := t.lookup.MetadataBackend().Get(context.Background(), item.Path, prefixes.ParentidAttr)
   432  				if err == nil && len(parentID) > 0 {
   433  					ref := &provider.Reference{
   434  						ResourceId: &provider.ResourceId{
   435  							StorageId: t.options.MountID,
   436  							SpaceId:   spaceID,
   437  							OpaqueId:  string(parentID),
   438  						},
   439  						Path: filepath.Base(item.Path),
   440  					}
   441  					oldRef := &provider.Reference{
   442  						ResourceId: &provider.ResourceId{
   443  							StorageId: t.options.MountID,
   444  							SpaceId:   spaceID,
   445  							OpaqueId:  string(previousParentID),
   446  						},
   447  						Path: filepath.Base(previousPath),
   448  					}
   449  					t.PublishEvent(events.ItemMoved{
   450  						SpaceOwner:   user,
   451  						Executant:    user,
   452  						Owner:        user,
   453  						Ref:          ref,
   454  						OldReference: oldRef,
   455  						Timestamp:    utils.TSNow(),
   456  					})
   457  				}
   458  			}
   459  		} else {
   460  			// This item had already been assimilated in the past. Update the path
   461  			t.log.Debug().Str("path", item.Path).Msg("updating cached path")
   462  			if err := t.lookup.(*lookup.Lookup).CacheID(context.Background(), spaceID, string(id), item.Path); err != nil {
   463  				t.log.Error().Err(err).Str("spaceID", spaceID).Str("id", string(id)).Str("path", item.Path).Msg("could not cache id")
   464  			}
   465  
   466  			_, err := t.updateFile(item.Path, string(id), spaceID)
   467  			if err != nil {
   468  				return err
   469  			}
   470  		}
   471  	} else {
   472  		t.log.Debug().Str("path", item.Path).Msg("new item detected")
   473  		// assimilate new file
   474  		newId := uuid.New().String()
   475  		fi, err := t.updateFile(item.Path, newId, spaceID)
   476  		if err != nil {
   477  			return err
   478  		}
   479  
   480  		ref := &provider.Reference{
   481  			ResourceId: &provider.ResourceId{
   482  				StorageId: t.options.MountID,
   483  				SpaceId:   spaceID,
   484  				OpaqueId:  newId,
   485  			},
   486  		}
   487  		if fi.IsDir() {
   488  			t.PublishEvent(events.ContainerCreated{
   489  				SpaceOwner: user,
   490  				Executant:  user,
   491  				Owner:      user,
   492  				Ref:        ref,
   493  				Timestamp:  utils.TSNow(),
   494  			})
   495  		} else {
   496  			if fi.Size() == 0 {
   497  				t.PublishEvent(events.FileTouched{
   498  					SpaceOwner: user,
   499  					Executant:  user,
   500  					Ref:        ref,
   501  					Timestamp:  utils.TSNow(),
   502  				})
   503  			} else {
   504  				t.PublishEvent(events.UploadReady{
   505  					SpaceOwner: user,
   506  					FileRef:    ref,
   507  					Timestamp:  utils.TSNow(),
   508  				})
   509  			}
   510  		}
   511  	}
   512  	return nil
   513  }
   514  
   515  func (t *Tree) updateFile(path, id, spaceID string) (fs.FileInfo, error) {
   516  	retries := 1
   517  	parentID := ""
   518  assimilate:
   519  	if id != spaceID {
   520  		// read parent
   521  		parentAttribs, err := t.lookup.MetadataBackend().All(context.Background(), filepath.Dir(path))
   522  		if err != nil {
   523  			return nil, fmt.Errorf("failed to read parent item attributes")
   524  		}
   525  
   526  		if len(parentAttribs) == 0 || len(parentAttribs[prefixes.IDAttr]) == 0 {
   527  			if retries == 0 {
   528  				return nil, fmt.Errorf("got empty parent attribs even after assimilating")
   529  			}
   530  
   531  			// assimilate parent first
   532  			err = t.assimilate(scanItem{Path: filepath.Dir(path), ForceRescan: false})
   533  			if err != nil {
   534  				return nil, err
   535  			}
   536  
   537  			// retry
   538  			retries--
   539  			goto assimilate
   540  		}
   541  		parentID = string(parentAttribs[prefixes.IDAttr])
   542  	}
   543  
   544  	// assimilate file
   545  	fi, err := os.Stat(path)
   546  	if err != nil {
   547  		return nil, errors.Wrap(err, "failed to stat item")
   548  	}
   549  
   550  	attrs, err := t.lookup.MetadataBackend().All(context.Background(), path)
   551  	if err != nil && !metadata.IsAttrUnset(err) {
   552  		return nil, errors.Wrap(err, "failed to get item attribs")
   553  	}
   554  	previousAttribs := node.Attributes(attrs)
   555  
   556  	attributes := node.Attributes{
   557  		prefixes.IDAttr:   []byte(id),
   558  		prefixes.NameAttr: []byte(filepath.Base(path)),
   559  	}
   560  	if len(parentID) > 0 {
   561  		attributes[prefixes.ParentidAttr] = []byte(parentID)
   562  	}
   563  
   564  	sha1h, md5h, adler32h, err := node.CalculateChecksums(context.Background(), path)
   565  	if err == nil {
   566  		attributes[prefixes.ChecksumPrefix+"sha1"] = sha1h.Sum(nil)
   567  		attributes[prefixes.ChecksumPrefix+"md5"] = md5h.Sum(nil)
   568  		attributes[prefixes.ChecksumPrefix+"adler32"] = adler32h.Sum(nil)
   569  	}
   570  
   571  	if fi.IsDir() {
   572  		attributes.SetInt64(prefixes.TypeAttr, int64(provider.ResourceType_RESOURCE_TYPE_CONTAINER))
   573  		attributes.SetInt64(prefixes.TreesizeAttr, 0)
   574  		if previousAttribs != nil && previousAttribs[prefixes.TreesizeAttr] != nil {
   575  			attributes[prefixes.TreesizeAttr] = previousAttribs[prefixes.TreesizeAttr]
   576  		}
   577  		attributes[prefixes.PropagationAttr] = []byte("1")
   578  	} else {
   579  		attributes.SetInt64(prefixes.TypeAttr, int64(provider.ResourceType_RESOURCE_TYPE_FILE))
   580  	}
   581  
   582  	n := node.New(spaceID, id, parentID, filepath.Base(path), fi.Size(), "", provider.ResourceType_RESOURCE_TYPE_FILE, nil, t.lookup)
   583  	n.SpaceRoot = &node.Node{SpaceID: spaceID, ID: spaceID}
   584  	err = t.Propagate(context.Background(), n, 0)
   585  	if err != nil {
   586  		return nil, errors.Wrap(err, "failed to propagate")
   587  	}
   588  
   589  	t.log.Debug().Str("path", path).Interface("attributes", attributes).Msg("setting attributes")
   590  	err = t.lookup.MetadataBackend().SetMultiple(context.Background(), path, attributes, false)
   591  	if err != nil {
   592  		return nil, errors.Wrap(err, "failed to set attributes")
   593  	}
   594  
   595  	if err := t.lookup.(*lookup.Lookup).CacheID(context.Background(), spaceID, id, path); err != nil {
   596  		t.log.Error().Err(err).Str("spaceID", spaceID).Str("id", id).Str("path", path).Msg("could not cache id")
   597  	}
   598  
   599  	return fi, nil
   600  }
   601  
   602  // WarmupIDCache warms up the id cache
   603  func (t *Tree) WarmupIDCache(root string, assimilate, onlyDirty bool) error {
   604  	root = filepath.Clean(root)
   605  	spaceID := []byte("")
   606  
   607  	scopeSpace := func(spaceCandidate string) error {
   608  		if !t.options.UseSpaceGroups {
   609  			return nil
   610  		}
   611  
   612  		// set the uid and gid for the space
   613  		fi, err := os.Stat(spaceCandidate)
   614  		if err != nil {
   615  			return err
   616  		}
   617  		sys := fi.Sys().(*syscall.Stat_t)
   618  		gid := int(sys.Gid)
   619  		_, err = t.userMapper.ScopeUserByIds(-1, gid)
   620  		return err
   621  	}
   622  
   623  	sizes := make(map[string]int64)
   624  	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
   625  		// skip lock and upload files
   626  		if isLockFile(path) {
   627  			return nil
   628  		}
   629  		if isTrash(path) || t.isUpload(path) {
   630  			return filepath.SkipDir
   631  		}
   632  
   633  		if err != nil {
   634  			return err
   635  		}
   636  
   637  		// calculate tree sizes
   638  		if !info.IsDir() {
   639  			dir := path
   640  			for dir != root {
   641  				dir = filepath.Clean(filepath.Dir(dir))
   642  				sizes[dir] += info.Size()
   643  			}
   644  		} else if onlyDirty {
   645  			dirty, err := t.isDirty(path)
   646  			if err != nil {
   647  				return err
   648  			}
   649  			if !dirty {
   650  				return filepath.SkipDir
   651  			}
   652  			sizes[path] += 0 // Make sure to set the size to 0 for empty directories
   653  		}
   654  
   655  		attribs, err := t.lookup.MetadataBackend().All(context.Background(), path)
   656  		if err == nil && len(attribs[prefixes.IDAttr]) > 0 {
   657  			nodeSpaceID := attribs[prefixes.SpaceIDAttr]
   658  			if len(nodeSpaceID) > 0 {
   659  				spaceID = nodeSpaceID
   660  
   661  				err = scopeSpace(path)
   662  				if err != nil {
   663  					return err
   664  				}
   665  			} else {
   666  				// try to find space
   667  				spaceCandidate := path
   668  				for strings.HasPrefix(spaceCandidate, t.options.Root) {
   669  					spaceID, err = t.lookup.MetadataBackend().Get(context.Background(), spaceCandidate, prefixes.SpaceIDAttr)
   670  					if err == nil {
   671  						err = scopeSpace(path)
   672  						if err != nil {
   673  							return err
   674  						}
   675  						break
   676  					}
   677  					spaceCandidate = filepath.Dir(spaceCandidate)
   678  				}
   679  			}
   680  			if len(spaceID) == 0 {
   681  				return nil // no space found
   682  			}
   683  
   684  			id, ok := attribs[prefixes.IDAttr]
   685  			if ok {
   686  				// Check if the item on the previous still exists. In this case it might have been a copy with extended attributes -> set new ID
   687  				previousPath, ok := t.lookup.(*lookup.Lookup).GetCachedID(context.Background(), string(spaceID), string(id))
   688  				if ok && previousPath != path {
   689  					// this id clashes with an existing id -> clear metadata and re-assimilate
   690  					_, err := os.Stat(previousPath)
   691  					if err == nil {
   692  						_ = t.lookup.MetadataBackend().Purge(context.Background(), path)
   693  						_ = t.assimilate(scanItem{Path: path, ForceRescan: true})
   694  					}
   695  				}
   696  				if err := t.lookup.(*lookup.Lookup).CacheID(context.Background(), string(spaceID), string(id), path); err != nil {
   697  					t.log.Error().Err(err).Str("spaceID", string(spaceID)).Str("id", string(id)).Str("path", path).Msg("could not cache id")
   698  				}
   699  			}
   700  		} else if assimilate {
   701  			if err := t.assimilate(scanItem{Path: path, ForceRescan: true}); err != nil {
   702  				t.log.Error().Err(err).Str("path", path).Msg("could not assimilate item")
   703  			}
   704  		}
   705  		return t.setDirty(path, false)
   706  	})
   707  
   708  	for dir, size := range sizes {
   709  		if dir == root {
   710  			// Propagate the size diff further up the tree
   711  			if err := t.propagateSizeDiff(dir, size); err != nil {
   712  				t.log.Error().Err(err).Str("path", dir).Msg("could not propagate size diff")
   713  			}
   714  		}
   715  		if err := t.lookup.MetadataBackend().Set(context.Background(), dir, prefixes.TreesizeAttr, []byte(fmt.Sprintf("%d", size))); err != nil {
   716  			t.log.Error().Err(err).Str("path", dir).Int64("size", size).Msg("could not set tree size")
   717  		}
   718  	}
   719  
   720  	if err != nil {
   721  		return err
   722  	}
   723  
   724  	return nil
   725  }
   726  
   727  func (t *Tree) propagateSizeDiff(dir string, size int64) error {
   728  	// First find the space id
   729  	spaceID, _, err := t.findSpaceId(dir)
   730  	if err != nil {
   731  		return err
   732  	}
   733  	attrs, err := t.lookup.MetadataBackend().All(context.Background(), dir)
   734  	if err != nil {
   735  		return err
   736  	}
   737  	n, err := t.lookup.NodeFromID(context.Background(), &provider.ResourceId{
   738  		StorageId: t.options.MountID,
   739  		SpaceId:   spaceID,
   740  		OpaqueId:  string(attrs[prefixes.IDAttr]),
   741  	})
   742  	if err != nil {
   743  		return err
   744  	}
   745  	oldSize, err := node.Attributes(attrs).Int64(prefixes.TreesizeAttr)
   746  	if err != nil {
   747  		return err
   748  	}
   749  	return t.Propagate(context.Background(), n, size-oldSize)
   750  }
   751  
   752  func (t *Tree) setDirty(path string, dirty bool) error {
   753  	return t.lookup.MetadataBackend().Set(context.Background(), path, dirtyFlag, []byte(fmt.Sprintf("%t", dirty)))
   754  }
   755  
   756  func (t *Tree) isDirty(path string) (bool, error) {
   757  	dirtyAttr, err := t.lookup.MetadataBackend().Get(context.Background(), path, dirtyFlag)
   758  	if err != nil {
   759  		return false, err
   760  	}
   761  	return string(dirtyAttr) == "true", nil
   762  }