2 glocate -- ZFS-diff-friendly locate-like utility
3 Copyright (C) 2022 Sergey Matveev <stargrave@stargrave.org>
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
36 "github.com/dustin/go-humanize"
37 "github.com/klauspost/compress/zstd"
49 func (a ByName) Len() int {
53 func (a ByName) Swap(i, j int) {
54 a[i], a[j] = a[j], a[i]
57 func (a ByName) Less(i, j int) bool {
58 return a[i].Name < a[j].Name
61 func (file *File) IsDir() bool {
62 return file.Name[len(file.Name)-1] == '/'
65 func walk(root string) ([]File, uint64, error) {
66 fd, err := os.Open(root)
74 ents, err := fd.ReadDir(1 << 10)
82 for _, ent := range ents {
83 file := File{Name: ent.Name()}
84 fullPath := path.Join(root, file.Name)
88 info, err = ent.Info()
90 log.Println("can not stat:", fullPath, ":", err)
91 files = append(files, file)
94 file.Mtime = info.ModTime().Unix()
96 file.Files, file.Size, err = walk(fullPath)
98 log.Println("can not walk:", fullPath, ":", err)
99 files = append(files, file)
102 } else if info.Mode().IsRegular() {
103 file.Size = uint64(info.Size())
105 files = append(files, file)
110 sort.Sort(ByName(files))
111 return files, size, nil
119 func load(dbPath string) *File {
120 fd, err := os.Open(dbPath)
125 comp, err := zstd.NewReader(fd)
129 dec := gob.NewDecoder(comp)
131 err = dec.Decode(&file)
139 func (db *File) dump(dbPath string) error {
140 tmp, err := os.CreateTemp(path.Dir(dbPath), "glocate")
144 defer os.Remove(tmp.Name())
145 comp, err := zstd.NewWriter(
146 tmp, zstd.WithEncoderLevel(zstd.SpeedBestCompression),
151 enc := gob.NewEncoder(comp)
164 umask := syscall.Umask(0)
166 err = os.Chmod(tmp.Name(), os.FileMode(0666&^umask))
170 return os.Rename(tmp.Name(), dbPath)
173 func (file *File) listBeauty(indent string, n int, isLast, veryFirst bool) {
175 fmt.Printf("[%s]\n", humanize.IBytes(file.Size))
184 fmt.Printf("%s%s %s\tâ%d [%s] %s\n",
185 indent, box, name, n, humanize.IBytes(file.Size),
186 time.Unix(file.Mtime, 0).Format("2006-01-02"),
194 for n, f := range file.Files {
196 f.listBeauty(indent, n, n == len(file.Files), false)
200 func (file *File) listSimple(root string, veryFirst bool) {
203 strconv.FormatUint(file.Size, 10),
204 time.Unix(file.Mtime, 0).Format("2006-01-02T15:04:05"),
210 for _, f := range file.Files {
211 f.listSimple(root+name, false)
215 func (file *File) listFiles(root string, veryFirst bool) {
220 fmt.Println(root + name)
223 for _, f := range file.Files {
224 f.listFiles(root, false)
228 func (db *File) find(p string) (file *File, parents []*File, idx int, err error) {
232 for _, ent := range strings.Split(p, "/") {
233 for idx, f = range file.Files {
234 if (ent == f.Name) || (ent+"/" == f.Name) {
235 parents = append(parents, file)
240 err = fmt.Errorf("no entity found: %s", ent)
246 func (db *File) remove(p string) error {
247 file, parents, idx, err := db.find(p)
251 lastParent := parents[len(parents)-1]
252 lastParent.Files = append(
253 lastParent.Files[:idx],
254 lastParent.Files[idx+1:]...,
256 for _, parent := range parents {
257 parent.Size -= file.Size
262 func (db *File) add(p string) error {
263 cols := strings.Split(p, "/")
264 cols, name := cols[:len(cols)-1], cols[len(cols)-1]
268 parent, _, _, err = db.find(path.Join(cols...))
275 info, err := os.Stat(p)
284 Size: uint64(info.Size()),
285 Mtime: info.ModTime().Unix(),
287 parent.Files = append(parent.Files, file)
288 sort.Sort(ByName(parent.Files))
289 parent.Size += file.Size
293 func deoctalize(s string) string {
294 chars := make([]byte, 0, len(s))
295 for i := 0; i < len(s); i++ {
297 b, err := strconv.ParseUint("0"+s[i+1:i+1+3], 0, 8)
301 chars = append(chars, byte(b))
304 chars = append(chars, s[i])
311 dbPath := flag.String("db", ".glocate.db", "Path to state file (database)")
312 doIndex := flag.Bool("index", false, "Initialize database")
313 doUpdate := flag.Bool("update", false, "Update database by zfs-diff's output")
314 showBeauty := flag.Bool("show-beauty", false, "Show beauty human-friendly listing")
315 showSimple := flag.Bool("show-simple", false, "Show simple listing")
316 stripPrefix := flag.String("strip-prefix", "", "Strip prefix from zfs-diff's output")
318 log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile)
321 files, size, err := walk(".")
325 db := File{Name: "./", Size: size, Files: files}
326 err = db.dump(*dbPath)
335 scanner := bufio.NewScanner(os.Stdin)
342 cols := strings.Split(t, "\t")
344 log.Fatalln("bad zfs-diff format")
348 name := deoctalize(strings.TrimPrefix(cols[1], *stripPrefix))
349 if err := db.remove(name); err != nil {
350 log.Println("can not -:", name, ":", err)
353 name := deoctalize(strings.TrimPrefix(cols[1], *stripPrefix))
354 if err := db.add(name); err != nil {
355 log.Println("can not +:", name, ":", err)
358 name := deoctalize(strings.TrimPrefix(cols[1], *stripPrefix))
362 file, _, _, err := db.find(name)
364 log.Println("can not M:", name, ":", err)
367 info, err := os.Stat(name)
369 log.Println("can not M:", name, ":", err)
372 if info.Mode().IsRegular() {
373 file.Size = uint64(info.Size())
375 file.Mtime = info.ModTime().Unix()
378 log.Fatalln("bad zfs-diff format for R")
380 name := deoctalize(strings.TrimPrefix(cols[1], *stripPrefix))
381 if err := db.remove(name); err != nil {
382 log.Println("can not R-:", name, ":", err)
385 name = deoctalize(strings.TrimPrefix(cols[2], *stripPrefix))
386 if err := db.add(name); err != nil {
387 log.Println("can not R+:", name, ":", err)
390 log.Fatalln("bad zfs-diff format")
393 if err := scanner.Err(); err != nil {
396 if err := db.dump(*dbPath); err != nil {
403 if len(flag.Args()) > 0 {
404 root := flag.Args()[0]
405 if root[:2] == "./" {
408 if root[len(root)-1:] == "/" {
409 root = root[:len(root)-1]
411 file, _, _, err := db.find(root)
421 db.listBeauty("", 0, false, veryFirst)
425 db.listSimple("", veryFirst)
428 db.listFiles("", veryFirst)