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 }