]> Cypherpunks.ru repositories - nncp.git/blob - src/cmd/nncp-reass/main.go
NNCPNOSYNC environment variable
[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 !nncp.NoSync {
241                         if err = tmp.Sync(); err != nil {
242                                 log.Fatalln("Can not sync:", err)
243                         }
244                 }
245                 if err = tmp.Close(); err != nil {
246                         log.Fatalln("Can not close:", err)
247                 }
248         }
249         ctx.LogD("reass-written", les, func(les nncp.LEs) string {
250                 return logMsg(les) + ": written"
251         })
252         if !keep {
253                 if err = os.Remove(path); err != nil {
254                         ctx.LogE("reass-removing", les, err, func(les nncp.LEs) string {
255                                 return logMsg(les) + ": removing"
256                         })
257                         hasErrors = true
258                 }
259         }
260         if stdout {
261                 ctx.LogI("reass", nncp.LEs{{K: "Path", V: path}}, func(les nncp.LEs) string {
262                         return logMsg(les) + ": done"
263                 })
264                 return !hasErrors
265         }
266
267         dstPathOrig := filepath.Join(mainDir, mainName)
268         dstPath := dstPathOrig
269         dstPathCtr := 0
270         for {
271                 if _, err = os.Stat(dstPath); err != nil {
272                         if os.IsNotExist(err) {
273                                 break
274                         }
275                         log.Fatalln(err)
276                 }
277                 dstPath = dstPathOrig + "." + strconv.Itoa(dstPathCtr)
278                 dstPathCtr++
279         }
280         if err = os.Rename(tmp.Name(), dstPath); err != nil {
281                 log.Fatalln(err)
282         }
283         if err = nncp.DirSync(mainDir); err != nil {
284                 log.Fatalln(err)
285         }
286         ctx.LogI("reass", nncp.LEs{{K: "Path", V: path}}, func(les nncp.LEs) string {
287                 return logMsg(les) + ": done"
288         })
289         return !hasErrors
290 }
291
292 func findMetas(ctx *nncp.Ctx, dirPath string) []string {
293         dir, err := os.Open(dirPath)
294         defer dir.Close()
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         fis, err := dir.Readdir(0)
303         dir.Close()
304         if err != nil {
305                 ctx.LogE("reass", nncp.LEs{{K: "Path", V: dirPath}}, err, logMsg)
306                 return nil
307         }
308         metaPaths := make([]string, 0)
309         for _, fi := range fis {
310                 if strings.HasSuffix(fi.Name(), nncp.ChunkedSuffixMeta) {
311                         metaPaths = append(metaPaths, filepath.Join(dirPath, fi.Name()))
312                 }
313         }
314         return metaPaths
315 }
316
317 func main() {
318         var (
319                 cfgPath   = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
320                 allNodes  = flag.Bool("all", false, "Process all found chunked files for all nodes")
321                 nodeRaw   = flag.String("node", "", "Process all found chunked files for that node")
322                 keep      = flag.Bool("keep", false, "Do not remove chunks while assembling")
323                 dryRun    = flag.Bool("dryrun", false, "Do not assemble whole file")
324                 dumpMeta  = flag.Bool("dump", false, "Print decoded human-readable FILE.nncp.meta")
325                 stdout    = flag.Bool("stdout", false, "Output reassembled FILE to stdout")
326                 spoolPath = flag.String("spool", "", "Override path to spool")
327                 logPath   = flag.String("log", "", "Override path to logfile")
328                 quiet     = flag.Bool("quiet", false, "Print only errors")
329                 showPrgrs = flag.Bool("progress", false, "Force progress showing")
330                 omitPrgrs = flag.Bool("noprogress", false, "Omit progress showing")
331                 debug     = flag.Bool("debug", false, "Print debug messages")
332                 version   = flag.Bool("version", false, "Print version information")
333                 warranty  = flag.Bool("warranty", false, "Print warranty information")
334         )
335         log.SetFlags(log.Lshortfile)
336         flag.Usage = usage
337         flag.Parse()
338         if *warranty {
339                 fmt.Println(nncp.Warranty)
340                 return
341         }
342         if *version {
343                 fmt.Println(nncp.VersionGet())
344                 return
345         }
346
347         ctx, err := nncp.CtxFromCmdline(
348                 *cfgPath,
349                 *spoolPath,
350                 *logPath,
351                 *quiet,
352                 *showPrgrs,
353                 *omitPrgrs,
354                 *debug,
355         )
356         if err != nil {
357                 log.Fatalln("Error during initialization:", err)
358         }
359
360         var nodeOnly *nncp.Node
361         if *nodeRaw != "" {
362                 nodeOnly, err = ctx.FindNode(*nodeRaw)
363                 if err != nil {
364                         log.Fatalln("Invalid -node specified:", err)
365                 }
366         }
367
368         if !(*allNodes || nodeOnly != nil || flag.NArg() > 0) {
369                 usage()
370                 os.Exit(1)
371         }
372         if flag.NArg() > 0 && (*allNodes || nodeOnly != nil) {
373                 usage()
374                 os.Exit(1)
375         }
376         if *allNodes && nodeOnly != nil {
377                 usage()
378                 os.Exit(1)
379         }
380
381         ctx.Umask()
382
383         if flag.NArg() > 0 {
384                 if process(ctx, flag.Arg(0), *keep, *dryRun, *stdout, *dumpMeta) {
385                         return
386                 }
387                 os.Exit(1)
388         }
389
390         hasErrors := false
391         if nodeOnly == nil {
392                 seenMetaPaths := make(map[string]struct{})
393                 for _, node := range ctx.Neigh {
394                         if node.Incoming == nil {
395                                 continue
396                         }
397                         for _, metaPath := range findMetas(ctx, *node.Incoming) {
398                                 if _, seen := seenMetaPaths[metaPath]; seen {
399                                         continue
400                                 }
401                                 if !process(ctx, metaPath, *keep, *dryRun, false, false) {
402                                         hasErrors = true
403                                 }
404                                 seenMetaPaths[metaPath] = struct{}{}
405                         }
406                 }
407         } else {
408                 if nodeOnly.Incoming == nil {
409                         log.Fatalln("Specified -node does not allow incoming")
410                 }
411                 for _, metaPath := range findMetas(ctx, *nodeOnly.Incoming) {
412                         if !process(ctx, metaPath, *keep, *dryRun, false, false) {
413                                 hasErrors = true
414                         }
415                 }
416         }
417         if hasErrors {
418                 os.Exit(1)
419         }
420 }