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