github.com/cs3org/reva/v2@v2.27.7/pkg/storage/utils/decomposedfs/revisions.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 "io" 24 "os" 25 "path/filepath" 26 "strings" 27 "time" 28 29 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 30 "github.com/pkg/errors" 31 "github.com/rogpeppe/go-internal/lockedfile" 32 33 "github.com/cs3org/reva/v2/pkg/appctx" 34 "github.com/cs3org/reva/v2/pkg/errtypes" 35 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/metadata/prefixes" 36 "github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/node" 37 "github.com/cs3org/reva/v2/pkg/storagespace" 38 "github.com/cs3org/reva/v2/pkg/utils" 39 ) 40 41 // Revision entries are stored inside the node folder and start with the same uuid as the current version. 42 // The `.REV.` indicates it is a revision and what follows is a timestamp, so multiple versions 43 // can be kept in the same location as the current file content. This prevents new fileuploads 44 // to trigger cross storage moves when revisions accidentally are stored on another partition, 45 // because the admin mounted a different partition there. 46 // We can add a background process to move old revisions to a slower storage 47 // and replace the revision file with a symbolic link in the future, if necessary. 48 49 // ListRevisions lists the revisions of the given resource 50 func (fs *Decomposedfs) ListRevisions(ctx context.Context, ref *provider.Reference) (revisions []*provider.FileVersion, err error) { 51 _, span := tracer.Start(ctx, "ListRevisions") 52 defer span.End() 53 var n *node.Node 54 if n, err = fs.lu.NodeFromResource(ctx, ref); err != nil { 55 return 56 } 57 if !n.Exists { 58 err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name)) 59 return 60 } 61 62 rp, err := fs.p.AssemblePermissions(ctx, n) 63 switch { 64 case err != nil: 65 return nil, err 66 case !rp.ListFileVersions: 67 f, _ := storagespace.FormatReference(ref) 68 if rp.Stat { 69 return nil, errtypes.PermissionDenied(f) 70 } 71 return nil, errtypes.NotFound(f) 72 } 73 74 revisions = []*provider.FileVersion{} 75 np := n.InternalPath() 76 if items, err := filepath.Glob(np + node.RevisionIDDelimiter + "*"); err == nil { 77 for i := range items { 78 if fs.lu.MetadataBackend().IsMetaFile(items[i]) || strings.HasSuffix(items[i], ".mlock") { 79 continue 80 } 81 82 if fi, err := os.Stat(items[i]); err == nil { 83 parts := strings.SplitN(fi.Name(), node.RevisionIDDelimiter, 2) 84 if len(parts) != 2 { 85 appctx.GetLogger(ctx).Error().Err(err).Str("name", fi.Name()).Msg("invalid revision name, skipping") 86 continue 87 } 88 mtime := fi.ModTime() 89 rev := &provider.FileVersion{ 90 Key: n.ID + node.RevisionIDDelimiter + parts[1], 91 Mtime: uint64(mtime.Unix()), 92 } 93 _, blobSize, err := fs.lu.ReadBlobIDAndSizeAttr(ctx, items[i], nil) 94 if err != nil { 95 appctx.GetLogger(ctx).Error().Err(err).Str("name", fi.Name()).Msg("error reading blobsize xattr, using 0") 96 } 97 rev.Size = uint64(blobSize) 98 etag, err := node.CalculateEtag(n.ID, mtime) 99 if err != nil { 100 return nil, errors.Wrapf(err, "error calculating etag") 101 } 102 rev.Etag = etag 103 revisions = append(revisions, rev) 104 } 105 } 106 } 107 // maybe we need to sort the list by key 108 /* 109 sort.Slice(revisions, func(i, j int) bool { 110 return revisions[i].Key > revisions[j].Key 111 }) 112 */ 113 114 return 115 } 116 117 // DownloadRevision returns a reader for the specified revision 118 // FIXME the CS3 api should explicitly allow initiating revision and trash download, a related issue is https://github.com/cs3org/reva/issues/1813 119 func (fs *Decomposedfs) DownloadRevision(ctx context.Context, ref *provider.Reference, revisionKey string, openReaderFunc func(md *provider.ResourceInfo) bool) (*provider.ResourceInfo, io.ReadCloser, error) { 120 _, span := tracer.Start(ctx, "DownloadRevision") 121 defer span.End() 122 log := appctx.GetLogger(ctx) 123 124 // verify revision key format 125 kp := strings.SplitN(revisionKey, node.RevisionIDDelimiter, 2) 126 if len(kp) != 2 { 127 log.Error().Str("revisionKey", revisionKey).Msg("malformed revisionKey") 128 return nil, nil, errtypes.NotFound(revisionKey) 129 } 130 log.Debug().Str("revisionKey", revisionKey).Msg("DownloadRevision") 131 132 spaceID := ref.ResourceId.SpaceId 133 // check if the node is available and has not been deleted 134 n, err := node.ReadNode(ctx, fs.lu, spaceID, kp[0], false, nil, false) 135 if err != nil { 136 return nil, nil, err 137 } 138 if !n.Exists { 139 err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name)) 140 return nil, nil, err 141 } 142 143 rp, err := fs.p.AssemblePermissions(ctx, n) 144 switch { 145 case err != nil: 146 return nil, nil, err 147 case !rp.ListFileVersions || !rp.InitiateFileDownload: // TODO add explicit permission in the CS3 api? 148 f, _ := storagespace.FormatReference(ref) 149 if rp.Stat { 150 return nil, nil, errtypes.PermissionDenied(f) 151 } 152 return nil, nil, errtypes.NotFound(f) 153 } 154 155 contentPath := fs.lu.InternalPath(spaceID, revisionKey) 156 157 blobid, blobsize, err := fs.lu.ReadBlobIDAndSizeAttr(ctx, contentPath, nil) 158 if err != nil { 159 return nil, nil, errors.Wrapf(err, "Decomposedfs: could not read blob id and size for revision '%s' of node '%s'", kp[1], n.ID) 160 } 161 162 revisionNode := node.Node{SpaceID: spaceID, BlobID: blobid, Blobsize: blobsize} // blobsize is needed for the s3ng blobstore 163 164 ri, err := n.AsResourceInfo(ctx, rp, nil, []string{"size", "mimetype", "etag"}, true) 165 if err != nil { 166 return nil, nil, err 167 } 168 169 // update resource info with revision data 170 mtime, err := time.Parse(time.RFC3339Nano, kp[1]) 171 if err != nil { 172 return nil, nil, errors.Wrapf(err, "Decomposedfs: could not parse mtime for revision '%s' of node '%s'", kp[1], n.ID) 173 } 174 ri.Size = uint64(blobsize) 175 ri.Mtime = utils.TimeToTS(mtime) 176 ri.Etag, err = node.CalculateEtag(n.ID, mtime) 177 if err != nil { 178 return nil, nil, errors.Wrapf(err, "error calculating etag for revision '%s' of node '%s'", kp[1], n.ID) 179 } 180 181 var reader io.ReadCloser 182 if openReaderFunc(ri) { 183 reader, err = fs.tp.ReadBlob(&revisionNode) 184 if err != nil { 185 return nil, nil, errors.Wrapf(err, "Decomposedfs: could not download blob of revision '%s' for node '%s'", n.ID, revisionKey) 186 } 187 } 188 return ri, reader, nil 189 } 190 191 // RestoreRevision restores the specified revision of the resource 192 func (fs *Decomposedfs) RestoreRevision(ctx context.Context, ref *provider.Reference, revisionKey string) (returnErr error) { 193 _, span := tracer.Start(ctx, "RestoreRevision") 194 defer span.End() 195 log := appctx.GetLogger(ctx) 196 197 // verify revision key format 198 kp := strings.SplitN(revisionKey, node.RevisionIDDelimiter, 2) 199 if len(kp) != 2 { 200 log.Error().Str("revisionKey", revisionKey).Msg("malformed revisionKey") 201 return errtypes.NotFound(revisionKey) 202 } 203 204 spaceID := ref.ResourceId.SpaceId 205 // check if the node is available and has not been deleted 206 n, err := node.ReadNode(ctx, fs.lu, spaceID, kp[0], false, nil, false) 207 if err != nil { 208 return err 209 } 210 if !n.Exists { 211 err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name)) 212 return err 213 } 214 215 rp, err := fs.p.AssemblePermissions(ctx, n) 216 switch { 217 case err != nil: 218 return err 219 case !rp.RestoreFileVersion: 220 f, _ := storagespace.FormatReference(ref) 221 if rp.Stat { 222 return errtypes.PermissionDenied(f) 223 } 224 return errtypes.NotFound(f) 225 } 226 227 // Set space owner in context 228 storagespace.ContextSendSpaceOwnerID(ctx, n.SpaceOwnerOrManager(ctx)) 229 230 // check lock 231 if err := n.CheckLock(ctx); err != nil { 232 return err 233 } 234 235 // write lock node before copying metadata 236 f, err := lockedfile.OpenFile(fs.lu.MetadataBackend().LockfilePath(n.InternalPath()), os.O_RDWR|os.O_CREATE, 0600) 237 if err != nil { 238 return err 239 } 240 defer func() { 241 _ = f.Close() 242 _ = os.Remove(fs.lu.MetadataBackend().LockfilePath(n.InternalPath())) 243 }() 244 245 // move current version to new revision 246 nodePath := fs.lu.InternalPath(spaceID, kp[0]) 247 mtime, err := n.GetMTime(ctx) 248 if err != nil { 249 log.Error().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("cannot read mtime") 250 return err 251 } 252 253 // revisions are stored alongside the actual file, so a rename can be efficient and does not cross storage / partition boundaries 254 newRevisionPath := fs.lu.InternalPath(spaceID, kp[0]+node.RevisionIDDelimiter+mtime.UTC().Format(time.RFC3339Nano)) 255 256 // touch new revision 257 if _, err := os.Create(newRevisionPath); err != nil { 258 return err 259 } 260 defer func() { 261 if returnErr != nil { 262 if err := os.Remove(newRevisionPath); err != nil { 263 log.Error().Err(err).Str("revision", filepath.Base(newRevisionPath)).Msg("could not clean up revision node") 264 } 265 if err := fs.lu.MetadataBackend().Purge(ctx, newRevisionPath); err != nil { 266 log.Error().Err(err).Str("revision", filepath.Base(newRevisionPath)).Msg("could not clean up revision node") 267 } 268 } 269 }() 270 271 // copy blob metadata from node to new revision node 272 err = fs.lu.CopyMetadataWithSourceLock(ctx, nodePath, newRevisionPath, func(attributeName string, value []byte) (newValue []byte, copy bool) { 273 return value, strings.HasPrefix(attributeName, prefixes.ChecksumPrefix) || // for checksums 274 attributeName == prefixes.TypeAttr || 275 attributeName == prefixes.BlobIDAttr || 276 attributeName == prefixes.BlobsizeAttr || 277 attributeName == prefixes.MTimeAttr // FIXME somewhere I mix up the revision time and the mtime, causing the restore to overwrite the other existing revisien 278 }, f, true) 279 if err != nil { 280 return errtypes.InternalError("failed to copy blob xattrs to version node: " + err.Error()) 281 } 282 283 // remember mtime from node as new revision mtime 284 if err = os.Chtimes(newRevisionPath, mtime, mtime); err != nil { 285 return errtypes.InternalError("failed to change mtime of version node") 286 } 287 288 // update blob id in node 289 290 // copy blob metadata from restored revision to node 291 restoredRevisionPath := fs.lu.InternalPath(spaceID, revisionKey) 292 err = fs.lu.CopyMetadata(ctx, restoredRevisionPath, nodePath, func(attributeName string, value []byte) (newValue []byte, copy bool) { 293 return value, strings.HasPrefix(attributeName, prefixes.ChecksumPrefix) || 294 attributeName == prefixes.TypeAttr || 295 attributeName == prefixes.BlobIDAttr || 296 attributeName == prefixes.BlobsizeAttr 297 }, false) 298 if err != nil { 299 return errtypes.InternalError("failed to copy blob xattrs to old revision to node: " + err.Error()) 300 } 301 // always set the node mtime to the current time 302 err = fs.lu.MetadataBackend().SetMultiple(ctx, nodePath, 303 map[string][]byte{ 304 prefixes.MTimeAttr: []byte(time.Now().UTC().Format(time.RFC3339Nano)), 305 }, 306 false) 307 if err != nil { 308 return errtypes.InternalError("failed to set mtime attribute on node: " + err.Error()) 309 } 310 311 revisionSize, err := fs.lu.MetadataBackend().GetInt64(ctx, restoredRevisionPath, prefixes.BlobsizeAttr) 312 if err != nil { 313 return errtypes.InternalError("failed to read blob size xattr from old revision") 314 } 315 316 // drop old revision 317 if err := os.Remove(restoredRevisionPath); err != nil { 318 log.Warn().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("could not delete old revision, continuing") 319 } 320 if err := os.Remove(fs.lu.MetadataBackend().MetadataPath(restoredRevisionPath)); err != nil { 321 log.Warn().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("could not delete old revision metadata, continuing") 322 } 323 if err := os.Remove(fs.lu.MetadataBackend().LockfilePath(restoredRevisionPath)); err != nil { 324 log.Warn().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("could not delete old revision metadata lockfile, continuing") 325 } 326 if err := fs.lu.MetadataBackend().Purge(ctx, restoredRevisionPath); err != nil { 327 log.Warn().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("could not purge old revision from cache, continuing") 328 } 329 330 // revision 5, current 10 (restore a smaller blob) -> 5-10 = -5 331 // revision 10, current 5 (restore a bigger blob) -> 10-5 = +5 332 sizeDiff := revisionSize - n.Blobsize 333 334 return fs.tp.Propagate(ctx, n, sizeDiff) 335 } 336 337 // DeleteRevision deletes the specified revision of the resource 338 func (fs *Decomposedfs) DeleteRevision(ctx context.Context, ref *provider.Reference, revisionKey string) error { 339 _, span := tracer.Start(ctx, "DeleteRevision") 340 defer span.End() 341 n, err := fs.getRevisionNode(ctx, ref, revisionKey, func(rp *provider.ResourcePermissions) bool { 342 return rp.RestoreFileVersion 343 }) 344 if err != nil { 345 return err 346 } 347 348 if err := os.RemoveAll(fs.lu.InternalPath(n.SpaceID, revisionKey)); err != nil { 349 return err 350 } 351 352 return fs.tp.DeleteBlob(n) 353 } 354 355 func (fs *Decomposedfs) getRevisionNode(ctx context.Context, ref *provider.Reference, revisionKey string, hasPermission func(*provider.ResourcePermissions) bool) (*node.Node, error) { 356 _, span := tracer.Start(ctx, "getRevisionNode") 357 defer span.End() 358 log := appctx.GetLogger(ctx) 359 360 // verify revision key format 361 kp := strings.SplitN(revisionKey, node.RevisionIDDelimiter, 2) 362 if len(kp) != 2 { 363 log.Error().Str("revisionKey", revisionKey).Msg("malformed revisionKey") 364 return nil, errtypes.NotFound(revisionKey) 365 } 366 log.Debug().Str("revisionKey", revisionKey).Msg("DownloadRevision") 367 368 spaceID := ref.ResourceId.SpaceId 369 // check if the node is available and has not been deleted 370 n, err := node.ReadNode(ctx, fs.lu, spaceID, kp[0], false, nil, false) 371 if err != nil { 372 return nil, err 373 } 374 if !n.Exists { 375 err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name)) 376 return nil, err 377 } 378 379 p, err := fs.p.AssemblePermissions(ctx, n) 380 switch { 381 case err != nil: 382 return nil, err 383 case !hasPermission(p): 384 return nil, errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) 385 } 386 387 // Set space owner in context 388 storagespace.ContextSendSpaceOwnerID(ctx, n.SpaceOwnerOrManager(ctx)) 389 390 return n, nil 391 }