github.com/cs3org/reva/v2@v2.27.7/pkg/storage/fs/posix/trashbin/trashbin.go (about) 1 // Copyright 2018-2024 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 trashbin 20 21 import ( 22 "context" 23 "fmt" 24 "os" 25 "path/filepath" 26 "strings" 27 "time" 28 29 "github.com/google/uuid" 30 "github.com/rs/zerolog" 31 32 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 33 typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" 34 "github.com/cs3org/reva/v2/pkg/storage" 35 "github.com/cs3org/reva/v2/pkg/storage/fs/posix/lookup" 36 "github.com/cs3org/reva/v2/pkg/storage/fs/posix/options" 37 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes" 38 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node" 39 "github.com/cs3org/reva/v2/pkg/utils" 40 ) 41 42 type Trashbin struct { 43 fs storage.FS 44 o *options.Options 45 lu *lookup.Lookup 46 log *zerolog.Logger 47 } 48 49 const ( 50 trashHeader = `[Trash Info]` 51 timeFormat = "2006-01-02T15:04:05" 52 ) 53 54 // New returns a new Trashbin 55 func New(o *options.Options, lu *lookup.Lookup, log *zerolog.Logger) (*Trashbin, error) { 56 return &Trashbin{ 57 o: o, 58 lu: lu, 59 log: log, 60 }, nil 61 } 62 63 func (tb *Trashbin) writeInfoFile(trashPath, id, path string) error { 64 c := trashHeader 65 c += "\nPath=" + path 66 c += "\nDeletionDate=" + time.Now().Format(timeFormat) 67 68 return os.WriteFile(filepath.Join(trashPath, "info", id+".trashinfo"), []byte(c), 0644) 69 } 70 71 func (tb *Trashbin) readInfoFile(trashPath, id string) (string, *typesv1beta1.Timestamp, error) { 72 c, err := os.ReadFile(filepath.Join(trashPath, "info", id+".trashinfo")) 73 if err != nil { 74 return "", nil, err 75 } 76 77 var ( 78 path string 79 ts *typesv1beta1.Timestamp 80 ) 81 82 for _, line := range strings.Split(string(c), "\n") { 83 if strings.HasPrefix(line, "DeletionDate=") { 84 t, err := time.ParseInLocation(timeFormat, strings.TrimSpace(strings.TrimPrefix(line, "DeletionDate=")), time.Local) 85 if err != nil { 86 return "", nil, err 87 } 88 ts = utils.TimeToTS(t) 89 } 90 if strings.HasPrefix(line, "Path=") { 91 path = strings.TrimPrefix(line, "Path=") 92 } 93 } 94 95 return path, ts, nil 96 } 97 98 // Setup the trashbin 99 func (tb *Trashbin) Setup(fs storage.FS) error { 100 if tb.fs != nil { 101 return nil 102 } 103 104 tb.fs = fs 105 return nil 106 } 107 108 func trashRootForNode(n *node.Node) string { 109 return filepath.Join(n.SpaceRoot.InternalPath(), ".Trash") 110 } 111 112 func (tb *Trashbin) MoveToTrash(ctx context.Context, n *node.Node, path string) error { 113 key := uuid.New().String() 114 trashPath := trashRootForNode(n) 115 116 err := os.MkdirAll(filepath.Join(trashPath, "info"), 0755) 117 if err != nil { 118 return err 119 } 120 err = os.MkdirAll(filepath.Join(trashPath, "files"), 0755) 121 if err != nil { 122 return err 123 } 124 125 relPath := strings.TrimPrefix(path, n.SpaceRoot.InternalPath()) 126 relPath = strings.TrimPrefix(relPath, "/") 127 err = tb.writeInfoFile(trashPath, key, relPath) 128 if err != nil { 129 return err 130 } 131 132 // purge metadata 133 if err = tb.lu.IDCache.DeleteByPath(ctx, path); err != nil { 134 return err 135 } 136 137 itemTrashPath := filepath.Join(trashPath, "files", key+".trashitem") 138 err = tb.lu.MetadataBackend().Rename(path, itemTrashPath) 139 if err != nil { 140 return err 141 } 142 143 return os.Rename(path, itemTrashPath) 144 } 145 146 // ListRecycle returns the list of available recycle items 147 // ref -> the space (= resourceid), key -> deleted node id, relativePath = relative to key 148 func (tb *Trashbin) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) { 149 n, err := tb.lu.NodeFromResource(ctx, ref) 150 if err != nil { 151 return nil, err 152 } 153 154 trashRoot := trashRootForNode(n) 155 base := filepath.Join(trashRoot, "files") 156 157 var originalPath string 158 var ts *typesv1beta1.Timestamp 159 if key != "" { 160 // this is listing a specific item/folder 161 base = filepath.Join(base, key+".trashitem", relativePath) 162 originalPath, ts, err = tb.readInfoFile(trashRoot, key) 163 originalPath = filepath.Join(originalPath, relativePath) 164 if err != nil { 165 return nil, err 166 } 167 } 168 169 items := []*provider.RecycleItem{} 170 entries, err := os.ReadDir(filepath.Clean(base)) 171 if err != nil { 172 switch err.(type) { 173 case *os.PathError: 174 return items, nil 175 default: 176 return nil, err 177 } 178 } 179 180 for _, entry := range entries { 181 var fi os.FileInfo 182 var entryOriginalPath string 183 var entryKey string 184 if strings.HasSuffix(entry.Name(), ".trashitem") { 185 entryKey = strings.TrimSuffix(entry.Name(), ".trashitem") 186 entryOriginalPath, ts, err = tb.readInfoFile(trashRoot, entryKey) 187 if err != nil { 188 continue 189 } 190 191 fi, err = entry.Info() 192 if err != nil { 193 continue 194 } 195 } else { 196 fi, err = os.Stat(filepath.Join(base, entry.Name())) 197 entryKey = entry.Name() 198 entryOriginalPath = filepath.Join(originalPath, entry.Name()) 199 if err != nil { 200 continue 201 } 202 } 203 204 item := &provider.RecycleItem{ 205 Key: filepath.Join(key, relativePath, entryKey), 206 Size: uint64(fi.Size()), 207 Ref: &provider.Reference{ 208 ResourceId: &provider.ResourceId{ 209 SpaceId: ref.GetResourceId().GetSpaceId(), 210 OpaqueId: ref.GetResourceId().GetSpaceId(), 211 }, 212 Path: entryOriginalPath, 213 }, 214 DeletionTime: ts, 215 } 216 if entry.IsDir() { 217 item.Type = provider.ResourceType_RESOURCE_TYPE_CONTAINER 218 } else { 219 item.Type = provider.ResourceType_RESOURCE_TYPE_FILE 220 } 221 222 items = append(items, item) 223 } 224 225 return items, nil 226 } 227 228 // RestoreRecycleItem restores the specified item 229 func (tb *Trashbin) RestoreRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string, restoreRef *provider.Reference) error { 230 n, err := tb.lu.NodeFromResource(ctx, ref) 231 if err != nil { 232 return err 233 } 234 235 trashRoot := trashRootForNode(n) 236 trashPath := filepath.Clean(filepath.Join(trashRoot, "files", key+".trashitem", relativePath)) 237 238 restoreBaseNode, err := tb.lu.NodeFromID(ctx, restoreRef.GetResourceId()) 239 if err != nil { 240 return err 241 } 242 restorePath := filepath.Join(restoreBaseNode.InternalPath(), restoreRef.GetPath()) 243 244 id, err := tb.lu.MetadataBackend().Get(ctx, trashPath, prefixes.IDAttr) 245 if err != nil { 246 return err 247 } 248 249 // update parent id in case it was restored to a different location 250 parentID, err := tb.lu.MetadataBackend().Get(ctx, filepath.Dir(restorePath), prefixes.IDAttr) 251 if err != nil { 252 return err 253 } 254 if len(parentID) == 0 { 255 return fmt.Errorf("trashbin: parent id not found for %s", restorePath) 256 } 257 258 err = tb.lu.MetadataBackend().Set(ctx, trashPath, prefixes.ParentidAttr, parentID) 259 if err != nil { 260 return err 261 } 262 263 // restore the item 264 err = os.Rename(trashPath, restorePath) 265 if err != nil { 266 return err 267 } 268 if err := tb.lu.CacheID(ctx, n.SpaceID, string(id), restorePath); err != nil { 269 tb.log.Error().Err(err).Str("spaceID", n.SpaceID).Str("id", string(id)).Str("path", restorePath).Msg("trashbin: error caching id") 270 } 271 272 // cleanup trash info 273 if relativePath == "." || relativePath == "/" { 274 return os.Remove(filepath.Join(trashRoot, "info", key+".trashinfo")) 275 } else { 276 return nil 277 } 278 } 279 280 // PurgeRecycleItem purges the specified item, all its children and all their revisions 281 func (tb *Trashbin) PurgeRecycleItem(ctx context.Context, ref *provider.Reference, key, relativePath string) error { 282 n, err := tb.lu.NodeFromResource(ctx, ref) 283 if err != nil { 284 return err 285 } 286 287 trashRoot := trashRootForNode(n) 288 err = os.RemoveAll(filepath.Clean(filepath.Join(trashRoot, "files", key+".trashitem", relativePath))) 289 if err != nil { 290 return err 291 } 292 293 cleanPath := filepath.Clean(relativePath) 294 if cleanPath == "." || cleanPath == "/" { 295 return os.Remove(filepath.Join(trashRoot, "info", key+".trashinfo")) 296 } 297 return nil 298 } 299 300 // EmptyRecycle empties the trash 301 func (tb *Trashbin) EmptyRecycle(ctx context.Context, ref *provider.Reference) error { 302 n, err := tb.lu.NodeFromResource(ctx, ref) 303 if err != nil { 304 return err 305 } 306 307 trashRoot := trashRootForNode(n) 308 err = os.RemoveAll(filepath.Clean(filepath.Join(trashRoot, "files"))) 309 if err != nil { 310 return err 311 } 312 return os.RemoveAll(filepath.Clean(filepath.Join(trashRoot, "info"))) 313 }