github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/decomposedfs/recycle.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 iofs "io/fs" 24 "os" 25 "path/filepath" 26 "strings" 27 "time" 28 29 "github.com/pkg/errors" 30 "golang.org/x/sync/errgroup" 31 32 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 33 types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" 34 "github.com/cs3org/reva/v2/pkg/appctx" 35 "github.com/cs3org/reva/v2/pkg/errtypes" 36 "github.com/cs3org/reva/v2/pkg/storage" 37 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/lookup" 38 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes" 39 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node" 40 "github.com/cs3org/reva/v2/pkg/storagespace" 41 ) 42 43 type DecomposedfsTrashbin struct { 44 fs *Decomposedfs 45 } 46 47 // Setup the trashbin 48 func (tb *DecomposedfsTrashbin) Setup(fs storage.FS) error { 49 if _, ok := fs.(*Decomposedfs); !ok { 50 return errors.New("invalid filesystem") 51 } 52 tb.fs = fs.(*Decomposedfs) 53 return nil 54 } 55 56 // Recycle items are stored inside the node folder and start with the uuid of the deleted node. 57 // The `.T.` indicates it is a trash item and what follows is the timestamp of the deletion. 58 // The deleted file is kept in the same location/dir as the original node. This prevents deletes 59 // from triggering cross storage moves when the trash is accidentally stored on another partition, 60 // because the admin mounted a different partition there. 61 // For an efficient listing of deleted nodes the ocis storage driver maintains a 'trash' folder 62 // with symlinks to trash files for every storagespace. 63 64 // ListRecycle returns the list of available recycle items 65 // ref -> the space (= resourceid), key -> deleted node id, relativePath = relative to key 66 func (tb *DecomposedfsTrashbin) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) { 67 _, span := tracer.Start(ctx, "ListRecycle") 68 defer span.End() 69 if ref == nil || ref.ResourceId == nil || ref.ResourceId.OpaqueId == "" { 70 return nil, errtypes.BadRequest("spaceid required") 71 } 72 if key == "" && relativePath != "" { 73 return nil, errtypes.BadRequest("key is required when navigating with a path") 74 } 75 spaceID := ref.ResourceId.OpaqueId 76 77 sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("key", key).Str("relative_path", relativePath).Logger() 78 79 // check permissions 80 trashnode, err := tb.fs.lu.NodeFromSpaceID(ctx, spaceID) 81 if err != nil { 82 return nil, err 83 } 84 rp, err := tb.fs.p.AssembleTrashPermissions(ctx, trashnode) 85 switch { 86 case err != nil: 87 return nil, err 88 case !rp.ListRecycle: 89 if rp.Stat { 90 return nil, errtypes.PermissionDenied(key) 91 } 92 return nil, errtypes.NotFound(key) 93 } 94 95 if key == "" && relativePath == "" { 96 return tb.listTrashRoot(ctx, spaceID) 97 } 98 99 // build a list of trash items relative to the given trash root and path 100 items := make([]*provider.RecycleItem, 0) 101 102 trashRootPath := filepath.Join(tb.getRecycleRoot(spaceID), lookup.Pathify(key, 4, 2)) 103 originalPath, _, timeSuffix, err := readTrashLink(trashRootPath) 104 if err != nil { 105 sublog.Error().Err(err).Str("trashRoot", trashRootPath).Msg("error reading trash link") 106 return nil, err 107 } 108 109 origin := "" 110 attrs, err := tb.fs.lu.MetadataBackend().All(ctx, originalPath) 111 if err != nil { 112 return items, err 113 } 114 // lookup origin path in extended attributes 115 if attrBytes, ok := attrs[prefixes.TrashOriginAttr]; ok { 116 origin = string(attrBytes) 117 } else { 118 sublog.Error().Err(err).Str("spaceid", spaceID).Msg("could not read origin path, skipping") 119 return nil, err 120 } 121 122 // all deleted items have the same deletion time 123 var deletionTime *types.Timestamp 124 if parsed, err := time.Parse(time.RFC3339Nano, timeSuffix); err == nil { 125 deletionTime = &types.Timestamp{ 126 Seconds: uint64(parsed.Unix()), 127 // TODO nanos 128 } 129 } else { 130 sublog.Error().Err(err).Msg("could not parse time format, ignoring") 131 } 132 133 var size int64 134 if relativePath == "" { 135 // this is the case when we want to directly list a file in the trashbin 136 nodeType := tb.fs.lu.TypeFromPath(ctx, originalPath) 137 switch nodeType { 138 case provider.ResourceType_RESOURCE_TYPE_FILE: 139 _, size, err = tb.fs.lu.ReadBlobIDAndSizeAttr(ctx, originalPath, nil) 140 if err != nil { 141 return items, err 142 } 143 case provider.ResourceType_RESOURCE_TYPE_CONTAINER: 144 size, err = tb.fs.lu.MetadataBackend().GetInt64(ctx, originalPath, prefixes.TreesizeAttr) 145 if err != nil { 146 return items, err 147 } 148 } 149 item := &provider.RecycleItem{ 150 Type: tb.fs.lu.TypeFromPath(ctx, originalPath), 151 Size: uint64(size), 152 Key: filepath.Join(key, relativePath), 153 DeletionTime: deletionTime, 154 Ref: &provider.Reference{ 155 Path: filepath.Join(origin, relativePath), 156 }, 157 } 158 items = append(items, item) 159 return items, err 160 } 161 162 // we have to read the names and stat the path to follow the symlinks 163 childrenPath := filepath.Join(originalPath, relativePath) 164 childrenDir, err := os.Open(childrenPath) 165 if err != nil { 166 return nil, err 167 } 168 169 names, err := childrenDir.Readdirnames(0) 170 if err != nil { 171 return nil, err 172 } 173 for _, name := range names { 174 resolvedChildPath, err := filepath.EvalSymlinks(filepath.Join(childrenPath, name)) 175 if err != nil { 176 sublog.Error().Err(err).Str("name", name).Msg("could not resolve symlink, skipping") 177 continue 178 } 179 180 // reset size 181 size = 0 182 183 nodeType := tb.fs.lu.TypeFromPath(ctx, resolvedChildPath) 184 switch nodeType { 185 case provider.ResourceType_RESOURCE_TYPE_FILE: 186 _, size, err = tb.fs.lu.ReadBlobIDAndSizeAttr(ctx, resolvedChildPath, nil) 187 if err != nil { 188 sublog.Error().Err(err).Str("name", name).Msg("invalid blob size, skipping") 189 continue 190 } 191 case provider.ResourceType_RESOURCE_TYPE_CONTAINER: 192 size, err = tb.fs.lu.MetadataBackend().GetInt64(ctx, resolvedChildPath, prefixes.TreesizeAttr) 193 if err != nil { 194 sublog.Error().Err(err).Str("name", name).Msg("invalid tree size, skipping") 195 continue 196 } 197 case provider.ResourceType_RESOURCE_TYPE_INVALID: 198 sublog.Error().Err(err).Str("name", name).Str("resolvedChildPath", resolvedChildPath).Msg("invalid node type, skipping") 199 continue 200 } 201 202 item := &provider.RecycleItem{ 203 Type: nodeType, 204 Size: uint64(size), 205 Key: filepath.Join(key, relativePath, name), 206 DeletionTime: deletionTime, 207 Ref: &provider.Reference{ 208 Path: filepath.Join(origin, relativePath, name), 209 }, 210 } 211 items = append(items, item) 212 } 213 return items, nil 214 } 215 216 // readTrashLink returns path, nodeID and timestamp 217 func readTrashLink(path string) (string, string, string, error) { 218 link, err := os.Readlink(path) 219 if err != nil { 220 return "", "", "", err 221 } 222 resolved, err := filepath.EvalSymlinks(path) 223 if err != nil { 224 return "", "", "", err 225 } 226 // ../../../../../nodes/e5/6c/75/a8/-d235-4cbb-8b4e-48b6fd0f2094.T.2022-02-16T14:38:11.769917408Z 227 // TODO use filepath.Separator to support windows 228 link = strings.ReplaceAll(link, "/", "") 229 // ..........nodese56c75a8-d235-4cbb-8b4e-48b6fd0f2094.T.2022-02-16T14:38:11.769917408Z 230 if link[0:15] != "..........nodes" || link[51:54] != node.TrashIDDelimiter { 231 return "", "", "", errtypes.InternalError("malformed trash link") 232 } 233 return resolved, link[15:51], link[54:], nil 234 } 235 236 func (tb *DecomposedfsTrashbin) listTrashRoot(ctx context.Context, spaceID string) ([]*provider.RecycleItem, error) { 237 log := appctx.GetLogger(ctx) 238 trashRoot := tb.getRecycleRoot(spaceID) 239 items := []*provider.RecycleItem{} 240 subTrees, err := filepath.Glob(trashRoot + "/*") 241 if err != nil { 242 return nil, err 243 } 244 245 numWorkers := tb.fs.o.MaxConcurrency 246 if len(subTrees) < numWorkers { 247 numWorkers = len(subTrees) 248 } 249 250 work := make(chan string, len(subTrees)) 251 results := make(chan *provider.RecycleItem, len(subTrees)) 252 253 g, ctx := errgroup.WithContext(ctx) 254 255 // Distribute work 256 g.Go(func() error { 257 defer close(work) 258 for _, itemPath := range subTrees { 259 select { 260 case work <- itemPath: 261 case <-ctx.Done(): 262 return ctx.Err() 263 } 264 } 265 return nil 266 }) 267 268 // Spawn workers that'll concurrently work the queue 269 for i := 0; i < numWorkers; i++ { 270 g.Go(func() error { 271 for subTree := range work { 272 matches, err := filepath.Glob(subTree + "/*/*/*/*") 273 if err != nil { 274 return err 275 } 276 277 for _, itemPath := range matches { 278 // TODO can we encode this in the path instead of reading the link? 279 nodePath, nodeID, timeSuffix, err := readTrashLink(itemPath) 280 if err != nil { 281 log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Msg("error reading trash link, skipping") 282 continue 283 } 284 285 md, err := os.Stat(nodePath) 286 if err != nil { 287 log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("could not stat trash item, skipping") 288 continue 289 } 290 291 attrs, err := tb.fs.lu.MetadataBackend().All(ctx, nodePath) 292 if err != nil { 293 log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("could not get extended attributes, skipping") 294 continue 295 } 296 297 nodeType := tb.fs.lu.TypeFromPath(ctx, nodePath) 298 if nodeType == provider.ResourceType_RESOURCE_TYPE_INVALID { 299 log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("invalid node type, skipping") 300 continue 301 } 302 303 item := &provider.RecycleItem{ 304 Type: nodeType, 305 Size: uint64(md.Size()), 306 Key: nodeID, 307 } 308 if deletionTime, err := time.Parse(time.RFC3339Nano, timeSuffix); err == nil { 309 item.DeletionTime = &types.Timestamp{ 310 Seconds: uint64(deletionTime.Unix()), 311 // TODO nanos 312 } 313 } else { 314 log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("spaceid", spaceID).Str("nodeid", nodeID).Str("dtime", timeSuffix).Msg("could not parse time format, ignoring") 315 } 316 317 // lookup origin path in extended attributes 318 if attr, ok := attrs[prefixes.TrashOriginAttr]; ok { 319 item.Ref = &provider.Reference{Path: string(attr)} 320 } else { 321 log.Error().Str("trashRoot", trashRoot).Str("item", itemPath).Str("spaceid", spaceID).Str("nodeid", nodeID).Str("dtime", timeSuffix).Msg("could not read origin path") 322 } 323 324 select { 325 case results <- item: 326 case <-ctx.Done(): 327 return ctx.Err() 328 } 329 } 330 } 331 return nil 332 }) 333 } 334 335 // Wait for things to settle down, then close results chan 336 go func() { 337 _ = g.Wait() // error is checked later 338 close(results) 339 }() 340 341 // Collect results 342 for ri := range results { 343 items = append(items, ri) 344 } 345 return items, nil 346 } 347 348 // RestoreRecycleItem restores the specified item 349 func (tb *DecomposedfsTrashbin) RestoreRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string, restoreRef *provider.Reference) error { 350 _, span := tracer.Start(ctx, "RestoreRecycleItem") 351 defer span.End() 352 if ref == nil { 353 return errtypes.BadRequest("missing reference, needs a space id") 354 } 355 356 var targetNode *node.Node 357 if restoreRef != nil { 358 tn, err := tb.fs.lu.NodeFromResource(ctx, restoreRef) 359 if err != nil { 360 return err 361 } 362 363 targetNode = tn 364 } 365 366 rn, parent, restoreFunc, err := tb.fs.tp.RestoreRecycleItemFunc(ctx, ref.ResourceId.SpaceId, key, relativePath, targetNode) 367 if err != nil { 368 return err 369 } 370 371 // check permissions of deleted node 372 rp, err := tb.fs.p.AssembleTrashPermissions(ctx, rn) 373 switch { 374 case err != nil: 375 return err 376 case !rp.RestoreRecycleItem: 377 if rp.Stat { 378 return errtypes.PermissionDenied(key) 379 } 380 return errtypes.NotFound(key) 381 } 382 383 // Set space owner in context 384 storagespace.ContextSendSpaceOwnerID(ctx, rn.SpaceOwnerOrManager(ctx)) 385 386 // check we can write to the parent of the restore reference 387 pp, err := tb.fs.p.AssemblePermissions(ctx, parent) 388 switch { 389 case err != nil: 390 return err 391 case !pp.InitiateFileUpload: 392 // share receiver cannot restore to a shared resource to which she does not have write permissions. 393 if rp.Stat { 394 return errtypes.PermissionDenied(key) 395 } 396 return errtypes.NotFound(key) 397 } 398 399 // Run the restore func 400 return restoreFunc() 401 } 402 403 // PurgeRecycleItem purges the specified item, all its children and all their revisions 404 func (tb *DecomposedfsTrashbin) PurgeRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string) error { 405 _, span := tracer.Start(ctx, "PurgeRecycleItem") 406 defer span.End() 407 if ref == nil { 408 return errtypes.BadRequest("missing reference, needs a space id") 409 } 410 411 rn, purgeFunc, err := tb.fs.tp.PurgeRecycleItemFunc(ctx, ref.ResourceId.OpaqueId, key, relativePath) 412 if err != nil { 413 if errors.Is(err, iofs.ErrNotExist) { 414 return errtypes.NotFound(key) 415 } 416 return err 417 } 418 419 // check permissions of deleted node 420 rp, err := tb.fs.p.AssembleTrashPermissions(ctx, rn) 421 switch { 422 case err != nil: 423 return err 424 case !rp.PurgeRecycle: 425 if rp.Stat { 426 return errtypes.PermissionDenied(key) 427 } 428 return errtypes.NotFound(key) 429 } 430 431 // Run the purge func 432 return purgeFunc() 433 } 434 435 // EmptyRecycle empties the trash 436 func (tb *DecomposedfsTrashbin) EmptyRecycle(ctx context.Context, ref *provider.Reference) error { 437 _, span := tracer.Start(ctx, "EmptyRecycle") 438 defer span.End() 439 if ref == nil || ref.ResourceId == nil || ref.ResourceId.OpaqueId == "" { 440 return errtypes.BadRequest("spaceid must be set") 441 } 442 443 items, err := tb.ListRecycle(ctx, ref, "", "") 444 if err != nil { 445 return err 446 } 447 448 for _, i := range items { 449 if err := tb.PurgeRecycleItem(ctx, ref, i.Key, ""); err != nil { 450 return err 451 } 452 } 453 // TODO what permission should we check? we could check the root node of the user? or the owner permissions on his home root node? 454 // The current impl will wipe your own trash. or when no user provided the trash of 'root' 455 return os.RemoveAll(tb.getRecycleRoot(ref.ResourceId.SpaceId)) 456 } 457 458 func (tb *DecomposedfsTrashbin) getRecycleRoot(spaceID string) string { 459 return filepath.Join(tb.fs.getSpaceRoot(spaceID), "trash") 460 }