]> Cypherpunks.ru repositories - nncp.git/blob - src/cmd/nncp-reass/main.go
Raise copyright years
[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-2022 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         "log"
31         "os"
32         "path/filepath"
33         "strconv"
34         "strings"
35
36         xdr "github.com/davecgh/go-xdr/xdr2"
37         "github.com/dustin/go-humanize"
38         "go.cypherpunks.ru/nncp/v8"
39 )
40
41 func usage() {
42         fmt.Fprintf(os.Stderr, nncp.UsageHeader())
43         fmt.Fprintf(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         defer fd.Close()
55         if err != nil {
56                 log.Fatalln("Can not open file:", err)
57         }
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 && os.IsNotExist(err) {
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                         badSize = uint64(fi.Size()) != metaPkt.FileSize%metaPkt.ChunkSize
128                 } else {
129                         badSize = uint64(fi.Size()) != metaPkt.ChunkSize
130                 }
131                 if badSize {
132                         ctx.LogE(
133                                 "reass-chunk",
134                                 lesChunk,
135                                 errors.New("invalid size"),
136                                 func(les nncp.LEs) string {
137                                         return fmt.Sprintf("%s: chunk %d", logMsg(les), chunkNum)
138                                 },
139                         )
140                         allChunksExist = false
141                 }
142         }
143         if !allChunksExist {
144                 return false
145         }
146
147         var hsh hash.Hash
148         allChecksumsGood := true
149         for chunkNum, chunkPath := range chunksPaths {
150                 fd, err = os.Open(chunkPath)
151                 if err != nil {
152                         log.Fatalln("Can not open file:", err)
153                 }
154                 fi, err := fd.Stat()
155                 if err != nil {
156                         log.Fatalln("Can not stat file:", err)
157                 }
158                 hsh = nncp.MTHNew(fi.Size(), 0)
159                 if _, err = nncp.CopyProgressed(
160                         hsh, bufio.NewReader(fd), "check",
161                         nncp.LEs{{K: "Pkt", V: chunkPath}, {K: "FullSize", V: fi.Size()}},
162                         ctx.ShowPrgrs,
163                 ); err != nil {
164                         log.Fatalln(err)
165                 }
166                 fd.Close()
167                 if bytes.Compare(hsh.Sum(nil), metaPkt.Checksums[chunkNum][:]) != 0 {
168                         ctx.LogE(
169                                 "reass-chunk",
170                                 nncp.LEs{{K: "Path", V: path}, {K: "Chunk", V: chunkNum}},
171                                 errors.New("checksum is bad"),
172                                 func(les nncp.LEs) string {
173                                         return fmt.Sprintf("%s: chunk %d", logMsg(les), chunkNum)
174                                 },
175                         )
176                         allChecksumsGood = false
177                 }
178         }
179         if !allChecksumsGood {
180                 return false
181         }
182         if dryRun {
183                 ctx.LogI("reass", nncp.LEs{{K: "path", V: path}}, logMsg)
184                 return true
185         }
186
187         var dst io.Writer
188         var tmp *os.File
189         if stdout {
190                 dst = os.Stdout
191                 les = nncp.LEs{{K: "path", V: path}}
192         } else {
193                 tmp, err = nncp.TempFile(mainDir, "reass")
194                 if err != nil {
195                         log.Fatalln(err)
196                 }
197                 les = nncp.LEs{{K: "path", V: path}, {K: "Tmp", V: tmp.Name()}}
198                 ctx.LogD("reass-tmp-created", les, func(les nncp.LEs) string {
199                         return fmt.Sprintf("%s: temporary %s created", logMsg(les), tmp.Name())
200                 })
201                 dst = tmp
202         }
203         dstW := bufio.NewWriter(dst)
204
205         hasErrors := false
206         for chunkNum, chunkPath := range chunksPaths {
207                 fd, err = os.Open(chunkPath)
208                 if err != nil {
209                         log.Fatalln("Can not open file:", err)
210                 }
211                 fi, err := fd.Stat()
212                 if err != nil {
213                         log.Fatalln("Can not stat file:", err)
214                 }
215                 if _, err = nncp.CopyProgressed(
216                         dstW, bufio.NewReader(fd), "reass",
217                         nncp.LEs{{K: "Pkt", V: chunkPath}, {K: "FullSize", V: fi.Size()}},
218                         ctx.ShowPrgrs,
219                 ); err != nil {
220                         log.Fatalln(err)
221                 }
222                 fd.Close()
223                 if !keep {
224                         if err = os.Remove(chunkPath); err != nil {
225                                 ctx.LogE(
226                                         "reass-chunk",
227                                         append(les, nncp.LE{K: "Chunk", V: chunkNum}), err,
228                                         func(les nncp.LEs) string {
229                                                 return fmt.Sprintf("%s: chunk %d", logMsg(les), chunkNum)
230                                         },
231                                 )
232                                 hasErrors = true
233                         }
234                 }
235         }
236         if err = dstW.Flush(); err != nil {
237                 log.Fatalln("Can not flush:", err)
238         }
239         if tmp != nil {
240                 if err = tmp.Sync(); err != nil {
241                         log.Fatalln("Can not sync:", err)
242                 }
243                 if err = tmp.Close(); err != nil {
244                         log.Fatalln("Can not close:", err)
245                 }
246         }
247         ctx.LogD("reass-written", les, func(les nncp.LEs) string {
248                 return logMsg(les) + ": written"
249         })
250         if !keep {
251                 if err = os.Remove(path); err != nil {
252                         ctx.LogE("reass-removing", les, err, func(les nncp.LEs) string {
253                                 return logMsg(les) + ": removing"
254                         })
255                         hasErrors = true
256                 }
257         }
258         if stdout {
259                 ctx.LogI("reass", nncp.LEs{{K: "Path", V: path}}, func(les nncp.LEs) string {
260                         return logMsg(les) + ": done"
261                 })
262                 return !hasErrors
263         }
264
265         dstPathOrig := filepath.Join(mainDir, mainName)
266         dstPath := dstPathOrig
267         dstPathCtr := 0
268         for {
269                 if _, err = os.Stat(dstPath); err != nil {
270                         if os.IsNotExist(err) {
271                                 break
272                         }
273                         log.Fatalln(err)
274                 }
275                 dstPath = dstPathOrig + "." + strconv.Itoa(dstPathCtr)
276                 dstPathCtr++
277         }
278         if err = os.Rename(tmp.Name(), dstPath); err != nil {
279                 log.Fatalln(err)
280         }
281         if err = nncp.DirSync(mainDir); err != nil {
282                 log.Fatalln(err)
283         }
284         ctx.LogI("reass", nncp.LEs{{K: "Path", V: path}}, func(les nncp.LEs) string {
285                 return logMsg(les) + ": done"
286         })
287         return !hasErrors
288 }
289
290 func findMetas(ctx *nncp.Ctx, dirPath string) []string {
291         dir, err := os.Open(dirPath)
292         defer dir.Close()
293         logMsg := func(les nncp.LEs) string {
294                 return "Finding .meta in " + dirPath
295         }
296         if err != nil {
297                 ctx.LogE("reass", nncp.LEs{{K: "Path", V: dirPath}}, err, logMsg)
298                 return nil
299         }
300         fis, err := dir.Readdir(0)
301         dir.Close()
302         if err != nil {
303                 ctx.LogE("reass", nncp.LEs{{K: "Path", V: dirPath}}, err, logMsg)
304                 return nil
305         }
306         metaPaths := make([]string, 0)
307         for _, fi := range fis {
308                 if strings.HasSuffix(fi.Name(), nncp.ChunkedSuffixMeta) {
309                         metaPaths = append(metaPaths, filepath.Join(dirPath, fi.Name()))
310                 }
311         }
312         return metaPaths
313 }
314
315 func main() {
316         var (
317                 cfgPath   = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
318                 allNodes  = flag.Bool("all", false, "Process all found chunked files for all nodes")
319                 nodeRaw   = flag.String("node", "", "Process all found chunked files for that node")
320                 keep      = flag.Bool("keep", false, "Do not remove chunks while assembling")
321                 dryRun    = flag.Bool("dryrun", false, "Do not assemble whole file")
322                 dumpMeta  = flag.Bool("dump", false, "Print decoded human-readable FILE.nncp.meta")
323                 stdout    = flag.Bool("stdout", false, "Output reassembled FILE to stdout")
324                 spoolPath = flag.String("spool", "", "Override path to spool")
325                 logPath   = flag.String("log", "", "Override path to logfile")
326                 quiet     = flag.Bool("quiet", false, "Print only errors")
327                 showPrgrs = flag.Bool("progress", false, "Force progress showing")
328                 omitPrgrs = flag.Bool("noprogress", false, "Omit progress showing")
329                 debug     = flag.Bool("debug", false, "Print debug messages")
330                 version   = flag.Bool("version", false, "Print version information")
331                 warranty  = flag.Bool("warranty", false, "Print warranty information")
332         )
333         log.SetFlags(log.Lshortfile)
334         flag.Usage = usage
335         flag.Parse()
336         if *warranty {
337                 fmt.Println(nncp.Warranty)
338                 return
339         }
340         if *version {
341                 fmt.Println(nncp.VersionGet())
342                 return
343         }
344
345         ctx, err := nncp.CtxFromCmdline(
346                 *cfgPath,
347                 *spoolPath,
348                 *logPath,
349                 *quiet,
350                 *showPrgrs,
351                 *omitPrgrs,
352                 *debug,
353         )
354         if err != nil {
355                 log.Fatalln("Error during initialization:", err)
356         }
357
358         var nodeOnly *nncp.Node
359         if *nodeRaw != "" {
360                 nodeOnly, err = ctx.FindNode(*nodeRaw)
361                 if err != nil {
362                         log.Fatalln("Invalid -node specified:", err)
363                 }
364         }
365
366         if !(*allNodes || nodeOnly != nil || flag.NArg() > 0) {
367                 usage()
368                 os.Exit(1)
369         }
370         if flag.NArg() > 0 && (*allNodes || nodeOnly != nil) {
371                 usage()
372                 os.Exit(1)
373         }
374         if *allNodes && nodeOnly != nil {
375                 usage()
376                 os.Exit(1)
377         }
378
379         ctx.Umask()
380
381         if flag.NArg() > 0 {
382                 if process(ctx, flag.Arg(0), *keep, *dryRun, *stdout, *dumpMeta) {
383                         return
384                 }
385                 os.Exit(1)
386         }
387
388         hasErrors := false
389         if nodeOnly == nil {
390                 seenMetaPaths := make(map[string]struct{})
391                 for _, node := range ctx.Neigh {
392                         if node.Incoming == nil {
393                                 continue
394                         }
395                         for _, metaPath := range findMetas(ctx, *node.Incoming) {
396                                 if _, seen := seenMetaPaths[metaPath]; seen {
397                                         continue
398                                 }
399                                 if !process(ctx, metaPath, *keep, *dryRun, false, false) {
400                                         hasErrors = true
401                                 }
402                                 seenMetaPaths[metaPath] = struct{}{}
403                         }
404                 }
405         } else {
406                 if nodeOnly.Incoming == nil {
407                         log.Fatalln("Specified -node does not allow incoming")
408                 }
409                 for _, metaPath := range findMetas(ctx, *nodeOnly.Incoming) {
410                         if !process(ctx, metaPath, *keep, *dryRun, false, false) {
411                                 hasErrors = true
412                         }
413                 }
414         }
415         if hasErrors {
416                 os.Exit(1)
417         }
418 }