]> Cypherpunks.ru repositories - nncp.git/commitdiff
nncp-cfgdir
authorSergey Matveev <stargrave@stargrave.org>
Fri, 9 Jul 2021 19:52:51 +0000 (22:52 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sat, 10 Jul 2021 13:28:44 +0000 (16:28 +0300)
14 files changed:
bin/cmd.list
doc/cfg/dir.texi [new file with mode: 0644]
doc/cfg/index.texi
doc/cmd/index.texi
doc/cmd/nncp-cfgdir.texi [new file with mode: 0644]
doc/news.ru.texi
doc/news.texi
ports/nncp/Makefile
ports/nncp/pkg-plist
src/cfg.go
src/cfgdir.go [new file with mode: 0644]
src/cmd/nncp-cfgdir/main.go [new file with mode: 0644]
src/ctx.go
src/nncp.go

index 693027a1fdc4f52c61d4911f60bbd1b977bf93e6..de2a35f2a07a4fc19d152c89fd15ec7974a2820b 100644 (file)
@@ -1,6 +1,7 @@
 nncp-bundle
 nncp-call
 nncp-caller
+nncp-cfgdir
 nncp-cfgenc
 nncp-cfgmin
 nncp-cfgnew
diff --git a/doc/cfg/dir.texi b/doc/cfg/dir.texi
new file mode 100644 (file)
index 0000000..8297925
--- /dev/null
@@ -0,0 +1,107 @@
+@node Configuration directory
+@section Configuration directory
+
+Optionally you can convert configuration file to the directory layout
+with @ref{nncp-cfgdir} command. And vice versa too, of course loosing
+the comment lines. Directory layout can looks like that:
+
+@example
+nncp-cfg-dir
+├── areas
+│   ├── home
+│   │   ├── id
+│   │   ├── incoming
+│   │   ├── prv
+│   │   ├── pub
+│   │   └── subs
+│   └── homero
+│       ├── id
+│       ├── exec
+│       │   └── sendmail
+│       ├── prv
+│       ├── pub
+│       └── allow-unknown
+├── log
+├── mcd-listen
+├── neigh
+│   ├── beta
+│   │   ├── exchpub
+│   │   ├── exec
+│   │   │   └── sendmail
+│   │   ├── id
+│   │   ├── incoming
+│   │   ├── noisepub
+│   │   ├── signpub
+│   │   └── via
+│   ├── gw
+│   │   ├── addrs
+│   │   │   ├── lan
+│   │   │   └── main
+│   │   ├── calls
+│   │   │   ├── 0
+│   │   │   │   ├── autotoss
+│   │   │   │   ├── cron
+│   │   │   │   ├── nice
+│   │   │   │   └── rxrate
+│   │   │   ├── 1
+│   │   │   │   ├── addr
+│   │   │   │   ├── autotoss
+│   │   │   │   ├── cron
+│   │   │   │   └── onlinedeadline
+│   │   │   └── 2
+│   │   │       ├── addr
+│   │   │       ├── autotoss
+│   │   │       ├── cron
+│   │   │       └── onlinedeadline
+│   │   ├── exchpub
+│   │   ├── exec
+│   │   │   └── sendmail
+│   │   ├── freq
+│   │   │   └── chunked
+│   │   ├── id
+│   │   ├── incoming
+│   │   ├── noisepub
+│   │   └── signpub
+│   └── self
+│       ├── exchpub
+│       ├── exec
+│       │   ├── appender
+│       │   ├── sendmail
+│       │   └── slow
+│       ├── freq
+│       │   └── path
+│       ├── id
+│       ├── incoming
+│       ├── noisepub
+│       └── signpub
+├── notify
+│   ├── file
+│   │   ├── from
+│   │   └── to
+│   └── freq
+│       ├── from
+│       └── to
+├── self
+│   ├── exchprv
+│   ├── exchpub
+│   ├── id
+│   ├── noiseprv
+│   ├── noisepub
+│   ├── signprv
+│   └── signpub
+└── spool
+@end example
+
+Your @option{-cfg} and @env{$NNCPCFG} could point to that directory,
+instead of @file{.hjson} file. It will be transparently converted to
+internal JSON representation. However it can not be encrypted with the
+@ref{nncp-cfgenc}.
+
+That layout should be much more machine friendly and scriptable. Each
+string parameters is stored as a single line plain text file. String
+arrays are newline-separated plain text files. Dictionaries are
+transformed to the subdirectories. Its structure should be
+self-describing. True booleans are stored as an empty flag-file
+existence (their absence equals to false). All names starting with "."
+are skipped. All files ending with @file{prv} are created with 600
+permissions, instead of the default 666.
index 1944eaa2b8b5d772bae60e852456f489e8639d7d..b44f2430e1ef02cd00e196f0882a956a81e8b0c0 100644 (file)
@@ -2,8 +2,9 @@
 @unnumbered Configuration file
 
 NNCP uses single file configuration file in @url{https://hjson.org/,
-Hjson} format. Initially it is created with @ref{nncp-cfgnew} command
-and at minimum it can look like this:
+Hjson} format (see also section about @ref{Configuration directory,
+directory layout}) . Initially it is created with @ref{nncp-cfgnew}
+command and at minimum it can look like this:
 
 @verbatim
 spool: /var/spool/nncp
@@ -32,7 +33,7 @@ neigh: {
 And for being able to communicate with at least one other node, you just
 need to add single key to the @code{neigh} section similar to the "self".
 
-All configuration file can be separated on five sections:
+Whole configuration file can be separated on five sections:
 
 @menu
 * General options: CfgGeneral.
@@ -40,6 +41,9 @@ All configuration file can be separated on five sections:
 * Notifications: CfgNotify.
 * Neighbours: CfgNeigh.
 * Areas: CfgAreas.
+
+You can optionally convert it to directory layout
+* Configuration directory::
 @end menu
 
 @include cfg/general.texi
@@ -47,3 +51,4 @@ All configuration file can be separated on five sections:
 @include cfg/notify.texi
 @include cfg/neigh.texi
 @include cfg/areas.texi
+@include cfg/dir.texi
index a360e28e882b3fbabff16cf89c4f447ee8dc3afb..b4cdce8c52e3ec46da202a8b09c5f5c801362c90 100644 (file)
@@ -48,6 +48,7 @@ Configuration file commands
 * nncp-cfgnew::
 * nncp-cfgmin::
 * nncp-cfgenc::
+* nncp-cfgdir::
 
 Packets creation commands
 
@@ -86,6 +87,7 @@ Maintenance, monitoring and debugging commands:
 @include cmd/nncp-cfgnew.texi
 @include cmd/nncp-cfgmin.texi
 @include cmd/nncp-cfgenc.texi
+@include cmd/nncp-cfgdir.texi
 @include cmd/nncp-file.texi
 @include cmd/nncp-exec.texi
 @include cmd/nncp-freq.texi
diff --git a/doc/cmd/nncp-cfgdir.texi b/doc/cmd/nncp-cfgdir.texi
new file mode 100644 (file)
index 0000000..ec45ddd
--- /dev/null
@@ -0,0 +1,12 @@
+@node nncp-cfgdir
+@section nncp-cfgdir
+
+@example
+$ nncp-cfgdir [options] [-cfg ...] -dump /path/to/dir
+$ nncp-cfgdir [options] -load /path/to/dir > cfg.hjson
+@end example
+
+@option{-dump} option dumps current configuration file to the
+@ref{Configuration directory, directory layout} at @file{/path/to/dir}.
+@option{-load} loads it and parses, outputing the resulting Hjson to
+stdout.
index 56710405360d63fc41962f3575aa7c87fe11493b..a3f6c401bf9d45f9b15d6324548831556a32c788 100644 (file)
@@ -2,6 +2,7 @@
 @section Новости
 
 @menu
+* Релиз 7.3.0::
 * Релиз 7.2.1::
 * Релиз 7.2.0::
 * Релиз 7.1.1::
 * Релиз 0.2::
 @end menu
 
+@node Релиз 7.3.0
+@subsection Релиз 7.3.0
+@itemize
+
+@item
+Возможность использовать конфигурацию в виде директории с набором
+файлов. Появилась команда @command{nncp-cfgdir}.
+
+@end itemize
+
 @node Релиз 7.2.1
 @subsection Релиз 7.2.1
 @itemize
index dcb808715a27dab0821dff5a939bae7f78216ba2..3375cd45745422e961a08961dd176dc4aee83dc2 100644 (file)
@@ -4,6 +4,7 @@
 See also this page @ref{Новости, on russian}.
 
 @menu
+* Release 7.3.0: Release 7_3_0.
 * Release 7.2.1: Release 7_2_1.
 * Release 7.2.0: Release 7_2_0.
 * Release 7.1.1: Release 7_1_1.
@@ -54,6 +55,16 @@ See also this page @ref{Новости, on russian}.
 * Release 0.2: Release 0_2.
 @end menu
 
+@node Release 7_3_0
+@section Release 7.3.0
+@itemize
+
+@item
+Ability to use directory with a bunch of files as a configuration.
+@command{nncp-cfgdir} command appeared.
+
+@end itemize
+
 @node Release 7_2_1
 @section Release 7.2.1
 @itemize
index 7aed8a6005c5a6ba803a5c3fc95a5ea3840b665b..c0022f087e2a1734e24dd3e29b43e6447a5bf397 100644 (file)
@@ -1,5 +1,5 @@
 PORTNAME=      nncp
-DISTVERSION=   7.2.0
+DISTVERSION=   7.3.0
 CATEGORIES=    net
 MASTER_SITES=  http://www.nncpgo.org/download/
 
index 268e547f49c009fac380ac0248d407538958adb8..e360a4ac72023754517d660e825aaac58cd360d4 100644 (file)
@@ -1,6 +1,7 @@
 bin/nncp-bundle
 bin/nncp-call
 bin/nncp-caller
+bin/nncp-cfgdir
 bin/nncp-cfgenc
 bin/nncp-cfgmin
 bin/nncp-cfgnew
index bacfd7e29243631c2ecb543d47217bb97b73c171..5a404edd67b2149d0673c3af5054bbbf522a9672 100644 (file)
@@ -51,8 +51,8 @@ type NodeJSON struct {
        ExchPub  string              `json:"exchpub"`
        SignPub  string              `json:"signpub"`
        NoisePub *string             `json:"noisepub,omitempty"`
-       Exec     map[string][]string `json:"exec,omitempty"`
        Incoming *string             `json:"incoming,omitempty"`
+       Exec     map[string][]string `json:"exec,omitempty"`
        Freq     *NodeFreqJSON       `json:"freq,omitempty"`
        Via      []string            `json:"via,omitempty"`
        Calls    []CallJSON          `json:"calls,omitempty"`
@@ -73,7 +73,7 @@ type NodeFreqJSON struct {
 }
 
 type CallJSON struct {
-       Cron           string
+       Cron           string  `json:"cron"`
        Nice           *string `json:"nice,omitempty"`
        Xx             *string `json:"xx,omitempty"`
        RxRate         *int    `json:"rxrate,omitempty"`
@@ -81,17 +81,17 @@ type CallJSON struct {
        Addr           *string `json:"addr,omitempty"`
        OnlineDeadline *uint   `json:"onlinedeadline,omitempty"`
        MaxOnlineTime  *uint   `json:"maxonlinetime,omitempty"`
-       WhenTxExists   *bool   `json:"when-tx-exists,omitempty"`
-       NoCK           *bool   `json:"nock"`
-       MCDIgnore      *bool   `json:"mcd-ignore"`
-
-       AutoToss       *bool `json:"autotoss,omitempty"`
-       AutoTossDoSeen *bool `json:"autotoss-doseen,omitempty"`
-       AutoTossNoFile *bool `json:"autotoss-nofile,omitempty"`
-       AutoTossNoFreq *bool `json:"autotoss-nofreq,omitempty"`
-       AutoTossNoExec *bool `json:"autotoss-noexec,omitempty"`
-       AutoTossNoTrns *bool `json:"autotoss-notrns,omitempty"`
-       AutoTossNoArea *bool `json:"autotoss-noarea,omitempty"`
+       WhenTxExists   bool    `json:"when-tx-exists,omitempty"`
+       NoCK           bool    `json:"nock,omitempty"`
+       MCDIgnore      bool    `json:"mcd-ignore,omitempty"`
+
+       AutoToss       bool `json:"autotoss,omitempty"`
+       AutoTossDoSeen bool `json:"autotoss-doseen,omitempty"`
+       AutoTossNoFile bool `json:"autotoss-nofile,omitempty"`
+       AutoTossNoFreq bool `json:"autotoss-nofreq,omitempty"`
+       AutoTossNoExec bool `json:"autotoss-noexec,omitempty"`
+       AutoTossNoTrns bool `json:"autotoss-notrns,omitempty"`
+       AutoTossNoArea bool `json:"autotoss-noarea,omitempty"`
 }
 
 type NodeOurJSON struct {
@@ -100,13 +100,13 @@ type NodeOurJSON struct {
        ExchPrv  string `json:"exchprv"`
        SignPub  string `json:"signpub"`
        SignPrv  string `json:"signprv"`
-       NoisePrv string `json:"noiseprv"`
        NoisePub string `json:"noisepub"`
+       NoisePrv string `json:"noiseprv"`
 }
 
 type FromToJSON struct {
-       From string
-       To   string
+       From string `json:"from"`
+       To   string `json:"to"`
 }
 
 type NotifyJSON struct {
@@ -117,34 +117,34 @@ type NotifyJSON struct {
 
 type AreaJSON struct {
        Id  string  `json:"id"`
-       Pub string  `json:"pub"`
+       Pub *string `json:"pub,omitempty"`
        Prv *string `json:"prv,omitempty"`
 
        Subs []string `json:"subs"`
 
-       Exec     map[string][]string `json:"exec,omitempty"`
        Incoming *string             `json:"incoming,omitempty"`
+       Exec     map[string][]string `json:"exec,omitempty"`
 
-       AllowUnknown *bool `json:"allow-unknown,omitempty"`
+       AllowUnknown bool `json:"allow-unknown,omitempty"`
 }
 
 type CfgJSON struct {
-       Spool string `json:"spool"`
-       Log   string `json:"log"`
-       Umask string `json:"umask,omitempty"`
+       Spool string  `json:"spool"`
+       Log   string  `json:"log"`
+       Umask *string `json:"umask,omitempty"`
 
        OmitPrgrs bool `json:"noprogress,omitempty"`
        NoHdr     bool `json:"nohdr,omitempty"`
 
+       MCDRxIfis []string       `json:"mcd-listen,omitempty"`
+       MCDTxIfis map[string]int `json:"mcd-send,omitempty"`
+
        Notify *NotifyJSON `json:"notify,omitempty"`
 
        Self  *NodeOurJSON        `json:"self"`
        Neigh map[string]NodeJSON `json:"neigh"`
 
-       MCDRxIfis []string       `json:"mcd-listen"`
-       MCDTxIfis map[string]int `json:"mcd-send"`
-
-       Areas map[string]AreaJSON `json:"areas"`
+       Areas map[string]AreaJSON `json:"areas,omitempty"`
 }
 
 func NewNode(name string, cfg NodeJSON) (*Node, error) {
@@ -303,36 +303,16 @@ func NewNode(name string, cfg NodeJSON) (*Node, error) {
                if callCfg.MaxOnlineTime != nil {
                        call.MaxOnlineTime = time.Duration(*callCfg.MaxOnlineTime) * time.Second
                }
-               if callCfg.WhenTxExists != nil {
-                       call.WhenTxExists = *callCfg.WhenTxExists
-               }
-               if callCfg.NoCK != nil {
-                       call.NoCK = *callCfg.NoCK
-               }
-               if callCfg.MCDIgnore != nil {
-                       call.MCDIgnore = *callCfg.MCDIgnore
-               }
-               if callCfg.AutoToss != nil {
-                       call.AutoToss = *callCfg.AutoToss
-               }
-               if callCfg.AutoTossDoSeen != nil {
-                       call.AutoTossDoSeen = *callCfg.AutoTossDoSeen
-               }
-               if callCfg.AutoTossNoFile != nil {
-                       call.AutoTossNoFile = *callCfg.AutoTossNoFile
-               }
-               if callCfg.AutoTossNoFreq != nil {
-                       call.AutoTossNoFreq = *callCfg.AutoTossNoFreq
-               }
-               if callCfg.AutoTossNoExec != nil {
-                       call.AutoTossNoExec = *callCfg.AutoTossNoExec
-               }
-               if callCfg.AutoTossNoTrns != nil {
-                       call.AutoTossNoTrns = *callCfg.AutoTossNoTrns
-               }
-               if callCfg.AutoTossNoArea != nil {
-                       call.AutoTossNoArea = *callCfg.AutoTossNoArea
-               }
+               call.WhenTxExists = callCfg.WhenTxExists
+               call.NoCK = callCfg.NoCK
+               call.MCDIgnore = callCfg.MCDIgnore
+               call.AutoToss = callCfg.AutoToss
+               call.AutoTossDoSeen = callCfg.AutoTossDoSeen
+               call.AutoTossNoFile = callCfg.AutoTossNoFile
+               call.AutoTossNoFreq = callCfg.AutoTossNoFreq
+               call.AutoTossNoExec = callCfg.AutoTossNoExec
+               call.AutoTossNoTrns = callCfg.AutoTossNoTrns
+               call.AutoTossNoArea = callCfg.AutoTossNoArea
 
                calls = append(calls, &call)
        }
@@ -449,19 +429,21 @@ func NewArea(ctx *Ctx, name string, cfg *AreaJSON) (*Area, error) {
        area := Area{
                Name:     name,
                Id:       areaId,
-               Pub:      new([32]byte),
                Subs:     subs,
                Exec:     cfg.Exec,
                Incoming: cfg.Incoming,
        }
-       pub, err := Base32Codec.DecodeString(cfg.Pub)
-       if err != nil {
-               return nil, err
-       }
-       if len(pub) != 32 {
-               return nil, errors.New("Invalid pub size")
+       if cfg.Pub != nil {
+               pub, err := Base32Codec.DecodeString(*cfg.Pub)
+               if err != nil {
+                       return nil, err
+               }
+               if len(pub) != 32 {
+                       return nil, errors.New("Invalid pub size")
+               }
+               area.Pub = new([32]byte)
+               copy(area.Pub[:], pub)
        }
-       copy(area.Pub[:], pub)
        if cfg.Prv != nil {
                prv, err := Base32Codec.DecodeString(*cfg.Prv)
                if err != nil {
@@ -473,13 +455,11 @@ func NewArea(ctx *Ctx, name string, cfg *AreaJSON) (*Area, error) {
                area.Prv = new([32]byte)
                copy(area.Prv[:], prv)
        }
-       if cfg.AllowUnknown != nil {
-               area.AllowUnknown = *cfg.AllowUnknown
-       }
+       area.AllowUnknown = cfg.AllowUnknown
        return &area, nil
 }
 
-func CfgParse(data []byte) (*Ctx, error) {
+func CfgParse(data []byte) (*CfgJSON, error) {
        var err error
        if bytes.Compare(data[:8], MagicNNCPBv3.B[:]) == 0 {
                os.Stderr.WriteString("Passphrase:") // #nosec G104
@@ -506,14 +486,17 @@ func CfgParse(data []byte) (*Ctx, error) {
                return nil, err
        }
        var cfgJSON CfgJSON
-       if err = json.Unmarshal(marshaled, &cfgJSON); err != nil {
-               return nil, err
-       }
+       err = json.Unmarshal(marshaled, &cfgJSON)
+       return &cfgJSON, err
+}
+
+func Cfg2Ctx(cfgJSON *CfgJSON) (*Ctx, error) {
        if _, exists := cfgJSON.Neigh["self"]; !exists {
                return nil, errors.New("self neighbour missing")
        }
        var self *NodeOur
        if cfgJSON.Self != nil {
+               var err error
                self, err = NewNodeOur(cfgJSON.Self)
                if err != nil {
                        return nil, err
@@ -528,8 +511,8 @@ func CfgParse(data []byte) (*Ctx, error) {
                return nil, errors.New("Log path must be absolute")
        }
        var umaskForce *int
-       if cfgJSON.Umask != "" {
-               r, err := strconv.ParseUint(cfgJSON.Umask, 8, 16)
+       if cfgJSON.Umask != nil {
+               r, err := strconv.ParseUint(*cfgJSON.Umask, 8, 16)
                if err != nil {
                        return nil, err
                }
diff --git a/src/cfgdir.go b/src/cfgdir.go
new file mode 100644 (file)
index 0000000..ec1e58c
--- /dev/null
@@ -0,0 +1,903 @@
+/*
+NNCP -- Node to Node copy, utilities for store-and-forward data exchange
+Copyright (C) 2016-2021 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package nncp
+
+import (
+       "fmt"
+       "io/ioutil"
+       "os"
+       "path/filepath"
+       "sort"
+       "strconv"
+       "strings"
+)
+
+func cfgDirMkdir(dst ...string) error {
+       return os.MkdirAll(filepath.Join(dst...), os.FileMode(0777))
+}
+
+func cfgDirSave(v interface{}, dst ...string) error {
+       var r string
+       switch v := v.(type) {
+       case *string:
+               if v == nil {
+                       return nil
+               }
+               r = *v
+       case string:
+               r = v
+       case *int:
+               if v == nil {
+                       return nil
+               }
+               r = strconv.Itoa(*v)
+       case *uint:
+               if v == nil {
+                       return nil
+               }
+               r = strconv.Itoa(int(*v))
+       case *uint64:
+               if v == nil {
+                       return nil
+               }
+               r = strconv.FormatUint(*v, 10)
+       case int:
+               r = strconv.Itoa(v)
+       default:
+               panic("unsupported value type")
+       }
+       mode := os.FileMode(0666)
+       if strings.HasSuffix(dst[len(dst)-1], "prv") {
+               mode = os.FileMode(0600)
+       }
+       return ioutil.WriteFile(filepath.Join(dst...), []byte(r+"\n"), mode)
+}
+
+func cfgDirTouch(dst ...string) error {
+       if fd, err := os.Create(filepath.Join(dst...)); err == nil {
+               fd.Close()
+       } else {
+               return err
+       }
+       return nil
+}
+
+func CfgToDir(dst string, cfg *CfgJSON) (err error) {
+       if err = cfgDirMkdir(dst); err != nil {
+               return
+       }
+       if err = cfgDirSave(cfg.Spool, dst, "spool"); err != nil {
+               return
+       }
+       if err = cfgDirSave(cfg.Log, dst, "log"); err != nil {
+               return
+       }
+       if err = cfgDirSave(cfg.Umask, dst, "umask"); err != nil {
+               return
+       }
+       if cfg.OmitPrgrs {
+               if err = cfgDirTouch(dst, "noprogress"); err != nil {
+                       return
+               }
+       }
+       if cfg.NoHdr {
+               if err = cfgDirTouch(dst, "nohdr"); err != nil {
+                       return
+               }
+       }
+
+       if len(cfg.MCDRxIfis) > 0 {
+               if err = cfgDirSave(
+                       strings.Join(cfg.MCDRxIfis, "\n"),
+                       dst, "mcd-listen",
+               ); err != nil {
+                       return
+               }
+       }
+       if len(cfg.MCDTxIfis) > 0 {
+               if err = cfgDirMkdir(dst, "mcd-send"); err != nil {
+                       return
+               }
+               for ifi, t := range cfg.MCDTxIfis {
+                       if err = cfgDirSave(t, dst, "mcd-send", ifi); err != nil {
+                               return
+                       }
+               }
+       }
+
+       if cfg.Notify != nil {
+               if cfg.Notify.File != nil {
+                       if err = cfgDirMkdir(dst, "notify", "file"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(
+                               cfg.Notify.File.From,
+                               dst, "notify", "file", "from",
+                       ); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(
+                               cfg.Notify.File.To,
+                               dst, "notify", "file", "to",
+                       ); err != nil {
+                               return
+                       }
+               }
+               if cfg.Notify.Freq != nil {
+                       if err = cfgDirMkdir(dst, "notify", "freq"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(
+                               cfg.Notify.Freq.From,
+                               dst, "notify", "freq", "from",
+                       ); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(
+                               cfg.Notify.Freq.To,
+                               dst, "notify", "freq", "to",
+                       ); err != nil {
+                               return
+                       }
+               }
+               for k, v := range cfg.Notify.Exec {
+                       if err = cfgDirMkdir(dst, "notify", "exec", k); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(v.From, dst, "notify", "exec", k, "from"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(v.To, dst, "notify", "exec", k, "to"); err != nil {
+                               return
+                       }
+               }
+       }
+
+       if err = cfgDirMkdir(dst, "self"); err != nil {
+               return
+       }
+       if err = cfgDirSave(cfg.Self.Id, dst, "self", "id"); err != nil {
+               return
+       }
+       if err = cfgDirSave(cfg.Self.ExchPub, dst, "self", "exchpub"); err != nil {
+               return
+       }
+       if err = cfgDirSave(cfg.Self.ExchPrv, dst, "self", "exchprv"); err != nil {
+               return
+       }
+       if err = cfgDirSave(cfg.Self.SignPub, dst, "self", "signpub"); err != nil {
+               return
+       }
+       if err = cfgDirSave(cfg.Self.SignPrv, dst, "self", "signprv"); err != nil {
+               return
+       }
+       if err = cfgDirSave(cfg.Self.NoisePub, dst, "self", "noisepub"); err != nil {
+               return
+       }
+       if err = cfgDirSave(cfg.Self.NoisePrv, dst, "self", "noiseprv"); err != nil {
+               return
+       }
+
+       for name, n := range cfg.Neigh {
+               if err = cfgDirMkdir(dst, "neigh", name); err != nil {
+                       return
+               }
+               if err = cfgDirSave(n.Id, dst, "neigh", name, "id"); err != nil {
+                       return
+               }
+               if err = cfgDirSave(n.ExchPub, dst, "neigh", name, "exchpub"); err != nil {
+                       return
+               }
+               if err = cfgDirSave(n.SignPub, dst, "neigh", name, "signpub"); err != nil {
+                       return
+               }
+               if err = cfgDirSave(n.NoisePub, dst, "neigh", name, "noisepub"); err != nil {
+                       return
+               }
+               if err = cfgDirSave(n.Incoming, dst, "neigh", name, "incoming"); err != nil {
+                       return
+               }
+
+               if len(n.Exec) > 0 {
+                       if err = cfgDirMkdir(dst, "neigh", name, "exec"); err != nil {
+                               return
+                       }
+                       for k, v := range n.Exec {
+                               if err = cfgDirSave(
+                                       strings.Join(v, "\n"),
+                                       dst, "neigh", name, "exec", k,
+                               ); err != nil {
+                                       return
+                               }
+                       }
+               }
+
+               if n.Freq != nil {
+                       if err = cfgDirMkdir(dst, "neigh", name, "freq"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(
+                               n.Freq.Path,
+                               dst, "neigh", name, "freq", "path",
+                       ); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(
+                               n.Freq.Chunked,
+                               dst, "neigh", name, "freq", "chunked",
+                       ); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(
+                               n.Freq.MinSize,
+                               dst, "neigh", name, "freq", "minsize",
+                       ); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(
+                               n.Freq.MaxSize,
+                               dst, "neigh", name, "freq", "maxsize",
+                       ); err != nil {
+                               return
+                       }
+               }
+
+               if len(n.Via) > 0 {
+                       if err = cfgDirSave(
+                               strings.Join(n.Via, "\n"),
+                               dst, "neigh", name, "via",
+                       ); err != nil {
+                               return
+                       }
+               }
+
+               if len(n.Addrs) > 0 {
+                       if err = cfgDirMkdir(dst, "neigh", name, "addrs"); err != nil {
+                               return
+                       }
+                       for k, v := range n.Addrs {
+                               if err = cfgDirSave(v, dst, "neigh", name, "addrs", k); err != nil {
+                                       return
+                               }
+                       }
+               }
+
+               if err = cfgDirSave(n.RxRate, dst, "neigh", name, "rxrate"); err != nil {
+                       return
+               }
+               if err = cfgDirSave(n.TxRate, dst, "neigh", name, "txrate"); err != nil {
+                       return
+               }
+               if err = cfgDirSave(n.OnlineDeadline, dst, "neigh", name, "onlinedeadline"); err != nil {
+                       return
+               }
+               if err = cfgDirSave(n.MaxOnlineTime, dst, "neigh", name, "maxonlinetime"); err != nil {
+                       return
+               }
+
+               for i, call := range n.Calls {
+                       is := strconv.Itoa(i)
+                       if err = cfgDirMkdir(dst, "neigh", name, "calls", is); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(call.Cron, dst, "neigh", name, "calls", is, "cron"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(call.Nice, dst, "neigh", name, "calls", is, "nice"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(call.Xx, dst, "neigh", name, "calls", is, "xx"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(call.RxRate, dst, "neigh", name, "calls", is, "rxrate"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(call.TxRate, dst, "neigh", name, "calls", is, "txrate"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(call.Addr, dst, "neigh", name, "calls", is, "addr"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(call.OnlineDeadline, dst, "neigh", name, "calls", is, "onlinedeadline"); err != nil {
+                               return
+                       }
+                       if err = cfgDirSave(call.MaxOnlineTime, dst, "neigh", name, "calls", is, "maxonlinetime"); err != nil {
+                               return
+                       }
+                       if call.WhenTxExists {
+                               if err = cfgDirTouch(dst, "neigh", name, "calls", is, "when-tx-exists"); err != nil {
+                                       return
+                               }
+                       }
+                       if call.NoCK {
+                               if err = cfgDirTouch(dst, "neigh", name, "calls", is, "nock"); err != nil {
+                                       return
+                               }
+                       }
+                       if call.MCDIgnore {
+                               if err = cfgDirTouch(dst, "neigh", name, "calls", is, "mcd-ignore"); err != nil {
+                                       return
+                               }
+                       }
+                       if call.AutoToss {
+                               if err = cfgDirTouch(dst, "neigh", name, "calls", is, "autotoss"); err != nil {
+                                       return
+                               }
+                       }
+                       if call.AutoTossDoSeen {
+                               if err = cfgDirTouch(dst, "neigh", name, "calls", is, "autotoss-doseen"); err != nil {
+                                       return
+                               }
+                       }
+                       if call.AutoTossNoFile {
+                               if err = cfgDirTouch(dst, "neigh", name, "calls", is, "autotoss-nofile"); err != nil {
+                                       return
+                               }
+                       }
+                       if call.AutoTossNoFreq {
+                               if err = cfgDirTouch(dst, "neigh", name, "calls", is, "autotoss-nofreq"); err != nil {
+                                       return
+                               }
+                       }
+                       if call.AutoTossNoExec {
+                               if err = cfgDirTouch(dst, "neigh", name, "calls", is, "autotoss-noexec"); err != nil {
+                                       return
+                               }
+                       }
+                       if call.AutoTossNoTrns {
+                               if err = cfgDirTouch(dst, "neigh", name, "calls", is, "autotoss-notrns"); err != nil {
+                                       return
+                               }
+                       }
+                       if call.AutoTossNoArea {
+                               if err = cfgDirTouch(dst, "neigh", name, "calls", is, "autotoss-noarea"); err != nil {
+                                       return
+                               }
+                       }
+               }
+       }
+
+       for name, a := range cfg.Areas {
+               if err = cfgDirMkdir(dst, "areas", name); err != nil {
+                       return
+               }
+               if err = cfgDirSave(a.Id, dst, "areas", name, "id"); err != nil {
+                       return
+               }
+               if err = cfgDirSave(a.Pub, dst, "areas", name, "pub"); err != nil {
+                       return
+               }
+               if err = cfgDirSave(a.Prv, dst, "areas", name, "prv"); err != nil {
+                       return
+               }
+               if err = cfgDirSave(a.Incoming, dst, "areas", name, "incoming"); err != nil {
+                       return
+               }
+               if a.AllowUnknown {
+                       if err = cfgDirTouch(dst, "areas", name, "allow-unknown"); err != nil {
+                               return
+                       }
+               }
+               if len(a.Exec) > 0 {
+                       if err = cfgDirMkdir(dst, "areas", name, "exec"); err != nil {
+                               return
+                       }
+                       for k, v := range a.Exec {
+                               if err = cfgDirSave(
+                                       strings.Join(v, "\n"),
+                                       dst, "areas", name, "exec", k,
+                               ); err != nil {
+                                       return
+                               }
+                       }
+               }
+               if len(a.Subs) > 0 {
+                       if err = cfgDirSave(
+                               strings.Join(a.Subs, "\n"),
+                               dst, "areas", name, "subs",
+                       ); err != nil {
+                               return
+                       }
+               }
+       }
+
+       return
+}
+
+func cfgDirLoad(src ...string) (v string, exists bool, err error) {
+       b, err := ioutil.ReadFile(filepath.Join(src...))
+       if err != nil {
+               if os.IsNotExist(err) {
+                       return "", false, nil
+               }
+               return "", false, err
+       }
+       return strings.TrimSuffix(string(b), "\n"), true, nil
+}
+
+func cfgDirLoadMust(src ...string) (v string, err error) {
+       s, exists, err := cfgDirLoad(src...)
+       if err != nil {
+               return "", err
+       }
+       if !exists {
+               return "", fmt.Errorf("required \"%s\" does not exist", src[len(src)-1])
+       }
+       return s, nil
+}
+
+func cfgDirLoadOpt(src ...string) (v *string, err error) {
+       s, exists, err := cfgDirLoad(src...)
+       if err != nil {
+               return nil, err
+       }
+       if !exists {
+               return nil, nil
+       }
+       return &s, nil
+}
+
+func cfgDirLoadIntOpt(src ...string) (i64 *int64, err error) {
+       s, err := cfgDirLoadOpt(src...)
+       if err != nil {
+               return nil, err
+       }
+       if s == nil {
+               return nil, nil
+       }
+       i, err := strconv.ParseInt(*s, 10, 64)
+       if err != nil {
+               return nil, err
+       }
+       return &i, nil
+}
+
+func cfgDirExists(src ...string) bool {
+       if _, err := os.Stat(filepath.Join(src...)); err == nil {
+               return true
+       }
+       return false
+}
+
+func cfgDirReadFromTo(src ...string) (*FromToJSON, error) {
+       fromTo := FromToJSON{}
+
+       var err error
+       fromTo.From, err = cfgDirLoadMust(append(src, "from")...)
+       if err != nil {
+               return nil, err
+       }
+
+       fromTo.To, err = cfgDirLoadMust(append(src, "to")...)
+       if err != nil {
+               return nil, err
+       }
+
+       return &fromTo, nil
+}
+
+func DirToCfg(src string) (*CfgJSON, error) {
+       cfg := CfgJSON{}
+       var err error
+
+       cfg.Spool, err = cfgDirLoadMust(src, "spool")
+       if err != nil {
+               return nil, err
+       }
+       cfg.Log, err = cfgDirLoadMust(src, "log")
+       if err != nil {
+               return nil, err
+       }
+
+       if cfg.Umask, err = cfgDirLoadOpt(src, "umask"); err != nil {
+               return nil, err
+       }
+       cfg.OmitPrgrs = cfgDirExists(src, "noprogress")
+       cfg.NoHdr = cfgDirExists(src, "nohdr")
+
+       sp, err := cfgDirLoadOpt(src, "mcd-listen")
+       if err != nil {
+               return nil, err
+       }
+       if sp != nil {
+               cfg.MCDRxIfis = strings.Split(*sp, "\n")
+       }
+
+       fis, err := ioutil.ReadDir(filepath.Join(src, "mcd-send"))
+       if err != nil && !os.IsNotExist(err) {
+               return nil, err
+       }
+       if len(fis) > 0 {
+               cfg.MCDTxIfis = make(map[string]int, len(fis))
+       }
+       for _, fi := range fis {
+               n := fi.Name()
+               if n[0] == '.' {
+                       continue
+               }
+               b, err := ioutil.ReadFile(filepath.Join(src, "mcd-send", fi.Name()))
+               if err != nil {
+                       return nil, err
+               }
+               i, err := strconv.Atoi(strings.TrimSuffix(string(b), "\n"))
+               if err != nil {
+                       return nil, err
+               }
+               cfg.MCDTxIfis[n] = i
+       }
+
+       notify := NotifyJSON{Exec: make(map[string]*FromToJSON)}
+       if cfgDirExists(src, "notify", "file") {
+               if notify.File, err = cfgDirReadFromTo(src, "notify", "file"); err != nil {
+                       return nil, err
+               }
+       }
+       if cfgDirExists(src, "notify", "freq") {
+               if notify.Freq, err = cfgDirReadFromTo(src, "notify", "freq"); err != nil {
+                       return nil, err
+               }
+       }
+       fis, err = ioutil.ReadDir(filepath.Join(src, "notify", "exec"))
+       if err != nil && !os.IsNotExist(err) {
+               return nil, err
+       }
+       for _, fi := range fis {
+               n := fi.Name()
+               if n[0] == '.' || !fi.IsDir() {
+                       continue
+               }
+               if notify.Exec[fi.Name()], err = cfgDirReadFromTo(src, "notify", "exec", n); err != nil {
+                       return nil, err
+               }
+       }
+       if notify.File != nil || notify.Freq != nil || len(notify.Exec) > 0 {
+               cfg.Notify = &notify
+       }
+
+       self := NodeOurJSON{}
+       if self.Id, err = cfgDirLoadMust(src, "self", "id"); err != nil {
+               return nil, err
+       }
+       if self.ExchPub, err = cfgDirLoadMust(src, "self", "exchpub"); err != nil {
+               return nil, err
+       }
+       if self.ExchPrv, err = cfgDirLoadMust(src, "self", "exchprv"); err != nil {
+               return nil, err
+       }
+       if self.SignPub, err = cfgDirLoadMust(src, "self", "signpub"); err != nil {
+               return nil, err
+       }
+       if self.SignPrv, err = cfgDirLoadMust(src, "self", "signprv"); err != nil {
+               return nil, err
+       }
+       if self.NoisePub, err = cfgDirLoadMust(src, "self", "noisepub"); err != nil {
+               return nil, err
+       }
+       if self.NoisePrv, err = cfgDirLoadMust(src, "self", "noiseprv"); err != nil {
+               return nil, err
+       }
+       cfg.Self = &self
+
+       cfg.Neigh = make(map[string]NodeJSON)
+       fis, err = ioutil.ReadDir(filepath.Join(src, "neigh"))
+       if err != nil && !os.IsNotExist(err) {
+               return nil, err
+       }
+       for _, fi := range fis {
+               n := fi.Name()
+               if n[0] == '.' {
+                       continue
+               }
+               node := NodeJSON{}
+               if node.Id, err = cfgDirLoadMust(src, "neigh", n, "id"); err != nil {
+                       return nil, err
+               }
+               if node.ExchPub, err = cfgDirLoadMust(src, "neigh", n, "exchpub"); err != nil {
+                       return nil, err
+               }
+               if node.SignPub, err = cfgDirLoadMust(src, "neigh", n, "signpub"); err != nil {
+                       return nil, err
+               }
+               if node.NoisePub, err = cfgDirLoadOpt(src, "neigh", n, "noisepub"); err != nil {
+                       return nil, err
+               }
+               if node.Incoming, err = cfgDirLoadOpt(src, "neigh", n, "incoming"); err != nil {
+                       return nil, err
+               }
+
+               node.Exec = make(map[string][]string)
+               fis2, err := ioutil.ReadDir(filepath.Join(src, "neigh", n, "exec"))
+               if err != nil && !os.IsNotExist(err) {
+                       return nil, err
+               }
+               for _, fi2 := range fis2 {
+                       n2 := fi2.Name()
+                       if n2[0] == '.' {
+                               continue
+                       }
+                       s, err := cfgDirLoadMust(src, "neigh", n, "exec", n2)
+                       if err != nil {
+                               return nil, err
+                       }
+                       node.Exec[n2] = strings.Split(s, "\n")
+               }
+
+               if cfgDirExists(src, "neigh", n, "freq") {
+                       node.Freq = &NodeFreqJSON{}
+                       if node.Freq.Path, err = cfgDirLoadOpt(src, "neigh", n, "freq", "path"); err != nil {
+                               return nil, err
+                       }
+
+                       i64, err := cfgDirLoadIntOpt(src, "neigh", n, "freq", "chunked")
+                       if err != nil {
+                               return nil, err
+                       }
+                       if i64 != nil {
+                               i := uint64(*i64)
+                               node.Freq.Chunked = &i
+                       }
+
+                       i64, err = cfgDirLoadIntOpt(src, "neigh", n, "freq", "minsize")
+                       if err != nil {
+                               return nil, err
+                       }
+                       if i64 != nil {
+                               i := uint64(*i64)
+                               node.Freq.MinSize = &i
+                       }
+
+                       i64, err = cfgDirLoadIntOpt(src, "neigh", n, "freq", "maxsize")
+                       if err != nil {
+                               return nil, err
+                       }
+                       if i64 != nil {
+                               i := uint64(*i64)
+                               node.Freq.MaxSize = &i
+                       }
+               }
+
+               via, err := cfgDirLoadOpt(src, "neigh", n, "via")
+               if err != nil {
+                       return nil, err
+               }
+               if via != nil {
+                       node.Via = strings.Split(*via, "\n")
+               }
+
+               node.Addrs = make(map[string]string)
+               fis2, err = ioutil.ReadDir(filepath.Join(src, "neigh", n, "addrs"))
+               if err != nil && !os.IsNotExist(err) {
+                       return nil, err
+               }
+               for _, fi2 := range fis2 {
+                       n2 := fi2.Name()
+                       if n2[0] == '.' {
+                               continue
+                       }
+                       if node.Addrs[n2], err = cfgDirLoadMust(src, "neigh", n, "addrs", n2); err != nil {
+                               return nil, err
+                       }
+               }
+
+               i64, err := cfgDirLoadIntOpt(src, "neigh", n, "rxrate")
+               if err != nil {
+                       return nil, err
+               }
+               if i64 != nil {
+                       i := int(*i64)
+                       node.RxRate = &i
+               }
+
+               i64, err = cfgDirLoadIntOpt(src, "neigh", n, "txrate")
+               if err != nil {
+                       return nil, err
+               }
+               if i64 != nil {
+                       i := int(*i64)
+                       node.TxRate = &i
+               }
+
+               i64, err = cfgDirLoadIntOpt(src, "neigh", n, "onlinedeadline")
+               if err != nil {
+                       return nil, err
+               }
+               if i64 != nil {
+                       i := uint(*i64)
+                       node.OnlineDeadline = &i
+               }
+
+               i64, err = cfgDirLoadIntOpt(src, "neigh", n, "maxonlinetime")
+               if err != nil {
+                       return nil, err
+               }
+               if i64 != nil {
+                       i := uint(*i64)
+                       node.MaxOnlineTime = &i
+               }
+
+               fis2, err = ioutil.ReadDir(filepath.Join(src, "neigh", n, "calls"))
+               if err != nil && !os.IsNotExist(err) {
+                       return nil, err
+               }
+               callsIdx := make([]int, 0, len(fis2))
+               for _, fi2 := range fis2 {
+                       n2 := fi2.Name()
+                       if !fi2.IsDir() {
+                               continue
+                       }
+                       i, err := strconv.Atoi(n2)
+                       if err != nil {
+                               continue
+                       }
+                       callsIdx = append(callsIdx, i)
+               }
+               sort.Ints(callsIdx)
+               for _, i := range callsIdx {
+                       call := CallJSON{}
+                       is := strconv.Itoa(i)
+                       if call.Cron, err = cfgDirLoadMust(
+                               src, "neigh", n, "calls", is, "cron",
+                       ); err != nil {
+                               return nil, err
+                       }
+                       if call.Nice, err = cfgDirLoadOpt(
+                               src, "neigh", n, "calls", is, "nice",
+                       ); err != nil {
+                               return nil, err
+                       }
+                       if call.Xx, err = cfgDirLoadOpt(
+                               src, "neigh", n, "calls", is, "xx",
+                       ); err != nil {
+                               return nil, err
+                       }
+
+                       i64, err = cfgDirLoadIntOpt(src, "neigh", n, "calls", is, "rxrate")
+                       if err != nil {
+                               return nil, err
+                       }
+                       if i64 != nil {
+                               i := int(*i64)
+                               call.RxRate = &i
+                       }
+
+                       i64, err = cfgDirLoadIntOpt(src, "neigh", n, "calls", is, "txrate")
+                       if err != nil {
+                               return nil, err
+                       }
+                       if i64 != nil {
+                               i := int(*i64)
+                               call.TxRate = &i
+                       }
+
+                       if call.Addr, err = cfgDirLoadOpt(
+                               src, "neigh", n, "calls", is, "addr",
+                       ); err != nil {
+                               return nil, err
+                       }
+
+                       i64, err = cfgDirLoadIntOpt(src, "neigh", n, "calls", is, "onlinedeadline")
+                       if err != nil {
+                               return nil, err
+                       }
+                       if i64 != nil {
+                               i := uint(*i64)
+                               call.OnlineDeadline = &i
+                       }
+
+                       i64, err = cfgDirLoadIntOpt(src, "neigh", n, "calls", is, "maxonlinetime")
+                       if err != nil {
+                               return nil, err
+                       }
+                       if i64 != nil {
+                               i := uint(*i64)
+                               call.MaxOnlineTime = &i
+                       }
+
+                       if cfgDirExists(src, "neigh", n, "calls", is, "when-tx-exists") {
+                               call.WhenTxExists = true
+                       }
+                       if cfgDirExists(src, "neigh", n, "calls", is, "nock") {
+                               call.NoCK = true
+                       }
+                       if cfgDirExists(src, "neigh", n, "calls", is, "mcd-ignore") {
+                               call.MCDIgnore = true
+                       }
+                       if cfgDirExists(src, "neigh", n, "calls", is, "autotoss") {
+                               call.AutoToss = true
+                       }
+                       if cfgDirExists(src, "neigh", n, "calls", is, "autotoss-doseen") {
+                               call.AutoTossDoSeen = true
+                       }
+                       if cfgDirExists(src, "neigh", n, "calls", is, "autotoss-nofile") {
+                               call.AutoTossNoFile = true
+                       }
+                       if cfgDirExists(src, "neigh", n, "calls", is, "autotoss-nofreq") {
+                               call.AutoTossNoFreq = true
+                       }
+                       if cfgDirExists(src, "neigh", n, "calls", is, "autotoss-noexec") {
+                               call.AutoTossNoExec = true
+                       }
+                       if cfgDirExists(src, "neigh", n, "calls", is, "autotoss-notrns") {
+                               call.AutoTossNoTrns = true
+                       }
+                       if cfgDirExists(src, "neigh", n, "calls", is, "autotoss-noarea") {
+                               call.AutoTossNoArea = true
+                       }
+                       node.Calls = append(node.Calls, call)
+               }
+               cfg.Neigh[n] = node
+       }
+
+       cfg.Areas = make(map[string]AreaJSON)
+       fis, err = ioutil.ReadDir(filepath.Join(src, "areas"))
+       if err != nil && !os.IsNotExist(err) {
+               return nil, err
+       }
+       for _, fi := range fis {
+               n := fi.Name()
+               if n[0] == '.' {
+                       continue
+               }
+               area := AreaJSON{}
+               if area.Id, err = cfgDirLoadMust(src, "areas", n, "id"); err != nil {
+                       return nil, err
+               }
+               if area.Pub, err = cfgDirLoadOpt(src, "areas", n, "pub"); err != nil {
+                       return nil, err
+               }
+               if area.Prv, err = cfgDirLoadOpt(src, "areas", n, "prv"); err != nil {
+                       return nil, err
+               }
+
+               subs, err := cfgDirLoadOpt(src, "areas", n, "subs")
+               if err != nil {
+                       return nil, err
+               }
+               if subs != nil {
+                       area.Subs = strings.Split(*subs, "\n")
+               }
+
+               area.Exec = make(map[string][]string)
+               fis2, err := ioutil.ReadDir(filepath.Join(src, "areas", n, "exec"))
+               if err != nil && !os.IsNotExist(err) {
+                       return nil, err
+               }
+               for _, fi2 := range fis2 {
+                       n2 := fi2.Name()
+                       if n2[0] == '.' {
+                               continue
+                       }
+                       s, err := cfgDirLoadMust(src, "areas", n, "exec", n2)
+                       if err != nil {
+                               return nil, err
+                       }
+                       area.Exec[n2] = strings.Split(s, "\n")
+               }
+
+               if area.Incoming, err = cfgDirLoadOpt(src, "areas", n, "incoming"); err != nil {
+                       return nil, err
+               }
+
+               if cfgDirExists(src, "areas", n, "allow-unknown") {
+                       area.AllowUnknown = true
+               }
+               cfg.Areas[n] = area
+       }
+
+       return &cfg, nil
+}
diff --git a/src/cmd/nncp-cfgdir/main.go b/src/cmd/nncp-cfgdir/main.go
new file mode 100644 (file)
index 0000000..cbc8f8f
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+NNCP -- Node to Node copy, utilities for store-and-forward data exchange
+Copyright (C) 2016-2021 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// Convert NNCP Hjson configuration file to the directory layout.
+package main
+
+import (
+       "flag"
+       "fmt"
+       "io/ioutil"
+       "log"
+       "os"
+
+       "github.com/hjson/hjson-go"
+       "go.cypherpunks.ru/nncp/v7"
+)
+
+func usage() {
+       fmt.Fprintf(os.Stderr, nncp.UsageHeader())
+       fmt.Fprintf(os.Stderr, "nncp-cfgdir -- Convert configuration file to the directory layout.\n\n")
+       fmt.Fprintf(os.Stderr, "Usage: %s [options] [-cfg ...] -dump /path/to/dir\n", os.Args[0])
+       fmt.Fprintf(os.Stderr, "       %s [options] -load /path/to/dir > cfg.hjson\nOptions:\n", os.Args[0])
+       flag.PrintDefaults()
+}
+
+func main() {
+       var (
+               doDump   = flag.Bool("dump", false, "Dump configuration file to the directory")
+               doLoad   = flag.Bool("load", false, "Load directory to create configuration file")
+               cfgPath  = flag.String("cfg", nncp.DefaultCfgPath, "Path to configuration file")
+               version  = flag.Bool("version", false, "Print version information")
+               warranty = flag.Bool("warranty", false, "Print warranty information")
+       )
+       log.SetFlags(log.Lshortfile)
+       flag.Usage = usage
+       flag.Parse()
+       if *warranty {
+               fmt.Println(nncp.Warranty)
+               return
+       }
+       if *version {
+               fmt.Println(nncp.VersionGet())
+               return
+       }
+
+       if (!*doDump && !*doLoad) || flag.NArg() != 1 {
+               usage()
+               os.Exit(1)
+       }
+
+       if *doDump {
+               cfgRaw, err := ioutil.ReadFile(*cfgPath)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               cfg, err := nncp.CfgParse(cfgRaw)
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               if err = nncp.CfgToDir(flag.Arg(0), cfg); err != nil {
+                       log.Fatalln(err)
+               }
+       }
+       if *doLoad {
+               cfg, err := nncp.DirToCfg(flag.Arg(0))
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               if _, err = nncp.Cfg2Ctx(cfg); err != nil {
+                       log.Fatalln(err)
+               }
+               marshaled, err := hjson.MarshalWithOptions(cfg, hjson.EncoderOptions{
+                       Eol:            "\n",
+                       BracesSameLine: true,
+                       QuoteAlways:    false,
+                       IndentBy:       "  ",
+                       AllowMinusZero: false,
+               })
+               if err != nil {
+                       log.Fatalln(err)
+               }
+               os.Stdout.Write(marshaled)
+               os.Stdout.WriteString("\n")
+       }
+}
index cb5c4282b868627e32691fe1e78aff346891bfa5..4d762d290cadfb83fde02a36e5bd74b8850a50d2 100644 (file)
@@ -88,9 +88,7 @@ func (ctx *Ctx) ensureRxDir(nodeId *NodeId) error {
 }
 
 func CtxFromCmdline(
-       cfgPath,
-       spoolPath,
-       logPath string,
+       cfgPath, spoolPath, logPath string,
        quiet, showPrgrs, omitPrgrs, debug bool,
 ) (*Ctx, error) {
        env := os.Getenv(CfgPathEnv)
@@ -100,11 +98,27 @@ func CtxFromCmdline(
        if showPrgrs && omitPrgrs {
                return nil, errors.New("simultaneous -progress and -noprogress")
        }
-       cfgRaw, err := ioutil.ReadFile(cfgPath)
+       fi, err := os.Stat(cfgPath)
        if err != nil {
                return nil, err
        }
-       ctx, err := CfgParse(cfgRaw)
+       var cfg *CfgJSON
+       if fi.IsDir() {
+               cfg, err = DirToCfg(cfgPath)
+               if err != nil {
+                       return nil, err
+               }
+       } else {
+               cfgRaw, err := ioutil.ReadFile(cfgPath)
+               if err != nil {
+                       return nil, err
+               }
+               cfg, err = CfgParse(cfgRaw)
+               if err != nil {
+                       return nil, err
+               }
+       }
+       ctx, err := Cfg2Ctx(cfg)
        if err != nil {
                return nil, err
        }
index 2c087b3511cb45d594046228828ceaf23d3ad9cd..3bfcb3a52e1a8763fba333046f9b56ba626d7ce6 100644 (file)
@@ -40,7 +40,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.`
 const Base32Encoded32Len = 52
 
 var (
-       Version string = "7.2.1"
+       Version string = "7.3.0"
 
        Base32Codec *base32.Encoding = base32.StdEncoding.WithPadding(base32.NoPadding)
 )