// Copyright (C) 2018 The Syncthing Authors. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. package cli import ( "bytes" "encoding/binary" "errors" "fmt" "sort" "github.com/urfave/cli" "github.com/syncthing/syncthing/lib/db" "github.com/syncthing/syncthing/lib/protocol" ) type fileInfoKey struct { folder uint32 device uint32 name string } type globalKey struct { folder uint32 name string } type sequenceKey struct { folder uint32 sequence uint64 } func indexCheck(*cli.Context) (err error) { ldb, err := getDB() if err != nil { return err } folders := make(map[uint32]string) devices := make(map[uint32]string) deviceToIDs := make(map[string]uint32) fileInfos := make(map[fileInfoKey]protocol.FileInfo) globals := make(map[globalKey]db.VersionList) sequences := make(map[sequenceKey]string) needs := make(map[globalKey]struct{}) blocklists := make(map[string]struct{}) versions := make(map[string]protocol.Vector) usedBlocklists := make(map[string]struct{}) usedVersions := make(map[string]struct{}) var localDeviceKey uint32 success := true defer func() { if err == nil { if success { fmt.Println("Index check completed succesfully.") } else { err = errors.New("Inconsistencies found in the index") } } }() it, err := ldb.NewPrefixIterator(nil) if err != nil { return err } for it.Next() { key := it.Key() switch key[0] { case db.KeyTypeDevice: folder := binary.BigEndian.Uint32(key[1:]) device := binary.BigEndian.Uint32(key[1+4:]) name := nulString(key[1+4+4:]) var f protocol.FileInfo err := f.Unmarshal(it.Value()) if err != nil { fmt.Println("Unable to unmarshal FileInfo:", err) success = false continue } fileInfos[fileInfoKey{folder, device, name}] = f case db.KeyTypeGlobal: folder := binary.BigEndian.Uint32(key[1:]) name := nulString(key[1+4:]) var flv db.VersionList if err := flv.Unmarshal(it.Value()); err != nil { fmt.Println("Unable to unmarshal VersionList:", err) success = false continue } globals[globalKey{folder, name}] = flv case db.KeyTypeFolderIdx: key := binary.BigEndian.Uint32(it.Key()[1:]) folders[key] = string(it.Value()) case db.KeyTypeDeviceIdx: key := binary.BigEndian.Uint32(it.Key()[1:]) devices[key] = string(it.Value()) deviceToIDs[string(it.Value())] = key if bytes.Equal(it.Value(), protocol.LocalDeviceID[:]) { localDeviceKey = key } case db.KeyTypeSequence: folder := binary.BigEndian.Uint32(key[1:]) seq := binary.BigEndian.Uint64(key[5:]) val := it.Value() sequences[sequenceKey{folder, seq}] = string(val[9:]) case db.KeyTypeNeed: folder := binary.BigEndian.Uint32(key[1:]) name := nulString(key[1+4:]) needs[globalKey{folder, name}] = struct{}{} case db.KeyTypeBlockList: hash := string(key[1:]) blocklists[hash] = struct{}{} case db.KeyTypeVersion: hash := string(key[1:]) var v protocol.Vector if err := v.Unmarshal(it.Value()); err != nil { fmt.Println("Unable to unmarshal Vector:", err) success = false continue } versions[hash] = v } } if localDeviceKey == 0 { fmt.Println("Missing key for local device in device index (bailing out)") success = false return } var missingSeq []sequenceKey for fk, fi := range fileInfos { if fk.name != fi.Name { fmt.Printf("Mismatching FileInfo name, %q (key) != %q (actual)\n", fk.name, fi.Name) success = false } folder := folders[fk.folder] if folder == "" { fmt.Printf("Unknown folder ID %d for FileInfo %q\n", fk.folder, fk.name) success = false continue } if devices[fk.device] == "" { fmt.Printf("Unknown device ID %d for FileInfo %q, folder %q\n", fk.folder, fk.name, folder) success = false } if fk.device == localDeviceKey { sk := sequenceKey{fk.folder, uint64(fi.Sequence)} name, ok := sequences[sk] if !ok { fmt.Printf("Sequence entry missing for FileInfo %q, folder %q, seq %d\n", fi.Name, folder, fi.Sequence) missingSeq = append(missingSeq, sk) success = false continue } if name != fi.Name { fmt.Printf("Sequence entry refers to wrong name, %q (seq) != %q (FileInfo), folder %q, seq %d\n", name, fi.Name, folder, fi.Sequence) success = false } } if len(fi.Blocks) == 0 && len(fi.BlocksHash) != 0 { key := string(fi.BlocksHash) if _, ok := blocklists[key]; !ok { fmt.Printf("Missing block list for file %q, block list hash %x\n", fi.Name, fi.BlocksHash) success = false } else { usedBlocklists[key] = struct{}{} } } if fi.VersionHash != nil { key := string(fi.VersionHash) if _, ok := versions[key]; !ok { fmt.Printf("Missing version vector for file %q, version hash %x\n", fi.Name, fi.VersionHash) success = false } else { usedVersions[key] = struct{}{} } } _, ok := globals[globalKey{fk.folder, fk.name}] if !ok { fmt.Printf("Missing global for file %q\n", fi.Name) success = false continue } } // Aggregate the ranges of missing sequence entries, print them sort.Slice(missingSeq, func(a, b int) bool { if missingSeq[a].folder != missingSeq[b].folder { return missingSeq[a].folder < missingSeq[b].folder } return missingSeq[a].sequence < missingSeq[b].sequence }) var folder uint32 var startSeq, prevSeq uint64 for _, sk := range missingSeq { if folder != sk.folder || sk.sequence != prevSeq+1 { if folder != 0 { fmt.Printf("Folder %d missing %d sequence entries: #%d - #%d\n", folder, prevSeq-startSeq+1, startSeq, prevSeq) } startSeq = sk.sequence folder = sk.folder } prevSeq = sk.sequence } if folder != 0 { fmt.Printf("Folder %d missing %d sequence entries: #%d - #%d\n", folder, prevSeq-startSeq+1, startSeq, prevSeq) } for gk, vl := range globals { folder := folders[gk.folder] if folder == "" { fmt.Printf("Unknown folder ID %d for VersionList %q\n", gk.folder, gk.name) success = false } checkGlobal := func(i int, device []byte, version protocol.Vector, invalid, deleted bool) { dev, ok := deviceToIDs[string(device)] if !ok { fmt.Printf("VersionList %q, folder %q refers to unknown device %q\n", gk.name, folder, device) success = false } fi, ok := fileInfos[fileInfoKey{gk.folder, dev, gk.name}] if !ok { fmt.Printf("VersionList %q, folder %q, entry %d refers to unknown FileInfo\n", gk.name, folder, i) success = false } fiv := fi.Version if fi.VersionHash != nil { fiv = versions[string(fi.VersionHash)] } if !fiv.Equal(version) { fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo version mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, version, fi.Version) success = false } if fi.IsInvalid() != invalid { fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo invalid mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, invalid, fi.IsInvalid()) success = false } if fi.IsDeleted() != deleted { fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo deleted mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, deleted, fi.IsDeleted()) success = false } } for i, fv := range vl.RawVersions { for _, device := range fv.Devices { checkGlobal(i, device, fv.Version, false, fv.Deleted) } for _, device := range fv.InvalidDevices { checkGlobal(i, device, fv.Version, true, fv.Deleted) } } // If we need this file we should have a need entry for it. False // positives from needsLocally for deleted files, where we might // legitimately lack an entry if we never had it, and ignored files. if needsLocally(vl) { _, ok := needs[gk] if !ok { fv, _ := vl.GetGlobal() devB, _ := fv.FirstDevice() dev := deviceToIDs[string(devB)] fi := fileInfos[fileInfoKey{gk.folder, dev, gk.name}] if !fi.IsDeleted() && !fi.IsIgnored() { fmt.Printf("Missing need entry for needed file %q, folder %q\n", gk.name, folder) } } } } seenSeq := make(map[fileInfoKey]uint64) for sk, name := range sequences { folder := folders[sk.folder] if folder == "" { fmt.Printf("Unknown folder ID %d for sequence entry %d, %q\n", sk.folder, sk.sequence, name) success = false continue } if prev, ok := seenSeq[fileInfoKey{folder: sk.folder, name: name}]; ok { fmt.Printf("Duplicate sequence entry for %q, folder %q, seq %d (prev %d)\n", name, folder, sk.sequence, prev) success = false } seenSeq[fileInfoKey{folder: sk.folder, name: name}] = sk.sequence fi, ok := fileInfos[fileInfoKey{sk.folder, localDeviceKey, name}] if !ok { fmt.Printf("Missing FileInfo for sequence entry %d, folder %q, %q\n", sk.sequence, folder, name) success = false continue } if fi.Sequence != int64(sk.sequence) { fmt.Printf("Sequence mismatch for %q, folder %q, %d (key) != %d (FileInfo)\n", name, folder, sk.sequence, fi.Sequence) success = false } } for nk := range needs { folder := folders[nk.folder] if folder == "" { fmt.Printf("Unknown folder ID %d for need entry %q\n", nk.folder, nk.name) success = false continue } vl, ok := globals[nk] if !ok { fmt.Printf("Missing global for need entry %q, folder %q\n", nk.name, folder) success = false continue } if !needsLocally(vl) { fmt.Printf("Need entry for file we don't need, %q, folder %q\n", nk.name, folder) success = false } } if d := len(blocklists) - len(usedBlocklists); d > 0 { fmt.Printf("%d block list entries out of %d needs GC\n", d, len(blocklists)) } if d := len(versions) - len(usedVersions); d > 0 { fmt.Printf("%d version entries out of %d needs GC\n", d, len(versions)) } return nil } func needsLocally(vl db.VersionList) bool { gfv, gok := vl.GetGlobal() if !gok { // That's weird, but we hardly need something non-existant return false } fv, ok := vl.Get(protocol.LocalDeviceID[:]) return db.Need(gfv, ok, fv.Version) }