]> Cypherpunks.ru repositories - nncp.git/blob - src/cmd/nncp-reass/main.go
fc71bd05e47e5ab793583705bcf5c374636d872b
[nncp.git] / src / cmd / nncp-reass / main.go
1 /*
2 NNCP -- Node to Node copy, utilities for store-and-forward data exchange
3 Copyright (C) 2016-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 // Reassembly chunked file.
19 package main
20
21 import (
22         "bufio"
23         "bytes"
24         "encoding/hex"
25         "errors"
26         "flag"
27         "fmt"
28         "hash"
29         "io"
30         "io/fs"
31         "log"
32         "os"
33         "path/filepath"
34         "strconv"
35         "strings"
36
37         xdr "github.com/davecgh/go-xdr/xdr2"
38         "github.com/dustin/go-humanize"
39         "go.cypherpunks.ru/nncp/v8"
40 )
41
42 func usage() {
43         fmt.Fprint(os.Stderr, nncp.UsageHeader())
44         fmt.Fprint(os.Stderr, "nncp-reass -- reassemble chunked files\n\n")
45         fmt.Fprintf(os.Stderr, "Usage: %s [options] [FILE.nncp.meta]\nOptions:\n", os.Args[0])
46         flag.PrintDefaults()
47         fmt.Fprint(os.Stderr, `
48 Neither FILE, nor -node nor -all can be set simultaneously,
49 but at least one of them must be specified.
50 `)
51 }
52
53 func process(ctx *nncp.Ctx, path string, keep, dryRun, stdout, dumpMeta bool) bool {
54         fd, err := os.Open(path)
55         if err != nil {
56                 log.Fatalln("Can not open file:", err)
57         }
58         defer fd.Close()
59         var metaPkt nncp.ChunkedMeta
60         les := nncp.LEs{{K: "Path", V: path}}
61         logMsg := func(les nncp.LEs) string {
62                 return fmt.Sprintf("Reassembling chunked file \"%s\"", path)
63         }
64         if _, err = xdr.Unmarshal(fd, &metaPkt); err != nil {
65                 ctx.LogE("reass-bad-meta", les, err, func(les nncp.LEs) string {
66                         return logMsg(les) + ": bad meta"
67                 })
68                 return false
69         }
70         fd.Close()
71         if metaPkt.Magic == nncp.MagicNNCPMv1.B {
72                 ctx.LogE("reass", les, nncp.MagicNNCPMv1.TooOld(), logMsg)
73                 return false
74         }
75         if metaPkt.Magic != nncp.MagicNNCPMv2.B {
76                 ctx.LogE("reass", les, nncp.BadMagic, logMsg)
77                 return false
78         }
79
80         metaName := filepath.Base(path)
81         if !strings.HasSuffix(metaName, nncp.ChunkedSuffixMeta) {
82                 ctx.LogE("reass", les, errors.New("invalid filename suffix"), logMsg)
83                 return false
84         }
85         mainName := strings.TrimSuffix(metaName, nncp.ChunkedSuffixMeta)
86         if dumpMeta {
87                 fmt.Printf("Original filename: %s\n", mainName)
88                 fmt.Printf(
89                         "File size: %s (%d bytes)\n",
90                         humanize.IBytes(metaPkt.FileSize),
91                         metaPkt.FileSize,
92                 )
93                 fmt.Printf(
94                         "Chunk size: %s (%d bytes)\n",
95                         humanize.IBytes(metaPkt.ChunkSize),
96                         metaPkt.ChunkSize,
97                 )
98                 fmt.Printf("Number of chunks: %d\n", len(metaPkt.Checksums))
99                 fmt.Println("Checksums:")
100                 for chunkNum, checksum := range metaPkt.Checksums {
101                         fmt.Printf("\t%d: %s\n", chunkNum, hex.EncodeToString(checksum[:]))
102                 }
103                 return true
104         }
105         mainDir := filepath.Dir(path)
106
107         chunksPaths := make([]string, 0, len(metaPkt.Checksums))
108         for i := 0; i < len(metaPkt.Checksums); i++ {
109                 chunksPaths = append(
110                         chunksPaths,
111                         filepath.Join(mainDir, mainName+nncp.ChunkedSuffixPart+strconv.Itoa(i)),
112                 )
113         }
114
115         allChunksExist := true
116         for chunkNum, chunkPath := range chunksPaths {
117                 fi, err := os.Stat(chunkPath)
118                 lesChunk := append(les, nncp.LE{K: "Chunk", V: chunkNum})
119                 if err != nil && errors.Is(err, fs.ErrNotExist) {
120                         ctx.LogI("reass-chunk-miss", lesChunk, func(les nncp.LEs) string {
121                                 return fmt.Sprintf("%s: chunk %d missing", logMsg(les), chunkNum)
122                         })
123                         allChunksExist = false
124                         continue
125                 }
126                 var badSize bool
127                 if chunkNum+1 == len(chunksPaths) {
128                         left := metaPkt.FileSize % metaPkt.ChunkSize
129                         badSize = left != 0 && uint64(fi.Size()) != left
130                 } else {
131                         badSize = uint64(fi.Size()) != metaPkt.ChunkSize
132                 }
133                 if badSize {
134                         ctx.LogE(
135                                 "reass-chunk",
136                                 lesChunk,
137                                 errors.New("invalid size"),
138                                 func(les nncp.LEs) string {
139                                         return fmt.Sprintf("%s: chunk %d", logMsg(les), chunkNum)
140                                 },
141                         )
142                         allChunksExist = false
143                 }
144         }
145         if !allChunksExist {
146                 return false
147         }
148
149         var hsh hash.Hash
150         allChecksumsGood := true
151         for chunkNum, chunkPath := range chunksPaths {
152                 fd, err = os.Open(chunkPath)
153                 if err != nil {
154                         log.Fatalln("Can not open file:", err)
155                 }
156                 fi, err := fd.Stat()
157                 if err != nil {
158                         log.Fatalln("Can not stat file:", err)
159                 }
160                 hsh = nncp.MTHNew(fi.Size(), 0)
161                 if _, err = nncp.CopyProgressed(
162                         hsh, bufio.NewReaderSize(fd, nncp.MTHBlockSize), "check",
163                         nncp.LEs{{K: "Pkt", V: chunkPath}, {K: "FullSize", V: fi.Size()}},
164                         ctx.ShowPrgrs,
165                 ); err != nil {
166                         log.Fatalln(err)
167                 }
168                 fd.Close()
169                 if !bytes.Equal(hsh.Sum(nil), metaPkt.Checksums[chunkNum][:]) {
170                         ctx.LogE(
171                                 "reass-chunk",
172                                 nncp.LEs{{K: "Path", V: path}, {K: "Chunk", V: chunkNum}},
173                                 errors.New("checksum is bad"),
174                                 func(les nncp.LEs) string {
175                                         return fmt.Sprintf("%s: chunk %d", logMsg(les), chunkNum)
176                                 },
177                         )
178                         allChecksumsGood = false
179                 }
180         }
181         if !allChecksumsGood {
182                 return false
183         }
184         if dryRun {
185                 ctx.LogI("reass", nncp.LEs{{K: "path", V: path}}, logMsg)
186                 return true
187         }
188
189         var dst io.Writer
190         var tmp *os.File
191         if stdout {
192                 dst = os.Stdout
193                 les = nncp.LEs{{K: "path", V: path}}
194         } else {
195                 tmp, err = nncp.TempFile(mainDir, "reass")
196                 if err != nil {
197                         log.Fatalln(err)
198                 }
199                 les = nncp.LEs{{K: "path", V: path}, {K: "Tmp", V: tmp.Name()}}
200                 ctx.LogD("reass-tmp-created", les, func(les nncp.LEs) string {
201                         return fmt.Sprintf("%s: temporary %s created", logMsg(les), tmp.Name())
202                 })
203                 dst = tmp
204         }
205         dstW := bufio.NewWriter(dst)
206
207         hasErrors := false
208         for chunkNum, chunkPath := range chunksPaths {
209                 fd, err = os.Open(chunkPath)
210                 if err != nil {
211                         log.Fatalln("Can not open file:", err)
212                 }
213                 fi, err := fd.Stat()
214                 if err != nil {
215                         log.Fatalln("Can not stat file:", err)
216                 }
217                 if _, err = nncp.CopyProgressed(
218                         dstW, bufio.NewReaderSize(fd, nncp.MTHBlockSize), "reass",
219                         nncp.LEs{{K: "Pkt", V: chunkPath}, {K: "FullSize", V: fi.Size()}},
220                         ctx.ShowPrgrs,
221                 ); err != nil {
222                         log.Fatalln(err)
223                 }
224                 fd.Close()
225                 if !keep {
226                         if err = os.Remove(chunkPath); err != nil {
227                                 ctx.LogE(
228                                         "reass-chunk",
229                                         append(les, nncp.LE{K: "Chunk", V: chunkNum}), err,
230                                         func(les nncp.LEs) string {
231                                                 return fmt.Sprintf("%s: chunk %d", logMsg(les), chunkNum)
232                                         },
233                                 )
234                                 hasErrors = true
235                         }
236                 }
237         }
238         if err = dstW.Flush(); err != nil {
239                 log.Fatalln("Can not flush:", err)
240         }
241         if tmp != nil {
242                 if !nncp.NoSync {
243                         if err = tmp.Sync(); err != nil {
244                                 log.Fatalln("Can not sync:", err)
245                         }
246                 }
247                 if err = tmp.Close(); err != nil {
248                         log.Fatalln("Can not close:", err)
249                 }
250         }
251         ctx.LogD("reass-written", les, func(les nncp.LEs) string {
252                 return logMsg(les) + ": written"
253         })
254         if !keep {
255                 if err = os.Remove(path); err != nil {
256                         ctx.LogE("reass-removing", les, err, func(les nncp.LEs) string {
257                                 return logMsg(les) + ": removing"
258                         })
259                         hasErrors = true
260                 }
261         }
262         if stdout {
263                 ctx.LogI("reass", nncp.LEs{{K: "Path", V: path}}, func(les nncp.LEs) string {
264                         return logMsg(les) + ": done"
265                 })
266                 return !hasErrors
267         }
268
269         dstPathOrig := filepath.Join(mainDir, mainName)
270         dstPath := dstPathOrig
271         dstPathCtr := 0
272         for {
273                 if _, err = os.Stat(dstPath); err != nil {
274                         if errors.Is(err, fs.ErrNotExist) {
275                                 break
276                         }
277                         log.Fatalln(err)
278                 }
279                 dstPath = dstPathOrig + "." + strconv.Itoa(dstPathCtr)
280                 dstPathCtr++
281         }
282         if err = os.Rename(tmp.Name(), dstPath); err != nil {
283                 log.Fatalln(err)
284         }
285         if err = nncp.DirSync(mainDir); err != nil {
286                 log.Fatalln(err)
287         }
288         ctx.LogI("reass", nncp.LEs{{K: "Path", V: path}}, func(les nncp.LEs) string {
289                 return logMsg(les) + ": done"
290         })
291         return !hasErrors
292 }
293
294 func findMetas(ctx *nncp.Ctx, dirPath string) []string {
295         dir, err := os.Open(dirPath)
296         logMsg := func(les nncp.LEs) string {
297                 return "Finding .meta in " + dirPath
298         }
299         if err != nil {
300                 ctx.LogE("reass", nncp.LEs{{K: "Path", V: dirPath}}, err, logMsg)
301                 return nil
302         }
303         defer dir.Close()
304         entries, err := dir.ReadDir(0)
305         dir.Close()
306         if err != nil {
307                 ctx.LogE("reass", nncp.LEs{{K: "Path", V: dirPath}}, err, logMsg)
308                 return nil
309         }
310         metaPaths := make([]string, 0)
311         for _, entry := range entries {
312                 if strings.HasSuffix(entry.Name(), nncp.ChunkedSuffixMeta) {
313                         metaPaths = append(metaPaths, filepath.Join(dirPath, entry.Name()))
314                 }
315         }
316         return metaPaths
317 }
318
319 func main() {
320         var (
321                 cfgPath   = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
322                 allNodes  = flag.Bool("all", false, "Process all found chunked files for all nodes")
323                 nodeRaw   = flag.String("node", "", "Process all found chunked files for that node")
324                 keep      = flag.Bool("keep", false, "Do not remove chunks while assembling")
325                 dryRun    = flag.Bool("dryrun", false, "Do not assemble whole file")
326                 dumpMeta  = flag.Bool("dump", false, "Print decoded human-readable FILE.nncp.meta")
327                 stdout    = flag.Bool("stdout", false, "Output reassembled FILE to stdout")
328                 spoolPath = flag.String("spool", "", "Override path to spool")
329                 logPath   = flag.String("log", "", "Override path to logfile")
330                 quiet     = flag.Bool("quiet", false, "Print only errors")
331                 showPrgrs = flag.Bool("progress", false, "Force progress showing")
332                 omitPrgrs = flag.Bool("noprogress", false, "Omit progress showing")
333                 debug     = flag.Bool("debug", false, "Print debug messages")
334                 version   = flag.Bool("version", false, "Print version information")
335                 warranty  = flag.Bool("warranty", false, "Print warranty information")
336         )
337         log.SetFlags(log.Lshortfile)
338         flag.Usage = usage
339         flag.Parse()
340         if *warranty {
341                 fmt.Println(nncp.Warranty)
342                 return
343         }
344         if *version {
345                 fmt.Println(nncp.VersionGet())
346                 return
347         }
348
349         ctx, err := nncp.CtxFromCmdline(
350                 *cfgPath,
351                 *spoolPath,
352                 *logPath,
353                 *quiet,
354                 *showPrgrs,
355                 *omitPrgrs,
356                 *debug,
357         )
358         if err != nil {
359                 log.Fatalln("Error during initialization:", err)
360         }
361
362         var nodeOnly *nncp.Node
363         if *nodeRaw != "" {
364                 nodeOnly, err = ctx.FindNode(*nodeRaw)
365                 if err != nil {
366                         log.Fatalln("Invalid -node specified:", err)
367                 }
368         }
369
370         if !(*allNodes || nodeOnly != nil || flag.NArg() > 0) {
371                 usage()
372                 os.Exit(1)
373         }
374         if flag.NArg() > 0 && (*allNodes || nodeOnly != nil) {
375                 usage()
376                 os.Exit(1)
377         }
378         if *allNodes && nodeOnly != nil {
379                 usage()
380                 os.Exit(1)
381         }
382
383         ctx.Umask()
384
385         if flag.NArg() > 0 {
386                 if process(ctx, flag.Arg(0), *keep, *dryRun, *stdout, *dumpMeta) {
387                         return
388                 }
389                 os.Exit(1)
390         }
391
392         hasErrors := false
393         if nodeOnly == nil {
394                 seenMetaPaths := make(map[string]struct{})
395                 for _, node := range ctx.Neigh {
396                         if node.Incoming == nil {
397                                 continue
398                         }
399                         for _, metaPath := range findMetas(ctx, *node.Incoming) {
400                                 if _, seen := seenMetaPaths[metaPath]; seen {
401                                         continue
402                                 }
403                                 if !process(ctx, metaPath, *keep, *dryRun, false, false) {
404                                         hasErrors = true
405                                 }
406                                 seenMetaPaths[metaPath] = struct{}{}
407                         }
408                 }
409         } else {
410                 if nodeOnly.Incoming == nil {
411                         log.Fatalln("Specified -node does not allow incoming")
412                 }
413                 for _, metaPath := range findMetas(ctx, *nodeOnly.Incoming) {
414                         if !process(ctx, metaPath, *keep, *dryRun, false, false) {
415                                 hasErrors = true
416                         }
417                 }
418         }
419         if hasErrors {
420                 os.Exit(1)
421         }
422 }