Zettelstore

Check-in Differences
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Difference From version-0.0.13 To version-0.0.14

2021-07-23
16:43
Increase version to 0.0.15-dev to begin next development cycle ... (check-in: ba65777850 user: stern tags: trunk)
11:02
Version 0.0.14 ... (check-in: 6fe53d5db2 user: stern tags: trunk, release, version-0.0.14)
2021-07-22
13:50
Add client API for retrieving zettel links ... (check-in: e43ff68174 user: stern tags: trunk)
2021-06-07
09:11
Increase version to 0.0.14-dev to begin next development cycle ... (check-in: 7dd6f4dd5c user: stern tags: trunk)
2021-06-01
12:35
Version 0.0.13 ... (check-in: 11d9b6da63 user: stern tags: trunk, release, version-0.0.13)
10:14
Log output while starting Command Line Server ... (check-in: 968a91bbaa user: stern tags: trunk)

Changes to Makefile.

     3      3   ##
     4      4   ## This file is part of zettelstore.
     5      5   ##
     6      6   ## Zettelstore is licensed under the latest version of the EUPL (European Union
     7      7   ## Public License). Please see file LICENSE.txt for your rights and obligations
     8      8   ## under this license.
     9      9   
    10         -.PHONY:  check build release clean
           10  +.PHONY:  check api build release clean
    11     11   
    12     12   check:
    13     13   	go run tools/build.go check
    14     14   
           15  +api:
           16  +	go run tools/build.go testapi
           17  +
    15     18   version:
    16     19   	@echo $(shell go run tools/build.go version)
    17     20   
    18     21   build:
    19     22   	go run tools/build.go build
    20     23   
    21     24   release:
    22     25   	go run tools/build.go release
    23     26   
    24     27   clean:
    25     28   	go run tools/build.go clean

Changes to VERSION.

     1         -0.0.13
            1  +0.0.14

Added api/api.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package api contains common definition used for client and server.
           12  +package api
           13  +
           14  +// AuthJSON contains the result of an authentication call.
           15  +type AuthJSON struct {
           16  +	Token   string `json:"token"`
           17  +	Type    string `json:"token_type"`
           18  +	Expires int    `json:"expires_in"`
           19  +}
           20  +
           21  +// ZidJSON contains the identifier data of a zettel.
           22  +type ZidJSON struct {
           23  +	ID  string `json:"id"`
           24  +	URL string `json:"url"`
           25  +}
           26  +
           27  +// ZidMetaJSON contains the identifier and the metadata of a zettel.
           28  +type ZidMetaJSON struct {
           29  +	ID   string            `json:"id"`
           30  +	URL  string            `json:"url"`
           31  +	Meta map[string]string `json:"meta"`
           32  +}
           33  +
           34  +// ZidMetaRelatedList contains identifier/metadata of a zettel and the same for related zettel
           35  +type ZidMetaRelatedList struct {
           36  +	ID   string            `json:"id"`
           37  +	URL  string            `json:"url"`
           38  +	Meta map[string]string `json:"meta"`
           39  +	List []ZidMetaJSON     `json:"list"`
           40  +}
           41  +
           42  +// ZettelLinksJSON store all links / connections from one zettel to other.
           43  +type ZettelLinksJSON struct {
           44  +	ID    string `json:"id"`
           45  +	URL   string `json:"url"`
           46  +	Links struct {
           47  +		Incoming []ZidJSON `json:"incoming,omitempty"`
           48  +		Outgoing []ZidJSON `json:"outgoing,omitempty"`
           49  +		Local    []string  `json:"local,omitempty"`
           50  +		External []string  `json:"external,omitempty"`
           51  +		Meta     []string  `json:"meta,omitempty"`
           52  +	} `json:"links"`
           53  +	Images struct {
           54  +		Outgoing []ZidJSON `json:"outgoing,omitempty"`
           55  +		Local    []string  `json:"local,omitempty"`
           56  +		External []string  `json:"external,omitempty"`
           57  +	} `json:"images,omitempty"`
           58  +	Cites []string `json:"cites,omitempty"`
           59  +}
           60  +
           61  +// ZettelDataJSON contains all data for a zettel.
           62  +type ZettelDataJSON struct {
           63  +	Meta     map[string]string `json:"meta"`
           64  +	Encoding string            `json:"encoding"`
           65  +	Content  string            `json:"content"`
           66  +}
           67  +
           68  +// ZettelJSON contains all data for a zettel, the identifier, the metadata, and the content.
           69  +type ZettelJSON struct {
           70  +	ID       string            `json:"id"`
           71  +	URL      string            `json:"url"`
           72  +	Meta     map[string]string `json:"meta"`
           73  +	Encoding string            `json:"encoding"`
           74  +	Content  string            `json:"content"`
           75  +}
           76  +
           77  +// ZettelListJSON contains data for a zettel list.
           78  +type ZettelListJSON struct {
           79  +	List []ZettelJSON `json:"list"`
           80  +}
           81  +
           82  +// TagListJSON specifies the list/map of tags
           83  +type TagListJSON struct {
           84  +	Tags map[string][]string `json:"tags"`
           85  +}
           86  +
           87  +// RoleListJSON specifies the list of roles.
           88  +type RoleListJSON struct {
           89  +	Roles []string `json:"role-list"`
           90  +}

Added api/const.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package api contains common definition used for client and server.
           12  +package api
           13  +
           14  +import (
           15  +	"fmt"
           16  +)
           17  +
           18  +// Additional HTTP constants used.
           19  +const (
           20  +	MethodMove = "MOVE" // HTTP method for renaming a zettel
           21  +
           22  +	HeaderAccept      = "Accept"
           23  +	HeaderContentType = "Content-Type"
           24  +	HeaderDestination = "Destination"
           25  +	HeaderLocation    = "Location"
           26  +)
           27  +
           28  +// Values for HTTP query parameter.
           29  +const (
           30  +	QueryKeyDepth  = "depth"
           31  +	QueryKeyDir    = "dir"
           32  +	QueryKeyFormat = "_format"
           33  +	QueryKeyLimit  = "limit"
           34  +	QueryKeyPart   = "_part"
           35  +)
           36  +
           37  +// Supported dir values.
           38  +const (
           39  +	DirBackward = "backward"
           40  +	DirForward  = "forward"
           41  +)
           42  +
           43  +// Supported format values.
           44  +const (
           45  +	FormatDJSON  = "djson"
           46  +	FormatHTML   = "html"
           47  +	FormatJSON   = "json"
           48  +	FormatNative = "native"
           49  +	FormatRaw    = "raw"
           50  +	FormatText   = "text"
           51  +	FormatZMK    = "zmk"
           52  +)
           53  +
           54  +var formatEncoder = map[string]EncodingEnum{
           55  +	FormatDJSON:  EncoderDJSON,
           56  +	FormatHTML:   EncoderHTML,
           57  +	FormatJSON:   EncoderJSON,
           58  +	FormatNative: EncoderNative,
           59  +	FormatRaw:    EncoderRaw,
           60  +	FormatText:   EncoderText,
           61  +	FormatZMK:    EncoderZmk,
           62  +}
           63  +var encoderFormat = map[EncodingEnum]string{}
           64  +
           65  +func init() {
           66  +	for k, v := range formatEncoder {
           67  +		encoderFormat[v] = k
           68  +	}
           69  +}
           70  +
           71  +// Encoder returns the internal encoder code for the given format string.
           72  +func Encoder(format string) EncodingEnum {
           73  +	if e, ok := formatEncoder[format]; ok {
           74  +		return e
           75  +	}
           76  +	return EncoderUnknown
           77  +}
           78  +
           79  +// EncodingEnum lists all valid encoder keys.
           80  +type EncodingEnum uint8
           81  +
           82  +// Values for EncoderEnum
           83  +const (
           84  +	EncoderUnknown EncodingEnum = iota
           85  +	EncoderDJSON
           86  +	EncoderHTML
           87  +	EncoderJSON
           88  +	EncoderNative
           89  +	EncoderRaw
           90  +	EncoderText
           91  +	EncoderZmk
           92  +)
           93  +
           94  +// String representation of an encoder key.
           95  +func (e EncodingEnum) String() string {
           96  +	if f, ok := encoderFormat[e]; ok {
           97  +		return f
           98  +	}
           99  +	return fmt.Sprintf("*Unknown*(%d)", e)
          100  +}
          101  +
          102  +// Supported part values.
          103  +const (
          104  +	PartID      = "id"
          105  +	PartMeta    = "meta"
          106  +	PartContent = "content"
          107  +	PartZettel  = "zettel"
          108  +)

Added api/urlbuilder.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package api contains common definition used for client and server.
           12  +package api
           13  +
           14  +import (
           15  +	"net/url"
           16  +	"strings"
           17  +
           18  +	"zettelstore.de/z/domain/id"
           19  +)
           20  +
           21  +type urlQuery struct{ key, val string }
           22  +
           23  +// URLBuilder should be used to create zettelstore URLs.
           24  +type URLBuilder struct {
           25  +	prefix   string
           26  +	key      byte
           27  +	path     []string
           28  +	query    []urlQuery
           29  +	fragment string
           30  +}
           31  +
           32  +// NewURLBuilder creates a new URL builder with the given prefix and key.
           33  +func NewURLBuilder(prefix string, key byte) *URLBuilder {
           34  +	return &URLBuilder{prefix: prefix, key: key}
           35  +}
           36  +
           37  +// Clone an URLBuilder
           38  +func (ub *URLBuilder) Clone() *URLBuilder {
           39  +	cpy := new(URLBuilder)
           40  +	cpy.key = ub.key
           41  +	if len(ub.path) > 0 {
           42  +		cpy.path = make([]string, 0, len(ub.path))
           43  +		cpy.path = append(cpy.path, ub.path...)
           44  +	}
           45  +	if len(ub.query) > 0 {
           46  +		cpy.query = make([]urlQuery, 0, len(ub.query))
           47  +		cpy.query = append(cpy.query, ub.query...)
           48  +	}
           49  +	cpy.fragment = ub.fragment
           50  +	return cpy
           51  +}
           52  +
           53  +// SetZid sets the zettel identifier.
           54  +func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder {
           55  +	if len(ub.path) > 0 {
           56  +		panic("Cannot add Zid")
           57  +	}
           58  +	ub.path = append(ub.path, zid.String())
           59  +	return ub
           60  +}
           61  +
           62  +// AppendPath adds a new path element
           63  +func (ub *URLBuilder) AppendPath(p string) *URLBuilder {
           64  +	ub.path = append(ub.path, p)
           65  +	return ub
           66  +}
           67  +
           68  +// AppendQuery adds a new query parameter
           69  +func (ub *URLBuilder) AppendQuery(key, value string) *URLBuilder {
           70  +	ub.query = append(ub.query, urlQuery{key, value})
           71  +	return ub
           72  +}
           73  +
           74  +// ClearQuery removes all query parameters.
           75  +func (ub *URLBuilder) ClearQuery() *URLBuilder {
           76  +	ub.query = nil
           77  +	ub.fragment = ""
           78  +	return ub
           79  +}
           80  +
           81  +// SetFragment stores the fragment
           82  +func (ub *URLBuilder) SetFragment(s string) *URLBuilder {
           83  +	ub.fragment = s
           84  +	return ub
           85  +}
           86  +
           87  +// String produces a string value.
           88  +func (ub *URLBuilder) String() string {
           89  +	var sb strings.Builder
           90  +
           91  +	sb.WriteString(ub.prefix)
           92  +	if ub.key != '/' {
           93  +		sb.WriteByte(ub.key)
           94  +	}
           95  +	for _, p := range ub.path {
           96  +		sb.WriteByte('/')
           97  +		sb.WriteString(url.PathEscape(p))
           98  +	}
           99  +	if len(ub.fragment) > 0 {
          100  +		sb.WriteByte('#')
          101  +		sb.WriteString(ub.fragment)
          102  +	}
          103  +	for i, q := range ub.query {
          104  +		if i == 0 {
          105  +			sb.WriteByte('?')
          106  +		} else {
          107  +			sb.WriteByte('&')
          108  +		}
          109  +		sb.WriteString(q.key)
          110  +		sb.WriteByte('=')
          111  +		sb.WriteString(url.QueryEscape(q.val))
          112  +	}
          113  +	return sb.String()
          114  +}

Changes to ast/ast.go.

    28     28   	Zid     id.Zid         // Zettel identification.
    29     29   	InhMeta *meta.Meta     // Metadata of the zettel, with inherited values.
    30     30   	Ast     BlockSlice     // Zettel abstract syntax tree is a sequence of block nodes.
    31     31   }
    32     32   
    33     33   // Node is the interface, all nodes must implement.
    34     34   type Node interface {
    35         -	Accept(v Visitor)
           35  +	WalkChildren(v Visitor)
    36     36   }
    37     37   
    38     38   // BlockNode is the interface that all block nodes must implement.
    39     39   type BlockNode interface {
    40     40   	Node
    41     41   	blockNode()
    42     42   }

Changes to ast/attr_test.go.

     1      1   //-----------------------------------------------------------------------------
     2         -// Copyright (c) 2020 Detlef Stern
            2  +// Copyright (c) 2020-2021 Detlef Stern
     3      3   //
     4      4   // This file is part of zettelstore.
     5      5   //
     6      6   // Zettelstore is licensed under the latest version of the EUPL (European Union
     7      7   // Public License). Please see file LICENSE.txt for your rights and obligations
     8      8   // under this license.
     9      9   //-----------------------------------------------------------------------------
................................................................................
    14     14   import (
    15     15   	"testing"
    16     16   
    17     17   	"zettelstore.de/z/ast"
    18     18   )
    19     19   
    20     20   func TestHasDefault(t *testing.T) {
           21  +	t.Parallel()
    21     22   	attr := &ast.Attributes{}
    22     23   	if attr.HasDefault() {
    23     24   		t.Error("Should not have default attr")
    24     25   	}
    25     26   	attr = &ast.Attributes{Attrs: map[string]string{"-": "value"}}
    26     27   	if !attr.HasDefault() {
    27     28   		t.Error("Should have default attr")
    28     29   	}
    29     30   }
    30     31   
    31     32   func TestAttrClone(t *testing.T) {
           33  +	t.Parallel()
    32     34   	orig := &ast.Attributes{}
    33     35   	clone := orig.Clone()
    34     36   	if len(clone.Attrs) > 0 {
    35     37   		t.Error("Attrs must be empty")
    36     38   	}
    37     39   
    38     40   	orig = &ast.Attributes{Attrs: map[string]string{"": "0", "-": "1", "a": "b"}}

Changes to ast/block.go.

    15     15   
    16     16   // ParaNode contains just a sequence of inline elements.
    17     17   // Another name is "paragraph".
    18     18   type ParaNode struct {
    19     19   	Inlines InlineSlice
    20     20   }
    21     21   
    22         -func (pn *ParaNode) blockNode()       {}
    23         -func (pn *ParaNode) itemNode()        {}
    24         -func (pn *ParaNode) descriptionNode() {}
           22  +func (pn *ParaNode) blockNode()       { /* Just a marker */ }
           23  +func (pn *ParaNode) itemNode()        { /* Just a marker */ }
           24  +func (pn *ParaNode) descriptionNode() { /* Just a marker */ }
    25     25   
    26         -// Accept a visitor and visit the node.
    27         -func (pn *ParaNode) Accept(v Visitor) { v.VisitPara(pn) }
           26  +// WalkChildren walks down the inline elements.
           27  +func (pn *ParaNode) WalkChildren(v Visitor) {
           28  +	WalkInlineSlice(v, pn.Inlines)
           29  +}
    28     30   
    29     31   //--------------------------------------------------------------------------
    30     32   
    31     33   // VerbatimNode contains lines of uninterpreted text
    32     34   type VerbatimNode struct {
    33         -	Code  VerbatimCode
           35  +	Kind  VerbatimKind
    34     36   	Attrs *Attributes
    35     37   	Lines []string
    36     38   }
    37     39   
    38         -// VerbatimCode specifies the format that is applied to code inline nodes.
    39         -type VerbatimCode int
           40  +// VerbatimKind specifies the format that is applied to code inline nodes.
           41  +type VerbatimKind uint8
    40     42   
    41     43   // Constants for VerbatimCode
    42     44   const (
    43         -	_               VerbatimCode = iota
           45  +	_               VerbatimKind = iota
    44     46   	VerbatimProg                 // Program code.
    45     47   	VerbatimComment              // Block comment
    46     48   	VerbatimHTML                 // Block HTML, e.g. for Markdown
    47     49   )
    48     50   
    49         -func (vn *VerbatimNode) blockNode() {}
    50         -func (vn *VerbatimNode) itemNode()  {}
           51  +func (vn *VerbatimNode) blockNode() { /* Just a marker */ }
           52  +func (vn *VerbatimNode) itemNode()  { /* Just a marker */ }
    51     53   
    52         -// Accept a visitor an visit the node.
    53         -func (vn *VerbatimNode) Accept(v Visitor) { v.VisitVerbatim(vn) }
           54  +// WalkChildren does nothing.
           55  +func (vn *VerbatimNode) WalkChildren(v Visitor) { /* No children*/ }
    54     56   
    55     57   //--------------------------------------------------------------------------
    56     58   
    57     59   // RegionNode encapsulates a region of block nodes.
    58     60   type RegionNode struct {
    59         -	Code    RegionCode
           61  +	Kind    RegionKind
    60     62   	Attrs   *Attributes
    61     63   	Blocks  BlockSlice
    62     64   	Inlines InlineSlice // Additional text at the end of the region
    63     65   }
    64     66   
    65         -// RegionCode specifies the actual region type.
    66         -type RegionCode int
           67  +// RegionKind specifies the actual region type.
           68  +type RegionKind uint8
    67     69   
    68     70   // Values for RegionCode
    69     71   const (
    70         -	_           RegionCode = iota
           72  +	_           RegionKind = iota
    71     73   	RegionSpan             // Just a span of blocks
    72     74   	RegionQuote            // A longer quotation
    73     75   	RegionVerse            // Line breaks matter
    74     76   )
    75     77   
    76         -func (rn *RegionNode) blockNode() {}
    77         -func (rn *RegionNode) itemNode()  {}
           78  +func (rn *RegionNode) blockNode() { /* Just a marker */ }
           79  +func (rn *RegionNode) itemNode()  { /* Just a marker */ }
    78     80   
    79         -// Accept a visitor and visit the node.
    80         -func (rn *RegionNode) Accept(v Visitor) { v.VisitRegion(rn) }
           81  +// WalkChildren walks down the blocks and the text.
           82  +func (rn *RegionNode) WalkChildren(v Visitor) {
           83  +	WalkBlockSlice(v, rn.Blocks)
           84  +	WalkInlineSlice(v, rn.Inlines)
           85  +}
    81     86   
    82     87   //--------------------------------------------------------------------------
    83     88   
    84     89   // HeadingNode stores the heading text and level.
    85     90   type HeadingNode struct {
    86     91   	Level   int
    87     92   	Inlines InlineSlice // Heading text, possibly formatted
    88     93   	Slug    string      // Heading text, suitable to be used as an URL fragment
    89     94   	Attrs   *Attributes
    90     95   }
    91     96   
    92         -func (hn *HeadingNode) blockNode() {}
    93         -func (hn *HeadingNode) itemNode()  {}
           97  +func (hn *HeadingNode) blockNode() { /* Just a marker */ }
           98  +func (hn *HeadingNode) itemNode()  { /* Just a marker */ }
    94     99   
    95         -// Accept a visitor and visit the node.
    96         -func (hn *HeadingNode) Accept(v Visitor) { v.VisitHeading(hn) }
          100  +// WalkChildren walks the heading text.
          101  +func (hn *HeadingNode) WalkChildren(v Visitor) {
          102  +	WalkInlineSlice(v, hn.Inlines)
          103  +}
    97    104   
    98    105   //--------------------------------------------------------------------------
    99    106   
   100    107   // HRuleNode specifies a horizontal rule.
   101    108   type HRuleNode struct {
   102    109   	Attrs *Attributes
   103    110   }
   104    111   
   105         -func (hn *HRuleNode) blockNode() {}
   106         -func (hn *HRuleNode) itemNode()  {}
          112  +func (hn *HRuleNode) blockNode() { /* Just a marker */ }
          113  +func (hn *HRuleNode) itemNode()  { /* Just a marker */ }
   107    114   
   108         -// Accept a visitor and visit the node.
   109         -func (hn *HRuleNode) Accept(v Visitor) { v.VisitHRule(hn) }
          115  +// WalkChildren does nothing.
          116  +func (hn *HRuleNode) WalkChildren(v Visitor) { /* No children*/ }
   110    117   
   111    118   //--------------------------------------------------------------------------
   112    119   
   113    120   // NestedListNode specifies a nestable list, either ordered or unordered.
   114    121   type NestedListNode struct {
   115         -	Code  NestedListCode
          122  +	Kind  NestedListKind
   116    123   	Items []ItemSlice
   117    124   	Attrs *Attributes
   118    125   }
   119    126   
   120         -// NestedListCode specifies the actual list type.
   121         -type NestedListCode int
          127  +// NestedListKind specifies the actual list type.
          128  +type NestedListKind uint8
   122    129   
   123    130   // Values for ListCode
   124    131   const (
   125         -	_                   NestedListCode = iota
          132  +	_                   NestedListKind = iota
   126    133   	NestedListOrdered                  // Ordered list.
   127    134   	NestedListUnordered                // Unordered list.
   128    135   	NestedListQuote                    // Quote list.
   129    136   )
   130    137   
   131         -func (ln *NestedListNode) blockNode() {}
   132         -func (ln *NestedListNode) itemNode()  {}
          138  +func (ln *NestedListNode) blockNode() { /* Just a marker */ }
          139  +func (ln *NestedListNode) itemNode()  { /* Just a marker */ }
   133    140   
   134         -// Accept a visitor and visit the node.
   135         -func (ln *NestedListNode) Accept(v Visitor) { v.VisitNestedList(ln) }
          141  +// WalkChildren walks down the items.
          142  +func (ln *NestedListNode) WalkChildren(v Visitor) {
          143  +	for _, item := range ln.Items {
          144  +		WalkItemSlice(v, item)
          145  +	}
          146  +}
   136    147   
   137    148   //--------------------------------------------------------------------------
   138    149   
   139    150   // DescriptionListNode specifies a description list.
   140    151   type DescriptionListNode struct {
   141    152   	Descriptions []Description
   142    153   }
................................................................................
   145    156   type Description struct {
   146    157   	Term         InlineSlice
   147    158   	Descriptions []DescriptionSlice
   148    159   }
   149    160   
   150    161   func (dn *DescriptionListNode) blockNode() {}
   151    162   
   152         -// Accept a visitor and visit the node.
   153         -func (dn *DescriptionListNode) Accept(v Visitor) { v.VisitDescriptionList(dn) }
          163  +// WalkChildren walks down to the descriptions.
          164  +func (dn *DescriptionListNode) WalkChildren(v Visitor) {
          165  +	for _, desc := range dn.Descriptions {
          166  +		WalkInlineSlice(v, desc.Term)
          167  +		for _, dns := range desc.Descriptions {
          168  +			WalkDescriptionSlice(v, dns)
          169  +		}
          170  +	}
          171  +}
   154    172   
   155    173   //--------------------------------------------------------------------------
   156    174   
   157    175   // TableNode specifies a full table
   158    176   type TableNode struct {
   159    177   	Header TableRow    // The header row
   160    178   	Align  []Alignment // Default column alignment
................................................................................
   179    197   	_            Alignment = iota
   180    198   	AlignDefault           // Default alignment, inherited
   181    199   	AlignLeft              // Left alignment
   182    200   	AlignCenter            // Center the content
   183    201   	AlignRight             // Right alignment
   184    202   )
   185    203   
   186         -func (tn *TableNode) blockNode() {}
          204  +func (tn *TableNode) blockNode() { /* Just a marker */ }
   187    205   
   188         -// Accept a visitor and visit the node.
   189         -func (tn *TableNode) Accept(v Visitor) { v.VisitTable(tn) }
          206  +// WalkChildren walks down to the cells.
          207  +func (tn *TableNode) WalkChildren(v Visitor) {
          208  +	for _, cell := range tn.Header {
          209  +		WalkInlineSlice(v, cell.Inlines)
          210  +	}
          211  +	for _, row := range tn.Rows {
          212  +		for _, cell := range row {
          213  +			WalkInlineSlice(v, cell.Inlines)
          214  +		}
          215  +	}
          216  +}
   190    217   
   191    218   //--------------------------------------------------------------------------
   192    219   
   193    220   // BLOBNode contains just binary data that must be interpreted according to
   194    221   // a syntax.
   195    222   type BLOBNode struct {
   196    223   	Title  string
   197    224   	Syntax string
   198    225   	Blob   []byte
   199    226   }
   200    227   
   201         -func (bn *BLOBNode) blockNode() {}
          228  +func (bn *BLOBNode) blockNode() { /* Just a marker */ }
   202    229   
   203         -// Accept a visitor and visit the node.
   204         -func (bn *BLOBNode) Accept(v Visitor) { v.VisitBLOB(bn) }
          230  +// WalkChildren does nothing.
          231  +func (bn *BLOBNode) WalkChildren(v Visitor) { /* No children*/ }

Changes to ast/inline.go.

    14     14   // Definitions of inline nodes.
    15     15   
    16     16   // TextNode just contains some text.
    17     17   type TextNode struct {
    18     18   	Text string // The text itself.
    19     19   }
    20     20   
    21         -func (tn *TextNode) inlineNode() {}
           21  +func (tn *TextNode) inlineNode() { /* Just a marker */ }
    22     22   
    23         -// Accept a visitor and visit the node.
    24         -func (tn *TextNode) Accept(v Visitor) { v.VisitText(tn) }
           23  +// WalkChildren does nothing.
           24  +func (tn *TextNode) WalkChildren(v Visitor) { /* No children*/ }
    25     25   
    26     26   // --------------------------------------------------------------------------
    27     27   
    28     28   // TagNode contains a tag.
    29     29   type TagNode struct {
    30     30   	Tag string // The text itself.
    31     31   }
    32     32   
    33         -func (tn *TagNode) inlineNode() {}
           33  +func (tn *TagNode) inlineNode() { /* Just a marker */ }
    34     34   
    35         -// Accept a visitor and visit the node.
    36         -func (tn *TagNode) Accept(v Visitor) { v.VisitTag(tn) }
           35  +// WalkChildren does nothing.
           36  +func (tn *TagNode) WalkChildren(v Visitor) { /* No children*/ }
    37     37   
    38     38   // --------------------------------------------------------------------------
    39     39   
    40     40   // SpaceNode tracks inter-word space characters.
    41     41   type SpaceNode struct {
    42     42   	Lexeme string
    43     43   }
    44     44   
    45         -func (sn *SpaceNode) inlineNode() {}
           45  +func (sn *SpaceNode) inlineNode() { /* Just a marker */ }
    46     46   
    47         -// Accept a visitor and visit the node.
    48         -func (sn *SpaceNode) Accept(v Visitor) { v.VisitSpace(sn) }
           47  +// WalkChildren does nothing.
           48  +func (sn *SpaceNode) WalkChildren(v Visitor) { /* No children*/ }
    49     49   
    50     50   // --------------------------------------------------------------------------
    51     51   
    52     52   // BreakNode signals a new line that must / should be interpreted as a new line break.
    53     53   type BreakNode struct {
    54     54   	Hard bool // Hard line break?
    55     55   }
    56     56   
    57         -func (bn *BreakNode) inlineNode() {}
           57  +func (bn *BreakNode) inlineNode() { /* Just a marker */ }
    58     58   
    59         -// Accept a visitor and visit the node.
    60         -func (bn *BreakNode) Accept(v Visitor) { v.VisitBreak(bn) }
           59  +// WalkChildren does nothing.
           60  +func (bn *BreakNode) WalkChildren(v Visitor) { /* No children*/ }
    61     61   
    62     62   // --------------------------------------------------------------------------
    63     63   
    64     64   // LinkNode contains the specified link.
    65     65   type LinkNode struct {
    66     66   	Ref     *Reference
    67     67   	Inlines InlineSlice // The text associated with the link.
    68     68   	OnlyRef bool        // True if no text was specified.
    69     69   	Attrs   *Attributes // Optional attributes
    70     70   }
    71     71   
    72         -func (ln *LinkNode) inlineNode() {}
           72  +func (ln *LinkNode) inlineNode() { /* Just a marker */ }
    73     73   
    74         -// Accept a visitor and visit the node.
    75         -func (ln *LinkNode) Accept(v Visitor) { v.VisitLink(ln) }
           74  +// WalkChildren walks to the link text.
           75  +func (ln *LinkNode) WalkChildren(v Visitor) {
           76  +	WalkInlineSlice(v, ln.Inlines)
           77  +}
    76     78   
    77     79   // --------------------------------------------------------------------------
    78     80   
    79     81   // ImageNode contains the specified image reference.
    80     82   type ImageNode struct {
    81     83   	Ref     *Reference  // Reference to image
    82     84   	Blob    []byte      // BLOB data of the image, as an alternative to Ref.
    83     85   	Syntax  string      // Syntax of Blob
    84     86   	Inlines InlineSlice // The text associated with the image.
    85     87   	Attrs   *Attributes // Optional attributes
    86     88   }
    87     89   
    88         -func (in *ImageNode) inlineNode() {}
           90  +func (in *ImageNode) inlineNode() { /* Just a marker */ }
    89     91   
    90         -// Accept a visitor and visit the node.
    91         -func (in *ImageNode) Accept(v Visitor) { v.VisitImage(in) }
           92  +// WalkChildren walks to the image text.
           93  +func (in *ImageNode) WalkChildren(v Visitor) {
           94  +	WalkInlineSlice(v, in.Inlines)
           95  +}
    92     96   
    93     97   // --------------------------------------------------------------------------
    94     98   
    95     99   // CiteNode contains the specified citation.
    96    100   type CiteNode struct {
    97    101   	Key     string      // The citation key
    98    102   	Inlines InlineSlice // The text associated with the citation.
    99    103   	Attrs   *Attributes // Optional attributes
   100    104   }
   101    105   
   102         -func (cn *CiteNode) inlineNode() {}
          106  +func (cn *CiteNode) inlineNode() { /* Just a marker */ }
   103    107   
   104         -// Accept a visitor and visit the node.
   105         -func (cn *CiteNode) Accept(v Visitor) { v.VisitCite(cn) }
          108  +// WalkChildren walks to the cite text.
          109  +func (cn *CiteNode) WalkChildren(v Visitor) {
          110  +	WalkInlineSlice(v, cn.Inlines)
          111  +}
   106    112   
   107    113   // --------------------------------------------------------------------------
   108    114   
   109    115   // MarkNode contains the specified merked position.
   110    116   // It is a BlockNode too, because although it is typically parsed during inline
   111    117   // mode, it is moved into block mode afterwards.
   112    118   type MarkNode struct {
   113    119   	Text string
   114    120   }
   115    121   
   116         -func (mn *MarkNode) inlineNode() {}
          122  +func (mn *MarkNode) inlineNode() { /* Just a marker */ }
   117    123   
   118         -// Accept a visitor and visit the node.
   119         -func (mn *MarkNode) Accept(v Visitor) { v.VisitMark(mn) }
          124  +// WalkChildren does nothing.
          125  +func (mn *MarkNode) WalkChildren(v Visitor) { /* No children*/ }
   120    126   
   121    127   // --------------------------------------------------------------------------
   122    128   
   123    129   // FootnoteNode contains the specified footnote.
   124    130   type FootnoteNode struct {
   125    131   	Inlines InlineSlice // The footnote text.
   126    132   	Attrs   *Attributes // Optional attributes
   127    133   }
   128    134   
   129         -func (fn *FootnoteNode) inlineNode() {}
          135  +func (fn *FootnoteNode) inlineNode() { /* Just a marker */ }
   130    136   
   131         -// Accept a visitor and visit the node.
   132         -func (fn *FootnoteNode) Accept(v Visitor) { v.VisitFootnote(fn) }
          137  +// WalkChildren walks to the footnote text.
          138  +func (fn *FootnoteNode) WalkChildren(v Visitor) {
          139  +	WalkInlineSlice(v, fn.Inlines)
          140  +}
   133    141   
   134    142   // --------------------------------------------------------------------------
   135    143   
   136    144   // FormatNode specifies some inline formatting.
   137    145   type FormatNode struct {
   138         -	Code    FormatCode
          146  +	Kind    FormatKind
   139    147   	Attrs   *Attributes // Optional attributes.
   140    148   	Inlines InlineSlice
   141    149   }
   142    150   
   143         -// FormatCode specifies the format that is applied to the inline nodes.
   144         -type FormatCode int
          151  +// FormatKind specifies the format that is applied to the inline nodes.
          152  +type FormatKind uint8
   145    153   
   146    154   // Constants for FormatCode
   147    155   const (
   148         -	_               FormatCode = iota
          156  +	_               FormatKind = iota
   149    157   	FormatItalic               // Italic text.
   150    158   	FormatEmph                 // Semantically emphasized text.
   151    159   	FormatBold                 // Bold text.
   152    160   	FormatStrong               // Semantically strongly emphasized text.
   153    161   	FormatUnder                // Underlined text.
   154    162   	FormatInsert               // Inserted text.
   155    163   	FormatStrike               // Text that is no longer relevant or no longer accurate.
................................................................................
   159    167   	FormatQuote                // Quoted text.
   160    168   	FormatQuotation            // Quotation text.
   161    169   	FormatSmall                // Smaller text.
   162    170   	FormatSpan                 // Generic inline container.
   163    171   	FormatMonospace            // Monospaced text.
   164    172   )
   165    173   
   166         -func (fn *FormatNode) inlineNode() {}
          174  +func (fn *FormatNode) inlineNode() { /* Just a marker */ }
   167    175   
   168         -// Accept a visitor and visit the node.
   169         -func (fn *FormatNode) Accept(v Visitor) { v.VisitFormat(fn) }
          176  +// WalkChildren walks to the formatted text.
          177  +func (fn *FormatNode) WalkChildren(v Visitor) {
          178  +	WalkInlineSlice(v, fn.Inlines)
          179  +}
   170    180   
   171    181   // --------------------------------------------------------------------------
   172    182   
   173    183   // LiteralNode specifies some uninterpreted text.
   174    184   type LiteralNode struct {
   175         -	Code  LiteralCode
          185  +	Kind  LiteralKind
   176    186   	Attrs *Attributes // Optional attributes.
   177    187   	Text  string
   178    188   }
   179    189   
   180         -// LiteralCode specifies the format that is applied to code inline nodes.
   181         -type LiteralCode int
          190  +// LiteralKind specifies the format that is applied to code inline nodes.
          191  +type LiteralKind uint8
   182    192   
   183    193   // Constants for LiteralCode
   184    194   const (
   185         -	_              LiteralCode = iota
          195  +	_              LiteralKind = iota
   186    196   	LiteralProg                // Inline program code.
   187    197   	LiteralKeyb                // Keyboard strokes.
   188    198   	LiteralOutput              // Sample output.
   189    199   	LiteralComment             // Inline comment
   190    200   	LiteralHTML                // Inline HTML, e.g. for Markdown
   191    201   )
   192    202   
   193         -func (rn *LiteralNode) inlineNode() {}
          203  +func (ln *LiteralNode) inlineNode() { /* Just a marker */ }
   194    204   
   195         -// Accept a visitor and visit the node.
   196         -func (rn *LiteralNode) Accept(v Visitor) { v.VisitLiteral(rn) }
          205  +// WalkChildren does nothing.
          206  +func (ln *LiteralNode) WalkChildren(v Visitor) { /* No children*/ }

Changes to ast/ref_test.go.

    14     14   import (
    15     15   	"testing"
    16     16   
    17     17   	"zettelstore.de/z/ast"
    18     18   )
    19     19   
    20     20   func TestParseReference(t *testing.T) {
           21  +	t.Parallel()
    21     22   	testcases := []struct {
    22     23   		link string
    23     24   		err  bool
    24     25   		exp  string
    25     26   	}{
    26     27   		{"", true, ""},
    27     28   		{"123", false, "123"},
................................................................................
    37     38   		if got.IsValid() && got.String() != tc.exp {
    38     39   			t.Errorf("TC=%d, Reference of %q is %q, but got %q", i, tc.link, tc.exp, got)
    39     40   		}
    40     41   	}
    41     42   }
    42     43   
    43     44   func TestReferenceIsZettelMaterial(t *testing.T) {
           45  +	t.Parallel()
    44     46   	testcases := []struct {
    45     47   		link       string
    46     48   		isZettel   bool
    47     49   		isExternal bool
    48     50   		isLocal    bool
    49     51   	}{
    50     52   		{"", false, false, false},

Deleted ast/traverser.go.

     1         -//-----------------------------------------------------------------------------
     2         -// Copyright (c) 2020-2021 Detlef Stern
     3         -//
     4         -// This file is part of zettelstore.
     5         -//
     6         -// Zettelstore is licensed under the latest version of the EUPL (European Union
     7         -// Public License). Please see file LICENSE.txt for your rights and obligations
     8         -// under this license.
     9         -//-----------------------------------------------------------------------------
    10         -
    11         -// Package ast provides the abstract syntax tree.
    12         -package ast
    13         -
    14         -// A traverser is a Visitor that just traverses the AST and delegates node
    15         -// spacific actions to a Visitor. This Visitor should not traverse the AST.
    16         -
    17         -// TopDownTraverser visits first the node and then the children nodes.
    18         -type TopDownTraverser struct {
    19         -	v Visitor
    20         -}
    21         -
    22         -// NewTopDownTraverser creates a new traverser.
    23         -func NewTopDownTraverser(visitor Visitor) TopDownTraverser {
    24         -	return TopDownTraverser{visitor}
    25         -}
    26         -
    27         -// VisitVerbatim has nothing to traverse.
    28         -func (t TopDownTraverser) VisitVerbatim(vn *VerbatimNode) { t.v.VisitVerbatim(vn) }
    29         -
    30         -// VisitRegion traverses the content and the additional text.
    31         -func (t TopDownTraverser) VisitRegion(rn *RegionNode) {
    32         -	t.v.VisitRegion(rn)
    33         -	t.VisitBlockSlice(rn.Blocks)
    34         -	t.VisitInlineSlice(rn.Inlines)
    35         -}
    36         -
    37         -// VisitHeading traverses the heading.
    38         -func (t TopDownTraverser) VisitHeading(hn *HeadingNode) {
    39         -	t.v.VisitHeading(hn)
    40         -	t.VisitInlineSlice(hn.Inlines)
    41         -}
    42         -
    43         -// VisitHRule traverses nothing.
    44         -func (t TopDownTraverser) VisitHRule(hn *HRuleNode) { t.v.VisitHRule(hn) }
    45         -
    46         -// VisitNestedList traverses all nested list elements.
    47         -func (t TopDownTraverser) VisitNestedList(ln *NestedListNode) {
    48         -	t.v.VisitNestedList(ln)
    49         -	for _, item := range ln.Items {
    50         -		t.visitItemSlice(item)
    51         -	}
    52         -}
    53         -
    54         -// VisitDescriptionList traverses all description terms and their associated
    55         -// descriptions.
    56         -func (t TopDownTraverser) VisitDescriptionList(dn *DescriptionListNode) {
    57         -	t.v.VisitDescriptionList(dn)
    58         -	for _, defs := range dn.Descriptions {
    59         -		t.VisitInlineSlice(defs.Term)
    60         -		for _, descr := range defs.Descriptions {
    61         -			t.visitDescriptionSlice(descr)
    62         -		}
    63         -	}
    64         -}
    65         -
    66         -// VisitPara traverses the inlines of a paragraph.
    67         -func (t TopDownTraverser) VisitPara(pn *ParaNode) {
    68         -	t.v.VisitPara(pn)
    69         -	t.VisitInlineSlice(pn.Inlines)
    70         -}
    71         -
    72         -// VisitTable traverses all cells of the header and then row-wise all cells of
    73         -// the table body.
    74         -func (t TopDownTraverser) VisitTable(tn *TableNode) {
    75         -	t.v.VisitTable(tn)
    76         -	for _, col := range tn.Header {
    77         -		t.VisitInlineSlice(col.Inlines)
    78         -	}
    79         -	for _, row := range tn.Rows {
    80         -		for _, col := range row {
    81         -			t.VisitInlineSlice(col.Inlines)
    82         -		}
    83         -	}
    84         -}
    85         -
    86         -// VisitBLOB traverses nothing.
    87         -func (t TopDownTraverser) VisitBLOB(bn *BLOBNode) { t.v.VisitBLOB(bn) }
    88         -
    89         -// VisitText traverses nothing.
    90         -func (t TopDownTraverser) VisitText(tn *TextNode) { t.v.VisitText(tn) }
    91         -
    92         -// VisitTag traverses nothing.
    93         -func (t TopDownTraverser) VisitTag(tn *TagNode) { t.v.VisitTag(tn) }
    94         -
    95         -// VisitSpace traverses nothing.
    96         -func (t TopDownTraverser) VisitSpace(sn *SpaceNode) { t.v.VisitSpace(sn) }
    97         -
    98         -// VisitBreak traverses nothing.
    99         -func (t TopDownTraverser) VisitBreak(bn *BreakNode) { t.v.VisitBreak(bn) }
   100         -
   101         -// VisitLink traverses the link text.
   102         -func (t TopDownTraverser) VisitLink(ln *LinkNode) {
   103         -	t.v.VisitLink(ln)
   104         -	t.VisitInlineSlice(ln.Inlines)
   105         -}
   106         -
   107         -// VisitImage traverses the image text.
   108         -func (t TopDownTraverser) VisitImage(in *ImageNode) {
   109         -	t.v.VisitImage(in)
   110         -	t.VisitInlineSlice(in.Inlines)
   111         -}
   112         -
   113         -// VisitCite traverses the cite text.
   114         -func (t TopDownTraverser) VisitCite(cn *CiteNode) {
   115         -	t.v.VisitCite(cn)
   116         -	t.VisitInlineSlice(cn.Inlines)
   117         -}
   118         -
   119         -// VisitFootnote traverses the footnote text.
   120         -func (t TopDownTraverser) VisitFootnote(fn *FootnoteNode) {
   121         -	t.v.VisitFootnote(fn)
   122         -	t.VisitInlineSlice(fn.Inlines)
   123         -}
   124         -
   125         -// VisitMark traverses nothing.
   126         -func (t TopDownTraverser) VisitMark(mn *MarkNode) { t.v.VisitMark(mn) }
   127         -
   128         -// VisitFormat traverses the formatted text.
   129         -func (t TopDownTraverser) VisitFormat(fn *FormatNode) {
   130         -	t.v.VisitFormat(fn)
   131         -	t.VisitInlineSlice(fn.Inlines)
   132         -}
   133         -
   134         -// VisitLiteral traverses nothing.
   135         -func (t TopDownTraverser) VisitLiteral(ln *LiteralNode) { t.v.VisitLiteral(ln) }
   136         -
   137         -// VisitBlockSlice traverses a block slice.
   138         -func (t TopDownTraverser) VisitBlockSlice(bns BlockSlice) {
   139         -	for _, bn := range bns {
   140         -		bn.Accept(t)
   141         -	}
   142         -}
   143         -
   144         -func (t TopDownTraverser) visitItemSlice(ins ItemSlice) {
   145         -	for _, in := range ins {
   146         -		in.Accept(t)
   147         -	}
   148         -}
   149         -
   150         -func (t TopDownTraverser) visitDescriptionSlice(dns DescriptionSlice) {
   151         -	for _, dn := range dns {
   152         -		dn.Accept(t)
   153         -	}
   154         -}
   155         -
   156         -// VisitInlineSlice traverses a block slice.
   157         -func (t TopDownTraverser) VisitInlineSlice(ins InlineSlice) {
   158         -	for _, in := range ins {
   159         -		in.Accept(t)
   160         -	}
   161         -}

Deleted ast/visitor.go.

     1         -//-----------------------------------------------------------------------------
     2         -// Copyright (c) 2020 Detlef Stern
     3         -//
     4         -// This file is part of zettelstore.
     5         -//
     6         -// Zettelstore is licensed under the latest version of the EUPL (European Union
     7         -// Public License). Please see file LICENSE.txt for your rights and obligations
     8         -// under this license.
     9         -//-----------------------------------------------------------------------------
    10         -
    11         -// Package ast provides the abstract syntax tree.
    12         -package ast
    13         -
    14         -// Visitor is the interface all visitors must implement.
    15         -type Visitor interface {
    16         -	// Block nodes
    17         -	VisitVerbatim(vn *VerbatimNode)
    18         -	VisitRegion(rn *RegionNode)
    19         -	VisitHeading(hn *HeadingNode)
    20         -	VisitHRule(hn *HRuleNode)
    21         -	VisitNestedList(ln *NestedListNode)
    22         -	VisitDescriptionList(dn *DescriptionListNode)
    23         -	VisitPara(pn *ParaNode)
    24         -	VisitTable(tn *TableNode)
    25         -	VisitBLOB(bn *BLOBNode)
    26         -
    27         -	// Inline nodes
    28         -	VisitText(tn *TextNode)
    29         -	VisitTag(tn *TagNode)
    30         -	VisitSpace(sn *SpaceNode)
    31         -	VisitBreak(bn *BreakNode)
    32         -	VisitLink(ln *LinkNode)
    33         -	VisitImage(in *ImageNode)
    34         -	VisitCite(cn *CiteNode)
    35         -	VisitFootnote(fn *FootnoteNode)
    36         -	VisitMark(mn *MarkNode)
    37         -	VisitFormat(fn *FormatNode)
    38         -	VisitLiteral(ln *LiteralNode)
    39         -}

Added ast/walk.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package ast provides the abstract syntax tree.
           12  +package ast
           13  +
           14  +// Visitor is a visitor for walking the AST.
           15  +type Visitor interface {
           16  +	Visit(node Node) Visitor
           17  +}
           18  +
           19  +// Walk traverses the AST.
           20  +func Walk(v Visitor, node Node) {
           21  +	if v = v.Visit(node); v == nil {
           22  +		return
           23  +	}
           24  +	node.WalkChildren(v)
           25  +	v.Visit(nil)
           26  +}
           27  +
           28  +// WalkBlockSlice traverse a block slice.
           29  +func WalkBlockSlice(v Visitor, bns BlockSlice) {
           30  +	for _, bn := range bns {
           31  +		Walk(v, bn)
           32  +	}
           33  +}
           34  +
           35  +// WalkInlineSlice traverses an inline slice.
           36  +func WalkInlineSlice(v Visitor, ins InlineSlice) {
           37  +	for _, in := range ins {
           38  +		Walk(v, in)
           39  +	}
           40  +}
           41  +
           42  +// WalkItemSlice traverses an item slice.
           43  +func WalkItemSlice(v Visitor, ins ItemSlice) {
           44  +	for _, in := range ins {
           45  +		Walk(v, in)
           46  +	}
           47  +}
           48  +
           49  +// WalkDescriptionSlice traverses an item slice.
           50  +func WalkDescriptionSlice(v Visitor, dns DescriptionSlice) {
           51  +	for _, dn := range dns {
           52  +		Walk(v, dn)
           53  +	}
           54  +}

Changes to auth/auth.go.

    10     10   
    11     11   // Package auth provides services for authentification / authorization.
    12     12   package auth
    13     13   
    14     14   import (
    15     15   	"time"
    16     16   
           17  +	"zettelstore.de/z/box"
    17     18   	"zettelstore.de/z/config"
    18     19   	"zettelstore.de/z/domain/id"
    19     20   	"zettelstore.de/z/domain/meta"
    20         -	"zettelstore.de/z/place"
    21     21   	"zettelstore.de/z/web/server"
    22     22   )
    23     23   
    24     24   // BaseManager allows to check some base auth modes.
    25     25   type BaseManager interface {
    26     26   	// IsReadonly returns true, if the systems is configured to run in read-only-mode.
    27     27   	IsReadonly() bool
................................................................................
    75     75   }
    76     76   
    77     77   // Manager is the main interface for providing the service.
    78     78   type Manager interface {
    79     79   	TokenManager
    80     80   	AuthzManager
    81     81   
    82         -	PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, Policy)
           82  +	BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy)
    83     83   }
    84     84   
    85     85   // Policy is an interface for checking access authorization.
    86     86   type Policy interface {
    87     87   	// User is allowed to create a new zettel.
    88     88   	CanCreate(user, newMeta *meta.Meta) bool
    89     89   

Changes to auth/impl/impl.go.

    17     17   	"io"
    18     18   	"time"
    19     19   
    20     20   	"github.com/pascaldekloe/jwt"
    21     21   
    22     22   	"zettelstore.de/z/auth"
    23     23   	"zettelstore.de/z/auth/policy"
           24  +	"zettelstore.de/z/box"
    24     25   	"zettelstore.de/z/config"
    25     26   	"zettelstore.de/z/domain/id"
    26     27   	"zettelstore.de/z/domain/meta"
    27     28   	"zettelstore.de/z/kernel"
    28         -	"zettelstore.de/z/place"
    29     29   	"zettelstore.de/z/web/server"
    30     30   )
    31     31   
    32     32   type myAuth struct {
    33     33   	readonly bool
    34     34   	owner    id.Zid
    35     35   	secret   []byte
................................................................................
   175    175   		if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown {
   176    176   			return ur
   177    177   		}
   178    178   	}
   179    179   	return meta.UserRoleReader
   180    180   }
   181    181   
   182         -func (a *myAuth) PlaceWithPolicy(auth server.Auth, unprotectedPlace place.Place, rtConfig config.Config) (place.Place, auth.Policy) {
   183         -	return policy.PlaceWithPolicy(auth, a, unprotectedPlace, rtConfig)
          182  +func (a *myAuth) BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) {
          183  +	return policy.BoxWithPolicy(auth, a, unprotectedBox, rtConfig)
   184    184   }

Added auth/policy/box.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package policy provides some interfaces and implementation for authorizsation policies.
           12  +package policy
           13  +
           14  +import (
           15  +	"context"
           16  +
           17  +	"zettelstore.de/z/auth"
           18  +	"zettelstore.de/z/box"
           19  +	"zettelstore.de/z/config"
           20  +	"zettelstore.de/z/domain"
           21  +	"zettelstore.de/z/domain/id"
           22  +	"zettelstore.de/z/domain/meta"
           23  +	"zettelstore.de/z/search"
           24  +	"zettelstore.de/z/web/server"
           25  +)
           26  +
           27  +// BoxWithPolicy wraps the given box inside a policy box.
           28  +func BoxWithPolicy(
           29  +	auth server.Auth,
           30  +	manager auth.AuthzManager,
           31  +	box box.Box,
           32  +	authConfig config.AuthConfig,
           33  +) (box.Box, auth.Policy) {
           34  +	pol := newPolicy(manager, authConfig)
           35  +	return newBox(auth, box, pol), pol
           36  +}
           37  +
           38  +// polBox implements a policy box.
           39  +type polBox struct {
           40  +	auth   server.Auth
           41  +	box    box.Box
           42  +	policy auth.Policy
           43  +}
           44  +
           45  +// newBox creates a new policy box.
           46  +func newBox(auth server.Auth, box box.Box, policy auth.Policy) box.Box {
           47  +	return &polBox{
           48  +		auth:   auth,
           49  +		box:    box,
           50  +		policy: policy,
           51  +	}
           52  +}
           53  +
           54  +func (pp *polBox) Location() string {
           55  +	return pp.box.Location()
           56  +}
           57  +
           58  +func (pp *polBox) CanCreateZettel(ctx context.Context) bool {
           59  +	return pp.box.CanCreateZettel(ctx)
           60  +}
           61  +
           62  +func (pp *polBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
           63  +	user := pp.auth.GetUser(ctx)
           64  +	if pp.policy.CanCreate(user, zettel.Meta) {
           65  +		return pp.box.CreateZettel(ctx, zettel)
           66  +	}
           67  +	return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid)
           68  +}
           69  +
           70  +func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
           71  +	zettel, err := pp.box.GetZettel(ctx, zid)
           72  +	if err != nil {
           73  +		return domain.Zettel{}, err
           74  +	}
           75  +	user := pp.auth.GetUser(ctx)
           76  +	if pp.policy.CanRead(user, zettel.Meta) {
           77  +		return zettel, nil
           78  +	}
           79  +	return domain.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid)
           80  +}
           81  +
           82  +func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) {
           83  +	return pp.box.GetAllZettel(ctx, zid)
           84  +}
           85  +
           86  +func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
           87  +	m, err := pp.box.GetMeta(ctx, zid)
           88  +	if err != nil {
           89  +		return nil, err
           90  +	}
           91  +	user := pp.auth.GetUser(ctx)
           92  +	if pp.policy.CanRead(user, m) {
           93  +		return m, nil
           94  +	}
           95  +	return nil, box.NewErrNotAllowed("GetMeta", user, zid)
           96  +}
           97  +
           98  +func (pp *polBox) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) {
           99  +	return pp.box.GetAllMeta(ctx, zid)
          100  +}
          101  +
          102  +func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) {
          103  +	return nil, box.NewErrNotAllowed("fetch-zids", pp.auth.GetUser(ctx), id.Invalid)
          104  +}
          105  +
          106  +func (pp *polBox) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) {
          107  +	user := pp.auth.GetUser(ctx)
          108  +	canRead := pp.policy.CanRead
          109  +	s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) })
          110  +	return pp.box.SelectMeta(ctx, s)
          111  +}
          112  +
          113  +func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
          114  +	return pp.box.CanUpdateZettel(ctx, zettel)
          115  +}
          116  +
          117  +func (pp *polBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
          118  +	zid := zettel.Meta.Zid
          119  +	user := pp.auth.GetUser(ctx)
          120  +	if !zid.IsValid() {
          121  +		return &box.ErrInvalidID{Zid: zid}
          122  +	}
          123  +	// Write existing zettel
          124  +	oldMeta, err := pp.box.GetMeta(ctx, zid)
          125  +	if err != nil {
          126  +		return err
          127  +	}
          128  +	if pp.policy.CanWrite(user, oldMeta, zettel.Meta) {
          129  +		return pp.box.UpdateZettel(ctx, zettel)
          130  +	}
          131  +	return box.NewErrNotAllowed("Write", user, zid)
          132  +}
          133  +
          134  +func (pp *polBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
          135  +	return pp.box.AllowRenameZettel(ctx, zid)
          136  +}
          137  +
          138  +func (pp *polBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
          139  +	meta, err := pp.box.GetMeta(ctx, curZid)
          140  +	if err != nil {
          141  +		return err
          142  +	}
          143  +	user := pp.auth.GetUser(ctx)
          144  +	if pp.policy.CanRename(user, meta) {
          145  +		return pp.box.RenameZettel(ctx, curZid, newZid)
          146  +	}
          147  +	return box.NewErrNotAllowed("Rename", user, curZid)
          148  +}
          149  +
          150  +func (pp *polBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
          151  +	return pp.box.CanDeleteZettel(ctx, zid)
          152  +}
          153  +
          154  +func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
          155  +	meta, err := pp.box.GetMeta(ctx, zid)
          156  +	if err != nil {
          157  +		return err
          158  +	}
          159  +	user := pp.auth.GetUser(ctx)
          160  +	if pp.policy.CanDelete(user, meta) {
          161  +		return pp.box.DeleteZettel(ctx, zid)
          162  +	}
          163  +	return box.NewErrNotAllowed("Delete", user, zid)
          164  +}

Changes to auth/policy/owner.go.

    60     60   	if user == nil {
    61     61   		return false
    62     62   	}
    63     63   	if role, ok := m.Get(meta.KeyRole); ok && role == meta.ValueRoleUser {
    64     64   		// Only the user can read its own zettel
    65     65   		return user.Zid == m.Zid
    66     66   	}
    67         -	return true
           67  +	switch o.manager.GetUserRole(user) {
           68  +	case meta.UserRoleReader, meta.UserRoleWriter, meta.UserRoleOwner:
           69  +		return true
           70  +	case meta.UserRoleCreator:
           71  +		return vis == meta.VisibilityCreator
           72  +	default:
           73  +		return false
           74  +	}
    68     75   }
    69     76   
    70     77   var noChangeUser = []string{
    71     78   	meta.KeyID,
    72     79   	meta.KeyRole,
    73     80   	meta.KeyUserID,
    74     81   	meta.KeyUserRole,
................................................................................
    94    101   		for _, key := range noChangeUser {
    95    102   			if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") {
    96    103   				return false
    97    104   			}
    98    105   		}
    99    106   		return true
   100    107   	}
   101         -	if o.manager.GetUserRole(user) == meta.UserRoleReader {
          108  +	switch userRole := o.manager.GetUserRole(user); userRole {
          109  +	case meta.UserRoleReader, meta.UserRoleCreator:
   102    110   		return false
   103    111   	}
   104    112   	return o.userCanCreate(user, newMeta)
   105    113   }
   106    114   
   107    115   func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool {
   108    116   	if user == nil || !o.pre.CanRename(user, m) {

Deleted auth/policy/place.go.

     1         -//-----------------------------------------------------------------------------
     2         -// Copyright (c) 2020-2021 Detlef Stern
     3         -//
     4         -// This file is part of zettelstore.
     5         -//
     6         -// Zettelstore is licensed under the latest version of the EUPL (European Union
     7         -// Public License). Please see file LICENSE.txt for your rights and obligations
     8         -// under this license.
     9         -//-----------------------------------------------------------------------------
    10         -
    11         -// Package policy provides some interfaces and implementation for authorizsation policies.
    12         -package policy
    13         -
    14         -import (
    15         -	"context"
    16         -	"io"
    17         -
    18         -	"zettelstore.de/z/auth"
    19         -	"zettelstore.de/z/config"
    20         -	"zettelstore.de/z/domain"
    21         -	"zettelstore.de/z/domain/id"
    22         -	"zettelstore.de/z/domain/meta"
    23         -	"zettelstore.de/z/place"
    24         -	"zettelstore.de/z/search"
    25         -	"zettelstore.de/z/web/server"
    26         -)
    27         -
    28         -// PlaceWithPolicy wraps the given place inside a policy place.
    29         -func PlaceWithPolicy(
    30         -	auth server.Auth,
    31         -	manager auth.AuthzManager,
    32         -	place place.Place,
    33         -	authConfig config.AuthConfig,
    34         -) (place.Place, auth.Policy) {
    35         -	pol := newPolicy(manager, authConfig)
    36         -	return newPlace(auth, place, pol), pol
    37         -}
    38         -
    39         -// polPlace implements a policy place.
    40         -type polPlace struct {
    41         -	auth   server.Auth
    42         -	place  place.Place
    43         -	policy auth.Policy
    44         -}
    45         -
    46         -// newPlace creates a new policy place.
    47         -func newPlace(auth server.Auth, place place.Place, policy auth.Policy) place.Place {
    48         -	return &polPlace{
    49         -		auth:   auth,
    50         -		place:  place,
    51         -		policy: policy,
    52         -	}
    53         -}
    54         -
    55         -func (pp *polPlace) Location() string {
    56         -	return pp.place.Location()
    57         -}
    58         -
    59         -func (pp *polPlace) CanCreateZettel(ctx context.Context) bool {
    60         -	return pp.place.CanCreateZettel(ctx)
    61         -}
    62         -
    63         -func (pp *polPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
    64         -	user := pp.auth.GetUser(ctx)
    65         -	if pp.policy.CanCreate(user, zettel.Meta) {
    66         -		return pp.place.CreateZettel(ctx, zettel)
    67         -	}
    68         -	return id.Invalid, place.NewErrNotAllowed("Create", user, id.Invalid)
    69         -}
    70         -
    71         -func (pp *polPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
    72         -	zettel, err := pp.place.GetZettel(ctx, zid)
    73         -	if err != nil {
    74         -		return domain.Zettel{}, err
    75         -	}
    76         -	user := pp.auth.GetUser(ctx)
    77         -	if pp.policy.CanRead(user, zettel.Meta) {
    78         -		return zettel, nil
    79         -	}
    80         -	return domain.Zettel{}, place.NewErrNotAllowed("GetZettel", user, zid)
    81         -}
    82         -
    83         -func (pp *polPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
    84         -	m, err := pp.place.GetMeta(ctx, zid)
    85         -	if err != nil {
    86         -		return nil, err
    87         -	}
    88         -	user := pp.auth.GetUser(ctx)
    89         -	if pp.policy.CanRead(user, m) {
    90         -		return m, nil
    91         -	}
    92         -	return nil, place.NewErrNotAllowed("GetMeta", user, zid)
    93         -}
    94         -
    95         -func (pp *polPlace) FetchZids(ctx context.Context) (id.Set, error) {
    96         -	return nil, place.NewErrNotAllowed("fetch-zids", pp.auth.GetUser(ctx), id.Invalid)
    97         -}
    98         -
    99         -func (pp *polPlace) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) {
   100         -	user := pp.auth.GetUser(ctx)
   101         -	canRead := pp.policy.CanRead
   102         -	s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) })
   103         -	return pp.place.SelectMeta(ctx, s)
   104         -}
   105         -
   106         -func (pp *polPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
   107         -	return pp.place.CanUpdateZettel(ctx, zettel)
   108         -}
   109         -
   110         -func (pp *polPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
   111         -	zid := zettel.Meta.Zid
   112         -	user := pp.auth.GetUser(ctx)
   113         -	if !zid.IsValid() {
   114         -		return &place.ErrInvalidID{Zid: zid}
   115         -	}
   116         -	// Write existing zettel
   117         -	oldMeta, err := pp.place.GetMeta(ctx, zid)
   118         -	if err != nil {
   119         -		return err
   120         -	}
   121         -	if pp.policy.CanWrite(user, oldMeta, zettel.Meta) {
   122         -		return pp.place.UpdateZettel(ctx, zettel)
   123         -	}
   124         -	return place.NewErrNotAllowed("Write", user, zid)
   125         -}
   126         -
   127         -func (pp *polPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
   128         -	return pp.place.AllowRenameZettel(ctx, zid)
   129         -}
   130         -
   131         -func (pp *polPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
   132         -	meta, err := pp.place.GetMeta(ctx, curZid)
   133         -	if err != nil {
   134         -		return err
   135         -	}
   136         -	user := pp.auth.GetUser(ctx)
   137         -	if pp.policy.CanRename(user, meta) {
   138         -		return pp.place.RenameZettel(ctx, curZid, newZid)
   139         -	}
   140         -	return place.NewErrNotAllowed("Rename", user, curZid)
   141         -}
   142         -
   143         -func (pp *polPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
   144         -	return pp.place.CanDeleteZettel(ctx, zid)
   145         -}
   146         -
   147         -func (pp *polPlace) DeleteZettel(ctx context.Context, zid id.Zid) error {
   148         -	meta, err := pp.place.GetMeta(ctx, zid)
   149         -	if err != nil {
   150         -		return err
   151         -	}
   152         -	user := pp.auth.GetUser(ctx)
   153         -	if pp.policy.CanDelete(user, meta) {
   154         -		return pp.place.DeleteZettel(ctx, zid)
   155         -	}
   156         -	return place.NewErrNotAllowed("Delete", user, zid)
   157         -}
   158         -
   159         -func (pp *polPlace) ReadStats(st *place.Stats) {
   160         -	pp.place.ReadStats(st)
   161         -}
   162         -
   163         -func (pp *polPlace) Dump(w io.Writer) {
   164         -	pp.place.Dump(w)
   165         -}

Changes to auth/policy/policy_test.go.

    17     17   
    18     18   	"zettelstore.de/z/auth"
    19     19   	"zettelstore.de/z/domain/id"
    20     20   	"zettelstore.de/z/domain/meta"
    21     21   )
    22     22   
    23     23   func TestPolicies(t *testing.T) {
           24  +	t.Parallel()
    24     25   	testScene := []struct {
    25     26   		readonly bool
    26     27   		withAuth bool
    27     28   		expert   bool
    28     29   	}{
    29     30   		{true, true, true},
    30     31   		{true, true, false},
................................................................................
    84     85   
    85     86   type authConfig struct{ expert bool }
    86     87   
    87     88   func (ac *authConfig) GetExpertMode() bool { return ac.expert }
    88     89   
    89     90   func (ac *authConfig) GetVisibility(m *meta.Meta) meta.Visibility {
    90     91   	if vis, ok := m.Get(meta.KeyVisibility); ok {
    91         -		switch vis {
    92         -		case meta.ValueVisibilityPublic:
    93         -			return meta.VisibilityPublic
    94         -		case meta.ValueVisibilityOwner:
    95         -			return meta.VisibilityOwner
    96         -		case meta.ValueVisibilityExpert:
    97         -			return meta.VisibilityExpert
    98         -		}
           92  +		return meta.GetVisibility(vis)
    99     93   	}
   100     94   	return meta.VisibilityLogin
   101     95   }
   102     96   
   103     97   func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly, isExpert bool) {
   104     98   	t.Helper()
   105     99   	anonUser := newAnon()
          100  +	creator := newCreator()
   106    101   	reader := newReader()
   107    102   	writer := newWriter()
   108    103   	owner := newOwner()
   109    104   	owner2 := newOwner2()
   110    105   	zettel := newZettel()
   111    106   	userZettel := newUserZettel()
   112    107   	testCases := []struct {
   113    108   		user *meta.Meta
   114    109   		meta *meta.Meta
   115    110   		exp  bool
   116    111   	}{
   117    112   		// No meta
   118    113   		{anonUser, nil, false},
          114  +		{creator, nil, false},
   119    115   		{reader, nil, false},
   120    116   		{writer, nil, false},
   121    117   		{owner, nil, false},
   122    118   		{owner2, nil, false},
   123    119   		// Ordinary zettel
   124    120   		{anonUser, zettel, !withAuth && !readonly},
          121  +		{creator, zettel, !readonly},
   125    122   		{reader, zettel, !withAuth && !readonly},
   126    123   		{writer, zettel, !readonly},
   127    124   		{owner, zettel, !readonly},
   128    125   		{owner2, zettel, !readonly},
   129    126   		// User zettel
   130    127   		{anonUser, userZettel, !withAuth && !readonly},
          128  +		{creator, userZettel, !withAuth && !readonly},
   131    129   		{reader, userZettel, !withAuth && !readonly},
   132    130   		{writer, userZettel, !withAuth && !readonly},
   133    131   		{owner, userZettel, !readonly},
   134    132   		{owner2, userZettel, !readonly},
   135    133   	}
   136    134   	for _, tc := range testCases {
   137    135   		t.Run("Create", func(tt *testing.T) {
................................................................................
   142    140   		})
   143    141   	}
   144    142   }
   145    143   
   146    144   func testRead(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) {
   147    145   	t.Helper()
   148    146   	anonUser := newAnon()
          147  +	creator := newCreator()
   149    148   	reader := newReader()
   150    149   	writer := newWriter()
   151    150   	owner := newOwner()
   152    151   	owner2 := newOwner2()
   153    152   	zettel := newZettel()
   154    153   	publicZettel := newPublicZettel()
          154  +	creatorZettel := newCreatorZettel()
   155    155   	loginZettel := newLoginZettel()
   156    156   	ownerZettel := newOwnerZettel()
   157    157   	expertZettel := newExpertZettel()
   158    158   	userZettel := newUserZettel()
   159    159   	testCases := []struct {
   160    160   		user *meta.Meta
   161    161   		meta *meta.Meta
   162    162   		exp  bool
   163    163   	}{
   164    164   		// No meta
   165    165   		{anonUser, nil, false},
          166  +		{creator, nil, false},
   166    167   		{reader, nil, false},
   167    168   		{writer, nil, false},
   168    169   		{owner, nil, false},
   169    170   		{owner2, nil, false},
   170    171   		// Ordinary zettel
   171    172   		{anonUser, zettel, !withAuth},
          173  +		{creator, zettel, !withAuth},
   172    174   		{reader, zettel, true},
   173    175   		{writer, zettel, true},
   174    176   		{owner, zettel, true},
   175    177   		{owner2, zettel, true},
   176    178   		// Public zettel
   177    179   		{anonUser, publicZettel, true},
          180  +		{creator, publicZettel, true},
   178    181   		{reader, publicZettel, true},
   179    182   		{writer, publicZettel, true},
   180    183   		{owner, publicZettel, true},
   181    184   		{owner2, publicZettel, true},
          185  +		// Creator zettel
          186  +		{anonUser, creatorZettel, !withAuth},
          187  +		{creator, creatorZettel, true},
          188  +		{reader, creatorZettel, true},
          189  +		{writer, creatorZettel, true},
          190  +		{owner, creatorZettel, true},
          191  +		{owner2, creatorZettel, true},
   182    192   		// Login zettel
   183    193   		{anonUser, loginZettel, !withAuth},
          194  +		{creator, loginZettel, !withAuth},
   184    195   		{reader, loginZettel, true},
   185    196   		{writer, loginZettel, true},
   186    197   		{owner, loginZettel, true},
   187    198   		{owner2, loginZettel, true},
   188    199   		// Owner zettel
   189    200   		{anonUser, ownerZettel, !withAuth},
          201  +		{creator, ownerZettel, !withAuth},
   190    202   		{reader, ownerZettel, !withAuth},
   191    203   		{writer, ownerZettel, !withAuth},
   192    204   		{owner, ownerZettel, true},
   193    205   		{owner2, ownerZettel, true},
   194    206   		// Expert zettel
   195    207   		{anonUser, expertZettel, !withAuth && expert},
          208  +		{creator, expertZettel, !withAuth && expert},
   196    209   		{reader, expertZettel, !withAuth && expert},
   197    210   		{writer, expertZettel, !withAuth && expert},
   198    211   		{owner, expertZettel, expert},
   199    212   		{owner2, expertZettel, expert},
   200    213   		// Other user zettel
   201    214   		{anonUser, userZettel, !withAuth},
          215  +		{creator, userZettel, !withAuth},
   202    216   		{reader, userZettel, !withAuth},
   203    217   		{writer, userZettel, !withAuth},
   204    218   		{owner, userZettel, true},
   205    219   		{owner2, userZettel, true},
   206    220   		// Own user zettel
          221  +		{creator, creator, true},
   207    222   		{reader, reader, true},
   208    223   		{writer, writer, true},
   209    224   		{owner, owner, true},
   210    225   		{owner, owner2, true},
   211    226   		{owner2, owner, true},
   212    227   		{owner2, owner2, true},
   213    228   	}
................................................................................
   220    235   		})
   221    236   	}
   222    237   }
   223    238   
   224    239   func testWrite(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) {
   225    240   	t.Helper()
   226    241   	anonUser := newAnon()
          242  +	creator := newCreator()
   227    243   	reader := newReader()
   228    244   	writer := newWriter()
   229    245   	owner := newOwner()
   230    246   	owner2 := newOwner2()
   231    247   	zettel := newZettel()
   232    248   	publicZettel := newPublicZettel()
   233    249   	loginZettel := newLoginZettel()
................................................................................
   246    262   		user *meta.Meta
   247    263   		old  *meta.Meta
   248    264   		new  *meta.Meta
   249    265   		exp  bool
   250    266   	}{
   251    267   		// No old and new meta
   252    268   		{anonUser, nil, nil, false},
          269  +		{creator, nil, nil, false},
   253    270   		{reader, nil, nil, false},
   254    271   		{writer, nil, nil, false},
   255    272   		{owner, nil, nil, false},
   256    273   		{owner2, nil, nil, false},
   257    274   		// No old meta
   258    275   		{anonUser, nil, zettel, false},
          276  +		{creator, nil, zettel, false},
   259    277   		{reader, nil, zettel, false},
   260    278   		{writer, nil, zettel, false},
   261    279   		{owner, nil, zettel, false},
   262    280   		{owner2, nil, zettel, false},
   263    281   		// No new meta
   264    282   		{anonUser, zettel, nil, false},
          283  +		{creator, zettel, nil, false},
   265    284   		{reader, zettel, nil, false},
   266    285   		{writer, zettel, nil, false},
   267    286   		{owner, zettel, nil, false},
   268    287   		{owner2, zettel, nil, false},
   269    288   		// Old an new zettel have different zettel identifier
   270    289   		{anonUser, zettel, publicZettel, false},
          290  +		{creator, zettel, publicZettel, false},
   271    291   		{reader, zettel, publicZettel, false},
   272    292   		{writer, zettel, publicZettel, false},
   273    293   		{owner, zettel, publicZettel, false},
   274    294   		{owner2, zettel, publicZettel, false},
   275    295   		// Overwrite a normal zettel
   276    296   		{anonUser, zettel, zettel, notAuthNotReadonly},
          297  +		{creator, zettel, zettel, notAuthNotReadonly},
   277    298   		{reader, zettel, zettel, notAuthNotReadonly},
   278    299   		{writer, zettel, zettel, !readonly},
   279    300   		{owner, zettel, zettel, !readonly},
   280    301   		{owner2, zettel, zettel, !readonly},
   281    302   		// Public zettel
   282    303   		{anonUser, publicZettel, publicZettel, notAuthNotReadonly},
          304  +		{creator, publicZettel, publicZettel, notAuthNotReadonly},
   283    305   		{reader, publicZettel, publicZettel, notAuthNotReadonly},
   284    306   		{writer, publicZettel, publicZettel, !readonly},
   285    307   		{owner, publicZettel, publicZettel, !readonly},
   286    308   		{owner2, publicZettel, publicZettel, !readonly},
   287    309   		// Login zettel
   288    310   		{anonUser, loginZettel, loginZettel, notAuthNotReadonly},
          311  +		{creator, loginZettel, loginZettel, notAuthNotReadonly},
   289    312   		{reader, loginZettel, loginZettel, notAuthNotReadonly},
   290    313   		{writer, loginZettel, loginZettel, !readonly},
   291    314   		{owner, loginZettel, loginZettel, !readonly},
   292    315   		{owner2, loginZettel, loginZettel, !readonly},
   293    316   		// Owner zettel
   294    317   		{anonUser, ownerZettel, ownerZettel, notAuthNotReadonly},
          318  +		{creator, ownerZettel, ownerZettel, notAuthNotReadonly},
   295    319   		{reader, ownerZettel, ownerZettel, notAuthNotReadonly},
   296    320   		{writer, ownerZettel, ownerZettel, notAuthNotReadonly},
   297    321   		{owner, ownerZettel, ownerZettel, !readonly},
   298    322   		{owner2, ownerZettel, ownerZettel, !readonly},
   299    323   		// Expert zettel
   300    324   		{anonUser, expertZettel, expertZettel, notAuthNotReadonly && expert},
          325  +		{creator, expertZettel, expertZettel, notAuthNotReadonly && expert},
   301    326   		{reader, expertZettel, expertZettel, notAuthNotReadonly && expert},
   302    327   		{writer, expertZettel, expertZettel, notAuthNotReadonly && expert},
   303    328   		{owner, expertZettel, expertZettel, !readonly && expert},
   304    329   		{owner2, expertZettel, expertZettel, !readonly && expert},
   305    330   		// Other user zettel
   306    331   		{anonUser, userZettel, userZettel, notAuthNotReadonly},
          332  +		{creator, userZettel, userZettel, notAuthNotReadonly},
   307    333   		{reader, userZettel, userZettel, notAuthNotReadonly},
   308    334   		{writer, userZettel, userZettel, notAuthNotReadonly},
   309    335   		{owner, userZettel, userZettel, !readonly},
   310    336   		{owner2, userZettel, userZettel, !readonly},
   311    337   		// Own user zettel
          338  +		{creator, creator, creator, !readonly},
   312    339   		{reader, reader, reader, !readonly},
   313    340   		{writer, writer, writer, !readonly},
   314    341   		{owner, owner, owner, !readonly},
   315    342   		{owner2, owner2, owner2, !readonly},
   316    343   		// Writer cannot change importand metadata of its own user zettel
   317    344   		{writer, writer, writerNew, notAuthNotReadonly},
   318    345   		// No r/o zettel
   319    346   		{anonUser, roFalse, roFalse, notAuthNotReadonly},
          347  +		{creator, roFalse, roFalse, notAuthNotReadonly},
   320    348   		{reader, roFalse, roFalse, notAuthNotReadonly},
   321    349   		{writer, roFalse, roFalse, !readonly},
   322    350   		{owner, roFalse, roFalse, !readonly},
   323    351   		{owner2, roFalse, roFalse, !readonly},
   324    352   		// Reader r/o zettel
   325    353   		{anonUser, roReader, roReader, false},
          354  +		{creator, roReader, roReader, false},
   326    355   		{reader, roReader, roReader, false},
   327    356   		{writer, roReader, roReader, !readonly},
   328    357   		{owner, roReader, roReader, !readonly},
   329    358   		{owner2, roReader, roReader, !readonly},
   330    359   		// Writer r/o zettel
   331    360   		{anonUser, roWriter, roWriter, false},
          361  +		{creator, roWriter, roWriter, false},
   332    362   		{reader, roWriter, roWriter, false},
   333    363   		{writer, roWriter, roWriter, false},
   334    364   		{owner, roWriter, roWriter, !readonly},
   335    365   		{owner2, roWriter, roWriter, !readonly},
   336    366   		// Owner r/o zettel
   337    367   		{anonUser, roOwner, roOwner, false},
          368  +		{creator, roOwner, roOwner, false},
   338    369   		{reader, roOwner, roOwner, false},
   339    370   		{writer, roOwner, roOwner, false},
   340    371   		{owner, roOwner, roOwner, false},
   341    372   		{owner2, roOwner, roOwner, false},
   342    373   		// r/o = true zettel
   343    374   		{anonUser, roTrue, roTrue, false},
          375  +		{creator, roTrue, roTrue, false},
   344    376   		{reader, roTrue, roTrue, false},
   345    377   		{writer, roTrue, roTrue, false},
   346    378   		{owner, roTrue, roTrue, false},
   347    379   		{owner2, roTrue, roTrue, false},
   348    380   	}
   349    381   	for _, tc := range testCases {
   350    382   		t.Run("Write", func(tt *testing.T) {
................................................................................
   355    387   		})
   356    388   	}
   357    389   }
   358    390   
   359    391   func testRename(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) {
   360    392   	t.Helper()
   361    393   	anonUser := newAnon()
          394  +	creator := newCreator()
   362    395   	reader := newReader()
   363    396   	writer := newWriter()
   364    397   	owner := newOwner()
   365    398   	owner2 := newOwner2()
   366    399   	zettel := newZettel()
   367    400   	expertZettel := newExpertZettel()
   368    401   	roFalse := newRoFalseZettel()
................................................................................
   374    407   	testCases := []struct {
   375    408   		user *meta.Meta
   376    409   		meta *meta.Meta
   377    410   		exp  bool
   378    411   	}{
   379    412   		// No meta
   380    413   		{anonUser, nil, false},
          414  +		{creator, nil, false},
   381    415   		{reader, nil, false},
   382    416   		{writer, nil, false},
   383    417   		{owner, nil, false},
   384    418   		{owner2, nil, false},
   385    419   		// Any zettel
   386    420   		{anonUser, zettel, notAuthNotReadonly},
          421  +		{creator, zettel, notAuthNotReadonly},
   387    422   		{reader, zettel, notAuthNotReadonly},
   388    423   		{writer, zettel, notAuthNotReadonly},
   389    424   		{owner, zettel, !readonly},
   390    425   		{owner2, zettel, !readonly},
   391    426   		// Expert zettel
   392    427   		{anonUser, expertZettel, notAuthNotReadonly && expert},
          428  +		{creator, expertZettel, notAuthNotReadonly && expert},
   393    429   		{reader, expertZettel, notAuthNotReadonly && expert},
   394    430   		{writer, expertZettel, notAuthNotReadonly && expert},
   395    431   		{owner, expertZettel, !readonly && expert},
   396    432   		{owner2, expertZettel, !readonly && expert},
   397    433   		// No r/o zettel
   398    434   		{anonUser, roFalse, notAuthNotReadonly},
          435  +		{creator, roFalse, notAuthNotReadonly},
   399    436   		{reader, roFalse, notAuthNotReadonly},
   400    437   		{writer, roFalse, notAuthNotReadonly},
   401    438   		{owner, roFalse, !readonly},
   402    439   		{owner2, roFalse, !readonly},
   403    440   		// Reader r/o zettel
   404    441   		{anonUser, roReader, false},
          442  +		{creator, roReader, false},
   405    443   		{reader, roReader, false},
   406    444   		{writer, roReader, notAuthNotReadonly},
   407    445   		{owner, roReader, !readonly},
   408    446   		{owner2, roReader, !readonly},
   409    447   		// Writer r/o zettel
   410    448   		{anonUser, roWriter, false},
          449  +		{creator, roWriter, false},
   411    450   		{reader, roWriter, false},
   412    451   		{writer, roWriter, false},
   413    452   		{owner, roWriter, !readonly},
   414    453   		{owner2, roWriter, !readonly},
   415    454   		// Owner r/o zettel
   416    455   		{anonUser, roOwner, false},
          456  +		{creator, roOwner, false},
   417    457   		{reader, roOwner, false},
   418    458   		{writer, roOwner, false},
   419    459   		{owner, roOwner, false},
   420    460   		{owner2, roOwner, false},
   421    461   		// r/o = true zettel
   422    462   		{anonUser, roTrue, false},
          463  +		{creator, roTrue, false},
   423    464   		{reader, roTrue, false},
   424    465   		{writer, roTrue, false},
   425    466   		{owner, roTrue, false},
   426    467   		{owner2, roTrue, false},
   427    468   	}
   428    469   	for _, tc := range testCases {
   429    470   		t.Run("Rename", func(tt *testing.T) {
................................................................................
   434    475   		})
   435    476   	}
   436    477   }
   437    478   
   438    479   func testDelete(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) {
   439    480   	t.Helper()
   440    481   	anonUser := newAnon()
          482  +	creator := newCreator()
   441    483   	reader := newReader()
   442    484   	writer := newWriter()
   443    485   	owner := newOwner()
   444    486   	owner2 := newOwner2()
   445    487   	zettel := newZettel()
   446    488   	expertZettel := newExpertZettel()
   447    489   	roFalse := newRoFalseZettel()
................................................................................
   453    495   	testCases := []struct {
   454    496   		user *meta.Meta
   455    497   		meta *meta.Meta
   456    498   		exp  bool
   457    499   	}{
   458    500   		// No meta
   459    501   		{anonUser, nil, false},
          502  +		{creator, nil, false},
   460    503   		{reader, nil, false},
   461    504   		{writer, nil, false},
   462    505   		{owner, nil, false},
   463    506   		{owner2, nil, false},
   464    507   		// Any zettel
   465    508   		{anonUser, zettel, notAuthNotReadonly},
          509  +		{creator, zettel, notAuthNotReadonly},
   466    510   		{reader, zettel, notAuthNotReadonly},
   467    511   		{writer, zettel, notAuthNotReadonly},
   468    512   		{owner, zettel, !readonly},
   469    513   		{owner2, zettel, !readonly},
   470    514   		// Expert zettel
   471    515   		{anonUser, expertZettel, notAuthNotReadonly && expert},
          516  +		{creator, expertZettel, notAuthNotReadonly && expert},
   472    517   		{reader, expertZettel, notAuthNotReadonly && expert},
   473    518   		{writer, expertZettel, notAuthNotReadonly && expert},
   474    519   		{owner, expertZettel, !readonly && expert},
   475    520   		{owner2, expertZettel, !readonly && expert},
   476    521   		// No r/o zettel
   477    522   		{anonUser, roFalse, notAuthNotReadonly},
          523  +		{creator, roFalse, notAuthNotReadonly},
   478    524   		{reader, roFalse, notAuthNotReadonly},
   479    525   		{writer, roFalse, notAuthNotReadonly},
   480    526   		{owner, roFalse, !readonly},
   481    527   		{owner2, roFalse, !readonly},
   482    528   		// Reader r/o zettel
   483    529   		{anonUser, roReader, false},
          530  +		{creator, roReader, false},
   484    531   		{reader, roReader, false},
   485    532   		{writer, roReader, notAuthNotReadonly},
   486    533   		{owner, roReader, !readonly},
   487    534   		{owner2, roReader, !readonly},
   488    535   		// Writer r/o zettel
   489    536   		{anonUser, roWriter, false},
          537  +		{creator, roWriter, false},
   490    538   		{reader, roWriter, false},
   491    539   		{writer, roWriter, false},
   492    540   		{owner, roWriter, !readonly},
   493    541   		{owner2, roWriter, !readonly},
   494    542   		// Owner r/o zettel
   495    543   		{anonUser, roOwner, false},
          544  +		{creator, roOwner, false},
   496    545   		{reader, roOwner, false},
   497    546   		{writer, roOwner, false},
   498    547   		{owner, roOwner, false},
   499    548   		{owner2, roOwner, false},
   500    549   		// r/o = true zettel
   501    550   		{anonUser, roTrue, false},
          551  +		{creator, roTrue, false},
   502    552   		{reader, roTrue, false},
   503    553   		{writer, roTrue, false},
   504    554   		{owner, roTrue, false},
   505    555   		{owner2, roTrue, false},
   506    556   	}
   507    557   	for _, tc := range testCases {
   508    558   		t.Run("Delete", func(tt *testing.T) {
................................................................................
   511    561   				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
   512    562   			}
   513    563   		})
   514    564   	}
   515    565   }
   516    566   
   517    567   const (
   518         -	readerZid = id.Zid(1013)
   519         -	writerZid = id.Zid(1015)
   520         -	ownerZid  = id.Zid(1017)
   521         -	owner2Zid = id.Zid(1019)
   522         -	zettelZid = id.Zid(1021)
   523         -	visZid    = id.Zid(1023)
   524         -	userZid   = id.Zid(1025)
          568  +	creatorZid = id.Zid(1013)
          569  +	readerZid  = id.Zid(1013)
          570  +	writerZid  = id.Zid(1015)
          571  +	ownerZid   = id.Zid(1017)
          572  +	owner2Zid  = id.Zid(1019)
          573  +	zettelZid  = id.Zid(1021)
          574  +	visZid     = id.Zid(1023)
          575  +	userZid    = id.Zid(1025)
   525    576   )
   526    577   
   527    578   func newAnon() *meta.Meta { return nil }
          579  +func newCreator() *meta.Meta {
          580  +	user := meta.New(creatorZid)
          581  +	user.Set(meta.KeyTitle, "Creator")
          582  +	user.Set(meta.KeyRole, meta.ValueRoleUser)
          583  +	user.Set(meta.KeyUserRole, meta.ValueUserRoleCreator)
          584  +	return user
          585  +}
   528    586   func newReader() *meta.Meta {
   529    587   	user := meta.New(readerZid)
   530    588   	user.Set(meta.KeyTitle, "Reader")
   531    589   	user.Set(meta.KeyRole, meta.ValueRoleUser)
   532    590   	user.Set(meta.KeyUserRole, meta.ValueUserRoleReader)
   533    591   	return user
   534    592   }
................................................................................
   559    617   	return m
   560    618   }
   561    619   func newPublicZettel() *meta.Meta {
   562    620   	m := meta.New(visZid)
   563    621   	m.Set(meta.KeyTitle, "Public Zettel")
   564    622   	m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic)
   565    623   	return m
          624  +}
          625  +func newCreatorZettel() *meta.Meta {
          626  +	m := meta.New(visZid)
          627  +	m.Set(meta.KeyTitle, "Creator Zettel")
          628  +	m.Set(meta.KeyVisibility, meta.ValueVisibilityCreator)
          629  +	return m
   566    630   }
   567    631   func newLoginZettel() *meta.Meta {
   568    632   	m := meta.New(visZid)
   569    633   	m.Set(meta.KeyTitle, "Login Zettel")
   570    634   	m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin)
   571    635   	return m
   572    636   }

Added box/box.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package box provides a generic interface to zettel boxes.
           12  +package box
           13  +
           14  +import (
           15  +	"context"
           16  +	"errors"
           17  +	"fmt"
           18  +	"io"
           19  +	"time"
           20  +
           21  +	"zettelstore.de/z/domain"
           22  +	"zettelstore.de/z/domain/id"
           23  +	"zettelstore.de/z/domain/meta"
           24  +	"zettelstore.de/z/search"
           25  +)
           26  +
           27  +// BaseBox is implemented by all Zettel boxes.
           28  +type BaseBox interface {
           29  +	// Location returns some information where the box is located.
           30  +	// Format is dependent of the box.
           31  +	Location() string
           32  +
           33  +	// CanCreateZettel returns true, if box could possibly create a new zettel.
           34  +	CanCreateZettel(ctx context.Context) bool
           35  +
           36  +	// CreateZettel creates a new zettel.
           37  +	// Returns the new zettel id (and an error indication).
           38  +	CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error)
           39  +
           40  +	// GetZettel retrieves a specific zettel.
           41  +	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
           42  +
           43  +	// GetMeta retrieves just the meta data of a specific zettel.
           44  +	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
           45  +
           46  +	// FetchZids returns the set of all zettel identifer managed by the box.
           47  +	FetchZids(ctx context.Context) (id.Set, error)
           48  +
           49  +	// CanUpdateZettel returns true, if box could possibly update the given zettel.
           50  +	CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool
           51  +
           52  +	// UpdateZettel updates an existing zettel.
           53  +	UpdateZettel(ctx context.Context, zettel domain.Zettel) error
           54  +
           55  +	// AllowRenameZettel returns true, if box will not disallow renaming the zettel.
           56  +	AllowRenameZettel(ctx context.Context, zid id.Zid) bool
           57  +
           58  +	// RenameZettel changes the current Zid to a new Zid.
           59  +	RenameZettel(ctx context.Context, curZid, newZid id.Zid) error
           60  +
           61  +	// CanDeleteZettel returns true, if box could possibly delete the given zettel.
           62  +	CanDeleteZettel(ctx context.Context, zid id.Zid) bool
           63  +
           64  +	// DeleteZettel removes the zettel from the box.
           65  +	DeleteZettel(ctx context.Context, zid id.Zid) error
           66  +}
           67  +
           68  +// ManagedBox is the interface of managed boxes.
           69  +type ManagedBox interface {
           70  +	BaseBox
           71  +
           72  +	// SelectMeta returns all zettel meta data that match the selection criteria.
           73  +	SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error)
           74  +
           75  +	// ReadStats populates st with box statistics
           76  +	ReadStats(st *ManagedBoxStats)
           77  +}
           78  +
           79  +// ManagedBoxStats records statistics about the box.
           80  +type ManagedBoxStats struct {
           81  +	// ReadOnly indicates that the content of a box cannot change.
           82  +	ReadOnly bool
           83  +
           84  +	// Zettel is the number of zettel managed by the box.
           85  +	Zettel int
           86  +}
           87  +
           88  +// StartStopper performs simple lifecycle management.
           89  +type StartStopper interface {
           90  +	// Start the box. Now all other functions of the box are allowed.
           91  +	// Starting an already started box is not allowed.
           92  +	Start(ctx context.Context) error
           93  +
           94  +	// Stop the started box. Now only the Start() function is allowed.
           95  +	Stop(ctx context.Context) error
           96  +}
           97  +
           98  +// Box is to be used outside the box package and its descendants.
           99  +type Box interface {
          100  +	BaseBox
          101  +
          102  +	// SelectMeta returns a list of metadata that comply to the given selection criteria.
          103  +	SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error)
          104  +
          105  +	// GetAllZettel retrieves a specific zettel from all managed boxes.
          106  +	GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error)
          107  +
          108  +	// GetAllMeta retrieves the meta data of a specific zettel from all managed boxes.
          109  +	GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error)
          110  +}
          111  +
          112  +// Stats record stattistics about a box.
          113  +type Stats struct {
          114  +	// ReadOnly indicates that boxes cannot be modified.
          115  +	ReadOnly bool
          116  +
          117  +	// NumManagedBoxes is the number of boxes managed.
          118  +	NumManagedBoxes int
          119  +
          120  +	// Zettel is the number of zettel managed by the box, including
          121  +	// duplicates across managed boxes.
          122  +	ZettelTotal int
          123  +
          124  +	// LastReload stores the timestamp when a full re-index was done.
          125  +	LastReload time.Time
          126  +
          127  +	// DurLastReload is the duration of the last full re-index run.
          128  +	DurLastReload time.Duration
          129  +
          130  +	// IndexesSinceReload counts indexing a zettel since the full re-index.
          131  +	IndexesSinceReload uint64
          132  +
          133  +	// ZettelIndexed is the number of zettel managed by the indexer.
          134  +	ZettelIndexed int
          135  +
          136  +	// IndexUpdates count the number of metadata updates.
          137  +	IndexUpdates uint64
          138  +
          139  +	// IndexedWords count the different words indexed.
          140  +	IndexedWords uint64
          141  +
          142  +	// IndexedUrls count the different URLs indexed.
          143  +	IndexedUrls uint64
          144  +}
          145  +
          146  +// Manager is a box-managing box.
          147  +type Manager interface {
          148  +	Box
          149  +	StartStopper
          150  +	Subject
          151  +
          152  +	// ReadStats populates st with box statistics
          153  +	ReadStats(st *Stats)
          154  +
          155  +	// Dump internal data to a Writer.
          156  +	Dump(w io.Writer)
          157  +}
          158  +
          159  +// UpdateReason gives an indication, why the ObserverFunc was called.
          160  +type UpdateReason uint8
          161  +
          162  +// Values for Reason
          163  +const (
          164  +	_        UpdateReason = iota
          165  +	OnReload              // Box was reloaded
          166  +	OnUpdate              // A zettel was created or changed
          167  +	OnDelete              // A zettel was removed
          168  +)
          169  +
          170  +// UpdateInfo contains all the data about a changed zettel.
          171  +type UpdateInfo struct {
          172  +	Box    Box
          173  +	Reason UpdateReason
          174  +	Zid    id.Zid
          175  +}
          176  +
          177  +// UpdateFunc is a function to be called when a change is detected.
          178  +type UpdateFunc func(UpdateInfo)
          179  +
          180  +// Subject is a box that notifies observers about changes.
          181  +type Subject interface {
          182  +	// RegisterObserver registers an observer that will be notified
          183  +	// if one or all zettel are found to be changed.
          184  +	RegisterObserver(UpdateFunc)
          185  +}
          186  +
          187  +// Enricher is used to update metadata by adding new properties.
          188  +type Enricher interface {
          189  +	// Enrich computes additional properties and updates the given metadata.
          190  +	// It is typically called by zettel reading methods.
          191  +	Enrich(ctx context.Context, m *meta.Meta, boxNumber int)
          192  +}
          193  +
          194  +// NoEnrichContext will signal an enricher that nothing has to be done.
          195  +// This is useful for an Indexer, but also for some box.Box calls, when
          196  +// just the plain metadata is needed.
          197  +func NoEnrichContext(ctx context.Context) context.Context {
          198  +	return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey)
          199  +}
          200  +
          201  +type ctxNoEnrichType struct{}
          202  +
          203  +var ctxNoEnrichKey ctxNoEnrichType
          204  +
          205  +// DoNotEnrich determines if the context is marked to not enrich metadata.
          206  +func DoNotEnrich(ctx context.Context) bool {
          207  +	_, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType)
          208  +	return ok
          209  +}
          210  +
          211  +// ErrNotAllowed is returned if the caller is not allowed to perform the operation.
          212  +type ErrNotAllowed struct {
          213  +	Op   string
          214  +	User *meta.Meta
          215  +	Zid  id.Zid
          216  +}
          217  +
          218  +// NewErrNotAllowed creates an new authorization error.
          219  +func NewErrNotAllowed(op string, user *meta.Meta, zid id.Zid) error {
          220  +	return &ErrNotAllowed{
          221  +		Op:   op,
          222  +		User: user,
          223  +		Zid:  zid,
          224  +	}
          225  +}
          226  +
          227  +func (err *ErrNotAllowed) Error() string {
          228  +	if err.User == nil {
          229  +		if err.Zid.IsValid() {
          230  +			return fmt.Sprintf(
          231  +				"operation %q on zettel %v not allowed for not authorized user",
          232  +				err.Op,
          233  +				err.Zid.String())
          234  +		}
          235  +		return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op)
          236  +	}
          237  +	if err.Zid.IsValid() {
          238  +		return fmt.Sprintf(
          239  +			"operation %q on zettel %v not allowed for user %v/%v",
          240  +			err.Op,
          241  +			err.Zid.String(),
          242  +			err.User.GetDefault(meta.KeyUserID, "?"),
          243  +			err.User.Zid.String())
          244  +	}
          245  +	return fmt.Sprintf(
          246  +		"operation %q not allowed for user %v/%v",
          247  +		err.Op,
          248  +		err.User.GetDefault(meta.KeyUserID, "?"),
          249  +		err.User.Zid.String())
          250  +}
          251  +
          252  +// Is return true, if the error is of type ErrNotAllowed.
          253  +func (err *ErrNotAllowed) Is(target error) bool { return true }
          254  +
          255  +// ErrStarted is returned when trying to start an already started box.
          256  +var ErrStarted = errors.New("box is already started")
          257  +
          258  +// ErrStopped is returned if calling methods on a box that was not started.
          259  +var ErrStopped = errors.New("box is stopped")
          260  +
          261  +// ErrReadOnly is returned if there is an attepmt to write to a read-only box.
          262  +var ErrReadOnly = errors.New("read-only box")
          263  +
          264  +// ErrNotFound is returned if a zettel was not found in the box.
          265  +var ErrNotFound = errors.New("zettel not found")
          266  +
          267  +// ErrConflict is returned if a box operation detected a conflict..
          268  +// One example: if calculating a new zettel identifier takes too long.
          269  +var ErrConflict = errors.New("conflict")
          270  +
          271  +// ErrInvalidID is returned if the zettel id is not appropriate for the box operation.
          272  +type ErrInvalidID struct{ Zid id.Zid }
          273  +
          274  +func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() }

Added box/compbox/compbox.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package compbox provides zettel that have computed content.
           12  +package compbox
           13  +
           14  +import (
           15  +	"context"
           16  +	"net/url"
           17  +
           18  +	"zettelstore.de/z/box"
           19  +	"zettelstore.de/z/box/manager"
           20  +	"zettelstore.de/z/domain"
           21  +	"zettelstore.de/z/domain/id"
           22  +	"zettelstore.de/z/domain/meta"
           23  +	"zettelstore.de/z/search"
           24  +)
           25  +
           26  +func init() {
           27  +	manager.Register(
           28  +		" comp",
           29  +		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
           30  +			return getCompBox(cdata.Number, cdata.Enricher), nil
           31  +		})
           32  +}
           33  +
           34  +type compBox struct {
           35  +	number   int
           36  +	enricher box.Enricher
           37  +}
           38  +
           39  +var myConfig *meta.Meta
           40  +var myZettel = map[id.Zid]struct {
           41  +	meta    func(id.Zid) *meta.Meta
           42  +	content func(*meta.Meta) string
           43  +}{
           44  +	id.VersionZid:              {genVersionBuildM, genVersionBuildC},
           45  +	id.HostZid:                 {genVersionHostM, genVersionHostC},
           46  +	id.OperatingSystemZid:      {genVersionOSM, genVersionOSC},
           47  +	id.BoxManagerZid:           {genManagerM, genManagerC},
           48  +	id.MetadataKeyZid:          {genKeysM, genKeysC},
           49  +	id.StartupConfigurationZid: {genConfigZettelM, genConfigZettelC},
           50  +}
           51  +
           52  +// Get returns the one program box.
           53  +func getCompBox(boxNumber int, mf box.Enricher) box.ManagedBox {
           54  +	return &compBox{number: boxNumber, enricher: mf}
           55  +}
           56  +
           57  +// Setup remembers important values.
           58  +func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() }
           59  +
           60  +func (pp *compBox) Location() string { return "" }
           61  +
           62  +func (pp *compBox) CanCreateZettel(ctx context.Context) bool { return false }
           63  +
           64  +func (pp *compBox) CreateZettel(
           65  +	ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
           66  +	return id.Invalid, box.ErrReadOnly
           67  +}
           68  +
           69  +func (pp *compBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
           70  +	if gen, ok := myZettel[zid]; ok && gen.meta != nil {
           71  +		if m := gen.meta(zid); m != nil {
           72  +			updateMeta(m)
           73  +			if genContent := gen.content; genContent != nil {
           74  +				return domain.Zettel{
           75  +					Meta:    m,
           76  +					Content: domain.NewContent(genContent(m)),
           77  +				}, nil
           78  +			}
           79  +			return domain.Zettel{Meta: m}, nil
           80  +		}
           81  +	}
           82  +	return domain.Zettel{}, box.ErrNotFound
           83  +}
           84  +
           85  +func (pp *compBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
           86  +	if gen, ok := myZettel[zid]; ok {
           87  +		if genMeta := gen.meta; genMeta != nil {
           88  +			if m := genMeta(zid); m != nil {
           89  +				updateMeta(m)
           90  +				return m, nil
           91  +			}
           92  +		}
           93  +	}
           94  +	return nil, box.ErrNotFound
           95  +}
           96  +
           97  +func (pp *compBox) FetchZids(ctx context.Context) (id.Set, error) {
           98  +	result := id.NewSetCap(len(myZettel))
           99  +	for zid, gen := range myZettel {
          100  +		if genMeta := gen.meta; genMeta != nil {
          101  +			if genMeta(zid) != nil {
          102  +				result[zid] = true
          103  +			}
          104  +		}
          105  +	}
          106  +	return result, nil
          107  +}
          108  +
          109  +func (pp *compBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) {
          110  +	for zid, gen := range myZettel {
          111  +		if genMeta := gen.meta; genMeta != nil {
          112  +			if m := genMeta(zid); m != nil {
          113  +				updateMeta(m)
          114  +				pp.enricher.Enrich(ctx, m, pp.number)
          115  +				if match(m) {
          116  +					res = append(res, m)
          117  +				}
          118  +			}
          119  +		}
          120  +	}
          121  +	return res, nil
          122  +}
          123  +
          124  +func (pp *compBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
          125  +	return false
          126  +}
          127  +
          128  +func (pp *compBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
          129  +	return box.ErrReadOnly
          130  +}
          131  +
          132  +func (pp *compBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
          133  +	_, ok := myZettel[zid]
          134  +	return !ok
          135  +}
          136  +
          137  +func (pp *compBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
          138  +	if _, ok := myZettel[curZid]; ok {
          139  +		return box.ErrReadOnly
          140  +	}
          141  +	return box.ErrNotFound
          142  +}
          143  +
          144  +func (pp *compBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false }
          145  +
          146  +func (pp *compBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
          147  +	if _, ok := myZettel[zid]; ok {
          148  +		return box.ErrReadOnly
          149  +	}
          150  +	return box.ErrNotFound
          151  +}
          152  +
          153  +func (pp *compBox) ReadStats(st *box.ManagedBoxStats) {
          154  +	st.ReadOnly = true
          155  +	st.Zettel = len(myZettel)
          156  +}
          157  +
          158  +func updateMeta(m *meta.Meta) {
          159  +	m.Set(meta.KeyNoIndex, meta.ValueTrue)
          160  +	m.Set(meta.KeySyntax, meta.ValueSyntaxZmk)
          161  +	m.Set(meta.KeyRole, meta.ValueRoleConfiguration)
          162  +	m.Set(meta.KeyLang, meta.ValueLangEN)
          163  +	m.Set(meta.KeyReadOnly, meta.ValueTrue)
          164  +	if _, ok := m.Get(meta.KeyVisibility); !ok {
          165  +		m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert)
          166  +	}
          167  +}

Added box/compbox/config.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package compbox provides zettel that have computed content.
           12  +package compbox
           13  +
           14  +import (
           15  +	"strings"
           16  +
           17  +	"zettelstore.de/z/domain/id"
           18  +	"zettelstore.de/z/domain/meta"
           19  +)
           20  +
           21  +func genConfigZettelM(zid id.Zid) *meta.Meta {
           22  +	if myConfig == nil {
           23  +		return nil
           24  +	}
           25  +	m := meta.New(zid)
           26  +	m.Set(meta.KeyTitle, "Zettelstore Startup Configuration")
           27  +	m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert)
           28  +	return m
           29  +}
           30  +
           31  +func genConfigZettelC(m *meta.Meta) string {
           32  +	var sb strings.Builder
           33  +	for i, p := range myConfig.Pairs(false) {
           34  +		if i > 0 {
           35  +			sb.WriteByte('\n')
           36  +		}
           37  +		sb.WriteString("; ''")
           38  +		sb.WriteString(p.Key)
           39  +		sb.WriteString("''")
           40  +		if p.Value != "" {
           41  +			sb.WriteString("\n: ``")
           42  +			for _, r := range p.Value {
           43  +				if r == '`' {
           44  +					sb.WriteByte('\\')
           45  +				}
           46  +				sb.WriteRune(r)
           47  +			}
           48  +			sb.WriteString("``")
           49  +		}
           50  +	}
           51  +	return sb.String()
           52  +}

Added box/compbox/keys.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package compbox provides zettel that have computed content.
           12  +package compbox
           13  +
           14  +import (
           15  +	"fmt"
           16  +	"strings"
           17  +
           18  +	"zettelstore.de/z/domain/id"
           19  +	"zettelstore.de/z/domain/meta"
           20  +)
           21  +
           22  +func genKeysM(zid id.Zid) *meta.Meta {
           23  +	m := meta.New(zid)
           24  +	m.Set(meta.KeyTitle, "Zettelstore Supported Metadata Keys")
           25  +	m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin)
           26  +	return m
           27  +}
           28  +
           29  +func genKeysC(*meta.Meta) string {
           30  +	keys := meta.GetSortedKeyDescriptions()
           31  +	var sb strings.Builder
           32  +	sb.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n")
           33  +	for _, kd := range keys {
           34  +		fmt.Fprintf(&sb,
           35  +			"|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty())
           36  +	}
           37  +	return sb.String()
           38  +}

Added box/compbox/manager.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package compbox provides zettel that have computed content.
           12  +package compbox
           13  +
           14  +import (
           15  +	"fmt"
           16  +	"strings"
           17  +
           18  +	"zettelstore.de/z/domain/id"
           19  +	"zettelstore.de/z/domain/meta"
           20  +	"zettelstore.de/z/kernel"
           21  +)
           22  +
           23  +func genManagerM(zid id.Zid) *meta.Meta {
           24  +	m := meta.New(zid)
           25  +	m.Set(meta.KeyTitle, "Zettelstore Box Manager")
           26  +	return m
           27  +}
           28  +
           29  +func genManagerC(*meta.Meta) string {
           30  +	kvl := kernel.Main.GetServiceStatistics(kernel.BoxService)
           31  +	if len(kvl) == 0 {
           32  +		return "No statistics available"
           33  +	}
           34  +	var sb strings.Builder
           35  +	sb.WriteString("|=Name|=Value>\n")
           36  +	for _, kv := range kvl {
           37  +		fmt.Fprintf(&sb, "| %v | %v\n", kv.Key, kv.Value)
           38  +	}
           39  +	return sb.String()
           40  +}

Added box/compbox/version.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package compbox provides zettel that have computed content.
           12  +package compbox
           13  +
           14  +import (
           15  +	"fmt"
           16  +
           17  +	"zettelstore.de/z/domain/id"
           18  +	"zettelstore.de/z/domain/meta"
           19  +	"zettelstore.de/z/kernel"
           20  +)
           21  +
           22  +func getVersionMeta(zid id.Zid, title string) *meta.Meta {
           23  +	m := meta.New(zid)
           24  +	m.Set(meta.KeyTitle, title)
           25  +	m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert)
           26  +	return m
           27  +}
           28  +
           29  +func genVersionBuildM(zid id.Zid) *meta.Meta {
           30  +	m := getVersionMeta(zid, "Zettelstore Version")
           31  +	m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic)
           32  +	return m
           33  +}
           34  +func genVersionBuildC(*meta.Meta) string {
           35  +	return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)
           36  +}
           37  +
           38  +func genVersionHostM(zid id.Zid) *meta.Meta {
           39  +	return getVersionMeta(zid, "Zettelstore Host")
           40  +}
           41  +func genVersionHostC(*meta.Meta) string {
           42  +	return kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string)
           43  +}
           44  +
           45  +func genVersionOSM(zid id.Zid) *meta.Meta {
           46  +	return getVersionMeta(zid, "Zettelstore Operating System")
           47  +}
           48  +func genVersionOSC(*meta.Meta) string {
           49  +	return fmt.Sprintf(
           50  +		"%v/%v",
           51  +		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string),
           52  +		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string),
           53  +	)
           54  +}

Added box/constbox/base.css.

            1  +*,*::before,*::after {
            2  +    box-sizing: border-box;
            3  +  }
            4  +  html {
            5  +    font-size: 1rem;
            6  +    font-family: serif;
            7  +    scroll-behavior: smooth;
            8  +    height: 100%;
            9  +  }
           10  +  body {
           11  +    margin: 0;
           12  +    min-height: 100vh;
           13  +    text-rendering: optimizeSpeed;
           14  +    line-height: 1.4;
           15  +    overflow-x: hidden;
           16  +    background-color: #f8f8f8 ;
           17  +    height: 100%;
           18  +  }
           19  +  nav.zs-menu {
           20  +    background-color: hsl(210, 28%, 90%);
           21  +    overflow: auto;
           22  +    white-space: nowrap;
           23  +    font-family: sans-serif;
           24  +    padding-left: .5rem;
           25  +  }
           26  +  nav.zs-menu > a {
           27  +    float:left;
           28  +    display: block;
           29  +    text-align: center;
           30  +    padding:.41rem .5rem;
           31  +    text-decoration: none;
           32  +    color:black;
           33  +  }
           34  +  nav.zs-menu > a:hover, .zs-dropdown:hover button {
           35  +    background-color: hsl(210, 28%, 80%);
           36  +  }
           37  +  nav.zs-menu form {
           38  +    float: right;
           39  +  }
           40  +  nav.zs-menu form input[type=text] {
           41  +    padding: .12rem;
           42  +    border: none;
           43  +    margin-top: .25rem;
           44  +    margin-right: .5rem;
           45  +  }
           46  +  .zs-dropdown {
           47  +    float: left;
           48  +    overflow: hidden;
           49  +  }
           50  +  .zs-dropdown > button {
           51  +    font-size: 16px;
           52  +    border: none;
           53  +    outline: none;
           54  +    color: black;
           55  +    padding:.41rem .5rem;
           56  +    background-color: inherit;
           57  +    font-family: inherit;
           58  +    margin: 0;
           59  +  }
           60  +  .zs-dropdown-content {
           61  +    display: none;
           62  +    position: absolute;
           63  +    background-color: #f9f9f9;
           64  +    min-width: 160px;
           65  +    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
           66  +    z-index: 1;
           67  +  }
           68  +  .zs-dropdown-content > a {
           69  +    float: none;
           70  +    color: black;
           71  +    padding:.41rem .5rem;
           72  +    text-decoration: none;
           73  +    display: block;
           74  +    text-align: left;
           75  +  }
           76  +  .zs-dropdown-content > a:hover {
           77  +    background-color: hsl(210, 28%, 75%);
           78  +  }
           79  +  .zs-dropdown:hover > .zs-dropdown-content {
           80  +    display: block;
           81  +  }
           82  +  main {
           83  +    padding: 0 1rem;
           84  +  }
           85  +  article > * + * {
           86  +    margin-top: .5rem;
           87  +  }
           88  +  article header {
           89  +    padding: 0;
           90  +    margin: 0;
           91  +  }
           92  +  h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal }
           93  +  h1 { font-size:1.5rem;  margin:.65rem 0 }
           94  +  h2 { font-size:1.25rem; margin:.70rem 0 }
           95  +  h3 { font-size:1.15rem; margin:.75rem 0 }
           96  +  h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold }
           97  +  h5 { font-size:1.05rem; margin:.8rem 0 }
           98  +  h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter }
           99  +  p {
          100  +    margin: .5rem 0 0 0;
          101  +  }
          102  +  ol,ul {
          103  +    padding-left: 1.1rem;
          104  +  }
          105  +  li,figure,figcaption,dl {
          106  +    margin: 0;
          107  +  }
          108  +  dt {
          109  +    margin: .5rem 0 0 0;
          110  +  }
          111  +  dt+dd {
          112  +    margin-top: 0;
          113  +  }
          114  +  dd {
          115  +    margin: .5rem 0 0 2rem;
          116  +  }
          117  +  dd > p:first-child {
          118  +    margin: 0 0 0 0;
          119  +  }
          120  +  blockquote {
          121  +    border-left: 0.5rem solid lightgray;
          122  +    padding-left: 1rem;
          123  +    margin-left: 1rem;
          124  +    margin-right: 2rem;
          125  +    font-style: italic;
          126  +  }
          127  +  blockquote p {
          128  +    margin-bottom: .5rem;
          129  +  }
          130  +  blockquote cite {
          131  +    font-style: normal;
          132  +  }
          133  +  table {
          134  +    border-collapse: collapse;
          135  +    border-spacing: 0;
          136  +    max-width: 100%;
          137  +  }
          138  +  th,td {
          139  +    text-align: left;
          140  +    padding: .25rem .5rem;
          141  +  }
          142  +  td { border-bottom: 1px solid hsl(0, 0%, 85%); }
          143  +  thead th { border-bottom: 2px solid hsl(0, 0%, 70%); }
          144  +  tfoot th { border-top: 2px solid hsl(0, 0%, 70%); }
          145  +  main form {
          146  +    padding: 0 .5em;
          147  +    margin: .5em 0 0 0;
          148  +  }
          149  +  main form:after {
          150  +    content: ".";
          151  +    display: block;
          152  +    height: 0;
          153  +    clear: both;
          154  +    visibility: hidden;
          155  +  }
          156  +  main form div {
          157  +    margin: .5em 0 0 0
          158  +  }
          159  +  input {
          160  +    font-family: monospace;
          161  +  }
          162  +  input[type="submit"],button,select {
          163  +    font: inherit;
          164  +  }
          165  +  label { font-family: sans-serif; font-size:.9rem }
          166  +  label::after { content:":" }
          167  +  textarea {
          168  +    font-family: monospace;
          169  +    resize: vertical;
          170  +    width: 100%;
          171  +  }
          172  +  .zs-input {
          173  +    padding: .5em;
          174  +    display:block;
          175  +    border:none;
          176  +    border-bottom:1px solid #ccc;
          177  +    width:100%;
          178  +  }
          179  +  .zs-button {
          180  +    float:right;
          181  +    margin: .5em 0 .5em 1em;
          182  +  }
          183  +  a:not([class]) {
          184  +    text-decoration-skip-ink: auto;
          185  +  }
          186  +  .zs-broken {
          187  +    text-decoration: line-through;
          188  +  }
          189  +  img {
          190  +    max-width: 100%;
          191  +  }
          192  +  .zs-endnotes {
          193  +    padding-top: .5rem;
          194  +    border-top: 1px solid;
          195  +  }
          196  +  code,pre,kbd {
          197  +    font-family: monospace;
          198  +    font-size: 85%;
          199  +  }
          200  +  code {
          201  +    padding: .1rem .2rem;
          202  +    background: #f0f0f0;
          203  +    border: 1px solid #ccc;
          204  +    border-radius: .25rem;
          205  +  }
          206  +  pre {
          207  +    padding: .5rem .7rem;
          208  +    max-width: 100%;
          209  +    overflow: auto;
          210  +    border: 1px solid #ccc;
          211  +    border-radius: .5rem;
          212  +    background: #f0f0f0;
          213  +  }
          214  +  pre code {
          215  +    font-size: 95%;
          216  +    position: relative;
          217  +    padding: 0;
          218  +    border: none;
          219  +  }
          220  +  div.zs-indication {
          221  +    padding: .5rem .7rem;
          222  +    max-width: 100%;
          223  +    border-radius: .5rem;
          224  +    border: 1px solid black;
          225  +  }
          226  +  div.zs-indication p:first-child {
          227  +    margin-top: 0;
          228  +  }
          229  +  span.zs-indication {
          230  +    border: 1px solid black;
          231  +    border-radius: .25rem;
          232  +    padding: .1rem .2rem;
          233  +    font-size: 95%;
          234  +  }
          235  +  .zs-example { border-style: dotted !important }
          236  +  .zs-error {
          237  +    background-color: lightpink;
          238  +    border-style: none !important;
          239  +    font-weight: bold;
          240  +  }
          241  +  kbd {
          242  +    background: hsl(210, 5%, 100%);
          243  +    border: 1px solid hsl(210, 5%, 70%);
          244  +    border-radius: .25rem;
          245  +    padding: .1rem .2rem;
          246  +    font-size: 75%;
          247  +  }
          248  +  .zs-meta {
          249  +    font-size:.75rem;
          250  +    color:#444;
          251  +    margin-bottom:1rem;
          252  +  }
          253  +  .zs-meta a {
          254  +    color:#444;
          255  +  }
          256  +  h1+.zs-meta {
          257  +    margin-top:-1rem;
          258  +  }
          259  +  details > summary {
          260  +    width: 100%;
          261  +    background-color: #eee;
          262  +    font-family:sans-serif;
          263  +  }
          264  +  details > ul {
          265  +    margin-top:0;
          266  +    padding-left:2rem;
          267  +    background-color: #eee;
          268  +  }
          269  +  footer {
          270  +    padding: 0 1rem;
          271  +  }
          272  +  @media (prefers-reduced-motion: reduce) {
          273  +    * {
          274  +      animation-duration: 0.01ms !important;
          275  +      animation-iteration-count: 1 !important;
          276  +      transition-duration: 0.01ms !important;
          277  +      scroll-behavior: auto !important;
          278  +    }
          279  +  }

Added box/constbox/base.mustache.

            1  +<!DOCTYPE html>
            2  +<html{{#Lang}} lang="{{Lang}}"{{/Lang}}>
            3  +<head>
            4  +<meta charset="utf-8">
            5  +<meta name="referrer" content="no-referrer">
            6  +<meta name="viewport" content="width=device-width, initial-scale=1.0">
            7  +<meta name="generator" content="Zettelstore">
            8  +<meta name="format-detection" content="telephone=no">
            9  +{{{MetaHeader}}}
           10  +<link rel="stylesheet" href="{{{CSSBaseURL}}}">
           11  +<link rel="stylesheet" href="{{{CSSUserURL}}}">
           12  +<title>{{Title}}</title>
           13  +</head>
           14  +<body>
           15  +<nav class="zs-menu">
           16  +<a href="{{{HomeURL}}}">Home</a>
           17  +{{#WithUser}}
           18  +<div class="zs-dropdown">
           19  +<button>User</button>
           20  +<nav class="zs-dropdown-content">
           21  +{{#WithAuth}}
           22  +{{#UserIsValid}}
           23  +<a href="{{{UserZettelURL}}}">{{UserIdent}}</a>
           24  +{{/UserIsValid}}
           25  +{{^UserIsValid}}
           26  +<a href="{{{LoginURL}}}">Login</a>
           27  +{{/UserIsValid}}
           28  +{{#UserIsValid}}
           29  +<a href="{{{UserLogoutURL}}}">Logout</a>
           30  +{{/UserIsValid}}
           31  +{{/WithAuth}}
           32  +</nav>
           33  +</div>
           34  +{{/WithUser}}
           35  +<div class="zs-dropdown">
           36  +<button>Lists</button>
           37  +<nav class="zs-dropdown-content">
           38  +<a href="{{{ListZettelURL}}}">List Zettel</a>
           39  +<a href="{{{ListRolesURL}}}">List Roles</a>
           40  +<a href="{{{ListTagsURL}}}">List Tags</a>
           41  +</nav>
           42  +</div>
           43  +{{#HasNewZettelLinks}}
           44  +<div class="zs-dropdown">
           45  +<button>New</button>
           46  +<nav class="zs-dropdown-content">
           47  +{{#NewZettelLinks}}
           48  +<a href="{{{URL}}}">{{Text}}</a>
           49  +{{/NewZettelLinks}}
           50  +</nav>
           51  +</div>
           52  +{{/HasNewZettelLinks}}
           53  +<form action="{{{SearchURL}}}">
           54  +<input type="text" placeholder="Search.." name="s">
           55  +</form>
           56  +</nav>
           57  +<main class="content">
           58  +{{{Content}}}
           59  +</main>
           60  +{{#FooterHTML}}
           61  +<footer>
           62  +{{{FooterHTML}}}
           63  +</footer>
           64  +{{/FooterHTML}}
           65  +</body>
           66  +</html>

Added box/constbox/constbox.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package constbox puts zettel inside the executable.
           12  +package constbox
           13  +
           14  +import (
           15  +	"context"
           16  +	_ "embed" // Allow to embed file content
           17  +	"net/url"
           18  +
           19  +	"zettelstore.de/z/box"
           20  +	"zettelstore.de/z/box/manager"
           21  +	"zettelstore.de/z/domain"
           22  +	"zettelstore.de/z/domain/id"
           23  +	"zettelstore.de/z/domain/meta"
           24  +	"zettelstore.de/z/search"
           25  +)
           26  +
           27  +func init() {
           28  +	manager.Register(
           29  +		" const",
           30  +		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
           31  +			return &constBox{
           32  +				number:   cdata.Number,
           33  +				zettel:   constZettelMap,
           34  +				enricher: cdata.Enricher,
           35  +			}, nil
           36  +		})
           37  +}
           38  +
           39  +type constHeader map[string]string
           40  +
           41  +func makeMeta(zid id.Zid, h constHeader) *meta.Meta {
           42  +	m := meta.New(zid)
           43  +	for k, v := range h {
           44  +		m.Set(k, v)
           45  +	}
           46  +	return m
           47  +}
           48  +
           49  +type constZettel struct {
           50  +	header  constHeader
           51  +	content domain.Content
           52  +}
           53  +
           54  +type constBox struct {
           55  +	number   int
           56  +	zettel   map[id.Zid]constZettel
           57  +	enricher box.Enricher
           58  +}
           59  +
           60  +func (cp *constBox) Location() string {
           61  +	return "const:"
           62  +}
           63  +
           64  +func (cp *constBox) CanCreateZettel(ctx context.Context) bool { return false }
           65  +
           66  +func (cp *constBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
           67  +	return id.Invalid, box.ErrReadOnly
           68  +}
           69  +
           70  +func (cp *constBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
           71  +	if z, ok := cp.zettel[zid]; ok {
           72  +		return domain.Zettel{Meta: makeMeta(zid, z.header), Content: z.content}, nil
           73  +	}
           74  +	return domain.Zettel{}, box.ErrNotFound
           75  +}
           76  +
           77  +func (cp *constBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
           78  +	if z, ok := cp.zettel[zid]; ok {
           79  +		return makeMeta(zid, z.header), nil
           80  +	}
           81  +	return nil, box.ErrNotFound
           82  +}
           83  +
           84  +func (cp *constBox) FetchZids(ctx context.Context) (id.Set, error) {
           85  +	result := id.NewSetCap(len(cp.zettel))
           86  +	for zid := range cp.zettel {
           87  +		result[zid] = true
           88  +	}
           89  +	return result, nil
           90  +}
           91  +
           92  +func (cp *constBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) {
           93  +	for zid, zettel := range cp.zettel {
           94  +		m := makeMeta(zid, zettel.header)
           95  +		cp.enricher.Enrich(ctx, m, cp.number)
           96  +		if match(m) {
           97  +			res = append(res, m)
           98  +		}
           99  +	}
          100  +	return res, nil
          101  +}
          102  +
          103  +func (cp *constBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
          104  +	return false
          105  +}
          106  +
          107  +func (cp *constBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
          108  +	return box.ErrReadOnly
          109  +}
          110  +
          111  +func (cp *constBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
          112  +	_, ok := cp.zettel[zid]
          113  +	return !ok
          114  +}
          115  +
          116  +func (cp *constBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
          117  +	if _, ok := cp.zettel[curZid]; ok {
          118  +		return box.ErrReadOnly
          119  +	}
          120  +	return box.ErrNotFound
          121  +}
          122  +func (cp *constBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false }
          123  +
          124  +func (cp *constBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
          125  +	if _, ok := cp.zettel[zid]; ok {
          126  +		return box.ErrReadOnly
          127  +	}
          128  +	return box.ErrNotFound
          129  +}
          130  +
          131  +func (cp *constBox) ReadStats(st *box.ManagedBoxStats) {
          132  +	st.ReadOnly = true
          133  +	st.Zettel = len(cp.zettel)
          134  +}
          135  +
          136  +const syntaxTemplate = "mustache"
          137  +
          138  +var constZettelMap = map[id.Zid]constZettel{
          139  +	id.ConfigurationZid: {
          140  +		constHeader{
          141  +			meta.KeyTitle:      "Zettelstore Runtime Configuration",
          142  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          143  +			meta.KeySyntax:     meta.ValueSyntaxNone,
          144  +			meta.KeyNoIndex:    meta.ValueTrue,
          145  +			meta.KeyVisibility: meta.ValueVisibilityOwner,
          146  +		},
          147  +		domain.NewContent("")},
          148  +	id.LicenseZid: {
          149  +		constHeader{
          150  +			meta.KeyTitle:      "Zettelstore License",
          151  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          152  +			meta.KeySyntax:     meta.ValueSyntaxText,
          153  +			meta.KeyLang:       meta.ValueLangEN,
          154  +			meta.KeyReadOnly:   meta.ValueTrue,
          155  +			meta.KeyVisibility: meta.ValueVisibilityPublic,
          156  +		},
          157  +		domain.NewContent(contentLicense)},
          158  +	id.AuthorsZid: {
          159  +		constHeader{
          160  +			meta.KeyTitle:      "Zettelstore Contributors",
          161  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          162  +			meta.KeySyntax:     meta.ValueSyntaxZmk,
          163  +			meta.KeyLang:       meta.ValueLangEN,
          164  +			meta.KeyReadOnly:   meta.ValueTrue,
          165  +			meta.KeyVisibility: meta.ValueVisibilityPublic,
          166  +		},
          167  +		domain.NewContent(contentContributors)},
          168  +	id.DependenciesZid: {
          169  +		constHeader{
          170  +			meta.KeyTitle:      "Zettelstore Dependencies",
          171  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          172  +			meta.KeySyntax:     meta.ValueSyntaxZmk,
          173  +			meta.KeyLang:       meta.ValueLangEN,
          174  +			meta.KeyReadOnly:   meta.ValueTrue,
          175  +			meta.KeyVisibility: meta.ValueVisibilityPublic,
          176  +		},
          177  +		domain.NewContent(contentDependencies)},
          178  +	id.BaseTemplateZid: {
          179  +		constHeader{
          180  +			meta.KeyTitle:      "Zettelstore Base HTML Template",
          181  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          182  +			meta.KeySyntax:     syntaxTemplate,
          183  +			meta.KeyNoIndex:    meta.ValueTrue,
          184  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          185  +		},
          186  +		domain.NewContent(contentBaseMustache)},
          187  +	id.LoginTemplateZid: {
          188  +		constHeader{
          189  +			meta.KeyTitle:      "Zettelstore Login Form HTML Template",
          190  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          191  +			meta.KeySyntax:     syntaxTemplate,
          192  +			meta.KeyNoIndex:    meta.ValueTrue,
          193  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          194  +		},
          195  +		domain.NewContent(contentLoginMustache)},
          196  +	id.ZettelTemplateZid: {
          197  +		constHeader{
          198  +			meta.KeyTitle:      "Zettelstore Zettel HTML Template",
          199  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          200  +			meta.KeySyntax:     syntaxTemplate,
          201  +			meta.KeyNoIndex:    meta.ValueTrue,
          202  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          203  +		},
          204  +		domain.NewContent(contentZettelMustache)},
          205  +	id.InfoTemplateZid: {
          206  +		constHeader{
          207  +			meta.KeyTitle:      "Zettelstore Info HTML Template",
          208  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          209  +			meta.KeySyntax:     syntaxTemplate,
          210  +			meta.KeyNoIndex:    meta.ValueTrue,
          211  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          212  +		},
          213  +		domain.NewContent(contentInfoMustache)},
          214  +	id.ContextTemplateZid: {
          215  +		constHeader{
          216  +			meta.KeyTitle:      "Zettelstore Context HTML Template",
          217  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          218  +			meta.KeySyntax:     syntaxTemplate,
          219  +			meta.KeyNoIndex:    meta.ValueTrue,
          220  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          221  +		},
          222  +		domain.NewContent(contentContextMustache)},
          223  +	id.FormTemplateZid: {
          224  +		constHeader{
          225  +			meta.KeyTitle:      "Zettelstore Form HTML Template",
          226  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          227  +			meta.KeySyntax:     syntaxTemplate,
          228  +			meta.KeyNoIndex:    meta.ValueTrue,
          229  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          230  +		},
          231  +		domain.NewContent(contentFormMustache)},
          232  +	id.RenameTemplateZid: {
          233  +		constHeader{
          234  +			meta.KeyTitle:      "Zettelstore Rename Form HTML Template",
          235  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          236  +			meta.KeySyntax:     syntaxTemplate,
          237  +			meta.KeyNoIndex:    meta.ValueTrue,
          238  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          239  +		},
          240  +		domain.NewContent(contentRenameMustache)},
          241  +	id.DeleteTemplateZid: {
          242  +		constHeader{
          243  +			meta.KeyTitle:      "Zettelstore Delete HTML Template",
          244  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          245  +			meta.KeySyntax:     syntaxTemplate,
          246  +			meta.KeyNoIndex:    meta.ValueTrue,
          247  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          248  +		},
          249  +		domain.NewContent(contentDeleteMustache)},
          250  +	id.ListTemplateZid: {
          251  +		constHeader{
          252  +			meta.KeyTitle:      "Zettelstore List Zettel HTML Template",
          253  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          254  +			meta.KeySyntax:     syntaxTemplate,
          255  +			meta.KeyNoIndex:    meta.ValueTrue,
          256  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          257  +		},
          258  +		domain.NewContent(contentListZettelMustache)},
          259  +	id.RolesTemplateZid: {
          260  +		constHeader{
          261  +			meta.KeyTitle:      "Zettelstore List Roles HTML Template",
          262  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          263  +			meta.KeySyntax:     syntaxTemplate,
          264  +			meta.KeyNoIndex:    meta.ValueTrue,
          265  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          266  +		},
          267  +		domain.NewContent(contentListRolesMustache)},
          268  +	id.TagsTemplateZid: {
          269  +		constHeader{
          270  +			meta.KeyTitle:      "Zettelstore List Tags HTML Template",
          271  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          272  +			meta.KeySyntax:     syntaxTemplate,
          273  +			meta.KeyNoIndex:    meta.ValueTrue,
          274  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          275  +		},
          276  +		domain.NewContent(contentListTagsMustache)},
          277  +	id.ErrorTemplateZid: {
          278  +		constHeader{
          279  +			meta.KeyTitle:      "Zettelstore Error HTML Template",
          280  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          281  +			meta.KeySyntax:     syntaxTemplate,
          282  +			meta.KeyNoIndex:    meta.ValueTrue,
          283  +			meta.KeyVisibility: meta.ValueVisibilityExpert,
          284  +		},
          285  +		domain.NewContent(contentErrorMustache)},
          286  +	id.BaseCSSZid: {
          287  +		constHeader{
          288  +			meta.KeyTitle:      "Zettelstore Base CSS",
          289  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          290  +			meta.KeySyntax:     "css",
          291  +			meta.KeyNoIndex:    meta.ValueTrue,
          292  +			meta.KeyVisibility: meta.ValueVisibilityPublic,
          293  +		},
          294  +		domain.NewContent(contentBaseCSS)},
          295  +	id.UserCSSZid: {
          296  +		constHeader{
          297  +			meta.KeyTitle:      "Zettelstore User CSS",
          298  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          299  +			meta.KeySyntax:     "css",
          300  +			meta.KeyVisibility: meta.ValueVisibilityPublic,
          301  +		},
          302  +		domain.NewContent("/* User-defined CSS */")},
          303  +	id.EmojiZid: {
          304  +		constHeader{
          305  +			meta.KeyTitle:      "Generic Emoji",
          306  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          307  +			meta.KeySyntax:     meta.ValueSyntaxGif,
          308  +			meta.KeyReadOnly:   meta.ValueTrue,
          309  +			meta.KeyVisibility: meta.ValueVisibilityPublic,
          310  +		},
          311  +		domain.NewContent(contentEmoji)},
          312  +	id.TOCNewTemplateZid: {
          313  +		constHeader{
          314  +			meta.KeyTitle:      "New Menu",
          315  +			meta.KeyRole:       meta.ValueRoleConfiguration,
          316  +			meta.KeySyntax:     meta.ValueSyntaxZmk,
          317  +			meta.KeyLang:       meta.ValueLangEN,
          318  +			meta.KeyVisibility: meta.ValueVisibilityCreator,
          319  +		},
          320  +		domain.NewContent(contentNewTOCZettel)},
          321  +	id.TemplateNewZettelZid: {
          322  +		constHeader{
          323  +			meta.KeyTitle:      "New Zettel",
          324  +			meta.KeyRole:       meta.ValueRoleZettel,
          325  +			meta.KeySyntax:     meta.ValueSyntaxZmk,
          326  +			meta.KeyVisibility: meta.ValueVisibilityCreator,
          327  +		},
          328  +		domain.NewContent("")},
          329  +	id.TemplateNewUserZid: {
          330  +		constHeader{
          331  +			meta.KeyTitle:                       "New User",
          332  +			meta.KeyRole:                        meta.ValueRoleUser,
          333  +			meta.KeySyntax:                      meta.ValueSyntaxNone,
          334  +			meta.NewPrefix + meta.KeyCredential: "",
          335  +			meta.NewPrefix + meta.KeyUserID:     "",
          336  +			meta.NewPrefix + meta.KeyUserRole:   meta.ValueUserRoleReader,
          337  +			meta.KeyVisibility:                  meta.ValueVisibilityOwner,
          338  +		},
          339  +		domain.NewContent("")},
          340  +	id.DefaultHomeZid: {
          341  +		constHeader{
          342  +			meta.KeyTitle:  "Home",
          343  +			meta.KeyRole:   meta.ValueRoleZettel,
          344  +			meta.KeySyntax: meta.ValueSyntaxZmk,
          345  +			meta.KeyLang:   meta.ValueLangEN,
          346  +		},
          347  +		domain.NewContent(contentHomeZettel)},
          348  +}
          349  +
          350  +//go:embed license.txt
          351  +var contentLicense string
          352  +
          353  +//go:embed contributors.zettel
          354  +var contentContributors string
          355  +
          356  +//go:embed dependencies.zettel
          357  +var contentDependencies string
          358  +
          359  +//go:embed base.mustache
          360  +var contentBaseMustache string
          361  +
          362  +//go:embed login.mustache
          363  +var contentLoginMustache string
          364  +
          365  +//go:embed zettel.mustache
          366  +var contentZettelMustache string
          367  +
          368  +//go:embed info.mustache
          369  +var contentInfoMustache string
          370  +
          371  +//go:embed context.mustache
          372  +var contentContextMustache string
          373  +
          374  +//go:embed form.mustache
          375  +var contentFormMustache string
          376  +
          377  +//go:embed rename.mustache
          378  +var contentRenameMustache string
          379  +
          380  +//go:embed delete.mustache
          381  +var contentDeleteMustache string
          382  +
          383  +//go:embed listzettel.mustache
          384  +var contentListZettelMustache string
          385  +
          386  +//go:embed listroles.mustache
          387  +var contentListRolesMustache string
          388  +
          389  +//go:embed listtags.mustache
          390  +var contentListTagsMustache string
          391  +
          392  +//go:embed error.mustache
          393  +var contentErrorMustache string
          394  +
          395  +//go:embed base.css
          396  +var contentBaseCSS string
          397  +
          398  +//go:embed emoji_spin.gif
          399  +var contentEmoji string
          400  +
          401  +//go:embed newtoc.zettel
          402  +var contentNewTOCZettel string
          403  +
          404  +//go:embed home.zettel
          405  +var contentHomeZettel string

Added box/constbox/context.mustache.

            1  +<nav>
            2  +<header>
            3  +<h1>{{Title}}</h1>
            4  +<div class="zs-meta">
            5  +<a href="{{{InfoURL}}}">Info</a>
            6  +&#183; <a href="?dir=backward">Backward</a>
            7  +&#183; <a href="?dir=both">Both</a>
            8  +&#183; <a href="?dir=forward">Forward</a>
            9  +&#183; Depth:{{#Depths}}&#x2000;<a href="{{{URL}}}">{{{Text}}}</a>{{/Depths}}
           10  +</div>
           11  +</header>
           12  +<p><a href="{{{Start.URL}}}">{{{Start.Text}}}</a></p>
           13  +<ul>
           14  +{{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li>
           15  +{{/Metas}}</ul>
           16  +</nav>

Added box/constbox/contributors.zettel.

            1  +Zettelstore is a software for humans made from humans.
            2  +
            3  +=== Licensor(s)
            4  +* Detlef Stern [[mailto:ds@zettelstore.de]]
            5  +** Main author
            6  +** Maintainer
            7  +
            8  +=== Contributors

Added box/constbox/delete.mustache.

            1  +<article>
            2  +<header>
            3  +<h1>Delete Zettel {{Zid}}</h1>
            4  +</header>
            5  +<p>Do you really want to delete this zettel?</p>
            6  +<dl>
            7  +{{#MetaPairs}}
            8  +<dt>{{Key}}:</dt><dd>{{Value}}</dd>
            9  +{{/MetaPairs}}
           10  +</dl>
           11  +<form method="POST">
           12  +<input class="zs-button" type="submit" value="Delete">
           13  +</form>
           14  +</article>
           15  +{{end}}

Added box/constbox/dependencies.zettel.

            1  +Zettelstore is made with the help of other software and other artifacts.
            2  +Thank you very much!
            3  +
            4  +This zettel lists all of them, together with their license.
            5  +
            6  +=== Go runtime and associated libraries
            7  +; License
            8  +: BSD 3-Clause "New" or "Revised" License
            9  +```
           10  +Copyright (c) 2009 The Go Authors. All rights reserved.
           11  +
           12  +Redistribution and use in source and binary forms, with or without
           13  +modification, are permitted provided that the following conditions are
           14  +met:
           15  +
           16  +   * Redistributions of source code must retain the above copyright
           17  +notice, this list of conditions and the following disclaimer.
           18  +   * Redistributions in binary form must reproduce the above
           19  +copyright notice, this list of conditions and the following disclaimer
           20  +in the documentation and/or other materials provided with the
           21  +distribution.
           22  +   * Neither the name of Google Inc. nor the names of its
           23  +contributors may be used to endorse or promote products derived from
           24  +this software without specific prior written permission.
           25  +
           26  +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
           27  +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
           28  +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
           29  +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
           30  +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
           31  +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
           32  +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
           33  +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
           34  +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
           35  +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
           36  +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
           37  +```
           38  +
           39  +=== Fsnotify
           40  +; URL
           41  +: [[https://fsnotify.org/]]
           42  +; License
           43  +: BSD 3-Clause "New" or "Revised" License
           44  +; Source
           45  +: [[https://github.com/fsnotify/fsnotify]]
           46  +```
           47  +Copyright (c) 2012 The Go Authors. All rights reserved.
           48  +Copyright (c) 2012-2019 fsnotify Authors. All rights reserved.
           49  +
           50  +Redistribution and use in source and binary forms, with or without
           51  +modification, are permitted provided that the following conditions are
           52  +met:
           53  +
           54  +   * Redistributions of source code must retain the above copyright
           55  +notice, this list of conditions and the following disclaimer.
           56  +   * Redistributions in binary form must reproduce the above
           57  +copyright notice, this list of conditions and the following disclaimer
           58  +in the documentation and/or other materials provided with the
           59  +distribution.
           60  +   * Neither the name of Google Inc. nor the names of its
           61  +contributors may be used to endorse or promote products derived from
           62  +this software without specific prior written permission.
           63  +
           64  +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
           65  +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
           66  +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
           67  +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
           68  +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
           69  +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
           70  +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
           71  +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
           72  +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
           73  +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
           74  +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
           75  +```
           76  +
           77  +=== hoisie/mustache / cbroglie/mustache
           78  +; URL & Source
           79  +: [[https://github.com/hoisie/mustache]] / [[https://github.com/cbroglie/mustache]]
           80  +; License
           81  +: MIT License
           82  +; Remarks
           83  +: cbroglie/mustache is a fork from hoisie/mustache (starting with commit [f9b4cbf]).
           84  +  cbroglie/mustache does not claim a copyright and includes just the license file from hoisie/mustache.
           85  +  cbroglie/mustache obviously continues with the original license.
           86  +
           87  +```
           88  +Copyright (c) 2009 Michael Hoisie
           89  +
           90  +Permission is hereby granted, free of charge, to any person obtaining a copy
           91  +of this software and associated documentation files (the "Software"), to deal
           92  +in the Software without restriction, including without limitation the rights
           93  +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
           94  +copies of the Software, and to permit persons to whom the Software is
           95  +furnished to do so, subject to the following conditions:
           96  +
           97  +The above copyright notice and this permission notice shall be included in
           98  +all copies or substantial portions of the Software.
           99  +
          100  +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
          101  +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
          102  +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
          103  +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
          104  +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
          105  +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
          106  +THE SOFTWARE.
          107  +```
          108  +
          109  +===  pascaldekloe/jwt
          110  +; URL & Source
          111  +: [[https://github.com/pascaldekloe/jwt]]
          112  +; License
          113  +: [[CC0 1.0 Universal|https://creativecommons.org/publicdomain/zero/1.0/legalcode]]
          114  +```
          115  +To the extent possible under law, Pascal S. de Kloe has waived all
          116  +copyright and related or neighboring rights to JWT. This work is
          117  +published from The Netherlands.
          118  +
          119  +https://creativecommons.org/publicdomain/zero/1.0/legalcode
          120  +```
          121  +
          122  +=== yuin/goldmark
          123  +; URL & Source
          124  +: [[https://github.com/yuin/goldmark]]
          125  +; License
          126  +: MIT License
          127  +```
          128  +MIT License
          129  +
          130  +Copyright (c) 2019 Yusuke Inuzuka
          131  +
          132  +Permission is hereby granted, free of charge, to any person obtaining a copy
          133  +of this software and associated documentation files (the "Software"), to deal
          134  +in the Software without restriction, including without limitation the rights
          135  +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
          136  +copies of the Software, and to permit persons to whom the Software is
          137  +furnished to do so, subject to the following conditions:
          138  +
          139  +The above copyright notice and this permission notice shall be included in all
          140  +copies or substantial portions of the Software.
          141  +
          142  +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
          143  +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
          144  +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
          145  +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
          146  +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
          147  +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
          148  +SOFTWARE.
          149  +```

Added box/constbox/emoji_spin.gif.

cannot compute difference between binary files

Added box/constbox/error.mustache.

            1  +<article>
            2  +<header>
            3  +<h1>{{ErrorTitle}}</h1>
            4  +</header>
            5  +{{ErrorText}}
            6  +</article>

Added box/constbox/form.mustache.

            1  +<article>
            2  +<header>
            3  +<h1>{{Heading}}</h1>
            4  +</header>
            5  +<form method="POST">
            6  +<div>
            7  +<label for="title">Title</label>
            8  +<input class="zs-input" type="text" id="title" name="title" placeholder="Title.." value="{{MetaTitle}}" autofocus>
            9  +</div>
           10  +<div>
           11  +<div>
           12  +<label for="role">Role</label>
           13  +<input class="zs-input" type="text" id="role" name="role" placeholder="role.." value="{{MetaRole}}">
           14  +</div>
           15  +<label for="tags">Tags</label>
           16  +<input class="zs-input" type="text" id="tags" name="tags" placeholder="#tag" value="{{MetaTags}}">
           17  +</div>
           18  +<div>
           19  +<label for="meta">Metadata</label>
           20  +<textarea class="zs-input" id="meta" name="meta" rows="4" placeholder="metakey: metavalue">
           21  +{{#MetaPairsRest}}
           22  +{{Key}}: {{Value}}
           23  +{{/MetaPairsRest}}
           24  +</textarea>
           25  +</div>
           26  +<div>
           27  +<label for="syntax">Syntax</label>
           28  +<input class="zs-input" type="text" id="syntax" name="syntax" placeholder="syntax.." value="{{MetaSyntax}}">
           29  +</div>
           30  +<div>
           31  +{{#IsTextContent}}
           32  +<label for="content">Content</label>
           33  +<textarea class="zs-input zs-content" id="meta" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea>
           34  +{{/IsTextContent}}
           35  +</div>
           36  +<input class="zs-button" type="submit" value="Submit">
           37  +</form>
           38  +</article>

Added box/constbox/home.zettel.

            1  +=== Thank you for using Zettelstore!
            2  +
            3  +You will find the lastest information about Zettelstore at [[https://zettelstore.de]].
            4  +Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version.
            5  +You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading.
            6  +Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading.
            7  +Since Zettelstore is currently in a development state, every upgrade might fix some of your problems.
            8  +To check for versions, there is a zettel with the [[current version|00000000000001]] of your Zettelstore.
            9  +
           10  +If you have problems concerning Zettelstore,
           11  +do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]].
           12  +
           13  +=== Reporting errors
           14  +If you have encountered an error, please include the content of the following zettel in your mail (if possible):
           15  +* [[Zettelstore Version|00000000000001]]
           16  +* [[Zettelstore Operating System|00000000000003]]
           17  +* [[Zettelstore Startup Configuration|00000000000096]]
           18  +* [[Zettelstore Runtime Configuration|00000000000100]]
           19  +
           20  +Additionally, you have to describe, what you have done before that error occurs
           21  +and what you have expected instead.
           22  +Please do not forget to include the error message, if there is one.
           23  +
           24  +Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"".
           25  +Otherwise, only some zettel are linked.
           26  +To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]:
           27  +please set the metadata value of the key ''expert-mode'' to true.
           28  +To do you, enter the string ''expert-mode:true'' inside the edit view of the metadata.
           29  +
           30  +=== Information about this zettel
           31  +This zettel is your home zettel.
           32  +It is part of the Zettelstore software itself.
           33  +Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel.
           34  +
           35  +You can change the content of this zettel by clicking on ""Edit"" above.
           36  +This allows you to customize your home zettel.
           37  +
           38  +Alternatively, you can designate another zettel as your home zettel.
           39  +Edit the [[Zettelstore Runtime Configuration|00000000000100]] by adding the metadata key ''home-zettel''.
           40  +Its value is the identifier of the zettel that should act as the new home zettel.
           41  +You will find the identifier of each zettel between their ""Edit"" and the ""Info"" link above.
           42  +The identifier of this zettel is ''00010000000000''.
           43  +If you provide a wrong identifier, this zettel will be shown as the home zettel.
           44  +Take a look inside the manual for further details.

Added box/constbox/info.mustache.

            1  +<article>
            2  +<header>
            3  +<h1>Information for Zettel {{Zid}}</h1>
            4  +<a href="{{{WebURL}}}">Web</a>
            5  +&#183; <a href="{{{ContextURL}}}">Context</a>
            6  +{{#CanWrite}} &#183; <a href="{{{EditURL}}}">Edit</a>{{/CanWrite}}
            7  +{{#CanFolge}} &#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
            8  +{{#CanCopy}} &#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
            9  +{{#CanRename}}&#183; <a href="{{{RenameURL}}}">Rename</a>{{/CanRename}}
           10  +{{#CanDelete}}&#183; <a href="{{{DeleteURL}}}">Delete</a>{{/CanDelete}}
           11  +</header>
           12  +<h2>Interpreted Metadata</h2>
           13  +<table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table>
           14  +{{#HasLinks}}
           15  +<h2>References</h2>
           16  +{{#HasLocLinks}}
           17  +<h3>Local</h3>
           18  +<ul>
           19  +{{#LocLinks}}
           20  +{{#Valid}}<li><a href="{{{Zid}}}">{{Zid}}</a></li>{{/Valid}}
           21  +{{^Valid}}<li>{{Zid}}</li>{{/Valid}}
           22  +{{/LocLinks}}
           23  +</ul>
           24  +{{/HasLocLinks}}
           25  +{{#HasExtLinks}}
           26  +<h3>External</h3>
           27  +<ul>
           28  +{{#ExtLinks}}
           29  +<li><a href="{{{.}}}"{{{ExtNewWindow}}}>{{.}}</a></li>
           30  +{{/ExtLinks}}
           31  +</ul>
           32  +{{/HasExtLinks}}
           33  +{{/HasLinks}}
           34  +<h2>Parts and format</h3>
           35  +<table>
           36  +{{#Matrix}}
           37  +<tr>
           38  +{{#Elements}}{{#HasURL}}<td><a href="{{{URL}}}">{{Text}}</td>{{/HasURL}}{{^HasURL}}<th>{{Text}}</th>{{/HasURL}}
           39  +{{/Elements}}
           40  +</tr>
           41  +{{/Matrix}}
           42  +</table>
           43  +{{#HasShadowLinks}}
           44  +<h2>Shadowed Boxes</h2>
           45  +<ul>{{#ShadowLinks}}<li>{{.}}</li>{{/ShadowLinks}}</ul>
           46  +{{/HasShadowLinks}}
           47  +{{#Endnotes}}{{{Endnotes}}}{{/Endnotes}}
           48  +</article>

Added box/constbox/license.txt.

            1  +Copyright (c) 2020-2021 Detlef Stern
            2  +
            3  +                          Licensed under the EUPL
            4  +
            5  +Zettelstore is licensed under the European Union Public License, version 1.2 or
            6  +later (EUPL v. 1.2). The license is available in the official languages of the
            7  +EU. The English version is included here. Please see
            8  +https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official
            9  +translations of the other languages.
           10  +
           11  +
           12  +-------------------------------------------------------------------------------
           13  +
           14  +
           15  +EUROPEAN UNION PUBLIC LICENCE v. 1.2
           16  +EUPL © the European Union 2007, 2016
           17  +
           18  +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
           19  +below) which is provided under the terms of this Licence. Any use of the Work,
           20  +other than as authorised under this Licence is prohibited (to the extent such
           21  +use is covered by a right of the copyright holder of the Work).
           22  +
           23  +The Work is provided under the terms of this Licence when the Licensor (as
           24  +defined below) has placed the following notice immediately following the
           25  +copyright notice for the Work:
           26  +
           27  +                          Licensed under the EUPL
           28  +
           29  +or has expressed by any other means his willingness to license under the EUPL.
           30  +
           31  +1. Definitions
           32  +
           33  +In this Licence, the following terms have the following meaning:
           34  +
           35  +— ‘The Licence’: this Licence.
           36  +— ‘The Original Work’: the work or software distributed or communicated by the
           37  +  Licensor under this Licence, available as Source Code and also as Executable
           38  +  Code as the case may be.
           39  +— ‘Derivative Works’: the works or software that could be created by the
           40  +  Licensee, based upon the Original Work or modifications thereof. This Licence
           41  +  does not define the extent of modification or dependence on the Original Work
           42  +  required in order to classify a work as a Derivative Work; this extent is
           43  +  determined by copyright law applicable in the country mentioned in Article
           44  +  15.
           45  +— ‘The Work’: the Original Work or its Derivative Works.
           46  +— ‘The Source Code’: the human-readable form of the Work which is the most
           47  +  convenient for people to study and modify.
           48  +— ‘The Executable Code’: any code which has generally been compiled and which
           49  +  is meant to be interpreted by a computer as a program.
           50  +— ‘The Licensor’: the natural or legal person that distributes or communicates
           51  +  the Work under the Licence.
           52  +— ‘Contributor(s)’: any natural or legal person who modifies the Work under the
           53  +  Licence, or otherwise contributes to the creation of a Derivative Work.
           54  +— ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
           55  +  the Work under the terms of the Licence.
           56  +— ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
           57  +  renting, distributing, communicating, transmitting, or otherwise making
           58  +  available, online or offline, copies of the Work or providing access to its
           59  +  essential functionalities at the disposal of any other natural or legal
           60  +  person.
           61  +
           62  +2. Scope of the rights granted by the Licence
           63  +
           64  +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
           65  +sublicensable licence to do the following, for the duration of copyright vested
           66  +in the Original Work:
           67  +
           68  +— use the Work in any circumstance and for all usage,
           69  +— reproduce the Work,
           70  +— modify the Work, and make Derivative Works based upon the Work,
           71  +— communicate to the public, including the right to make available or display
           72  +  the Work or copies thereof to the public and perform publicly, as the case
           73  +  may be, the Work,
           74  +— distribute the Work or copies thereof,
           75  +— lend and rent the Work or copies thereof,
           76  +— sublicense rights in the Work or copies thereof.
           77  +
           78  +Those rights can be exercised on any media, supports and formats, whether now
           79  +known or later invented, as far as the applicable law permits so.
           80  +
           81  +In the countries where moral rights apply, the Licensor waives his right to
           82  +exercise his moral right to the extent allowed by law in order to make
           83  +effective the licence of the economic rights here above listed.
           84  +
           85  +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
           86  +any patents held by the Licensor, to the extent necessary to make use of the
           87  +rights granted on the Work under this Licence.
           88  +
           89  +3. Communication of the Source Code
           90  +
           91  +The Licensor may provide the Work either in its Source Code form, or as
           92  +Executable Code. If the Work is provided as Executable Code, the Licensor
           93  +provides in addition a machine-readable copy of the Source Code of the Work
           94  +along with each copy of the Work that the Licensor distributes or indicates, in
           95  +a notice following the copyright notice attached to the Work, a repository
           96  +where the Source Code is easily and freely accessible for as long as the
           97  +Licensor continues to distribute or communicate the Work.
           98  +
           99  +4. Limitations on copyright
          100  +
          101  +Nothing in this Licence is intended to deprive the Licensee of the benefits
          102  +from any exception or limitation to the exclusive rights of the rights owners
          103  +in the Work, of the exhaustion of those rights or of other applicable
          104  +limitations thereto.
          105  +
          106  +5. Obligations of the Licensee
          107  +
          108  +The grant of the rights mentioned above is subject to some restrictions and
          109  +obligations imposed on the Licensee. Those obligations are the following:
          110  +
          111  +Attribution right: The Licensee shall keep intact all copyright, patent or
          112  +trademarks notices and all notices that refer to the Licence and to the
          113  +disclaimer of warranties. The Licensee must include a copy of such notices and
          114  +a copy of the Licence with every copy of the Work he/she distributes or
          115  +communicates. The Licensee must cause any Derivative Work to carry prominent
          116  +notices stating that the Work has been modified and the date of modification.
          117  +
          118  +Copyleft clause: If the Licensee distributes or communicates copies of the
          119  +Original Works or Derivative Works, this Distribution or Communication will be
          120  +done under the terms of this Licence or of a later version of this Licence
          121  +unless the Original Work is expressly distributed only under this version of
          122  +the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
          123  +(becoming Licensor) cannot offer or impose any additional terms or conditions
          124  +on the Work or Derivative Work that alter or restrict the terms of the Licence.
          125  +
          126  +Compatibility clause: If the Licensee Distributes or Communicates Derivative
          127  +Works or copies thereof based upon both the Work and another work licensed
          128  +under a Compatible Licence, this Distribution or Communication can be done
          129  +under the terms of this Compatible Licence. For the sake of this clause,
          130  +‘Compatible Licence’ refers to the licences listed in the appendix attached to
          131  +this Licence. Should the Licensee's obligations under the Compatible Licence
          132  +conflict with his/her obligations under this Licence, the obligations of the
          133  +Compatible Licence shall prevail.
          134  +
          135  +Provision of Source Code: When distributing or communicating copies of the
          136  +Work, the Licensee will provide a machine-readable copy of the Source Code or
          137  +indicate a repository where this Source will be easily and freely available for
          138  +as long as the Licensee continues to distribute or communicate the Work.
          139  +
          140  +Legal Protection: This Licence does not grant permission to use the trade
          141  +names, trademarks, service marks, or names of the Licensor, except as required
          142  +for reasonable and customary use in describing the origin of the Work and
          143  +reproducing the content of the copyright notice.
          144  +
          145  +6. Chain of Authorship
          146  +
          147  +The original Licensor warrants that the copyright in the Original Work granted
          148  +hereunder is owned by him/her or licensed to him/her and that he/she has the
          149  +power and authority to grant the Licence.
          150  +
          151  +Each Contributor warrants that the copyright in the modifications he/she brings
          152  +to the Work are owned by him/her or licensed to him/her and that he/she has the
          153  +power and authority to grant the Licence.
          154  +
          155  +Each time You accept the Licence, the original Licensor and subsequent
          156  +Contributors grant You a licence to their contributions to the Work, under the
          157  +terms of this Licence.
          158  +
          159  +7. Disclaimer of Warranty
          160  +
          161  +The Work is a work in progress, which is continuously improved by numerous
          162  +Contributors. It is not a finished work and may therefore contain defects or
          163  +‘bugs’ inherent to this type of development.
          164  +
          165  +For the above reason, the Work is provided under the Licence on an ‘as is’
          166  +basis and without warranties of any kind concerning the Work, including without
          167  +limitation merchantability, fitness for a particular purpose, absence of
          168  +defects or errors, accuracy, non-infringement of intellectual property rights
          169  +other than copyright as stated in Article 6 of this Licence.
          170  +
          171  +This disclaimer of warranty is an essential part of the Licence and a condition
          172  +for the grant of any rights to the Work.
          173  +
          174  +8. Disclaimer of Liability
          175  +
          176  +Except in the cases of wilful misconduct or damages directly caused to natural
          177  +persons, the Licensor will in no event be liable for any direct or indirect,
          178  +material or moral, damages of any kind, arising out of the Licence or of the
          179  +use of the Work, including without limitation, damages for loss of goodwill,
          180  +work stoppage, computer failure or malfunction, loss of data or any commercial
          181  +damage, even if the Licensor has been advised of the possibility of such
          182  +damage. However, the Licensor will be liable under statutory product liability
          183  +laws as far such laws apply to the Work.
          184  +
          185  +9. Additional agreements
          186  +
          187  +While distributing the Work, You may choose to conclude an additional
          188  +agreement, defining obligations or services consistent with this Licence.
          189  +However, if accepting obligations, You may act only on your own behalf and on
          190  +your sole responsibility, not on behalf of the original Licensor or any other
          191  +Contributor, and only if You agree to indemnify, defend, and hold each
          192  +Contributor harmless for any liability incurred by, or claims asserted against
          193  +such Contributor by the fact You have accepted any warranty or additional
          194  +liability.
          195  +
          196  +10. Acceptance of the Licence
          197  +
          198  +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
          199  +placed under the bottom of a window displaying the text of this Licence or by
          200  +affirming consent in any other similar way, in accordance with the rules of
          201  +applicable law. Clicking on that icon indicates your clear and irrevocable
          202  +acceptance of this Licence and all of its terms and conditions.
          203  +
          204  +Similarly, you irrevocably accept this Licence and all of its terms and
          205  +conditions by exercising any rights granted to You by Article 2 of this
          206  +Licence, such as the use of the Work, the creation by You of a Derivative Work
          207  +or the Distribution or Communication by You of the Work or copies thereof.
          208  +
          209  +11. Information to the public
          210  +
          211  +In case of any Distribution or Communication of the Work by means of electronic
          212  +communication by You (for example, by offering to download the Work from
          213  +a remote location) the distribution channel or media (for example, a website)
          214  +must at least provide to the public the information requested by the applicable
          215  +law regarding the Licensor, the Licence and the way it may be accessible,
          216  +concluded, stored and reproduced by the Licensee.
          217  +
          218  +12. Termination of the Licence
          219  +
          220  +The Licence and the rights granted hereunder will terminate automatically upon
          221  +any breach by the Licensee of the terms of the Licence.
          222  +
          223  +Such a termination will not terminate the licences of any person who has
          224  +received the Work from the Licensee under the Licence, provided such persons
          225  +remain in full compliance with the Licence.
          226  +
          227  +13. Miscellaneous
          228  +
          229  +Without prejudice of Article 9 above, the Licence represents the complete
          230  +agreement between the Parties as to the Work.
          231  +
          232  +If any provision of the Licence is invalid or unenforceable under applicable
          233  +law, this will not affect the validity or enforceability of the Licence as
          234  +a whole. Such provision will be construed or reformed so as necessary to make
          235  +it valid and enforceable.
          236  +
          237  +The European Commission may publish other linguistic versions or new versions
          238  +of this Licence or updated versions of the Appendix, so far this is required
          239  +and reasonable, without reducing the scope of the rights granted by the
          240  +Licence. New versions of the Licence will be published with a unique version
          241  +number.
          242  +
          243  +All linguistic versions of this Licence, approved by the European Commission,
          244  +have identical value. Parties can take advantage of the linguistic version of
          245  +their choice.
          246  +
          247  +14. Jurisdiction
          248  +
          249  +Without prejudice to specific agreement between parties,
          250  +
          251  +— any litigation resulting from the interpretation of this License, arising
          252  +  between the European Union institutions, bodies, offices or agencies, as
          253  +  a Licensor, and any Licensee, will be subject to the jurisdiction of the
          254  +  Court of Justice of the European Union, as laid down in article 272 of the
          255  +  Treaty on the Functioning of the European Union,
          256  +— any litigation arising between other parties and resulting from the
          257  +  interpretation of this License, will be subject to the exclusive jurisdiction
          258  +  of the competent court where the Licensor resides or conducts its primary
          259  +  business.
          260  +
          261  +15. Applicable Law
          262  +
          263  +Without prejudice to specific agreement between parties,
          264  +
          265  +— this Licence shall be governed by the law of the European Union Member State
          266  +  where the Licensor has his seat, resides or has his registered office,
          267  +— this licence shall be governed by Belgian law if the Licensor has no seat,
          268  +  residence or registered office inside a European Union Member State.
          269  +
          270  +
          271  +                                  Appendix
          272  +
          273  +
          274  +‘Compatible Licences’ according to Article 5 EUPL are:
          275  +
          276  +— GNU General Public License (GPL) v. 2, v. 3
          277  +— GNU Affero General Public License (AGPL) v. 3
          278  +— Open Software License (OSL) v. 2.1, v. 3.0
          279  +— Eclipse Public License (EPL) v. 1.0
          280  +— CeCILL v. 2.0, v. 2.1
          281  +— Mozilla Public Licence (MPL) v. 2
          282  +— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
          283  +— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
          284  +  works other than software
          285  +— European Union Public Licence (EUPL) v. 1.1, v. 1.2
          286  +— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
          287  +  Reciprocity (LiLiQ-R+)
          288  +
          289  +The European Commission may update this Appendix to later versions of the above
          290  +licences without producing a new version of the EUPL, as long as they provide
          291  +the rights granted in Article 2 of this Licence and protect the covered Source
          292  +Code from exclusive appropriation.
          293  +
          294  +All other changes or additions to this Appendix require the production of a new
          295  +EUPL version.

Added box/constbox/listroles.mustache.

            1  +<nav>
            2  +<header>
            3  +<h1>Currently used roles</h1>
            4  +</header>
            5  +<ul>
            6  +{{#Roles}}<li><a href="{{{URL}}}">{{Text}}</a></li>
            7  +{{/Roles}}</ul>
            8  +</nav>

Added box/constbox/listtags.mustache.

            1  +<nav>
            2  +<header>
            3  +<h1>Currently used tags</h1>
            4  +<div class="zs-meta">
            5  +<a href="{{{ListTagsURL}}}">All</a>{{#MinCounts}}, <a href="{{{URL}}}">{{Count}}</a>{{/MinCounts}}
            6  +</div>
            7  +</header>
            8  +{{#Tags}} <a href="{{{URL}}}" style="font-size:{{Size}}%">{{Name}}</a><sup>{{Count}}</sup>
            9  +{{/Tags}}
           10  +</nav>

Added box/constbox/listzettel.mustache.

            1  +<header>
            2  +<h1>{{Title}}</h1>
            3  +</header>
            4  +<ul>
            5  +{{#Metas}}<li><a href="{{{URL}}}">{{{Text}}}</a></li>
            6  +{{/Metas}}</ul>

Added box/constbox/login.mustache.

            1  +<article>
            2  +<header>
            3  +<h1>{{Title}}</h1>
            4  +</header>
            5  +{{#Retry}}
            6  +<div class="zs-indication zs-error">Wrong user name / password. Try again.</div>
            7  +{{/Retry}}
            8  +<form method="POST" action="?_format=html">
            9  +<div>
           10  +<label for="username">User name</label>
           11  +<input class="zs-input" type="text" id="username" name="username" placeholder="Your user name.." autofocus>
           12  +</div>
           13  +<div>
           14  +<label for="password">Password</label>
           15  +<input class="zs-input" type="password" id="password" name="password" placeholder="Your password..">
           16  +</div>
           17  +<input class="zs-button" type="submit" value="Login">
           18  +</form>
           19  +</article>

Added box/constbox/newtoc.zettel.

            1  +This zettel lists all zettel that should act as a template for new zettel.
            2  +These zettel will be included in the ""New"" menu of the WebUI.
            3  +* [[New Zettel|00000000090001]]
            4  +* [[New User|00000000090002]]

Added box/constbox/rename.mustache.

            1  +<article>
            2  +<header>
            3  +<h1>Rename Zettel {{.Zid}}</h1>
            4  +</header>
            5  +<p>Do you really want to rename this zettel?</p>
            6  +<form method="POST">
            7  +<div>
            8  +<label for="newid">New zettel id</label>
            9  +<input class="zs-input" type="text" id="newzid" name="newzid" placeholder="ZID.." value="{{Zid}}" autofocus>
           10  +</div>
           11  +<input type="hidden" id="curzid" name="curzid" value="{{Zid}}">
           12  +<input class="zs-button" type="submit" value="Rename">
           13  +</form>
           14  +<dl>
           15  +{{#MetaPairs}}
           16  +<dt>{{Key}}:</dt><dd>{{Value}}</dd>
           17  +{{/MetaPairs}}
           18  +</dl>
           19  +</article>

Added box/constbox/zettel.mustache.

            1  +<article>
            2  +<header>
            3  +<h1>{{{HTMLTitle}}}</h1>
            4  +<div class="zs-meta">
            5  +{{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> &#183;{{/CanWrite}}
            6  +{{Zid}} &#183;
            7  +<a href="{{{InfoURL}}}">Info</a> &#183;
            8  +(<a href="{{{RoleURL}}}">{{RoleText}}</a>)
            9  +{{#HasTags}}&#183; {{#Tags}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags}}{{/HasTags}}
           10  +{{#CanCopy}}&#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
           11  +{{#CanFolge}}&#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
           12  +{{#FolgeRefs}}<br>Folge: {{{FolgeRefs}}}{{/FolgeRefs}}
           13  +{{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}}
           14  +{{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}}
           15  +</div>
           16  +</header>
           17  +{{{Content}}}
           18  +{{#HasBackLinks}}
           19  +<details>
           20  +<summary>Additional links to this zettel</summary>
           21  +<ul>
           22  +{{#BackLinks}}
           23  +<li><a href="{{{URL}}}">{{Text}}</a></li>
           24  +{{/BackLinks}}
           25  +</ul>
           26  +</details>
           27  +{{/HasBackLinks}}
           28  +</article>

Added box/dirbox/dirbox.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package dirbox provides a directory-based zettel box.
           12  +package dirbox
           13  +
           14  +import (
           15  +	"context"
           16  +	"errors"
           17  +	"net/url"
           18  +	"os"
           19  +	"path/filepath"
           20  +	"strconv"
           21  +	"strings"
           22  +	"sync"
           23  +	"time"
           24  +
           25  +	"zettelstore.de/z/box"
           26  +	"zettelstore.de/z/box/dirbox/directory"
           27  +	"zettelstore.de/z/box/filebox"
           28  +	"zettelstore.de/z/box/manager"
           29  +	"zettelstore.de/z/domain"
           30  +	"zettelstore.de/z/domain/id"
           31  +	"zettelstore.de/z/domain/meta"
           32  +	"zettelstore.de/z/search"
           33  +)
           34  +
           35  +func init() {
           36  +	manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
           37  +		path := getDirPath(u)
           38  +		if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
           39  +			return nil, err
           40  +		}
           41  +		dirSrvSpec, defWorker, maxWorker := getDirSrvInfo(u.Query().Get("type"))
           42  +		dp := dirBox{
           43  +			number:     cdata.Number,
           44  +			location:   u.String(),
           45  +			readonly:   getQueryBool(u, "readonly"),
           46  +			cdata:      *cdata,
           47  +			dir:        path,
           48  +			dirRescan:  time.Duration(getQueryInt(u, "rescan", 60, 3600, 30*24*60*60)) * time.Second,
           49  +			dirSrvSpec: dirSrvSpec,
           50  +			fSrvs:      uint32(getQueryInt(u, "worker", 1, defWorker, maxWorker)),
           51  +		}
           52  +		return &dp, nil
           53  +	})
           54  +}
           55  +
           56  +type directoryServiceSpec int
           57  +
           58  +const (
           59  +	_ directoryServiceSpec = iota
           60  +	dirSrvAny
           61  +	dirSrvSimple
           62  +	dirSrvNotify
           63  +)
           64  +
           65  +func getDirPath(u *url.URL) string {
           66  +	if u.Opaque != "" {
           67  +		return filepath.Clean(u.Opaque)
           68  +	}
           69  +	return filepath.Clean(u.Path)
           70  +}
           71  +
           72  +func getQueryBool(u *url.URL, key string) bool {
           73  +	_, ok := u.Query()[key]
           74  +	return ok
           75  +}
           76  +
           77  +func getQueryInt(u *url.URL, key string, min, def, max int) int {
           78  +	sVal := u.Query().Get(key)
           79  +	if sVal == "" {
           80  +		return def
           81  +	}
           82  +	iVal, err := strconv.Atoi(sVal)
           83  +	if err != nil {
           84  +		return def
           85  +	}
           86  +	if iVal < min {
           87  +		return min
           88  +	}
           89  +	if iVal > max {
           90  +		return max
           91  +	}
           92  +	return iVal
           93  +}
           94  +
           95  +// dirBox uses a directory to store zettel as files.
           96  +type dirBox struct {
           97  +	number     int
           98  +	location   string
           99  +	readonly   bool
          100  +	cdata      manager.ConnectData
          101  +	dir        string
          102  +	dirRescan  time.Duration
          103  +	dirSrvSpec directoryServiceSpec
          104  +	dirSrv     directory.Service
          105  +	mustNotify bool
          106  +	fSrvs      uint32
          107  +	fCmds      []chan fileCmd
          108  +	mxCmds     sync.RWMutex
          109  +}
          110  +
          111  +func (dp *dirBox) Location() string {
          112  +	return dp.location
          113  +}
          114  +
          115  +func (dp *dirBox) Start(ctx context.Context) error {
          116  +	dp.mxCmds.Lock()
          117  +	dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs)
          118  +	for i := uint32(0); i < dp.fSrvs; i++ {
          119  +		cc := make(chan fileCmd)
          120  +		go fileService(i, cc)
          121  +		dp.fCmds = append(dp.fCmds, cc)
          122  +	}
          123  +	dp.setupDirService()
          124  +	dp.mxCmds.Unlock()
          125  +	if dp.dirSrv == nil {
          126  +		panic("No directory service")
          127  +	}
          128  +	return dp.dirSrv.Start()
          129  +}
          130  +
          131  +func (dp *dirBox) Stop(ctx context.Context) error {
          132  +	dirSrv := dp.dirSrv
          133  +	dp.dirSrv = nil
          134  +	err := dirSrv.Stop()
          135  +	for _, c := range dp.fCmds {
          136  +		close(c)
          137  +	}
          138  +	return err
          139  +}
          140  +
          141  +func (dp *dirBox) notifyChanged(reason box.UpdateReason, zid id.Zid) {
          142  +	if dp.mustNotify {
          143  +		if chci := dp.cdata.Notify; chci != nil {
          144  +			chci <- box.UpdateInfo{Reason: reason, Zid: zid}
          145  +		}
          146  +	}
          147  +}
          148  +
          149  +func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd {
          150  +	// Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
          151  +	sum := 2166136261 ^ uint32(zid)
          152  +	sum *= 16777619
          153  +	sum ^= uint32(zid >> 32)
          154  +	sum *= 16777619
          155  +
          156  +	dp.mxCmds.RLock()
          157  +	defer dp.mxCmds.RUnlock()
          158  +	return dp.fCmds[sum%dp.fSrvs]
          159  +}
          160  +
          161  +func (dp *dirBox) CanCreateZettel(ctx context.Context) bool {
          162  +	return !dp.readonly
          163  +}
          164  +
          165  +func (dp *dirBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
          166  +	if dp.readonly {
          167  +		return id.Invalid, box.ErrReadOnly
          168  +	}
          169  +
          170  +	entry, err := dp.dirSrv.GetNew()
          171  +	if err != nil {
          172  +		return id.Invalid, err
          173  +	}
          174  +	meta := zettel.Meta
          175  +	meta.Zid = entry.Zid
          176  +	dp.updateEntryFromMeta(entry, meta)
          177  +
          178  +	err = setZettel(dp, entry, zettel)
          179  +	if err == nil {
          180  +		dp.dirSrv.UpdateEntry(entry)
          181  +	}
          182  +	dp.notifyChanged(box.OnUpdate, meta.Zid)
          183  +	return meta.Zid, err
          184  +}
          185  +
          186  +func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
          187  +	entry, err := dp.dirSrv.GetEntry(zid)
          188  +	if err != nil || !entry.IsValid() {
          189  +		return domain.Zettel{}, box.ErrNotFound
          190  +	}
          191  +	m, c, err := getMetaContent(dp, entry, zid)
          192  +	if err != nil {
          193  +		return domain.Zettel{}, err
          194  +	}
          195  +	dp.cleanupMeta(ctx, m)
          196  +	zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)}
          197  +	return zettel, nil
          198  +}
          199  +
          200  +func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
          201  +	entry, err := dp.dirSrv.GetEntry(zid)
          202  +	if err != nil || !entry.IsValid() {
          203  +		return nil, box.ErrNotFound
          204  +	}
          205  +	m, err := getMeta(dp, entry, zid)
          206  +	if err != nil {
          207  +		return nil, err
          208  +	}
          209  +	dp.cleanupMeta(ctx, m)
          210  +	return m, nil
          211  +}
          212  +
          213  +func (dp *dirBox) FetchZids(ctx context.Context) (id.Set, error) {
          214  +	entries, err := dp.dirSrv.GetEntries()
          215  +	if err != nil {
          216  +		return nil, err
          217  +	}
          218  +	result := id.NewSetCap(len(entries))
          219  +	for _, entry := range entries {
          220  +		result[entry.Zid] = true
          221  +	}
          222  +	return result, nil
          223  +}
          224  +
          225  +func (dp *dirBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) {
          226  +	entries, err := dp.dirSrv.GetEntries()
          227  +	if err != nil {
          228  +		return nil, err
          229  +	}
          230  +	res = make([]*meta.Meta, 0, len(entries))
          231  +	// The following loop could be parallelized if needed for performance.
          232  +	for _, entry := range entries {
          233  +		m, err1 := getMeta(dp, entry, entry.Zid)
          234  +		err = err1
          235  +		if err != nil {
          236  +			continue
          237  +		}
          238  +		dp.cleanupMeta(ctx, m)
          239  +		dp.cdata.Enricher.Enrich(ctx, m, dp.number)
          240  +
          241  +		if match(m) {
          242  +			res = append(res, m)
          243  +		}
          244  +	}
          245  +	if err != nil {
          246  +		return nil, err
          247  +	}
          248  +	return res, nil
          249  +}
          250  +
          251  +func (dp *dirBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
          252  +	return !dp.readonly
          253  +}
          254  +
          255  +func (dp *dirBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
          256  +	if dp.readonly {
          257  +		return box.ErrReadOnly
          258  +	}
          259  +
          260  +	meta := zettel.Meta
          261  +	if !meta.Zid.IsValid() {
          262  +		return &box.ErrInvalidID{Zid: meta.Zid}
          263  +	}
          264  +	entry, err := dp.dirSrv.GetEntry(meta.Zid)
          265  +	if err != nil {
          266  +		return err
          267  +	}
          268  +	if !entry.IsValid() {
          269  +		// Existing zettel, but new in this box.
          270  +		entry = &directory.Entry{Zid: meta.Zid}
          271  +		dp.updateEntryFromMeta(entry, meta)
          272  +	} else if entry.MetaSpec == directory.MetaSpecNone {
          273  +		defaultMeta := filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt)
          274  +		if !meta.Equal(defaultMeta, true) {
          275  +			dp.updateEntryFromMeta(entry, meta)
          276  +			dp.dirSrv.UpdateEntry(entry)
          277  +		}
          278  +	}
          279  +	err = setZettel(dp, entry, zettel)
          280  +	if err == nil {
          281  +		dp.notifyChanged(box.OnUpdate, meta.Zid)
          282  +	}
          283  +	return err
          284  +}
          285  +
          286  +func (dp *dirBox) updateEntryFromMeta(entry *directory.Entry, meta *meta.Meta) {
          287  +	entry.MetaSpec, entry.ContentExt = dp.calcSpecExt(meta)
          288  +	basePath := dp.calcBasePath(entry)
          289  +	if entry.MetaSpec == directory.MetaSpecFile {
          290  +		entry.MetaPath = basePath + ".meta"
          291  +	}
          292  +	entry.ContentPath = basePath + "." + entry.ContentExt
          293  +	entry.Duplicates = false
          294  +}
          295  +
          296  +func (dp *dirBox) calcBasePath(entry *directory.Entry) string {
          297  +	p := entry.ContentPath
          298  +	if p == "" {
          299  +		return filepath.Join(dp.dir, entry.Zid.String())
          300  +	}
          301  +	// ContentPath w/o the file extension
          302  +	return p[0 : len(p)-len(filepath.Ext(p))]
          303  +}
          304  +
          305  +func (dp *dirBox) calcSpecExt(m *meta.Meta) (directory.MetaSpec, string) {
          306  +	if m.YamlSep {
          307  +		return directory.MetaSpecHeader, "zettel"
          308  +	}
          309  +	syntax := m.GetDefault(meta.KeySyntax, "bin")
          310  +	switch syntax {
          311  +	case meta.ValueSyntaxNone, meta.ValueSyntaxZmk:
          312  +		return directory.MetaSpecHeader, "zettel"
          313  +	}
          314  +	for _, s := range dp.cdata.Config.GetZettelFileSyntax() {
          315  +		if s == syntax {
          316  +			return directory.MetaSpecHeader, "zettel"
          317  +		}
          318  +	}
          319  +	return directory.MetaSpecFile, syntax
          320  +}
          321  +
          322  +func (dp *dirBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
          323  +	return !dp.readonly
          324  +}
          325  +
          326  +func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
          327  +	if curZid == newZid {
          328  +		return nil
          329  +	}
          330  +	curEntry, err := dp.dirSrv.GetEntry(curZid)
          331  +	if err != nil || !curEntry.IsValid() {
          332  +		return box.ErrNotFound
          333  +	}
          334  +	if dp.readonly {
          335  +		return box.ErrReadOnly
          336  +	}
          337  +
          338  +	// Check whether zettel with new ID already exists in this box.
          339  +	if _, err = dp.GetMeta(ctx, newZid); err == nil {
          340  +		return &box.ErrInvalidID{Zid: newZid}
          341  +	}
          342  +
          343  +	oldMeta, oldContent, err := getMetaContent(dp, curEntry, curZid)
          344  +	if err != nil {
          345  +		return err
          346  +	}
          347  +
          348  +	newEntry := directory.Entry{
          349  +		Zid:         newZid,
          350  +		MetaSpec:    curEntry.MetaSpec,
          351  +		MetaPath:    renamePath(curEntry.MetaPath, curZid, newZid),
          352  +		ContentPath: renamePath(curEntry.ContentPath, curZid, newZid),
          353  +		ContentExt:  curEntry.ContentExt,
          354  +	}
          355  +
          356  +	if err = dp.dirSrv.RenameEntry(curEntry, &newEntry); err != nil {
          357  +		return err
          358  +	}
          359  +	oldMeta.Zid = newZid
          360  +	newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)}
          361  +	if err = setZettel(dp, &newEntry, newZettel); err != nil {
          362  +		// "Rollback" rename. No error checking...
          363  +		dp.dirSrv.RenameEntry(&newEntry, curEntry)
          364  +		return err
          365  +	}
          366  +	err = deleteZettel(dp, curEntry, curZid)
          367  +	if err == nil {
          368  +		dp.notifyChanged(box.OnDelete, curZid)
          369  +		dp.notifyChanged(box.OnUpdate, newZid)
          370  +	}
          371  +	return err
          372  +}
          373  +
          374  +func (dp *dirBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
          375  +	if dp.readonly {
          376  +		return false
          377  +	}
          378  +	entry, err := dp.dirSrv.GetEntry(zid)
          379  +	return err == nil && entry.IsValid()
          380  +}
          381  +
          382  +func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
          383  +	if dp.readonly {
          384  +		return box.ErrReadOnly
          385  +	}
          386  +
          387  +	entry, err := dp.dirSrv.GetEntry(zid)
          388  +	if err != nil || !entry.IsValid() {
          389  +		return box.ErrNotFound
          390  +	}
          391  +	dp.dirSrv.DeleteEntry(zid)
          392  +	err = deleteZettel(dp, entry, zid)
          393  +	if err == nil {
          394  +		dp.notifyChanged(box.OnDelete, zid)
          395  +	}
          396  +	return err
          397  +}
          398  +
          399  +func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) {
          400  +	st.ReadOnly = dp.readonly
          401  +	st.Zettel, _ = dp.dirSrv.NumEntries()
          402  +}
          403  +
          404  +func (dp *dirBox) cleanupMeta(ctx context.Context, m *meta.Meta) {
          405  +	if role, ok := m.Get(meta.KeyRole); !ok || role == "" {
          406  +		m.Set(meta.KeyRole, dp.cdata.Config.GetDefaultRole())
          407  +	}
          408  +	if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
          409  +		m.Set(meta.KeySyntax, dp.cdata.Config.GetDefaultSyntax())
          410  +	}
          411  +}
          412  +
          413  +func renamePath(path string, curID, newID id.Zid) string {
          414  +	dir, file := filepath.Split(path)
          415  +	if cur := curID.String(); strings.HasPrefix(file, cur) {
          416  +		file = newID.String() + file[len(cur):]
          417  +		return filepath.Join(dir, file)
          418  +	}
          419  +	return path
          420  +}

Added box/dirbox/directory/directory.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package directory manages the directory interface of a dirstore.
           12  +package directory
           13  +
           14  +import "zettelstore.de/z/domain/id"
           15  +
           16  +// Service is the interface of a directory service.
           17  +type Service interface {
           18  +	Start() error
           19  +	Stop() error
           20  +	NumEntries() (int, error)
           21  +	GetEntries() ([]*Entry, error)
           22  +	GetEntry(zid id.Zid) (*Entry, error)
           23  +	GetNew() (*Entry, error)
           24  +	UpdateEntry(entry *Entry) error
           25  +	RenameEntry(curEntry, newEntry *Entry) error
           26  +	DeleteEntry(zid id.Zid) error
           27  +}
           28  +
           29  +// MetaSpec defines all possibilities where meta data can be stored.
           30  +type MetaSpec int
           31  +
           32  +// Constants for MetaSpec
           33  +const (
           34  +	_              MetaSpec = iota
           35  +	MetaSpecNone            // no meta information
           36  +	MetaSpecFile            // meta information is in meta file
           37  +	MetaSpecHeader          // meta information is in header
           38  +)
           39  +
           40  +// Entry stores everything for a directory entry.
           41  +type Entry struct {
           42  +	Zid         id.Zid
           43  +	MetaSpec    MetaSpec // location of meta information
           44  +	MetaPath    string   // file path of meta information
           45  +	ContentPath string   // file path of zettel content
           46  +	ContentExt  string   // (normalized) file extension of zettel content
           47  +	Duplicates  bool     // multiple content files
           48  +}
           49  +
           50  +// IsValid checks whether the entry is valid.
           51  +func (e *Entry) IsValid() bool {
           52  +	return e != nil && e.Zid.IsValid()
           53  +}

Added box/dirbox/makedir.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package dirbox provides a directory-based zettel box.
           12  +package dirbox
           13  +
           14  +import (
           15  +	"zettelstore.de/z/box/dirbox/notifydir"
           16  +	"zettelstore.de/z/box/dirbox/simpledir"
           17  +	"zettelstore.de/z/kernel"
           18  +)
           19  +
           20  +func getDirSrvInfo(dirType string) (directoryServiceSpec, int, int) {
           21  +	for count := 0; count < 2; count++ {
           22  +		switch dirType {
           23  +		case kernel.BoxDirTypeNotify:
           24  +			return dirSrvNotify, 7, 1499
           25  +		case kernel.BoxDirTypeSimple:
           26  +			return dirSrvSimple, 1, 1
           27  +		default:
           28  +			dirType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string)
           29  +		}
           30  +	}
           31  +	panic("unable to set default dir box type: " + dirType)
           32  +}
           33  +
           34  +func (dp *dirBox) setupDirService() {
           35  +	switch dp.dirSrvSpec {
           36  +	case dirSrvSimple:
           37  +		dp.dirSrv = simpledir.NewService(dp.dir)
           38  +		dp.mustNotify = true
           39  +	default:
           40  +		dp.dirSrv = notifydir.NewService(dp.dir, dp.dirRescan, dp.cdata.Notify)
           41  +		dp.mustNotify = false
           42  +	}
           43  +}

Added box/dirbox/notifydir/notifydir.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package notifydir manages the notified directory part of a dirstore.
           12  +package notifydir
           13  +
           14  +import (
           15  +	"time"
           16  +
           17  +	"zettelstore.de/z/box"
           18  +	"zettelstore.de/z/box/dirbox/directory"
           19  +	"zettelstore.de/z/domain/id"
           20  +)
           21  +
           22  +// notifyService specifies a directory scan service.
           23  +type notifyService struct {
           24  +	dirPath    string
           25  +	rescanTime time.Duration
           26  +	done       chan struct{}
           27  +	cmds       chan dirCmd
           28  +	infos      chan<- box.UpdateInfo
           29  +}
           30  +
           31  +// NewService creates a new directory service.
           32  +func NewService(directoryPath string, rescanTime time.Duration, chci chan<- box.UpdateInfo) directory.Service {
           33  +	srv := &notifyService{
           34  +		dirPath:    directoryPath,
           35  +		rescanTime: rescanTime,
           36  +		cmds:       make(chan dirCmd),
           37  +		infos:      chci,
           38  +	}
           39  +	return srv
           40  +}
           41  +
           42  +// Start makes the directory service operational.
           43  +func (srv *notifyService) Start() error {
           44  +	tick := make(chan struct{})
           45  +	rawEvents := make(chan *fileEvent)
           46  +	events := make(chan *fileEvent)
           47  +
           48  +	ready := make(chan int)
           49  +	go srv.directoryService(events, ready)
           50  +	go collectEvents(events, rawEvents)
           51  +	go watchDirectory(srv.dirPath, rawEvents, tick)
           52  +
           53  +	if srv.done != nil {
           54  +		panic("src.done already set")
           55  +	}
           56  +	srv.done = make(chan struct{})
           57  +	go ping(tick, srv.rescanTime, srv.done)
           58  +	<-ready
           59  +	return nil
           60  +}
           61  +
           62  +// Stop stops the directory service.
           63  +func (srv *notifyService) Stop() error {
           64  +	close(srv.done)
           65  +	srv.done = nil
           66  +	return nil
           67  +}
           68  +
           69  +func (srv *notifyService) notifyChange(reason box.UpdateReason, zid id.Zid) {
           70  +	if chci := srv.infos; chci != nil {
           71  +		chci <- box.UpdateInfo{Reason: reason, Zid: zid}
           72  +	}
           73  +}
           74  +
           75  +// NumEntries returns the number of managed zettel.
           76  +func (srv *notifyService) NumEntries() (int, error) {
           77  +	resChan := make(chan resNumEntries)
           78  +	srv.cmds <- &cmdNumEntries{resChan}
           79  +	return <-resChan, nil
           80  +}
           81  +
           82  +// GetEntries returns an unsorted list of all current directory entries.
           83  +func (srv *notifyService) GetEntries() ([]*directory.Entry, error) {
           84  +	resChan := make(chan resGetEntries)
           85  +	srv.cmds <- &cmdGetEntries{resChan}
           86  +	return <-resChan, nil
           87  +}
           88  +
           89  +// GetEntry returns the entry with the specified zettel id. If there is no such
           90  +// zettel id, an empty entry is returned.
           91  +func (srv *notifyService) GetEntry(zid id.Zid) (*directory.Entry, error) {
           92  +	resChan := make(chan resGetEntry)
           93  +	srv.cmds <- &cmdGetEntry{zid, resChan}
           94  +	return <-resChan, nil
           95  +}
           96  +
           97  +// GetNew returns an entry with a new zettel id.
           98  +func (srv *notifyService) GetNew() (*directory.Entry, error) {
           99  +	resChan := make(chan resNewEntry)
          100  +	srv.cmds <- &cmdNewEntry{resChan}
          101  +	result := <-resChan
          102  +	return result.entry, result.err
          103  +}
          104  +
          105  +// UpdateEntry notifies the directory of an updated entry.
          106  +func (srv *notifyService) UpdateEntry(entry *directory.Entry) error {
          107  +	resChan := make(chan struct{})
          108  +	srv.cmds <- &cmdUpdateEntry{entry, resChan}
          109  +	<-resChan
          110  +	return nil
          111  +}
          112  +
          113  +// RenameEntry notifies the directory of an renamed entry.
          114  +func (srv *notifyService) RenameEntry(curEntry, newEntry *directory.Entry) error {
          115  +	resChan := make(chan resRenameEntry)
          116  +	srv.cmds <- &cmdRenameEntry{curEntry, newEntry, resChan}
          117  +	return <-resChan
          118  +}
          119  +
          120  +// DeleteEntry removes a zettel id from the directory of entries.
          121  +func (srv *notifyService) DeleteEntry(zid id.Zid) error {
          122  +	resChan := make(chan struct{})
          123  +	srv.cmds <- &cmdDeleteEntry{zid, resChan}
          124  +	<-resChan
          125  +	return nil
          126  +}

Added box/dirbox/notifydir/service.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package notifydir manages the notified directory part of a dirstore.
           12  +package notifydir
           13  +
           14  +import (
           15  +	"log"
           16  +	"time"
           17  +
           18  +	"zettelstore.de/z/box"
           19  +	"zettelstore.de/z/box/dirbox/directory"
           20  +	"zettelstore.de/z/domain/id"
           21  +)
           22  +
           23  +// ping sends every tick a signal to reload the directory list
           24  +func ping(tick chan<- struct{}, rescanTime time.Duration, done <-chan struct{}) {
           25  +	ticker := time.NewTicker(rescanTime)
           26  +	defer close(tick)
           27  +	for {
           28  +		select {
           29  +		case _, ok := <-ticker.C:
           30  +			if !ok {
           31  +				return
           32  +			}
           33  +			tick <- struct{}{}
           34  +		case _, ok := <-done:
           35  +			if !ok {
           36  +				ticker.Stop()
           37  +				return
           38  +			}
           39  +		}
           40  +	}
           41  +}
           42  +
           43  +func newEntry(ev *fileEvent) *directory.Entry {
           44  +	de := new(directory.Entry)
           45  +	de.Zid = ev.zid
           46  +	updateEntry(de, ev)
           47  +	return de
           48  +}
           49  +
           50  +func updateEntry(de *directory.Entry, ev *fileEvent) {
           51  +	if ev.ext == "meta" {
           52  +		de.MetaSpec = directory.MetaSpecFile
           53  +		de.MetaPath = ev.path
           54  +		return
           55  +	}
           56  +	if de.ContentExt != "" && de.ContentExt != ev.ext {
           57  +		de.Duplicates = true
           58  +		return
           59  +	}
           60  +	if de.MetaSpec != directory.MetaSpecFile {
           61  +		if ev.ext == "zettel" {
           62  +			de.MetaSpec = directory.MetaSpecHeader
           63  +		} else {
           64  +			de.MetaSpec = directory.MetaSpecNone
           65  +		}
           66  +	}
           67  +	de.ContentPath = ev.path
           68  +	de.ContentExt = ev.ext
           69  +}
           70  +
           71  +type dirMap map[id.Zid]*directory.Entry
           72  +
           73  +func dirMapUpdate(dm dirMap, ev *fileEvent) {
           74  +	de := dm[ev.zid]
           75  +	if de == nil {
           76  +		dm[ev.zid] = newEntry(ev)
           77  +		return
           78  +	}
           79  +	updateEntry(de, ev)
           80  +}
           81  +
           82  +func deleteFromMap(dm dirMap, ev *fileEvent) {
           83  +	if ev.ext == "meta" {
           84  +		if entry, ok := dm[ev.zid]; ok {
           85  +			if entry.MetaSpec == directory.MetaSpecFile {
           86  +				entry.MetaSpec = directory.MetaSpecNone
           87  +				return
           88  +			}
           89  +		}
           90  +	}
           91  +	delete(dm, ev.zid)
           92  +}
           93  +
           94  +// directoryService is the main service.
           95  +func (srv *notifyService) directoryService(events <-chan *fileEvent, ready chan<- int) {
           96  +	curMap := make(dirMap)
           97  +	var newMap dirMap
           98  +	for {
           99  +		select {
          100  +		case ev, ok := <-events:
          101  +			if !ok {
          102  +				return
          103  +			}
          104  +			switch ev.status {
          105  +			case fileStatusReloadStart:
          106  +				newMap = make(dirMap)
          107  +			case fileStatusReloadEnd:
          108  +				curMap = newMap
          109  +				newMap = nil
          110  +				if ready != nil {
          111  +					ready <- len(curMap)
          112  +					close(ready)
          113  +					ready = nil
          114  +				}
          115  +				srv.notifyChange(box.OnReload, id.Invalid)
          116  +			case fileStatusError:
          117  +				log.Println("DIRBOX", "ERROR", ev.err)
          118  +			case fileStatusUpdate:
          119  +				srv.processFileUpdateEvent(ev, curMap, newMap)
          120  +			case fileStatusDelete:
          121  +				srv.processFileDeleteEvent(ev, curMap, newMap)
          122  +			}
          123  +		case cmd, ok := <-srv.cmds:
          124  +			if ok {
          125  +				cmd.run(curMap)
          126  +			}
          127  +		}
          128  +	}
          129  +}
          130  +
          131  +func (srv *notifyService) processFileUpdateEvent(ev *fileEvent, curMap, newMap dirMap) {
          132  +	if newMap != nil {
          133  +		dirMapUpdate(newMap, ev)
          134  +	} else {
          135  +		dirMapUpdate(curMap, ev)
          136  +		srv.notifyChange(box.OnUpdate, ev.zid)
          137  +	}
          138  +}
          139  +
          140  +func (srv *notifyService) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) {
          141  +	if newMap != nil {
          142  +		deleteFromMap(newMap, ev)
          143  +	} else {
          144  +		deleteFromMap(curMap, ev)
          145  +		srv.notifyChange(box.OnDelete, ev.zid)
          146  +	}
          147  +}
          148  +
          149  +type dirCmd interface {
          150  +	run(m dirMap)
          151  +}
          152  +
          153  +type cmdNumEntries struct {
          154  +	result chan<- resNumEntries
          155  +}
          156  +type resNumEntries = int
          157  +
          158  +func (cmd *cmdNumEntries) run(m dirMap) {
          159  +	cmd.result <- len(m)
          160  +}
          161  +
          162  +type cmdGetEntries struct {
          163  +	result chan<- resGetEntries
          164  +}
          165  +type resGetEntries []*directory.Entry
          166  +
          167  +func (cmd *cmdGetEntries) run(m dirMap) {
          168  +	res := make([]*directory.Entry, len(m))
          169  +	i := 0
          170  +	for _, de := range m {
          171  +		entry := *de
          172  +		res[i] = &entry
          173  +		i++
          174  +	}
          175  +	cmd.result <- res
          176  +}
          177  +
          178  +type cmdGetEntry struct {
          179  +	zid    id.Zid
          180  +	result chan<- resGetEntry
          181  +}
          182  +type resGetEntry = *directory.Entry
          183  +
          184  +func (cmd *cmdGetEntry) run(m dirMap) {
          185  +	entry := m[cmd.zid]
          186  +	if entry == nil {
          187  +		cmd.result <- nil
          188  +	} else {
          189  +		result := *entry
          190  +		cmd.result <- &result
          191  +	}
          192  +}
          193  +
          194  +type cmdNewEntry struct {
          195  +	result chan<- resNewEntry
          196  +}
          197  +type resNewEntry struct {
          198  +	entry *directory.Entry
          199  +	err   error
          200  +}
          201  +
          202  +func (cmd *cmdNewEntry) run(m dirMap) {
          203  +	zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) {
          204  +		_, ok := m[zid]
          205  +		return !ok, nil
          206  +	})
          207  +	if err != nil {
          208  +		cmd.result <- resNewEntry{nil, err}
          209  +		return
          210  +	}
          211  +	entry := &directory.Entry{Zid: zid}
          212  +	m[zid] = entry
          213  +	cmd.result <- resNewEntry{&directory.Entry{Zid: zid}, nil}
          214  +}
          215  +
          216  +type cmdUpdateEntry struct {
          217  +	entry  *directory.Entry
          218  +	result chan<- struct{}
          219  +}
          220  +
          221  +func (cmd *cmdUpdateEntry) run(m dirMap) {
          222  +	entry := *cmd.entry
          223  +	m[entry.Zid] = &entry
          224  +	cmd.result <- struct{}{}
          225  +}
          226  +
          227  +type cmdRenameEntry struct {
          228  +	curEntry *directory.Entry
          229  +	newEntry *directory.Entry
          230  +	result   chan<- resRenameEntry
          231  +}
          232  +
          233  +type resRenameEntry = error
          234  +
          235  +func (cmd *cmdRenameEntry) run(m dirMap) {
          236  +	newEntry := *cmd.newEntry
          237  +	newZid := newEntry.Zid
          238  +	if _, found := m[newZid]; found {
          239  +		cmd.result <- &box.ErrInvalidID{Zid: newZid}
          240  +		return
          241  +	}
          242  +	delete(m, cmd.curEntry.Zid)
          243  +	m[newZid] = &newEntry
          244  +	cmd.result <- nil
          245  +}
          246  +
          247  +type cmdDeleteEntry struct {
          248  +	zid    id.Zid
          249  +	result chan<- struct{}
          250  +}
          251  +
          252  +func (cmd *cmdDeleteEntry) run(m dirMap) {
          253  +	delete(m, cmd.zid)
          254  +	cmd.result <- struct{}{}
          255  +}

Added box/dirbox/notifydir/watch.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package notifydir manages the notified directory part of a dirstore.
           12  +package notifydir
           13  +
           14  +import (
           15  +	"os"
           16  +	"path/filepath"
           17  +	"regexp"
           18  +	"time"
           19  +
           20  +	"github.com/fsnotify/fsnotify"
           21  +
           22  +	"zettelstore.de/z/domain/id"
           23  +)
           24  +
           25  +var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`)
           26  +
           27  +func matchValidFileName(name string) []string {
           28  +	return validFileName.FindStringSubmatch(name)
           29  +}
           30  +
           31  +type fileStatus int
           32  +
           33  +const (
           34  +	fileStatusNone fileStatus = iota
           35  +	fileStatusReloadStart
           36  +	fileStatusReloadEnd
           37  +	fileStatusError
           38  +	fileStatusUpdate
           39  +	fileStatusDelete
           40  +)
           41  +
           42  +type fileEvent struct {
           43  +	status fileStatus
           44  +	path   string // Full file path
           45  +	zid    id.Zid
           46  +	ext    string // File extension
           47  +	err    error  // Error if Status == fileStatusError
           48  +}
           49  +
           50  +type sendResult int
           51  +
           52  +const (
           53  +	sendDone sendResult = iota
           54  +	sendReload
           55  +	sendExit
           56  +)
           57  +
           58  +func watchDirectory(directory string, events chan<- *fileEvent, tick <-chan struct{}) {
           59  +	defer close(events)
           60  +
           61  +	var watcher *fsnotify.Watcher
           62  +	defer func() {
           63  +		if watcher != nil {
           64  +			watcher.Close()
           65  +		}
           66  +	}()
           67  +
           68  +	sendEvent := func(ev *fileEvent) sendResult {
           69  +		select {
           70  +		case events <- ev:
           71  +		case _, ok := <-tick:
           72  +			if ok {
           73  +				return sendReload
           74  +			}
           75  +			return sendExit
           76  +		}
           77  +		return sendDone
           78  +	}
           79  +
           80  +	sendError := func(err error) sendResult {
           81  +		return sendEvent(&fileEvent{status: fileStatusError, err: err})
           82  +	}
           83  +
           84  +	sendFileEvent := func(status fileStatus, path string, match []string) sendResult {
           85  +		zid, err := id.Parse(match[1])
           86  +		if err != nil {
           87  +			return sendDone
           88  +		}
           89  +		event := &fileEvent{
           90  +			status: status,
           91  +			path:   path,
           92  +			zid:    zid,
           93  +			ext:    match[3],
           94  +		}
           95  +		return sendEvent(event)
           96  +	}
           97  +
           98  +	reloadStartEvent := &fileEvent{status: fileStatusReloadStart}
           99  +	reloadEndEvent := &fileEvent{status: fileStatusReloadEnd}
          100  +	reloadFiles := func() bool {
          101  +		entries, err := os.ReadDir(directory)
          102  +		if err != nil {
          103  +			if res := sendError(err); res != sendDone {
          104  +				return res == sendReload
          105  +			}
          106  +			return true
          107  +		}
          108  +
          109  +		if res := sendEvent(reloadStartEvent); res != sendDone {
          110  +			return res == sendReload
          111  +		}
          112  +
          113  +		if watcher != nil {
          114  +			watcher.Close()
          115  +		}
          116  +		watcher, err = fsnotify.NewWatcher()
          117  +		if err != nil {
          118  +			if res := sendError(err); res != sendDone {
          119  +				return res == sendReload
          120  +			}
          121  +		}
          122  +
          123  +		for _, entry := range entries {
          124  +			if entry.IsDir() {
          125  +				continue
          126  +			}
          127  +			if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() {
          128  +				continue
          129  +			}
          130  +			name := entry.Name()
          131  +			match := matchValidFileName(name)
          132  +			if len(match) > 0 {
          133  +				path := filepath.Join(directory, name)
          134  +				if res := sendFileEvent(fileStatusUpdate, path, match); res != sendDone {
          135  +					return res == sendReload
          136  +				}
          137  +			}
          138  +		}
          139  +
          140  +		if watcher != nil {
          141  +			err = watcher.Add(directory)
          142  +			if err != nil {
          143  +				if res := sendError(err); res != sendDone {
          144  +					return res == sendReload
          145  +				}
          146  +			}
          147  +		}
          148  +		if res := sendEvent(reloadEndEvent); res != sendDone {
          149  +			return res == sendReload
          150  +		}
          151  +		return true
          152  +	}
          153  +
          154  +	handleEvents := func() bool {
          155  +		const createOps = fsnotify.Create | fsnotify.Write
          156  +		const deleteOps = fsnotify.Remove | fsnotify.Rename
          157  +
          158  +		for {
          159  +			select {
          160  +			case wevent, ok := <-watcher.Events:
          161  +				if !ok {
          162  +					return false
          163  +				}
          164  +				path := filepath.Clean(wevent.Name)
          165  +				match := matchValidFileName(filepath.Base(path))
          166  +				if len(match) == 0 {
          167  +					continue
          168  +				}
          169  +				if wevent.Op&createOps != 0 {
          170  +					if fi, err := os.Lstat(path); err != nil || !fi.Mode().IsRegular() {
          171  +						continue
          172  +					}
          173  +					if res := sendFileEvent(
          174  +						fileStatusUpdate, path, match); res != sendDone {
          175  +						return res == sendReload
          176  +					}
          177  +				}
          178  +				if wevent.Op&deleteOps != 0 {
          179  +					if res := sendFileEvent(
          180  +						fileStatusDelete, path, match); res != sendDone {
          181  +						return res == sendReload
          182  +					}
          183  +				}
          184  +			case err, ok := <-watcher.Errors:
          185  +				if !ok {
          186  +					return false
          187  +				}
          188  +				if res := sendError(err); res != sendDone {
          189  +					return res == sendReload
          190  +				}
          191  +			case _, ok := <-tick:
          192  +				return ok
          193  +			}
          194  +		}
          195  +	}
          196  +
          197  +	for {
          198  +		if !reloadFiles() {
          199  +			return
          200  +		}
          201  +		if watcher == nil {
          202  +			if _, ok := <-tick; !ok {
          203  +				return
          204  +			}
          205  +		} else {
          206  +			if !handleEvents() {
          207  +				return
          208  +			}
          209  +		}
          210  +	}
          211  +}
          212  +
          213  +func sendCollectedEvents(out chan<- *fileEvent, events []*fileEvent) {
          214  +	for _, ev := range events {
          215  +		if ev.status != fileStatusNone {
          216  +			out <- ev
          217  +		}
          218  +	}
          219  +}
          220  +
          221  +func addEvent(events []*fileEvent, ev *fileEvent) []*fileEvent {
          222  +	switch ev.status {
          223  +	case fileStatusNone:
          224  +		return events
          225  +	case fileStatusReloadStart:
          226  +		events = events[0:0]
          227  +	case fileStatusUpdate, fileStatusDelete:
          228  +		if len(events) > 0 && mergeEvents(events, ev) {
          229  +			return events
          230  +		}
          231  +	}
          232  +	return append(events, ev)
          233  +}
          234  +
          235  +func mergeEvents(events []*fileEvent, ev *fileEvent) bool {
          236  +	for i := len(events) - 1; i >= 0; i-- {
          237  +		oev := events[i]
          238  +		switch oev.status {
          239  +		case fileStatusReloadStart, fileStatusReloadEnd:
          240  +			return false
          241  +		case fileStatusUpdate, fileStatusDelete:
          242  +			if ev.path == oev.path {
          243  +				if ev.status == oev.status {
          244  +					return true
          245  +				}
          246  +				oev.status = fileStatusNone
          247  +				return false
          248  +			}
          249  +		}
          250  +	}
          251  +	return false
          252  +}
          253  +
          254  +func collectEvents(out chan<- *fileEvent, in <-chan *fileEvent) {
          255  +	defer close(out)
          256  +
          257  +	var sendTime time.Time
          258  +	sendTimeSet := false
          259  +	ticker := time.NewTicker(500 * time.Millisecond)
          260  +	defer ticker.Stop()
          261  +
          262  +	events := make([]*fileEvent, 0, 32)
          263  +	buffer := false
          264  +	for {
          265  +		select {
          266  +		case ev, ok := <-in:
          267  +			if !ok {
          268  +				sendCollectedEvents(out, events)
          269  +				return
          270  +			}
          271  +			if ev.status == fileStatusReloadStart {
          272  +				buffer = false
          273  +				events = events[0:0]
          274  +			}
          275  +			if buffer {
          276  +				if !sendTimeSet {
          277  +					sendTime = time.Now().Add(1500 * time.Millisecond)
          278  +					sendTimeSet = true
          279  +				}
          280  +				events = addEvent(events, ev)
          281  +				if len(events) > 1024 {
          282  +					sendCollectedEvents(out, events)
          283  +					events = events[0:0]
          284  +					sendTimeSet = false
          285  +				}
          286  +				continue
          287  +			}
          288  +			out <- ev
          289  +			if ev.status == fileStatusReloadEnd {
          290  +				buffer = true
          291  +			}
          292  +		case now := <-ticker.C:
          293  +			if sendTimeSet && now.After(sendTime) {
          294  +				sendCollectedEvents(out, events)
          295  +				events = events[0:0]
          296  +				sendTimeSet = false
          297  +			}
          298  +		}
          299  +	}
          300  +}

Added box/dirbox/notifydir/watch_test.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package notifydir manages the notified directory part of a dirstore.
           12  +package notifydir
           13  +
           14  +import "testing"
           15  +
           16  +func sameStringSlices(sl1, sl2 []string) bool {
           17  +	if len(sl1) != len(sl2) {
           18  +		return false
           19  +	}
           20  +	for i := 0; i < len(sl1); i++ {
           21  +		if sl1[i] != sl2[i] {
           22  +			return false
           23  +		}
           24  +	}
           25  +	return true
           26  +}
           27  +
           28  +func TestMatchValidFileName(t *testing.T) {
           29  +	t.Parallel()
           30  +	testcases := []struct {
           31  +		name string
           32  +		exp  []string
           33  +	}{
           34  +		{"", []string{}},
           35  +		{".txt", []string{}},
           36  +		{"12345678901234.txt", []string{"12345678901234", ".txt", "txt"}},
           37  +		{"12345678901234abc.txt", []string{"12345678901234", ".txt", "txt"}},
           38  +		{"12345678901234.abc.txt", []string{"12345678901234", ".txt", "txt"}},
           39  +	}
           40  +
           41  +	for i, tc := range testcases {
           42  +		got := matchValidFileName(tc.name)
           43  +		if len(got) == 0 {
           44  +			if len(tc.exp) > 0 {
           45  +				t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got)
           46  +			}
           47  +		} else {
           48  +			if got[0] != tc.name {
           49  +				t.Errorf("TC=%d, name=%q, got=%v", i, tc.name, got)
           50  +			}
           51  +			if !sameStringSlices(got[1:], tc.exp) {
           52  +				t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got)
           53  +			}
           54  +		}
           55  +	}
           56  +}

Added box/dirbox/service.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package dirbox provides a directory-based zettel box.
           12  +package dirbox
           13  +
           14  +import (
           15  +	"os"
           16  +
           17  +	"zettelstore.de/z/box/dirbox/directory"
           18  +	"zettelstore.de/z/box/filebox"
           19  +	"zettelstore.de/z/domain"
           20  +	"zettelstore.de/z/domain/id"
           21  +	"zettelstore.de/z/domain/meta"
           22  +	"zettelstore.de/z/input"
           23  +)
           24  +
           25  +func fileService(num uint32, cmds <-chan fileCmd) {
           26  +	for cmd := range cmds {
           27  +		cmd.run()
           28  +	}
           29  +}
           30  +
           31  +type fileCmd interface {
           32  +	run()
           33  +}
           34  +
           35  +// COMMAND: getMeta ----------------------------------------
           36  +//
           37  +// Retrieves the meta data from a zettel.
           38  +
           39  +func getMeta(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, error) {
           40  +	rc := make(chan resGetMeta)
           41  +	dp.getFileChan(zid) <- &fileGetMeta{entry, rc}
           42  +	res := <-rc
           43  +	close(rc)
           44  +	return res.meta, res.err
           45  +}
           46  +
           47  +type fileGetMeta struct {
           48  +	entry *directory.Entry
           49  +	rc    chan<- resGetMeta
           50  +}
           51  +type resGetMeta struct {
           52  +	meta *meta.Meta
           53  +	err  error
           54  +}
           55  +
           56  +func (cmd *fileGetMeta) run() {
           57  +	entry := cmd.entry
           58  +	var m *meta.Meta
           59  +	var err error
           60  +	switch entry.MetaSpec {
           61  +	case directory.MetaSpecFile:
           62  +		m, err = parseMetaFile(entry.Zid, entry.MetaPath)
           63  +	case directory.MetaSpecHeader:
           64  +		m, _, err = parseMetaContentFile(entry.Zid, entry.ContentPath)
           65  +	default:
           66  +		m = filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt)
           67  +	}
           68  +	if err == nil {
           69  +		cmdCleanupMeta(m, entry)
           70  +	}
           71  +	cmd.rc <- resGetMeta{m, err}
           72  +}
           73  +
           74  +// COMMAND: getMetaContent ----------------------------------------
           75  +//
           76  +// Retrieves the meta data and the content of a zettel.
           77  +
           78  +func getMetaContent(dp *dirBox, entry *directory.Entry, zid id.Zid) (*meta.Meta, string, error) {
           79  +	rc := make(chan resGetMetaContent)
           80  +	dp.getFileChan(zid) <- &fileGetMetaContent{entry, rc}
           81  +	res := <-rc
           82  +	close(rc)
           83  +	return res.meta, res.content, res.err
           84  +}
           85  +
           86  +type fileGetMetaContent struct {
           87  +	entry *directory.Entry
           88  +	rc    chan<- resGetMetaContent
           89  +}
           90  +type resGetMetaContent struct {
           91  +	meta    *meta.Meta
           92  +	content string
           93  +	err     error
           94  +}
           95  +
           96  +func (cmd *fileGetMetaContent) run() {
           97  +	var m *meta.Meta
           98  +	var content string
           99  +	var err error
          100  +
          101  +	entry := cmd.entry
          102  +	switch entry.MetaSpec {
          103  +	case directory.MetaSpecFile:
          104  +		m, err = parseMetaFile(entry.Zid, entry.MetaPath)
          105  +		if err == nil {
          106  +			content, err = readFileContent(entry.ContentPath)
          107  +		}
          108  +	case directory.MetaSpecHeader:
          109  +		m, content, err = parseMetaContentFile(entry.Zid, entry.ContentPath)
          110  +	default:
          111  +		m = filebox.CalcDefaultMeta(entry.Zid, entry.ContentExt)
          112  +		content, err = readFileContent(entry.ContentPath)
          113  +	}
          114  +	if err == nil {
          115  +		cmdCleanupMeta(m, entry)
          116  +	}
          117  +	cmd.rc <- resGetMetaContent{m, content, err}
          118  +}
          119  +
          120  +// COMMAND: setZettel ----------------------------------------
          121  +//
          122  +// Writes a new or exsting zettel.
          123  +
          124  +func setZettel(dp *dirBox, entry *directory.Entry, zettel domain.Zettel) error {
          125  +	rc := make(chan resSetZettel)
          126  +	dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc}
          127  +	err := <-rc
          128  +	close(rc)
          129  +	return err
          130  +}
          131  +
          132  +type fileSetZettel struct {
          133  +	entry  *directory.Entry
          134  +	zettel domain.Zettel
          135  +	rc     chan<- resSetZettel
          136  +}
          137  +type resSetZettel = error
          138  +
          139  +func (cmd *fileSetZettel) run() {
          140  +	var err error
          141  +	switch cmd.entry.MetaSpec {
          142  +	case directory.MetaSpecFile:
          143  +		err = cmd.runMetaSpecFile()
          144  +	case directory.MetaSpecHeader:
          145  +		err = cmd.runMetaSpecHeader()
          146  +	case directory.MetaSpecNone:
          147  +		// TODO: if meta has some additional infos: write meta to new .meta;
          148  +		// update entry in dir
          149  +		err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString())
          150  +	default:
          151  +		panic("TODO: ???")
          152  +	}
          153  +	cmd.rc <- err
          154  +}
          155  +
          156  +func (cmd *fileSetZettel) runMetaSpecFile() error {
          157  +	f, err := openFileWrite(cmd.entry.MetaPath)
          158  +	if err == nil {
          159  +		err = writeFileZid(f, cmd.zettel.Meta.Zid)
          160  +		if err == nil {
          161  +			_, err = cmd.zettel.Meta.Write(f, true)
          162  +			if err1 := f.Close(); err == nil {
          163  +				err = err1
          164  +			}
          165  +			if err == nil {
          166  +				err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString())
          167  +			}
          168  +		}
          169  +	}
          170  +	return err
          171  +}
          172  +
          173  +func (cmd *fileSetZettel) runMetaSpecHeader() error {
          174  +	f, err := openFileWrite(cmd.entry.ContentPath)
          175  +	if err == nil {
          176  +		err = writeFileZid(f, cmd.zettel.Meta.Zid)
          177  +		if err == nil {
          178  +			_, err = cmd.zettel.Meta.WriteAsHeader(f, true)
          179  +			if err == nil {
          180  +				_, err = f.WriteString(cmd.zettel.Content.AsString())
          181  +				if err1 := f.Close(); err == nil {
          182  +					err = err1
          183  +				}
          184  +			}
          185  +		}
          186  +	}
          187  +	return err
          188  +}
          189  +
          190  +// COMMAND: deleteZettel ----------------------------------------
          191  +//
          192  +// Deletes an existing zettel.
          193  +
          194  +func deleteZettel(dp *dirBox, entry *directory.Entry, zid id.Zid) error {
          195  +	rc := make(chan resDeleteZettel)
          196  +	dp.getFileChan(zid) <- &fileDeleteZettel{entry, rc}
          197  +	err := <-rc
          198  +	close(rc)
          199  +	return err
          200  +}
          201  +
          202  +type fileDeleteZettel struct {
          203  +	entry *directory.Entry
          204  +	rc    chan<- resDeleteZettel
          205  +}
          206  +type resDeleteZettel = error
          207  +
          208  +func (cmd *fileDeleteZettel) run() {
          209  +	var err error
          210  +
          211  +	switch cmd.entry.MetaSpec {
          212  +	case directory.MetaSpecFile:
          213  +		err1 := os.Remove(cmd.entry.MetaPath)
          214  +		err = os.Remove(cmd.entry.ContentPath)
          215  +		if err == nil {
          216  +			err = err1
          217  +		}
          218  +	case directory.MetaSpecHeader:
          219  +		err = os.Remove(cmd.entry.ContentPath)
          220  +	case directory.MetaSpecNone:
          221  +		err = os.Remove(cmd.entry.ContentPath)
          222  +	default:
          223  +		panic("TODO: ???")
          224  +	}
          225  +	cmd.rc <- err
          226  +}
          227  +
          228  +// Utility functions ----------------------------------------
          229  +
          230  +func readFileContent(path string) (string, error) {
          231  +	data, err := os.ReadFile(path)
          232  +	if err != nil {
          233  +		return "", err
          234  +	}
          235  +	return string(data), nil
          236  +}
          237  +
          238  +func parseMetaFile(zid id.Zid, path string) (*meta.Meta, error) {
          239  +	src, err := readFileContent(path)
          240  +	if err != nil {
          241  +		return nil, err
          242  +	}
          243  +	inp := input.NewInput(src)
          244  +	return meta.NewFromInput(zid, inp), nil
          245  +}
          246  +
          247  +func parseMetaContentFile(zid id.Zid, path string) (*meta.Meta, string, error) {
          248  +	src, err := readFileContent(path)
          249  +	if err != nil {
          250  +		return nil, "", err
          251  +	}
          252  +	inp := input.NewInput(src)
          253  +	meta := meta.NewFromInput(zid, inp)
          254  +	return meta, src[inp.Pos:], nil
          255  +}
          256  +
          257  +func cmdCleanupMeta(m *meta.Meta, entry *directory.Entry) {
          258  +	filebox.CleanupMeta(
          259  +		m,
          260  +		entry.Zid, entry.ContentExt,
          261  +		entry.MetaSpec == directory.MetaSpecFile,
          262  +		entry.Duplicates,
          263  +	)
          264  +}
          265  +
          266  +func openFileWrite(path string) (*os.File, error) {
          267  +	return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
          268  +}
          269  +
          270  +func writeFileZid(f *os.File, zid id.Zid) error {
          271  +	_, err := f.WriteString("id: ")
          272  +	if err == nil {
          273  +		_, err = f.Write(zid.Bytes())
          274  +		if err == nil {
          275  +			_, err = f.WriteString("\n")
          276  +		}
          277  +	}
          278  +	return err
          279  +}
          280  +
          281  +func writeFileContent(path, content string) error {
          282  +	f, err := openFileWrite(path)
          283  +	if err == nil {
          284  +		_, err = f.WriteString(content)
          285  +		if err1 := f.Close(); err == nil {
          286  +			err = err1
          287  +		}
          288  +	}
          289  +	return err
          290  +}

Added box/dirbox/simpledir/simpledir.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package simpledir manages the directory part of a dirstore.
           12  +package simpledir
           13  +
           14  +import (
           15  +	"os"
           16  +	"path/filepath"
           17  +	"regexp"
           18  +	"sync"
           19  +
           20  +	"zettelstore.de/z/box"
           21  +	"zettelstore.de/z/box/dirbox/directory"
           22  +	"zettelstore.de/z/domain/id"
           23  +)
           24  +
           25  +// simpleService specifies a directory service without scanning.
           26  +type simpleService struct {
           27  +	dirPath string
           28  +	mx      sync.Mutex
           29  +}
           30  +
           31  +// NewService creates a new directory service.
           32  +func NewService(directoryPath string) directory.Service {
           33  +	return &simpleService{
           34  +		dirPath: directoryPath,
           35  +	}
           36  +}
           37  +
           38  +func (ss *simpleService) Start() error {
           39  +	ss.mx.Lock()
           40  +	defer ss.mx.Unlock()
           41  +	_, err := os.ReadDir(ss.dirPath)
           42  +	return err
           43  +}
           44  +
           45  +func (ss *simpleService) Stop() error {
           46  +	return nil
           47  +}
           48  +
           49  +func (ss *simpleService) NumEntries() (int, error) {
           50  +	ss.mx.Lock()
           51  +	defer ss.mx.Unlock()
           52  +	entries, err := ss.getEntries()
           53  +	if err == nil {
           54  +		return len(entries), nil
           55  +	}
           56  +	return 0, err
           57  +}
           58  +
           59  +func (ss *simpleService) GetEntries() ([]*directory.Entry, error) {
           60  +	ss.mx.Lock()
           61  +	defer ss.mx.Unlock()
           62  +	entrySet, err := ss.getEntries()
           63  +	if err != nil {
           64  +		return nil, err
           65  +	}
           66  +	result := make([]*directory.Entry, 0, len(entrySet))
           67  +	for _, entry := range entrySet {
           68  +		result = append(result, entry)
           69  +	}
           70  +	return result, nil
           71  +}
           72  +func (ss *simpleService) getEntries() (map[id.Zid]*directory.Entry, error) {
           73  +	dirEntries, err := os.ReadDir(ss.dirPath)
           74  +	if err != nil {
           75  +		return nil, err
           76  +	}
           77  +	entrySet := make(map[id.Zid]*directory.Entry)
           78  +	for _, dirEntry := range dirEntries {
           79  +		if dirEntry.IsDir() {
           80  +			continue
           81  +		}
           82  +		if info, err1 := dirEntry.Info(); err1 != nil || !info.Mode().IsRegular() {
           83  +			continue
           84  +		}
           85  +		name := dirEntry.Name()
           86  +		match := matchValidFileName(name)
           87  +		if len(match) == 0 {
           88  +			continue
           89  +		}
           90  +		zid, err := id.Parse(match[1])
           91  +		if err != nil {
           92  +			continue
           93  +		}
           94  +		var entry *directory.Entry
           95  +		if e, ok := entrySet[zid]; ok {
           96  +			entry = e
           97  +		} else {
           98  +			entry = &directory.Entry{Zid: zid}
           99  +			entrySet[zid] = entry
          100  +		}
          101  +		updateEntry(entry, filepath.Join(ss.dirPath, name), match[3])
          102  +	}
          103  +	return entrySet, nil
          104  +}
          105  +
          106  +var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`)
          107  +
          108  +func matchValidFileName(name string) []string {
          109  +	return validFileName.FindStringSubmatch(name)
          110  +}
          111  +
          112  +func updateEntry(entry *directory.Entry, path, ext string) {
          113  +	if ext == "meta" {
          114  +		entry.MetaSpec = directory.MetaSpecFile
          115  +		entry.MetaPath = path
          116  +	} else if entry.ContentExt != "" && entry.ContentExt != ext {
          117  +		entry.Duplicates = true
          118  +	} else {
          119  +		if entry.MetaSpec != directory.MetaSpecFile {
          120  +			if ext == "zettel" {
          121  +				entry.MetaSpec = directory.MetaSpecHeader
          122  +			} else {
          123  +				entry.MetaSpec = directory.MetaSpecNone
          124  +			}
          125  +		}
          126  +		entry.ContentPath = path
          127  +		entry.ContentExt = ext
          128  +	}
          129  +}
          130  +
          131  +func (ss *simpleService) GetEntry(zid id.Zid) (*directory.Entry, error) {
          132  +	ss.mx.Lock()
          133  +	defer ss.mx.Unlock()
          134  +	return ss.getEntry(zid)
          135  +}
          136  +func (ss *simpleService) getEntry(zid id.Zid) (*directory.Entry, error) {
          137  +	pattern := filepath.Join(ss.dirPath, zid.String()) + "*.*"
          138  +	paths, err := filepath.Glob(pattern)
          139  +	if err != nil {
          140  +		return nil, err
          141  +	}
          142  +	if len(paths) == 0 {
          143  +		return nil, nil
          144  +	}
          145  +	entry := &directory.Entry{Zid: zid}
          146  +	for _, path := range paths {
          147  +		ext := filepath.Ext(path)
          148  +		if len(ext) > 0 && ext[0] == '.' {
          149  +			ext = ext[1:]
          150  +		}
          151  +		updateEntry(entry, path, ext)
          152  +	}
          153  +	return entry, nil
          154  +}
          155  +
          156  +func (ss *simpleService) GetNew() (*directory.Entry, error) {
          157  +	ss.mx.Lock()
          158  +	defer ss.mx.Unlock()
          159  +	zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) {
          160  +		entry, err := ss.getEntry(zid)
          161  +		if err != nil {
          162  +			return false, nil
          163  +		}
          164  +		return !entry.IsValid(), nil
          165  +	})
          166  +	if err != nil {
          167  +		return nil, err
          168  +	}
          169  +	return &directory.Entry{Zid: zid}, nil
          170  +}
          171  +
          172  +func (ss *simpleService) UpdateEntry(entry *directory.Entry) error {
          173  +	// Nothing to to, since the actual file update is done by dirbox.
          174  +	return nil
          175  +}
          176  +
          177  +func (ss *simpleService) RenameEntry(curEntry, newEntry *directory.Entry) error {
          178  +	// Nothing to to, since the actual file rename is done by dirbox.
          179  +	return nil
          180  +}
          181  +
          182  +func (ss *simpleService) DeleteEntry(zid id.Zid) error {
          183  +	// Nothing to to, since the actual file delete is done by dirbox.
          184  +	return nil
          185  +}

Added box/filebox/filebox.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package filebox provides boxes that are stored in a file.
           12  +package filebox
           13  +
           14  +import (
           15  +	"errors"
           16  +	"net/url"
           17  +	"path/filepath"
           18  +	"strings"
           19  +
           20  +	"zettelstore.de/z/box"
           21  +	"zettelstore.de/z/box/manager"
           22  +	"zettelstore.de/z/domain/id"
           23  +	"zettelstore.de/z/domain/meta"
           24  +)
           25  +
           26  +func init() {
           27  +	manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
           28  +		path := getFilepathFromURL(u)
           29  +		ext := strings.ToLower(filepath.Ext(path))
           30  +		if ext != ".zip" {
           31  +			return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String())
           32  +		}
           33  +		return &zipBox{
           34  +			number:   cdata.Number,
           35  +			name:     path,
           36  +			enricher: cdata.Enricher,
           37  +		}, nil
           38  +	})
           39  +}
           40  +
           41  +func getFilepathFromURL(u *url.URL) string {
           42  +	name := u.Opaque
           43  +	if name == "" {
           44  +		name = u.Path
           45  +	}
           46  +	components := strings.Split(name, "/")
           47  +	fileName := filepath.Join(components...)
           48  +	if len(components) > 0 && components[0] == "" {
           49  +		return "/" + fileName
           50  +	}
           51  +	return fileName
           52  +}
           53  +
           54  +var alternativeSyntax = map[string]string{
           55  +	"htm": "html",
           56  +}
           57  +
           58  +func calculateSyntax(ext string) string {
           59  +	ext = strings.ToLower(ext)
           60  +	if syntax, ok := alternativeSyntax[ext]; ok {
           61  +		return syntax
           62  +	}
           63  +	return ext
           64  +}
           65  +
           66  +// CalcDefaultMeta returns metadata with default values for the given entry.
           67  +func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta {
           68  +	m := meta.New(zid)
           69  +	m.Set(meta.KeyTitle, zid.String())
           70  +	m.Set(meta.KeySyntax, calculateSyntax(ext))
           71  +	return m
           72  +}
           73  +
           74  +// CleanupMeta enhances the given metadata.
           75  +func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta, duplicates bool) {
           76  +	if title, ok := m.Get(meta.KeyTitle); !ok || title == "" {
           77  +		m.Set(meta.KeyTitle, zid.String())
           78  +	}
           79  +
           80  +	if inMeta {
           81  +		if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" {
           82  +			dm := CalcDefaultMeta(zid, ext)
           83  +			syntax, ok = dm.Get(meta.KeySyntax)
           84  +			if !ok {
           85  +				panic("Default meta must contain syntax")
           86  +			}
           87  +			m.Set(meta.KeySyntax, syntax)
           88  +		}
           89  +	}
           90  +
           91  +	if duplicates {
           92  +		m.Set(meta.KeyDuplicates, meta.ValueTrue)
           93  +	}
           94  +}

Added box/filebox/zipbox.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package filebox provides boxes that are stored in a file.
           12  +package filebox
           13  +
           14  +import (
           15  +	"archive/zip"
           16  +	"context"
           17  +	"io"
           18  +	"regexp"
           19  +	"strings"
           20  +
           21  +	"zettelstore.de/z/box"
           22  +	"zettelstore.de/z/domain"
           23  +	"zettelstore.de/z/domain/id"
           24  +	"zettelstore.de/z/domain/meta"
           25  +	"zettelstore.de/z/input"
           26  +	"zettelstore.de/z/search"
           27  +)
           28  +
           29  +var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`)
           30  +
           31  +func matchValidFileName(name string) []string {
           32  +	return validFileName.FindStringSubmatch(name)
           33  +}
           34  +
           35  +type zipEntry struct {
           36  +	metaName     string
           37  +	contentName  string
           38  +	contentExt   string // (normalized) file extension of zettel content
           39  +	metaInHeader bool
           40  +}
           41  +
           42  +type zipBox struct {
           43  +	number   int
           44  +	name     string
           45  +	enricher box.Enricher
           46  +	zettel   map[id.Zid]*zipEntry // no lock needed, because read-only after creation
           47  +}
           48  +
           49  +func (zp *zipBox) Location() string {
           50  +	if strings.HasPrefix(zp.name, "/") {
           51  +		return "file://" + zp.name
           52  +	}
           53  +	return "file:" + zp.name
           54  +}
           55  +
           56  +func (zp *zipBox) Start(ctx context.Context) error {
           57  +	reader, err := zip.OpenReader(zp.name)
           58  +	if err != nil {
           59  +		return err
           60  +	}
           61  +	defer reader.Close()
           62  +	zp.zettel = make(map[id.Zid]*zipEntry)
           63  +	for _, f := range reader.File {
           64  +		match := matchValidFileName(f.Name)
           65  +		if len(match) < 1 {
           66  +			continue
           67  +		}
           68  +		zid, err := id.Parse(match[1])
           69  +		if err != nil {
           70  +			continue
           71  +		}
           72  +		zp.addFile(zid, f.Name, match[3])
           73  +	}
           74  +	return nil
           75  +}
           76  +
           77  +func (zp *zipBox) addFile(zid id.Zid, name, ext string) {
           78  +	entry := zp.zettel[zid]
           79  +	if entry == nil {
           80  +		entry = &zipEntry{}
           81  +		zp.zettel[zid] = entry
           82  +	}
           83  +	switch ext {
           84  +	case "zettel":
           85  +		if entry.contentExt == "" {
           86  +			entry.contentName = name
           87  +			entry.contentExt = ext
           88  +			entry.metaInHeader = true
           89  +		}
           90  +	case "meta":
           91  +		entry.metaName = name
           92  +		entry.metaInHeader = false
           93  +	default:
           94  +		if entry.contentExt == "" {
           95  +			entry.contentExt = ext
           96  +			entry.contentName = name
           97  +		}
           98  +	}
           99  +}
          100  +
          101  +func (zp *zipBox) Stop(ctx context.Context) error {
          102  +	zp.zettel = nil
          103  +	return nil
          104  +}
          105  +
          106  +func (zp *zipBox) CanCreateZettel(ctx context.Context) bool { return false }
          107  +
          108  +func (zp *zipBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
          109  +	return id.Invalid, box.ErrReadOnly
          110  +}
          111  +
          112  +func (zp *zipBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
          113  +	entry, ok := zp.zettel[zid]
          114  +	if !ok {
          115  +		return domain.Zettel{}, box.ErrNotFound
          116  +	}
          117  +	reader, err := zip.OpenReader(zp.name)
          118  +	if err != nil {
          119  +		return domain.Zettel{}, err
          120  +	}
          121  +	defer reader.Close()
          122  +
          123  +	var m *meta.Meta
          124  +	var src string
          125  +	var inMeta bool
          126  +	if entry.metaInHeader {
          127  +		src, err = readZipFileContent(reader, entry.contentName)
          128  +		if err != nil {
          129  +			return domain.Zettel{}, err
          130  +		}
          131  +		inp := input.NewInput(src)
          132  +		m = meta.NewFromInput(zid, inp)
          133  +		src = src[inp.Pos:]
          134  +	} else if metaName := entry.metaName; metaName != "" {
          135  +		m, err = readZipMetaFile(reader, zid, metaName)
          136  +		if err != nil {
          137  +			return domain.Zettel{}, err
          138  +		}
          139  +		src, err = readZipFileContent(reader, entry.contentName)
          140  +		if err != nil {
          141  +			return domain.Zettel{}, err
          142  +		}
          143  +		inMeta = true
          144  +	} else {
          145  +		m = CalcDefaultMeta(zid, entry.contentExt)
          146  +	}
          147  +	CleanupMeta(m, zid, entry.contentExt, inMeta, false)
          148  +	return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil
          149  +}
          150  +
          151  +func (zp *zipBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
          152  +	entry, ok := zp.zettel[zid]
          153  +	if !ok {
          154  +		return nil, box.ErrNotFound
          155  +	}
          156  +	reader, err := zip.OpenReader(zp.name)
          157  +	if err != nil {
          158  +		return nil, err
          159  +	}
          160  +	defer reader.Close()
          161  +	return readZipMeta(reader, zid, entry)
          162  +}
          163  +
          164  +func (zp *zipBox) FetchZids(ctx context.Context) (id.Set, error) {
          165  +	result := id.NewSetCap(len(zp.zettel))
          166  +	for zid := range zp.zettel {
          167  +		result[zid] = true
          168  +	}
          169  +	return result, nil
          170  +}
          171  +
          172  +func (zp *zipBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) {
          173  +	reader, err := zip.OpenReader(zp.name)
          174  +	if err != nil {
          175  +		return nil, err
          176  +	}
          177  +	defer reader.Close()
          178  +	for zid, entry := range zp.zettel {
          179  +		m, err := readZipMeta(reader, zid, entry)
          180  +		if err != nil {
          181  +			continue
          182  +		}
          183  +		zp.enricher.Enrich(ctx, m, zp.number)
          184  +		if match(m) {
          185  +			res = append(res, m)
          186  +		}
          187  +	}
          188  +	return res, nil
          189  +}
          190  +
          191  +func (zp *zipBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
          192  +	return false
          193  +}
          194  +
          195  +func (zp *zipBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
          196  +	return box.ErrReadOnly
          197  +}
          198  +
          199  +func (zp *zipBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
          200  +	_, ok := zp.zettel[zid]
          201  +	return !ok
          202  +}
          203  +
          204  +func (zp *zipBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
          205  +	if _, ok := zp.zettel[curZid]; ok {
          206  +		return box.ErrReadOnly
          207  +	}
          208  +	return box.ErrNotFound
          209  +}
          210  +
          211  +func (zp *zipBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false }
          212  +
          213  +func (zp *zipBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
          214  +	if _, ok := zp.zettel[zid]; ok {
          215  +		return box.ErrReadOnly
          216  +	}
          217  +	return box.ErrNotFound
          218  +}
          219  +
          220  +func (zp *zipBox) ReadStats(st *box.ManagedBoxStats) {
          221  +	st.ReadOnly = true
          222  +	st.Zettel = len(zp.zettel)
          223  +}
          224  +
          225  +func readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *zipEntry) (m *meta.Meta, err error) {
          226  +	var inMeta bool
          227  +	if entry.metaInHeader {
          228  +		m, err = readZipMetaFile(reader, zid, entry.contentName)
          229  +	} else if metaName := entry.metaName; metaName != "" {
          230  +		m, err = readZipMetaFile(reader, zid, entry.metaName)
          231  +		inMeta = true
          232  +	} else {
          233  +		m = CalcDefaultMeta(zid, entry.contentExt)
          234  +	}
          235  +	if err == nil {
          236  +		CleanupMeta(m, zid, entry.contentExt, inMeta, false)
          237  +	}
          238  +	return m, err
          239  +}
          240  +
          241  +func readZipMetaFile(reader *zip.ReadCloser, zid id.Zid, name string) (*meta.Meta, error) {
          242  +	src, err := readZipFileContent(reader, name)
          243  +	if err != nil {
          244  +		return nil, err
          245  +	}
          246  +	inp := input.NewInput(src)
          247  +	return meta.NewFromInput(zid, inp), nil
          248  +}
          249  +
          250  +func readZipFileContent(reader *zip.ReadCloser, name string) (string, error) {
          251  +	f, err := reader.Open(name)
          252  +	if err != nil {
          253  +		return "", err
          254  +	}
          255  +	defer f.Close()
          256  +	buf, err := io.ReadAll(f)
          257  +	if err != nil {
          258  +		return "", err
          259  +	}
          260  +	return string(buf), nil
          261  +}

Added box/helper.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package box provides a generic interface to zettel boxes.
           12  +package box
           13  +
           14  +import (
           15  +	"time"
           16  +
           17  +	"zettelstore.de/z/domain/id"
           18  +)
           19  +
           20  +// GetNewZid calculates a new and unused zettel identifier, based on the current date and time.
           21  +func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) {
           22  +	withSeconds := false
           23  +	for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout)
           24  +		zid := id.New(withSeconds)
           25  +		found, err := testZid(zid)
           26  +		if err != nil {
           27  +			return id.Invalid, err
           28  +		}
           29  +		if found {
           30  +			return zid, nil
           31  +		}
           32  +		// TODO: do not wait here unconditionally.
           33  +		time.Sleep(100 * time.Millisecond)
           34  +		withSeconds = true
           35  +	}
           36  +	return id.Invalid, ErrConflict
           37  +}

Added box/manager/anteroom.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package manager coordinates the various boxes and indexes of a Zettelstore.
           12  +package manager
           13  +
           14  +import (
           15  +	"sync"
           16  +
           17  +	"zettelstore.de/z/domain/id"
           18  +)
           19  +
           20  +type arAction int
           21  +
           22  +const (
           23  +	arNothing arAction = iota
           24  +	arReload
           25  +	arUpdate
           26  +	arDelete
           27  +)
           28  +
           29  +type anteroom struct {
           30  +	num     uint64
           31  +	next    *anteroom
           32  +	waiting map[id.Zid]arAction
           33  +	curLoad int
           34  +	reload  bool
           35  +}
           36  +
           37  +type anterooms struct {
           38  +	mx      sync.Mutex
           39  +	nextNum uint64
           40  +	first   *anteroom
           41  +	last    *anteroom
           42  +	maxLoad int
           43  +}
           44  +
           45  +func newAnterooms(maxLoad int) *anterooms {
           46  +	return &anterooms{maxLoad: maxLoad}
           47  +}
           48  +
           49  +func (ar *anterooms) Enqueue(zid id.Zid, action arAction) {
           50  +	if !zid.IsValid() || action == arNothing || action == arReload {
           51  +		return
           52  +	}
           53  +	ar.mx.Lock()
           54  +	defer ar.mx.Unlock()
           55  +	if ar.first == nil {
           56  +		ar.first = ar.makeAnteroom(zid, action)
           57  +		ar.last = ar.first
           58  +		return
           59  +	}
           60  +	for room := ar.first; room != nil; room = room.next {
           61  +		if room.reload {
           62  +			continue // Do not put zettel in reload room
           63  +		}
           64  +		a, ok := room.waiting[zid]
           65  +		if !ok {
           66  +			continue
           67  +		}
           68  +		switch action {
           69  +		case a:
           70  +			return
           71  +		case arUpdate:
           72  +			room.waiting[zid] = action
           73  +		case arDelete:
           74  +			room.waiting[zid] = action
           75  +		}
           76  +		return
           77  +	}
           78  +	if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) {
           79  +		room.waiting[zid] = action
           80  +		room.curLoad++
           81  +		return
           82  +	}
           83  +	room := ar.makeAnteroom(zid, action)
           84  +	ar.last.next = room
           85  +	ar.last = room
           86  +}
           87  +
           88  +func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom {
           89  +	c := ar.maxLoad
           90  +	if c == 0 {
           91  +		c = 100
           92  +	}
           93  +	waiting := make(map[id.Zid]arAction, c)
           94  +	waiting[zid] = action
           95  +	ar.nextNum++
           96  +	return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false}
           97  +}
           98  +
           99  +func (ar *anterooms) Reset() {
          100  +	ar.mx.Lock()
          101  +	defer ar.mx.Unlock()
          102  +	ar.first = ar.makeAnteroom(id.Invalid, arReload)
          103  +	ar.last = ar.first
          104  +}
          105  +
          106  +func (ar *anterooms) Reload(newZids id.Set) uint64 {
          107  +	ar.mx.Lock()
          108  +	defer ar.mx.Unlock()
          109  +	newWaiting := createWaitingSet(newZids, arUpdate)
          110  +	ar.deleteReloadedRooms()
          111  +
          112  +	if ns := len(newWaiting); ns > 0 {
          113  +		ar.nextNum++
          114  +		ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newWaiting, curLoad: ns}
          115  +		if ar.first.next == nil {
          116  +			ar.last = ar.first
          117  +		}
          118  +		return ar.nextNum
          119  +	}
          120  +
          121  +	ar.first = nil
          122  +	ar.last = nil
          123  +	return 0
          124  +}
          125  +
          126  +func createWaitingSet(zids id.Set, action arAction) map[id.Zid]arAction {
          127  +	waitingSet := make(map[id.Zid]arAction, len(zids))
          128  +	for zid := range zids {
          129  +		if zid.IsValid() {
          130  +			waitingSet[zid] = action
          131  +		}
          132  +	}
          133  +	return waitingSet
          134  +}
          135  +
          136  +func (ar *anterooms) deleteReloadedRooms() {
          137  +	room := ar.first
          138  +	for room != nil && room.reload {
          139  +		room = room.next
          140  +	}
          141  +	ar.first = room
          142  +	if room == nil {
          143  +		ar.last = nil
          144  +	}
          145  +}
          146  +
          147  +func (ar *anterooms) Dequeue() (arAction, id.Zid, uint64) {
          148  +	ar.mx.Lock()
          149  +	defer ar.mx.Unlock()
          150  +	if ar.first == nil {
          151  +		return arNothing, id.Invalid, 0
          152  +	}
          153  +	for zid, action := range ar.first.waiting {
          154  +		roomNo := ar.first.num
          155  +		delete(ar.first.waiting, zid)
          156  +		if len(ar.first.waiting) == 0 {
          157  +			ar.first = ar.first.next
          158  +			if ar.first == nil {
          159  +				ar.last = nil
          160  +			}
          161  +		}
          162  +		return action, zid, roomNo
          163  +	}
          164  +	return arNothing, id.Invalid, 0
          165  +}

Added box/manager/anteroom_test.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package manager coordinates the various boxes and indexes of a Zettelstore.
           12  +package manager
           13  +
           14  +import (
           15  +	"testing"
           16  +
           17  +	"zettelstore.de/z/domain/id"
           18  +)
           19  +
           20  +func TestSimple(t *testing.T) {
           21  +	t.Parallel()
           22  +	ar := newAnterooms(2)
           23  +	ar.Enqueue(id.Zid(1), arUpdate)
           24  +	action, zid, rno := ar.Dequeue()
           25  +	if zid != id.Zid(1) || action != arUpdate || rno != 1 {
           26  +		t.Errorf("Expected arUpdate/1/1, but got %v/%v/%v", action, zid, rno)
           27  +	}
           28  +	action, zid, _ = ar.Dequeue()
           29  +	if zid != id.Invalid && action != arDelete {
           30  +		t.Errorf("Expected invalid Zid, but got %v", zid)
           31  +	}
           32  +	ar.Enqueue(id.Zid(1), arUpdate)
           33  +	ar.Enqueue(id.Zid(2), arUpdate)
           34  +	if ar.first != ar.last {
           35  +		t.Errorf("Expected one room, but got more")
           36  +	}
           37  +	ar.Enqueue(id.Zid(3), arUpdate)
           38  +	if ar.first == ar.last {
           39  +		t.Errorf("Expected more than one room, but got only one")
           40  +	}
           41  +
           42  +	count := 0
           43  +	for ; count < 1000; count++ {
           44  +		action, _, _ := ar.Dequeue()
           45  +		if action == arNothing {
           46  +			break
           47  +		}
           48  +	}
           49  +	if count != 3 {
           50  +		t.Errorf("Expected 3 dequeues, but got %v", count)
           51  +	}
           52  +}
           53  +
           54  +func TestReset(t *testing.T) {
           55  +	t.Parallel()
           56  +	ar := newAnterooms(1)
           57  +	ar.Enqueue(id.Zid(1), arUpdate)
           58  +	ar.Reset()
           59  +	action, zid, _ := ar.Dequeue()
           60  +	if action != arReload || zid != id.Invalid {
           61  +		t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid)
           62  +	}
           63  +	ar.Reload(id.NewSet(3, 4))
           64  +	ar.Enqueue(id.Zid(5), arUpdate)
           65  +	ar.Enqueue(id.Zid(5), arDelete)
           66  +	ar.Enqueue(id.Zid(5), arDelete)
           67  +	ar.Enqueue(id.Zid(5), arUpdate)
           68  +	if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ {
           69  +		t.Errorf("Expected 2 rooms")
           70  +	}
           71  +	action, zid1, _ := ar.Dequeue()
           72  +	if action != arUpdate {
           73  +		t.Errorf("Expected arUpdate, but got %v", action)
           74  +	}
           75  +	action, zid2, _ := ar.Dequeue()
           76  +	if action != arUpdate {
           77  +		t.Errorf("Expected arUpdate, but got %v", action)
           78  +	}
           79  +	if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) {
           80  +		t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2)
           81  +	}
           82  +	action, zid, _ = ar.Dequeue()
           83  +	if zid != id.Zid(5) || action != arUpdate {
           84  +		t.Errorf("Expected 5/arUpdate, but got %v/%v", zid, action)
           85  +	}
           86  +	action, zid, _ = ar.Dequeue()
           87  +	if action != arNothing || zid != id.Invalid {
           88  +		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
           89  +	}
           90  +
           91  +	ar = newAnterooms(1)
           92  +	ar.Reload(id.NewSet(id.Zid(6)))
           93  +	action, zid, _ = ar.Dequeue()
           94  +	if zid != id.Zid(6) || action != arUpdate {
           95  +		t.Errorf("Expected 6/arUpdate, but got %v/%v", zid, action)
           96  +	}
           97  +	action, zid, _ = ar.Dequeue()
           98  +	if action != arNothing || zid != id.Invalid {
           99  +		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
          100  +	}
          101  +
          102  +	ar = newAnterooms(1)
          103  +	ar.Enqueue(id.Zid(8), arUpdate)
          104  +	ar.Reload(nil)
          105  +	action, zid, _ = ar.Dequeue()
          106  +	if action != arNothing || zid != id.Invalid {
          107  +		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
          108  +	}
          109  +}

Added box/manager/box.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package manager coordinates the various boxes and indexes of a Zettelstore.
           12  +package manager
           13  +
           14  +import (
           15  +	"context"
           16  +	"errors"
           17  +	"sort"
           18  +	"strings"
           19  +
           20  +	"zettelstore.de/z/box"
           21  +	"zettelstore.de/z/domain"
           22  +	"zettelstore.de/z/domain/id"
           23  +	"zettelstore.de/z/domain/meta"
           24  +	"zettelstore.de/z/search"
           25  +)
           26  +
           27  +// Conatains all box.Box related functions
           28  +
           29  +// Location returns some information where the box is located.
           30  +func (mgr *Manager) Location() string {
           31  +	if len(mgr.boxes) <= 2 {
           32  +		return "NONE"
           33  +	}
           34  +	var sb strings.Builder
           35  +	for i := 0; i < len(mgr.boxes)-2; i++ {
           36  +		if i > 0 {
           37  +			sb.WriteString(", ")
           38  +		}
           39  +		sb.WriteString(mgr.boxes[i].Location())
           40  +	}
           41  +	return sb.String()
           42  +}
           43  +
           44  +// CanCreateZettel returns true, if box could possibly create a new zettel.
           45  +func (mgr *Manager) CanCreateZettel(ctx context.Context) bool {
           46  +	mgr.mgrMx.RLock()
           47  +	defer mgr.mgrMx.RUnlock()
           48  +	return mgr.started && mgr.boxes[0].CanCreateZettel(ctx)
           49  +}
           50  +
           51  +// CreateZettel creates a new zettel.
           52  +func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
           53  +	mgr.mgrMx.RLock()
           54  +	defer mgr.mgrMx.RUnlock()
           55  +	if !mgr.started {
           56  +		return id.Invalid, box.ErrStopped
           57  +	}
           58  +	return mgr.boxes[0].CreateZettel(ctx, zettel)
           59  +}
           60  +
           61  +// GetZettel retrieves a specific zettel.
           62  +func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
           63  +	mgr.mgrMx.RLock()
           64  +	defer mgr.mgrMx.RUnlock()
           65  +	if !mgr.started {
           66  +		return domain.Zettel{}, box.ErrStopped
           67  +	}
           68  +	for i, p := range mgr.boxes {
           69  +		if z, err := p.GetZettel(ctx, zid); err != box.ErrNotFound {
           70  +			if err == nil {
           71  +				mgr.Enrich(ctx, z.Meta, i+1)
           72  +			}
           73  +			return z, err
           74  +		}
           75  +	}
           76  +	return domain.Zettel{}, box.ErrNotFound
           77  +}
           78  +
           79  +// GetAllZettel retrieves a specific zettel from all managed boxes.
           80  +func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) {
           81  +	mgr.mgrMx.RLock()
           82  +	defer mgr.mgrMx.RUnlock()
           83  +	if !mgr.started {
           84  +		return nil, box.ErrStopped
           85  +	}
           86  +	var result []domain.Zettel
           87  +	for i, p := range mgr.boxes {
           88  +		if z, err := p.GetZettel(ctx, zid); err == nil {
           89  +			mgr.Enrich(ctx, z.Meta, i+1)
           90  +			result = append(result, z)
           91  +		}
           92  +	}
           93  +	return result, nil
           94  +}
           95  +
           96  +// GetMeta retrieves just the meta data of a specific zettel.
           97  +func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
           98  +	mgr.mgrMx.RLock()
           99  +	defer mgr.mgrMx.RUnlock()
          100  +	if !mgr.started {
          101  +		return nil, box.ErrStopped
          102  +	}
          103  +	for i, p := range mgr.boxes {
          104  +		if m, err := p.GetMeta(ctx, zid); err != box.ErrNotFound {
          105  +			if err == nil {
          106  +				mgr.Enrich(ctx, m, i+1)
          107  +			}
          108  +			return m, err
          109  +		}
          110  +	}
          111  +	return nil, box.ErrNotFound
          112  +}
          113  +
          114  +// GetAllMeta retrieves the meta data of a specific zettel from all managed boxes.
          115  +func (mgr *Manager) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) {
          116  +	mgr.mgrMx.RLock()
          117  +	defer mgr.mgrMx.RUnlock()
          118  +	if !mgr.started {
          119  +		return nil, box.ErrStopped
          120  +	}
          121  +	var result []*meta.Meta
          122  +	for i, p := range mgr.boxes {
          123  +		if m, err := p.GetMeta(ctx, zid); err == nil {
          124  +			mgr.Enrich(ctx, m, i+1)
          125  +			result = append(result, m)
          126  +		}
          127  +	}
          128  +	return result, nil
          129  +}
          130  +
          131  +// FetchZids returns the set of all zettel identifer managed by the box.
          132  +func (mgr *Manager) FetchZids(ctx context.Context) (result id.Set, err error) {
          133  +	mgr.mgrMx.RLock()
          134  +	defer mgr.mgrMx.RUnlock()
          135  +	if !mgr.started {
          136  +		return nil, box.ErrStopped
          137  +	}
          138  +	for _, p := range mgr.boxes {
          139  +		zids, err := p.FetchZids(ctx)
          140  +		if err != nil {
          141  +			return nil, err
          142  +		}
          143  +		if result == nil {
          144  +			result = zids
          145  +		} else if len(result) <= len(zids) {
          146  +			for zid := range result {
          147  +				zids[zid] = true
          148  +			}
          149  +			result = zids
          150  +		} else {
          151  +			for zid := range zids {
          152  +				result[zid] = true
          153  +			}
          154  +		}
          155  +	}
          156  +	return result, nil
          157  +}
          158  +
          159  +// SelectMeta returns all zettel meta data that match the selection
          160  +// criteria. The result is ordered by descending zettel id.
          161  +func (mgr *Manager) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) {
          162  +	mgr.mgrMx.RLock()
          163  +	defer mgr.mgrMx.RUnlock()
          164  +	if !mgr.started {
          165  +		return nil, box.ErrStopped
          166  +	}
          167  +	var result []*meta.Meta
          168  +	match := s.CompileMatch(mgr)
          169  +	for _, p := range mgr.boxes {
          170  +		selected, err := p.SelectMeta(ctx, match)
          171  +		if err != nil {
          172  +			return nil, err
          173  +		}
          174  +		sort.Slice(selected, func(i, j int) bool { return selected[i].Zid > selected[j].Zid })
          175  +		if len(result) == 0 {
          176  +			result = selected
          177  +		} else {
          178  +			result = box.MergeSorted(result, selected)
          179  +		}
          180  +	}
          181  +	if s == nil {
          182  +		return result, nil
          183  +	}
          184  +	return s.Sort(result), nil
          185  +}
          186  +
          187  +// CanUpdateZettel returns true, if box could possibly update the given zettel.
          188  +func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
          189  +	mgr.mgrMx.RLock()
          190  +	defer mgr.mgrMx.RUnlock()
          191  +	return mgr.started && mgr.boxes[0].CanUpdateZettel(ctx, zettel)
          192  +}
          193  +
          194  +// UpdateZettel updates an existing zettel.
          195  +func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
          196  +	mgr.mgrMx.RLock()
          197  +	defer mgr.mgrMx.RUnlock()
          198  +	if !mgr.started {
          199  +		return box.ErrStopped
          200  +	}
          201  +	// Remove all (computed) properties from metadata before storing the zettel.
          202  +	zettel.Meta = zettel.Meta.Clone()
          203  +	for _, p := range zettel.Meta.PairsRest(true) {
          204  +		if mgr.propertyKeys[p.Key] {
          205  +			zettel.Meta.Delete(p.Key)
          206  +		}
          207  +	}
          208  +	return mgr.boxes[0].UpdateZettel(ctx, zettel)
          209  +}
          210  +
          211  +// AllowRenameZettel returns true, if box will not disallow renaming the zettel.
          212  +func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
          213  +	mgr.mgrMx.RLock()
          214  +	defer mgr.mgrMx.RUnlock()
          215  +	if !mgr.started {
          216  +		return false
          217  +	}
          218  +	for _, p := range mgr.boxes {
          219  +		if !p.AllowRenameZettel(ctx, zid) {
          220  +			return false
          221  +		}
          222  +	}
          223  +	return true
          224  +}
          225  +
          226  +// RenameZettel changes the current zid to a new zid.
          227  +func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
          228  +	mgr.mgrMx.RLock()
          229  +	defer mgr.mgrMx.RUnlock()
          230  +	if !mgr.started {
          231  +		return box.ErrStopped
          232  +	}
          233  +	for i, p := range mgr.boxes {
          234  +		err := p.RenameZettel(ctx, curZid, newZid)
          235  +		if err != nil && !errors.Is(err, box.ErrNotFound) {
          236  +			for j := 0; j < i; j++ {
          237  +				mgr.boxes[j].RenameZettel(ctx, newZid, curZid)
          238  +			}
          239  +			return err
          240  +		}
          241  +	}
          242  +	return nil
          243  +}
          244  +
          245  +// CanDeleteZettel returns true, if box could possibly delete the given zettel.
          246  +func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
          247  +	mgr.mgrMx.RLock()
          248  +	defer mgr.mgrMx.RUnlock()
          249  +	if !mgr.started {
          250  +		return false
          251  +	}
          252  +	for _, p := range mgr.boxes {
          253  +		if p.CanDeleteZettel(ctx, zid) {
          254  +			return true
          255  +		}
          256  +	}
          257  +	return false
          258  +}
          259  +
          260  +// DeleteZettel removes the zettel from the box.
          261  +func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error {
          262  +	mgr.mgrMx.RLock()
          263  +	defer mgr.mgrMx.RUnlock()
          264  +	if !mgr.started {
          265  +		return box.ErrStopped
          266  +	}
          267  +	for _, p := range mgr.boxes {
          268  +		err := p.DeleteZettel(ctx, zid)
          269  +		if err == nil {
          270  +			return nil
          271  +		}
          272  +		if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) {
          273  +			return err
          274  +		}
          275  +	}
          276  +	return box.ErrNotFound
          277  +}

Added box/manager/collect.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package manager coordinates the various boxes and indexes of a Zettelstore.
           12  +package manager
           13  +
           14  +import (
           15  +	"strings"
           16  +
           17  +	"zettelstore.de/z/ast"
           18  +	"zettelstore.de/z/box/manager/store"
           19  +	"zettelstore.de/z/domain/id"
           20  +	"zettelstore.de/z/strfun"
           21  +)
           22  +
           23  +type collectData struct {
           24  +	refs  id.Set
           25  +	words store.WordSet
           26  +	urls  store.WordSet
           27  +}
           28  +
           29  +func (data *collectData) initialize() {
           30  +	data.refs = id.NewSet()
           31  +	data.words = store.NewWordSet()
           32  +	data.urls = store.NewWordSet()
           33  +}
           34  +
           35  +func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) {
           36  +	ast.WalkBlockSlice(data, zn.Ast)
           37  +}
           38  +
           39  +func collectInlineIndexData(ins ast.InlineSlice, data *collectData) {
           40  +	ast.WalkInlineSlice(data, ins)
           41  +}
           42  +
           43  +func (data *collectData) Visit(node ast.Node) ast.Visitor {
           44  +	switch n := node.(type) {
           45  +	case *ast.VerbatimNode:
           46  +		for _, line := range n.Lines {
           47  +			data.addText(line)
           48  +		}
           49  +	case *ast.TextNode:
           50  +		data.addText(n.Text)
           51  +	case *ast.TagNode:
           52  +		data.addText(n.Tag)
           53  +	case *ast.LinkNode:
           54  +		data.addRef(n.Ref)
           55  +	case *ast.ImageNode:
           56  +		data.addRef(n.Ref)
           57  +	case *ast.LiteralNode:
           58  +		data.addText(n.Text)
           59  +	}
           60  +	return data
           61  +}
           62  +
           63  +func (data *collectData) addText(s string) {
           64  +	for _, word := range strfun.NormalizeWords(s) {
           65  +		data.words.Add(word)
           66  +	}
           67  +}
           68  +
           69  +func (data *collectData) addRef(ref *ast.Reference) {
           70  +	if ref == nil {
           71  +		return
           72  +	}
           73  +	if ref.IsExternal() {
           74  +		data.urls.Add(strings.ToLower(ref.Value))
           75  +	}
           76  +	if !ref.IsZettel() {
           77  +		return
           78  +	}
           79  +	if zid, err := id.Parse(ref.URL.Path); err == nil {
           80  +		data.refs[zid] = true
           81  +	}
           82  +}

Added box/manager/enrich.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package manager coordinates the various boxes and indexes of a Zettelstore.
           12  +package manager
           13  +
           14  +import (
           15  +	"context"
           16  +	"strconv"
           17  +
           18  +	"zettelstore.de/z/box"
           19  +	"zettelstore.de/z/domain/meta"
           20  +)
           21  +
           22  +// Enrich computes additional properties and updates the given metadata.
           23  +func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) {
           24  +	if box.DoNotEnrich(ctx) {
           25  +		// Enrich is called indirectly via indexer or enrichment is not requested
           26  +		// because of other reasons -> ignore this call, do not update meta data
           27  +		return
           28  +	}
           29  +	m.Set(meta.KeyBoxNumber, strconv.Itoa(boxNumber))
           30  +	computePublished(m)
           31  +	mgr.idxStore.Enrich(ctx, m)
           32  +}
           33  +
           34  +func computePublished(m *meta.Meta) {
           35  +	if _, ok := m.Get(meta.KeyPublished); ok {
           36  +		return
           37  +	}
           38  +	if modified, ok := m.Get(meta.KeyModified); ok {
           39  +		if _, ok = meta.TimeValue(modified); ok {
           40  +			m.Set(meta.KeyPublished, modified)
           41  +			return
           42  +		}
           43  +	}
           44  +	zid := m.Zid.String()
           45  +	if _, ok := meta.TimeValue(zid); ok {
           46  +		m.Set(meta.KeyPublished, zid)
           47  +		return
           48  +	}
           49  +
           50  +	// Neither the zettel was modified nor the zettel identifer contains a valid
           51  +	// timestamp. In this case do not set the "published" property.
           52  +}

Added box/manager/indexer.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package manager coordinates the various boxes and indexes of a Zettelstore.
           12  +package manager
           13  +
           14  +import (
           15  +	"context"
           16  +	"net/url"
           17  +	"time"
           18  +
           19  +	"zettelstore.de/z/box"
           20  +	"zettelstore.de/z/box/manager/store"
           21  +	"zettelstore.de/z/domain"
           22  +	"zettelstore.de/z/domain/id"
           23  +	"zettelstore.de/z/domain/meta"
           24  +	"zettelstore.de/z/kernel"
           25  +	"zettelstore.de/z/parser"
           26  +	"zettelstore.de/z/strfun"
           27  +)
           28  +
           29  +// SearchEqual returns all zettel that contains the given exact word.
           30  +// The word must be normalized through Unicode NKFD, trimmed and not empty.
           31  +func (mgr *Manager) SearchEqual(word string) id.Set {
           32  +	return mgr.idxStore.SearchEqual(word)
           33  +}
           34  +
           35  +// SearchPrefix returns all zettel that have a word with the given prefix.
           36  +// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
           37  +func (mgr *Manager) SearchPrefix(prefix string) id.Set {
           38  +	return mgr.idxStore.SearchPrefix(prefix)
           39  +}
           40  +
           41  +// SearchSuffix returns all zettel that have a word with the given suffix.
           42  +// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
           43  +func (mgr *Manager) SearchSuffix(suffix string) id.Set {
           44  +	return mgr.idxStore.SearchSuffix(suffix)
           45  +}
           46  +
           47  +// SearchContains returns all zettel that contains the given string.
           48  +// The string must be normalized through Unicode NKFD, trimmed and not empty.
           49  +func (mgr *Manager) SearchContains(s string) id.Set {
           50  +	return mgr.idxStore.SearchContains(s)
           51  +}
           52  +
           53  +// idxIndexer runs in the background and updates the index data structures.
           54  +// This is the main service of the idxIndexer.
           55  +func (mgr *Manager) idxIndexer() {
           56  +	// Something may panic. Ensure a running indexer.
           57  +	defer func() {
           58  +		if r := recover(); r != nil {
           59  +			kernel.Main.LogRecover("Indexer", r)
           60  +			go mgr.idxIndexer()
           61  +		}
           62  +	}()
           63  +
           64  +	timerDuration := 15 * time.Second
           65  +	timer := time.NewTimer(timerDuration)
           66  +	ctx := box.NoEnrichContext(context.Background())
           67  +	for {
           68  +		mgr.idxWorkService(ctx)
           69  +		if !mgr.idxSleepService(timer, timerDuration) {
           70  +			return
           71  +		}
           72  +	}
           73  +}
           74  +
           75  +func (mgr *Manager) idxWorkService(ctx context.Context) {
           76  +	var roomNum uint64
           77  +	var start time.Time
           78  +	for {
           79  +		switch action, zid, arRoomNum := mgr.idxAr.Dequeue(); action {
           80  +		case arNothing:
           81  +			return
           82  +		case arReload:
           83  +			roomNum = 0
           84  +			zids, err := mgr.FetchZids(ctx)
           85  +			if err == nil {
           86  +				start = time.Now()
           87  +				if rno := mgr.idxAr.Reload(zids); rno > 0 {
           88  +					roomNum = rno
           89  +				}
           90  +				mgr.idxMx.Lock()
           91  +				mgr.idxLastReload = time.Now()
           92  +				mgr.idxSinceReload = 0
           93  +				mgr.idxMx.Unlock()
           94  +			}
           95  +		case arUpdate:
           96  +			zettel, err := mgr.GetZettel(ctx, zid)
           97  +			if err != nil {
           98  +				// TODO: on some errors put the zid into a "try later" set
           99  +				continue
          100  +			}
          101  +			mgr.idxMx.Lock()
          102  +			if arRoomNum == roomNum {
          103  +				mgr.idxDurReload = time.Since(start)
          104  +			}
          105  +			mgr.idxSinceReload++
          106  +			mgr.idxMx.Unlock()
          107  +			mgr.idxUpdateZettel(ctx, zettel)
          108  +		case arDelete:
          109  +			if _, err := mgr.GetMeta(ctx, zid); err == nil {
          110  +				// Zettel was not deleted. This might occur, if zettel was
          111  +				// deleted in secondary dirbox, but is still present in
          112  +				// first dirbox (or vice versa). Re-index zettel in case
          113  +				// a hidden zettel was recovered
          114  +				mgr.idxAr.Enqueue(zid, arUpdate)
          115  +			}
          116  +			mgr.idxMx.Lock()
          117  +			mgr.idxSinceReload++
          118  +			mgr.idxMx.Unlock()
          119  +			mgr.idxDeleteZettel(zid)
          120  +		}
          121  +	}
          122  +}
          123  +
          124  +func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool {
          125  +	select {
          126  +	case _, ok := <-mgr.idxReady:
          127  +		if !ok {
          128  +			return false
          129  +		}
          130  +	case _, ok := <-timer.C:
          131  +		if !ok {
          132  +			return false
          133  +		}
          134  +		timer.Reset(timerDuration)
          135  +	case <-mgr.done:
          136  +		if !timer.Stop() {
          137  +			<-timer.C
          138  +		}
          139  +		return false
          140  +	}
          141  +	return true
          142  +}
          143  +
          144  +func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) {
          145  +	m := zettel.Meta
          146  +	if m.GetBool(meta.KeyNoIndex) {
          147  +		// Zettel maybe in index
          148  +		toCheck := mgr.idxStore.DeleteZettel(ctx, m.Zid)
          149  +		mgr.idxCheckZettel(toCheck)
          150  +		return
          151  +	}
          152  +
          153  +	var cData collectData
          154  +	cData.initialize()
          155  +	collectZettelIndexData(parser.ParseZettel(zettel, "", mgr.rtConfig), &cData)
          156  +	zi := store.NewZettelIndex(m.Zid)
          157  +	mgr.idxCollectFromMeta(ctx, m, zi, &cData)
          158  +	mgr.idxProcessData(ctx, zi, &cData)
          159  +	toCheck := mgr.idxStore.UpdateReferences(ctx, zi)
          160  +	mgr.idxCheckZettel(toCheck)
          161  +}
          162  +
          163  +func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) {
          164  +	for _, pair := range m.Pairs(false) {
          165  +		descr := meta.GetDescription(pair.Key)
          166  +		if descr.IsComputed() {
          167  +			continue
          168  +		}
          169  +		switch descr.Type {
          170  +		case meta.TypeID:
          171  +			mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi)
          172  +		case meta.TypeIDSet:
          173  +			for _, val := range meta.ListFromValue(pair.Value) {
          174  +				mgr.idxUpdateValue(ctx, descr.Inverse, val, zi)
          175  +			}
          176  +		case meta.TypeZettelmarkup:
          177  +			collectInlineIndexData(parser.ParseMetadata(pair.Value), cData)
          178  +		case meta.TypeURL:
          179  +			if _, err := url.Parse(pair.Value); err == nil {
          180  +				cData.urls.Add(pair.Value)
          181  +			}
          182  +		default:
          183  +			for _, word := range strfun.NormalizeWords(pair.Value) {
          184  +				cData.words.Add(word)
          185  +			}
          186  +		}
          187  +	}
          188  +}
          189  +
          190  +func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) {
          191  +	for ref := range cData.refs {
          192  +		if _, err := mgr.GetMeta(ctx, ref); err == nil {
          193  +			zi.AddBackRef(ref)
          194  +		} else {
          195  +			zi.AddDeadRef(ref)
          196  +		}
          197  +	}
          198  +	zi.SetWords(cData.words)
          199  +	zi.SetUrls(cData.urls)
          200  +}
          201  +
          202  +func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) {
          203  +	zid, err := id.Parse(value)
          204  +	if err != nil {
          205  +		return
          206  +	}
          207  +	if _, err := mgr.GetMeta(ctx, zid); err != nil {
          208  +		zi.AddDeadRef(zid)
          209  +		return
          210  +	}
          211  +	if inverseKey == "" {
          212  +		zi.AddBackRef(zid)
          213  +		return
          214  +	}
          215  +	zi.AddMetaRef(inverseKey, zid)
          216  +}
          217  +
          218  +func (mgr *Manager) idxDeleteZettel(zid id.Zid) {
          219  +	toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid)
          220  +	mgr.idxCheckZettel(toCheck)
          221  +}
          222  +
          223  +func (mgr *Manager) idxCheckZettel(s id.Set) {
          224  +	for zid := range s {
          225  +		mgr.idxAr.Enqueue(zid, arUpdate)
          226  +	}
          227  +}

Added box/manager/manager.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package manager coordinates the various boxes and indexes of a Zettelstore.
           12  +package manager
           13  +
           14  +import (
           15  +	"context"
           16  +	"io"
           17  +	"log"
           18  +	"net/url"
           19  +	"sort"
           20  +	"sync"
           21  +	"time"
           22  +
           23  +	"zettelstore.de/z/auth"
           24  +	"zettelstore.de/z/box"
           25  +	"zettelstore.de/z/box/manager/memstore"
           26  +	"zettelstore.de/z/box/manager/store"
           27  +	"zettelstore.de/z/config"
           28  +	"zettelstore.de/z/domain/id"
           29  +	"zettelstore.de/z/domain/meta"
           30  +	"zettelstore.de/z/kernel"
           31  +)
           32  +
           33  +// ConnectData contains all administration related values.
           34  +type ConnectData struct {
           35  +	Number   int // number of the box, starting with 1.
           36  +	Config   config.Config
           37  +	Enricher box.Enricher
           38  +	Notify   chan<- box.UpdateInfo
           39  +}
           40  +
           41  +// Connect returns a handle to the specified box.
           42  +func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) {
           43  +	if authManager.IsReadonly() {
           44  +		rawURL := u.String()
           45  +		// TODO: the following is wrong under some circumstances:
           46  +		// 1. fragment is set
           47  +		if q := u.Query(); len(q) == 0 {
           48  +			rawURL += "?readonly"
           49  +		} else if _, ok := q["readonly"]; !ok {
           50  +			rawURL += "&readonly"
           51  +		}
           52  +		var err error
           53  +		if u, err = url.Parse(rawURL); err != nil {
           54  +			return nil, err
           55  +		}
           56  +	}
           57  +
           58  +	if create, ok := registry[u.Scheme]; ok {
           59  +		return create(u, cdata)
           60  +	}
           61  +	return nil, &ErrInvalidScheme{u.Scheme}
           62  +}
           63  +
           64  +// ErrInvalidScheme is returned if there is no box with the given scheme.
           65  +type ErrInvalidScheme struct{ Scheme string }
           66  +
           67  +func (err *ErrInvalidScheme) Error() string { return "Invalid scheme: " + err.Scheme }
           68  +
           69  +type createFunc func(*url.URL, *ConnectData) (box.ManagedBox, error)
           70  +
           71  +var registry = map[string]createFunc{}
           72  +
           73  +// Register the encoder for later retrieval.
           74  +func Register(scheme string, create createFunc) {
           75  +	if _, ok := registry[scheme]; ok {
           76  +		log.Fatalf("Box with scheme %q already registered", scheme)
           77  +	}
           78  +	registry[scheme] = create
           79  +}
           80  +
           81  +// GetSchemes returns all registered scheme, ordered by scheme string.
           82  +func GetSchemes() []string {
           83  +	result := make([]string, 0, len(registry))
           84  +	for scheme := range registry {
           85  +		result = append(result, scheme)
           86  +	}
           87  +	sort.Strings(result)
           88  +	return result
           89  +}
           90  +
           91  +// Manager is a coordinating box.
           92  +type Manager struct {
           93  +	mgrMx        sync.RWMutex
           94  +	started      bool
           95  +	rtConfig     config.Config
           96  +	boxes        []box.ManagedBox
           97  +	observers    []box.UpdateFunc
           98  +	mxObserver   sync.RWMutex
           99  +	done         chan struct{}
          100  +	infos        chan box.UpdateInfo
          101  +	propertyKeys map[string]bool // Set of property key names
          102  +
          103  +	// Indexer data
          104  +	idxStore store.Store
          105  +	idxAr    *anterooms
          106  +	idxReady chan struct{} // Signal a non-empty anteroom to background task
          107  +
          108  +	// Indexer stats data
          109  +	idxMx          sync.RWMutex
          110  +	idxLastReload  time.Time
          111  +	idxDurReload   time.Duration
          112  +	idxSinceReload uint64
          113  +}
          114  +
          115  +// New creates a new managing box.
          116  +func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) {
          117  +	propertyKeys := make(map[string]bool)
          118  +	for _, kd := range meta.GetSortedKeyDescriptions() {
          119  +		if kd.IsProperty() {
          120  +			propertyKeys[kd.Name] = true
          121  +		}
          122  +	}
          123  +	mgr := &Manager{
          124  +		rtConfig:     rtConfig,
          125  +		infos:        make(chan box.UpdateInfo, len(boxURIs)*10),
          126  +		propertyKeys: propertyKeys,
          127  +
          128  +		idxStore: memstore.New(),
          129  +		idxAr:    newAnterooms(10),
          130  +		idxReady: make(chan struct{}, 1),
          131  +	}
          132  +	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos}
          133  +	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
          134  +	for _, uri := range boxURIs {
          135  +		p, err := Connect(uri, authManager, &cdata)
          136  +		if err != nil {
          137  +			return nil, err
          138  +		}
          139  +		if p != nil {
          140  +			boxes = append(boxes, p)
          141  +			cdata.Number++
          142  +		}
          143  +	}
          144  +	constbox, err := registry[" const"](nil, &cdata)
          145  +	if err != nil {
          146  +		return nil, err
          147  +	}
          148  +	cdata.Number++
          149  +	compbox, err := registry[" comp"](nil, &cdata)
          150  +	if err != nil {
          151  +		return nil, err
          152  +	}
          153  +	cdata.Number++
          154  +	boxes = append(boxes, constbox, compbox)
          155  +	mgr.boxes = boxes
          156  +	return mgr, nil
          157  +}
          158  +
          159  +// RegisterObserver registers an observer that will be notified
          160  +// if a zettel was found to be changed.
          161  +func (mgr *Manager) RegisterObserver(f box.UpdateFunc) {
          162  +	if f != nil {
          163  +		mgr.mxObserver.Lock()
          164  +		mgr.observers = append(mgr.observers, f)
          165  +		mgr.mxObserver.Unlock()
          166  +	}
          167  +}
          168  +
          169  +func (mgr *Manager) notifyObserver(ci *box.UpdateInfo) {
          170  +	mgr.mxObserver.RLock()
          171  +	observers := mgr.observers
          172  +	mgr.mxObserver.RUnlock()
          173  +	for _, ob := range observers {
          174  +		ob(*ci)
          175  +	}
          176  +}
          177  +
          178  +func (mgr *Manager) notifier() {
          179  +	// The call to notify may panic. Ensure a running notifier.
          180  +	defer func() {
          181  +		if r := recover(); r != nil {
          182  +			kernel.Main.LogRecover("Notifier", r)
          183  +			go mgr.notifier()
          184  +		}
          185  +	}()
          186  +
          187  +	for {
          188  +		select {
          189  +		case ci, ok := <-mgr.infos:
          190  +			if ok {
          191  +				mgr.idxEnqueue(ci.Reason, ci.Zid)
          192  +				if ci.Box == nil {
          193  +					ci.Box = mgr
          194  +				}
          195  +				mgr.notifyObserver(&ci)
          196  +			}
          197  +		case <-mgr.done:
          198  +			return
          199  +		}
          200  +	}
          201  +}
          202  +
          203  +func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) {
          204  +	switch reason {
          205  +	case box.OnReload:
          206  +		mgr.idxAr.Reset()
          207  +	case box.OnUpdate:
          208  +		mgr.idxAr.Enqueue(zid, arUpdate)
          209  +	case box.OnDelete:
          210  +		mgr.idxAr.Enqueue(zid, arDelete)
          211  +	default:
          212  +		return
          213  +	}
          214  +	select {
          215  +	case mgr.idxReady <- struct{}{}:
          216  +	default:
          217  +	}
          218  +}
          219  +
          220  +// Start the box. Now all other functions of the box are allowed.
          221  +// Starting an already started box is not allowed.
          222  +func (mgr *Manager) Start(ctx context.Context) error {
          223  +	mgr.mgrMx.Lock()
          224  +	if mgr.started {
          225  +		mgr.mgrMx.Unlock()
          226  +		return box.ErrStarted
          227  +	}
          228  +	for i := len(mgr.boxes) - 1; i >= 0; i-- {
          229  +		ssi, ok := mgr.boxes[i].(box.StartStopper)
          230  +		if !ok {
          231  +			continue
          232  +		}
          233  +		err := ssi.Start(ctx)
          234  +		if err == nil {
          235  +			continue
          236  +		}
          237  +		for j := i + 1; j < len(mgr.boxes); j++ {
          238  +			if ssj, ok := mgr.boxes[j].(box.StartStopper); ok {
          239  +				ssj.Stop(ctx)
          240  +			}
          241  +		}
          242  +		mgr.mgrMx.Unlock()
          243  +		return err
          244  +	}
          245  +	mgr.idxAr.Reset() // Ensure an initial index run
          246  +	mgr.done = make(chan struct{})
          247  +	go mgr.notifier()
          248  +	go mgr.idxIndexer()
          249  +
          250  +	// mgr.startIndexer(mgr)
          251  +	mgr.started = true
          252  +	mgr.mgrMx.Unlock()
          253  +	mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid}
          254  +	return nil
          255  +}
          256  +
          257  +// Stop the started box. Now only the Start() function is allowed.
          258  +func (mgr *Manager) Stop(ctx context.Context) error {
          259  +	mgr.mgrMx.Lock()
          260  +	defer mgr.mgrMx.Unlock()
          261  +	if !mgr.started {
          262  +		return box.ErrStopped
          263  +	}
          264  +	close(mgr.done)
          265  +	var err error
          266  +	for _, p := range mgr.boxes {
          267  +		if ss, ok := p.(box.StartStopper); ok {
          268  +			if err1 := ss.Stop(ctx); err1 != nil && err == nil {
          269  +				err = err1
          270  +			}
          271  +		}
          272  +	}
          273  +	mgr.started = false
          274  +	return err
          275  +}
          276  +
          277  +// ReadStats populates st with box statistics.
          278  +func (mgr *Manager) ReadStats(st *box.Stats) {
          279  +	mgr.mgrMx.RLock()
          280  +	defer mgr.mgrMx.RUnlock()
          281  +	subStats := make([]box.ManagedBoxStats, len(mgr.boxes))
          282  +	for i, p := range mgr.boxes {
          283  +		p.ReadStats(&subStats[i])
          284  +	}
          285  +
          286  +	st.ReadOnly = true
          287  +	sumZettel := 0
          288  +	for _, sst := range subStats {
          289  +		if !sst.ReadOnly {
          290  +			st.ReadOnly = false
          291  +		}
          292  +		sumZettel += sst.Zettel
          293  +	}
          294  +	st.NumManagedBoxes = len(mgr.boxes)
          295  +	st.ZettelTotal = sumZettel
          296  +
          297  +	var storeSt store.Stats
          298  +	mgr.idxMx.RLock()
          299  +	defer mgr.idxMx.RUnlock()
          300  +	mgr.idxStore.ReadStats(&storeSt)
          301  +
          302  +	st.LastReload = mgr.idxLastReload
          303  +	st.IndexesSinceReload = mgr.idxSinceReload
          304  +	st.DurLastReload = mgr.idxDurReload
          305  +	st.ZettelIndexed = storeSt.Zettel
          306  +	st.IndexUpdates = storeSt.Updates
          307  +	st.IndexedWords = storeSt.Words
          308  +	st.IndexedUrls = storeSt.Urls
          309  +}
          310  +
          311  +// Dump internal data structures to a Writer.
          312  +func (mgr *Manager) Dump(w io.Writer) {
          313  +	mgr.idxStore.Dump(w)
          314  +}

Added box/manager/memstore/memstore.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package memstore stored the index in main memory.
           12  +package memstore
           13  +
           14  +import (
           15  +	"context"
           16  +	"fmt"
           17  +	"io"
           18  +	"sort"
           19  +	"strings"
           20  +	"sync"
           21  +
           22  +	"zettelstore.de/z/box/manager/store"
           23  +	"zettelstore.de/z/domain/id"
           24  +	"zettelstore.de/z/domain/meta"
           25  +)
           26  +
           27  +type metaRefs struct {
           28  +	forward  id.Slice
           29  +	backward id.Slice
           30  +}
           31  +
           32  +type zettelIndex struct {
           33  +	dead     id.Slice
           34  +	forward  id.Slice
           35  +	backward id.Slice
           36  +	meta     map[string]metaRefs
           37  +	words    []string
           38  +	urls     []string
           39  +}
           40  +
           41  +func (zi *zettelIndex) isEmpty() bool {
           42  +	if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 {
           43  +		return false
           44  +	}
           45  +	return zi.meta == nil || len(zi.meta) == 0
           46  +}
           47  +
           48  +type stringRefs map[string]id.Slice
           49  +
           50  +type memStore struct {
           51  +	mx    sync.RWMutex
           52  +	idx   map[id.Zid]*zettelIndex
           53  +	dead  map[id.Zid]id.Slice // map dead refs where they occur
           54  +	words stringRefs
           55  +	urls  stringRefs
           56  +
           57  +	// Stats
           58  +	updates uint64
           59  +}
           60  +
           61  +// New returns a new memory-based index store.
           62  +func New() store.Store {
           63  +	return &memStore{
           64  +		idx:   make(map[id.Zid]*zettelIndex),
           65  +		dead:  make(map[id.Zid]id.Slice),
           66  +		words: make(stringRefs),
           67  +		urls:  make(stringRefs),
           68  +	}
           69  +}
           70  +
           71  +func (ms *memStore) Enrich(ctx context.Context, m *meta.Meta) {
           72  +	if ms.doEnrich(ctx, m) {
           73  +		ms.mx.Lock()
           74  +		ms.updates++
           75  +		ms.mx.Unlock()
           76  +	}
           77  +}
           78  +
           79  +func (ms *memStore) doEnrich(ctx context.Context, m *meta.Meta) bool {
           80  +	ms.mx.RLock()
           81  +	defer ms.mx.RUnlock()
           82  +	zi, ok := ms.idx[m.Zid]
           83  +	if !ok {
           84  +		return false
           85  +	}
           86  +	var updated bool
           87  +	if len(zi.dead) > 0 {
           88  +		m.Set(meta.KeyDead, zi.dead.String())
           89  +		updated = true
           90  +	}
           91  +	back := removeOtherMetaRefs(m, zi.backward.Copy())
           92  +	if len(zi.backward) > 0 {
           93  +		m.Set(meta.KeyBackward, zi.backward.String())
           94  +		updated = true
           95  +	}
           96  +	if len(zi.forward) > 0 {
           97  +		m.Set(meta.KeyForward, zi.forward.String())
           98  +		back = remRefs(back, zi.forward)
           99  +		updated = true
          100  +	}
          101  +	if len(zi.meta) > 0 {
          102  +		for k, refs := range zi.meta {
          103  +			if len(refs.backward) > 0 {
          104  +				m.Set(k, refs.backward.String())
          105  +				back = remRefs(back, refs.backward)
          106  +				updated = true
          107  +			}
          108  +		}
          109  +	}
          110  +	if len(back) > 0 {
          111  +		m.Set(meta.KeyBack, back.String())
          112  +		updated = true
          113  +	}
          114  +	return updated
          115  +}
          116  +
          117  +// SearchEqual returns all zettel that contains the given exact word.
          118  +// The word must be normalized through Unicode NKFD, trimmed and not empty.
          119  +func (ms *memStore) SearchEqual(word string) id.Set {
          120  +	ms.mx.RLock()
          121  +	defer ms.mx.RUnlock()
          122  +	result := id.NewSet()
          123  +	if refs, ok := ms.words[word]; ok {
          124  +		result.AddSlice(refs)
          125  +	}
          126  +	if refs, ok := ms.urls[word]; ok {
          127  +		result.AddSlice(refs)
          128  +	}
          129  +	zid, err := id.Parse(word)
          130  +	if err != nil {
          131  +		return result
          132  +	}
          133  +	zi, ok := ms.idx[zid]
          134  +	if !ok {
          135  +		return result
          136  +	}
          137  +
          138  +	addBackwardZids(result, zid, zi)
          139  +	return result
          140  +}
          141  +
          142  +// SearchPrefix returns all zettel that have a word with the given prefix.
          143  +// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
          144  +func (ms *memStore) SearchPrefix(prefix string) id.Set {
          145  +	ms.mx.RLock()
          146  +	defer ms.mx.RUnlock()
          147  +	result := ms.selectWithPred(prefix, strings.HasPrefix)
          148  +	l := len(prefix)
          149  +	if l > 14 {
          150  +		return result
          151  +	}
          152  +	minZid, err := id.Parse(prefix + "00000000000000"[:14-l])
          153  +	if err != nil {
          154  +		return result
          155  +	}
          156  +	maxZid, err := id.Parse(prefix + "99999999999999"[:14-l])
          157  +	if err != nil {
          158  +		return result
          159  +	}
          160  +	for zid, zi := range ms.idx {
          161  +		if minZid <= zid && zid <= maxZid {
          162  +			addBackwardZids(result, zid, zi)
          163  +		}
          164  +	}
          165  +	return result
          166  +}
          167  +
          168  +// SearchSuffix returns all zettel that have a word with the given suffix.
          169  +// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
          170  +func (ms *memStore) SearchSuffix(suffix string) id.Set {
          171  +	ms.mx.RLock()
          172  +	defer ms.mx.RUnlock()
          173  +	result := ms.selectWithPred(suffix, strings.HasSuffix)
          174  +	l := len(suffix)
          175  +	if l > 14 {
          176  +		return result
          177  +	}
          178  +	val, err := id.ParseUint(suffix)
          179  +	if err != nil {
          180  +		return result
          181  +	}
          182  +	modulo := uint64(1)
          183  +	for i := 0; i < l; i++ {
          184  +		modulo *= 10
          185  +	}
          186  +	for zid, zi := range ms.idx {
          187  +		if uint64(zid)%modulo == val {
          188  +			addBackwardZids(result, zid, zi)
          189  +		}
          190  +	}
          191  +	return result
          192  +}
          193  +
          194  +// SearchContains returns all zettel that contains the given string.
          195  +// The string must be normalized through Unicode NKFD, trimmed and not empty.
          196  +func (ms *memStore) SearchContains(s string) id.Set {
          197  +	ms.mx.RLock()
          198  +	defer ms.mx.RUnlock()
          199  +	result := ms.selectWithPred(s, strings.Contains)
          200  +	if len(s) > 14 {
          201  +		return result
          202  +	}
          203  +	if _, err := id.ParseUint(s); err != nil {
          204  +		return result
          205  +	}
          206  +	for zid, zi := range ms.idx {
          207  +		if strings.Contains(zid.String(), s) {
          208  +			addBackwardZids(result, zid, zi)
          209  +		}
          210  +	}
          211  +	return result
          212  +}
          213  +
          214  +func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set {
          215  +	// Must only be called if ms.mx is read-locked!
          216  +	result := id.NewSet()
          217  +	for word, refs := range ms.words {
          218  +		if !pred(word, s) {
          219  +			continue
          220  +		}
          221  +		result.AddSlice(refs)
          222  +	}
          223  +	for u, refs := range ms.urls {
          224  +		if !pred(u, s) {
          225  +			continue
          226  +		}
          227  +		result.AddSlice(refs)
          228  +	}
          229  +	return result
          230  +}
          231  +
          232  +func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) {
          233  +	// Must only be called if ms.mx is read-locked!
          234  +	result[zid] = true
          235  +	result.AddSlice(zi.backward)
          236  +	for _, mref := range zi.meta {
          237  +		result.AddSlice(mref.backward)
          238  +	}
          239  +}
          240  +
          241  +func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice {
          242  +	for _, p := range m.PairsRest(false) {
          243  +		switch meta.Type(p.Key) {
          244  +		case meta.TypeID:
          245  +			if zid, err := id.Parse(p.Value); err == nil {
          246  +				back = remRef(back, zid)
          247  +			}
          248  +		case meta.TypeIDSet:
          249  +			for _, val := range meta.ListFromValue(p.Value) {
          250  +				if zid, err := id.Parse(val); err == nil {
          251  +					back = remRef(back, zid)
          252  +				}
          253  +			}
          254  +		}
          255  +	}
          256  +	return back
          257  +}
          258  +
          259  +func (ms *memStore) UpdateReferences(ctx context.Context, zidx *store.ZettelIndex) id.Set {
          260  +	ms.mx.Lock()
          261  +	defer ms.mx.Unlock()
          262  +	zi, ziExist := ms.idx[zidx.Zid]
          263  +	if !ziExist || zi == nil {
          264  +		zi = &zettelIndex{}
          265  +		ziExist = false
          266  +	}
          267  +
          268  +	// Is this zettel an old dead reference mentioned in other zettel?
          269  +	var toCheck id.Set
          270  +	if refs, ok := ms.dead[zidx.Zid]; ok {
          271  +		// These must be checked later again
          272  +		toCheck = id.NewSet(refs...)
          273  +		delete(ms.dead, zidx.Zid)
          274  +	}
          275  +
          276  +	ms.updateDeadReferences(zidx, zi)
          277  +	ms.updateForwardBackwardReferences(zidx, zi)
          278  +	ms.updateMetadataReferences(zidx, zi)
          279  +	zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords())
          280  +	zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls())
          281  +
          282  +	// Check if zi must be inserted into ms.idx
          283  +	if !ziExist && !zi.isEmpty() {
          284  +		ms.idx[zidx.Zid] = zi
          285  +	}
          286  +
          287  +	return toCheck
          288  +}
          289  +
          290  +func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) {
          291  +	// Must only be called if ms.mx is write-locked!
          292  +	drefs := zidx.GetDeadRefs()
          293  +	newRefs, remRefs := refsDiff(drefs, zi.dead)
          294  +	zi.dead = drefs
          295  +	for _, ref := range remRefs {
          296  +		ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid)
          297  +	}
          298  +	for _, ref := range newRefs {
          299  +		ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid)
          300  +	}
          301  +}
          302  +
          303  +func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) {
          304  +	// Must only be called if ms.mx is write-locked!
          305  +	brefs := zidx.GetBackRefs()
          306  +	newRefs, remRefs := refsDiff(brefs, zi.forward)
          307  +	zi.forward = brefs
          308  +	for _, ref := range remRefs {
          309  +		bzi := ms.getEntry(ref)
          310  +		bzi.backward = remRef(bzi.backward, zidx.Zid)
          311  +	}
          312  +	for _, ref := range newRefs {
          313  +		bzi := ms.getEntry(ref)
          314  +		bzi.backward = addRef(bzi.backward, zidx.Zid)
          315  +	}
          316  +}
          317  +
          318  +func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) {
          319  +	// Must only be called if ms.mx is write-locked!
          320  +	metarefs := zidx.GetMetaRefs()
          321  +	for key, mr := range zi.meta {
          322  +		if _, ok := metarefs[key]; ok {
          323  +			continue
          324  +		}
          325  +		ms.removeInverseMeta(zidx.Zid, key, mr.forward)
          326  +	}
          327  +	if zi.meta == nil {
          328  +		zi.meta = make(map[string]metaRefs)
          329  +	}
          330  +	for key, mrefs := range metarefs {
          331  +		mr := zi.meta[key]
          332  +		newRefs, remRefs := refsDiff(mrefs, mr.forward)
          333  +		mr.forward = mrefs
          334  +		zi.meta[key] = mr
          335  +
          336  +		for _, ref := range newRefs {
          337  +			bzi := ms.getEntry(ref)
          338  +			if bzi.meta == nil {
          339  +				bzi.meta = make(map[string]metaRefs)
          340  +			}
          341  +			bmr := bzi.meta[key]
          342  +			bmr.backward = addRef(bmr.backward, zidx.Zid)
          343  +			bzi.meta[key] = bmr
          344  +		}
          345  +		ms.removeInverseMeta(zidx.Zid, key, remRefs)
          346  +	}
          347  +}
          348  +
          349  +func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string {
          350  +	// Must only be called if ms.mx is write-locked!
          351  +	newWords, removeWords := next.Diff(prev)
          352  +	for _, word := range newWords {
          353  +		if refs, ok := srefs[word]; ok {
          354  +			srefs[word] = addRef(refs, zid)
          355  +			continue
          356  +		}
          357  +		srefs[word] = id.Slice{zid}
          358  +	}
          359  +	for _, word := range removeWords {
          360  +		refs, ok := srefs[word]
          361  +		if !ok {
          362  +			continue
          363  +		}
          364  +		refs2 := remRef(refs, zid)
          365  +		if len(refs2) == 0 {
          366  +			delete(srefs, word)
          367  +			continue
          368  +		}
          369  +		srefs[word] = refs2
          370  +	}
          371  +	return next.Words()
          372  +}
          373  +
          374  +func (ms *memStore) getEntry(zid id.Zid) *zettelIndex {
          375  +	// Must only be called if ms.mx is write-locked!
          376  +	if zi, ok := ms.idx[zid]; ok {
          377  +		return zi
          378  +	}
          379  +	zi := &zettelIndex{}
          380  +	ms.idx[zid] = zi
          381  +	return zi
          382  +}
          383  +
          384  +func (ms *memStore) DeleteZettel(ctx context.Context, zid id.Zid) id.Set {
          385  +	ms.mx.Lock()
          386  +	defer ms.mx.Unlock()
          387  +
          388  +	zi, ok := ms.idx[zid]
          389  +	if !ok {
          390  +		return nil
          391  +	}
          392  +
          393  +	ms.deleteDeadSources(zid, zi)
          394  +	toCheck := ms.deleteForwardBackward(zid, zi)
          395  +	if len(zi.meta) > 0 {
          396  +		for key, mrefs := range zi.meta {
          397  +			ms.removeInverseMeta(zid, key, mrefs.forward)
          398  +		}
          399  +	}
          400  +	ms.deleteWords(zid, zi.words)
          401  +	delete(ms.idx, zid)
          402  +	return toCheck
          403  +}
          404  +
          405  +func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) {
          406  +	// Must only be called if ms.mx is write-locked!
          407  +	for _, ref := range zi.dead {
          408  +		if drefs, ok := ms.dead[ref]; ok {
          409  +			drefs = remRef(drefs, zid)
          410  +			if len(drefs) > 0 {
          411  +				ms.dead[ref] = drefs
          412  +			} else {
          413  +				delete(ms.dead, ref)
          414  +			}
          415  +		}
          416  +	}
          417  +}
          418  +
          419  +func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set {
          420  +	// Must only be called if ms.mx is write-locked!
          421  +	var toCheck id.Set
          422  +	for _, ref := range zi.forward {
          423  +		if fzi, ok := ms.idx[ref]; ok {
          424  +			fzi.backward = remRef(fzi.backward, zid)
          425  +		}
          426  +	}
          427  +	for _, ref := range zi.backward {
          428  +		if bzi, ok := ms.idx[ref]; ok {
          429  +			bzi.forward = remRef(bzi.forward, zid)
          430  +			if toCheck == nil {
          431  +				toCheck = id.NewSet()
          432  +			}
          433  +			toCheck[ref] = true
          434  +		}
          435  +	}
          436  +	return toCheck
          437  +}
          438  +
          439  +func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) {
          440  +	// Must only be called if ms.mx is write-locked!
          441  +	for _, ref := range forward {
          442  +		bzi, ok := ms.idx[ref]
          443  +		if !ok || bzi.meta == nil {
          444  +			continue
          445  +		}
          446  +		bmr, ok := bzi.meta[key]
          447  +		if !ok {
          448  +			continue
          449  +		}
          450  +		bmr.backward = remRef(bmr.backward, zid)
          451  +		if len(bmr.backward) > 0 || len(bmr.forward) > 0 {
          452  +			bzi.meta[key] = bmr
          453  +		} else {
          454  +			delete(bzi.meta, key)
          455  +			if len(bzi.meta) == 0 {
          456  +				bzi.meta = nil
          457  +			}
          458  +		}
          459  +	}
          460  +}
          461  +
          462  +func (ms *memStore) deleteWords(zid id.Zid, words []string) {
          463  +	// Must only be called if ms.mx is write-locked!
          464  +	for _, word := range words {
          465  +		refs, ok := ms.words[word]
          466  +		if !ok {
          467  +			continue
          468  +		}
          469  +		refs2 := remRef(refs, zid)
          470  +		if len(refs2) == 0 {
          471  +			delete(ms.words, word)
          472  +			continue
          473  +		}
          474  +		ms.words[word] = refs2
          475  +	}
          476  +}
          477  +
          478  +func (ms *memStore) ReadStats(st *store.Stats) {
          479  +	ms.mx.RLock()
          480  +	st.Zettel = len(ms.idx)
          481  +	st.Updates = ms.updates
          482  +	st.Words = uint64(len(ms.words))
          483  +	st.Urls = uint64(len(ms.urls))
          484  +	ms.mx.RUnlock()
          485  +}
          486  +
          487  +func (ms *memStore) Dump(w io.Writer) {
          488  +	ms.mx.RLock()
          489  +	defer ms.mx.RUnlock()
          490  +
          491  +	io.WriteString(w, "=== Dump\n")
          492  +	ms.dumpIndex(w)
          493  +	ms.dumpDead(w)
          494  +	dumpStringRefs(w, "Words", "", "", ms.words)
          495  +	dumpStringRefs(w, "URLs", "[[", "]]", ms.urls)
          496  +}
          497  +
          498  +func (ms *memStore) dumpIndex(w io.Writer) {
          499  +	if len(ms.idx) == 0 {
          500  +		return
          501  +	}
          502  +	io.WriteString(w, "==== Zettel Index\n")
          503  +	zids := make(id.Slice, 0, len(ms.idx))
          504  +	for id := range ms.idx {
          505  +		zids = append(zids, id)
          506  +	}
          507  +	zids.Sort()
          508  +	for _, id := range zids {
          509  +		fmt.Fprintln(w, "=====", id)
          510  +		zi := ms.idx[id]
          511  +		if len(zi.dead) > 0 {
          512  +			fmt.Fprintln(w, "* Dead:", zi.dead)
          513  +		}
          514  +		dumpZids(w, "* Forward:", zi.forward)
          515  +		dumpZids(w, "* Backward:", zi.backward)
          516  +		for k, fb := range zi.meta {
          517  +			fmt.Fprintln(w, "* Meta", k)
          518  +			dumpZids(w, "** Forward:", fb.forward)
          519  +			dumpZids(w, "** Backward:", fb.backward)
          520  +		}
          521  +		dumpStrings(w, "* Words", "", "", zi.words)
          522  +		dumpStrings(w, "* URLs", "[[", "]]", zi.urls)
          523  +	}
          524  +}
          525  +
          526  +func (ms *memStore) dumpDead(w io.Writer) {
          527  +	if len(ms.dead) == 0 {
          528  +		return
          529  +	}
          530  +	fmt.Fprintf(w, "==== Dead References\n")
          531  +	zids := make(id.Slice, 0, len(ms.dead))
          532  +	for id := range ms.dead {
          533  +		zids = append(zids, id)
          534  +	}
          535  +	zids.Sort()
          536  +	for _, id := range zids {
          537  +		fmt.Fprintln(w, ";", id)
          538  +		fmt.Fprintln(w, ":", ms.dead[id])
          539  +	}
          540  +}
          541  +
          542  +func dumpZids(w io.Writer, prefix string, zids id.Slice) {
          543  +	if len(zids) > 0 {
          544  +		io.WriteString(w, prefix)
          545  +		for _, zid := range zids {
          546  +			io.WriteString(w, " ")
          547  +			w.Write(zid.Bytes())
          548  +		}
          549  +		fmt.Fprintln(w)
          550  +	}
          551  +}
          552  +
          553  +func dumpStrings(w io.Writer, title, preString, postString string, slice []string) {
          554  +	if len(slice) > 0 {
          555  +		sl := make([]string, len(slice))
          556  +		copy(sl, slice)
          557  +		sort.Strings(sl)
          558  +		fmt.Fprintln(w, title)
          559  +		for _, s := range sl {
          560  +			fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString)
          561  +		}
          562  +	}
          563  +
          564  +}
          565  +
          566  +func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) {
          567  +	if len(srefs) == 0 {
          568  +		return
          569  +	}
          570  +	fmt.Fprintln(w, "====", title)
          571  +	slice := make([]string, 0, len(srefs))
          572  +	for s := range srefs {
          573  +		slice = append(slice, s)
          574  +	}
          575  +	sort.Strings(slice)
          576  +	for _, s := range slice {
          577  +		fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString)
          578  +		fmt.Fprintln(w, ":", srefs[s])
          579  +	}
          580  +}

Added box/manager/memstore/refs.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package memstore stored the index in main memory.
           12  +package memstore
           13  +
           14  +import "zettelstore.de/z/domain/id"
           15  +
           16  +func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) {
           17  +	npos, opos := 0, 0
           18  +	for npos < len(refsN) && opos < len(refsO) {
           19  +		rn, ro := refsN[npos], refsO[opos]
           20  +		if rn == ro {
           21  +			npos++
           22  +			opos++
           23  +			continue
           24  +		}
           25  +		if rn < ro {
           26  +			newRefs = append(newRefs, rn)
           27  +			npos++
           28  +			continue
           29  +		}
           30  +		remRefs = append(remRefs, ro)
           31  +		opos++
           32  +	}
           33  +	if npos < len(refsN) {
           34  +		newRefs = append(newRefs, refsN[npos:]...)
           35  +	}
           36  +	if opos < len(refsO) {
           37  +		remRefs = append(remRefs, refsO[opos:]...)
           38  +	}
           39  +	return newRefs, remRefs
           40  +}
           41  +
           42  +func addRef(refs id.Slice, ref id.Zid) id.Slice {
           43  +	hi := len(refs)
           44  +	for lo := 0; lo < hi; {
           45  +		m := lo + (hi-lo)/2
           46  +		if r := refs[m]; r == ref {
           47  +			return refs
           48  +		} else if r < ref {
           49  +			lo = m + 1
           50  +		} else {
           51  +			hi = m
           52  +		}
           53  +	}
           54  +	refs = append(refs, id.Invalid)
           55  +	copy(refs[hi+1:], refs[hi:])
           56  +	refs[hi] = ref
           57  +	return refs
           58  +}
           59  +
           60  +func remRefs(refs, rem id.Slice) id.Slice {
           61  +	if len(refs) == 0 || len(rem) == 0 {
           62  +		return refs
           63  +	}
           64  +	result := make(id.Slice, 0, len(refs))
           65  +	rpos, dpos := 0, 0
           66  +	for rpos < len(refs) && dpos < len(rem) {
           67  +		rr, dr := refs[rpos], rem[dpos]
           68  +		if rr < dr {
           69  +			result = append(result, rr)
           70  +			rpos++
           71  +			continue
           72  +		}
           73  +		if dr < rr {
           74  +			dpos++
           75  +			continue
           76  +		}
           77  +		rpos++
           78  +		dpos++
           79  +	}
           80  +	if rpos < len(refs) {
           81  +		result = append(result, refs[rpos:]...)
           82  +	}
           83  +	return result
           84  +}
           85  +
           86  +func remRef(refs id.Slice, ref id.Zid) id.Slice {
           87  +	hi := len(refs)
           88  +	for lo := 0; lo < hi; {
           89  +		m := lo + (hi-lo)/2
           90  +		if r := refs[m]; r == ref {
           91  +			copy(refs[m:], refs[m+1:])
           92  +			refs = refs[:len(refs)-1]
           93  +			return refs
           94  +		} else if r < ref {
           95  +			lo = m + 1
           96  +		} else {
           97  +			hi = m
           98  +		}
           99  +	}
          100  +	return refs
          101  +}

Added box/manager/memstore/refs_test.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package memstore stored the index in main memory.
           12  +package memstore
           13  +
           14  +import (
           15  +	"testing"
           16  +
           17  +	"zettelstore.de/z/domain/id"
           18  +)
           19  +
           20  +func assertRefs(t *testing.T, i int, got, exp id.Slice) {
           21  +	t.Helper()
           22  +	if got == nil && exp != nil {
           23  +		t.Errorf("%d: got nil, but expected %v", i, exp)
           24  +		return
           25  +	}
           26  +	if got != nil && exp == nil {
           27  +		t.Errorf("%d: expected nil, but got %v", i, got)
           28  +		return
           29  +	}
           30  +	if len(got) != len(exp) {
           31  +		t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got))
           32  +		return
           33  +	}
           34  +	for p, n := range exp {
           35  +		if got := got[p]; got != id.Zid(n) {
           36  +			t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got)
           37  +		}
           38  +	}
           39  +}
           40  +
           41  +func TestRefsDiff(t *testing.T) {
           42  +	t.Parallel()
           43  +	testcases := []struct {
           44  +		in1, in2   id.Slice
           45  +		exp1, exp2 id.Slice
           46  +	}{
           47  +		{nil, nil, nil, nil},
           48  +		{id.Slice{1}, nil, id.Slice{1}, nil},
           49  +		{nil, id.Slice{1}, nil, id.Slice{1}},
           50  +		{id.Slice{1}, id.Slice{1}, nil, nil},
           51  +		{id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil},
           52  +		{id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}},
           53  +		{id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}},
           54  +	}
           55  +	for i, tc := range testcases {
           56  +		got1, got2 := refsDiff(tc.in1, tc.in2)
           57  +		assertRefs(t, i, got1, tc.exp1)
           58  +		assertRefs(t, i, got2, tc.exp2)
           59  +	}
           60  +}
           61  +
           62  +func TestAddRef(t *testing.T) {
           63  +	t.Parallel()
           64  +	testcases := []struct {
           65  +		ref id.Slice
           66  +		zid uint
           67  +		exp id.Slice
           68  +	}{
           69  +		{nil, 5, id.Slice{5}},
           70  +		{id.Slice{1}, 5, id.Slice{1, 5}},
           71  +		{id.Slice{10}, 5, id.Slice{5, 10}},
           72  +		{id.Slice{5}, 5, id.Slice{5}},
           73  +		{id.Slice{1, 10}, 5, id.Slice{1, 5, 10}},
           74  +		{id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}},
           75  +	}
           76  +	for i, tc := range testcases {
           77  +		got := addRef(tc.ref, id.Zid(tc.zid))
           78  +		assertRefs(t, i, got, tc.exp)
           79  +	}
           80  +}
           81  +
           82  +func TestRemRefs(t *testing.T) {
           83  +	t.Parallel()
           84  +	testcases := []struct {
           85  +		in1, in2 id.Slice
           86  +		exp      id.Slice
           87  +	}{
           88  +		{nil, nil, nil},
           89  +		{nil, id.Slice{}, nil},
           90  +		{id.Slice{}, nil, id.Slice{}},
           91  +		{id.Slice{}, id.Slice{}, id.Slice{}},
           92  +		{id.Slice{1}, id.Slice{5}, id.Slice{1}},
           93  +		{id.Slice{10}, id.Slice{5}, id.Slice{10}},
           94  +		{id.Slice{1, 5}, id.Slice{5}, id.Slice{1}},
           95  +		{id.Slice{5, 10}, id.Slice{5}, id.Slice{10}},
           96  +		{id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}},
           97  +		{id.Slice{1}, id.Slice{2, 5}, id.Slice{1}},
           98  +		{id.Slice{10}, id.Slice{2, 5}, id.Slice{10}},
           99  +		{id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}},
          100  +		{id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}},
          101  +		{id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}},
          102  +		{id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}},
          103  +		{id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}},
          104  +		{id.Slice{1}, id.Slice{5, 9}, id.Slice{1}},
          105  +		{id.Slice{10}, id.Slice{5, 9}, id.Slice{10}},
          106  +		{id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}},
          107  +		{id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}},
          108  +		{id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}},
          109  +		{id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}},
          110  +		{id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}},
          111  +	}
          112  +	for i, tc := range testcases {
          113  +		got := remRefs(tc.in1, tc.in2)
          114  +		assertRefs(t, i, got, tc.exp)
          115  +	}
          116  +}
          117  +
          118  +func TestRemRef(t *testing.T) {
          119  +	t.Parallel()
          120  +	testcases := []struct {
          121  +		ref id.Slice
          122  +		zid uint
          123  +		exp id.Slice
          124  +	}{
          125  +		{nil, 5, nil},
          126  +		{id.Slice{}, 5, id.Slice{}},
          127  +		{id.Slice{5}, 5, id.Slice{}},
          128  +		{id.Slice{1}, 5, id.Slice{1}},
          129  +		{id.Slice{10}, 5, id.Slice{10}},
          130  +		{id.Slice{1, 5}, 5, id.Slice{1}},
          131  +		{id.Slice{5, 10}, 5, id.Slice{10}},
          132  +		{id.Slice{1, 5, 10}, 5, id.Slice{1, 10}},
          133  +	}
          134  +	for i, tc := range testcases {
          135  +		got := remRef(tc.ref, id.Zid(tc.zid))
          136  +		assertRefs(t, i, got, tc.exp)
          137  +	}
          138  +}

Added box/manager/store/store.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package store contains general index data for storing a zettel index.
           12  +package store
           13  +
           14  +import (
           15  +	"context"
           16  +	"io"
           17  +
           18  +	"zettelstore.de/z/domain/id"
           19  +	"zettelstore.de/z/domain/meta"
           20  +	"zettelstore.de/z/search"
           21  +)
           22  +
           23  +// Stats records statistics about the store.
           24  +type Stats struct {
           25  +	// Zettel is the number of zettel managed by the indexer.
           26  +	Zettel int
           27  +
           28  +	// Updates count the number of metadata updates.
           29  +	Updates uint64
           30  +
           31  +	// Words count the different words stored in the store.
           32  +	Words uint64
           33  +
           34  +	// Urls count the different URLs stored in the store.
           35  +	Urls uint64
           36  +}
           37  +
           38  +// Store all relevant zettel data. There may be multiple implementations, i.e.
           39  +// memory-based, file-based, based on SQLite, ...
           40  +type Store interface {
           41  +	search.Searcher
           42  +
           43  +	// Entrich metadata with data from store.
           44  +	Enrich(ctx context.Context, m *meta.Meta)
           45  +
           46  +	// UpdateReferences for a specific zettel.
           47  +	// Returns set of zettel identifier that must also be checked for changes.
           48  +	UpdateReferences(context.Context, *ZettelIndex) id.Set
           49  +
           50  +	// DeleteZettel removes index data for given zettel.
           51  +	// Returns set of zettel identifier that must also be checked for changes.
           52  +	DeleteZettel(context.Context, id.Zid) id.Set
           53  +
           54  +	// ReadStats populates st with store statistics.
           55  +	ReadStats(st *Stats)
           56  +
           57  +	// Dump the content to a Writer.
           58  +	Dump(io.Writer)
           59  +}

Added box/manager/store/wordset.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package store contains general index data for storing a zettel index.
           12  +package store
           13  +
           14  +// WordSet contains the set of all words, with the count of their occurrences.
           15  +type WordSet map[string]int
           16  +
           17  +// NewWordSet returns a new WordSet.
           18  +func NewWordSet() WordSet { return make(WordSet) }
           19  +
           20  +// Add one word to the set
           21  +func (ws WordSet) Add(s string) {
           22  +	ws[s] = ws[s] + 1
           23  +}
           24  +
           25  +// Words gives the slice of all words in the set.
           26  +func (ws WordSet) Words() []string {
           27  +	if len(ws) == 0 {
           28  +		return nil
           29  +	}
           30  +	words := make([]string, 0, len(ws))
           31  +	for w := range ws {
           32  +		words = append(words, w)
           33  +	}
           34  +	return words
           35  +}
           36  +
           37  +// Diff calculates the word slice to be added and to be removed from oldWords
           38  +// to get the given word set.
           39  +func (ws WordSet) Diff(oldWords []string) (newWords, removeWords []string) {
           40  +	if len(ws) == 0 {
           41  +		return nil, oldWords
           42  +	}
           43  +	if len(oldWords) == 0 {
           44  +		return ws.Words(), nil
           45  +	}
           46  +	oldSet := make(WordSet, len(oldWords))
           47  +	for _, ow := range oldWords {
           48  +		if _, ok := ws[ow]; ok {
           49  +			oldSet[ow] = 1
           50  +			continue
           51  +		}
           52  +		removeWords = append(removeWords, ow)
           53  +	}
           54  +	for w := range ws {
           55  +		if _, ok := oldSet[w]; ok {
           56  +			continue
           57  +		}
           58  +		newWords = append(newWords, w)
           59  +	}
           60  +	return newWords, removeWords
           61  +}

Added box/manager/store/wordset_test.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package store contains general index data for storing a zettel index.
           12  +package store_test
           13  +
           14  +import (
           15  +	"sort"
           16  +	"testing"
           17  +
           18  +	"zettelstore.de/z/box/manager/store"
           19  +)
           20  +
           21  +func equalWordList(exp, got []string) bool {
           22  +	if len(exp) != len(got) {
           23  +		return false
           24  +	}
           25  +	if len(got) == 0 {
           26  +		return len(exp) == 0
           27  +	}
           28  +	sort.Strings(got)
           29  +	for i, w := range exp {
           30  +		if w != got[i] {
           31  +			return false
           32  +		}
           33  +	}
           34  +	return true
           35  +}
           36  +
           37  +func TestWordsWords(t *testing.T) {
           38  +	t.Parallel()
           39  +	testcases := []struct {
           40  +		words store.WordSet
           41  +		exp   []string
           42  +	}{
           43  +		{nil, nil},
           44  +		{store.WordSet{}, nil},
           45  +		{store.WordSet{"a": 1, "b": 2}, []string{"a", "b"}},
           46  +	}
           47  +	for i, tc := range testcases {
           48  +		got := tc.words.Words()
           49  +		if !equalWordList(tc.exp, got) {
           50  +			t.Errorf("%d: %v.Words() == %v, but got %v", i, tc.words, tc.exp, got)
           51  +		}
           52  +	}
           53  +}
           54  +
           55  +func TestWordsDiff(t *testing.T) {
           56  +	t.Parallel()
           57  +	testcases := []struct {
           58  +		cur        store.WordSet
           59  +		old        []string
           60  +		expN, expR []string
           61  +	}{
           62  +		{nil, nil, nil, nil},
           63  +		{store.WordSet{}, []string{}, nil, nil},
           64  +		{store.WordSet{"a": 1}, []string{}, []string{"a"}, nil},
           65  +		{store.WordSet{"a": 1}, []string{"b"}, []string{"a"}, []string{"b"}},
           66  +		{store.WordSet{}, []string{"b"}, nil, []string{"b"}},
           67  +		{store.WordSet{"a": 1}, []string{"a"}, nil, nil},
           68  +	}
           69  +	for i, tc := range testcases {
           70  +		gotN, gotR := tc.cur.Diff(tc.old)
           71  +		if !equalWordList(tc.expN, gotN) {
           72  +			t.Errorf("%d: %v.Diff(%v)->new %v, but got %v", i, tc.cur, tc.old, tc.expN, gotN)
           73  +		}
           74  +		if !equalWordList(tc.expR, gotR) {
           75  +			t.Errorf("%d: %v.Diff(%v)->rem %v, but got %v", i, tc.cur, tc.old, tc.expR, gotR)
           76  +		}
           77  +	}
           78  +}

Added box/manager/store/zettel.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package store contains general index data for storing a zettel index.
           12  +package store
           13  +
           14  +import "zettelstore.de/z/domain/id"
           15  +
           16  +// ZettelIndex contains all index data of a zettel.
           17  +type ZettelIndex struct {
           18  +	Zid      id.Zid            // zid of the indexed zettel
           19  +	backrefs id.Set            // set of back references
           20  +	metarefs map[string]id.Set // references to inverse keys
           21  +	deadrefs id.Set            // set of dead references
           22  +	words    WordSet
           23  +	urls     WordSet
           24  +}
           25  +
           26  +// NewZettelIndex creates a new zettel index.
           27  +func NewZettelIndex(zid id.Zid) *ZettelIndex {
           28  +	return &ZettelIndex{
           29  +		Zid:      zid,
           30  +		backrefs: id.NewSet(),
           31  +		metarefs: make(map[string]id.Set),
           32  +		deadrefs: id.NewSet(),
           33  +	}
           34  +}
           35  +
           36  +// AddBackRef adds a reference to a zettel where the current zettel links to
           37  +// without any more information.
           38  +func (zi *ZettelIndex) AddBackRef(zid id.Zid) {
           39  +	zi.backrefs[zid] = true
           40  +}
           41  +
           42  +// AddMetaRef adds a named reference to a zettel. On that zettel, the given
           43  +// metadata key should point back to the current zettel.
           44  +func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) {
           45  +	if zids, ok := zi.metarefs[key]; ok {
           46  +		zids[zid] = true
           47  +		return
           48  +	}
           49  +	zi.metarefs[key] = id.NewSet(zid)
           50  +}
           51  +
           52  +// AddDeadRef adds a dead reference to a zettel.
           53  +func (zi *ZettelIndex) AddDeadRef(zid id.Zid) {
           54  +	zi.deadrefs[zid] = true
           55  +}
           56  +
           57  +// SetWords sets the words to the given value.
           58  +func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words }
           59  +
           60  +// SetUrls sets the words to the given value.
           61  +func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls }
           62  +
           63  +// GetDeadRefs returns all dead references as a sorted list.
           64  +func (zi *ZettelIndex) GetDeadRefs() id.Slice {
           65  +	return zi.deadrefs.Sorted()
           66  +}
           67  +
           68  +// GetBackRefs returns all back references as a sorted list.
           69  +func (zi *ZettelIndex) GetBackRefs() id.Slice {
           70  +	return zi.backrefs.Sorted()
           71  +}
           72  +
           73  +// GetMetaRefs returns all meta references as a map of strings to a sorted list of references
           74  +func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice {
           75  +	if len(zi.metarefs) == 0 {
           76  +		return nil
           77  +	}
           78  +	result := make(map[string]id.Slice, len(zi.metarefs))
           79  +	for key, refs := range zi.metarefs {
           80  +		result[key] = refs.Sorted()
           81  +	}
           82  +	return result
           83  +}
           84  +
           85  +// GetWords returns a reference to the set of words. It must not be modified.
           86  +func (zi *ZettelIndex) GetWords() WordSet { return zi.words }
           87  +
           88  +// GetUrls returns a reference to the set of URLs. It must not be modified.
           89  +func (zi *ZettelIndex) GetUrls() WordSet { return zi.urls }

Added box/membox/membox.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package membox stores zettel volatile in main memory.
           12  +package membox
           13  +
           14  +import (
           15  +	"context"
           16  +	"net/url"
           17  +	"sync"
           18  +
           19  +	"zettelstore.de/z/box"
           20  +	"zettelstore.de/z/box/manager"
           21  +	"zettelstore.de/z/domain"
           22  +	"zettelstore.de/z/domain/id"
           23  +	"zettelstore.de/z/domain/meta"
           24  +	"zettelstore.de/z/search"
           25  +)
           26  +
           27  +func init() {
           28  +	manager.Register(
           29  +		"mem",
           30  +		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
           31  +			return &memBox{u: u, cdata: *cdata}, nil
           32  +		})
           33  +}
           34  +
           35  +type memBox struct {
           36  +	u      *url.URL
           37  +	cdata  manager.ConnectData
           38  +	zettel map[id.Zid]domain.Zettel
           39  +	mx     sync.RWMutex
           40  +}
           41  +
           42  +func (mp *memBox) notifyChanged(reason box.UpdateReason, zid id.Zid) {
           43  +	if chci := mp.cdata.Notify; chci != nil {
           44  +		chci <- box.UpdateInfo{Reason: reason, Zid: zid}
           45  +	}
           46  +}
           47  +
           48  +func (mp *memBox) Location() string {
           49  +	return mp.u.String()
           50  +}
           51  +
           52  +func (mp *memBox) Start(ctx context.Context) error {
           53  +	mp.mx.Lock()
           54  +	mp.zettel = make(map[id.Zid]domain.Zettel)
           55  +	mp.mx.Unlock()
           56  +	return nil
           57  +}
           58  +
           59  +func (mp *memBox) Stop(ctx context.Context) error {
           60  +	mp.mx.Lock()
           61  +	mp.zettel = nil
           62  +	mp.mx.Unlock()
           63  +	return nil
           64  +}
           65  +
           66  +func (mp *memBox) CanCreateZettel(ctx context.Context) bool { return true }
           67  +
           68  +func (mp *memBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
           69  +	mp.mx.Lock()
           70  +	zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) {
           71  +		_, ok := mp.zettel[zid]
           72  +		return !ok, nil
           73  +	})
           74  +	if err != nil {
           75  +		mp.mx.Unlock()
           76  +		return id.Invalid, err
           77  +	}
           78  +	meta := zettel.Meta.Clone()
           79  +	meta.Zid = zid
           80  +	zettel.Meta = meta
           81  +	mp.zettel[zid] = zettel
           82  +	mp.mx.Unlock()
           83  +	mp.notifyChanged(box.OnUpdate, zid)
           84  +	return zid, nil
           85  +}
           86  +
           87  +func (mp *memBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
           88  +	mp.mx.RLock()
           89  +	zettel, ok := mp.zettel[zid]
           90  +	mp.mx.RUnlock()
           91  +	if !ok {
           92  +		return domain.Zettel{}, box.ErrNotFound
           93  +	}
           94  +	zettel.Meta = zettel.Meta.Clone()
           95  +	return zettel, nil
           96  +}
           97  +
           98  +func (mp *memBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
           99  +	mp.mx.RLock()
          100  +	zettel, ok := mp.zettel[zid]
          101  +	mp.mx.RUnlock()
          102  +	if !ok {
          103  +		return nil, box.ErrNotFound
          104  +	}
          105  +	return zettel.Meta.Clone(), nil
          106  +}
          107  +
          108  +func (mp *memBox) FetchZids(ctx context.Context) (id.Set, error) {
          109  +	mp.mx.RLock()
          110  +	result := id.NewSetCap(len(mp.zettel))
          111  +	for zid := range mp.zettel {
          112  +		result[zid] = true
          113  +	}
          114  +	mp.mx.RUnlock()
          115  +	return result, nil
          116  +}
          117  +
          118  +func (mp *memBox) SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) {
          119  +	result := make([]*meta.Meta, 0, len(mp.zettel))
          120  +	mp.mx.RLock()
          121  +	for _, zettel := range mp.zettel {
          122  +		m := zettel.Meta.Clone()
          123  +		mp.cdata.Enricher.Enrich(ctx, m, mp.cdata.Number)
          124  +		if match(m) {
          125  +			result = append(result, m)
          126  +		}
          127  +	}
          128  +	mp.mx.RUnlock()
          129  +	return result, nil
          130  +}
          131  +
          132  +func (mp *memBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
          133  +	return true
          134  +}
          135  +
          136  +func (mp *memBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
          137  +	mp.mx.Lock()
          138  +	meta := zettel.Meta.Clone()
          139  +	if !meta.Zid.IsValid() {
          140  +		return &box.ErrInvalidID{Zid: meta.Zid}
          141  +	}
          142  +	zettel.Meta = meta
          143  +	mp.zettel[meta.Zid] = zettel
          144  +	mp.mx.Unlock()
          145  +	mp.notifyChanged(box.OnUpdate, meta.Zid)
          146  +	return nil
          147  +}
          148  +
          149  +func (mp *memBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return true }
          150  +
          151  +func (mp *memBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
          152  +	mp.mx.Lock()
          153  +	zettel, ok := mp.zettel[curZid]
          154  +	if !ok {
          155  +		mp.mx.Unlock()
          156  +		return box.ErrNotFound
          157  +	}
          158  +
          159  +	// Check that there is no zettel with newZid
          160  +	if _, ok = mp.zettel[newZid]; ok {
          161  +		mp.mx.Unlock()
          162  +		return &box.ErrInvalidID{Zid: newZid}
          163  +	}
          164  +
          165  +	meta := zettel.Meta.Clone()
          166  +	meta.Zid = newZid
          167  +	zettel.Meta = meta
          168  +	mp.zettel[newZid] = zettel
          169  +	delete(mp.zettel, curZid)
          170  +	mp.mx.Unlock()
          171  +	mp.notifyChanged(box.OnDelete, curZid)
          172  +	mp.notifyChanged(box.OnUpdate, newZid)
          173  +	return nil
          174  +}
          175  +
          176  +func (mp *memBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
          177  +	mp.mx.RLock()
          178  +	_, ok := mp.zettel[zid]
          179  +	mp.mx.RUnlock()
          180  +	return ok
          181  +}
          182  +
          183  +func (mp *memBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
          184  +	mp.mx.Lock()
          185  +	if _, ok := mp.zettel[zid]; !ok {
          186  +		mp.mx.Unlock()
          187  +		return box.ErrNotFound
          188  +	}
          189  +	delete(mp.zettel, zid)
          190  +	mp.mx.Unlock()
          191  +	mp.notifyChanged(box.OnDelete, zid)
          192  +	return nil
          193  +}
          194  +
          195  +func (mp *memBox) ReadStats(st *box.ManagedBoxStats) {
          196  +	st.ReadOnly = false
          197  +	mp.mx.RLock()
          198  +	st.Zettel = len(mp.zettel)
          199  +	mp.mx.RUnlock()
          200  +}

Added box/merge.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2020-2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package box provides a generic interface to zettel boxes.
           12  +package box
           13  +
           14  +import "zettelstore.de/z/domain/meta"
           15  +
           16  +// MergeSorted returns a merged sequence of metadata, sorted by Zid.
           17  +// The lists first and second must be sorted descending by Zid.
           18  +func MergeSorted(first, second []*meta.Meta) []*meta.Meta {
           19  +	lenFirst := len(first)
           20  +	lenSecond := len(second)
           21  +	result := make([]*meta.Meta, 0, lenFirst+lenSecond)
           22  +	iFirst := 0
           23  +	iSecond := 0
           24  +	for iFirst < lenFirst && iSecond < lenSecond {
           25  +		zidFirst := first[iFirst].Zid
           26  +		zidSecond := second[iSecond].Zid
           27  +		if zidFirst > zidSecond {
           28  +			result = append(result, first[iFirst])
           29  +			iFirst++
           30  +		} else if zidFirst < zidSecond {
           31  +			result = append(result, second[iSecond])
           32  +			iSecond++
           33  +		} else { // zidFirst == zidSecond
           34  +			result = append(result, first[iFirst])
           35  +			iFirst++
           36  +			iSecond++
           37  +		}
           38  +	}
           39  +	if iFirst < lenFirst {
           40  +		result = append(result, first[iFirst:]...)
           41  +	} else {
           42  +		result = append(result, second[iSecond:]...)
           43  +	}
           44  +
           45  +	return result
           46  +}

Added client/client.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package client provides a client for accessing the Zettelstore via its API.
           12  +package client
           13  +
           14  +import (
           15  +	"bytes"
           16  +	"context"
           17  +	"encoding/json"
           18  +	"errors"
           19  +	"io"
           20  +	"net/http"
           21  +	"net/url"
           22  +	"strconv"
           23  +	"strings"
           24  +	"time"
           25  +
           26  +	"zettelstore.de/z/api"
           27  +	"zettelstore.de/z/domain/id"
           28  +)
           29  +
           30  +// Client contains all data to execute requests.
           31  +type Client struct {
           32  +	baseURL   string
           33  +	username  string
           34  +	password  string
           35  +	token     string
           36  +	tokenType string
           37  +	expires   time.Time
           38  +}
           39  +
           40  +// NewClient create a new client.
           41  +func NewClient(baseURL string) *Client {
           42  +	if !strings.HasSuffix(baseURL, "/") {
           43  +		baseURL += "/"
           44  +	}
           45  +	c := Client{baseURL: baseURL}
           46  +	return &c
           47  +}
           48  +
           49  +func (c *Client) newURLBuilder(key byte) *api.URLBuilder {
           50  +	return api.NewURLBuilder(c.baseURL, key)
           51  +}
           52  +func (c *Client) newRequest(ctx context.Context, method string, ub *api.URLBuilder, body io.Reader) (*http.Request, error) {
           53  +	return http.NewRequestWithContext(ctx, method, ub.String(), body)
           54  +}
           55  +
           56  +func (c *Client) executeRequest(req *http.Request) (*http.Response, error) {
           57  +	if c.token != "" {
           58  +		req.Header.Add("Authorization", c.tokenType+" "+c.token)
           59  +	}
           60  +	client := http.Client{}
           61  +	resp, err := client.Do(req)
           62  +	if err != nil {
           63  +		if resp != nil && resp.Body != nil {
           64  +			resp.Body.Close()
           65  +		}
           66  +		return nil, err
           67  +	}
           68  +	return resp, err
           69  +}
           70  +
           71  +func (c *Client) buildAndExecuteRequest(
           72  +	ctx context.Context, method string, ub *api.URLBuilder, body io.Reader, h http.Header) (*http.Response, error) {
           73  +	req, err := c.newRequest(ctx, method, ub, body)
           74  +	if err != nil {
           75  +		return nil, err
           76  +	}
           77  +	err = c.updateToken(ctx)
           78  +	if err != nil {
           79  +		return nil, err
           80  +	}
           81  +	for key, val := range h {
           82  +		req.Header[key] = append(req.Header[key], val...)
           83  +	}
           84  +	return c.executeRequest(req)
           85  +}
           86  +
           87  +// SetAuth sets authentication data.
           88  +func (c *Client) SetAuth(username, password string) {
           89  +	c.username = username
           90  +	c.password = password
           91  +	c.token = ""
           92  +	c.tokenType = ""
           93  +	c.expires = time.Time{}
           94  +}
           95  +
           96  +func (c *Client) executeAuthRequest(req *http.Request) error {
           97  +	resp, err := c.executeRequest(req)
           98  +	if err != nil {
           99  +		return err
          100  +	}
          101  +	defer resp.Body.Close()
          102  +	if resp.StatusCode != http.StatusOK {
          103  +		return errors.New(resp.Status)
          104  +	}
          105  +	dec := json.NewDecoder(resp.Body)
          106  +	var tinfo api.AuthJSON
          107  +	err = dec.Decode(&tinfo)
          108  +	if err != nil {
          109  +		return err
          110  +	}
          111  +	c.token = tinfo.Token
          112  +	c.tokenType = tinfo.Type
          113  +	c.expires = time.Now().Add(time.Duration(tinfo.Expires*10/9) * time.Second)
          114  +	return nil
          115  +}
          116  +
          117  +func (c *Client) updateToken(ctx context.Context) error {
          118  +	if c.username == "" {
          119  +		return nil
          120  +	}
          121  +	if time.Now().After(c.expires) {
          122  +		return c.Authenticate(ctx)
          123  +	}
          124  +	return c.RefreshToken(ctx)
          125  +}
          126  +
          127  +// Authenticate sets a new token by sending user name and password.
          128  +func (c *Client) Authenticate(ctx context.Context) error {
          129  +	authData := url.Values{"username": {c.username}, "password": {c.password}}
          130  +	req, err := c.newRequest(ctx, http.MethodPost, c.newURLBuilder('v'), strings.NewReader(authData.Encode()))
          131  +	if err != nil {
          132  +		return err
          133  +	}
          134  +	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
          135  +	return c.executeAuthRequest(req)
          136  +}
          137  +
          138  +// RefreshToken updates the access token
          139  +func (c *Client) RefreshToken(ctx context.Context) error {
          140  +	req, err := c.newRequest(ctx, http.MethodPut, c.newURLBuilder('v'), nil)
          141  +	if err != nil {
          142  +		return err
          143  +	}
          144  +	return c.executeAuthRequest(req)
          145  +}
          146  +
          147  +// CreateZettel creates a new zettel and returns its URL.
          148  +func (c *Client) CreateZettel(ctx context.Context, data *api.ZettelDataJSON) (id.Zid, error) {
          149  +	var buf bytes.Buffer
          150  +	if err := encodeZettelData(&buf, data); err != nil {
          151  +		return id.Invalid, err
          152  +	}
          153  +	ub := c.jsonZettelURLBuilder(nil)
          154  +	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf, nil)
          155  +	if err != nil {
          156  +		return id.Invalid, err
          157  +	}
          158  +	defer resp.Body.Close()
          159  +	if resp.StatusCode != http.StatusCreated {
          160  +		return id.Invalid, errors.New(resp.Status)
          161  +	}
          162  +	dec := json.NewDecoder(resp.Body)
          163  +	var newZid api.ZidJSON
          164  +	err = dec.Decode(&newZid)
          165  +	if err != nil {
          166  +		return id.Invalid, err
          167  +	}
          168  +	zid, err := id.Parse(newZid.ID)
          169  +	if err != nil {
          170  +		return id.Invalid, err
          171  +	}
          172  +	return zid, nil
          173  +}
          174  +
          175  +func encodeZettelData(buf *bytes.Buffer, data *api.ZettelDataJSON) error {
          176  +	enc := json.NewEncoder(buf)
          177  +	enc.SetEscapeHTML(false)
          178  +	return enc.Encode(&data)
          179  +}
          180  +
          181  +// ListZettel returns a list of all Zettel.
          182  +func (c *Client) ListZettel(ctx context.Context, query url.Values) ([]api.ZettelJSON, error) {
          183  +	ub := c.jsonZettelURLBuilder(query)
          184  +	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
          185  +	if err != nil {
          186  +		return nil, err
          187  +	}
          188  +	defer resp.Body.Close()
          189  +	if resp.StatusCode != http.StatusOK {
          190  +		return nil, errors.New(resp.Status)
          191  +	}
          192  +	dec := json.NewDecoder(resp.Body)
          193  +	var zl api.ZettelListJSON
          194  +	err = dec.Decode(&zl)
          195  +	if err != nil {
          196  +		return nil, err
          197  +	}
          198  +	return zl.List, nil
          199  +}
          200  +
          201  +// GetZettelJSON returns a zettel as a JSON struct.
          202  +func (c *Client) GetZettelJSON(ctx context.Context, zid id.Zid, query url.Values) (*api.ZettelDataJSON, error) {
          203  +	ub := c.jsonZettelURLBuilder(query).SetZid(zid)
          204  +	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
          205  +	if err != nil {
          206  +		return nil, err
          207  +	}
          208  +	defer resp.Body.Close()
          209  +	if resp.StatusCode != http.StatusOK {
          210  +		return nil, errors.New(resp.Status)
          211  +	}
          212  +	dec := json.NewDecoder(resp.Body)
          213  +	var out api.ZettelDataJSON
          214  +	err = dec.Decode(&out)
          215  +	if err != nil {
          216  +		return nil, err
          217  +	}
          218  +	return &out, nil
          219  +}
          220  +
          221  +// GetEvaluatedZettel return a zettel in a defined encoding.
          222  +func (c *Client) GetEvaluatedZettel(ctx context.Context, zid id.Zid, enc api.EncodingEnum) (string, error) {
          223  +	ub := c.jsonZettelURLBuilder(nil).SetZid(zid)
          224  +	ub.AppendQuery(api.QueryKeyFormat, enc.String())
          225  +	ub.AppendQuery(api.QueryKeyPart, api.PartContent)
          226  +	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
          227  +	if err != nil {
          228  +		return "", err
          229  +	}
          230  +	defer resp.Body.Close()
          231  +	if resp.StatusCode != http.StatusOK {
          232  +		return "", errors.New(resp.Status)
          233  +	}
          234  +	content, err := io.ReadAll(resp.Body)
          235  +	if err != nil {
          236  +		return "", err
          237  +	}
          238  +	return string(content), nil
          239  +}
          240  +
          241  +// GetZettelOrder returns metadata of the given zettel and, more important,
          242  +// metadata of zettel that are referenced in a list within the first zettel.
          243  +func (c *Client) GetZettelOrder(ctx context.Context, zid id.Zid) (*api.ZidMetaRelatedList, error) {
          244  +	ub := c.newURLBuilder('o').SetZid(zid)
          245  +	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
          246  +	if err != nil {
          247  +		return nil, err
          248  +	}
          249  +	defer resp.Body.Close()
          250  +	if resp.StatusCode != http.StatusOK {
          251  +		return nil, errors.New(resp.Status)
          252  +	}
          253  +	dec := json.NewDecoder(resp.Body)
          254  +	var out api.ZidMetaRelatedList
          255  +	err = dec.Decode(&out)
          256  +	if err != nil {
          257  +		return nil, err
          258  +	}
          259  +	return &out, nil
          260  +}
          261  +
          262  +// ContextDirection specifies how the context should be calculated.
          263  +type ContextDirection uint8
          264  +
          265  +// Allowed values for ContextDirection
          266  +const (
          267  +	_ ContextDirection = iota
          268  +	DirBoth
          269  +	DirBackward
          270  +	DirForward
          271  +)
          272  +
          273  +// GetZettelContext returns metadata of the given zettel and, more important,
          274  +// metadata of zettel that for the context of the first zettel.
          275  +func (c *Client) GetZettelContext(
          276  +	ctx context.Context, zid id.Zid, dir ContextDirection, depth, limit int) (
          277  +	*api.ZidMetaRelatedList, error,
          278  +) {
          279  +	ub := c.newURLBuilder('x').SetZid(zid)
          280  +	switch dir {
          281  +	case DirBackward:
          282  +		ub.AppendQuery(api.QueryKeyDir, api.DirBackward)
          283  +	case DirForward:
          284  +		ub.AppendQuery(api.QueryKeyDir, api.DirForward)
          285  +	}
          286  +	if depth > 0 {
          287  +		ub.AppendQuery(api.QueryKeyDepth, strconv.Itoa(depth))
          288  +	}
          289  +	if limit > 0 {
          290  +		ub.AppendQuery(api.QueryKeyLimit, strconv.Itoa(limit))
          291  +	}
          292  +	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
          293  +	if err != nil {
          294  +		return nil, err
          295  +	}
          296  +	defer resp.Body.Close()
          297  +	if resp.StatusCode != http.StatusOK {
          298  +		return nil, errors.New(resp.Status)
          299  +	}
          300  +	dec := json.NewDecoder(resp.Body)
          301  +	var out api.ZidMetaRelatedList
          302  +	err = dec.Decode(&out)
          303  +	if err != nil {
          304  +		return nil, err
          305  +	}
          306  +	return &out, nil
          307  +}
          308  +
          309  +// GetZettelLinks returns connections to ohter zettel, images, externals URLs.
          310  +func (c *Client) GetZettelLinks(ctx context.Context, zid id.Zid) (*api.ZettelLinksJSON, error) {
          311  +	ub := c.newURLBuilder('l').SetZid(zid)
          312  +	resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil)
          313  +	if err != nil {
          314  +		return nil, err
          315  +	}
          316  +	defer resp.Body.Close()
          317  +	if resp.StatusCode != http.StatusOK {
          318  +		return nil, errors.New(resp.Status)
          319  +	}
          320  +	dec := json.NewDecoder(resp.Body)
          321  +	var out api.ZettelLinksJSON
          322  +	err = dec.Decode(&out)
          323  +	if err != nil {
          324  +		return nil, err
          325  +	}
          326  +	return &out, nil
          327  +}
          328  +
          329  +// UpdateZettel updates an existing zettel.
          330  +func (c *Client) UpdateZettel(ctx context.Context, zid id.Zid, data *api.ZettelDataJSON) error {
          331  +	var buf bytes.Buffer
          332  +	if err := encodeZettelData(&buf, data); err != nil {
          333  +		return err
          334  +	}
          335  +	ub := c.jsonZettelURLBuilder(nil).SetZid(zid)
          336  +	resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf, nil)
          337  +	if err != nil {
          338  +		return err
          339  +	}
          340  +	defer resp.Body.Close()
          341  +	if resp.StatusCode != http.StatusNoContent {
          342  +		return errors.New(resp.Status)
          343  +	}
          344  +	return nil
          345  +}
          346  +
          347  +// RenameZettel renames a zettel.
          348  +func (c *Client) RenameZettel(ctx context.Context, oldZid, newZid id.Zid) error {
          349  +	ub := c.jsonZettelURLBuilder(nil).SetZid(oldZid)
          350  +	h := http.Header{
          351  +		api.HeaderDestination: {c.jsonZettelURLBuilder(nil).SetZid(newZid).String()},
          352  +	}
          353  +	resp, err := c.buildAndExecuteRequest(ctx, api.MethodMove, ub, nil, h)
          354  +	if err != nil {
          355  +		return err
          356  +	}
          357  +	defer resp.Body.Close()
          358  +	if resp.StatusCode != http.StatusNoContent {
          359  +		return errors.New(resp.Status)
          360  +	}
          361  +	return nil
          362  +}
          363  +
          364  +// DeleteZettel deletes a zettel with the given identifier.
          365  +func (c *Client) DeleteZettel(ctx context.Context, zid id.Zid) error {
          366  +	ub := c.jsonZettelURLBuilder(nil).SetZid(zid)
          367  +	resp, err := c.buildAndExecuteRequest(ctx, http.MethodDelete, ub, nil, nil)
          368  +	if err != nil {
          369  +		return err
          370  +	}
          371  +	defer resp.Body.Close()
          372  +	if resp.StatusCode != http.StatusNoContent {
          373  +		return errors.New(resp.Status)
          374  +	}
          375  +	return nil
          376  +}
          377  +
          378  +func (c *Client) jsonZettelURLBuilder(query url.Values) *api.URLBuilder {
          379  +	ub := c.newURLBuilder('z')
          380  +	for key, values := range query {
          381  +		if key == api.QueryKeyFormat {
          382  +			continue
          383  +		}
          384  +		for _, val := range values {
          385  +			ub.AppendQuery(key, val)
          386  +		}
          387  +	}
          388  +	return ub
          389  +}
          390  +
          391  +// ListTags returns a map of all tags, together with the associated zettel containing this tag.
          392  +func (c *Client) ListTags(ctx context.Context) (map[string][]string, error) {
          393  +	err := c.updateToken(ctx)
          394  +	if err != nil {
          395  +		return nil, err
          396  +	}
          397  +	req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('t'), nil)
          398  +	if err != nil {
          399  +		return nil, err
          400  +	}
          401  +	resp, err := c.executeRequest(req)
          402  +	if err != nil {
          403  +		return nil, err
          404  +	}
          405  +	defer resp.Body.Close()
          406  +	if resp.StatusCode != http.StatusOK {
          407  +		return nil, errors.New(resp.Status)
          408  +	}
          409  +	dec := json.NewDecoder(resp.Body)
          410  +	var tl api.TagListJSON
          411  +	err = dec.Decode(&tl)
          412  +	if err != nil {
          413  +		return nil, err
          414  +	}
          415  +	return tl.Tags, nil
          416  +}
          417  +
          418  +// ListRoles returns a list of all roles.
          419  +func (c *Client) ListRoles(ctx context.Context) ([]string, error) {
          420  +	err := c.updateToken(ctx)
          421  +	if err != nil {
          422  +		return nil, err
          423  +	}
          424  +	req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('r'), nil)
          425  +	if err != nil {
          426  +		return nil, err
          427  +	}
          428  +	resp, err := c.executeRequest(req)
          429  +	if err != nil {
          430  +		return nil, err
          431  +	}
          432  +	defer resp.Body.Close()
          433  +	if resp.StatusCode != http.StatusOK {
          434  +		return nil, errors.New(resp.Status)
          435  +	}
          436  +	dec := json.NewDecoder(resp.Body)
          437  +	var rl api.RoleListJSON
          438  +	err = dec.Decode(&rl)
          439  +	if err != nil {
          440  +		return nil, err
          441  +	}
          442  +	return rl.Roles, nil
          443  +}

Added client/client_test.go.

            1  +//-----------------------------------------------------------------------------
            2  +// Copyright (c) 2021 Detlef Stern
            3  +//
            4  +// This file is part of zettelstore.
            5  +//
            6  +// Zettelstore is licensed under the latest version of the EUPL (European Union
            7  +// Public License). Please see file LICENSE.txt for your rights and obligations
            8  +// under this license.
            9  +//-----------------------------------------------------------------------------
           10  +
           11  +// Package client provides a client for accessing the Zettelstore via its API.
           12  +package client_test
           13  +
           14  +import (
           15  +	"context"
           16  +	"flag"
           17  +	"fmt"
           18  +	"net/url"
           19  +	"testing"
           20  +
           21  +	"zettelstore.de/z/api"
           22  +	"zettelstore.de/z/client"
           23  +	"zettelstore.de/z/domain/id"
           24  +	"zettelstore.de/z/domain/meta"
           25  +)
           26  +
           27  +func TestCreateRenameDeleteZettel(t *testing.T) {
           28  +	// Is not to be allowed to run in parallel with other tests.
           29  +	c := getClient()
           30  +	c.SetAuth("creator", "creator")
           31  +	zid, err := c.CreateZettel(context.Background(), &api.ZettelDataJSON{
           32  +		Meta:     nil,
           33  +		Encoding: "",
           34  +		Content:  "Example",
           35  +	})
           36  +	if err != nil {
           37  +		t.Error("Cannot create zettel:", err)
           38  +		return
           39  +	}
           40  +	if !zid.IsValid() {
           41  +		t.Error("Invalid zettel ID", zid)
           42  +		return
           43  +	}
           44  +	newZid := zid + 1
           45  +	c.SetAuth("owner", "owner")
           46  +	err = c.RenameZettel(context.Background(), zid, newZid)
           47  +	if err != nil {
           48  +		t.Error("Cannot rename", zid, ":", err)
           49  +		newZid = zid
           50  +	}
           51  +	err = c.DeleteZettel(context.Background(), newZid)
           52  +	if err != nil {
           53  +		t.Error("Cannot delete", zid, ":", err)
           54  +		return
           55  +	}
           56  +}
           57  +
           58  +func TestUpdateZettel(t *testing.T) {
           59  +	t.Parallel()
           60  +	c := getClient()
           61  +	c.SetAuth("writer", "writer")
           62  +	z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil)
           63  +	if err != nil {
           64  +		t.Error(err)
           65  +		return
           66  +	}
           67  +	if got := z.Meta[meta.KeyTitle]; got != "Home" {
           68  +		t.Errorf("Title of zettel is not \"Home\", but %q", got)
           69  +		return
           70  +	}
           71  +	newTitle := "New Home"
           72  +	z.Meta[meta.KeyTitle] = newTitle
           73  +	err = c.UpdateZettel(context.Background(), id.DefaultHomeZid, z)
           74  +	if err != nil {
           75  +		t.Error(err)
           76  +		return
           77  +	}
           78  +	zt, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, nil)
           79  +	if err != nil {
           80  +		t.Error(err)
           81  +		return
           82  +	}
           83  +	if got := zt.Meta[meta.KeyTitle]; got != newTitle {
           84  +		t.Errorf("Title of zettel is not %q, but %q", newTitle, got)
           85  +	}
           86  +}
           87  +
           88  +func TestList(t *testing.T) {
           89  +	testdata := []struct {
           90  +		user string
           91  +		exp  int
           92  +	}{
           93  +		{"", 7},
           94  +		{"creator", 10},
           95  +		{"reader", 12},
           96  +		{"writer", 12},
           97  +		{"owner", 34},
           98  +	}
           99  +
          100  +	t.Parallel()
          101  +	c := getClient()
          102  +	query := url.Values{api.QueryKeyFormat: {"html"}} // Client must remove "html"
          103  +	for i, tc := range testdata {
          104  +		t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) {
          105  +			c.SetAuth(tc.user, tc.user)
          106  +			l, err := c.ListZettel(context.Background(), query)
          107  +			if err != nil {
          108  +				tt.Error(err)
          109  +				return
          110  +			}
          111  +			got := len(l)
          112  +			if got != tc.exp {
          113  +				tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l)
          114  +			}
          115  +		})
          116  +	}
          117  +	l, err := c.ListZettel(context.Background(), url.Values{meta.KeyRole: {meta.ValueRoleConfiguration}})
          118  +	if err != nil {
          119  +		t.Error(err)
          120  +		return
          121  +	}
          122  +	got := len(l)
          123  +	if got != 27 {
          124  +		t.Errorf("List of length %d expected, but got %d\n%v", 27, got, l)
          125  +	}
          126  +}
          127  +func TestGetZettel(t *testing.T) {
          128  +	t.Parallel()
          129  +	c := getClient()
          130  +	c.SetAuth("owner", "owner")
          131  +	z, err := c.GetZettelJSON(context.Background(), id.DefaultHomeZid, url.Values{api.QueryKeyPart: {api.PartContent}})
          132  +	if err != nil {
          133  +		t.Error(err)
          134  +		return
          135  +	}
          136  +	if m := z.Meta; len(m) > 0 {
          137  +		t.Errorf("Exptected empty meta, but got %v", z.Meta)
          138  +	}
          139  +	if z.Content == "" || z.Encoding != "" {
          140  +		t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding)
          141  +	}
          142  +}
          143  +
          144  +func TestGetEvaluatedZettel(t *testing.T) {
          145  +	t.Parallel()
          146  +	c := getClient()
          147  +	c.SetAuth("owner", "owner")
          148  +	encodings := []api.EncodingEnum{
          149  +		api.EncoderDJSON,
          150  +		api.EncoderHTML,
          151  +		api.EncoderNative,
          152  +		api.EncoderText,
          153  +	}
          154  +	for _, enc := range encodings {
          155  +		content, err := c.GetEvaluatedZettel(context.Background(), id.DefaultHomeZid, enc)
          156  +		if err != nil {
          157  +			t.Error(err)
          158  +			continue
          159  +		}
          160  +		if len(content) == 0 {
          161  +			t.Errorf("Empty content for encoding %v", enc)
          162  +		}
          163  +	}
          164  +}
          165  +
          166  +func TestGetZettelOrder(t *testing.T) {
          167  +	t.Parallel()
          168  +	c := getClient()
          169  +	c.SetAuth("owner", "owner")
          170  +	rl, err := c.GetZettelOrder(context.Background(), id.TOCNewTemplateZid)
          171  +	if err != nil {
          172  +		t.Error(err)
          173  +		return
          174  +	}
          175  +	if rl.ID != id.TOCNewTemplateZid.String() {
          176  +		t.Errorf("Expected an Zid %v, but got %v", id.TOCNewTemplateZid, rl.ID)
          177  +		return
          178  +	}
          179  +	l := rl.List
          180  +	if got := len(l); got != 2 {
          181  +		t.Errorf("Expected list fo length 2, got %d", got)
          182  +		return
          183  +	}
          184  +	if got := l[0].ID; got != id.TemplateNewZettelZid.String() {
          185  +		t.Errorf("Expected result[0]=%v, but got %v", id.TemplateNewZettelZid, got)
          186  +	}
          187  +	if got := l[1].ID; got != id.TemplateNewUserZid.String() {
          188  +		t.Errorf("Expected result[1]=%v, but got %v", id.TemplateNewUserZid, got)
          189  +	}
          190  +}
          191  +
          192  +func TestGetZettelContext(t *testing.T) {
          193  +	t.Parallel()
          194  +	c := getClient()
          195  +	c.SetAuth("owner", "owner")
          196  +	rl, err := c.GetZettelContext(context.Background(), id.VersionZid, client.DirBoth, 0, 3)
          197  +	if err != nil {
          198  +		t.Error(err)
          199  +		return
          200  +	}
          201  +	if rl.ID != id.VersionZid.String() {
          202  +		t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID)
          203  +		return
          204  +	}
          205  +	l := rl.List
          206  +	if got := len(l); got != 3 {
          207  +		t.Errorf("Expected list fo length 3, got %d", got)
          208  +		return
          209  +	}
          210  +	if got := l[0].ID; got != id.DefaultHomeZid.String() {
          211  +		t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got)
          212  +	}
          213  +	if got := l[1].ID; got != id.OperatingSystemZid.String() {
          214  +		t.Errorf("Expected result[1]=%v, but got %v", id.OperatingSystemZid, got)
          215  +	}
          216  +	if got := l[2].ID; got != id.StartupConfigurationZid.String() {
          217  +		t.Errorf("Expected result[2]=%v, but got %v", id.StartupConfigurationZid, got)
          218  +	}
          219  +
          220  +	rl, err = c.GetZettelContext(context.Background(), id.VersionZid, client.DirBackward, 0, 0)
          221  +	if err != nil {
          222  +		t.Error(err)
          223  +		return
          224  +	}
          225  +	if rl.ID != id.VersionZid.String() {
          226  +		t.Errorf("Expected an Zid %v, but got %v", id.VersionZid, rl.ID)
          227  +		return
          228  +	}
          229  +	l = rl.List
          230  +	if got := len(l); got != 1 {
          231  +		t.Errorf("Expected list fo length 1, got %d", got)
          232  +		return
          233  +	}
          234  +	if got := l[0].ID; got != id.DefaultHomeZid.String() {
          235  +		t.Errorf("Expected result[0]=%v, but got %v", id.DefaultHomeZid, got)
          236  +	}
          237  +}
          238  +
          239  +func TestGetZettelLinks(t *testing.T) {
          240  +	t.Parallel()
          241  +	c := getClient()
          242  +	c.SetAuth("owner", "owner")
          243  +	zl, err := c.GetZettelLinks(context.Background(), id.DefaultHomeZid)
          244  +	if err != nil {
          245  +		t.Error(err)
          246  +		return
          247  +	}
          248  +	if zl.ID != id.DefaultHomeZid.String() {
          249  +		t.Errorf("Expected an Zid %v, but got %v", id.DefaultHomeZid, zl.ID)
          250  +		return
          251  +	}
          252  +	if len(zl.Links.Incoming) != 0 {
          253  +		t.Error("No incomings expected", zl.Links.Incoming)
          254  +	}
          255  +	if got := len(zl.Links.Outgoing); got != 4 {
          256  +		t.Errorf("Expected 4 outgoing links, got %d", got)
          257  +	}
          258  +	if got := len(zl.Links.Local); got != 1 {
          259  +		t.Errorf("Expected 1 local link, got %d", got)
          260  +	}
          261  +	if got := len(zl.Links.External); got != 4 {
          262  +		t.Errorf("Expected 4 external link, got %d", got)
          263  +	}
          264  +}
          265  +
          266  +func TestListTags(t *testing.T) {
          267  +	t.Parallel()
          268  +	c := getClient()
          269  +	c.SetAuth("owner", "owner")
          270  +	tm, err := c.ListTags(context.Background())
          271  +	if err != nil {
          272  +		t.Error(err)
          273  +		return
          274  +	}
          275  +	tags := []struct {
          276  +		key  string
          277  +		size int
          278  +	}{
          279  +		{"#invisible", 1},
          280  +		{"#user", 4},
          281  +		{"#test", 4},
          282  +	}
          283  +	if len(tm) != len(tags) {
          284  +		t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm)
          285  +	}
          286  +	for _, tag := range tags {
          287  +		if zl, ok := tm[tag.key]; !ok {
          288  +			t.Errorf("No tag %v: %v", tag.key, tm)
          289  +		} else if len(zl) != tag.size {
          290  +			t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl)
          291  +		}
          292  +	}
          293  +	for i, id := range tm["#user"] {
          294  +		if id != tm["#test"][i] {
          295  +			t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"])
          296  +		}
          297  +	}
          298  +}
          299  +
          300  +func TestListRoles(t *testing.T) {
          301  +	t.Parallel()
          302  +	c := getClient()
          303  +	c.SetAuth("owner", "owner")
          304  +	rl, err := c.ListRoles(context.Background())
          305  +	if err != nil {
          306  +		t.Error(err)
          307  +		return
          308  +	}
          309  +	exp := []string{"configuration", "user", "zettel"}
          310  +	if len(rl) != len(exp) {
          311  +		t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl)
          312  +	}
          313  +	for i, id := range exp {
          314  +		if id != rl[i] {
          315  +			t.Errorf("Role list pos %d: expected %q, got %q", i, id, rl[i])
          316  +		}
          317  +	}
          318  +}
          319  +
          320  +var baseURL string
          321  +
          322  +func init() {
          323  +	flag.StringVar(&baseURL, "base-url", "", "Base URL")
          324  +}
          325  +
          326  +func getClient() *client.Client { return client.NewClient(baseURL) }
          327  +
          328  +// TestMain controls whether client API tests should run or not.
          329  +func TestMain(m *testing.M) {
          330  +	flag.Parse()
          331  +	if baseURL != "" {
          332  +		m.Run()
          333  +	}
          334  +}

Changes to cmd/cmd_file.go.

    12     12   
    13     13   import (
    14     14   	"flag"
    15     15   	"fmt"
    16     16   	"io"
    17     17   	"os"
    18     18   
           19  +	"zettelstore.de/z/api"
    19     20   	"zettelstore.de/z/domain"
    20     21   	"zettelstore.de/z/domain/id"
    21     22   	"zettelstore.de/z/domain/meta"
    22     23   	"zettelstore.de/z/encoder"
    23     24   	"zettelstore.de/z/input"
    24     25   	"zettelstore.de/z/parser"
    25     26   )
................................................................................
    36     37   		domain.Zettel{
    37     38   			Meta:    m,
    38     39   			Content: domain.NewContent(inp.Src[inp.Pos:]),
    39     40   		},
    40     41   		m.GetDefault(meta.KeySyntax, meta.ValueSyntaxZmk),
    41     42   		nil,
    42     43   	)
    43         -	enc := encoder.Create(format, &encoder.Environment{Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN)})
           44  +	enc := encoder.Create(api.Encoder(format), &encoder.Environment{Lang: m.GetDefault(meta.KeyLang, meta.ValueLangEN)})
    44     45   	if enc == nil {
    45     46   		fmt.Fprintf(os.Stderr, "Unknown format %q\n", format)
    46     47   		return 2, nil
    47     48   	}
    48     49   	_, err = enc.WriteZettel(os.Stdout, z, format != "raw")
    49     50   	if err != nil {
    50     51   		return 2, err

Changes to cmd/cmd_run.go.

    10     10   
    11     11   package cmd
    12     12   
    13     13   import (
    14     14   	"flag"
    15     15   	"net/http"
    16     16   
           17  +	zsapi "zettelstore.de/z/api"
    17     18   	"zettelstore.de/z/auth"
           19  +	"zettelstore.de/z/box"
    18     20   	"zettelstore.de/z/config"
    19     21   	"zettelstore.de/z/domain/meta"
    20     22   	"zettelstore.de/z/kernel"
    21         -	"zettelstore.de/z/place"
    22     23   	"zettelstore.de/z/usecase"
    23         -	"zettelstore.de/z/web/adapter"
    24     24   	"zettelstore.de/z/web/adapter/api"
    25     25   	"zettelstore.de/z/web/adapter/webui"
    26     26   	"zettelstore.de/z/web/server"
    27     27   )
    28     28   
    29     29   // ---------- Subcommand: run ------------------------------------------------
    30     30   
................................................................................
    54     54   	kern.SetDebug(debug)
    55     55   	if err := kern.StartService(kernel.WebService); err != nil {
    56     56   		return 1, err
    57     57   	}
    58     58   	return 0, nil
    59     59   }
    60     60   
    61         -func setupRouting(webSrv server.Server, placeManager place.Manager, authManager auth.Manager, rtConfig config.Config) {
    62         -	protectedPlaceManager, authPolicy := authManager.PlaceWithPolicy(webSrv, placeManager, rtConfig)
           61  +func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) {
           62  +	protectedBoxManager, authPolicy := authManager.BoxWithPolicy(webSrv, boxManager, rtConfig)
    63     63   	api := api.New(webSrv, authManager, authManager, webSrv, rtConfig)
    64         -	wui := webui.New(webSrv, authManager, rtConfig, authManager, placeManager, authPolicy)
           64  +	wui := webui.New(webSrv, authManager, rtConfig, authManager, boxManager, authPolicy)
    65     65   
    66         -	ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, placeManager)
    67         -	ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedPlaceManager)
    68         -	ucGetMeta := usecase.NewGetMeta(protectedPlaceManager)
    69         -	ucGetZettel := usecase.NewGetZettel(protectedPlaceManager)
           66  +	ucAuthenticate := usecase.NewAuthenticate(authManager, authManager, boxManager)
           67  +	ucCreateZettel := usecase.NewCreateZettel(rtConfig, protectedBoxManager)
           68  +	ucGetMeta := usecase.NewGetMeta(protectedBoxManager)
           69  +	ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager)
           70  +	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
    70     71   	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
    71         -	ucListMeta := usecase.NewListMeta(protectedPlaceManager)
    72         -	ucListRoles := usecase.NewListRole(protectedPlaceManager)
    73         -	ucListTags := usecase.NewListTags(protectedPlaceManager)
    74         -	ucZettelContext := usecase.NewZettelContext(protectedPlaceManager)
           72  +	ucListMeta := usecase.NewListMeta(protectedBoxManager)
           73  +	ucListRoles := usecase.NewListRole(protectedBoxManager)
           74  +	ucListTags := usecase.NewListTags(protectedBoxManager)
           75  +	ucZettelContext := usecase.NewZettelContext(protectedBoxManager)
           76  +	ucDelete := usecase.NewDeleteZettel(protectedBoxManager)
           77  +	ucUpdate := usecase.NewUpdateZettel(protectedBoxManager)
           78  +	ucRename := usecase.NewRenameZettel(protectedBoxManager)
           79  +
           80  +	webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager))
    75     81   
    76         -	webSrv.Handle("/", wui.MakeGetRootHandler(protectedPlaceManager))
           82  +	// Web user interface
    77     83   	webSrv.AddListRoute('a', http.MethodGet, wui.MakeGetLoginHandler())
    78         -	webSrv.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler(
    79         -		api.MakePostLoginHandlerAPI(ucAuthenticate),
    80         -		wui.MakePostLoginHandlerHTML(ucAuthenticate)))
    81         -	webSrv.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler())
           84  +	webSrv.AddListRoute('a', http.MethodPost, wui.MakePostLoginHandler(ucAuthenticate))
    82     85   	webSrv.AddZettelRoute('a', http.MethodGet, wui.MakeGetLogoutHandler())
    83     86   	if !authManager.IsReadonly() {
    84     87   		webSrv.AddZettelRoute('b', http.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta))
    85         -		webSrv.AddZettelRoute('b', http.MethodPost, wui.MakePostRenameZettelHandler(
    86         -			usecase.NewRenameZettel(protectedPlaceManager)))
           88  +		webSrv.AddZettelRoute('b', http.MethodPost, wui.MakePostRenameZettelHandler(ucRename))
    87     89   		webSrv.AddZettelRoute('c', http.MethodGet, wui.MakeGetCopyZettelHandler(
    88     90   			ucGetZettel, usecase.NewCopyZettel()))
    89     91   		webSrv.AddZettelRoute('c', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel))
    90     92   		webSrv.AddZettelRoute('d', http.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel))
    91         -		webSrv.AddZettelRoute('d', http.MethodPost, wui.MakePostDeleteZettelHandler(
    92         -			usecase.NewDeleteZettel(protectedPlaceManager)))
           93  +		webSrv.AddZettelRoute('d', http.MethodPost, wui.MakePostDeleteZettelHandler(ucDelete))
    93     94   		webSrv.AddZettelRoute('e', http.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel))
    94         -		webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler(
    95         -			usecase.NewUpdateZettel(protectedPlaceManager)))
           95  +		webSrv.AddZettelRoute('e', http.MethodPost, wui.MakeEditSetZettelHandler(ucUpdate))
    96     96   		webSrv.AddZettelRoute('f', http.MethodGet, wui.MakeGetFolgeZettelHandler(
    97     97   			ucGetZettel, usecase.NewFolgeZettel(rtConfig)))
    98     98   		webSrv.AddZettelRoute('f', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel))
    99     99   		webSrv.AddZettelRoute('g', http.MethodGet, wui.MakeGetNewZettelHandler(
   100    100   			ucGetZettel, usecase.NewNewZettel()))
   101    101   		webSrv.AddZettelRoute('g', http.MethodPost, wui.MakePostCreateZettelHandler(ucCreateZettel))
   102    102   	}
   103    103   	webSrv.AddListRoute('f', http.MethodGet, wui.MakeSearchHandler(
   104         -		usecase.NewSearch(protectedPlaceManager), ucGetMeta, ucGetZettel))
          104  +		usecase.NewSearch(protectedBoxManager), ucGetMeta, ucGetZettel))
   105    105   	webSrv.AddListRoute('h', http.MethodGet, wui.MakeListHTMLMetaHandler(
   106    106   		ucListMeta, ucListRoles, ucListTags))
   107    107   	webSrv.AddZettelRoute('h', http.MethodGet, wui.MakeGetHTMLZettelHandler(
   108    108   		ucParseZettel, ucGetMeta))
   109         -	webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler(ucParseZettel, ucGetMeta))
          109  +	webSrv.AddZettelRoute('i', http.MethodGet, wui.MakeGetInfoHandler(
          110  +		ucParseZettel, ucGetMeta, ucGetAllMeta))
   110    111   	webSrv.AddZettelRoute('j', http.MethodGet, wui.MakeZettelContextHandler(ucZettelContext))
   111    112   
          113  +	// API
   112    114   	webSrv.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel))
   113    115   	webSrv.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler(
   114         -		usecase.NewZettelOrder(protectedPlaceManager, ucParseZettel)))
          116  +		usecase.NewZettelOrder(protectedBoxManager, ucParseZettel)))
   115    117   	webSrv.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles))
   116    118   	webSrv.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags))
   117         -	webSrv.AddZettelRoute('y', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext))
          119  +	webSrv.AddListRoute('v', http.MethodPost, api.MakePostLoginHandler(ucAuthenticate))
          120  +	webSrv.AddListRoute('v', http.MethodPut, api.MakeRenewAuthHandler())
          121  +	webSrv.AddZettelRoute('x', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext))
   118    122   	webSrv.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler(
   119         -		usecase.NewListMeta(protectedPlaceManager), ucGetMeta, ucParseZettel))
          123  +		usecase.NewListMeta(protectedBoxManager), ucGetMeta, ucParseZettel))
   120    124   	webSrv.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler(
   121    125   		ucParseZettel, ucGetMeta))
          126  +	if !authManager.IsReadonly() {
          127  +		webSrv.AddListRoute('z', http.MethodPost, api.MakePostCreateZettelHandler(ucCreateZettel))
          128  +		webSrv.AddZettelRoute('z', http.MethodDelete, api.MakeDeleteZettelHandler(ucDelete))
          129  +		webSrv.AddZettelRoute('z', http.MethodPut, api.MakeUpdateZettelHandler(ucUpdate))
          130  +		webSrv.AddZettelRoute('z', zsapi.MethodMove, api.MakeRenameZettelHandler(ucRename))
          131  +	}
   122    132   
   123    133   	if authManager.WithAuth() {
   124         -		webSrv.SetUserRetriever(usecase.NewGetUserByZid(placeManager))
          134  +		webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager))
   125    135   	}
   126    136   }

Changes to cmd/command.go.

    15     15   	"sort"
    16     16   
    17     17   	"zettelstore.de/z/domain/meta"
    18     18   )
    19     19   
    20     20   // Command stores information about commands / sub-commands.
    21     21   type Command struct {
    22         -	Name   string              // command name as it appears on the command line
    23         -	Func   CommandFunc         // function that executes a command
    24         -	Places bool                // if true then places will be set up
    25         -	Header bool                // Print a heading on startup
    26         -	Flags  func(*flag.FlagSet) // function to set up flag.FlagSet
    27         -	flags  *flag.FlagSet       // flags that belong to the command
           22  +	Name       string              // command name as it appears on the command line
           23  +	Func       CommandFunc         // function that executes a command
           24  +	Boxes      bool                // if true then boxes will be set up
           25  +	Header     bool                // Print a heading on startup
           26  +	LineServer bool                // Start admin line server
           27  +	Flags      func(*flag.FlagSet) // function to set up flag.FlagSet
           28  +	flags      *flag.FlagSet       // flags that belong to the command
    28     29   
    29     30   }
    30     31   
    31     32   // CommandFunc is the function that executes the command.
    32     33   // It accepts the parsed command line parameters.
    33     34   // It returns the exit code and an error.
    34     35   type CommandFunc func(*flag.FlagSet, *meta.Meta) (int, error)

Changes to cmd/fd_limit_raise.go.

    37     37   		return err
    38     38   	}
    39     39   	err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
    40     40   	if err != nil {
    41     41   		return err
    42     42   	}
    43     43   	if rLimit.Cur < minFiles {
    44         -		log.Printf("Make sure you have no more than %d files in all your places if you enabled notification\n", rLimit.Cur)
           44  +		log.Printf("Make sure you have no more than %d files in all your boxes if you enabled notification\n", rLimit.Cur)
    45     45   	}
    46     46   	return nil
    47     47   }

Changes to cmd/main.go.

    18     18   	"net/url"
    19     19   	"os"
    20     20   	"strconv"
    21     21   	"strings"
    22     22   
    23     23   	"zettelstore.de/z/auth"
    24     24   	"zettelstore.de/z/auth/impl"
           25  +	"zettelstore.de/z/box"
           26  +	"zettelstore.de/z/box/compbox"
           27  +	"zettelstore.de/z/box/manager"
    25     28   	"zettelstore.de/z/config"
    26     29   	"zettelstore.de/z/domain/id"
    27     30   	"zettelstore.de/z/domain/meta"
    28     31   	"zettelstore.de/z/input"
    29     32   	"zettelstore.de/z/kernel"
    30         -	"zettelstore.de/z/place"
    31         -	"zettelstore.de/z/place/manager"
    32         -	"zettelstore.de/z/place/progplace"
    33     33   	"zettelstore.de/z/web/server"
    34     34   )
    35     35   
    36     36   const (
    37     37   	defConfigfile = ".zscfg"
    38     38   )
    39     39   
................................................................................
    50     50   	})
    51     51   	RegisterCommand(Command{
    52     52   		Name:   "version",
    53     53   		Func:   func(*flag.FlagSet, *meta.Meta) (int, error) { return 0, nil },
    54     54   		Header: true,
    55     55   	})
    56     56   	RegisterCommand(Command{
    57         -		Name:   "run",
    58         -		Func:   runFunc,
    59         -		Places: true,
    60         -		Header: true,
    61         -		Flags:  flgRun,
           57  +		Name:       "run",
           58  +		Func:       runFunc,
           59  +		Boxes:      true,
           60  +		Header:     true,
           61  +		LineServer: true,
           62  +		Flags:      flgRun,
    62     63   	})
    63     64   	RegisterCommand(Command{
    64     65   		Name:   "run-simple",
    65     66   		Func:   runSimpleFunc,
    66         -		Places: true,
           67  +		Boxes:  true,
    67     68   		Header: true,
    68     69   		Flags:  flgSimpleRun,
    69     70   	})
    70     71   	RegisterCommand(Command{
    71     72   		Name: "file",
    72     73   		Func: cmdFile,
    73     74   		Flags: func(fs *flag.FlagSet) {
................................................................................
   109    110   		case "d":
   110    111   			val := flg.Value.String()
   111    112   			if strings.HasPrefix(val, "/") {
   112    113   				val = "dir://" + val
   113    114   			} else {
   114    115   				val = "dir:" + val
   115    116   			}
   116         -			cfg.Set(keyPlaceOneURI, val)
          117  +			cfg.Set(keyBoxOneURI, val)
   117    118   		case "r":
   118    119   			cfg.Set(keyReadOnly, flg.Value.String())
   119    120   		case "v":
   120    121   			cfg.Set(keyVerbose, flg.Value.String())
   121    122   		}
   122    123   	})
   123    124   	return cfg
................................................................................
   129    130   		fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s)
   130    131   		return "", err
   131    132   	}
   132    133   	return strconv.Itoa(port), nil
   133    134   }
   134    135   
   135    136   const (
   136         -	keyAdminPort           = "admin-port"
   137         -	keyDefaultDirPlaceType = "default-dir-place-type"
   138         -	keyInsecureCookie      = "insecure-cookie"
   139         -	keyListenAddr          = "listen-addr"
   140         -	keyOwner               = "owner"
   141         -	keyPersistentCookie    = "persistent-cookie"
   142         -	keyPlaceOneURI         = kernel.PlaceURIs + "1"
   143         -	keyReadOnly            = "read-only-mode"
   144         -	keyTokenLifetimeHTML   = "token-lifetime-html"
   145         -	keyTokenLifetimeAPI    = "token-lifetime-api"
   146         -	keyURLPrefix           = "url-prefix"
   147         -	keyVerbose             = "verbose"
          137  +	keyAdminPort         = "admin-port"
          138  +	keyDefaultDirBoxType = "default-dir-box-type"
          139  +	keyInsecureCookie    = "insecure-cookie"
          140  +	keyListenAddr        = "listen-addr"
          141  +	keyOwner             = "owner"
          142  +	keyPersistentCookie  = "persistent-cookie"
          143  +	keyBoxOneURI         = kernel.BoxURIs + "1"
          144  +	keyReadOnly          = "read-only-mode"
          145  +	keyTokenLifetimeHTML = "token-lifetime-html"
          146  +	keyTokenLifetimeAPI  = "token-lifetime-api"
          147  +	keyURLPrefix         = "url-prefix"
          148  +	keyVerbose           = "verbose"
   148    149   )
   149    150   
   150    151   func setServiceConfig(cfg *meta.Meta) error {
   151    152   	ok := setConfigValue(true, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose))
   152    153   	if val, found := cfg.Get(keyAdminPort); found {
   153    154   		ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val)
   154    155   	}
   155    156   
   156    157   	ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, ""))
   157    158   	ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly))
   158    159   
   159    160   	ok = setConfigValue(
   160         -		ok, kernel.PlaceService, kernel.PlaceDefaultDirType,
   161         -		cfg.GetDefault(keyDefaultDirPlaceType, kernel.PlaceDirTypeNotify))
   162         -	ok = setConfigValue(ok, kernel.PlaceService, kernel.PlaceURIs+"1", "dir:./zettel")
   163         -	format := kernel.PlaceURIs + "%v"
          161  +		ok, kernel.BoxService, kernel.BoxDefaultDirType,
          162  +		cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify))
          163  +	ok = setConfigValue(ok, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel")
          164  +	format := kernel.BoxURIs + "%v"
   164    165   	for i := 1; ; i++ {
   165    166   		key := fmt.Sprintf(format, i)
   166    167   		val, found := cfg.Get(key)
   167    168   		if !found {
   168    169   			break
   169    170   		}
   170         -		ok = setConfigValue(ok, kernel.PlaceService, key, val)
          171  +		ok = setConfigValue(ok, kernel.BoxService, key, val)
   171    172   	}
   172    173   
   173    174   	ok = setConfigValue(
   174    175   		ok, kernel.WebService, kernel.WebListenAddress,
   175    176   		cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
   176    177   	ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/"))
   177    178   	ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie))
................................................................................
   191    192   	done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val))
   192    193   	if !done {
   193    194   		kernel.Main.Log("unable to set configuration:", key, val)
   194    195   	}
   195    196   	return ok && done
   196    197   }
   197    198   
   198         -func setupOperations(cfg *meta.Meta, withPlaces bool) {
   199         -	var createManager kernel.CreatePlaceManagerFunc
   200         -	if withPlaces {
          199  +func setupOperations(cfg *meta.Meta, withBoxes bool) {
          200  +	var createManager kernel.CreateBoxManagerFunc
          201  +	if withBoxes {
   201    202   		err := raiseFdLimit()
   202    203   		if err != nil {
   203    204   			srvm := kernel.Main
   204    205   			srvm.Log("Raising some limitions did not work:", err)
   205    206   			srvm.Log("Prepare to encounter errors. Most of them can be mitigated. See the manual for details")
   206         -			srvm.SetConfig(kernel.PlaceService, kernel.PlaceDefaultDirType, kernel.PlaceDirTypeSimple)
          207  +			srvm.SetConfig(kernel.BoxService, kernel.BoxDefaultDirType, kernel.BoxDirTypeSimple)
   207    208   		}
   208         -		createManager = func(placeURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (place.Manager, error) {
   209         -			progplace.Setup(cfg)
   210         -			return manager.New(placeURIs, authManager, rtConfig)
          209  +		createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) {
          210  +			compbox.Setup(cfg)
          211  +			return manager.New(boxURIs, authManager, rtConfig)
   211    212   		}
   212    213   	} else {
   213         -		createManager = func([]*url.URL, auth.Manager, config.Config) (place.Manager, error) { return nil, nil }
          214  +		createManager = func([]*url.URL, auth.Manager, config.Config) (box.Manager, error) { return nil, nil }
   214    215   	}
   215    216   
   216    217   	kernel.Main.SetCreators(
   217    218   		func(readonly bool, owner id.Zid) (auth.Manager, error) {
   218    219   			return impl.New(readonly, owner, cfg.GetDefault("secret", "")), nil
   219    220   		},
   220    221   		createManager,
   221         -		func(srv server.Server, plMgr place.Manager, authMgr auth.Manager, rtConfig config.Config) error {
          222  +		func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error {
   222    223   			setupRouting(srv, plMgr, authMgr, rtConfig)
   223    224   			return nil
   224    225   		},
   225    226   	)
   226    227   }
   227    228   
   228    229   func executeCommand(name string, args ...string) int {
................................................................................
   237    238   		return 1
   238    239   	}
   239    240   	cfg := getConfig(fs)
   240    241   	if err := setServiceConfig(cfg); err != nil {
   241    242   		fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
   242    243   		return 2
   243    244   	}
   244         -	setupOperations(cfg, command.Places)
   245         -	kernel.Main.Start(command.Header)
          245  +	setupOperations(cfg, command.Boxes)
          246  +	kernel.Main.Start(command.Header, command.LineServer)
   246    247   	exitCode, err := command.Func(fs, cfg)
   247    248   	if err != nil {
   248    249   		fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
   249    250   	}
   250    251   	kernel.Main.Shutdown(true)
   251    252   	return exitCode
   252    253   }

Changes to cmd/register.go.

     9      9   //-----------------------------------------------------------------------------
    10     10   
    11     11   // Package cmd provides command generic functions.
    12     12   package cmd
    13     13   
    14     14   // Mention all needed encoders, parsers and stores to have them registered.
    15     15   import (
           16  +	_ "zettelstore.de/z/box/compbox"       // Allow to use computed box.
           17  +	_ "zettelstore.de/z/box/constbox"      // Allow to use global internal box.
           18  +	_ "zettelstore.de/z/box/dirbox"        // Allow to use directory box.
           19  +	_ "zettelstore.de/z/box/filebox"       // Allow to use file box.
           20  +	_ "zettelstore.de/z/box/membox"        // Allow to use in-memory box.
    16     21   	_ "zettelstore.de/z/encoder/htmlenc"   // Allow to use HTML encoder.
    17     22   	_ "zettelstore.de/z/encoder/jsonenc"   // Allow to use JSON encoder.
    18     23   	_ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder.
    19     24   	_ "zettelstore.de/z/encoder/rawenc"    // Allow to use raw encoder.
    20     25   	_ "zettelstore.de/z/encoder/textenc"   // Allow to use text encoder.
    21     26   	_ "zettelstore.de/z/encoder/zmkenc"    // Allow to use zmk encoder.
    22     27   	_ "zettelstore.de/z/kernel/impl"       // Allow kernel implementation to create itself
    23     28   	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
    24     29   	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
    25     30   	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
    26     31   	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
    27     32   	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
    28         -	_ "zettelstore.de/z/place/constplace"  // Allow to use global internal place.
    29         -	_ "zettelstore.de/z/place/dirplace"    // Allow to use directory place.
    30         -	_ "zettelstore.de/z/place/fileplace"   // Allow to use file place.
    31         -	_ "zettelstore.de/z/place/memplace"    // Allow to use memory place.
    32         -	_ "zettelstore.de/z/place/progplace"   // Allow to use computed place.
    33     33   )

Changes to collect/collect.go.

     7      7   // Public License). Please see file LICENSE.txt for your rights and obligations
     8      8   // under this license.
     9      9   //-----------------------------------------------------------------------------
    10     10   
    11     11   // Package collect provides functions to collect items from a syntax tree.
    12     12   package collect
    13     13   
    14         -import (
    15         -	"zettelstore.de/z/ast"
    16         -)
           14  +import "zettelstore.de/z/ast"
    17     15   
    18     16   // Summary stores the relevant parts of the syntax tree
    19     17   type Summary struct {
    20     18   	Links  []*ast.Reference // list of all referenced links
    21     19   	Images []*ast.Reference // list of all referenced images
    22     20   	Cites  []*ast.CiteNode  // list of all referenced citations
    23     21   }
    24     22   
    25     23   // References returns all references mentioned in the given zettel. This also
    26     24   // includes references to images.
    27         -func References(zn *ast.ZettelNode) Summary {
    28         -	lv := linkVisitor{}
    29         -	ast.NewTopDownTraverser(&lv).VisitBlockSlice(zn.Ast)
    30         -	return lv.summary
           25  +func References(zn *ast.ZettelNode) (s Summary) {
           26  +	ast.WalkBlockSlice(&s, zn.Ast)
           27  +	return s
    31     28   }
    32     29   
    33         -type linkVisitor struct {
    34         -	summary Summary
    35         -}
    36         -
    37         -// VisitVerbatim does nothing.
    38         -func (lv *linkVisitor) VisitVerbatim(vn *ast.VerbatimNode) {}
    39         -
    40         -// VisitRegion does nothing.
    41         -func (lv *linkVisitor) VisitRegion(rn *ast.RegionNode) {}
    42         -
    43         -// VisitHeading does nothing.
    44         -func (lv *linkVisitor) VisitHeading(hn *ast.HeadingNode) {}
    45         -
    46         -// VisitHRule does nothing.
    47         -func (lv *linkVisitor) VisitHRule(hn *ast.HRuleNode) {}
    48         -
    49         -// VisitList does nothing.
    50         -func (lv *linkVisitor) VisitNestedList(ln *ast.NestedListNode) {}
    51         -
    52         -// VisitDescriptionList does nothing.
    53         -func (lv *linkVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {}
    54         -
    55         -// VisitPara does nothing.
    56         -func (lv *linkVisitor) VisitPara(pn *ast.ParaNode) {}
    57         -
    58         -// VisitTable does nothing.
    59         -func (lv *linkVisitor) VisitTable(tn *ast.TableNode) {}
    60         -
    61         -// VisitBLOB does nothing.
    62         -func (lv *linkVisitor) VisitBLOB(bn *ast.BLOBNode) {}
    63         -
    64         -// VisitText does nothing.
    65         -func (lv *linkVisitor) VisitText(tn *ast.TextNode) {}
    66         -
    67         -// VisitTag does nothing.
    68         -func (lv *linkVisitor) VisitTag(tn *ast.TagNode) {}
    69         -
    70         -// VisitSpace does nothing.
    71         -func (lv *linkVisitor) VisitSpace(sn *ast.SpaceNode) {}
    72         -
    73         -// VisitBreak does nothing.
    74         -func (lv *linkVisitor) VisitBreak(bn *ast.BreakNode) {}
    75         -
    76         -// VisitLink collects the given link as a reference.
    77         -func (lv *linkVisitor) VisitLink(ln *ast.LinkNode) {
    78         -	lv.summary.Links = append(lv.summary.Links, ln.Ref)
    79         -}
    80         -
    81         -// VisitImage collects the image links as a reference.
    82         -func (lv *linkVisitor) VisitImage(in *ast.ImageNode) {
    83         -	if in.Ref != nil {
    84         -		lv.summary.Images = append(lv.summary.Images, in.Ref)
           30  +// Visit all node to collect data for the summary.
           31  +func (s *Summary) Visit(node ast.Node) ast.Visitor {
           32  +	switch n := node.(type) {
           33  +	case *ast.LinkNode:
           34  +		s.Links = append(s.Links, n.Ref)
           35  +	case *ast.ImageNode:
           36  +		if n.Ref != nil {
           37  +			s.Images = append(s.Images, n.Ref)
           38  +		}
           39  +	case *ast.CiteNode:
           40  +		s.Cites = append(s.Cites, n)
    85     41   	}
           42  +	return s
    86     43   }
    87         -
    88         -// VisitCite collects the citation.
    89         -func (lv *linkVisitor) VisitCite(cn *ast.CiteNode) {
    90         -	lv.summary.Cites = append(lv.summary.Cites, cn)
    91         -}
    92         -
    93         -// VisitFootnote does nothing.
    94         -func (lv *linkVisitor) VisitFootnote(fn *ast.FootnoteNode) {}
    95         -
    96         -// VisitMark does nothing.
    97         -func (lv *linkVisitor) VisitMark(mn *ast.MarkNode) {}
    98         -
    99         -// VisitFormat does nothing.
   100         -func (lv *linkVisitor) VisitFormat(fn *ast.FormatNode) {}
   101         -
   102         -// VisitLiteral does nothing.
   103         -func (lv *linkVisitor) VisitLiteral(ln *ast.LiteralNode) {}

Changes to collect/collect_test.go.

    23     23   	if !r.IsValid() {
    24     24   		panic(s)
    25     25   	}
    26     26   	return r
    27     27   }
    28     28   
    29     29   func TestLinks(t *testing.T) {
           30  +	t.Parallel()
    30     31   	zn := &ast.ZettelNode{}
    31     32   	summary := collect.References(zn)
    32     33   	if summary.Links != nil || summary.Images != nil {
    33     34   		t.Error("No links/images expected, but got:", summary.Links, "and", summary.Images)
    34     35   	}
    35     36   
    36     37   	intNode := &ast.LinkNode{Ref: parseRef("01234567890123")}
................................................................................
    50     51   	summary = collect.References(zn)
    51     52   	if cnt := len(summary.Links); cnt != 3 {
    52     53   		t.Error("Link count does not work. Expected: 3, got", summary.Links)
    53     54   	}
    54     55   }
    55     56   
    56     57   func TestImage(t *testing.T) {
           58  +	t.Parallel()
    57     59   	zn := &ast.ZettelNode{
    58     60   		Ast: ast.BlockSlice{
    59     61   			&ast.ParaNode{
    60     62   				Inlines: ast.InlineSlice{
    61     63   					&ast.ImageNode{Ref: parseRef("12345678901234")},
    62     64   				},
    63     65   			},

Changes to collect/order.go.

    13     13   
    14     14   import "zettelstore.de/z/ast"
    15     15   
    16     16   // Order of internal reference within the given zettel.
    17     17   func Order(zn *ast.ZettelNode) (result []*ast.Reference) {
    18     18   	for _, bn := range zn.Ast {
    19     19   		if ln, ok := bn.(*ast.NestedListNode); ok {
    20         -			switch ln.Code {
           20  +			switch ln.Kind {
    21     21   			case ast.NestedListOrdered, ast.NestedListUnordered:
    22     22   				for _, is := range ln.Items {
    23     23   					if ref := firstItemZettelReference(is); ref != nil {
    24     24   						result = append(result, ref)
    25     25   					}
    26     26   				}
    27     27   			}

Changes to config/config.go.

    52     52   
    53     53   	// GetMarkerExternal returns the current value of the "marker-external" key.
    54     54   	GetMarkerExternal() string
    55     55   
    56     56   	// GetFooterHTML returns HTML code that should be embedded into the footer
    57     57   	// of each WebUI page.
    58     58   	GetFooterHTML() string
    59         -
    60         -	// GetListPageSize returns the maximum length of a list to be returned in WebUI.
    61         -	// A value less or equal to zero signals no limit.
    62         -	GetListPageSize() int
    63     59   }
    64     60   
    65     61   // AuthConfig are relevant configuration values for authentication.
    66     62   type AuthConfig interface {
    67     63   	// GetExpertMode returns the current value of the "expert-mode" key
    68     64   	GetExpertMode() bool
    69     65   

Changes to docs/manual/00001002000000.zettel.

    11     11   : It should be not hard to write other software that works with your zettel.
    12     12   ; Single user
    13     13   : All zettel belong to you, only to you.
    14     14     Zettelstore provides its services only to one person: you.
    15     15     If your device is securely configured, there should be no risk that others are able to read or update your zettel.
    16     16   : If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel.
    17     17   ; Ease of installation
    18         -: If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate place and start working.
           18  +: If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working.
    19     19   : Upgrading the software is done just by replacing the executable with a newer one.
    20     20   ; Ease of operation
    21         -: There is only one executable for Zettelstore and one directory, where your zettel are placed.
           21  +: There is only one executable for Zettelstore and one directory, where your zettel are stored.
    22     22   : If you decide to use multiple directories, you are free to configure Zettelstore appropriately.
    23     23   ; Multiple modes of operation
    24     24   : You can use Zettelstore as a standalone software on your device, but you are not restricted to it.
    25     25   : You can install the software on a central server, or you can install it on all your devices with no restrictions how to synchronize your zettel.
    26     26   ; Multiple user interfaces
    27     27   : Zettelstore provides a default web-based user interface.
    28     28     Anybody can provide alternative user interfaces, e.g. for special purposes.
    29     29   ; Simple service
    30     30   : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them.
    31     31   : External software can be written to deeply analyze your zettel and the structures they form.

Changes to docs/manual/00001003000000.zettel.

     5      5   syntax: zmk
     6      6   
     7      7   === The curious user
     8      8   You just want to check out the Zettelstore software
     9      9   
    10     10   * Grab the appropriate executable and copy it into any directory
    11     11   * Start the Zettelstore software, e.g. with a double click
    12         -* A sub-directory ""zettel"" will be created in the directory where you placed the executable.
           12  +* A sub-directory ""zettel"" will be created in the directory where you put the executable.
    13     13     It will contain your future zettel.
    14     14   * Open the URI [[http://localhost:23123]] with your web browser.
    15     15     It will present you a mostly empty Zettelstore.
    16     16     There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information.
    17     17   * Please read the instructions for the web-based user interface and learn about the various ways to write zettel.
    18     18   * If you restart your device, please make sure to start your Zettelstore again.
    19     19   
................................................................................
    39     39   ```sh
    40     40   # sudo useradd --system --gid zettelstore \
    41     41       --create-home --home-dir /var/lib/zettelstore \
    42     42       --shell /usr/sbin/nologin \
    43     43       --comment "Zettelstore server" \
    44     44       zettelstore
    45     45   ```
    46         -Create a systemd service file and place it into ''/etc/systemd/system/zettelstore.service'':
           46  +Create a systemd service file and store it into ''/etc/systemd/system/zettelstore.service'':
    47     47   ```ini
    48     48   [Unit]
    49     49   Description=Zettelstore
    50     50   After=network.target
    51     51   
    52     52   [Service]
    53     53   Type=simple

Changes to docs/manual/00001004010000.zettel.

     1      1   id: 00001004010000
     2      2   title: Zettelstore startup configuration
     3      3   role: manual
     4      4   tags: #configuration #manual #zettelstore
     5      5   syntax: zmk
     6         -modified: 20210525121644
            6  +modified: 20210712234656
     7      7   
     8      8   The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options.
     9      9   These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons.
    10         -For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are placed.
           10  +For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored.
    11     11   An attacker that is able to change the owner can do anything.
    12     12   Therefore only the owner of the computer on which Zettelstore runs can change this information.
    13     13   
    14     14   The file for startup configuration must be created via a text editor in advance.
    15     15   
    16     16   The syntax of the configuration file is the same as for any zettel metadata.
    17     17   The following keys are supported:
    18     18   
    19     19   ; [!admin-port]''admin-port''
    20     20   : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]].
    21         -  A value of ''0'' (the default) disables the administrators console.
           21  +  A value of ''0'' (the default) disables the administrator console.
           22  +  The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]].
    22     23   
    23     24     On most operating systems, the value must be greater than ''1024'' unless you start Zettelstore with the full privileges of a system administrator (which is not recommended).
    24     25   
    25     26     Default: ''0''
    26         -; [!default-dir-place-type]''default-dir-place-type''
    27         -: Specifies the default value for the (sub-) type of [[directory places|00001004011400#type]].
    28         -  Zettel are typically stored in such places.
           27  +; [!box-uri-x]''box-uri-//X//'', where //X// is a number greater or equal to one
           28  +: Specifies a [[box|00001004011200]] where zettel are stored.
           29  +  During startup //X// is counted up, starting with one, until no key is found.
           30  +  This allows to configure more than one box.
           31  +
           32  +  If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ''dir://.zettel''.
           33  +  In this case, even a key ''box-uri-2'' will be ignored.
           34  +; [!default-dir-box-type]''default-dir-box-type''
           35  +: Specifies the default value for the (sub-) type of [[directory boxes|00001004011400#type]].
           36  +  Zettel are typically stored in such boxes.
    29     37   
    30     38     Default: ''notify''
    31     39   ; [!insecure-cookie]''insecure-cookie''
    32     40   : Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP).
    33     41     Otherwise web browser are free to ignore the authentication cookie.
    34     42   
    35     43     Default: ''false''
................................................................................
    48     56     On these devices, the operating system is free to stop the web browser and to remove temporary cookies.
    49     57     Therefore, an authenticated user will be logged off.
    50     58   
    51     59     If ''true'', a persistent cookie is used.
    52     60     Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds.
    53     61   
    54     62     Default: ''false''
    55         -; [!place-uri-X]''place-uri-//X//'', where //X// is a number greater or equal to one
    56         -: Specifies a [[place|00001004011200]] where zettel are stored.
    57         -  During startup //X// is counted up, starting with one, until no key is found.
    58         -  This allows to configure more than one place.
    59         -
    60         -  If no ''place-uri-1'' key is given, the overall effect will be the same as if only ''place-uri-1'' was specified with the value ''dir://.zettel''.
    61         -  In this case, even a key ''place-uri-2'' will be ignored.
    62     63   ; [!read-only-mode]''read-only-mode''