feat: add debug commands for folder counts and files (#10206)

This adds two debugging commands that print information directly from
the database; one for folder counts, and one for file metadata for files
matching a pattern in a folder. E.g.,

```
% syncthing debug database-counts p3jms-73gps
DEVICE   TYPE       FLAGS    DELETED  COUNT  SIZE
-local-  FILE       -------  ---      0      0
-local-  FILE       --G----  ---      2473   70094796496
-local-  DIRECTORY  -------  ---      0      0
-local-  DIRECTORY  --G----  ---      19     2432
PSEUDOP  FILE       -------  ---      2473   70094796496
PSEUDOP  FILE       -nG----  ---      0      0
PSEUDOP  DIRECTORY  -------  ---      19     2432
PSEUDOP  DIRECTORY  -nG----  ---      0      0
```

```
% syncthing debug database-file p3jms-73gps 20240929-DSCF1387
DSCF1387
DEVICE   TYPE  NAME                          SEQUENCE  DELETED  MODIFIED                        SIZE      FLAGS    VERSION             BLOCKLIST
-local-  FILE  Austin/20240929-DSCF1387.raf  1204      ---      2024-09-29T01:10:54Z            48911888  --G----  HX2ELNU:1744213700  fsQdMvUL
PSEUDOP  FILE  Austin/20240929-DSCF1387.raf  22279     ---      2024-09-29T01:10:54Z            48911888  -------  HX2ELNU:1744213700  fsQdMvUL
-local-  FILE  Austin/20240929-DSCF1387.xmp  1196      ---      2024-10-16T08:08:35.137501751Z  5579      --G----  HX2ELNU:1744213700  xDGMnepi
PSEUDOP  FILE  Austin/20240929-DSCF1387.xmp  19910     ---      2024-10-16T08:08:35.137501751Z  5579      -------  HX2ELNU:1744213700  xDGMnepi
```

The local flag bits get a string representation for the bitmask,

```
	FlagLocalUnsupported:   "u",
	FlagLocalIgnored:       "i",
	FlagLocalMustRescan:    "r",
	FlagLocalReceiveOnly:   "e",
	FlagLocalGlobal:        "G",
	FlagLocalNeeded:        "n",
	FlagLocalRemoteInvalid: "v",
```
This commit is contained in:
Jakob Borg
2025-07-04 15:46:24 +02:00
committed by GitHub
parent 06dd8ee6d7
commit ff88430efb
7 changed files with 189 additions and 3 deletions

View File

@@ -10,6 +10,7 @@ import (
"database/sql"
"errors"
"fmt"
"io"
"iter"
"path/filepath"
"strings"
@@ -376,6 +377,22 @@ func (s *DB) DropDevice(device protocol.DeviceID) error {
})
}
func (s *DB) DebugCounts(out io.Writer, folder string) error {
fdb, err := s.getFolderDB(folder, false)
if err != nil {
return err
}
return fdb.DebugCounts(out)
}
func (s *DB) DebugFilePattern(out io.Writer, folder, name string) error {
fdb, err := s.getFolderDB(folder, false)
if err != nil {
return err
}
return fdb.DebugFilePattern(out, name)
}
// forEachFolder runs the function for each currently open folderDB,
// returning the first error that was encountered.
func (s *DB) forEachFolder(fn func(fdb *folderDB) error) error {

View File

@@ -16,7 +16,7 @@ type countsRow struct {
Count int
Size int64
Deleted bool
LocalFlags int64 `db:"local_flags"`
LocalFlags protocol.FlagLocal `db:"local_flags"`
}
func (s *folderDB) CountLocal(device protocol.DeviceID) (db.Counts, error) {

View File

@@ -8,9 +8,14 @@ package sqlite
import (
"database/sql"
"encoding/base64"
"errors"
"fmt"
"io"
"iter"
"strings"
"text/tabwriter"
"time"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/itererr"
@@ -126,3 +131,82 @@ func (s *folderDB) ListDevicesForFolder() ([]protocol.DeviceID, error) {
}
return devs, nil
}
func (s *folderDB) DebugCounts(out io.Writer) error {
type deviceCountsRow struct {
countsRow
DeviceID string
}
delMap := map[bool]string{
true: "del",
false: "---",
}
var res []deviceCountsRow
if err := s.stmt(`
SELECT d.device_id as deviceid, s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
INNER JOIN devices d ON d.idx = s.device_idx
`).Select(&res); err != nil {
return wrap(err)
}
tw := tabwriter.NewWriter(out, 2, 2, 2, ' ', 0)
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n", "DEVICE", "TYPE", "FLAGS", "DELETED", "COUNT", "SIZE")
for _, row := range res {
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%d\t%d\n", shortDevice(row.DeviceID), shortType(row.Type), row.LocalFlags.HumanString(), delMap[row.Deleted], row.Count, row.Size)
}
return tw.Flush()
}
func (s *folderDB) DebugFilePattern(out io.Writer, name string) error {
type hashFileMetadata struct {
db.FileMetadata
Version dbVector
BlocklistHash []byte
DeviceID string
}
name = "%" + name + "%"
res := itererr.Zip(iterStructs[hashFileMetadata](s.stmt(`
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.local_flags as localflags, f.version, f.blocklist_hash as blocklisthash, d.device_id as deviceid FROM files f
INNER JOIN devices d ON d.idx = f.device_idx
WHERE f.name LIKE ?
ORDER BY f.name, f.device_idx
`).Queryx(name)))
delMap := map[bool]string{
true: "del",
false: "---",
}
tw := tabwriter.NewWriter(out, 2, 2, 2, ' ', 0)
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", "DEVICE", "TYPE", "NAME", "SEQUENCE", "DELETED", "MODIFIED", "SIZE", "FLAGS", "VERSION", "BLOCKLIST")
for row, err := range res {
if err != nil {
return err
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%d\t%s\t%s\t%d\t%s\t%s\t%s\n", shortDevice(row.DeviceID), shortType(row.Type), row.Name, row.Sequence, delMap[row.Deleted], row.ModTime().UTC().Format(time.RFC3339Nano), row.Size, row.LocalFlags.HumanString(), row.Version.HumanString(), shortHash(row.BlocklistHash))
}
return tw.Flush()
}
func shortDevice(s string) string {
if dev, err := protocol.DeviceIDFromString(s); err == nil && dev == protocol.LocalDeviceID {
return "-local-"
}
short, _, _ := strings.Cut(s, "-")
return short
}
func shortType(t protocol.FileInfoType) string {
return strings.TrimPrefix(t.String(), "FILE_INFO_TYPE_")
}
func shortHash(bs []byte) string {
if len(bs) == 0 {
return "-nil-"
}
return base64.RawStdEncoding.EncodeToString(bs)[:8]
}