github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/vfs/version.go (about) 1 package vfs 2 3 import ( 4 "sort" 5 "time" 6 7 "github.com/cozy/cozy-stack/pkg/config/config" 8 "github.com/cozy/cozy-stack/pkg/consts" 9 "github.com/cozy/cozy-stack/pkg/couchdb" 10 "github.com/cozy/cozy-stack/pkg/jsonapi" 11 "github.com/cozy/cozy-stack/pkg/prefixer" 12 ) 13 14 // Version is used for storing the metadata about previous versions of file 15 // contents. The content its-self is stored on the local file system or in 16 // Swift. 17 type Version struct { 18 DocID string `json:"_id,omitempty"` 19 DocRev string `json:"_rev,omitempty"` 20 UpdatedAt time.Time `json:"updated_at"` 21 ByteSize int64 `json:"size,string"` 22 MD5Sum []byte `json:"md5sum"` 23 Tags []string `json:"tags"` 24 Metadata Metadata `json:"metadata,omitempty"` 25 CozyMetadata FilesCozyMetadata `json:"cozyMetadata,omitempty"` 26 Rels struct { 27 File struct { 28 Data struct { 29 ID string `json:"_id"` 30 Type string `json:"_type"` 31 } `json:"data"` 32 } `json:"file"` 33 } `json:"relationships"` 34 } 35 36 // ID returns the version identifier 37 func (v *Version) ID() string { return v.DocID } 38 39 // Rev returns the version revision 40 func (v *Version) Rev() string { return v.DocRev } 41 42 // DocType returns the version document type 43 func (v *Version) DocType() string { return consts.FilesVersions } 44 45 // Clone implements couchdb.Doc 46 func (v *Version) Clone() couchdb.Doc { 47 cloned := *v 48 cloned.MD5Sum = make([]byte, len(v.MD5Sum)) 49 copy(cloned.MD5Sum, v.MD5Sum) 50 cloned.Tags = make([]string, len(v.Tags)) 51 copy(cloned.Tags, v.Tags) 52 cloned.Metadata = make(Metadata, len(v.Metadata)) 53 for k, val := range v.Metadata { 54 cloned.Metadata[k] = val 55 } 56 meta := v.CozyMetadata.Clone() 57 cloned.CozyMetadata = *meta 58 return &cloned 59 } 60 61 // SetID changes the version qualified identifier 62 func (v *Version) SetID(id string) { v.DocID = id } 63 64 // SetRev changes the version revision 65 func (v *Version) SetRev(rev string) { v.DocRev = rev } 66 67 // Included is part of jsonapi.Object interface 68 func (v *Version) Included() []jsonapi.Object { return nil } 69 70 // Relationships is part of jsonapi.Object interface 71 func (v *Version) Relationships() jsonapi.RelationshipMap { return nil } 72 73 // Links is part of jsonapi.Object interface 74 func (v *Version) Links() *jsonapi.LinksList { return nil } 75 76 // NewVersion returns a version from a given FileDoc. It is often used just 77 // before modifying the content of this file. 78 // Note that the _id is precomputed as it can be useful to use it for a storage 79 // location before the version is saved in CouchDB. 80 func NewVersion(file *FileDoc) *Version { 81 var instanceURL string 82 if file.CozyMetadata != nil { 83 instanceURL = file.CozyMetadata.UploadedOn 84 } 85 fcm := NewCozyMetadata(instanceURL) 86 id := file.InternalID 87 if id == "" { 88 id = file.Rev() 89 } 90 v := &Version{ 91 DocID: file.ID() + "/" + id, 92 UpdatedAt: file.UpdatedAt, 93 ByteSize: file.ByteSize, 94 MD5Sum: file.MD5Sum, 95 Tags: file.Tags, 96 Metadata: file.Metadata, 97 CozyMetadata: *fcm, 98 } 99 v.Rels.File.Data.ID = file.ID() 100 v.Rels.File.Data.Type = consts.Files 101 v.CozyMetadata.UploadedOn = instanceURL 102 at := file.UpdatedAt 103 if file.CozyMetadata != nil && file.CozyMetadata.UploadedAt != nil { 104 at = *file.CozyMetadata.UploadedAt 105 } 106 v.CozyMetadata.UploadedAt = &at 107 if file.CozyMetadata != nil && file.CozyMetadata.UploadedBy != nil { 108 by := *file.CozyMetadata.UploadedBy 109 v.CozyMetadata.UploadedBy = &by 110 } 111 return v 112 } 113 114 // SetMetaFromVersion takes the metadata from the version and copies them to 115 // the file document. 116 func SetMetaFromVersion(file *FileDoc, version *Version) { 117 file.UpdatedAt = version.UpdatedAt 118 file.ByteSize = version.ByteSize 119 file.MD5Sum = version.MD5Sum 120 file.Tags = version.Tags 121 file.Metadata = version.Metadata 122 if file.CozyMetadata == nil { 123 file.CozyMetadata = NewCozyMetadata("") 124 file.CozyMetadata.CreatedAt = file.CreatedAt 125 } 126 file.CozyMetadata.UploadedOn = version.CozyMetadata.UploadedOn 127 at := *version.CozyMetadata.UploadedAt 128 file.CozyMetadata.UploadedAt = &at 129 if version.CozyMetadata.UploadedBy != nil { 130 by := *version.CozyMetadata.UploadedBy 131 file.CozyMetadata.UploadedBy = &by 132 } 133 } 134 135 // FindVersion returns the version for the given id 136 func FindVersion(db prefixer.Prefixer, id string) (*Version, error) { 137 doc := &Version{} 138 if err := couchdb.GetDoc(db, consts.FilesVersions, id, doc); err != nil { 139 return nil, err 140 } 141 return doc, nil 142 } 143 144 // VersionsFor returns the list of the versions for a given file identifier. 145 func VersionsFor(db prefixer.Prefixer, fileID string) ([]*Version, error) { 146 var versions []*Version 147 req := &couchdb.AllDocsRequest{ 148 StartKey: fileID + "/", 149 EndKey: fileID + "0", // 0 is the next character after / in ascii 150 } 151 if err := couchdb.GetAllDocs(db, consts.FilesVersions, req, &versions); err != nil { 152 return nil, err 153 } 154 return versions, nil 155 } 156 157 type ActionForCandidateVersion int 158 159 const ( 160 DoNothingForCandidateVersion ActionForCandidateVersion = iota 161 KeepCandidateVersion 162 CleanCandidateVersion 163 ) 164 165 // FindVersionsToClean returns a bool to say if the candidate version must be 166 // cleaned, a list of old versions to clean, and an error. The rules to know 167 // the versions to clean or keep are: 168 // - the tagged versions are kept 169 // - two versions must not be too close in time 170 // - there is a maximal number of versions. 171 func FindVersionsToClean(db Prefixer, fileID string, candidate *Version) (ActionForCandidateVersion, []*Version, error) { 172 olds, err := VersionsFor(db, fileID) 173 if err != nil { 174 return DoNothingForCandidateVersion, nil, err 175 } 176 maxNumber, minDelay := getVersioningConfig(db.GetContextName()) 177 action, toClean := detectVersionsToClean(candidate, olds, maxNumber, minDelay) 178 return action, toClean, nil 179 } 180 181 func getVersioningConfig(contextName string) (int, time.Duration) { 182 cfg := config.GetConfig() 183 maxNumber := cfg.Fs.Versioning.MaxNumberToKeep 184 minDelay := cfg.Fs.Versioning.MinDelayBetweenTwoVersions 185 186 context, _ := cfg.Fs.Contexts[contextName].(map[string]interface{}) 187 if number, ok := context["max_number_of_versions_to_keep"].(int); ok { 188 maxNumber = number 189 } 190 if delay, ok := context["min_delay_between_two_versions"].(string); ok { 191 if min, err := time.ParseDuration(delay); err == nil { 192 minDelay = min 193 } 194 } 195 196 return maxNumber, minDelay 197 } 198 199 func detectVersionsToClean(candidate *Version, olds []*Version, maxNumber int, minDelay time.Duration) (ActionForCandidateVersion, []*Version) { 200 if maxNumber == 0 { 201 return CleanCandidateVersion, olds 202 } 203 204 if len(olds) == 0 { 205 return KeepCandidateVersion, nil 206 } 207 208 // When there are 2 concurrent uploads for the same file, we may have a 209 // race condition on the candidate version, and we can avoid it by checking 210 // the ID. 211 for _, old := range olds { 212 if old.DocID == candidate.DocID { 213 return DoNothingForCandidateVersion, nil 214 } 215 } 216 217 sort.Slice(olds, func(i, j int) bool { 218 return olds[i].CozyMetadata.CreatedAt.Before(olds[j].CozyMetadata.CreatedAt) 219 }) 220 221 // We will keep the candidate version if it has no tags and is not too 222 // close to the previous version. 223 action := KeepCandidateVersion 224 if candidate != nil && len(candidate.Tags) == 0 { 225 candidateTime := candidate.CozyMetadata.CreatedAt 226 previousTime := olds[len(olds)-1].CozyMetadata.CreatedAt 227 if previousTime.Add(minDelay).After(candidateTime) { 228 action = CleanCandidateVersion 229 } 230 } 231 232 // Find the number of old versions to clean 233 nb := len(olds) + 1 - maxNumber // +1 is for the current version 234 if candidate != nil && action == KeepCandidateVersion { 235 nb++ // +1 for the candidate version (if it is kept) 236 } 237 if nb <= 0 { 238 return action, nil 239 } 240 241 var toClean []*Version 242 for _, v := range olds { 243 if len(v.Tags) > 0 { 244 continue 245 } 246 toClean = append(toClean, v) 247 nb-- 248 if nb <= 0 { 249 break 250 } 251 } 252 return action, toClean 253 } 254 255 var _ jsonapi.Object = &Version{}