github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/vfs/vfsafero/fsck.go (about)

     1  package vfsafero
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"encoding/json"
     7  	"errors"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/cozy/cozy-stack/model/vfs"
    14  	"github.com/cozy/cozy-stack/pkg/consts"
    15  	"github.com/cozy/cozy-stack/pkg/couchdb"
    16  	"github.com/spf13/afero"
    17  )
    18  
    19  var errFailFast = errors.New("fail fast")
    20  
    21  func (afs *aferoVFS) Fsck(accumulate func(log *vfs.FsckLog), failFast bool) error {
    22  	entries := make(map[string]*vfs.TreeFile, 1024)
    23  	tree, err := afs.BuildTree(func(f *vfs.TreeFile) {
    24  		if !f.IsOrphan {
    25  			entries[f.Fullpath] = f
    26  		}
    27  	})
    28  	if err != nil {
    29  		return err
    30  	}
    31  	if err = afs.CheckTreeIntegrity(tree, accumulate, failFast); err != nil {
    32  		if errors.Is(err, vfs.ErrFsckFailFast) {
    33  			return nil
    34  		}
    35  		return err
    36  	}
    37  	return afs.checkFiles(entries, accumulate, failFast)
    38  }
    39  
    40  func (afs *aferoVFS) CheckFilesConsistency(accumulate func(log *vfs.FsckLog), failFast bool) error {
    41  	entries := make(map[string]*vfs.TreeFile, 1024)
    42  	_, err := afs.BuildTree(func(f *vfs.TreeFile) {
    43  		if !f.IsOrphan {
    44  			entries[f.Fullpath] = f
    45  		}
    46  	})
    47  	if err != nil {
    48  		return err
    49  	}
    50  	return afs.checkFiles(entries, accumulate, failFast)
    51  }
    52  
    53  func (afs *aferoVFS) checkFiles(
    54  	entries map[string]*vfs.TreeFile,
    55  	accumulate func(log *vfs.FsckLog),
    56  	failFast bool,
    57  ) error {
    58  	versions := make(map[string]*vfs.Version, 1024)
    59  	err := couchdb.ForeachDocs(afs, consts.FilesVersions, func(_ string, data json.RawMessage) error {
    60  		v := &vfs.Version{}
    61  		if erru := json.Unmarshal(data, v); erru != nil {
    62  			return erru
    63  		}
    64  		versions[pathForVersion(v)] = v
    65  		return nil
    66  	})
    67  	if err != nil {
    68  		return err
    69  	}
    70  
    71  	err = afero.Walk(afs.fs, "/", func(fullpath string, info os.FileInfo, err error) error {
    72  		if err != nil {
    73  			return err
    74  		}
    75  
    76  		if fullpath == vfs.WebappsDirName ||
    77  			fullpath == vfs.KonnectorsDirName ||
    78  			fullpath == vfs.ThumbsDirName {
    79  			return filepath.SkipDir
    80  		}
    81  
    82  		if strings.HasPrefix(fullpath, vfs.VersionsDirName) {
    83  			if info.IsDir() {
    84  				return nil
    85  			}
    86  			_, ok := versions[fullpath]
    87  			if !ok {
    88  				accumulate(&vfs.FsckLog{
    89  					Type:       vfs.IndexMissing,
    90  					IsVersion:  true,
    91  					VersionDoc: fileInfosToVersionDoc(fullpath, info),
    92  				})
    93  			}
    94  			delete(versions, fullpath)
    95  			return nil
    96  		}
    97  
    98  		f, ok := entries[fullpath]
    99  		if !ok {
   100  			accumulate(&vfs.FsckLog{
   101  				Type:    vfs.IndexMissing,
   102  				IsFile:  true,
   103  				FileDoc: fileInfosToFileDoc(fullpath, info),
   104  			})
   105  			if failFast {
   106  				return errFailFast
   107  			}
   108  		} else if f.IsDir != info.IsDir() {
   109  			if f.IsDir {
   110  				accumulate(&vfs.FsckLog{
   111  					Type:    vfs.TypeMismatch,
   112  					IsFile:  true,
   113  					FileDoc: f,
   114  					DirDoc:  fileInfosToDirDoc(fullpath, info),
   115  				})
   116  			} else {
   117  				accumulate(&vfs.FsckLog{
   118  					Type:    vfs.TypeMismatch,
   119  					IsFile:  false,
   120  					DirDoc:  f,
   121  					FileDoc: fileInfosToFileDoc(fullpath, info),
   122  				})
   123  			}
   124  			if failFast {
   125  				return errFailFast
   126  			}
   127  		} else if !f.IsDir {
   128  			var fd afero.File
   129  			fd, err = afs.fs.Open(fullpath)
   130  			if err != nil {
   131  				return err
   132  			}
   133  			h := md5.New()
   134  			if _, err = io.Copy(h, fd); err != nil {
   135  				fd.Close()
   136  				return err
   137  			}
   138  			if err = fd.Close(); err != nil {
   139  				return err
   140  			}
   141  			md5sum := h.Sum(nil)
   142  			if !bytes.Equal(md5sum, f.MD5Sum) || f.ByteSize != info.Size() {
   143  				accumulate(&vfs.FsckLog{
   144  					Type:    vfs.ContentMismatch,
   145  					IsFile:  true,
   146  					FileDoc: f,
   147  					ContentMismatch: &vfs.FsckContentMismatch{
   148  						SizeFile:    info.Size(),
   149  						SizeIndex:   f.ByteSize,
   150  						MD5SumFile:  md5sum,
   151  						MD5SumIndex: f.MD5Sum,
   152  					},
   153  				})
   154  				if failFast {
   155  					return errFailFast
   156  				}
   157  			}
   158  		}
   159  		delete(entries, fullpath)
   160  		return nil
   161  	})
   162  	if err != nil {
   163  		if errors.Is(err, errFailFast) {
   164  			return nil
   165  		}
   166  		return err
   167  	}
   168  
   169  	for _, f := range entries {
   170  		if f.IsDir {
   171  			accumulate(&vfs.FsckLog{
   172  				Type:   vfs.FSMissing,
   173  				IsFile: false,
   174  				DirDoc: f,
   175  			})
   176  		} else {
   177  			accumulate(&vfs.FsckLog{
   178  				Type:    vfs.FSMissing,
   179  				IsFile:  true,
   180  				FileDoc: f,
   181  			})
   182  		}
   183  		if failFast {
   184  			return nil
   185  		}
   186  	}
   187  
   188  	for _, v := range versions {
   189  		accumulate(&vfs.FsckLog{
   190  			Type:       vfs.FSMissing,
   191  			IsVersion:  true,
   192  			VersionDoc: v,
   193  		})
   194  		if failFast {
   195  			return nil
   196  		}
   197  	}
   198  
   199  	return nil
   200  }
   201  
   202  func fileInfosToDirDoc(fullpath string, fileinfo os.FileInfo) *vfs.TreeFile {
   203  	return &vfs.TreeFile{
   204  		DirOrFileDoc: vfs.DirOrFileDoc{
   205  			DirDoc: &vfs.DirDoc{
   206  				Type:      consts.DirType,
   207  				DocName:   fileinfo.Name(),
   208  				DirID:     "",
   209  				CreatedAt: fileinfo.ModTime(),
   210  				UpdatedAt: fileinfo.ModTime(),
   211  				Fullpath:  fullpath,
   212  			},
   213  		},
   214  	}
   215  }
   216  
   217  func fileInfosToFileDoc(fullpath string, fileinfo os.FileInfo) *vfs.TreeFile {
   218  	trashed := strings.HasPrefix(fullpath, vfs.TrashDirName)
   219  	contentType, md5sum, _ := extractContentTypeAndMD5(fullpath)
   220  	mime, class := vfs.ExtractMimeAndClass(contentType)
   221  	return &vfs.TreeFile{
   222  		DirOrFileDoc: vfs.DirOrFileDoc{
   223  			DirDoc: &vfs.DirDoc{
   224  				Type:      consts.FileType,
   225  				DocName:   fileinfo.Name(),
   226  				DirID:     "",
   227  				CreatedAt: fileinfo.ModTime(),
   228  				UpdatedAt: fileinfo.ModTime(),
   229  				Fullpath:  fullpath,
   230  			},
   231  			ByteSize:   fileinfo.Size(),
   232  			Mime:       mime,
   233  			Class:      class,
   234  			Executable: int(fileinfo.Mode()|0111) > 0,
   235  			MD5Sum:     md5sum,
   236  			Trashed:    trashed,
   237  		},
   238  	}
   239  }
   240  
   241  func fileInfosToVersionDoc(fullpath string, fileinfo os.FileInfo) *vfs.Version {
   242  	_, md5sum, _ := extractContentTypeAndMD5(fullpath)
   243  	v := &vfs.Version{
   244  		UpdatedAt: fileinfo.ModTime(),
   245  		ByteSize:  fileinfo.Size(),
   246  		MD5Sum:    md5sum,
   247  	}
   248  	parts := strings.Split(fullpath, "/")
   249  	var fileID string
   250  	if len(parts) > 3 {
   251  		fileID = parts[len(parts)-3] + parts[len(parts)-2]
   252  	}
   253  	v.DocID = fileID + "/" + parts[len(parts)-1]
   254  	v.Rels.File.Data.ID = fileID
   255  	v.Rels.File.Data.Type = consts.Files
   256  	return v
   257  }