]> Sergey Matveev's repositories - glocate.git/blob - diff.go
Newer ZFS has different octalize rule
[glocate.git] / diff.go
1 /*
2 glocate -- ZFS-diff-friendly locate-like utility
3 Copyright (C) 2022-2023 Sergey Matveev <stargrave@stargrave.org>
4
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.
8
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.
13
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/>.
16 */
17
18 package main
19
20 import (
21         "bufio"
22         "io"
23         "log"
24         "os"
25         "sort"
26         "strings"
27 )
28
29 type Ren struct {
30         src []string
31         dst []string
32 }
33
34 type BySrc []Ren
35
36 func (a BySrc) Len() int {
37         return len(a)
38 }
39
40 func (a BySrc) Swap(i, j int) {
41         a[i], a[j] = a[j], a[i]
42 }
43
44 func (a BySrc) Less(i, j int) bool {
45         return namesCmp(a[i].src, a[j].src) < 0
46 }
47
48 type EntByName []*Ent
49
50 func (a EntByName) Len() int {
51         return len(a)
52 }
53
54 func (a EntByName) Swap(i, j int) {
55         a[i], a[j] = a[j], a[i]
56 }
57
58 func (a EntByName) Less(i, j int) bool {
59         return namesCmp(a[i].name, a[j].name) < 0
60 }
61
62 func updateWithDiff(dbPath, strip string) *os.File {
63         scanner := bufio.NewScanner(os.Stdin)
64         var t string
65         var delsNames []string
66         var addsNames []string
67         var modsNames []string
68         var rens []Ren
69         var isDir bool
70         for scanner.Scan() {
71                 t = scanner.Text()
72                 if len(t) == 0 {
73                         continue
74                 }
75                 cols := strings.Split(t, "\t")
76                 if len(cols) < 3 {
77                         log.Fatalln("bad zfs-diff format")
78                 }
79                 isDir = cols[1] == "/"
80                 name := deoctalize(strings.TrimPrefix(cols[2], strip))
81                 name = strings.TrimRight(name, "/")
82                 if name == "" {
83                         continue
84                 }
85                 name = "./" + name
86                 if isDir {
87                         name += "/"
88                 }
89                 switch cols[0] {
90                 case "-":
91                         delsNames = append(delsNames, name)
92                 case "+":
93                         addsNames = append(addsNames, name)
94                 case "M":
95                         modsNames = append(modsNames, name)
96                 case "R":
97                         if len(cols) != 4 {
98                                 log.Fatalln("bad zfs-diff format for R")
99                         }
100                         dst := "./" + deoctalize(strings.TrimPrefix(cols[3], strip))
101                         if isDir {
102                                 dst += "/"
103                                 rens = append(rens, Ren{
104                                         src: nameSplit(name),
105                                         dst: nameSplit(dst),
106                                 })
107                         } else {
108                                 delsNames = append(delsNames, name)
109                                 addsNames = append(addsNames, dst)
110                         }
111                 default:
112                         log.Fatalln("bad zfs-diff format")
113                 }
114         }
115
116         entsReader := make(chan Ent, 1<<10)
117         db, err := os.Open(dbPath)
118         if err != nil {
119                 log.Fatalln(err)
120         }
121         dels := make([][]string, 0, len(delsNames)+len(rens))
122         adds := make([][]string, 0, len(addsNames)+len(rens))
123         mods := make([]*Ent, 0, len(modsNames)+len(rens))
124         if len(rens) > 0 {
125                 sort.Sort(BySrc(rens))
126                 go reader(db, entsReader)
127                 var ent Ent
128                 var ok, met bool
129                 for {
130                         ent, ok = <-entsReader
131                         if !ok {
132                                 break
133                         }
134                 Retry:
135                         if len(rens) > 0 {
136                                 if hasPrefix(ent.name, rens[0].src) {
137                                         dels = append(dels, ent.name)
138                                         dst := append(
139                                                 append([]string{}, rens[0].dst...),
140                                                 ent.name[len(rens[0].src):]...,
141                                         )
142                                         adds = append(adds, dst)
143                                         mods = append(mods, &Ent{name: dst})
144                                         if !met {
145                                                 // strip "/" from prefix directory
146                                                 dst := rens[0].dst
147                                                 last := dst[len(dst)-1]
148                                                 dst[len(dst)-1] = last[:len(last)-1]
149                                                 met = true
150                                         }
151                                 } else if met {
152                                         met = false
153                                         rens = rens[1:]
154                                         goto Retry
155                                 }
156                         }
157                 }
158                 rens = nil
159         }
160
161         for _, name := range delsNames {
162                 dels = append(dels, nameSplit(name))
163         }
164         delsNames = nil
165         sort.Sort(ByName(dels))
166
167         for _, name := range addsNames {
168                 adds = append(adds, nameSplit(name))
169                 modsNames = append(modsNames, name)
170         }
171         addsNames = nil
172         sort.Sort(ByName(adds))
173
174         for _, name := range modsNames {
175                 mods = append(mods, &Ent{name: nameSplit(name)})
176         }
177         modsNames = nil
178         sort.Sort(EntByName(mods))
179         var info os.FileInfo
180         for _, ent := range mods {
181                 info, err = os.Stat(nameJoin(ent.name))
182                 if err != nil {
183                         log.Println("can not stat:", nameJoin(ent.name), ":", err)
184                         continue
185                 }
186                 if info.Mode().IsRegular() {
187                         ent.size = info.Size()
188                 }
189                 ent.mtime = info.ModTime().Unix()
190         }
191
192         _, err = db.Seek(0, io.SeekStart)
193         if err != nil {
194                 log.Fatalln(err)
195         }
196         tmp0, err := os.CreateTemp(TmpDir, "glocate-idx")
197         if err != nil {
198                 log.Fatalln(err)
199         }
200         defer os.Remove(tmp0.Name())
201         entsReader = make(chan Ent, 1<<10)
202         entsDirSizer := make(chan Ent, 1<<10)
203         entsWriter := make(chan Ent, 1<<10)
204         go reader(db, entsReader)
205
206         dirSizerJob := make(chan struct{})
207         var dirSizes []int64
208         sinkBack := make(chan Ent, 1)
209         go func() {
210                 dirSizer(&dirSizes, 1, sinkBack, entsDirSizer, entsWriter)
211                 close(dirSizerJob)
212         }()
213
214         writerJob := make(chan struct{})
215         go func() {
216                 writer(tmp0, entsWriter)
217                 close(writerJob)
218         }()
219
220         for ent := range entsReader {
221                 if len(dels) > 0 && namesCmp(ent.name, dels[0]) == 0 {
222                         dels = dels[1:]
223                         continue
224                 }
225                 for len(adds) > 0 && namesCmp(adds[0], ent.name) < 0 {
226                         if namesCmp(mods[0].name, adds[0]) != 0 {
227                                 panic("+ and M lists are out of sync")
228                         }
229                         newEnt := Ent{
230                                 name:  adds[0],
231                                 mtime: mods[0].mtime,
232                                 size:  mods[0].size,
233                         }
234                         entsDirSizer <- newEnt
235                         adds = adds[1:]
236                         mods = mods[1:]
237                 }
238                 if len(mods) > 0 && namesCmp(ent.name, mods[0].name) == 0 {
239                         ent.mtime = mods[0].mtime
240                         ent.size = mods[0].size
241                         mods = mods[1:]
242                 }
243                 if ent.IsDir() {
244                         ent.size = 0
245                 }
246                 entsDirSizer <- ent
247         }
248         for len(adds) > 0 {
249                 if namesCmp(mods[0].name, adds[0]) != 0 {
250                         panic("+ and M lists are out of sync")
251                 }
252                 newEnt := Ent{
253                         name:  adds[0],
254                         mtime: mods[0].mtime,
255                         size:  mods[0].size,
256                 }
257                 entsDirSizer <- newEnt
258                 adds = adds[1:]
259                 mods = mods[1:]
260         }
261
262         close(entsDirSizer)
263         <-dirSizerJob
264         close(entsWriter)
265         <-writerJob
266
267         tmp1 := applyDirSizes(tmp0, dirSizes)
268         tmp0.Close()
269         os.Remove(tmp0.Name())
270         return tmp1
271 }