github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/vfs/directory.go (about) 1 package vfs 2 3 import ( 4 "errors" 5 "os" 6 "path" 7 "path/filepath" 8 "strings" 9 "time" 10 11 "github.com/cozy/cozy-stack/pkg/consts" 12 "github.com/cozy/cozy-stack/pkg/couchdb" 13 ) 14 15 // DirDoc is a struct containing all the informations about a 16 // directory. It implements the couchdb.Doc and jsonapi.Object 17 // interfaces. 18 type DirDoc struct { 19 // Type of document. Useful to (de)serialize and filter the data 20 // from couch. 21 Type string `json:"type"` 22 // Qualified file identifier 23 DocID string `json:"_id,omitempty"` 24 // Directory revision 25 DocRev string `json:"_rev,omitempty"` 26 // Directory name 27 DocName string `json:"name,omitempty"` 28 // Parent directory identifier 29 DirID string `json:"dir_id,omitempty"` 30 RestorePath string `json:"restore_path,omitempty"` 31 32 CreatedAt time.Time `json:"created_at"` 33 UpdatedAt time.Time `json:"updated_at"` 34 Tags []string `json:"tags,omitempty"` 35 36 // Directory path on VFS. 37 // Fullpath should always be present. It is marked "omitempty" because 38 // DirDoc is the base of the DirOrFile struct. 39 Fullpath string `json:"path,omitempty"` 40 41 ReferencedBy []couchdb.DocReference `json:"referenced_by,omitempty"` 42 NotSynchronizedOn []couchdb.DocReference `json:"not_synchronized_on,omitempty"` 43 44 Metadata Metadata `json:"metadata,omitempty"` 45 CozyMetadata *FilesCozyMetadata `json:"cozyMetadata,omitempty"` 46 } 47 48 // ID returns the directory qualified identifier 49 func (d *DirDoc) ID() string { return d.DocID } 50 51 // Rev returns the directory revision 52 func (d *DirDoc) Rev() string { return d.DocRev } 53 54 // DocType returns the directory document type 55 func (d *DirDoc) DocType() string { return consts.Files } 56 57 // Clone implements couchdb.Doc 58 func (d *DirDoc) Clone() couchdb.Doc { 59 cloned := *d 60 cloned.Tags = make([]string, len(d.Tags)) 61 copy(cloned.Tags, d.Tags) 62 cloned.ReferencedBy = make([]couchdb.DocReference, len(d.ReferencedBy)) 63 copy(cloned.ReferencedBy, d.ReferencedBy) 64 cloned.NotSynchronizedOn = make([]couchdb.DocReference, len(d.NotSynchronizedOn)) 65 copy(cloned.NotSynchronizedOn, d.NotSynchronizedOn) 66 cloned.Metadata = make(Metadata, len(d.Metadata)) 67 for k, v := range d.Metadata { 68 cloned.Metadata[k] = v 69 } 70 if d.CozyMetadata != nil { 71 cloned.CozyMetadata = d.CozyMetadata.Clone() 72 } 73 return &cloned 74 } 75 76 // SetID changes the directory qualified identifier 77 func (d *DirDoc) SetID(id string) { d.DocID = id } 78 79 // SetRev changes the directory revision 80 func (d *DirDoc) SetRev(rev string) { d.DocRev = rev } 81 82 // Path is used to generate the file path 83 func (d *DirDoc) Path(fs FilePather) (string, error) { 84 return d.Fullpath, nil 85 } 86 87 // Parent returns the parent directory document 88 func (d *DirDoc) Parent(fs VFS) (*DirDoc, error) { 89 parent, err := fs.DirByID(d.DirID) 90 if os.IsNotExist(err) { 91 err = ErrParentDoesNotExist 92 } 93 return parent, err 94 } 95 96 // Name returns base name of the file 97 func (d *DirDoc) Name() string { return d.DocName } 98 99 // Size returns the length in bytes for regular files; system-dependent for others 100 func (d *DirDoc) Size() int64 { return 0 } 101 102 // Mode returns the file mode bits 103 func (d *DirDoc) Mode() os.FileMode { return 0755 } 104 105 // ModTime returns the modification time 106 func (d *DirDoc) ModTime() time.Time { return d.UpdatedAt } 107 108 // IsDir returns the abbreviation for Mode().IsDir() 109 func (d *DirDoc) IsDir() bool { return true } 110 111 // Sys returns the underlying data source (can return nil) 112 func (d *DirDoc) Sys() interface{} { return nil } 113 114 // IsEmpty returns whether or not the directory has at least one child. 115 func (d *DirDoc) IsEmpty(fs VFS) (bool, error) { 116 iter := fs.DirIterator(d, &IteratorOptions{ByFetch: 1}) 117 _, _, err := iter.Next() 118 if errors.Is(err, ErrIteratorDone) { 119 return true, nil 120 } 121 return false, err 122 } 123 124 // AddReferencedBy adds referenced_by to the directory 125 func (d *DirDoc) AddReferencedBy(ri ...couchdb.DocReference) { 126 for _, ref := range ri { 127 if !containsDocReference(d.ReferencedBy, ref) { 128 d.ReferencedBy = append(d.ReferencedBy, ref) 129 } 130 } 131 } 132 133 // RemoveReferencedBy removes one or several referenced_by to the directory 134 func (d *DirDoc) RemoveReferencedBy(ri ...couchdb.DocReference) { 135 // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating 136 referenced := d.ReferencedBy[:0] 137 for _, ref := range d.ReferencedBy { 138 if !containsDocReference(ri, ref) { 139 referenced = append(referenced, ref) 140 } 141 } 142 d.ReferencedBy = referenced 143 } 144 145 // AddNotSynchronizedOn adds not_synchronized_on to the directory 146 func (d *DirDoc) AddNotSynchronizedOn(refs ...couchdb.DocReference) { 147 for _, ref := range refs { 148 if !containsDocReference(d.NotSynchronizedOn, ref) { 149 d.NotSynchronizedOn = append(d.NotSynchronizedOn, ref) 150 } 151 } 152 } 153 154 // RemoveNotSynchronizedOn removes one or several not_synchronized_on to the 155 // directory 156 func (d *DirDoc) RemoveNotSynchronizedOn(refs ...couchdb.DocReference) { 157 // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating 158 references := d.NotSynchronizedOn[:0] 159 for _, ref := range d.NotSynchronizedOn { 160 if !containsDocReference(refs, ref) { 161 references = append(references, ref) 162 } 163 } 164 d.NotSynchronizedOn = references 165 } 166 167 // NewDirDoc is the DirDoc constructor. The given name is validated. 168 func NewDirDoc(index Indexer, name, dirID string, tags []string) (*DirDoc, error) { 169 if dirID == "" { 170 dirID = consts.RootDirID 171 } 172 173 var dirPath string 174 if dirID == consts.RootDirID { 175 dirPath = "/" 176 } else { 177 parent, err := index.DirByID(dirID) 178 if err != nil { 179 return nil, err 180 } 181 dirPath = parent.Fullpath 182 } 183 184 return NewDirDocWithPath(name, dirID, dirPath, tags) 185 } 186 187 // NewDirDocWithParent returns an instance of DirDoc from a parent document. 188 // The given name is validated. 189 func NewDirDocWithParent(name string, parent *DirDoc, tags []string) (*DirDoc, error) { 190 return NewDirDocWithPath(name, parent.DocID, parent.Fullpath, tags) 191 } 192 193 // NewDirDocWithPath returns an instance of DirDoc its directory ID and path. 194 // The given name is validated. 195 func NewDirDocWithPath(name, dirID, dirPath string, tags []string) (*DirDoc, error) { 196 if err := checkFileName(name); err != nil { 197 return nil, err 198 } 199 200 if dirPath == "" || dirPath == "." { 201 dirPath = "/" 202 } 203 fullpath := path.Join(dirPath, name) 204 if err := checkDepth(fullpath); err != nil { 205 return nil, err 206 } 207 208 createDate := time.Now() 209 return &DirDoc{ 210 Type: consts.DirType, 211 DocName: name, 212 DirID: dirID, 213 214 CreatedAt: createDate, 215 UpdatedAt: createDate, 216 Tags: uniqueTags(tags), 217 Fullpath: fullpath, 218 }, nil 219 } 220 221 // ModifyDirMetadata modify the metadata associated to a directory. It 222 // can be used to rename or move the directory in the VFS. 223 func ModifyDirMetadata(fs VFS, olddoc *DirDoc, patch *DocPatch) (*DirDoc, error) { 224 id := olddoc.ID() 225 if id == consts.RootDirID || id == consts.TrashDirID { 226 return nil, os.ErrInvalid 227 } 228 229 var oldFavorite *bool 230 if olddoc.CozyMetadata != nil { 231 oldFavorite = &olddoc.CozyMetadata.Favorite 232 } 233 234 var err error 235 cdate := olddoc.CreatedAt 236 patch, err = normalizeDocPatch(&DocPatch{ 237 Name: &olddoc.DocName, 238 DirID: &olddoc.DirID, 239 RestorePath: &olddoc.RestorePath, 240 Tags: &olddoc.Tags, 241 UpdatedAt: &olddoc.UpdatedAt, 242 CozyMetadata: CozyMetadataPatch{Favorite: oldFavorite}, 243 }, patch, cdate) 244 245 if err != nil { 246 return nil, err 247 } 248 249 var newdoc *DirDoc 250 if *patch.DirID != olddoc.DirID { 251 if strings.HasPrefix(olddoc.Fullpath, TrashDirName) { 252 return nil, ErrFileInTrash 253 } 254 newdoc, err = NewDirDoc(fs, *patch.Name, *patch.DirID, *patch.Tags) 255 } else { 256 newdoc, err = NewDirDocWithPath(*patch.Name, olddoc.DirID, path.Dir(olddoc.Fullpath), *patch.Tags) 257 } 258 if err != nil { 259 return nil, err 260 } 261 262 newdoc.RestorePath = *patch.RestorePath 263 newdoc.CreatedAt = cdate 264 newdoc.UpdatedAt = *patch.UpdatedAt 265 newdoc.ReferencedBy = olddoc.ReferencedBy 266 newdoc.NotSynchronizedOn = olddoc.NotSynchronizedOn 267 newdoc.Metadata = olddoc.Metadata 268 newdoc.CozyMetadata = olddoc.CozyMetadata 269 if newdoc.CozyMetadata != nil && patch.CozyMetadata.Favorite != nil { 270 newdoc.CozyMetadata.Favorite = *patch.CozyMetadata.Favorite 271 } 272 273 if err = fs.UpdateDirDoc(olddoc, newdoc); err != nil { 274 return nil, err 275 } 276 return newdoc, nil 277 } 278 279 // TrashDir is used to delete a directory given its document 280 func TrashDir(fs VFS, olddoc *DirDoc) (*DirDoc, error) { 281 oldpath, err := olddoc.Path(fs) 282 if err != nil { 283 return nil, err 284 } 285 286 if strings.HasPrefix(oldpath, TrashDirName) { 287 return nil, ErrFileInTrash 288 } 289 290 trashDirID := consts.TrashDirID 291 restorePath := path.Dir(oldpath) 292 293 var newdoc *DirDoc 294 err = tryOrUseSuffix(olddoc.DocName, conflictFormat, func(name string) error { 295 newdoc = olddoc.Clone().(*DirDoc) 296 newdoc.DirID = trashDirID 297 newdoc.RestorePath = restorePath 298 newdoc.DocName = name 299 newdoc.Fullpath = path.Join(TrashDirName, name) 300 newdoc.CozyMetadata = olddoc.CozyMetadata 301 return fs.UpdateDirDoc(olddoc, newdoc) 302 }) 303 if err != nil { 304 return nil, err 305 } 306 return newdoc, nil 307 } 308 309 // RestoreDir is used to restore a trashed directory given its document 310 func RestoreDir(fs VFS, olddoc *DirDoc) (*DirDoc, error) { 311 oldpath, err := olddoc.Path(fs) 312 if err != nil { 313 return nil, err 314 } 315 316 restoreDir, err := getRestoreDir(fs, oldpath, olddoc.RestorePath) 317 if err != nil { 318 return nil, err 319 } 320 321 name := stripConflictSuffix(olddoc.DocName) 322 323 var newdoc *DirDoc 324 err = tryOrUseSuffix(name, conflictFormat, func(name string) error { 325 newdoc = olddoc.Clone().(*DirDoc) 326 newdoc.DirID = restoreDir.DocID 327 newdoc.RestorePath = "" 328 newdoc.DocName = name 329 newdoc.Fullpath = path.Join(restoreDir.Fullpath, name) 330 newdoc.CozyMetadata = olddoc.CozyMetadata 331 return fs.UpdateDirDoc(olddoc, newdoc) 332 }) 333 if err != nil { 334 return nil, err 335 } 336 337 return newdoc, nil 338 } 339 340 // FilterNotSynchronizedDocs filters a changes feed to replace documents in 341 // not_synchronized_on directories with deleted: true entries. 342 func FilterNotSynchronizedDocs(fs VFS, clientID string, changes *couchdb.ChangesResponse) error { 343 if len(changes.Results) == 0 { 344 return nil 345 } 346 347 notSynchronizedDirs, err := fetchNotSynchronizedOn(fs, clientID) 348 if err != nil { 349 return err 350 } 351 if len(notSynchronizedDirs.byID) == 0 { 352 return nil 353 } 354 355 fp := NewFilePatherWithCache(fs.GetIndexer()) 356 for i := range changes.Results { 357 doc := changes.Results[i].Doc 358 if isNotSynchronized(fp, notSynchronizedDirs, doc) { 359 var rev string 360 if len(changes.Results[i].Changes) > 0 { 361 rev = changes.Results[i].Changes[0].Rev 362 } 363 docID := changes.Results[i].DocID 364 changes.Results[i].Doc = couchdb.JSONDoc{ 365 M: map[string]interface{}{ 366 "_id": docID, 367 "_rev": rev, 368 "_deleted": true, 369 }, 370 Type: consts.Files, 371 } 372 changes.Results[i].Deleted = true 373 } 374 } 375 376 return nil 377 } 378 379 type notSynchronizedMap struct { 380 byID map[string]struct{} 381 byPath map[string]struct{} 382 } 383 384 func fetchNotSynchronizedOn(fs VFS, clientID string) (notSynchronizedMap, error) { 385 m := notSynchronizedMap{} 386 docs, err := fs.ListNotSynchronizedOn(clientID) 387 if err != nil || len(docs) == 0 { 388 return m, err 389 } 390 391 m.byID = make(map[string]struct{}) 392 m.byPath = make(map[string]struct{}) 393 for _, doc := range docs { 394 m.byID[doc.DocID] = struct{}{} 395 m.byPath[doc.Fullpath] = struct{}{} 396 } 397 return m, nil 398 } 399 400 func isNotSynchronized(fp FilePather, notSynchronizedDirs notSynchronizedMap, doc couchdb.JSONDoc) bool { 401 docID := doc.ID() 402 if _, ok := notSynchronizedDirs.byID[docID]; ok { 403 return true 404 } 405 406 fpath, _ := doc.M["path"].(string) 407 if doc.M["type"] == consts.FileType { 408 dirID, _ := doc.M["dir_id"].(string) 409 fpath, _ = fp.FilePath(&FileDoc{DocID: docID, DirID: dirID}) 410 } 411 412 for { 413 if _, ok := notSynchronizedDirs.byPath[fpath]; ok { 414 return true 415 } 416 if fpath == "" || fpath == "/" { 417 return false 418 } 419 fpath = filepath.Dir(fpath) 420 } 421 } 422 423 var ( 424 _ couchdb.Doc = &DirDoc{} 425 _ os.FileInfo = &DirDoc{} 426 )