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