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  )