DELETED .deepsource.toml Index: .deepsource.toml ================================================================== --- .deepsource.toml +++ .deepsource.toml @@ -1,8 +0,0 @@ -version = 1 - -[[analyzers]] -name = "go" -enabled = true - - [analyzers.meta] -import_paths = ["github.com/zettelstore/zettelstore"] Index: .fossil-settings/ignore-glob ================================================================== --- .fossil-settings/ignore-glob +++ .fossil-settings/ignore-glob @@ -1,2 +1,3 @@ bin/* releases/* +parser/pikchr/*.out Index: LICENSE.txt ================================================================== --- LICENSE.txt +++ LICENSE.txt @@ -1,6 +1,6 @@ -Copyright (c) 2020-2022 Detlef Stern +Copyright (c) 2020-present Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the Index: Makefile ================================================================== --- Makefile +++ Makefile @@ -1,9 +1,9 @@ -## Copyright (c) 2020-2021 Detlef Stern +## Copyright (c) 2020-present Detlef Stern ## -## This file is part of zettelstore. +## This file is part of Zettelstore. ## ## Zettelstore is licensed under the latest version of the EUPL (European Union ## Public License). Please see file LICENSE.txt for your rights and obligations ## under this license. Index: README.md ================================================================== --- README.md +++ README.md @@ -21,6 +21,6 @@ The software, including the manual, is licensed under the [European Union Public License 1.2 (or later)](https://zettelstore.de/home/file?name=LICENSE.txt&ci=trunk). -[Stay tuned](https://twitter.com/zettelstore)… +[Stay tuned](https://mastodon.social/tags/Zettelstore) … Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.5.0 +0.14.0 Index: ast/ast.go ================================================================== --- ast/ast.go +++ ast/ast.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -12,20 +12,20 @@ package ast import ( "net/url" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // ZettelNode is the root node of the abstract syntax tree. // It is *not* part of the visitor pattern. type ZettelNode struct { Meta *meta.Meta // Original metadata - Content domain.Content // Original content + Content zettel.Content // Original content Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. Ast BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. Syntax string // Syntax / parser that produced the Ast } @@ -82,7 +82,8 @@ RefStateSelf // Reference to same zettel with a fragment RefStateFound // Reference to an existing internal zettel, URL is ajusted RefStateBroken // Reference to a non-existing internal zettel RefStateHosted // Reference to local hosted non-Zettel, without URL change RefStateBased // Reference to local non-Zettel, to be prefixed + RefStateQuery // Reference to a zettel query RefStateExternal // Reference to external material ) Index: ast/block.go ================================================================== --- ast/block.go +++ ast/block.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -8,11 +8,11 @@ // under this license. //----------------------------------------------------------------------------- package ast -import "zettelstore.de/c/attrs" +import "zettelstore.de/client.fossil/attrs" // Definition of Block nodes. // BlockSlice is a slice of BlockNodes. type BlockSlice []BlockNode @@ -88,15 +88,10 @@ func (*VerbatimNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ } -// Supported syntax values for VerbatimEval. -const ( - VerbatimEvalSyntaxDraw = "draw" -) - //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { Kind RegionKind @@ -270,11 +265,12 @@ //-------------------------------------------------------------------------- // TranscludeNode specifies block content from other zettel to embedded in // current zettel type TranscludeNode struct { - Ref *Reference + Attrs attrs.Attributes + Ref *Reference } func (*TranscludeNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. @@ -283,14 +279,14 @@ //-------------------------------------------------------------------------- // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { - Title string - Syntax string - Blob []byte + Description InlineSlice + Syntax string + Blob []byte } func (*BLOBNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ } Index: ast/inline.go ================================================================== --- ast/inline.go +++ ast/inline.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -11,11 +11,11 @@ package ast import ( "unicode/utf8" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/attrs" ) // Definitions of inline nodes. // InlineSlice is a list of BlockNodes. @@ -53,22 +53,10 @@ func (*TextNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*TextNode) WalkChildren(Visitor) { /* No children*/ } -// -------------------------------------------------------------------------- - -// TagNode contains a tag. -type TagNode struct { - Tag string // The text itself. -} - -func (*TagNode) inlineNode() { /* Just a marker */ } - -// WalkChildren does nothing. -func (*TagNode) WalkChildren(Visitor) { /* No children*/ } - // -------------------------------------------------------------------------- // SpaceNode tracks inter-word space characters. type SpaceNode struct { Lexeme string Index: ast/ref.go ================================================================== --- ast/ref.go +++ ast/ref.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -10,20 +10,26 @@ package ast import ( "net/url" + "strings" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/zettel/id" ) + +// QueryPrefix is the prefix that denotes a query expression. +const QueryPrefix = "query:" // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { - switch s { - case "", "00000000000000": + if invalidReference(s) { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } + if strings.HasPrefix(s, QueryPrefix) { + return &Reference{URL: nil, Value: s[len(QueryPrefix):], State: RefStateQuery} + } if state, ok := localState(s); ok { if state == RefStateBased { s = s[1:] } u, err := url.Parse(s) @@ -33,20 +39,25 @@ } u, err := url.Parse(s) if err != nil { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } - if len(u.Scheme)+len(u.Opaque)+len(u.Host) == 0 && u.User == nil { + if !externalURL(u) { if _, err = id.Parse(u.Path); err == nil { return &Reference{URL: u, Value: s, State: RefStateZettel} } if u.Path == "" && u.Fragment != "" { return &Reference{URL: u, Value: s, State: RefStateSelf} } } return &Reference{URL: u, Value: s, State: RefStateExternal} } + +func invalidReference(s string) bool { return s == "" || s == "00000000000000" } +func externalURL(u *url.URL) bool { + return u.Scheme != "" || u.Opaque != "" || u.Host != "" || u.User != nil +} func localState(path string) (RefState, bool) { if len(path) > 0 && path[0] == '/' { if len(path) > 1 && path[1] == '/' { return RefStateBased, true @@ -64,10 +75,13 @@ // String returns the string representation of a reference. func (r Reference) String() string { if r.URL != nil { return r.URL.String() + } + if r.State == RefStateQuery { + return QueryPrefix + r.Value } return r.Value } // IsValid returns true if reference is valid Index: ast/ref_test.go ================================================================== --- ast/ref_test.go +++ ast/ref_test.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations Index: ast/walk.go ================================================================== --- ast/walk.go +++ ast/walk.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations Index: ast/walk_test.go ================================================================== --- ast/walk_test.go +++ ast/walk_test.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -11,11 +11,11 @@ package ast_test import ( "testing" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" ) func BenchmarkWalk(b *testing.B) { root := ast.BlockSlice{ Index: auth/auth.go ================================================================== --- auth/auth.go +++ auth/auth.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -14,13 +14,12 @@ import ( "time" "zettelstore.de/z/box" "zettelstore.de/z/config" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/web/server" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // BaseManager allows to check some base auth modes. type BaseManager interface { // IsReadonly returns true, if the systems is configured to run in read-only-mode. @@ -41,12 +40,12 @@ type TokenKind int // Allowed values of token kind const ( _ TokenKind = iota - KindJSON - KindHTML + KindAPI + KindwebUI ) // TokenData contains some important elements from a token. type TokenData struct { Token []byte @@ -77,11 +76,11 @@ // Manager is the main interface for providing the service. type Manager interface { TokenManager AuthzManager - BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy) + BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, Policy) } // Policy is an interface for checking access authorization. type Policy interface { // User is allowed to create a new zettel. Index: auth/cred/cred.go ================================================================== --- auth/cred/cred.go +++ auth/cred/cred.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -13,11 +13,11 @@ import ( "bytes" "golang.org/x/crypto/bcrypt" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/zettel/id" ) // HashCredential returns a hashed vesion of the given credential func HashCredential(zid id.Zid, ident, credential string) (string, error) { fullCredential := createFullCredential(zid, ident, credential) @@ -42,12 +42,12 @@ return false, err } func createFullCredential(zid id.Zid, ident, credential string) []byte { var buf bytes.Buffer - buf.WriteString(zid.String()) + buf.Write(zid.Bytes()) buf.WriteByte(' ') buf.WriteString(ident) buf.WriteByte(' ') buf.WriteString(credential) return buf.Bytes() } ADDED auth/impl/digest.go Index: auth/impl/digest.go ================================================================== --- auth/impl/digest.go +++ auth/impl/digest.go @@ -0,0 +1,83 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +//----------------------------------------------------------------------------- + +package impl + +import ( + "bytes" + "crypto" + "crypto/hmac" + "encoding/base64" + + "zettelstore.de/sx.fossil" + "zettelstore.de/sx.fossil/sxreader" +) + +var encoding = base64.RawURLEncoding + +const digestAlg = crypto.SHA384 + +func sign(claim sx.Object, secret []byte) ([]byte, error) { + var buf bytes.Buffer + sx.Print(&buf, claim) + token := make([]byte, encoding.EncodedLen(buf.Len())) + encoding.Encode(token, buf.Bytes()) + + digest := hmac.New(digestAlg.New, secret) + _, err := digest.Write(buf.Bytes()) + if err != nil { + return nil, err + } + dig := digest.Sum(nil) + encDig := make([]byte, encoding.EncodedLen(len(dig))) + encoding.Encode(encDig, dig) + + token = append(token, '.') + token = append(token, encDig...) + return token, nil +} + +func check(token []byte, secret []byte) (sx.Object, error) { + i := bytes.IndexByte(token, '.') + if i <= 0 || 1024 < i { + return nil, ErrMalformedToken + } + buf := make([]byte, len(token)) + n, err := encoding.Decode(buf, token[:i]) + if err != nil { + return nil, err + } + rdr := sxreader.MakeReader(bytes.NewReader(buf[:n])) + obj, err := rdr.Read() + if err != nil { + return nil, err + } + + var objBuf bytes.Buffer + _, err = sx.Print(&objBuf, obj) + if err != nil { + return nil, err + } + + digest := hmac.New(digestAlg.New, secret) + _, err = digest.Write(objBuf.Bytes()) + if err != nil { + return nil, err + } + + n, err = encoding.Decode(buf, token[i+1:]) + if err != nil { + return nil, err + } + if !hmac.Equal(buf[:n], digest.Sum(nil)) { + return nil, ErrMalformedToken + } + return obj, nil +} Index: auth/impl/impl.go ================================================================== --- auth/impl/impl.go +++ auth/impl/impl.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -15,21 +15,20 @@ "errors" "hash/fnv" "io" "time" - "github.com/pascaldekloe/jwt" - - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/sexp" + "zettelstore.de/sx.fossil" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/box" "zettelstore.de/z/config" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/web/server" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) type myAuth struct { readonly bool owner id.Zid @@ -66,11 +65,12 @@ } // IsReadonly returns true, if the systems is configured to run in read-only-mode. func (a *myAuth) IsReadonly() bool { return a.readonly } -const reqHash = jwt.HS512 +// ErrMalformedToken signals a broken token. +var ErrMalformedToken = errors.New("auth: malformed token") // ErrNoIdent signals that the 'ident' key is missing. var ErrNoIdent = errors.New("auth: missing ident") // ErrOtherKind signals that the token was defined for another token kind. @@ -85,68 +85,66 @@ if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) - claims := jwt.Claims{ - Registered: jwt.Registered{ - Subject: subject, - Expires: jwt.NewNumericTime(now.Add(d)), - Issued: jwt.NewNumericTime(now), - }, - Set: map[string]interface{}{ - "zid": ident.Zid.String(), - "_tk": int(kind), - }, - } - token, err := claims.HMACSign(reqHash, a.secret) - if err != nil { - return nil, err - } - return token, nil + sClaim := sx.MakeList( + sx.Int64(kind), + sx.String(subject), + sx.Int64(now.Unix()), + sx.Int64(now.Add(d).Unix()), + sx.Int64(ident.Zid), + ) + return sign(sClaim, a.secret) } // ErrTokenExpired signals an exired token var ErrTokenExpired = errors.New("auth: token expired") // CheckToken checks the validity of the token and returns relevant data. -func (a *myAuth) CheckToken(token []byte, k auth.TokenKind) (auth.TokenData, error) { - h, err := jwt.NewHMAC(reqHash, a.secret) - if err != nil { - return auth.TokenData{}, err - } - claims, err := h.Check(token) - if err != nil { - return auth.TokenData{}, err - } - now := time.Now().Round(time.Second) - expires := claims.Expires.Time() - if expires.Before(now) { - return auth.TokenData{}, ErrTokenExpired - } - ident := claims.Subject +func (a *myAuth) CheckToken(tok []byte, k auth.TokenKind) (auth.TokenData, error) { + var tokenData auth.TokenData + + obj, err := check(tok, a.secret) + if err != nil { + return tokenData, err + } + + tokenData.Token = tok + err = setupTokenData(obj, k, &tokenData) + return tokenData, err +} + +func setupTokenData(obj sx.Object, k auth.TokenKind, tokenData *auth.TokenData) error { + vals, err := sexp.ParseList(obj, "isiii") + if err != nil { + return ErrMalformedToken + } + if auth.TokenKind(vals[0].(sx.Int64)) != k { + return ErrOtherKind + } + ident := vals[1].(sx.String) if ident == "" { - return auth.TokenData{}, ErrNoIdent - } - if zidS, ok := claims.Set["zid"].(string); ok { - if zid, err2 := id.Parse(zidS); err2 == nil { - if kind, ok2 := claims.Set["_tk"].(float64); ok2 { - if auth.TokenKind(kind) == k { - return auth.TokenData{ - Token: token, - Now: now, - Issued: claims.Issued.Time(), - Expires: expires, - Ident: ident, - Zid: zid, - }, nil - } - } - return auth.TokenData{}, ErrOtherKind - } - } - return auth.TokenData{}, ErrNoZid + return ErrNoIdent + } + issued := time.Unix(int64(vals[2].(sx.Int64)), 0) + expires := time.Unix(int64(vals[3].(sx.Int64)), 0) + now := time.Now().Round(time.Second) + if expires.Before(now) { + return ErrTokenExpired + } + zid := id.Zid(vals[4].(sx.Int64)) + if !zid.IsValid() { + return ErrNoZid + } + + tokenData.Ident = ident.String() + tokenData.Issued = issued + tokenData.Now = now + tokenData.Expires = expires + tokenData.Zid = zid + return nil } func (a *myAuth) Owner() id.Zid { return a.owner } func (a *myAuth) IsOwner(zid id.Zid) bool { @@ -172,8 +170,8 @@ } } return meta.UserRoleReader } -func (a *myAuth) BoxWithPolicy(auth server.Auth, unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { - return policy.BoxWithPolicy(auth, a, unprotectedBox, rtConfig) +func (a *myAuth) BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { + return policy.BoxWithPolicy(a, unprotectedBox, rtConfig) } Index: auth/policy/anon.go ================================================================== --- auth/policy/anon.go +++ auth/policy/anon.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -11,11 +11,11 @@ package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" - "zettelstore.de/z/domain/meta" + "zettelstore.de/z/zettel/meta" ) type anonPolicy struct { authConfig config.AuthConfig pre auth.Policy Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -14,39 +14,32 @@ "context" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" + "zettelstore.de/z/query" "zettelstore.de/z/web/server" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // BoxWithPolicy wraps the given box inside a policy box. -func BoxWithPolicy( - auth server.Auth, - manager auth.AuthzManager, - box box.Box, - authConfig config.AuthConfig, -) (box.Box, auth.Policy) { +func BoxWithPolicy(manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig) (box.Box, auth.Policy) { pol := newPolicy(manager, authConfig) - return newBox(auth, box, pol), pol + return newBox(box, pol), pol } // polBox implements a policy box. type polBox struct { - auth server.Auth box box.Box policy auth.Policy } // newBox creates a new policy box. -func newBox(auth server.Auth, box box.Box, policy auth.Policy) box.Box { +func newBox(box box.Box, policy auth.Policy) box.Box { return &polBox{ - auth: auth, box: box, policy: policy, } } @@ -56,77 +49,73 @@ func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } -func (pp *polBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { - user := pp.auth.GetUser(ctx) +func (pp *polBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { + user := server.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.box.CreateZettel(ctx, zettel) } return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid) } -func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { - zettel, err := pp.box.GetZettel(ctx, zid) +func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { + z, err := pp.box.GetZettel(ctx, zid) if err != nil { - return domain.Zettel{}, err + return zettel.Zettel{}, err } - user := pp.auth.GetUser(ctx) - if pp.policy.CanRead(user, zettel.Meta) { - return zettel, nil + user := server.GetUser(ctx) + if pp.policy.CanRead(user, z.Meta) { + return z, nil } - return domain.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) + return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) } -func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { +func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { return pp.box.GetAllZettel(ctx, zid) } + +func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) { + return nil, box.NewErrNotAllowed("fetch-zids", server.GetUser(ctx), id.Invalid) +} func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { return nil, err } - user := pp.auth.GetUser(ctx) + user := server.GetUser(ctx) if pp.policy.CanRead(user, m) { return m, nil } return nil, box.NewErrNotAllowed("GetMeta", user, zid) } -func (pp *polBox) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { - return pp.box.GetAllMeta(ctx, zid) -} - -func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) { - return nil, box.NewErrNotAllowed("fetch-zids", pp.auth.GetUser(ctx), id.Invalid) -} - -func (pp *polBox) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { - user := pp.auth.GetUser(ctx) +func (pp *polBox) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { + user := server.GetUser(ctx) canRead := pp.policy.CanRead - s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) - return pp.box.SelectMeta(ctx, s) + q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) + return pp.box.SelectMeta(ctx, metaSeq, q) } -func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { +func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { return pp.box.CanUpdateZettel(ctx, zettel) } -func (pp *polBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { +func (pp *polBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { zid := zettel.Meta.Zid - user := pp.auth.GetUser(ctx) + user := server.GetUser(ctx) if !zid.IsValid() { - return &box.ErrInvalidID{Zid: zid} + return box.ErrInvalidZid{Zid: zid.String()} } // Write existing zettel - oldMeta, err := pp.box.GetMeta(ctx, zid) + oldZettel, err := pp.box.GetZettel(ctx, zid) if err != nil { return err } - if pp.policy.CanWrite(user, oldMeta, zettel.Meta) { + if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) { return pp.box.UpdateZettel(ctx, zettel) } return box.NewErrNotAllowed("Write", user, zid) } @@ -133,16 +122,16 @@ func (pp *polBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return pp.box.AllowRenameZettel(ctx, zid) } func (pp *polBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - meta, err := pp.box.GetMeta(ctx, curZid) + z, err := pp.box.GetZettel(ctx, curZid) if err != nil { return err } - user := pp.auth.GetUser(ctx) - if pp.policy.CanRename(user, meta) { + user := server.GetUser(ctx) + if pp.policy.CanRename(user, z.Meta) { return pp.box.RenameZettel(ctx, curZid, newZid) } return box.NewErrNotAllowed("Rename", user, curZid) } @@ -149,23 +138,23 @@ func (pp *polBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return pp.box.CanDeleteZettel(ctx, zid) } func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error { - meta, err := pp.box.GetMeta(ctx, zid) + z, err := pp.box.GetZettel(ctx, zid) if err != nil { return err } - user := pp.auth.GetUser(ctx) - if pp.policy.CanDelete(user, meta) { + user := server.GetUser(ctx) + if pp.policy.CanDelete(user, z.Meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } func (pp *polBox) Refresh(ctx context.Context) error { - user := pp.auth.GetUser(ctx) + user := server.GetUser(ctx) if pp.policy.CanRefresh(user) { return pp.box.Refresh(ctx) } return box.NewErrNotAllowed("Refresh", user, id.Invalid) } Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -1,21 +1,21 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy import ( - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" - "zettelstore.de/z/domain/meta" + "zettelstore.de/z/zettel/meta" ) type defaultPolicy struct { manager auth.AuthzManager } Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -1,22 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy import ( - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" - "zettelstore.de/z/domain/meta" + "zettelstore.de/z/zettel/meta" ) type ownerPolicy struct { manager auth.AuthzManager authConfig config.AuthConfig Index: auth/policy/policy.go ================================================================== --- auth/policy/policy.go +++ auth/policy/policy.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -12,11 +12,11 @@ package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" - "zettelstore.de/z/domain/meta" + "zettelstore.de/z/zettel/meta" ) // newPolicy creates a policy based on given constraints. func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy { var pol auth.Policy Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -12,14 +12,14 @@ import ( "fmt" "testing" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func TestPolicies(t *testing.T) { t.Parallel() testScene := []struct { Index: auth/policy/readonly.go ================================================================== --- auth/policy/readonly.go +++ auth/policy/readonly.go @@ -1,22 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy -import "zettelstore.de/z/domain/meta" +import "zettelstore.de/z/zettel/meta" type roPolicy struct{} func (*roPolicy) CanCreate(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false } func (*roPolicy) CanRename(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanDelete(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } Index: box/box.go ================================================================== --- box/box.go +++ box/box.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -14,19 +14,17 @@ import ( "context" "errors" "fmt" "io" - "net/url" - "strconv" "time" - "zettelstore.de/c/api" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" + "zettelstore.de/client.fossil/api" + "zettelstore.de/z/query" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // BaseBox is implemented by all Zettel boxes. type BaseBox interface { // Location returns some information where the box is located. @@ -36,23 +34,20 @@ // CanCreateZettel returns true, if box could possibly create a new zettel. CanCreateZettel(ctx context.Context) bool // CreateZettel creates a new zettel. // Returns the new zettel id (and an error indication). - CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) + CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) // GetZettel retrieves a specific zettel. - GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) - - // GetMeta retrieves just the meta data of a specific zettel. - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) + GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) // CanUpdateZettel returns true, if box could possibly update the given zettel. - CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool + CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool // UpdateZettel updates an existing zettel. - UpdateZettel(ctx context.Context, zettel domain.Zettel) error + UpdateZettel(ctx context.Context, zettel zettel.Zettel) error // AllowRenameZettel returns true, if box will not disallow renaming the zettel. AllowRenameZettel(ctx context.Context, zid id.Zid) bool // RenameZettel changes the current Zid to a new Zid. @@ -72,16 +67,19 @@ type MetaFunc func(*meta.Meta) // ManagedBox is the interface of managed boxes. type ManagedBox interface { BaseBox + + // HasZettel returns true, if box conains zettel with given identifier. + HasZettel(context.Context, id.Zid) bool // Apply identifier of every zettel to the given function, if predicate returns true. - ApplyZid(context.Context, ZidFunc, search.RetrievePredicate) error + ApplyZid(context.Context, ZidFunc, query.RetrievePredicate) error // Apply metadata of every zettel to the given function, if predicate returns true. - ApplyMeta(context.Context, MetaFunc, search.RetrievePredicate) error + ApplyMeta(context.Context, MetaFunc, query.RetrievePredicate) error // ReadStats populates st with box statistics ReadStats(st *ManagedBoxStats) } @@ -91,15 +89,33 @@ ReadOnly bool // Zettel is the number of zettel managed by the box. Zettel int } + +// StartState enumerates the possible states of starting and stopping a box. +// +// StartStateStopped -> StartStateStarting -> StartStateStarted -> StateStateStopping -> StartStateStopped. +// +// Other transitions are also possible. +type StartState uint8 + +// Constant values of StartState +const ( + StartStateStopped StartState = iota + StartStateStarting + StartStateStarted + StartStateStopping +) // StartStopper performs simple lifecycle management. type StartStopper interface { + // State the current status of the box. + State() StartState + // Start the box. Now all other functions of the box are allowed. - // Starting an already started box is not allowed. + // Starting a box, which is not in state StartStateStopped is not allowed. Start(ctx context.Context) error // Stop the started box. Now only the Start() function is allowed. Stop(ctx context.Context) } @@ -114,19 +130,20 @@ type Box interface { BaseBox // FetchZids returns the set of all zettel identifer managed by the box. FetchZids(ctx context.Context) (id.Set, error) + + // GetMeta returns the metadata of the zettel with the given identifier. + GetMeta(context.Context, id.Zid) (*meta.Meta, error) // SelectMeta returns a list of metadata that comply to the given selection criteria. - SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) + // If `metaSeq` is `nil`, the box assumes metadata of all available zettel. + SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) // GetAllZettel retrieves a specific zettel from all managed boxes. - GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) - - // GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. - GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) + GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) // Refresh the data from the box and from its managed sub-boxes. Refresh(context.Context) error } @@ -181,18 +198,18 @@ type UpdateReason uint8 // Values for Reason const ( _ UpdateReason = iota + OnReady // Box is started and fully operational OnReload // Box was reloaded - OnUpdate // A zettel was created or changed - OnDelete // A zettel was removed + OnZettel // Something with a zettel happened ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { - Box Box + Box BaseBox Reason UpdateReason Zid id.Zid } // UpdateFunc is a function to be called when a change is detected. @@ -226,10 +243,18 @@ // DoNotEnrich determines if the context is marked to not enrich metadata. func DoNotEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) return ok } + +// NoEnrichQuery provides a context that signals not to enrich, if the query does not need this. +func NoEnrichQuery(ctx context.Context, q *query.Query) context.Context { + if q.EnrichNeeded() { + return ctx + } + return NoEnrichContext(ctx) +} // ErrNotAllowed is returned if the caller is not allowed to perform the operation. type ErrNotAllowed struct { Op string User *meta.Meta @@ -248,28 +273,22 @@ func (err *ErrNotAllowed) Error() string { if err.User == nil { if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for not authorized user", - err.Op, - err.Zid.String()) + err.Op, err.Zid) } return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) } if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for user %v/%v", - err.Op, - err.Zid.String(), - err.User.GetDefault(api.KeyUserID, "?"), - err.User.Zid.String()) + err.Op, err.Zid, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid) } return fmt.Sprintf( "operation %q not allowed for user %v/%v", - err.Op, - err.User.GetDefault(api.KeyUserID, "?"), - err.User.Zid.String()) + err.Op, err.User.GetDefault(api.KeyUserID, "?"), err.User.Zid) } // Is return true, if the error is of type ErrNotAllowed. func (*ErrNotAllowed) Is(error) bool { return true } @@ -280,44 +299,23 @@ var ErrStopped = errors.New("box is stopped") // ErrReadOnly is returned if there is an attepmt to write to a read-only box. var ErrReadOnly = errors.New("read-only box") -// ErrNotFound is returned if a zettel was not found in the box. -var ErrNotFound = errors.New("zettel not found") +// ErrZettelNotFound is returned if a zettel was not found in the box. +type ErrZettelNotFound struct{ Zid id.Zid } + +func (eznf ErrZettelNotFound) Error() string { return "zettel not found: " + eznf.Zid.String() } + +//var ErrZettelNotFound = errors.New("zettel not found") // ErrConflict is returned if a box operation detected a conflict.. // One example: if calculating a new zettel identifier takes too long. var ErrConflict = errors.New("conflict") // ErrCapacity is returned if a box has reached its capacity. var ErrCapacity = errors.New("capacity exceeded") -// ErrInvalidID is returned if the zettel id is not appropriate for the box operation. -type ErrInvalidID struct{ Zid id.Zid } - -func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() } - -// GetQueryBool is a helper function to extract bool values from a box URI. -func GetQueryBool(u *url.URL, key string) bool { - _, ok := u.Query()[key] - return ok -} - -// GetQueryInt is a helper function to extract int values of a specified range from a box URI. -func GetQueryInt(u *url.URL, key string, min, def, max int) int { - sVal := u.Query().Get(key) - if sVal == "" { - return def - } - iVal, err := strconv.Atoi(sVal) - if err != nil { - return def - } - if iVal < min { - return min - } - if iVal > max { - return max - } - return iVal -} +// ErrInvalidZid is returned if the zettel id is not appropriate for the box operation. +type ErrInvalidZid struct{ Zid string } + +func (err ErrInvalidZid) Error() string { return "invalid Zettel id: " + err.Zid } Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -13,19 +13,19 @@ import ( "context" "net/url" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" - "zettelstore.de/z/search" + "zettelstore.de/z/query" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func init() { manager.Register( " comp", @@ -70,50 +70,42 @@ func (*compBox) Location() string { return "" } func (*compBox) CanCreateZettel(context.Context) bool { return false } -func (cb *compBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { +func (cb *compBox) CreateZettel(context.Context, zettel.Zettel) (id.Zid, error) { cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel") return id.Invalid, box.ErrReadOnly } -func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { +func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { if gen, ok := myZettel[zid]; ok && gen.meta != nil { if m := gen.meta(zid); m != nil { updateMeta(m) if genContent := gen.content; genContent != nil { - cb.log.Trace().Msg("GetMeta/Content") - return domain.Zettel{ - Meta: m, - Content: domain.NewContent(genContent(m)), - }, nil - } - cb.log.Trace().Msg("GetMeta/NoContent") - return domain.Zettel{Meta: m}, nil - } - } - cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel/Err") - return domain.Zettel{}, box.ErrNotFound -} - -func (cb *compBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { - if gen, ok := myZettel[zid]; ok { - if genMeta := gen.meta; genMeta != nil { - if m := genMeta(zid); m != nil { - updateMeta(m) - cb.log.Trace().Msg("GetMeta") - return m, nil - } - } - } - cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta/Err") - return nil, box.ErrNotFound -} - -func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error { - cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") + cb.log.Trace().Msg("GetZettel/Content") + return zettel.Zettel{ + Meta: m, + Content: zettel.NewContent(genContent(m)), + }, nil + } + cb.log.Trace().Msg("GetZettel/NoContent") + return zettel.Zettel{Meta: m}, nil + } + } + err := box.ErrZettelNotFound{Zid: zid} + cb.log.Trace().Err(err).Msg("GetZettel/Err") + return zettel.Zettel{}, err +} + +func (*compBox) HasZettel(_ context.Context, zid id.Zid) bool { + _, found := myZettel[zid] + return found +} + +func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { + cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyZid") for zid, gen := range myZettel { if !constraint(zid) { continue } if genMeta := gen.meta; genMeta != nil { @@ -123,11 +115,11 @@ } } return nil } -func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint search.RetrievePredicate) error { +func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") for zid, gen := range myZettel { if !constraint(zid) { continue } @@ -140,37 +132,39 @@ } } return nil } -func (*compBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } +func (*compBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return false } -func (cb *compBox) UpdateZettel(context.Context, domain.Zettel) error { +func (cb *compBox) UpdateZettel(context.Context, zettel.Zettel) error { cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel") return box.ErrReadOnly } func (*compBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { _, ok := myZettel[zid] return !ok } -func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error { - err := box.ErrNotFound +func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) { if _, ok := myZettel[curZid]; ok { err = box.ErrReadOnly + } else { + err = box.ErrZettelNotFound{Zid: curZid} } cb.log.Trace().Err(err).Msg("RenameZettel") return err } func (*compBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } -func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) error { - err := box.ErrNotFound +func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { if _, ok := myZettel[zid]; ok { err = box.ErrReadOnly + } else { + err = box.ErrZettelNotFound{Zid: zid} } cb.log.Trace().Err(err).Msg("DeleteZettel") return err } @@ -180,14 +174,14 @@ cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } func updateMeta(m *meta.Meta) { if _, ok := m.Get(api.KeySyntax); !ok { - m.Set(api.KeySyntax, api.ValueSyntaxZmk) + m.Set(api.KeySyntax, meta.SyntaxZmk) } m.Set(api.KeyRole, api.ValueRoleConfiguration) m.Set(api.KeyLang, api.ValueLangEN) m.Set(api.KeyReadOnly, api.ValueTrue) if _, ok := m.Get(api.KeyVisibility); !ok { m.Set(api.KeyVisibility, api.ValueVisibilityExpert) } } Index: box/compbox/config.go ================================================================== --- box/compbox/config.go +++ box/compbox/config.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -11,21 +11,23 @@ package compbox import ( "bytes" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/client.fossil/api" + "zettelstore.de/z/kernel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Startup Configuration") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityExpert) return m } func genConfigZettelC(*meta.Meta) []byte { Index: box/compbox/keys.go ================================================================== --- box/compbox/keys.go +++ box/compbox/keys.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -12,18 +12,20 @@ import ( "bytes" "fmt" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/client.fossil/api" + "zettelstore.de/z/kernel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func genKeysM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Supported Metadata Keys") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genKeysC(*meta.Meta) []byte { @@ -30,9 +32,9 @@ keys := meta.GetSortedKeyDescriptions() var buf bytes.Buffer buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { fmt.Fprintf(&buf, - "|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) + "|[[%v|query:%v?]]|%v|%v|%v\n", kd.Name, kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) } return buf.Bytes() } Index: box/compbox/log.go ================================================================== --- box/compbox/log.go +++ box/compbox/log.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -11,20 +11,22 @@ package compbox import ( "bytes" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func genLogM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Log") - m.Set(api.KeySyntax, api.ValueSyntaxText) + m.Set(api.KeySyntax, meta.SyntaxText) + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) + m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.ZidLayout)) return m } func genLogC(*meta.Meta) []byte { const tsFormat = "2006-01-02 15:04:05.999999" Index: box/compbox/manager.go ================================================================== --- box/compbox/manager.go +++ box/compbox/manager.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -12,19 +12,20 @@ import ( "bytes" "fmt" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func genManagerM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Box Manager") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) return m } func genManagerC(*meta.Meta) []byte { kvl := kernel.Main.GetServiceStatistics(kernel.BoxService) Index: box/compbox/parser.go ================================================================== --- box/compbox/parser.go +++ box/compbox/parser.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -14,26 +14,28 @@ "bytes" "fmt" "sort" "strings" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/client.fossil/api" + "zettelstore.de/z/kernel" "zettelstore.de/z/parser" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func genParserM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Supported Parser") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genParserC(*meta.Meta) []byte { var buf bytes.Buffer - buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Image Format?:\n") + buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Text Format?:|=Image Format?:\n") syntaxes := parser.GetSyntaxes() sort.Strings(syntaxes) for _, syntax := range syntaxes { info := parser.Get(syntax) if info.Name != syntax { @@ -40,10 +42,10 @@ continue } altNames := info.AltNames sort.Strings(altNames) fmt.Fprintf( - &buf, "|%v|%v|%v|%v\n", - syntax, strings.Join(altNames, ", "), info.IsTextParser, info.IsImageFormat) + &buf, "|%v|%v|%v|%v|%v\n", + syntax, strings.Join(altNames, ", "), info.IsASTParser, info.IsTextFormat, info.IsImageFormat) } return buf.Bytes() } Index: box/compbox/version.go ================================================================== --- box/compbox/version.go +++ box/compbox/version.go @@ -1,22 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package compbox import ( - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func getVersionMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, title) @@ -24,30 +24,35 @@ return m } func genVersionBuildM(zid id.Zid) *meta.Meta { m := getVersionMeta(zid, "Zettelstore Version") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genVersionBuildC(*meta.Meta) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) } func genVersionHostM(zid id.Zid) *meta.Meta { - return getVersionMeta(zid, "Zettelstore Host") + m := getVersionMeta(zid, "Zettelstore Host") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) + return m } func genVersionHostC(*meta.Meta) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string)) } func genVersionOSM(zid id.Zid) *meta.Meta { - return getVersionMeta(zid, "Zettelstore Operating System") + m := getVersionMeta(zid, "Zettelstore Operating System") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) + return m } func genVersionOSC(*meta.Meta) []byte { goOS := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string) goArch := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string) result := make([]byte, 0, len(goOS)+len(goArch)+1) result = append(result, goOS...) result = append(result, '/') return append(result, goArch...) } Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -81,11 +81,11 @@ h3 { font-size:1.15rem; margin:.75rem 0 } h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } h5 { font-size:1.05rem; margin:.8rem 0 } h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } p { margin: .5rem 0 0 0 } - ol,ul { padding-left: 1.1rem } + p.zs-tag-zettel { margin-top: .5rem; margin-left: 0.5rem } li,figure,figcaption,dl { margin: 0 } dt { margin: .5rem 0 0 0 } dt+dd { margin-top: 0 } dd { margin: .5rem 0 0 2rem } dd > p:first-child { margin: 0 0 0 0 } @@ -101,17 +101,17 @@ table { border-collapse: collapse; border-spacing: 0; max-width: 100%; } - th,td { + thead>tr>td { border-bottom: 2px solid hsl(0, 0%, 70%); font-weight: bold } + tfoot>tr>td { border-top: 2px solid hsl(0, 0%, 70%); font-weight: bold } + td { text-align: left; padding: .25rem .5rem; + border-bottom: 1px solid hsl(0, 0%, 85%) } - td { border-bottom: 1px solid hsl(0, 0%, 85%) } - thead th { border-bottom: 2px solid hsl(0, 0%, 70%) } - tfoot th { border-top: 2px solid hsl(0, 0%, 70%) } main form { padding: 0 .5em; margin: .5em 0 0 0; } main form:after { @@ -137,12 +137,17 @@ border-bottom:1px solid #ccc; width:100%; } input.zs-primary { float:right } input.zs-secondary { float:left } + input.zs-upload { + padding-left: 1em; + padding-right: 1em; + } a:not([class]) { text-decoration-skip-ink: auto } a.broken { text-decoration: line-through } + a.external::after { content: "➚"; display: inline-block } img { max-width: 100% } img.right { float: right } ol.zs-endnotes { padding-top: .5rem; border-top: 1px solid; @@ -183,11 +188,10 @@ border: 1px solid black; border-radius: .25rem; padding: .1rem .2rem; font-size: 95%; } - .zs-example { border-style: dotted !important } .zs-info { background-color: lightblue; padding: .5rem 1rem; } .zs-warning { @@ -197,13 +201,13 @@ .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } - td.left,th.left { text-align:left } - td.center,th.center { text-align:center } - td.right,th.right { text-align:right } + td.left { text-align:left } + td.center { text-align:center } + td.right { text-align:right } .zs-font-size-0 { font-size:75% } .zs-font-size-1 { font-size:83% } .zs-font-size-2 { font-size:100% } .zs-font-size-3 { font-size:117% } .zs-font-size-4 { font-size:150% } DELETED box/constbox/base.mustache Index: box/constbox/base.mustache ================================================================== --- box/constbox/base.mustache +++ box/constbox/base.mustache @@ -1,66 +0,0 @@ - - - - - - - -{{{MetaHeader}}} - - -{{#CSSRoleURL}}{{/CSSRoleURL}} -{{Title}} - - - -
-{{{Content}}} -
-{{#FooterHTML}}{{/FooterHTML}} -{{#DebugMode}}
WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!
{{/DebugMode}} - - ADDED box/constbox/base.sxn Index: box/constbox/base.sxn ================================================================== --- box/constbox/base.sxn +++ box/constbox/base.sxn @@ -0,0 +1,48 @@ +`(@@@@ +(html ,@(if lang `((@ (lang ,lang)))) +(head + (meta (@ (charset "utf-8"))) + (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0"))) + (meta (@ (name "generator") (content "Zettelstore"))) + (meta (@ (name "format-detection") (content "telephone=no"))) + ,@META-HEADER + (link (@ (rel "stylesheet") (href ,css-base-url))) + (link (@ (rel "stylesheet") (href ,css-user-url))) + ,@(ROLE-DEFAULT-meta (current-environment)) + (title ,title)) +(body + (nav (@ (class "zs-menu")) + (a (@ (href ,home-url)) "Home") + ,@(if with-auth + `((div (@ (class "zs-dropdown")) + (button "User") + (nav (@ (class "zs-dropdown-content")) + ,@(if user-is-valid + `((a (@ (href ,user-zettel-url)) ,user-ident) + (a (@ (href ,logout-url)) "Logout")) + `((a (@ (href ,login-url)) "Login")) + ) + ))) + ) + (div (@ (class "zs-dropdown")) + (button "Lists") + (nav (@ (class "zs-dropdown-content")) + (a (@ (href ,list-zettel-url)) "List Zettel") + (a (@ (href ,list-roles-url)) "List Roles") + (a (@ (href ,list-tags-url)) "List Tags") + ,@(if (bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh"))) + )) + ,@(if new-zettel-links + `((div (@ (class "zs-dropdown")) + (button "New") + (nav (@ (class "zs-dropdown-content")) + ,@(map wui-link new-zettel-links) + ))) + ) + (form (@ (action ,search-url)) + (input (@ (type "text") (placeholder "Search..") (name ,query-key-query) (dir "auto")))) + ) + (main (@ (class "content")) ,DETAIL) + ,@(if FOOTER `((footer (hr) ,@FOOTER))) + ,@(if debug-mode '((div (b "WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!")))) +))) Index: box/constbox/constbox.go ================================================================== --- box/constbox/constbox.go +++ box/constbox/constbox.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -14,19 +14,19 @@ import ( "context" _ "embed" // Allow to embed file content "net/url" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" - "zettelstore.de/z/search" + "zettelstore.de/z/query" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func init() { manager.Register( " const", @@ -43,11 +43,11 @@ type constHeader map[string]string type constZettel struct { header constHeader - content domain.Content + content zettel.Content } type constBox struct { log *logger.Logger number int @@ -57,44 +57,41 @@ func (*constBox) Location() string { return "const:" } func (*constBox) CanCreateZettel(context.Context) bool { return false } -func (cb *constBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { +func (cb *constBox) CreateZettel(context.Context, zettel.Zettel) (id.Zid, error) { cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel") return id.Invalid, box.ErrReadOnly } -func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { +func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { if z, ok := cb.zettel[zid]; ok { cb.log.Trace().Msg("GetZettel") - return domain.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil - } - cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel") - return domain.Zettel{}, box.ErrNotFound -} - -func (cb *constBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { - if z, ok := cb.zettel[zid]; ok { - cb.log.Trace().Msg("GetMeta") - return meta.NewWithData(zid, z.header), nil - } - cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta") - return nil, box.ErrNotFound -} - -func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error { + return zettel.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil + } + err := box.ErrZettelNotFound{Zid: zid} + cb.log.Trace().Err(err).Msg("GetZettel/Err") + return zettel.Zettel{}, err +} + +func (cb *constBox) HasZettel(_ context.Context, zid id.Zid) bool { + _, found := cb.zettel[zid] + return found +} + +func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyZid") for zid := range cb.zettel { if constraint(zid) { handle(zid) } } return nil } -func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint search.RetrievePredicate) error { +func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyMeta") for zid, zettel := range cb.zettel { if constraint(zid) { m := meta.NewWithData(zid, zettel.header) cb.enricher.Enrich(ctx, m, cb.number) @@ -102,37 +99,39 @@ } } return nil } -func (*constBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } +func (*constBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return false } -func (cb *constBox) UpdateZettel(context.Context, domain.Zettel) error { +func (cb *constBox) UpdateZettel(context.Context, zettel.Zettel) error { cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel") return box.ErrReadOnly } func (cb *constBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { _, ok := cb.zettel[zid] return !ok } -func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error { - err := box.ErrNotFound +func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) { if _, ok := cb.zettel[curZid]; ok { err = box.ErrReadOnly + } else { + err = box.ErrZettelNotFound{Zid: curZid} } cb.log.Trace().Err(err).Msg("RenameZettel") return err } func (*constBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } -func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) error { - err := box.ErrNotFound +func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { if _, ok := cb.zettel[zid]; ok { err = box.ErrReadOnly + } else { + err = box.ErrZettelNotFound{Zid: zid} } cb.log.Trace().Err(err).Msg("DeleteZettel") return err } @@ -140,216 +139,234 @@ st.ReadOnly = true st.Zettel = len(cb.zettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } -const syntaxTemplate = "mustache" - var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ api.KeyTitle: "Zettelstore Runtime Configuration", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxNone, + api.KeySyntax: meta.SyntaxNone, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityOwner, }, - domain.NewContent(nil)}, + zettel.NewContent(nil)}, id.MustParse(api.ZidLicense): { constHeader{ api.KeyTitle: "Zettelstore License", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxText, + api.KeySyntax: meta.SyntaxText, + api.KeyCreated: "20210504135842", api.KeyLang: api.ValueLangEN, + api.KeyModified: "20220131153422", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityPublic, }, - domain.NewContent(contentLicense)}, + zettel.NewContent(contentLicense)}, id.MustParse(api.ZidAuthors): { constHeader{ api.KeyTitle: "Zettelstore Contributors", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxZmk, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20210504135842", api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityLogin, }, - domain.NewContent(contentContributors)}, + zettel.NewContent(contentContributors)}, id.MustParse(api.ZidDependencies): { constHeader{ api.KeyTitle: "Zettelstore Dependencies", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxZmk, + api.KeySyntax: meta.SyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityLogin, + api.KeyCreated: "20210504135842", + api.KeyModified: "20230601163100", }, - domain.NewContent(contentDependencies)}, + zettel.NewContent(contentDependencies)}, id.BaseTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Base HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20230510155100", + api.KeyModified: "20230827212200", api.KeyVisibility: api.ValueVisibilityExpert, }, - domain.NewContent(contentBaseMustache)}, + zettel.NewContent(contentBaseSxn)}, id.LoginTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Login Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20230527144100", api.KeyVisibility: api.ValueVisibilityExpert, }, - domain.NewContent(contentLoginMustache)}, + zettel.NewContent(contentLoginSxn)}, id.ZettelTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20230510155300", + api.KeyModified: "20230907203300", api.KeyVisibility: api.ValueVisibilityExpert, }, - domain.NewContent(contentZettelMustache)}, + zettel.NewContent(contentZettelSxn)}, id.InfoTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Info HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20230907203300", api.KeyVisibility: api.ValueVisibilityExpert, }, - domain.NewContent(contentInfoMustache)}, - id.ContextTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore Context HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentContextMustache)}, + zettel.NewContent(contentInfoSxn)}, id.FormTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20230621132600", api.KeyVisibility: api.ValueVisibilityExpert, }, - domain.NewContent(contentFormMustache)}, + zettel.NewContent(contentFormSxn)}, id.RenameTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Rename Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20230707190246", api.KeyVisibility: api.ValueVisibilityExpert, }, - domain.NewContent(contentRenameMustache)}, + zettel.NewContent(contentRenameSxn)}, id.DeleteTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Delete HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20230621133100", api.KeyVisibility: api.ValueVisibilityExpert, }, - domain.NewContent(contentDeleteMustache)}, + zettel.NewContent(contentDeleteSxn)}, id.ListTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore List Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentListZettelMustache)}, - id.RolesTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore List Roles HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentListRolesMustache)}, - id.TagsTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore List Tags HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentListTagsMustache)}, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20230704122100", + api.KeyModified: "20230829223600", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentListZettelSxn)}, id.ErrorTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Error HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20210305133215", + api.KeyModified: "20230527224800", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentErrorSxn)}, + id.StartSxnZid: { + constHeader{ + api.KeyTitle: "Zettelstore Sxn Start Code", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20230824160700", + api.KeyVisibility: api.ValueVisibilityExpert, + api.KeyPrecursor: id.BaseSxnZid.String(), + }, + zettel.NewContent(contentStartCodeSxn)}, + id.BaseSxnZid: { + constHeader{ + api.KeyTitle: "Zettelstore Sxn Base Code", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20230619132800", + api.KeyModified: "20230907203100", + api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityExpert, }, - domain.NewContent(contentErrorMustache)}, + zettel.NewContent(contentBaseCodeSxn)}, id.MustParse(api.ZidBaseCSS): { constHeader{ api.KeyTitle: "Zettelstore Base CSS", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: "css", + api.KeySyntax: meta.SyntaxCSS, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityPublic, }, - domain.NewContent(contentBaseCSS)}, + zettel.NewContent(contentBaseCSS)}, id.MustParse(api.ZidUserCSS): { constHeader{ api.KeyTitle: "Zettelstore User CSS", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: "css", + api.KeySyntax: meta.SyntaxCSS, + api.KeyCreated: "20210622110143", api.KeyVisibility: api.ValueVisibilityPublic, }, - domain.NewContent([]byte("/* User-defined CSS */"))}, - id.RoleCSSMapZid: { - constHeader{ - api.KeyTitle: "Zettelstore Role to CSS Map", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxNone, - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(nil)}, + zettel.NewContent([]byte("/* User-defined CSS */"))}, id.EmojiZid: { constHeader{ api.KeyTitle: "Zettelstore Generic Emoji", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxGif, + api.KeySyntax: meta.SyntaxGif, api.KeyReadOnly: api.ValueTrue, + api.KeyCreated: "20210504175807", api.KeyVisibility: api.ValueVisibilityPublic, }, - domain.NewContent(contentEmoji)}, + zettel.NewContent(contentEmoji)}, id.TOCNewTemplateZid: { constHeader{ api.KeyTitle: "New Menu", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxZmk, + api.KeySyntax: meta.SyntaxZmk, api.KeyLang: api.ValueLangEN, + api.KeyCreated: "20210217161829", api.KeyVisibility: api.ValueVisibilityCreator, }, - domain.NewContent(contentNewTOCZettel)}, + zettel.NewContent(contentNewTOCZettel)}, id.MustParse(api.ZidTemplateNewZettel): { constHeader{ api.KeyTitle: "New Zettel", api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: api.ValueSyntaxZmk, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20201028185209", api.KeyVisibility: api.ValueVisibilityCreator, }, - domain.NewContent(nil)}, + zettel.NewContent(nil)}, id.MustParse(api.ZidTemplateNewUser): { constHeader{ api.KeyTitle: "New User", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxNone, + api.KeySyntax: meta.SyntaxNone, + api.KeyCreated: "20201028185209", meta.NewPrefix + api.KeyCredential: "", meta.NewPrefix + api.KeyUserID: "", meta.NewPrefix + api.KeyUserRole: api.ValueUserRoleReader, api.KeyVisibility: api.ValueVisibilityOwner, }, - domain.NewContent(nil)}, + zettel.NewContent(nil)}, id.DefaultHomeZid: { constHeader{ - api.KeyTitle: "Home", - api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyLang: api.ValueLangEN, + api.KeyTitle: "Home", + api.KeyRole: api.ValueRoleZettel, + api.KeySyntax: meta.SyntaxZmk, + api.KeyLang: api.ValueLangEN, + api.KeyCreated: "20210210190757", }, - domain.NewContent(contentHomeZettel)}, + zettel.NewContent(contentHomeZettel)}, } //go:embed license.txt var contentLicense []byte @@ -357,45 +374,42 @@ var contentContributors []byte //go:embed dependencies.zettel var contentDependencies []byte -//go:embed base.mustache -var contentBaseMustache []byte - -//go:embed login.mustache -var contentLoginMustache []byte - -//go:embed zettel.mustache -var contentZettelMustache []byte - -//go:embed info.mustache -var contentInfoMustache []byte - -//go:embed context.mustache -var contentContextMustache []byte - -//go:embed form.mustache -var contentFormMustache []byte - -//go:embed rename.mustache -var contentRenameMustache []byte - -//go:embed delete.mustache -var contentDeleteMustache []byte - -//go:embed listzettel.mustache -var contentListZettelMustache []byte - -//go:embed listroles.mustache -var contentListRolesMustache []byte - -//go:embed listtags.mustache -var contentListTagsMustache []byte - -//go:embed error.mustache -var contentErrorMustache []byte +//go:embed base.sxn +var contentBaseSxn []byte + +//go:embed login.sxn +var contentLoginSxn []byte + +//go:embed zettel.sxn +var contentZettelSxn []byte + +//go:embed info.sxn +var contentInfoSxn []byte + +//go:embed form.sxn +var contentFormSxn []byte + +//go:embed rename.sxn +var contentRenameSxn []byte + +//go:embed delete.sxn +var contentDeleteSxn []byte + +//go:embed listzettel.sxn +var contentListZettelSxn []byte + +//go:embed error.sxn +var contentErrorSxn []byte + +//go:embed start.sxn +var contentStartCodeSxn []byte + +//go:embed wuicode.sxn +var contentBaseCodeSxn []byte //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif DELETED box/constbox/context.mustache Index: box/constbox/context.mustache ================================================================== --- box/constbox/context.mustache +++ box/constbox/context.mustache @@ -1,16 +0,0 @@ - DELETED box/constbox/delete.mustache Index: box/constbox/delete.mustache ================================================================== --- box/constbox/delete.mustache +++ box/constbox/delete.mustache @@ -1,43 +0,0 @@ -
-
-

Delete Zettel {{Zid}}

-
-

Do you really want to delete this zettel?

-{{#HasShadows}} -
-

Infomation

-

If you delete this zettel, the previoulsy shadowed zettel from overlayed box {{ShadowedBox}} becomes available.

-
-{{/HasShadows}} -{{#HasIncoming}} -
-

Warning!

-

If you delete this zettel, incoming references from the following zettel will become invalid.

-
    -{{#Incoming}} -
  • {{Text}}
  • -{{/Incoming}} -
-
-{{/HasIncoming}} -{{#HasUselessFiles}} -
-

Warning!

-

Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.

-
    -{{#UselessFiles}} -
  • {{{.}}}
  • -{{/UselessFiles}} -
-
-{{/HasUselessFiles}} -
-{{#MetaPairs}} -
{{Key}}:
{{Value}}
-{{/MetaPairs}} -
-
- -
-
-{{end}} ADDED box/constbox/delete.sxn Index: box/constbox/delete.sxn ================================================================== --- box/constbox/delete.sxn +++ box/constbox/delete.sxn @@ -0,0 +1,26 @@ +`(article + (header (h1 "Delete Zettel " ,zid)) + (p "Do you really want to delete this zettel?") + ,@(if shadowed-box + `((div (@ (class "zs-info")) + (h2 "Information") + (p "If you delete this zettel, the previously shadowed zettel from overlayed box " ,shadowed-box " becomes available.") + )) + ) + ,@(if incoming + `((div (@ (class "zs-warning")) + (h2 "Warning!") + (p "If you delete this zettel, incoming references from the following zettel will become invalid.") + (ul ,@(map wui-item-link incoming)) + )) + ) + ,@(if (and (bound? 'useless) useless) + `((div (@ (class "zs-warning")) + (h2 "Warning!") + (p "Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") + (ul ,@(map wui-item useless)) + )) + ) + ,(wui-meta-desc metapairs) + (form (@ (method "POST")) (input (@ (class "zs-primary") (type "submit") (value "Delete")))) +) Index: box/constbox/dependencies.zettel ================================================================== --- box/constbox/dependencies.zettel +++ box/constbox/dependencies.zettel @@ -72,83 +72,35 @@ ; License : BSD 3-Clause "New" or "Revised" License ; Source : [[https://github.com/fsnotify/fsnotify]] ``` -Copyright (c) 2012 The Go Authors. All rights reserved. -Copyright (c) 2012-2019 fsnotify Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -``` - -=== hoisie/mustache / cbroglie/mustache -; URL & Source -: [[https://github.com/hoisie/mustache]] / [[https://github.com/cbroglie/mustache]] -; License -: MIT License -; Remarks -: cbroglie/mustache is a fork from hoisie/mustache (starting with commit [f9b4cbf]). - cbroglie/mustache does not claim a copyright and includes just the license file from hoisie/mustache. - cbroglie/mustache obviously continues with the original license. - -``` -Copyright (c) 2009 Michael Hoisie - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -``` - -=== pascaldekloe/jwt -; URL & Source -: [[https://github.com/pascaldekloe/jwt]] -; License -: [[CC0 1.0 Universal|https://creativecommons.org/publicdomain/zero/1.0/legalcode]] -``` -To the extent possible under law, Pascal S. de Kloe has waived all -copyright and related or neighboring rights to JWT. This work is -published from The Netherlands. - -https://creativecommons.org/publicdomain/zero/1.0/legalcode +Copyright © 2012 The Go Authors. All rights reserved. +Copyright © fsnotify Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +* Neither the name of Google Inc. nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === yuin/goldmark ; URL & Source : [[https://github.com/yuin/goldmark]] @@ -175,5 +127,16 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` + +=== sx, zettelstore-client +These are companion projects, written by the current main developer of Zettelstore. +They are published under the same license, [[EUPL v1.2, or later|00000000000004]]. + +; URL & Source sx +: [[https://zettelstore.de/sx]] +; URL & Source zettelstore-client +: [[https://zettelstore.de/client/]] +; License: +: European Union Public License, version 1.2 (EUPL v1.2), or later. DELETED box/constbox/error.mustache Index: box/constbox/error.mustache ================================================================== --- box/constbox/error.mustache +++ box/constbox/error.mustache @@ -1,6 +0,0 @@ -
-
-

{{ErrorTitle}}

-
-{{ErrorText}} -
ADDED box/constbox/error.sxn Index: box/constbox/error.sxn ================================================================== --- box/constbox/error.sxn +++ box/constbox/error.sxn @@ -0,0 +1,4 @@ +`(article + (header (h1 ,heading)) + ,message +) DELETED box/constbox/form.mustache Index: box/constbox/form.mustache ================================================================== --- box/constbox/form.mustache +++ box/constbox/form.mustache @@ -1,54 +0,0 @@ -
-
-

{{Heading}}

-
-
-
- - -
-
-
- - -{{#HasRoleData}} - -{{#RoleData}} - -{{/HasRoleData}} -
- - -
-
- - -
-
- - -{{#HasSyntaxData}} - -{{#SyntaxData}} - -{{/HasSyntaxData}}
-
-{{#IsTextContent}} - - -{{/IsTextContent}} -
-
- - -
-
-
ADDED box/constbox/form.sxn Index: box/constbox/form.sxn ================================================================== --- box/constbox/form.sxn +++ box/constbox/form.sxn @@ -0,0 +1,38 @@ +`(article + (header (h1 ,heading)) + (form (@ (action ,form-action-url) (method "POST") (enctype "multipart/form-data")) + (div + (label (@ (for "zs-title")) "Title " (a (@ (title "Main heading of this zettel.")) (@H "ⓘ"))) + (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title") (placeholder "Title..") (value ,meta-title) (dir "auto") (autofocus)))) + (div + (label (@ (for "zs-role")) "Role " (a (@ (title "One word, without spaces, to set the main role of this zettel.")) (@H "ⓘ"))) + (input (@ (class "zs-input") (type "text") (id "zs-role") (name "role") (placeholder "role..") (value ,meta-role) (dir "auto") + ,@(if role-data '((list "zs-role-data"))) + )) + ,@(wui-datalist "zs-role-data" role-data) + ) + (div + (label (@ (for "zs-tags")) "Tags " (a (@ (title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "ⓘ"))) + (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags") (placeholder "#tag") (value ,meta-tags) (dir "auto")))) + (div + (label (@ (for "zs-meta")) "Metadata " (a (@ (title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "ⓘ"))) + (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4") (placeholder "metakey: metavalue") (dir "auto")) ,meta)) + (div + (label (@ (for "zs-syntax")) "Syntax " (a (@ (title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "ⓘ"))) + (input (@ (class "zs-input") (type "text") (id "zs-syntax") (name "syntax") (placeholder "syntax..") (value ,meta-syntax) (dir "auto") + ,@(if syntax-data '((list "zs-syntax-data"))) + )) + ,@(wui-datalist "zs-syntax-data" syntax-data) + ) + ,@(if (bound? 'content) + `((div + (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "ⓘ"))) + (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") (placeholder "Zettel content..") (dir "auto")) ,content) + )) + ) + (div + (input (@ (class "zs-primary") (type "submit") (value "Submit"))) + (input (@ (class "zs-secondary") (type "submit") (value "Save") (formaction "?save"))) + (input (@ (class "zs-upload") (type "file") (id "zs-file") (name "file"))) + )) +) DELETED box/constbox/info.mustache Index: box/constbox/info.mustache ================================================================== --- box/constbox/info.mustache +++ box/constbox/info.mustache @@ -1,67 +0,0 @@ -
-
-

Information for Zettel {{Zid}}

-WebContext -{{#CanWrite}} · Edit{{/CanWrite}} -{{#CanFolge}} · Folge{{/CanFolge}} -{{#CanCopy}} · Copy{{/CanCopy}} -{{#CanRename}}· Rename{{/CanRename}} -{{#CanDelete}}· Delete{{/CanDelete}} -
-

Interpreted Metadata

-{{#MetaData}}{{/MetaData}}
{{Key}}{{{Value}}}
-

References

-{{#HasLocLinks}} -

Local

- -{{/HasLocLinks}} -{{#HasExtLinks}} -

External

- -{{/HasExtLinks}} -

Unlinked

- -
- - -
-

Parts and encodings

- -{{#EvalMatrix}} - - -{{#Elements}} -{{/Elements}} - -{{/EvalMatrix}} -
{{Header}}{{Text}}
-

Parsed (not evaluated)

- -{{#ParseMatrix}} - - -{{#Elements}} -{{/Elements}} - -{{/ParseMatrix}} -
{{Header}}{{Text}}
-{{#HasShadowLinks}} -

Shadowed Boxes

- -{{/HasShadowLinks}} -{{#Endnotes}}{{{Endnotes}}}{{/Endnotes}} -
ADDED box/constbox/info.sxn Index: box/constbox/info.sxn ================================================================== --- box/constbox/info.sxn +++ box/constbox/info.sxn @@ -0,0 +1,33 @@ +`(article + (header (h1 "Information for Zettel " ,zid) + (p + (a (@ (href ,web-url)) "Web") + (@H " · ") (a (@ (href ,context-url)) "Context") + ,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit"))) + ,@(ROLE-DEFAULT-actions (current-environment)) + ,@(if (bound? 'rename-url) `((@H " · ") (a (@ (href ,rename-url)) "Rename"))) + ,@(if (bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete"))) + ) + ) + (h2 "Interpreted Metadata") + (table ,@(map wui-table-row metadata)) + (h2 "References") + ,@(if local-links `((h3 "Local") (ul ,@(map wui-valid-link local-links)))) + ,@(if query-links `((h3 "Queries") (ul ,@(map wui-item-link query-links)))) + ,@(if ext-links `((h3 "External") (ul ,@(map wui-item-popup-link ext-links)))) + (h3 "Unlinked") + ,@unlinked-content + (form + (label (@ (for "phrase")) "Search Phrase") + (input (@ (class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase))) + ) + (h2 "Parts and encodings") + ,(wui-enc-matrix enc-eval) + (h3 "Parsed (not evaluated)") + ,(wui-enc-matrix enc-parsed) + ,@(if shadow-links + `((h2 "Shadowed Boxes") + (ul ,@(map wui-item shadow-links)) + ) + ) +) Index: box/constbox/license.txt ================================================================== --- box/constbox/license.txt +++ box/constbox/license.txt @@ -1,6 +1,6 @@ -Copyright (c) 2020-2022 Detlef Stern +Copyright (c) 2020-present Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the DELETED box/constbox/listroles.mustache Index: box/constbox/listroles.mustache ================================================================== --- box/constbox/listroles.mustache +++ box/constbox/listroles.mustache @@ -1,8 +0,0 @@ - DELETED box/constbox/listtags.mustache Index: box/constbox/listtags.mustache ================================================================== --- box/constbox/listtags.mustache +++ box/constbox/listtags.mustache @@ -1,10 +0,0 @@ - DELETED box/constbox/listzettel.mustache Index: box/constbox/listzettel.mustache ================================================================== --- box/constbox/listzettel.mustache +++ box/constbox/listzettel.mustache @@ -1,6 +0,0 @@ -
-

{{Title}}

-
- ADDED box/constbox/listzettel.sxn Index: box/constbox/listzettel.sxn ================================================================== --- box/constbox/listzettel.sxn +++ box/constbox/listzettel.sxn @@ -0,0 +1,22 @@ +`(article + (header (h1 ,heading)) + (form (@ (action ,search-url)) + (input (@ (class "zs-input") (type "text") (placeholder "Search..") (name ,query-key-query) (value ,query-value) (dir "auto")))) + ,@(if (bound? 'tag-zettel) + `((p (@ (class "zs-tag-zettel")) "Tag zettel: " ,@tag-zettel)) + ) + ,@content + ,@endnotes + (form (@ (action ,(if (bound? 'create-url) create-url))) + "Other encodings: " + (a (@ (href ,data-url)) "data") + ", " + (a (@ (href ,plain-url)) "plain") + ,@(if (bound? 'create-url) + `((input (@ (type "hidden") (name ,query-key-query) (value ,query-value))) + (input (@ (type "hidden") (name ,query-key-seed) (value ,seed))) + (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel"))) + ) + ) + ) +) DELETED box/constbox/login.mustache Index: box/constbox/login.mustache ================================================================== --- box/constbox/login.mustache +++ box/constbox/login.mustache @@ -1,19 +0,0 @@ -
-
-

{{Title}}

-
-{{#Retry}} -
Wrong user name / password. Try again.
-{{/Retry}} -
-
- - -
-
- - -
-
-
-
ADDED box/constbox/login.sxn Index: box/constbox/login.sxn ================================================================== --- box/constbox/login.sxn +++ box/constbox/login.sxn @@ -0,0 +1,14 @@ +`(article + (header (h1 "Login")) + ,@(if retry '((div (@ (class "zs-indication zs-error")) "Wrong user name / password. Try again."))) + (form (@ (method "POST") (action "")) + (div + (label (@ (for "username")) "User name:") + (input (@ (class "zs-input") (type "text") (id "username") (name "username") (placeholder "Your user name..") (autofocus)))) + (div + (label (@ (for "password")) "Password:") + (input (@ (class "zs-input") (type "password") (id "password") (name "password") (placeholder "Your password..")))) + (div + (input (@ (class "zs-primary") (type "submit") (value "Login")))) + ) +) DELETED box/constbox/rename.mustache Index: box/constbox/rename.mustache ================================================================== --- box/constbox/rename.mustache +++ box/constbox/rename.mustache @@ -1,41 +0,0 @@ -
-
-

Rename Zettel {{Zid}}

-
-

Do you really want to rename this zettel?

-{{#HasIncoming}} -
-

Warning!

-

If you rename this zettel, incoming references from the following zettel will become invalid.

-
    -{{#Incoming}} -
  • {{Text}}
  • -{{/Incoming}} -
-
-{{/HasIncoming}} -{{#HasUselessFiles}} -
-

Warning!

-

Renaming this zettel will also delete the following files, so that they will not be interpreted as content for a zettel with identifier {{Zid}}.

-
    -{{#UselessFiles}} -
  • {{{.}}}
  • -{{/UselessFiles}} -
-
-{{/HasUselessFiles}} -
-
- - -
- -
-
-
-{{#MetaPairs}} -
{{Key}}:
{{Value}}
-{{/MetaPairs}} -
-
ADDED box/constbox/rename.sxn Index: box/constbox/rename.sxn ================================================================== --- box/constbox/rename.sxn +++ box/constbox/rename.sxn @@ -0,0 +1,26 @@ +`(article + (header (h1 "Rename Zettel " ,zid)) + (p "Do you really want to rename this zettel?") + ,@(if incoming + `((div (@ (class "zs-warning")) + (h2 "Warning!") + (p "If you rename this zettel, incoming references from the following zettel will become invalid.") + (ul ,@(map wui-item-link incoming)) + )) + ) + ,@(if (and (bound? 'useless) useless) + `((div (@ (class "zs-warning")) + (h2 "Warning!") + (p "Renaming this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") + (ul ,@(map wui-item useless)) + )) + ) + (form (@ (method "POST")) + (input (@ (type "hidden") (id "curzid") (name "curzid") (value ,zid))) + (div + (label (@ (for "newzid")) "New zettel id") + (input (@ (class "zs-input") (type "text") (id "newzid") (name "newzid") (placeholder "ZID..") (value ,zid) (autofocus)))) + (div (input (@ (class "zs-primary") (type "submit") (value "Rename")))) + ) + ,(wui-meta-desc metapairs) +) ADDED box/constbox/start.sxn Index: box/constbox/start.sxn ================================================================== --- box/constbox/start.sxn +++ box/constbox/start.sxn @@ -0,0 +1,14 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present Detlef Stern +;;; +;;; This file is part of Zettelstore. +;;; +;;; Zettelstore is licensed under the latest version of the EUPL (European +;;; Union Public License). Please see file LICENSE.txt for your rights and +;;; obligations under this license. +;;;---------------------------------------------------------------------------- + +;;; This zettel is the start of the loading sequence for Sx code used in the +;;; Zettelstore. Via the precursor metadata, dependend zettel are evaluated +;;; before this zettel. You must always depend, directly or indirectly on the +;;; "Zettelstore Sxn Base Code" zettel. It provides the base definitions. ADDED box/constbox/wuicode.sxn Index: box/constbox/wuicode.sxn ================================================================== --- box/constbox/wuicode.sxn +++ box/constbox/wuicode.sxn @@ -0,0 +1,123 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present Detlef Stern +;;; +;;; This file is part of Zettelstore. +;;; +;;; Zettelstore is licensed under the latest version of the EUPL (European +;;; Union Public License). Please see file LICENSE.txt for your rights and +;;; obligations under this license. +;;;---------------------------------------------------------------------------- + +;; wui-list-item returns the argument as a HTML list item. +(define (wui-item s) `(li ,s)) + +;; wui-table-row takes a pair and translates it into a HTML table row with +;; two columns. +(define (wui-table-row p) + `(tr (td ,(car p)) (td ,(cdr p)))) + +;; wui-valid-link translates a local link into a HTML link. A link is a pair +;; (valid . url). If valid is not truish, only the invalid url is returned. +(define (wui-valid-link l) + (if (car l) + `(li (a (@ (href ,(cdr l))) ,(cdr l))) + `(li ,(cdr l)))) + +;; wui-link takes a link (title . url) and returns a HTML reference. +(define (wui-link q) + `(a (@ (href ,(cdr q))) ,(car q))) + +;; wui-item-link taks a pair (text . url) and returns a HTML link inside +;; a list item. +(define (wui-item-link q) `(li ,(wui-link q))) + +;; wui-tdata-link taks a pair (text . url) and returns a HTML link inside +;; a table data item. +(define (wui-tdata-link q) `(td ,(wui-link q))) + +;; wui-item-popup-link is like 'wui-item-link, but the HTML link will open +;; a new tab / window. +(define (wui-item-popup-link e) + `(li (a (@ (href ,e) (target "_blank") (rel "noopener noreferrer")) ,e))) + +;; wui-option-value returns a value for an HTML option element. +(define (wui-option-value v) `(option (@ (value ,v)))) + +;; wui-datalist returns a HTML datalist with the given HTML identifier and a +;; list of values. +(define (wui-datalist id lst) + (if lst + `((datalist (@ (id ,id)) ,@(map wui-option-value lst))))) + +;; wui-pair-desc-item takes a pair '(term . text) and returns a list with +;; a HTML description term and a HTML description data. +(define (wui-pair-desc-item p) `((dt ,(car p)) (dd ,(cdr p)))) + +;; wui-meta-desc returns a HTML description list made from the list of pairs +;; given. +(define (wui-meta-desc l) + `(dl ,@(apply append (map wui-pair-desc-item l)))) + +;; wui-enc-matrix returns the HTML table of all encodings and parts. +(define (wui-enc-matrix matrix) + `(table + ,@(map + (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row)))) + matrix))) + +;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel +;; identifier. It is used in the base template to update the metadata of the +;; HTML page to include some role specific CSS code. +;; Referenced in function "ROLE-DEFAULT-meta". +(define CSS-ROLE-map '()) + +;; ROLE-DEFAULT-meta returns some metadata for the base template. Any role +;; specific code should include the returned list of this function. +(define (ROLE-DEFAULT-meta env) + `(,@(let (meta-role (environment-lookup 'meta-role env)) + (let (entry (assoc CSS-ROLE-map meta-role)) + (if (pair? entry) + `((link (@ (rel "stylesheet") (href ,(zid-content-path (cdr entry)))))) + ) + ) + ) + ) +) + +;;; ACTION-SEPARATOR defines a HTML value that separates actions links. +(define ACTION-SEPARATOR '(@H " · ")) + +;;; ROLE-DEFAULT-actions returns the default text for actions. +(define (ROLE-DEFAULT-actions env) + `(,@(let (copy-url (environment-lookup 'copy-url env)) + (if (defined? copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))) + ,@(let (version-url (environment-lookup 'version-url env)) + (if (defined? version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))) + ,@(let (child-url (environment-lookup 'child-url env)) + (if (defined? child-url) `((@H " · ") (a (@ (href ,child-url)) "Child")))) + ,@(let (folge-url (environment-lookup 'folge-url env)) + (if (defined? folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))) + ) +) + +;;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag". +(define (ROLE-tag-actions env) + `(,@(ROLE-DEFAULT-actions env) + ,@(let (title (environment-lookup 'title env)) + (if (and (defined? title) title) + `(,ACTION-SEPARATOR (a (@ (href ,(query->url (string-append "tags:" title)))) "Zettel")) + ) + ) + ) +) + +;;; ROLE-DEFAULT-heading returns the default text for headings, below the +;;; references of a zettel. In most cases it should be called from an +;;; overwriting function. +(define (ROLE-DEFAULT-heading env) + `(,@(let (meta-url (environment-lookup 'meta-url env)) + (if (defined? meta-url) `((br) "URL: " ,(url-to-html meta-url)))) + ,@(let (meta-author (environment-lookup 'meta-author env)) + (if (and (defined? meta-author) meta-author) `((br) "By " ,meta-author))) + ) +) DELETED box/constbox/zettel.mustache Index: box/constbox/zettel.mustache ================================================================== --- box/constbox/zettel.mustache +++ box/constbox/zettel.mustache @@ -1,41 +0,0 @@ -
-
-

{{{HTMLTitle}}}

-
-{{#CanWrite}}Edit ·{{/CanWrite}} -{{Zid}} · -Info · -({{RoleText}}) -{{#HasTags}}· {{#Tags}} {{Text}}{{/Tags}}{{/HasTags}} -{{#CanCopy}}· Copy{{/CanCopy}} -{{#CanFolge}}· Folge{{/CanFolge}} -{{#PrecursorRefs}}
Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}} -{{#HasExtURL}}
URL: {{ExtURL}}{{/HasExtURL}} -
-
-{{{Content}}} -
-{{#HasFolgeLinks}} - -{{/HasFolgeLinks}} -{{#HasBackLinks}} - -{{/HasBackLinks}} ADDED box/constbox/zettel.sxn Index: box/constbox/zettel.sxn ================================================================== --- box/constbox/zettel.sxn +++ box/constbox/zettel.sxn @@ -0,0 +1,30 @@ +`(article + (header + (h1 ,heading) + (div (@ (class "zs-meta")) + ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " · "))) + ,zid (@H " · ") + (a (@ (href ,info-url)) "Info") (@H " · ") + "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role))) + ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role)) + `((@H " → ") (a (@ (href ,folge-role-url)) ,meta-folge-role))) + ")" + ,@(if tag-refs `((@H " · ") ,@tag-refs)) + ,@(ROLE-DEFAULT-actions (current-environment)) + ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs)) + ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs)) + ,@(if superior-refs `((br) "Superior: " ,superior-refs)) + ,@(ROLE-DEFAULT-heading (current-environment)) + ) + ) + ,@content + ,endnotes + ,@(if (or folge-links subordinate-links back-links successor-links) + `((nav + ,@(if folge-links `((details (@ (open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) + ,@(if subordinate-links `((details (@ (open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) + ,@(if back-links `((details (@ (open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) + ,@(if successor-links `((details (@ (open)) (summary "Successors") (ul ,@(map wui-item-link successor-links))))) + )) + ) +) Index: box/dirbox/dirbox.go ================================================================== --- box/dirbox/dirbox.go +++ box/dirbox/dirbox.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -20,16 +20,16 @@ "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/box/notify" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" - "zettelstore.de/z/search" + "zettelstore.de/z/query" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { var log *logger.Logger @@ -126,10 +126,28 @@ } func (dp *dirBox) Location() string { return dp.location } + +func (dp *dirBox) State() box.StartState { + if ds := dp.dirSrv; ds != nil { + switch ds.State() { + case notify.DsCreated: + return box.StartStateStopped + case notify.DsStarting: + return box.StartStateStarting + case notify.DsWorking: + return box.StartStateStarted + case notify.DsMissing: + return box.StartStateStarted + case notify.DsStopping: + return box.StartStateStopping + } + } + return box.StartStateStopped +} func (dp *dirBox) Start(context.Context) error { dp.mxCmds.Lock() defer dp.mxCmds.Unlock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) @@ -151,10 +169,11 @@ dp.log.Fatal().Err(err).Msg("Unable to create directory supervisor") dp.stopFileServices() return err } dp.dirSrv = notify.NewDirService( + dp, dp.log.Clone().Str("sub", "dirsrv").Child(), notifier, dp.cdata.Notify, ) dp.dirSrv.Start() @@ -179,14 +198,14 @@ for _, c := range dp.fCmds { close(c) } } -func (dp *dirBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { +func (dp *dirBox) notifyChanged(zid id.Zid) { if chci := dp.cdata.Notify; chci != nil { - dp.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChanged") - chci <- box.UpdateInfo{Reason: reason, Zid: zid} + dp.log.Trace().Zid(zid).Msg("notifyChanged") + chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} } } func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function @@ -202,11 +221,11 @@ func (dp *dirBox) CanCreateZettel(_ context.Context) bool { return !dp.readonly } -func (dp *dirBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { +func (dp *dirBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { if dp.readonly { return id.Invalid, box.ErrReadOnly } newZid, err := dp.dirSrv.SetNewDirEntry() @@ -220,56 +239,43 @@ err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } - dp.notifyChanged(box.OnUpdate, meta.Zid) + dp.notifyChanged(meta.Zid) dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel") return meta.Zid, err } -func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { +func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { - return domain.Zettel{}, box.ErrNotFound + return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } m, c, err := dp.srvGetMetaContent(ctx, entry, zid) if err != nil { - return domain.Zettel{}, err + return zettel.Zettel{}, err } - zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)} + zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(c)} dp.log.Trace().Zid(zid).Msg("GetZettel") return zettel, nil } -func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - m, err := dp.doGetMeta(ctx, zid) - dp.log.Trace().Zid(zid).Err(err).Msg("GetMeta") - return m, err -} -func (dp *dirBox) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - entry := dp.dirSrv.GetDirEntry(zid) - if !entry.IsValid() { - return nil, box.ErrNotFound - } - m, err := dp.srvGetMeta(ctx, entry, zid) - if err != nil { - return nil, err - } - return m, nil -} - -func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error { +func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool { + return dp.dirSrv.GetDirEntry(zid).IsValid() +} + +func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { handle(entry.Zid) } return nil } -func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint search.RetrievePredicate) error { +func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyMeta") // The following loop could be parallelized if needed for performance. for _, entry := range entries { @@ -282,23 +288,23 @@ handle(m) } return nil } -func (dp *dirBox) CanUpdateZettel(context.Context, domain.Zettel) bool { +func (dp *dirBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return !dp.readonly } -func (dp *dirBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { +func (dp *dirBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { if dp.readonly { return box.ErrReadOnly } meta := zettel.Meta zid := meta.Zid if !zid.IsValid() { - return &box.ErrInvalidID{Zid: zid} + return box.ErrInvalidZid{Zid: zid.String()} } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { // Existing zettel, but new in this box. entry = ¬ify.DirEntry{Zid: zid} @@ -305,17 +311,17 @@ } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) dp.dirSrv.UpdateDirEntry(entry) err := dp.srvSetZettel(ctx, entry, zettel) if err == nil { - dp.notifyChanged(box.OnUpdate, zid) + dp.notifyChanged(zid) } dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel") return err } -func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content domain.Content) { +func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content zettel.Content) { entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax) } func (dp *dirBox) AllowRenameZettel(context.Context, id.Zid) bool { return !dp.readonly @@ -325,19 +331,19 @@ if curZid == newZid { return nil } curEntry := dp.dirSrv.GetDirEntry(curZid) if !curEntry.IsValid() { - return box.ErrNotFound + return box.ErrZettelNotFound{Zid: curZid} } if dp.readonly { return box.ErrReadOnly } // Check whether zettel with new ID already exists in this box. - if _, err := dp.doGetMeta(ctx, newZid); err == nil { - return &box.ErrInvalidID{Zid: newZid} + if dp.HasZettel(ctx, newZid) { + return box.ErrInvalidZid{Zid: newZid.String()} } oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid) if err != nil { return err @@ -346,20 +352,20 @@ newEntry, err := dp.dirSrv.RenameDirEntry(curEntry, newZid) if err != nil { return err } oldMeta.Zid = newZid - newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)} + newZettel := zettel.Zettel{Meta: oldMeta, Content: zettel.NewContent(oldContent)} if err = dp.srvSetZettel(ctx, &newEntry, newZettel); err != nil { // "Rollback" rename. No error checking... dp.dirSrv.RenameDirEntry(&newEntry, curZid) return err } err = dp.srvDeleteZettel(ctx, curEntry, curZid) if err == nil { - dp.notifyChanged(box.OnDelete, curZid) - dp.notifyChanged(box.OnUpdate, newZid) + dp.notifyChanged(curZid) + dp.notifyChanged(newZid) } dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel") return err } @@ -376,19 +382,19 @@ return box.ErrReadOnly } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { - return box.ErrNotFound + return box.ErrZettelNotFound{Zid: zid} } err := dp.dirSrv.DeleteDirEntry(zid) if err != nil { return nil } err = dp.srvDeleteZettel(ctx, entry, zid) if err == nil { - dp.notifyChanged(box.OnDelete, zid) + dp.notifyChanged(zid) } dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel") return err } Index: box/dirbox/dirbox_test.go ================================================================== --- box/dirbox/dirbox_test.go +++ box/dirbox/dirbox_test.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- Index: box/dirbox/service.go ================================================================== --- box/dirbox/service.go +++ box/dirbox/service.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -17,23 +17,23 @@ "path/filepath" "time" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/notify" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func fileService(i uint32, log *logger.Logger, dirPath string, cmds <-chan fileCmd) { // Something may panic. Ensure a running service. defer func() { - if r := recover(); r != nil { - kernel.Main.LogRecover("FileService", r) + if ri := recover(); ri != nil { + kernel.Main.LogRecover("FileService", ri) go fileService(i, log, dirPath, cmds) } }() log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started") @@ -166,11 +166,11 @@ // COMMAND: srvSetZettel ---------------------------------------- // // Writes a new or exsting zettel. -func (dp *dirBox) srvSetZettel(ctx context.Context, entry *notify.DirEntry, zettel domain.Zettel) error { +func (dp *dirBox) srvSetZettel(ctx context.Context, entry *notify.DirEntry, zettel zettel.Zettel) error { rc := make(chan resSetZettel, 1) dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { @@ -181,11 +181,11 @@ } } type fileSetZettel struct { entry *notify.DirEntry - zettel domain.Zettel + zettel zettel.Zettel rc chan<- resSetZettel } type resSetZettel = error func (cmd *fileSetZettel) run(log *logger.Logger, dirPath string) { Index: box/filebox/filebox.go ================================================================== --- box/filebox/filebox.go +++ box/filebox/filebox.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -15,16 +15,16 @@ "errors" "net/url" "path/filepath" "strings" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getFilepathFromURL(u) Index: box/filebox/zipbox.go ================================================================== --- box/filebox/zipbox.go +++ box/filebox/zipbox.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -16,16 +16,16 @@ "io" "strings" "zettelstore.de/z/box" "zettelstore.de/z/box/notify" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/logger" - "zettelstore.de/z/search" + "zettelstore.de/z/query" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) type zipBox struct { log *logger.Logger number int @@ -39,22 +39,37 @@ if strings.HasPrefix(zb.name, "/") { return "file://" + zb.name } return "file:" + zb.name } + +func (zb *zipBox) State() box.StartState { + if ds := zb.dirSrv; ds != nil { + switch ds.State() { + case notify.DsCreated: + return box.StartStateStopped + case notify.DsStarting: + return box.StartStateStarting + case notify.DsWorking: + return box.StartStateStarted + case notify.DsMissing: + return box.StartStateStarted + case notify.DsStopping: + return box.StartStateStopping + } + } + return box.StartStateStopped +} func (zb *zipBox) Start(context.Context) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } reader.Close() - zipNotifier, err := notify.NewSimpleZipNotifier(zb.log, zb.name) - if err != nil { - return err - } - zb.dirSrv = notify.NewDirService(zb.log, zipNotifier, zb.notify) + zipNotifier := notify.NewSimpleZipNotifier(zb.log, zb.name) + zb.dirSrv = notify.NewDirService(zb, zb.log, zipNotifier, zb.notify) zb.dirSrv.Start() return nil } func (zb *zipBox) Refresh(_ context.Context) { @@ -62,28 +77,29 @@ zb.log.Trace().Msg("Refresh") } func (zb *zipBox) Stop(context.Context) { zb.dirSrv.Stop() + zb.dirSrv = nil } func (*zipBox) CanCreateZettel(context.Context) bool { return false } -func (zb *zipBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { +func (zb *zipBox) CreateZettel(context.Context, zettel.Zettel) (id.Zid, error) { err := box.ErrReadOnly zb.log.Trace().Err(err).Msg("CreateZettel") return id.Invalid, err } -func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { +func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { - return domain.Zettel{}, box.ErrNotFound + return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } reader, err := zip.OpenReader(zb.name) if err != nil { - return domain.Zettel{}, err + return zettel.Zettel{}, err } defer reader.Close() var m *meta.Meta var src []byte @@ -94,11 +110,11 @@ if contentName == "" { zb.log.Panic().Zid(zid).Msg("No meta, no content in zipBox.GetZettel") } src, err = readZipFileContent(reader, entry.ContentName) if err != nil { - return domain.Zettel{}, err + return zettel.Zettel{}, err } if entry.HasMetaInContent() { inp := input.NewInput(src) m = meta.NewFromInput(zid, inp) src = src[inp.Pos:] @@ -106,51 +122,40 @@ m = CalcDefaultMeta(zid, entry.ContentExt) } } else { m, err = readZipMetaFile(reader, zid, metaName) if err != nil { - return domain.Zettel{}, err + return zettel.Zettel{}, err } inMeta = true if contentName != "" { src, err = readZipFileContent(reader, entry.ContentName) if err != nil { - return domain.Zettel{}, err + return zettel.Zettel{}, err } } } CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) zb.log.Trace().Zid(zid).Msg("GetZettel") - return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil -} - -func (zb *zipBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { - entry := zb.dirSrv.GetDirEntry(zid) - if !entry.IsValid() { - return nil, box.ErrNotFound - } - reader, err := zip.OpenReader(zb.name) - if err != nil { - return nil, err - } - defer reader.Close() - m, err := zb.readZipMeta(reader, zid, entry) - zb.log.Trace().Err(err).Zid(zid).Msg("GetMeta") - return m, err -} - -func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error { + return zettel.Zettel{Meta: m, Content: zettel.NewContent(src)}, nil +} + +func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool { + return zb.dirSrv.GetDirEntry(zid).IsValid() +} + +func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := zb.dirSrv.GetDirEntries(constraint) zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { handle(entry.Zid) } return nil } -func (zb *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint search.RetrievePredicate) error { +func (zb *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } defer reader.Close() @@ -168,13 +173,13 @@ handle(m) } return nil } -func (*zipBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } +func (*zipBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return false } -func (zb *zipBox) UpdateZettel(context.Context, domain.Zettel) error { +func (zb *zipBox) UpdateZettel(context.Context, zettel.Zettel) error { err := box.ErrReadOnly zb.log.Trace().Err(err).Msg("UpdateZettel") return err } @@ -188,11 +193,11 @@ if curZid == newZid { err = nil } curEntry := zb.dirSrv.GetDirEntry(curZid) if !curEntry.IsValid() { - err = box.ErrNotFound + err = box.ErrZettelNotFound{Zid: curZid} } zb.log.Trace().Err(err).Msg("RenameZettel") return err } @@ -200,11 +205,11 @@ func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrReadOnly entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { - err = box.ErrNotFound + err = box.ErrZettelNotFound{Zid: zid} } zb.log.Trace().Err(err).Msg("DeleteZettel") return err } Index: box/helper.go ================================================================== --- box/helper.go +++ box/helper.go @@ -1,21 +1,23 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package box import ( + "net/url" + "strconv" "time" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/zettel/id" ) // GetNewZid calculates a new and unused zettel identifier, based on the current date and time. func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) { withSeconds := false @@ -32,5 +34,30 @@ time.Sleep(100 * time.Millisecond) withSeconds = true } return id.Invalid, ErrConflict } + +// GetQueryBool is a helper function to extract bool values from a box URI. +func GetQueryBool(u *url.URL, key string) bool { + _, ok := u.Query()[key] + return ok +} + +// GetQueryInt is a helper function to extract int values of a specified range from a box URI. +func GetQueryInt(u *url.URL, key string, min, def, max int) int { + sVal := u.Query().Get(key) + if sVal == "" { + return def + } + iVal, err := strconv.Atoi(sVal) + if err != nil { + return def + } + if iVal < min { + return min + } + if iVal > max { + return max + } + return iVal +} Index: box/manager/anteroom.go ================================================================== --- box/manager/anteroom.go +++ box/manager/anteroom.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -11,108 +11,97 @@ package manager import ( "sync" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/zettel/id" ) type arAction int const ( arNothing arAction = iota arReload - arUpdate - arDelete + arZettel ) type anteroom struct { num uint64 next *anteroom - waiting map[id.Zid]arAction + waiting id.Set curLoad int reload bool } -type anterooms struct { +type anteroomQueue struct { mx sync.Mutex nextNum uint64 first *anteroom last *anteroom maxLoad int } -func newAnterooms(maxLoad int) *anterooms { - return &anterooms{maxLoad: maxLoad} -} +func newAnteroomQueue(maxLoad int) *anteroomQueue { return &anteroomQueue{maxLoad: maxLoad} } -func (ar *anterooms) Enqueue(zid id.Zid, action arAction) { - if !zid.IsValid() || action == arNothing || action == arReload { +func (ar *anteroomQueue) EnqueueZettel(zid id.Zid) { + if !zid.IsValid() { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { - ar.first = ar.makeAnteroom(zid, action) + ar.first = ar.makeAnteroom(zid) ar.last = ar.first return } for room := ar.first; room != nil; room = room.next { if room.reload { continue // Do not put zettel in reload room } - a, ok := room.waiting[zid] - if !ok { - continue - } - switch action { - case a: - return - case arUpdate: - room.waiting[zid] = action - case arDelete: - room.waiting[zid] = action - } - return + if _, ok := room.waiting[zid]; ok { + // Zettel is already waiting. Nothing to do. + return + } } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { - room.waiting[zid] = action + room.waiting.Add(zid) room.curLoad++ return } - room := ar.makeAnteroom(zid, action) + room := ar.makeAnteroom(zid) ar.last.next = room ar.last = room } -func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom { +func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom { + ar.nextNum++ + if zid == id.Invalid { + return &anteroom{num: ar.nextNum, next: nil, waiting: nil, curLoad: 0, reload: true} + } c := ar.maxLoad if c == 0 { c = 100 } - waiting := make(map[id.Zid]arAction, c) - waiting[zid] = action - ar.nextNum++ + waiting := id.NewSetCap(ar.maxLoad, zid) return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false} } -func (ar *anterooms) Reset() { +func (ar *anteroomQueue) Reset() { ar.mx.Lock() defer ar.mx.Unlock() - ar.first = ar.makeAnteroom(id.Invalid, arReload) + ar.first = ar.makeAnteroom(id.Invalid) ar.last = ar.first } -func (ar *anterooms) Reload(newZids id.Set) uint64 { +func (ar *anteroomQueue) Reload(allZids id.Set) uint64 { ar.mx.Lock() defer ar.mx.Unlock() - newWaiting := createWaitingSet(newZids, arUpdate) ar.deleteReloadedRooms() - if ns := len(newWaiting); ns > 0 { + if ns := len(allZids); ns > 0 { ar.nextNum++ - ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newWaiting, curLoad: ns} + ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: allZids, curLoad: ns, reload: true} if ar.first.next == nil { ar.last = ar.first } return ar.nextNum } @@ -120,21 +109,11 @@ ar.first = nil ar.last = nil return 0 } -func createWaitingSet(zids id.Set, action arAction) map[id.Zid]arAction { - waitingSet := make(map[id.Zid]arAction, len(zids)) - for zid := range zids { - if zid.IsValid() { - waitingSet[zid] = action - } - } - return waitingSet -} - -func (ar *anterooms) deleteReloadedRooms() { +func (ar *anteroomQueue) deleteReloadedRooms() { room := ar.first for room != nil && room.reload { room = room.next } ar.first = room @@ -141,24 +120,34 @@ if room == nil { ar.last = nil } } -func (ar *anterooms) Dequeue() (arAction, id.Zid, uint64) { - ar.mx.Lock() - defer ar.mx.Unlock() - if ar.first == nil { - return arNothing, id.Invalid, 0 - } - for zid, action := range ar.first.waiting { - roomNo := ar.first.num - delete(ar.first.waiting, zid) - if len(ar.first.waiting) == 0 { - ar.first = ar.first.next - if ar.first == nil { - ar.last = nil - } - } - return action, zid, roomNo - } - return arNothing, id.Invalid, 0 +func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, uint64) { + ar.mx.Lock() + defer ar.mx.Unlock() + first := ar.first + if first == nil { + return arNothing, id.Invalid, 0 + } + roomNo := first.num + if first.waiting == nil && first.reload { + ar.removeFirst() + return arReload, id.Invalid, roomNo + } + for zid := range first.waiting { + delete(first.waiting, zid) + if len(first.waiting) == 0 { + ar.removeFirst() + } + return arZettel, zid, roomNo + } + ar.removeFirst() + return arNothing, id.Invalid, 0 +} + +func (ar *anteroomQueue) removeFirst() { + ar.first = ar.first.next + if ar.first == nil { + ar.last = nil + } } Index: box/manager/anteroom_test.go ================================================================== --- box/manager/anteroom_test.go +++ box/manager/anteroom_test.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -11,31 +11,31 @@ package manager import ( "testing" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/zettel/id" ) func TestSimple(t *testing.T) { t.Parallel() - ar := newAnterooms(2) - ar.Enqueue(id.Zid(1), arUpdate) + ar := newAnteroomQueue(2) + ar.EnqueueZettel(id.Zid(1)) action, zid, rno := ar.Dequeue() - if zid != id.Zid(1) || action != arUpdate || rno != 1 { - t.Errorf("Expected arUpdate/1/1, but got %v/%v/%v", action, zid, rno) + if zid != id.Zid(1) || action != arZettel || rno != 1 { + t.Errorf("Expected arZettel/1/1, but got %v/%v/%v", action, zid, rno) } - action, zid, _ = ar.Dequeue() - if zid != id.Invalid && action != arDelete { + _, zid, _ = ar.Dequeue() + if zid != id.Invalid { t.Errorf("Expected invalid Zid, but got %v", zid) } - ar.Enqueue(id.Zid(1), arUpdate) - ar.Enqueue(id.Zid(2), arUpdate) + ar.EnqueueZettel(id.Zid(1)) + ar.EnqueueZettel(id.Zid(2)) if ar.first != ar.last { t.Errorf("Expected one room, but got more") } - ar.Enqueue(id.Zid(3), arUpdate) + ar.EnqueueZettel(id.Zid(3)) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 @@ -50,59 +50,57 @@ } } func TestReset(t *testing.T) { t.Parallel() - ar := newAnterooms(1) - ar.Enqueue(id.Zid(1), arUpdate) + ar := newAnteroomQueue(1) + ar.EnqueueZettel(id.Zid(1)) ar.Reset() action, zid, _ := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } ar.Reload(id.NewSet(3, 4)) - ar.Enqueue(id.Zid(5), arUpdate) - ar.Enqueue(id.Zid(5), arDelete) - ar.Enqueue(id.Zid(5), arDelete) - ar.Enqueue(id.Zid(5), arUpdate) + ar.EnqueueZettel(id.Zid(5)) + ar.EnqueueZettel(id.Zid(5)) if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ { t.Errorf("Expected 2 rooms") } action, zid1, _ := ar.Dequeue() - if action != arUpdate { - t.Errorf("Expected arUpdate, but got %v", action) + if action != arZettel { + t.Errorf("Expected arZettel, but got %v", action) } action, zid2, _ := ar.Dequeue() - if action != arUpdate { - t.Errorf("Expected arUpdate, but got %v", action) + if action != arZettel { + t.Errorf("Expected arZettel, but got %v", action) } if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) { t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2) } action, zid, _ = ar.Dequeue() - if zid != id.Zid(5) || action != arUpdate { - t.Errorf("Expected 5/arUpdate, but got %v/%v", zid, action) + if zid != id.Zid(5) || action != arZettel { + t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action) + } + action, zid, _ = ar.Dequeue() + if action != arNothing || zid != id.Invalid { + t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) + } + + ar = newAnteroomQueue(1) + ar.Reload(id.NewSet(id.Zid(6))) + action, zid, _ = ar.Dequeue() + if zid != id.Zid(6) || action != arZettel { + t.Errorf("Expected 6/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } - ar = newAnterooms(1) - ar.Reload(id.NewSet(id.Zid(6))) - action, zid, _ = ar.Dequeue() - if zid != id.Zid(6) || action != arUpdate { - t.Errorf("Expected 6/arUpdate, but got %v/%v", zid, action) - } - action, zid, _ = ar.Dequeue() - if action != arNothing || zid != id.Invalid { - t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) - } - - ar = newAnterooms(1) - ar.Enqueue(id.Zid(8), arUpdate) + ar = newAnteroomQueue(1) + ar.EnqueueZettel(id.Zid(8)) ar.Reload(nil) action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } } Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -9,211 +9,205 @@ //----------------------------------------------------------------------------- package manager import ( - "bytes" "context" "errors" + "strings" "zettelstore.de/z/box" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" + "zettelstore.de/z/query" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // Conatains all box.Box related functions // Location returns some information where the box is located. func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { return "NONE" } - var buf bytes.Buffer + var sb strings.Builder for i := 0; i < len(mgr.boxes)-2; i++ { if i > 0 { - buf.WriteString(", ") + sb.WriteString(", ") } - buf.WriteString(mgr.boxes[i].Location()) + sb.WriteString(mgr.boxes[i].Location()) } - return buf.String() + return sb.String() } // CanCreateZettel returns true, if box could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - return mgr.started && mgr.boxes[0].CanCreateZettel(ctx) + return mgr.State() == box.StartStateStarted && mgr.boxes[0].CanCreateZettel(ctx) } // CreateZettel creates a new zettel. -func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { +func (mgr *Manager) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { mgr.mgrLog.Debug().Msg("CreateZettel") - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { + if mgr.State() != box.StartStateStarted { return id.Invalid, box.ErrStopped } + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() return mgr.boxes[0].CreateZettel(ctx, zettel) } // GetZettel retrieves a specific zettel. -func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { +func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel") + if mgr.State() != box.StartStateStarted { + return zettel.Zettel{}, box.ErrStopped + } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - if !mgr.started { - return domain.Zettel{}, box.ErrStopped - } for i, p := range mgr.boxes { - if z, err := p.GetZettel(ctx, zid); err != box.ErrNotFound { + var errZNF box.ErrZettelNotFound + if z, err := p.GetZettel(ctx, zid); !errors.As(err, &errZNF) { if err == nil { mgr.Enrich(ctx, z.Meta, i+1) } return z, err } } - return domain.Zettel{}, box.ErrNotFound + return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } // GetAllZettel retrieves a specific zettel from all managed boxes. -func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { +func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel") - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { + if mgr.State() != box.StartStateStarted { return nil, box.ErrStopped } - var result []domain.Zettel + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + var result []zettel.Zettel for i, p := range mgr.boxes { if z, err := p.GetZettel(ctx, zid); err == nil { mgr.Enrich(ctx, z.Meta, i+1) result = append(result, z) } } return result, nil } -// GetMeta retrieves just the meta data of a specific zettel. -func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta") - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return nil, box.ErrStopped - } - return mgr.doGetMeta(ctx, zid) -} - -func (mgr *Manager) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - for i, p := range mgr.boxes { - if m, err := p.GetMeta(ctx, zid); err != box.ErrNotFound { - if err == nil { - mgr.Enrich(ctx, m, i+1) - } - return m, err - } - } - return nil, box.ErrNotFound -} - -// GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. -func (mgr *Manager) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { - mgr.mgrLog.Debug().Zid(zid).Msg("GetAllMeta") - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { - return nil, box.ErrStopped - } - var result []*meta.Meta - for i, p := range mgr.boxes { - if m, err := p.GetMeta(ctx, zid); err == nil { - mgr.Enrich(ctx, m, i+1) - result = append(result, m) - } - } - return result, nil -} - // FetchZids returns the set of all zettel identifer managed by the box. func (mgr *Manager) FetchZids(ctx context.Context) (id.Set, error) { mgr.mgrLog.Debug().Msg("FetchZids") - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { + if mgr.State() != box.StartStateStarted { return nil, box.ErrStopped } result := id.Set{} + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { - err := p.ApplyZid(ctx, func(zid id.Zid) { result.Zid(zid) }, func(id.Zid) bool { return true }) + err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, func(id.Zid) bool { return true }) if err != nil { return nil, err } } return result, nil } -type metaMap map[id.Zid]*meta.Meta +func (mgr *Manager) HasZettel(ctx context.Context, zid id.Zid) bool { + mgr.mgrLog.Debug().Zid(zid).Msg("HasZettel") + if mgr.State() != box.StartStateStarted { + return false + } + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + for _, bx := range mgr.boxes { + if bx.HasZettel(ctx, zid) { + return true + } + } + return false +} + +func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { + mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta") + if mgr.State() != box.StartStateStarted { + return nil, box.ErrStopped + } + + m, err := mgr.idxStore.GetMeta(ctx, zid) + if err != nil { + return nil, err + } + mgr.Enrich(ctx, m, 0) + return m, nil +} // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. -func (mgr *Manager) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { +func (mgr *Manager) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { if msg := mgr.mgrLog.Debug(); msg.Enabled() { - msg.Str("query", s.String()).Msg("SelectMeta") + msg.Str("query", q.String()).Msg("SelectMeta") + } + if mgr.State() != box.StartStateStarted { + return nil, box.ErrStopped } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - if !mgr.started { - return nil, box.ErrStopped - } - - searchPred, match := s.RetrieveAndCompileMatch(mgr) - selected, rejected := metaMap{}, id.Set{} - handleMeta := func(m *meta.Meta) { - zid := m.Zid - if rejected.Contains(zid) { - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") - return - } - if _, ok := selected[zid]; ok { - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected") - return - } - if match(m) { - selected[zid] = m - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match") - } else { - rejected.Zid(zid) - mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject") - } - } - for _, p := range mgr.boxes { - if err := p.ApplyMeta(ctx, handleMeta, searchPred); err != nil { - return nil, err + + compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq) + if result := compSearch.Result(); result != nil { + mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found without ApplyMeta") + return result, nil + } + selected := map[id.Zid]*meta.Meta{} + for _, term := range compSearch.Terms { + rejected := id.Set{} + handleMeta := func(m *meta.Meta) { + zid := m.Zid + if rejected.ContainsOrNil(zid) { + mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") + return + } + if _, ok := selected[zid]; ok { + mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected") + return + } + if compSearch.PreMatch(m) && term.Match(m) { + selected[zid] = m + mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match") + } else { + rejected.Add(zid) + mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject") + } + } + for _, p := range mgr.boxes { + if err2 := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err2 != nil { + return nil, err2 + } } } result := make([]*meta.Meta, 0, len(selected)) for _, m := range selected { result = append(result, m) } - return s.Sort(result), nil + result = compSearch.AfterSearch(result) + mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found with ApplyMeta") + return result, nil } // CanUpdateZettel returns true, if box could possibly update the given zettel. -func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { +func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - return mgr.started && mgr.boxes[0].CanUpdateZettel(ctx, zettel) + return mgr.State() == box.StartStateStarted && mgr.boxes[0].CanUpdateZettel(ctx, zettel) } // UpdateZettel updates an existing zettel. -func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { +func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel") - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { + if mgr.State() != box.StartStateStarted { return box.ErrStopped } // Remove all (computed) properties from metadata before storing the zettel. zettel.Meta = zettel.Meta.Clone() for _, p := range zettel.Meta.ComputedPairsRest() { @@ -224,15 +218,15 @@ return mgr.boxes[0].UpdateZettel(ctx, zettel) } // AllowRenameZettel returns true, if box will not disallow renaming the zettel. func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { + if mgr.State() != box.StartStateStarted { + return false + } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - if !mgr.started { - return false - } for _, p := range mgr.boxes { if !p.AllowRenameZettel(ctx, zid) { return false } } @@ -240,18 +234,19 @@ } // RenameZettel changes the current zid to a new zid. func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { mgr.mgrLog.Debug().Zid(curZid).Zid(newZid).Msg("RenameZettel") - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { + if mgr.State() != box.StartStateStarted { return box.ErrStopped } + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() for i, p := range mgr.boxes { err := p.RenameZettel(ctx, curZid, newZid) - if err != nil && !errors.Is(err, box.ErrNotFound) { + var errZNF box.ErrZettelNotFound + if err != nil && !errors.As(err, &errZNF) { for j := 0; j < i; j++ { mgr.boxes[j].RenameZettel(ctx, newZid, curZid) } return err } @@ -259,15 +254,15 @@ return nil } // CanDeleteZettel returns true, if box could possibly delete the given zettel. func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { + if mgr.State() != box.StartStateStarted { + return false + } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - if !mgr.started { - return false - } for _, p := range mgr.boxes { if p.CanDeleteZettel(ctx, zid) { return true } } @@ -275,21 +270,22 @@ } // DeleteZettel removes the zettel from the box. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel") + if mgr.State() != box.StartStateStarted { + return box.ErrStopped + } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - if !mgr.started { - return box.ErrStopped - } for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) if err == nil { return nil } - if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) { + var errZNF box.ErrZettelNotFound + if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) { return err } } - return box.ErrNotFound + return box.ErrZettelNotFound{Zid: zid} } Index: box/manager/collect.go ================================================================== --- box/manager/collect.go +++ box/manager/collect.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -13,26 +13,24 @@ import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/domain/id" "zettelstore.de/z/strfun" + "zettelstore.de/z/zettel/id" ) type collectData struct { refs id.Set words store.WordSet urls store.WordSet - itags store.WordSet } func (data *collectData) initialize() { data.refs = id.NewSet() data.words = store.NewWordSet() data.urls = store.NewWordSet() - data.itags = store.NewWordSet() } func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) { ast.Walk(data, &zn.Ast) } @@ -47,17 +45,16 @@ data.addText(string(n.Content)) case *ast.TranscludeNode: data.addRef(n.Ref) case *ast.TextNode: data.addText(n.Text) - case *ast.TagNode: - data.addText(n.Tag) - data.itags.Add("#" + strings.ToLower(n.Tag)) case *ast.LinkNode: data.addRef(n.Ref) case *ast.EmbedRefNode: data.addRef(n.Ref) + case *ast.CiteNode: + data.addText(n.Key) case *ast.LiteralNode: data.addText(string(n.Content)) } return data } @@ -77,8 +74,8 @@ } if !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { - data.refs.Zid(zid) + data.refs.Add(zid) } } Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -12,26 +12,100 @@ import ( "context" "strconv" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" - "zettelstore.de/z/domain/meta" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { + + // Calculate computed, but stored values. + if _, ok := m.Get(api.KeyCreated); !ok { + m.Set(api.KeyCreated, computeCreated(m.Zid)) + } + if box.DoNotEnrich(ctx) { // Enrich is called indirectly via indexer or enrichment is not requested - // because of other reasons -> ignore this call, do not update meta data + // because of other reasons -> ignore this call, do not update metadata return } - m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) computePublished(m) + if boxNumber > 0 { + m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) + } mgr.idxStore.Enrich(ctx, m) } + +func computeCreated(zid id.Zid) string { + if zid <= 10101000000 { + // A year 0000 is not allowed and therefore an artificaial Zid. + // In the year 0001, the month must be > 0. + // In the month 000101, the day must be > 0. + return "00010101000000" + } + seconds := zid % 100 + if seconds > 59 { + seconds = 59 + } + zid /= 100 + minutes := zid % 100 + if minutes > 59 { + minutes = 59 + } + zid /= 100 + hours := zid % 100 + if hours > 23 { + hours = 23 + } + zid /= 100 + day := zid % 100 + zid /= 100 + month := zid % 100 + year := zid / 100 + month, day = sanitizeMonthDay(year, month, day) + created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds + return created.String() +} + +func sanitizeMonthDay(year, month, day id.Zid) (id.Zid, id.Zid) { + if day < 1 { + day = 1 + } + if month < 1 { + month = 1 + } + if month > 12 { + month = 12 + } + + switch month { + case 1, 3, 5, 7, 8, 10, 12: + if day > 31 { + day = 31 + } + case 4, 6, 9, 11: + if day > 30 { + day = 30 + } + case 2: + if year%4 != 0 || (year%100 == 0 && year%400 != 0) { + if day > 28 { + day = 28 + } + } else { + if day > 29 { + day = 29 + } + } + } + return month, day +} func computePublished(m *meta.Meta) { if _, ok := m.Get(api.KeyPublished); ok { return } @@ -38,10 +112,16 @@ if modified, ok := m.Get(api.KeyModified); ok { if _, ok = meta.TimeValue(modified); ok { m.Set(api.KeyPublished, modified) return } + } + if created, ok := m.Get(api.KeyCreated); ok { + if _, ok = meta.TimeValue(created); ok { + m.Set(api.KeyPublished, created) + return + } } zid := m.Zid.String() if _, ok := meta.TimeValue(zid); ok { m.Set(api.KeyPublished, zid) return Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -16,16 +16,16 @@ "net/url" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchEqual(word string) id.Set { @@ -73,12 +73,12 @@ // idxIndexer runs in the background and updates the index data structures. // This is the main service of the idxIndexer. func (mgr *Manager) idxIndexer() { // Something may panic. Ensure a running indexer. defer func() { - if r := recover(); r != nil { - kernel.Main.LogRecover("Indexer", r) + if ri := recover(); ri != nil { + kernel.Main.LogRecover("Indexer", ri) go mgr.idxIndexer() } }() timerDuration := 15 * time.Second @@ -107,42 +107,34 @@ start = time.Now() if rno := mgr.idxAr.Reload(zids); rno > 0 { roomNum = rno } mgr.idxMx.Lock() - mgr.idxLastReload = time.Now() + mgr.idxLastReload = time.Now().Local() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } - case arUpdate: - mgr.idxLog.Debug().Zid(zid).Msg("update") + case arZettel: + mgr.idxLog.Debug().Zid(zid).Msg("zettel") zettel, err := mgr.GetZettel(ctx, zid) if err != nil { - // TODO: on some errors put the zid into a "try later" set + // Zettel was deleted or is not accessible b/c of other reasons + mgr.idxLog.Trace().Zid(zid).Msg("delete") + mgr.idxMx.Lock() + mgr.idxSinceReload++ + mgr.idxMx.Unlock() + mgr.idxDeleteZettel(zid) continue } + mgr.idxLog.Trace().Zid(zid).Msg("update") mgr.idxMx.Lock() if arRoomNum == roomNum { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() mgr.idxUpdateZettel(ctx, zettel) - case arDelete: - mgr.idxLog.Debug().Zid(zid).Msg("delete") - if _, err := mgr.GetMeta(ctx, zid); err == nil { - // Zettel was not deleted. This might occur, if zettel was - // deleted in secondary dirbox, but is still present in - // first dirbox (or vice versa). Re-index zettel in case - // a hidden zettel was recovered - mgr.idxLog.Debug().Zid(zid).Msg("not deleted") - mgr.idxAr.Enqueue(zid, arUpdate) - } - mgr.idxMx.Lock() - mgr.idxSinceReload++ - mgr.idxMx.Unlock() - mgr.idxDeleteZettel(zid) } } } func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { @@ -163,27 +155,27 @@ return false } return true } -func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) { +func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) { var cData collectData cData.initialize() - collectZettelIndexData(parser.ParseZettel(zettel, "", mgr.rtConfig), &cData) + collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) m := zettel.Meta - zi := store.NewZettelIndex(m.Zid) + zi := store.NewZettelIndex(m) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { - for _, pair := range m.Pairs() { + for _, pair := range m.ComputedPairs() { descr := meta.GetDescription(pair.Key) - if descr.IsComputed() { + if descr.IsProperty() { continue } switch descr.Type { case meta.TypeID: mgr.idxUpdateValue(ctx, descr.Inverse, pair.Value, zi) @@ -206,42 +198,41 @@ } } func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { for ref := range cData.refs { - if _, err := mgr.GetMeta(ctx, ref); err == nil { + if mgr.HasZettel(ctx, ref) { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } } zi.SetWords(cData.words) zi.SetUrls(cData.urls) - zi.SetITags(cData.itags) } func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } - if _, err = mgr.GetMeta(ctx, zid); err != nil { + if !mgr.HasZettel(ctx, zid) { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) return } - zi.AddMetaRef(inverseKey, zid) + zi.AddInverseRef(inverseKey, zid) } func (mgr *Manager) idxDeleteZettel(zid id.Zid) { toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCheckZettel(s id.Set) { for zid := range s { - mgr.idxAr.Enqueue(zid, arUpdate) + mgr.idxAr.EnqueueZettel(zid) } } Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -16,21 +16,21 @@ "io" "net/url" "sync" "time" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/maps" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/memstore" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. @@ -83,12 +83,13 @@ func GetSchemes() []string { return maps.Keys(registry) } // Manager is a coordinating box. type Manager struct { mgrLog *logger.Logger + stateMx sync.RWMutex + state box.StartState mgrMx sync.RWMutex - started bool rtConfig config.Config boxes []box.ManagedBox observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} @@ -96,19 +97,32 @@ propertyKeys strfun.Set // Set of property key names // Indexer data idxLog *logger.Logger idxStore store.Store - idxAr *anterooms + idxAr *anteroomQueue idxReady chan struct{} // Signal a non-empty anteroom to background task // Indexer stats data idxMx sync.RWMutex idxLastReload time.Time idxDurReload time.Duration idxSinceReload uint64 } + +func (mgr *Manager) setState(newState box.StartState) { + mgr.stateMx.Lock() + mgr.state = newState + mgr.stateMx.Unlock() +} + +func (mgr *Manager) State() box.StartState { + mgr.stateMx.RLock() + state := mgr.state + mgr.stateMx.RUnlock() + return state +} // New creates a new managing box. func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { descrs := meta.GetSortedKeyDescriptions() propertyKeys := make(strfun.Set, len(descrs)) @@ -124,11 +138,11 @@ infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, idxLog: boxLog.Clone().Str("box", "index").Child(), idxStore: memstore.New(), - idxAr: newAnterooms(10), + idxAr: newAnteroomQueue(1000), idxReady: make(chan struct{}, 1), } cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { @@ -167,12 +181,12 @@ } func (mgr *Manager) notifier() { // The call to notify may panic. Ensure a running notifier. defer func() { - if r := recover(); r != nil { - kernel.Main.LogRecover("Notifier", r) + if ri := recover(); ri != nil { + kernel.Main.LogRecover("Notifier", ri) go mgr.notifier() } }() tsLastEvent := time.Now() @@ -193,15 +207,18 @@ mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier") if ignoreUpdate(cache, now, reason, zid) { mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored") continue } + mgr.idxEnqueue(reason, zid) if ci.Box == nil { ci.Box = mgr } - mgr.notifyObserver(&ci) + if mgr.State() == box.StartStateStarted { + mgr.notifyObserver(&ci) + } } case <-mgr.done: return } } @@ -226,17 +243,18 @@ return false } func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { switch reason { + case box.OnReady: + return case box.OnReload: mgr.idxAr.Reset() - case box.OnUpdate: - mgr.idxAr.Enqueue(zid, arUpdate) - case box.OnDelete: - mgr.idxAr.Enqueue(zid, arDelete) + case box.OnZettel: + mgr.idxAr.EnqueueZettel(zid) default: + mgr.mgrLog.Warn().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") return } select { case mgr.idxReady <- struct{}{}: default: @@ -254,65 +272,94 @@ // Start the box. Now all other functions of the box are allowed. // Starting an already started box is not allowed. func (mgr *Manager) Start(ctx context.Context) error { mgr.mgrMx.Lock() - if mgr.started { - mgr.mgrMx.Unlock() + defer mgr.mgrMx.Unlock() + if mgr.State() != box.StartStateStopped { return box.ErrStarted } + mgr.setState(box.StartStateStarting) for i := len(mgr.boxes) - 1; i >= 0; i-- { ssi, ok := mgr.boxes[i].(box.StartStopper) if !ok { continue } err := ssi.Start(ctx) if err == nil { continue } + mgr.setState(box.StartStateStopping) for j := i + 1; j < len(mgr.boxes); j++ { if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 { ssj.Stop(ctx) } } - mgr.mgrMx.Unlock() + mgr.setState(box.StartStateStopped) return err } mgr.idxAr.Reset() // Ensure an initial index run mgr.done = make(chan struct{}) go mgr.notifier() - go mgr.idxIndexer() + + mgr.waitBoxesAreStarted() + mgr.setState(box.StartStateStarted) + mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady}) - mgr.started = true - mgr.mgrMx.Unlock() + go mgr.idxIndexer() return nil } + +func (mgr *Manager) waitBoxesAreStarted() { + const waitTime = 10 * time.Millisecond + const waitLoop = int(1 * time.Second / waitTime) + for i := 1; !mgr.allBoxesStarted(); i++ { + if i%waitLoop == 0 { + if time.Duration(i)*waitTime > time.Minute { + mgr.mgrLog.Warn().Msg("Waiting for more than one minute to start") + } else { + mgr.mgrLog.Trace().Msg("Wait for boxes to start") + } + } + time.Sleep(waitTime) + } +} + +func (mgr *Manager) allBoxesStarted() bool { + for _, bx := range mgr.boxes { + if b, ok := bx.(box.StartStopper); ok && b.State() != box.StartStateStarted { + return false + } + } + return true +} // Stop the started box. Now only the Start() function is allowed. func (mgr *Manager) Stop(ctx context.Context) { mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() - if !mgr.started { + if mgr.State() != box.StartStateStarted { return } + mgr.setState(box.StartStateStopping) close(mgr.done) for _, p := range mgr.boxes { if ss, ok := p.(box.StartStopper); ok { ss.Stop(ctx) } } - mgr.started = false + mgr.setState(box.StartStateStopped) } // Refresh internal box data. func (mgr *Manager) Refresh(ctx context.Context) error { mgr.mgrLog.Debug().Msg("Refresh") + if mgr.State() != box.StartStateStarted { + return box.ErrStopped + } mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() - if !mgr.started { - return box.ErrStopped - } mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} for _, bx := range mgr.boxes { if rb, ok := bx.(box.Refresher); ok { rb.Refresh(ctx) } Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ box/manager/memstore/memstore.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -17,67 +17,74 @@ "io" "sort" "strings" "sync" - "zettelstore.de/c/api" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/maps" + "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) -type metaRefs struct { +type bidiRefs struct { forward id.Slice backward id.Slice } -type zettelIndex struct { - dead id.Slice - forward id.Slice - backward id.Slice - meta map[string]metaRefs - words []string - urls []string - itags string // Inline tags -} - -func (zi *zettelIndex) isEmpty() bool { - if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 { - return false - } - return len(zi.meta) == 0 +type zettelData struct { + meta *meta.Meta + dead id.Slice + forward id.Slice + backward id.Slice + otherRefs map[string]bidiRefs + words []string + urls []string } type stringRefs map[string]id.Slice type memStore struct { - mx sync.RWMutex - idx map[id.Zid]*zettelIndex - dead map[id.Zid]id.Slice // map dead refs where they occur - words stringRefs - urls stringRefs + mx sync.RWMutex + intern map[string]string // map to intern strings + idx map[id.Zid]*zettelData + dead map[id.Zid]id.Slice // map dead refs where they occur + words stringRefs + urls stringRefs // Stats + mxStats sync.Mutex updates uint64 } // New returns a new memory-based index store. func New() store.Store { return &memStore{ - idx: make(map[id.Zid]*zettelIndex), - dead: make(map[id.Zid]id.Slice), - words: make(stringRefs), - urls: make(stringRefs), + intern: make(map[string]string, 1024), + idx: make(map[id.Zid]*zettelData), + dead: make(map[id.Zid]id.Slice), + words: make(stringRefs), + urls: make(stringRefs), + } +} + +func (ms *memStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { + ms.mx.RLock() + defer ms.mx.RUnlock() + if zi, found := ms.idx[zid]; found && zi.meta != nil { + // zi.meta is nil, if zettel was referenced, but is not indexed yet. + return zi.meta.Clone(), nil } + return nil, box.ErrZettelNotFound{Zid: zid} } func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) { if ms.doEnrich(m) { - ms.mx.Lock() + ms.mxStats.Lock() ms.updates++ - ms.mx.Unlock() + ms.mxStats.Unlock() } } func (ms *memStore) doEnrich(m *meta.Meta) bool { ms.mx.RLock() @@ -89,21 +96,21 @@ var updated bool if len(zi.dead) > 0 { m.Set(api.KeyDead, zi.dead.String()) updated = true } - back := removeOtherMetaRefs(m, zi.backward.Copy()) + back := removeOtherMetaRefs(m, zi.backward.Clone()) if len(zi.backward) > 0 { m.Set(api.KeyBackward, zi.backward.String()) updated = true } if len(zi.forward) > 0 { m.Set(api.KeyForward, zi.forward.String()) back = remRefs(back, zi.forward) updated = true } - for k, refs := range zi.meta { + for k, refs := range zi.otherRefs { if len(refs.backward) > 0 { m.Set(k, refs.backward.String()) back = remRefs(back, refs.backward) updated = true } @@ -110,22 +117,10 @@ } if len(back) > 0 { m.Set(api.KeyBack, back.String()) updated = true } - if itags := zi.itags; itags != "" { - m.Set(api.KeyContentTags, itags) - if tags, ok2 := m.Get(api.KeyTags); ok2 { - m.Set(api.KeyAllTags, tags+" "+itags) - } else { - m.Set(api.KeyAllTags, itags) - } - updated = true - } else if tags, ok2 := m.Get(api.KeyTags); ok2 { - m.Set(api.KeyAllTags, tags) - updated = true - } return updated } // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. @@ -132,14 +127,14 @@ func (ms *memStore) SearchEqual(word string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := id.NewSet() if refs, ok := ms.words[word]; ok { - result.AddSlice(refs) + result.CopySlice(refs) } if refs, ok := ms.urls[word]; ok { - result.AddSlice(refs) + result.CopySlice(refs) } zid, err := id.Parse(word) if err != nil { return result } @@ -160,17 +155,22 @@ result := ms.selectWithPred(prefix, strings.HasPrefix) l := len(prefix) if l > 14 { return result } - minZid, err := id.Parse(prefix + "00000000000000"[:14-l]) + maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) if err != nil { return result } - maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) - if err != nil { - return result + var minZid id.Zid + if l < 14 && prefix == "0000000000000"[:l] { + minZid = id.Zid(1) + } else { + minZid, err = id.Parse(prefix + "00000000000000"[:14-l]) + if err != nil { + return result + } } for zid, zi := range ms.idx { if minZid <= zid && zid <= maxZid { addBackwardZids(result, zid, zi) } @@ -229,27 +229,27 @@ result := id.NewSet() for word, refs := range ms.words { if !pred(word, s) { continue } - result.AddSlice(refs) + result.CopySlice(refs) } for u, refs := range ms.urls { if !pred(u, s) { continue } - result.AddSlice(refs) + result.CopySlice(refs) } return result } -func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) { +func addBackwardZids(result id.Set, zid id.Zid, zi *zettelData) { // Must only be called if ms.mx is read-locked! - result.Zid(zid) - result.AddSlice(zi.backward) - for _, mref := range zi.meta { - result.AddSlice(mref.backward) + result.Add(zid) + result.CopySlice(zi.backward) + for _, mref := range zi.otherRefs { + result.CopySlice(mref.backward) } } func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { for _, p := range m.PairsRest() { @@ -268,15 +268,16 @@ } return back } func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { + m := ms.makeMeta(zidx) ms.mx.Lock() defer ms.mx.Unlock() zi, ziExist := ms.idx[zidx.Zid] if !ziExist || zi == nil { - zi = &zettelIndex{} + zi = &zettelData{} ziExist = false } // Is this zettel an old dead reference mentioned in other zettel? var toCheck id.Set @@ -284,26 +285,65 @@ // These must be checked later again toCheck = id.NewSet(refs...) delete(ms.dead, zidx.Zid) } + zi.meta = m ms.updateDeadReferences(zidx, zi) - ms.updateForwardBackwardReferences(zidx, zi) - ms.updateMetadataReferences(zidx, zi) + ids := ms.updateForwardBackwardReferences(zidx, zi) + toCheck = toCheck.Copy(ids) + ids = ms.updateMetadataReferences(zidx, zi) + toCheck = toCheck.Copy(ids) zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords()) zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) - zi.itags = setITags(zidx.GetITags()) // Check if zi must be inserted into ms.idx - if !ziExist && !zi.isEmpty() { + if !ziExist { ms.idx[zidx.Zid] = zi } return toCheck } -func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) { +var internableKeys = map[string]bool{ + api.KeyRole: true, + api.KeySyntax: true, + api.KeyFolgeRole: true, + api.KeyLang: true, + api.KeyReadOnly: true, +} + +func isInternableValue(key string) bool { + if internableKeys[key] { + return true + } + return strings.HasSuffix(key, "-role") +} + +func (ms *memStore) internString(s string) string { + if is, found := ms.intern[s]; found { + return is + } + ms.intern[s] = s + return s +} + +func (ms *memStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta { + origM := zidx.GetMeta() + copyM := meta.New(origM.Zid) + for _, p := range origM.Pairs() { + key := ms.internString(p.Key) + if isInternableValue(key) { + copyM.Set(key, ms.internString(p.Value)) + } else if key == api.KeyBoxNumber || !meta.IsComputed(key) { + copyM.Set(key, p.Value) + } + } + return copyM +} + +func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) { // Must only be called if ms.mx is write-locked! drefs := zidx.GetDeadRefs() newRefs, remRefs := refsDiff(drefs, zi.dead) zi.dead = drefs for _, ref := range remRefs { @@ -312,58 +352,71 @@ for _, ref := range newRefs { ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) } } -func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) { +func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set { // Must only be called if ms.mx is write-locked! brefs := zidx.GetBackRefs() newRefs, remRefs := refsDiff(brefs, zi.forward) zi.forward = brefs + + var toCheck id.Set for _, ref := range remRefs { - bzi := ms.getEntry(ref) + bzi := ms.getOrCreateEntry(ref) bzi.backward = remRef(bzi.backward, zidx.Zid) + if bzi.meta == nil { + toCheck = toCheck.Add(ref) + } } for _, ref := range newRefs { - bzi := ms.getEntry(ref) + bzi := ms.getOrCreateEntry(ref) bzi.backward = addRef(bzi.backward, zidx.Zid) + if bzi.meta == nil { + toCheck = toCheck.Add(ref) + } } + return toCheck } -func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) { +func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set { // Must only be called if ms.mx is write-locked! - metarefs := zidx.GetMetaRefs() - for key, mr := range zi.meta { - if _, ok := metarefs[key]; ok { + inverseRefs := zidx.GetInverseRefs() + for key, mr := range zi.otherRefs { + if _, ok := inverseRefs[key]; ok { continue } ms.removeInverseMeta(zidx.Zid, key, mr.forward) } - if zi.meta == nil { - zi.meta = make(map[string]metaRefs) + if zi.otherRefs == nil { + zi.otherRefs = make(map[string]bidiRefs) } - for key, mrefs := range metarefs { - mr := zi.meta[key] + var toCheck id.Set + for key, mrefs := range inverseRefs { + mr := zi.otherRefs[key] newRefs, remRefs := refsDiff(mrefs, mr.forward) mr.forward = mrefs - zi.meta[key] = mr + zi.otherRefs[key] = mr for _, ref := range newRefs { - bzi := ms.getEntry(ref) - if bzi.meta == nil { - bzi.meta = make(map[string]metaRefs) + bzi := ms.getOrCreateEntry(ref) + if bzi.otherRefs == nil { + bzi.otherRefs = make(map[string]bidiRefs) } - bmr := bzi.meta[key] + bmr := bzi.otherRefs[key] bmr.backward = addRef(bmr.backward, zidx.Zid) - bzi.meta[key] = bmr + bzi.otherRefs[key] = bmr + if bzi.meta == nil { + toCheck = toCheck.Add(ref) + } } ms.removeInverseMeta(zidx.Zid, key, remRefs) } + return toCheck } func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { - // Must only be called if ms.mx is write-locked! newWords, removeWords := next.Diff(prev) for _, word := range newWords { if refs, ok := srefs[word]; ok { srefs[word] = addRef(refs, zid) continue @@ -383,25 +436,16 @@ srefs[word] = refs2 } return next.Words() } -func setITags(next store.WordSet) string { - itags := next.Words() - if len(itags) == 0 { - return "" - } - sort.Strings(itags) - return strings.Join(itags, " ") -} - -func (ms *memStore) getEntry(zid id.Zid) *zettelIndex { +func (ms *memStore) getOrCreateEntry(zid id.Zid) *zettelData { // Must only be called if ms.mx is write-locked! if zi, ok := ms.idx[zid]; ok { return zi } - zi := &zettelIndex{} + zi := &zettelData{} ms.idx[zid] = zi return zi } func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set { @@ -413,21 +457,21 @@ return nil } ms.deleteDeadSources(zid, zi) toCheck := ms.deleteForwardBackward(zid, zi) - if len(zi.meta) > 0 { - for key, mrefs := range zi.meta { + if len(zi.otherRefs) > 0 { + for key, mrefs := range zi.otherRefs { ms.removeInverseMeta(zid, key, mrefs.forward) } } ms.deleteWords(zid, zi.words) delete(ms.idx, zid) return toCheck } -func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) { +func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelData) { // Must only be called if ms.mx is write-locked! for _, ref := range zi.dead { if drefs, ok := ms.dead[ref]; ok { drefs = remRef(drefs, zid) if len(drefs) > 0 { @@ -437,11 +481,11 @@ } } } } -func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set { +func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelData) id.Set { // Must only be called if ms.mx is write-locked! var toCheck id.Set for _, ref := range zi.forward { if fzi, ok := ms.idx[ref]; ok { fzi.backward = remRef(fzi.backward, zid) @@ -451,34 +495,34 @@ if bzi, ok := ms.idx[ref]; ok { bzi.forward = remRef(bzi.forward, zid) if toCheck == nil { toCheck = id.NewSet() } - toCheck.Zid(ref) + toCheck.Add(ref) } } return toCheck } func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { // Must only be called if ms.mx is write-locked! for _, ref := range forward { bzi, ok := ms.idx[ref] - if !ok || bzi.meta == nil { + if !ok || bzi.otherRefs == nil { continue } - bmr, ok := bzi.meta[key] + bmr, ok := bzi.otherRefs[key] if !ok { continue } bmr.backward = remRef(bmr.backward, zid) if len(bmr.backward) > 0 || len(bmr.forward) > 0 { - bzi.meta[key] = bmr + bzi.otherRefs[key] = bmr } else { - delete(bzi.meta, key) - if len(bzi.meta) == 0 { - bzi.meta = nil + delete(bzi.otherRefs, key) + if len(bzi.otherRefs) == 0 { + bzi.otherRefs = nil } } } } @@ -499,14 +543,16 @@ } func (ms *memStore) ReadStats(st *store.Stats) { ms.mx.RLock() st.Zettel = len(ms.idx) - st.Updates = ms.updates st.Words = uint64(len(ms.words)) st.Urls = uint64(len(ms.urls)) ms.mx.RUnlock() + ms.mxStats.Lock() + st.Updates = ms.updates + ms.mxStats.Unlock() } func (ms *memStore) Dump(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() @@ -534,11 +580,11 @@ if len(zi.dead) > 0 { fmt.Fprintln(w, "* Dead:", zi.dead) } dumpZids(w, "* Forward:", zi.forward) dumpZids(w, "* Backward:", zi.backward) - for k, fb := range zi.meta { + for k, fb := range zi.otherRefs { fmt.Fprintln(w, "* Meta", k) dumpZids(w, "** Forward:", fb.forward) dumpZids(w, "** Backward:", fb.backward) } dumpStrings(w, "* Words", "", "", zi.words) Index: box/manager/memstore/refs.go ================================================================== --- box/manager/memstore/refs.go +++ box/manager/memstore/refs.go @@ -1,18 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package memstore -import "zettelstore.de/z/domain/id" +import ( + "slices" + + "zettelstore.de/z/zettel/id" +) func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { npos, opos := 0, 0 for npos < len(refsN) && opos < len(refsO) { rn, ro := refsN[npos], refsO[opos] @@ -48,13 +52,11 @@ lo = m + 1 } else { hi = m } } - refs = append(refs, id.Invalid) - copy(refs[hi+1:], refs[hi:]) - refs[hi] = ref + refs = slices.Insert(refs, hi, ref) return refs } func remRefs(refs, rem id.Slice) id.Slice { if len(refs) == 0 || len(rem) == 0 { Index: box/manager/memstore/refs_test.go ================================================================== --- box/manager/memstore/refs_test.go +++ box/manager/memstore/refs_test.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -11,11 +11,11 @@ package memstore import ( "testing" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/zettel/id" ) func assertRefs(t *testing.T, i int, got, exp id.Slice) { t.Helper() if got == nil && exp != nil { Index: box/manager/store/store.go ================================================================== --- box/manager/store/store.go +++ box/manager/store/store.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -13,13 +13,13 @@ import ( "context" "io" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" - "zettelstore.de/z/search" + "zettelstore.de/z/query" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // Stats records statistics about the store. type Stats struct { // Zettel is the number of zettel managed by the indexer. @@ -36,11 +36,14 @@ } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { - search.Searcher + query.Searcher + + // GetMeta returns the metadata of the zettel with the given identifier. + GetMeta(context.Context, id.Zid) (*meta.Meta, error) // Entrich metadata with data from store. Enrich(ctx context.Context, m *meta.Meta) // UpdateReferences for a specific zettel. Index: box/manager/store/wordset.go ================================================================== --- box/manager/store/wordset.go +++ box/manager/store/wordset.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- Index: box/manager/store/wordset_test.go ================================================================== --- box/manager/store/wordset_test.go +++ box/manager/store/wordset_test.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- Index: box/manager/store/zettel.go ================================================================== --- box/manager/store/zettel.go +++ box/manager/store/zettel.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -8,80 +8,80 @@ // under this license. //----------------------------------------------------------------------------- package store -import "zettelstore.de/z/domain/id" +import ( + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) // ZettelIndex contains all index data of a zettel. type ZettelIndex struct { - Zid id.Zid // zid of the indexed zettel - backrefs id.Set // set of back references - metarefs map[string]id.Set // references to inverse keys - deadrefs id.Set // set of dead references - words WordSet - urls WordSet - itags WordSet + Zid id.Zid // zid of the indexed zettel + meta *meta.Meta // full metadata + backrefs id.Set // set of back references + inverseRefs map[string]id.Set // references of inverse keys + deadrefs id.Set // set of dead references + words WordSet + urls WordSet } // NewZettelIndex creates a new zettel index. -func NewZettelIndex(zid id.Zid) *ZettelIndex { +func NewZettelIndex(m *meta.Meta) *ZettelIndex { return &ZettelIndex{ - Zid: zid, - backrefs: id.NewSet(), - metarefs: make(map[string]id.Set), - deadrefs: id.NewSet(), + Zid: m.Zid, + meta: m, + backrefs: id.NewSet(), + inverseRefs: make(map[string]id.Set), + deadrefs: id.NewSet(), } } // AddBackRef adds a reference to a zettel where the current zettel links to // without any more information. func (zi *ZettelIndex) AddBackRef(zid id.Zid) { - zi.backrefs.Zid(zid) + zi.backrefs.Add(zid) } -// AddMetaRef adds a named reference to a zettel. On that zettel, the given +// AddInverseRef adds a named reference to a zettel. On that zettel, the given // metadata key should point back to the current zettel. -func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) { - if zids, ok := zi.metarefs[key]; ok { - zids.Zid(zid) +func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) { + if zids, ok := zi.inverseRefs[key]; ok { + zids.Add(zid) return } - zi.metarefs[key] = id.NewSet(zid) + zi.inverseRefs[key] = id.NewSet(zid) } // AddDeadRef adds a dead reference to a zettel. func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { - zi.deadrefs.Zid(zid) + zi.deadrefs.Add(zid) } // SetWords sets the words to the given value. func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } // SetUrls sets the words to the given value. func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls } -// SetITags sets the words to the given value. -func (zi *ZettelIndex) SetITags(itags WordSet) { zi.itags = itags } +// GetDeadRefs returns all dead references as a sorted list. +func (zi *ZettelIndex) GetDeadRefs() id.Slice { return zi.deadrefs.Sorted() } -// GetDeadRefs returns all dead references as a sorted list. -func (zi *ZettelIndex) GetDeadRefs() id.Slice { - return zi.deadrefs.Sorted() -} +// GetMeta return just the raw metadata. +func (zi *ZettelIndex) GetMeta() *meta.Meta { return zi.meta } // GetBackRefs returns all back references as a sorted list. -func (zi *ZettelIndex) GetBackRefs() id.Slice { - return zi.backrefs.Sorted() -} +func (zi *ZettelIndex) GetBackRefs() id.Slice { return zi.backrefs.Sorted() } -// GetMetaRefs returns all meta references as a map of strings to a sorted list of references -func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice { - if len(zi.metarefs) == 0 { +// GetInverseRefs returns all inverse meta references as a map of strings to a sorted list of references +func (zi *ZettelIndex) GetInverseRefs() map[string]id.Slice { + if len(zi.inverseRefs) == 0 { return nil } - result := make(map[string]id.Slice, len(zi.metarefs)) - for key, refs := range zi.metarefs { + result := make(map[string]id.Slice, len(zi.inverseRefs)) + for key, refs := range zi.inverseRefs { result[key] = refs.Sorted() } return result } @@ -88,8 +88,5 @@ // GetWords returns a reference to the set of words. It must not be modified. func (zi *ZettelIndex) GetWords() WordSet { return zi.words } // GetUrls returns a reference to the set of URLs. It must not be modified. func (zi *ZettelIndex) GetUrls() WordSet { return zi.urls } - -// GetITags returns a reference to the set of internal tags. It must not be modified. -func (zi *ZettelIndex) GetITags() WordSet { return zi.itags } Index: box/membox/membox.go ================================================================== --- box/membox/membox.go +++ box/membox/membox.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -16,16 +16,15 @@ "net/url" "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" - "zettelstore.de/z/search" + "zettelstore.de/z/query" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" ) func init() { manager.Register( "mem", @@ -46,27 +45,36 @@ u *url.URL cdata manager.ConnectData maxZettel int maxBytes int mx sync.RWMutex // Protects the following fields - zettel map[id.Zid]domain.Zettel + zettel map[id.Zid]zettel.Zettel curBytes int } -func (mb *memBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { +func (mb *memBox) notifyChanged(zid id.Zid) { if chci := mb.cdata.Notify; chci != nil { - chci <- box.UpdateInfo{Reason: reason, Zid: zid} + chci <- box.UpdateInfo{Box: mb, Reason: box.OnZettel, Zid: zid} } } func (mb *memBox) Location() string { return mb.u.String() } + +func (mb *memBox) State() box.StartState { + mb.mx.RLock() + defer mb.mx.RUnlock() + if mb.zettel == nil { + return box.StartStateStopped + } + return box.StartStateStarted +} func (mb *memBox) Start(context.Context) error { mb.mx.Lock() - mb.zettel = make(map[id.Zid]domain.Zettel) + mb.zettel = make(map[id.Zid]zettel.Zettel) mb.curBytes = 0 mb.mx.Unlock() mb.log.Trace().Int("max-zettel", int64(mb.maxZettel)).Int("max-bytes", int64(mb.maxBytes)).Msg("Start Box") return nil } @@ -81,11 +89,11 @@ mb.mx.RLock() defer mb.mx.RUnlock() return len(mb.zettel) < mb.maxZettel } -func (mb *memBox) CreateZettel(_ context.Context, zettel domain.Zettel) (id.Zid, error) { +func (mb *memBox) CreateZettel(_ context.Context, zettel zettel.Zettel) (id.Zid, error) { mb.mx.Lock() newBytes := mb.curBytes + zettel.Length() if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes { mb.mx.Unlock() return id.Invalid, box.ErrCapacity @@ -102,39 +110,35 @@ meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(box.OnUpdate, zid) + mb.notifyChanged(zid) mb.log.Trace().Zid(zid).Msg("CreateZettel") return zid, nil } -func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { - mb.mx.RLock() - zettel, ok := mb.zettel[zid] - mb.mx.RUnlock() - if !ok { - return domain.Zettel{}, box.ErrNotFound - } - zettel.Meta = zettel.Meta.Clone() - mb.log.Trace().Msg("GetZettel") - return zettel, nil -} - -func (mb *memBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { - mb.mx.RLock() - zettel, ok := mb.zettel[zid] - mb.mx.RUnlock() - if !ok { - return nil, box.ErrNotFound - } - mb.log.Trace().Msg("GetMeta") - return zettel.Meta.Clone(), nil -} - -func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint search.RetrievePredicate) error { +func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { + mb.mx.RLock() + z, ok := mb.zettel[zid] + mb.mx.RUnlock() + if !ok { + return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} + } + z.Meta = z.Meta.Clone() + mb.log.Trace().Msg("GetZettel") + return z, nil +} + +func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool { + mb.mx.RLock() + _, found := mb.zettel[zid] + mb.mx.RUnlock() + return found +} + +func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyZid") for zid := range mb.zettel { if constraint(zid) { @@ -142,11 +146,11 @@ } } return nil } -func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint search.RetrievePredicate) error { +func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyMeta") for zid, zettel := range mb.zettel { if constraint(zid) { @@ -156,11 +160,11 @@ } } return nil } -func (mb *memBox) CanUpdateZettel(_ context.Context, zettel domain.Zettel) bool { +func (mb *memBox) CanUpdateZettel(_ context.Context, zettel zettel.Zettel) bool { mb.mx.RLock() defer mb.mx.RUnlock() zid := zettel.Meta.Zid if !zid.IsValid() { return false @@ -171,14 +175,14 @@ newBytes -= prevZettel.Length() } return newBytes < mb.maxBytes } -func (mb *memBox) UpdateZettel(_ context.Context, zettel domain.Zettel) error { +func (mb *memBox) UpdateZettel(_ context.Context, zettel zettel.Zettel) error { m := zettel.Meta.Clone() if !m.Zid.IsValid() { - return &box.ErrInvalidID{Zid: m.Zid} + return box.ErrInvalidZid{Zid: m.Zid.String()} } mb.mx.Lock() newBytes := mb.curBytes + zettel.Length() if prevZettel, found := mb.zettel[m.Zid]; found { @@ -191,11 +195,11 @@ zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(box.OnUpdate, m.Zid) + mb.notifyChanged(m.Zid) mb.log.Trace().Msg("UpdateZettel") return nil } func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true } @@ -203,27 +207,27 @@ func (mb *memBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { mb.mx.Lock() zettel, ok := mb.zettel[curZid] if !ok { mb.mx.Unlock() - return box.ErrNotFound + return box.ErrZettelNotFound{Zid: curZid} } // Check that there is no zettel with newZid if _, ok = mb.zettel[newZid]; ok { mb.mx.Unlock() - return &box.ErrInvalidID{Zid: newZid} + return box.ErrInvalidZid{Zid: newZid.String()} } meta := zettel.Meta.Clone() meta.Zid = newZid zettel.Meta = meta mb.zettel[newZid] = zettel delete(mb.zettel, curZid) mb.mx.Unlock() - mb.notifyChanged(box.OnDelete, curZid) - mb.notifyChanged(box.OnUpdate, newZid) + mb.notifyChanged(curZid) + mb.notifyChanged(newZid) mb.log.Trace().Msg("RenameZettel") return nil } func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { @@ -236,16 +240,16 @@ func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error { mb.mx.Lock() oldZettel, found := mb.zettel[zid] if !found { mb.mx.Unlock() - return box.ErrNotFound + return box.ErrZettelNotFound{Zid: zid} } delete(mb.zettel, zid) mb.curBytes -= oldZettel.Length() mb.mx.Unlock() - mb.notifyChanged(box.OnDelete, zid) + mb.notifyChanged(zid) mb.log.Trace().Msg("DeleteZettel") return nil } func (mb *memBox) ReadStats(st *box.ManagedBoxStats) { Index: box/notify/directory.go ================================================================== --- box/notify/directory.go +++ box/notify/directory.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -17,68 +17,80 @@ "regexp" "strings" "sync" "zettelstore.de/z/box" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/parser" - "zettelstore.de/z/search" + "zettelstore.de/z/query" "zettelstore.de/z/strfun" + "zettelstore.de/z/zettel/id" ) type entrySet map[id.Zid]*DirEntry -// directoryState signal the internal state of the service. +// DirServiceState signal the internal state of the service. // // The following state transitions are possible: // --newDirService--> dsCreated // dsCreated --Start--> dsStarting // dsStarting --last list notification--> dsWorking // dsWorking --directory missing--> dsMissing // dsMissing --last list notification--> dsWorking // --Stop--> dsStopping -type directoryState uint8 +type DirServiceState uint8 const ( - dsCreated directoryState = iota - dsStarting // Reading inital scan - dsWorking // Initial scan complete, fully operational - dsMissing // Directory is missing - dsStopping // Service is shut down + DsCreated DirServiceState = iota + DsStarting // Reading inital scan + DsWorking // Initial scan complete, fully operational + DsMissing // Directory is missing + DsStopping // Service is shut down ) // DirService specifies a directory service for file based zettel. type DirService struct { + box box.ManagedBox log *logger.Logger dirPath string notifier Notifier infos chan<- box.UpdateInfo mx sync.RWMutex // protects status, entries - state directoryState + state DirServiceState entries entrySet } // ErrNoDirectory signals missing directory data. var ErrNoDirectory = errors.New("unable to retrieve zettel directory information") // NewDirService creates a new directory service. -func NewDirService(log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService { +func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService { return &DirService{ + box: box, log: log, notifier: notifier, infos: chci, - state: dsCreated, + state: DsCreated, } } + +// State the current service state. +func (ds *DirService) State() DirServiceState { + ds.mx.RLock() + state := ds.state + ds.mx.RUnlock() + return state +} // Start the directory service. func (ds *DirService) Start() { ds.mx.Lock() - ds.state = dsStarting + ds.state = DsStarting ds.mx.Unlock() - go ds.updateEvents() + var newEntries entrySet + go ds.updateEvents(newEntries) } // Refresh the directory entries. func (ds *DirService) Refresh() { ds.notifier.Refresh() @@ -85,11 +97,11 @@ } // Stop the directory service. func (ds *DirService) Stop() { ds.mx.Lock() - ds.state = dsStopping + ds.state = DsStopping ds.mx.Unlock() ds.notifier.Close() } func (ds *DirService) logMissingEntry(action string) error { @@ -107,11 +119,11 @@ } return len(ds.entries) } // GetDirEntries returns a list of directory entries, which satisfy the given constraint. -func (ds *DirService) GetDirEntries(constraint search.RetrievePredicate) []*DirEntry { +func (ds *DirService) GetDirEntries(constraint query.RetrievePredicate) []*DirEntry { ds.mx.RLock() defer ds.mx.RUnlock() if ds.entries == nil { return nil } @@ -177,11 +189,11 @@ defer ds.mx.Unlock() if ds.entries == nil { return DirEntry{}, ds.logMissingEntry("rename") } if _, found := ds.entries[newZid]; found { - return DirEntry{}, &box.ErrInvalidID{Zid: newZid} + return DirEntry{}, box.ErrInvalidZid{Zid: newZid.String()} } oldZid := oldEntry.Zid newEntry := DirEntry{ Zid: newZid, MetaName: renameFilename(oldEntry.MetaName, oldZid, newZid), @@ -210,68 +222,87 @@ } delete(ds.entries, zid) return nil } -func (ds *DirService) updateEvents() { - var newEntries entrySet +func (ds *DirService) updateEvents(newEntries entrySet) { + // Something may panic. Ensure a running service. + defer func() { + if ri := recover(); ri != nil { + kernel.Main.LogRecover("DirectoryService", ri) + go ds.updateEvents(newEntries) + } + }() + for ev := range ds.notifier.Events() { - ds.mx.RLock() - state := ds.state - ds.mx.RUnlock() - - if msg := ds.log.Trace(); msg.Enabled() { - msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent") - } - if state == dsStopping { + e, ok := ds.handleEvent(ev, newEntries) + if !ok { break } - - switch ev.Op { - case Error: - newEntries = nil - if state != dsMissing { - ds.log.Warn().Err(ev.Err).Msg("Notifier confused") - } - case Make: - newEntries = make(entrySet) - case List: - if ev.Name == "" { - zids := getNewZids(newEntries) - ds.mx.Lock() - fromMissing := ds.state == dsMissing - prevEntries := ds.entries - ds.entries = newEntries - ds.state = dsWorking - ds.mx.Unlock() - newEntries = nil - ds.onCreateDirectory(zids, prevEntries) - if fromMissing { - ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found") - } - } else if newEntries != nil { - ds.onUpdateFileEvent(newEntries, ev.Name) - } - case Destroy: - newEntries = nil - ds.onDestroyDirectory() - ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing") - case Update: - ds.mx.Lock() - zid := ds.onUpdateFileEvent(ds.entries, ev.Name) - ds.mx.Unlock() - if zid != id.Invalid { - ds.notifyChange(box.OnUpdate, zid) - } - case Delete: - ds.mx.Lock() - ds.onDeleteFileEvent(ds.entries, ev.Name) - ds.mx.Unlock() - default: - ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") - } - } + newEntries = e + } +} +func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) { + ds.mx.RLock() + state := ds.state + ds.mx.RUnlock() + + if msg := ds.log.Trace(); msg.Enabled() { + msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent") + } + if state == DsStopping { + return nil, false + } + + switch ev.Op { + case Error: + newEntries = nil + if state != DsMissing { + ds.log.Warn().Err(ev.Err).Msg("Notifier confused") + } + case Make: + newEntries = make(entrySet) + case List: + if ev.Name == "" { + zids := getNewZids(newEntries) + ds.mx.Lock() + fromMissing := ds.state == DsMissing + prevEntries := ds.entries + ds.entries = newEntries + ds.state = DsWorking + ds.mx.Unlock() + ds.onCreateDirectory(zids, prevEntries) + if fromMissing { + ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found") + } + return nil, true + } + if newEntries != nil { + ds.onUpdateFileEvent(newEntries, ev.Name) + } + case Destroy: + ds.onDestroyDirectory() + ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing") + return nil, true + case Update: + ds.mx.Lock() + zid := ds.onUpdateFileEvent(ds.entries, ev.Name) + ds.mx.Unlock() + if zid != id.Invalid { + ds.notifyChange(zid) + } + case Delete: + ds.mx.Lock() + zid := ds.onDeleteFileEvent(ds.entries, ev.Name) + ds.mx.Unlock() + if zid != id.Invalid { + ds.notifyChange(zid) + } + default: + ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") + } + return newEntries, true } func getNewZids(entries entrySet) id.Slice { zids := make(id.Slice, 0, len(entries)) for zid := range entries { @@ -280,29 +311,29 @@ return zids } func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) { for _, zid := range zids { - ds.notifyChange(box.OnUpdate, zid) + ds.notifyChange(zid) delete(prevEntries, zid) } // These were previously stored, by are not found now. // Notify system that these were deleted, e.g. for updating the index. for zid := range prevEntries { - ds.notifyChange(box.OnDelete, zid) + ds.notifyChange(zid) } } func (ds *DirService) onDestroyDirectory() { ds.mx.Lock() entries := ds.entries ds.entries = nil - ds.state = dsMissing + ds.state = DsMissing ds.mx.Unlock() for zid := range entries { - ds.notifyChange(box.OnDelete, zid) + ds.notifyChange(zid) } } var validFileName = regexp.MustCompile(`^(\d{14})`) @@ -344,30 +375,31 @@ if dupName1 != "" { ds.log.Warn().Str("name", dupName1).Msg("Duplicate content (is ignored)") if dupName2 != "" { ds.log.Warn().Str("name", dupName2).Msg("Duplicate content (is ignored)") } + return id.Invalid } return zid } -func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) { +func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid { if entries == nil { - return + return id.Invalid } zid := seekZid(name) if zid == id.Invalid { - return + return id.Invalid } entry, found := entries[zid] if !found { - return + return zid } for i, dupName := range entry.UselessFiles { if dupName == name { removeDuplicate(entry, i) - return + return zid } } if name == entry.ContentName { entry.ContentName = "" entry.ContentExt = "" @@ -376,12 +408,12 @@ entry.MetaName = "" ds.replayUpdateUselessFiles(entry) } if entry.ContentName == "" && entry.MetaName == "" { delete(entries, zid) - ds.notifyChange(box.OnDelete, zid) } + return zid } func removeDuplicate(entry *DirEntry, i int) { if len(entry.UselessFiles) == 1 { entry.UselessFiles = nil @@ -546,12 +578,15 @@ if newExt == "zmk" { return true } oldInfo := parser.Get(oldExt) newInfo := parser.Get(newExt) - if oldTextParser := oldInfo.IsTextParser; oldTextParser != newInfo.IsTextParser { - return !oldTextParser + if oldASTParser := oldInfo.IsASTParser; oldASTParser != newInfo.IsASTParser { + return !oldASTParser + } + if oldTextFormat := oldInfo.IsTextFormat; oldTextFormat != newInfo.IsTextFormat { + return !oldTextFormat } if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat { return oldImageFormat } if oldPrimary := primarySyntax.Has(oldExt); oldPrimary != primarySyntax.Has(newExt) { @@ -565,11 +600,11 @@ return newLen < oldLen } return newExt < oldExt } -func (ds *DirService) notifyChange(reason box.UpdateReason, zid id.Zid) { +func (ds *DirService) notifyChange(zid id.Zid) { if chci := ds.infos; chci != nil { - ds.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChange") - chci <- box.UpdateInfo{Reason: reason, Zid: zid} + ds.log.Trace().Zid(zid).Msg("notifyChange") + chci <- box.UpdateInfo{Box: ds.box, Reason: box.OnZettel, Zid: zid} } } Index: box/notify/directory_test.go ================================================================== --- box/notify/directory_test.go +++ box/notify/directory_test.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2022 Detlef Stern +// Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -11,17 +11,18 @@ package notify import ( "testing" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. + _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func TestSeekZid(t *testing.T) { testcases := []struct { name string @@ -46,15 +47,20 @@ } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats - api.ValueSyntaxZmk, "markdown", "md", + meta.SyntaxZmk, meta.SyntaxDraw, meta.SyntaxMarkdown, meta.SyntaxMD, // Other supported text formats - "css", "txt", api.ValueSyntaxHTML, api.ValueSyntaxNone, "mustache", api.ValueSyntaxText, "plain", - // Supported graphics formats - api.ValueSyntaxGif, "png", api.ValueSyntaxSVG, "jpeg", "jpg", + meta.SyntaxCSS, meta.SyntaxSxn, meta.SyntaxTxt, meta.SyntaxHTML, + meta.SyntaxText, meta.SyntaxPlain, + // Supported text graphics formats + meta.SyntaxSVG, + meta.SyntaxNone, + // Supported binary graphic formats + meta.SyntaxGif, meta.SyntaxPNG, meta.SyntaxJPEG, meta.SyntaxWebp, meta.SyntaxJPG, + // Unsupported syntax values "gz", "cpp", "tar", "cppc", } for oldI, oldExt := range extVals { for newI, newExt := range extVals { Index: box/notify/entry.go ================================================================== --- box/notify/entry.go +++ box/notify/entry.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -11,15 +11,15 @@ package notify import ( "path/filepath" - "zettelstore.de/c/api" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/parser" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) const ( extZettel = "zettel" // file contains metadata and content extBin = "bin" // file contains binary content @@ -46,11 +46,11 @@ func (e *DirEntry) HasMetaInContent() bool { return e.IsValid() && extIsMetaAndContent(e.ContentExt) } // SetupFromMetaContent fills entry data based on metadata and zettel content. -func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content domain.Content, getZettelFileSyntax func() []string) { +func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content zettel.Content, getZettelFileSyntax func() []string) { if e.Zid != m.Zid { panic("Zid differ") } if contentName := e.ContentName; contentName != "" { if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" { @@ -78,11 +78,11 @@ e.MetaName = e.calcBaseName(e.ContentName) } } } -func contentExtWithMeta(syntax string, content domain.Content) string { +func contentExtWithMeta(syntax string, content zettel.Content) string { p := parser.Get(syntax) if content.IsBinary() { if p.IsImageFormat { return syntax } @@ -97,11 +97,11 @@ func calcContentExt(syntax string, yamlSep bool, getZettelFileSyntax func() []string) string { if yamlSep { return extZettel } switch syntax { - case api.ValueSyntaxNone, api.ValueSyntaxZmk: + case meta.SyntaxNone, meta.SyntaxZmk: return extZettel } for _, s := range getZettelFileSyntax() { if s == syntax { return extZettel Index: box/notify/fsdir.go ================================================================== --- box/notify/fsdir.go +++ box/notify/fsdir.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -92,107 +92,141 @@ defer close(fsdn.events) defer close(fsdn.refresh) if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) { return } + for fsdn.readAndProcessEvent() { } } func (fsdn *fsdirNotifier) readAndProcessEvent() bool { select { case <-fsdn.done: + fsdn.traceDone(1) return false default: } select { case <-fsdn.done: + fsdn.traceDone(2) return false case <-fsdn.refresh: + fsdn.log.Trace().Msg("refresh") listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) case err, ok := <-fsdn.base.Errors: + fsdn.log.Trace().Err(err).Bool("ok", ok).Msg("got errors") if !ok { return false } select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: + fsdn.traceDone(3) return false } case ev, ok := <-fsdn.base.Events: + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Bool("ok", ok).Msg("file event") if !ok { return false } if !fsdn.processEvent(&ev) { return false } } return true } + +func (fsdn *fsdirNotifier) traceDone(pos int64) { + fsdn.log.Trace().Int("i", pos).Msg("done with read and process events") +} func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool { if strings.HasPrefix(ev.Name, fsdn.path) { if len(ev.Name) == len(fsdn.path) { return fsdn.processDirEvent(ev) } return fsdn.processFileEvent(ev) } + fsdn.log.Trace().Str("path", fsdn.path).Str("name", ev.Name).Str("op", ev.Op.String()).Msg("event does not match") return true } -const deleteFsOps = fsnotify.Remove | fsnotify.Rename -const updateFsOps = fsnotify.Create | fsnotify.Write - func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool { - if ev.Op&deleteFsOps != 0 { + if ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename) { fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory removed") fsdn.base.Remove(fsdn.path) select { case fsdn.events <- Event{Op: Destroy}: case <-fsdn.done: + fsdn.log.Trace().Int("i", 1).Msg("done dir event processing") return false } return true } - if ev.Op&fsnotify.Create != 0 { + + if ev.Has(fsnotify.Create) { err := fsdn.base.Add(fsdn.path) if err != nil { fsdn.log.IfErr(err).Str("name", fsdn.path).Msg("Unable to add directory") select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: + fsdn.log.Trace().Int("i", 2).Msg("done dir event processing") return false } } fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory added") return listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) } + + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("Directory processed") return true } func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool { - if ev.Op&deleteFsOps != 0 { - fsdn.log.Trace().Str("name", ev.Name).Uint("op", uint64(ev.Op)).Msg("File deleted") - select { - case fsdn.events <- Event{Op: Delete, Name: filepath.Base(ev.Name)}: - case <-fsdn.done: - return false - } - return true - } - if ev.Op&updateFsOps != 0 { - if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() { - return true - } - fsdn.log.Trace().Str("name", ev.Name).Uint("op", uint64(ev.Op)).Msg("File updated") - select { - case fsdn.events <- Event{Op: Update, Name: filepath.Base(ev.Name)}: - case <-fsdn.done: - return false - } + if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) { + if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() { + regular := err == nil && fi.Mode().IsRegular() + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Err(err).Bool("regular", regular).Msg("error with file") + return true + } + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated") + return fsdn.sendEvent(Update, filepath.Base(ev.Name)) + } + + if ev.Has(fsnotify.Rename) { + fi, err := os.Lstat(ev.Name) + if err != nil { + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted") + return fsdn.sendEvent(Delete, filepath.Base(ev.Name)) + } + if fi.Mode().IsRegular() { + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated") + return fsdn.sendEvent(Update, filepath.Base(ev.Name)) + } + fsdn.log.Trace().Str("name", ev.Name).Msg("File not regular") + return true + } + + if ev.Has(fsnotify.Remove) { + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted") + return fsdn.sendEvent(Delete, filepath.Base(ev.Name)) + } + + fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File processed") + return true +} + +func (fsdn *fsdirNotifier) sendEvent(op EventOp, filename string) bool { + select { + case fsdn.events <- Event{Op: op, Name: filename}: + case <-fsdn.done: + fsdn.log.Trace().Msg("done file event processing") + return false } return true } func (fsdn *fsdirNotifier) Close() { close(fsdn.done) } Index: box/notify/helper.go ================================================================== --- box/notify/helper.go +++ box/notify/helper.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- Index: box/notify/notify.go ================================================================== --- box/notify/notify.go +++ box/notify/notify.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -33,19 +33,19 @@ // Error signals a detected error. Details are in Event.Err. // // Make signals that the container is detected. List events will follow. // // List signals a found file, if Event.Name is not empty. Otherwise it signals -// the end of files within the container. +// the end of files within the container. // // Destroy signals that the container is not there any more. It might me Make later again. // -// Update signals that file Event.Name was created/updated. File name is relative -// to the container. +// Update signals that file Event.Name was created/updated. +// File name is relative to the container. // -// Delete signals that file Event.Name was removed. File name is relative to -// the container's name. +// Delete signals that file Event.Name was removed. +// File name is relative to the container's name. const ( _ EventOp = iota Error // Error while operating Make // Make container List // List container Index: box/notify/simpledir.go ================================================================== --- box/notify/simpledir.go +++ box/notify/simpledir.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -42,20 +42,20 @@ return sdn, nil } // NewSimpleZipNotifier creates a zip-file based notifier that will not receive // any notifications from the operating system. -func NewSimpleZipNotifier(log *logger.Logger, zipPath string) (Notifier, error) { +func NewSimpleZipNotifier(log *logger.Logger, zipPath string) Notifier { sdn := &simpleDirNotifier{ log: log, events: make(chan Event), done: make(chan struct{}), refresh: make(chan struct{}), fetcher: newZipPathFetcher(zipPath), } go sdn.eventLoop() - return sdn, nil + return sdn } func (sdn *simpleDirNotifier) Events() <-chan Event { return sdn.events } Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -9,22 +9,23 @@ //----------------------------------------------------------------------------- package cmd import ( + "context" "flag" "fmt" "io" "os" - "zettelstore.de/c/api" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) // ---------- Subcommand: file ----------------------------------------------- func cmdFile(fs *flag.FlagSet) (int, error) { @@ -32,15 +33,16 @@ m, inp, err := getInput(fs.Args()) if m == nil { return 2, err } z := parser.ParseZettel( - domain.Zettel{ + context.Background(), + zettel.Zettel{ Meta: m, - Content: domain.NewContent(inp.Src[inp.Pos:]), + Content: zettel.NewContent(inp.Src[inp.Pos:]), }, - m.GetDefault(api.KeySyntax, api.ValueSyntaxZmk), + m.GetDefault(api.KeySyntax, meta.SyntaxZmk), nil, ) encdr := encoder.Create(api.Encoder(enc)) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) Index: cmd/cmd_password.go ================================================================== --- cmd/cmd_password.go +++ cmd/cmd_password.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- @@ -15,13 +15,13 @@ "fmt" "os" "golang.org/x/term" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth/cred" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/zettel/id" ) // ---------- Subcommand: password ------------------------------------------- func cmdPassword(fs *flag.FlagSet) (int, error) { Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -9,20 +9,23 @@ //----------------------------------------------------------------------------- package cmd import ( + "context" "flag" + "net/http" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" + "zettelstore.de/z/zettel/meta" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { @@ -44,98 +47,88 @@ kernel.Main.WaitForShutdown() return exitCode, err } func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { - protectedBoxManager, authPolicy := authManager.BoxWithPolicy(webSrv, boxManager, rtConfig) + protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) kern := kernel.Main webLog := kern.GetLogger(kernel.WebService) - a := api.New( - webLog.Clone().Str("adapter", "api").Child(), - webSrv, authManager, authManager, webSrv, rtConfig, authPolicy) - wui := webui.New( - webLog.Clone().Str("adapter", "wui").Child(), - webSrv, authManager, rtConfig, authManager, boxManager, authPolicy) - - authLog := kern.GetLogger(kernel.AuthService) - ucLog := kern.GetLogger(kernel.CoreService).WithUser(webSrv) - ucAuthenticate := usecase.NewAuthenticate(authLog, authManager, authManager, boxManager) - ucIsAuth := usecase.NewIsAuthenticated(ucLog, webSrv, authManager) - ucCreateZettel := usecase.NewCreateZettel(ucLog, rtConfig, protectedBoxManager) - ucGetMeta := usecase.NewGetMeta(protectedBoxManager) - ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) + + var getUser getUserImpl + logAuth := kern.GetLogger(kernel.AuthService) + logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) + ucGetUser := usecase.NewGetUser(authManager, boxManager) + ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, &ucGetUser) + ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager) + ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager) + ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) - ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta) - ucListMeta := usecase.NewListMeta(protectedBoxManager) + ucQuery := usecase.NewQuery(protectedBoxManager) + ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery) + ucQuery.SetEvaluate(&ucEvaluate) ucListSyntax := usecase.NewListSyntax(protectedBoxManager) ucListRoles := usecase.NewListRoles(protectedBoxManager) - ucListTags := usecase.NewListTags(protectedBoxManager) - ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig) - ucDelete := usecase.NewDeleteZettel(ucLog, protectedBoxManager) - ucUpdate := usecase.NewUpdateZettel(ucLog, protectedBoxManager) - ucRename := usecase.NewRenameZettel(ucLog, protectedBoxManager) - ucUnlinkedRefs := usecase.NewUnlinkedReferences(protectedBoxManager, rtConfig) - ucRefresh := usecase.NewRefresh(ucLog, protectedBoxManager) + ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager) + ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager) + ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager) + ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager) ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) + a := api.New( + webLog.Clone().Str("adapter", "api").Child(), + webSrv, authManager, authManager, rtConfig, authPolicy) + wui := webui.New( + webLog.Clone().Str("adapter", "wui").Child(), + webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate) + webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) + if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" { + const assetPrefix = "/assets/" + webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir)))) + webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir)) + } // Web user interface if !authManager.IsReadonly() { - webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler( - ucGetMeta, &ucEvaluate)) + webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename)) + webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(&ucQuery, &ucEvaluate, ucListRoles, ucListSyntax)) + webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler( ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) - webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler( - ucGetMeta, ucGetAllMeta, &ucEvaluate)) + webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel, ucGetAllZettel)) webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate)) } webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh)) - webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler( - ucListMeta, ucListRoles, ucListTags, &ucEvaluate)) - webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler( - &ucEvaluate, ucGetMeta)) + webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery)) + webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel)) webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler()) webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler( - ucParseZettel, &ucEvaluate, ucGetMeta, ucGetAllMeta, ucUnlinkedRefs)) - webSrv.AddZettelRoute('k', server.MethodGet, wui.MakeZettelContextHandler( - ucZettelContext, &ucEvaluate)) + ucParseZettel, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery)) // API webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler()) - webSrv.AddListRoute('j', server.MethodGet, a.MakeListMetaHandler(ucListMeta)) - webSrv.AddZettelRoute('j', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel)) - webSrv.AddListRoute('m', server.MethodGet, a.MakeListMapMetaHandler(ucListRoles, ucListTags)) - webSrv.AddZettelRoute('m', server.MethodGet, a.MakeGetMetaHandler(ucGetMeta)) - webSrv.AddZettelRoute('o', server.MethodGet, a.MakeGetOrderHandler( - usecase.NewZettelOrder(protectedBoxManager, ucEvaluate))) - webSrv.AddZettelRoute('p', server.MethodGet, a.MakeGetParsedZettelHandler(ucParseZettel)) - webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler( - ucGetMeta, ucUnlinkedRefs, &ucEvaluate)) - webSrv.AddZettelRoute('v', server.MethodGet, a.MakeGetEvalZettelHandler(ucEvaluate)) webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) - webSrv.AddZettelRoute('x', server.MethodGet, a.MakeZettelContextHandler(ucZettelContext)) - webSrv.AddListRoute('z', server.MethodGet, a.MakeListPlainHandler(ucListMeta)) - webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetPlainZettelHandler(ucGetZettel)) + webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery)) + webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate)) if !authManager.IsReadonly() { - webSrv.AddListRoute('j', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) - webSrv.AddZettelRoute('j', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) - webSrv.AddZettelRoute('j', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) - webSrv.AddZettelRoute('j', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) - webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreatePlainZettelHandler(&ucCreateZettel)) - webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdatePlainZettelHandler(&ucUpdate)) + webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) + webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) } if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } } + +type getUserImpl struct{} + +func (*getUserImpl) GetUser(ctx context.Context) *meta.Meta { return server.GetUser(ctx) } Index: cmd/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -11,11 +11,11 @@ package cmd import ( "flag" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/maps" "zettelstore.de/z/logger" ) // Command stores information about commands / sub-commands. type Command struct { @@ -46,11 +46,11 @@ } if _, ok := commands[cmd.Name]; ok { panic("Command already registered: " + cmd.Name) } cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError) - cmd.flags.String("l", logger.InfoLevel.String(), "global log level") + cmd.flags.String("l", logger.InfoLevel.String(), "log level specification") if cmd.SetFlags != nil { cmd.SetFlags(cmd.flags) } commands[cmd.Name] = cmd DELETED cmd/fd_limit.go Index: cmd/fd_limit.go ================================================================== --- cmd/fd_limit.go +++ cmd/fd_limit.go @@ -1,16 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -//go:build !darwin -// +build !darwin - -package cmd - -func raiseFdLimit() error { return nil } DELETED cmd/fd_limit_raise.go Index: cmd/fd_limit_raise.go ================================================================== --- cmd/fd_limit_raise.go +++ cmd/fd_limit_raise.go @@ -1,51 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021 Detlef Stern -// -// This file is part of zettelstore. -// -// Zettelstore is licensed under the latest version of the EUPL (European Union -// Public License). Please see file LICENSE.txt for your rights and obligations -// under this license. -//----------------------------------------------------------------------------- - -//go:build darwin -// +build darwin - -package cmd - -import ( - "fmt" - "syscall" - - "zettelstore.de/z/kernel" -) - -const minFiles = 1048576 - -func raiseFdLimit() error { - var rLimit syscall.Rlimit - err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) - if err != nil { - return err - } - if rLimit.Cur >= minFiles { - return nil - } - rLimit.Cur = minFiles - if rLimit.Cur > rLimit.Max { - rLimit.Cur = rLimit.Max - } - err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) - if err != nil { - return err - } - err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) - if err != nil { - return err - } - if rLimit.Cur < minFiles { - msg := fmt.Sprintf("Make sure you have no more than %d files in all your boxes if you enabled notification\n", rLimit.Cur) - kernel.Main.GetKernelLogger().Mandatory().Msg(msg) - } - return nil -} Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -9,33 +9,34 @@ //----------------------------------------------------------------------------- package cmd import ( - "errors" + "crypto/sha256" "flag" "fmt" "net" "net/url" "os" "runtime/debug" "strconv" "strings" + "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/box" "zettelstore.de/z/box/compbox" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) const strRunSimple = "run-simple" func init() { @@ -85,19 +86,19 @@ Name: "password", Func: cmdPassword, }) } -func fetchStartupConfiguration(fs *flag.FlagSet) (cfg *meta.Meta) { +func fetchStartupConfiguration(fs *flag.FlagSet) (string, *meta.Meta) { if configFlag := fs.Lookup("c"); configFlag != nil { if filename := configFlag.Value.String(); filename != "" { content, err := readConfiguration(filename) - return createConfiguration(content, err) + return filename, createConfiguration(content, err) } } - content, err := searchAndReadConfiguration() - return createConfiguration(content, err) + filename, content, err := searchAndReadConfiguration() + return filename, createConfiguration(content, err) } func createConfiguration(content []byte, err error) *meta.Meta { if err != nil { return meta.New(id.Invalid) @@ -105,31 +106,27 @@ return meta.NewFromInput(id.Invalid, input.NewInput(content)) } func readConfiguration(filename string) ([]byte, error) { return os.ReadFile(filename) } -func searchAndReadConfiguration() ([]byte, error) { - for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg"} { +func searchAndReadConfiguration() (string, []byte, error) { + for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg", ".zscfg"} { if content, err := readConfiguration(filename); err == nil { - return content, nil + return filename, content, nil } } - return readConfiguration(".zscfg") + return "", nil, os.ErrNotExist } -func getConfig(fs *flag.FlagSet) *meta.Meta { - cfg := fetchStartupConfiguration(fs) +func getConfig(fs *flag.FlagSet) (string, *meta.Meta) { + filename, cfg := fetchStartupConfiguration(fs) fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": - if portStr, err := parsePort(flg.Value.String()); err == nil { - cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", portStr)) - } + cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String())) case "a": - if portStr, err := parsePort(flg.Value.String()); err == nil { - cfg.Set(keyAdminPort, portStr) - } + cfg.Set(keyAdminPort, flg.Value.String()) case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { @@ -145,20 +142,11 @@ cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) - return cfg -} - -func parsePort(s string) (string, error) { - port, err := net.LookupPort("tcp", s) - if err != nil { - fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s) - return "", err - } - return strconv.Itoa(port), nil + return filename, cfg } func deleteConfiguredBoxes(cfg *meta.Meta) { for _, p := range cfg.PairsRest() { if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) { @@ -167,13 +155,16 @@ } } const ( keyAdminPort = "admin-port" + keyAssetDir = "asset-dir" + keyBaseURL = "base-url" keyDebug = "debug-mode" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" + keyInsecureHTML = "insecure-html" keyListenAddr = "listen-addr" keyLogLevel = "log-level" keyMaxRequestSize = "max-request-size" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" @@ -183,69 +174,72 @@ keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" keyVerbose = "verbose-mode" ) -func setServiceConfig(cfg *meta.Meta) error { +func setServiceConfig(cfg *meta.Meta) bool { debugMode := cfg.GetBool(keyDebug) if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel { - kernel.Main.SetGlobalLogLevel(logger.DebugLevel) - } - if strLevel, found := cfg.Get(keyLogLevel); found { - if level := logger.ParseLevel(strLevel); level.IsValid() { - kernel.Main.SetGlobalLogLevel(level) - } - } - ok := setConfigValue(true, kernel.CoreService, kernel.CoreDebug, debugMode) - ok = setConfigValue(ok, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) + kernel.Main.SetLogLevel(logger.DebugLevel.String()) + } + if logLevel, found := cfg.Get(keyLogLevel); found { + kernel.Main.SetLogLevel(logLevel) + } + err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode) + err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) if val, found := cfg.Get(keyAdminPort); found { - ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val) - } - - ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, "")) - ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly)) - - ok = setConfigValue( - ok, kernel.BoxService, kernel.BoxDefaultDirType, + err = setConfigValue(err, kernel.CoreService, kernel.CorePort, val) + } + + err = setConfigValue(err, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, "")) + err = setConfigValue(err, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly)) + + err = setConfigValue( + err, kernel.BoxService, kernel.BoxDefaultDirType, cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify)) - ok = setConfigValue(ok, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel") - format := kernel.BoxURIs + "%v" + err = setConfigValue(err, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel") for i := 1; ; i++ { - key := fmt.Sprintf(format, i) + key := kernel.BoxURIs + strconv.Itoa(i) val, found := cfg.Get(key) if !found { break } - ok = setConfigValue(ok, kernel.BoxService, key, val) - } - - ok = setConfigValue( - ok, kernel.WebService, kernel.WebListenAddress, - cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) - ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/")) - ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) - ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) + err = setConfigValue(err, kernel.BoxService, key, val) + } + + err = setConfigValue(err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML)) + + err = setConfigValue(err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) + if val, found := cfg.Get(keyBaseURL); found { + err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val) + } + if val, found := cfg.Get(keyURLPrefix); found { + err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val) + } + err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) + err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) if val, found := cfg.Get(keyMaxRequestSize); found { - ok = setConfigValue(ok, kernel.WebService, kernel.WebMaxRequestSize, val) - } - ok = setConfigValue( - ok, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) - ok = setConfigValue( - ok, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) - - if !ok { - return errors.New("unable to set configuration") - } - return nil -} - -func setConfigValue(ok bool, subsys kernel.Service, key string, val interface{}) bool { - done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val)) - if !done { - kernel.Main.GetKernelLogger().Error().Str(key, fmt.Sprint(val)).Msg("Unable to set configuration") - } - return ok && done + err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val) + } + err = setConfigValue( + err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) + err = setConfigValue( + err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) + if val, found := cfg.Get(keyAssetDir); found { + err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val) + } + return err == nil +} + +func setConfigValue(err error, subsys kernel.Service, key string, val any) error { + if err == nil { + err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val)) + if err != nil { + kernel.Main.GetKernelLogger().Fatal().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration") + } + } + return err } func executeCommand(name string, args ...string) int { command, ok := Get(name) if !ok { @@ -255,26 +249,19 @@ fs := command.GetFlags() if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "%s: unable to parse flags: %v %v\n", name, args, err) return 1 } - cfg := getConfig(fs) - if err := setServiceConfig(cfg); err != nil { - fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) + filename, cfg := getConfig(fs) + if !setServiceConfig(cfg) { + fs.Usage() return 2 } kern := kernel.Main var createManager kernel.CreateBoxManagerFunc if command.Boxes { - err := raiseFdLimit() - if err != nil { - logger := kern.GetKernelLogger() - logger.IfErr(err).Msg("Raising some limitions did not work") - logger.Error().Msg("Prepare to encounter errors. Most of them can be mitigated. See the manual for details") - kern.SetConfig(kernel.BoxService, kernel.BoxDefaultDirType, kernel.BoxDirTypeSimple) - } createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) { compbox.Setup(cfg) return manager.New(boxURIs, authManager, rtConfig) } } else { @@ -284,10 +271,12 @@ secret := cfg.GetDefault("secret", "") if len(secret) < 16 && cfg.GetDefault(keyOwner, "") != "" { fmt.Fprintf(os.Stderr, "secret must have at least length 16 when authentication is enabled, but is %q\n", secret) return 2 } + cfg.Delete("secret") + secret = fmt.Sprintf("%x", sha256.Sum256([]byte(secret))) kern.SetCreators( func(readonly bool, owner id.Zid) (auth.Manager, error) { return impl.New(readonly, owner, secret), nil }, @@ -299,23 +288,23 @@ ) if command.Simple { kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true") } - kern.Start(command.Header, command.LineServer) + kern.Start(command.Header, command.LineServer, filename) exitCode, err := command.Func(fs) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kern.Shutdown(true) return exitCode } // runSimple is called, when the user just starts the software via a double click -// or via a simple call ``./zettelstore`` on the command line. +// or via a simple call “./zettelstore“ on the command line. func runSimple() int { - if _, err := searchAndReadConfiguration(); err == nil { + if _, _, err := searchAndReadConfiguration(); err == nil { return executeCommand(strRunSimple) } dir := "./zettel" if err := os.MkdirAll(dir, 0750); err != nil { fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err) @@ -327,13 +316,16 @@ var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") var memprofile = flag.String("memprofile", "", "write memory profile to `file`") // Main is the real entrypoint of the zettelstore. func Main(progName, buildVersion string) int { - fullVersion := retrieveFullVersion(buildVersion) - kernel.Main.SetConfig(kernel.CoreService, kernel.CoreProgname, progName) - kernel.Main.SetConfig(kernel.CoreService, kernel.CoreVersion, fullVersion) + info := retrieveVCSInfo(buildVersion) + fullVersion := info.revision + if info.dirty { + fullVersion += "-dirty" + } + kernel.Main.Setup(progName, fullVersion, info.time) flag.Parse() if *cpuprofile != "" || *memprofile != "" { if *cpuprofile != "" { kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile) } else { @@ -346,26 +338,38 @@ return runSimple() } return executeCommand(args[0], args[1:]...) } -func retrieveFullVersion(version string) string { +type vcsInfo struct { + revision string + dirty bool + time time.Time +} + +func retrieveVCSInfo(version string) vcsInfo { + buildTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) info, ok := debug.ReadBuildInfo() if !ok { - return version + return vcsInfo{revision: version, dirty: false, time: buildTime} } - var revision, dirty string + result := vcsInfo{time: buildTime} for _, kv := range info.Settings { switch kv.Key { case "vcs.revision": - revision = "+" + kv.Value + revision := "+" + kv.Value if len(revision) > 11 { revision = revision[:11] } + result.revision = version + revision case "vcs.modified": if kv.Value == "true" { - dirty = "-dirty" + result.dirty = true + } + case "vcs.time": + if t, err := time.Parse(time.RFC3339, kv.Value); err == nil { + result.time = t } } } - return version + revision + dirty + return result } Index: cmd/register.go ================================================================== --- cmd/register.go +++ cmd/register.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -17,16 +17,18 @@ _ "zettelstore.de/z/box/constbox" // Allow to use global internal box. _ "zettelstore.de/z/box/dirbox" // Allow to use directory box. _ "zettelstore.de/z/box/filebox" // Allow to use file box. _ "zettelstore.de/z/box/membox" // Allow to use in-memory box. _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. - _ "zettelstore.de/z/encoder/sexprenc" // Allow to use sexpr encoder. + _ "zettelstore.de/z/encoder/mdenc" // Allow to use markdown encoder. + _ "zettelstore.de/z/encoder/shtmlenc" // Allow to use SHTML encoder. + _ "zettelstore.de/z/encoder/szenc" // Allow to use Sz encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. - _ "zettelstore.de/z/encoder/zjsonenc" // Allow to use ZJSON encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. + _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) Index: cmd/zettelstore/main.go ================================================================== --- cmd/zettelstore/main.go +++ cmd/zettelstore/main.go @@ -1,9 +1,9 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2021 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // -// This file is part of zettelstore. +// This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- Index: collect/collect.go ================================================================== --- collect/collect.go +++ collect/collect.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations Index: collect/collect_test.go ================================================================== --- collect/collect_test.go +++ collect/collect_test.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations Index: collect/order.go ================================================================== --- collect/order.go +++ collect/order.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations Index: collect/split.go ================================================================== --- collect/split.go +++ collect/split.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations @@ -10,46 +10,47 @@ // Package config provides functions to retrieve runtime configuration data. package config import ( - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "context" + + "zettelstore.de/z/zettel/meta" +) + +// Key values that are supported by Config.Get +const ( + KeyFooterZettel = "footer-zettel" + KeyHomeZettel = "home-zettel" + // api.KeyLang ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig + + // Get returns the value of the given key. It searches first in the given metadata, + // then in the data of the current user, and at last in the system-wide data. + Get(ctx context.Context, m *meta.Meta, key string) string // AddDefaultValues enriches the given meta data with its default values. - AddDefaultValues(m *meta.Meta) *meta.Meta - - // GetDefaultLang returns the current value of the "default-lang" key. - GetDefaultLang() string + AddDefaultValues(context.Context, *meta.Meta) *meta.Meta // GetSiteName returns the current value of the "site-name" key. GetSiteName() string - // GetHomeZettel returns the value of the "home-zettel" key. - GetHomeZettel() id.Zid + // GetHTMLInsecurity returns the current + GetHTMLInsecurity() HTMLInsecurity - // GetMaxTransclusions return the maximum number of indirect transclusions. + // GetMaxTransclusions returns the maximum number of indirect transclusions. GetMaxTransclusions() int // GetYAMLHeader returns the current value of the "yaml-header" key. GetYAMLHeader() bool // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. GetZettelFileSyntax() []string - - // GetMarkerExternal returns the current value of the "marker-external" key. - GetMarkerExternal() string - - // GetFooterHTML returns HTML code that should be embedded into the footer - // of each WebUI page. - GetFooterHTML() string } // AuthConfig are relevant configuration values for authentication. type AuthConfig interface { // GetSimpleMode returns true if system tuns in simple-mode. @@ -60,13 +61,42 @@ // GetVisibility returns the visibility value of the metadata. GetVisibility(m *meta.Meta) meta.Visibility } -// GetLang returns the value of the "lang" key of the given meta. If there is -// no such value, GetDefaultLang is returned. -func GetLang(m *meta.Meta, cfg Config) string { - if val, ok := m.Get(api.KeyLang); ok { - return val +// HTMLInsecurity states what kind of insecure HTML is allowed. +// The lowest value is the most secure one (disallowing any HTML) +type HTMLInsecurity uint8 + +// Constant values for HTMLInsecurity: +const ( + NoHTML HTMLInsecurity = iota + SyntaxHTML + MarkdownHTML + ZettelmarkupHTML +) + +func (hi HTMLInsecurity) String() string { + switch hi { + case SyntaxHTML: + return "html" + case MarkdownHTML: + return "markdown" + case ZettelmarkupHTML: + return "zettelmarkup" + } + return "secure" +} + +// AllowHTML returns true, if the given HTML insecurity level matches the given syntax value. +func (hi HTMLInsecurity) AllowHTML(syntax string) bool { + switch hi { + case SyntaxHTML: + return syntax == meta.SyntaxHTML + case MarkdownHTML: + return syntax == meta.SyntaxHTML || syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD + case ZettelmarkupHTML: + return syntax == meta.SyntaxZmk || syntax == meta.SyntaxHTML || + syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD } - return cfg.GetDefaultLang() + return false } Index: docs/development/00010000000000.zettel ================================================================== --- docs/development/00010000000000.zettel +++ docs/development/00010000000000.zettel @@ -1,8 +1,10 @@ id: 00010000000000 title: Developments Notes role: zettel syntax: zmk -modified: 20210916194954 +created: 00010101000000 +modified: 20221026184905 * [[Required Software|20210916193200]] +* [[Fuzzing tests|20221026184300]] * [[Checklist for Release|20210916194900]] Index: docs/development/20210916193200.zettel ================================================================== --- docs/development/20210916193200.zettel +++ docs/development/20210916193200.zettel @@ -1,19 +1,28 @@ id: 20210916193200 title: Required Software role: zettel syntax: zmk -modified: 20211213190428 +created: 20210916193200 +modified: 20230405150541 The following software must be installed: -* A current, supported [[release of Go|https://golang.org/doc/devel/release.html]], -* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``, -* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``, -* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest`` +* A current, supported [[release of Go|https://go.dev/doc/devel/release]], +* [[Fossil|https://fossil-scm.org/]], +* [[Git|https://git-scm.org/]] (most dependencies are accessible via Git only). Make sure that the software is in your path, e.g. via: - ```sh export PATH=$PATH:/usr/local/go/bin export PATH=$PATH:$(go env GOPATH)/bin ``` + +The internal build tool need the following software. +It can be installed / updated via the build tool itself: ``go run tools/build.go tools``. + +Otherwise you can install the software by hand: + +* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``, +* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``, +* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``, +* [[govulncheck|https://golang.org/x/vuln/cmd/govulncheck]] via ``go install golang.org/x/vuln/cmd/govulncheck@latest``, Index: docs/development/20210916194900.zettel ================================================================== --- docs/development/20210916194900.zettel +++ docs/development/20210916194900.zettel @@ -1,13 +1,16 @@ id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk -modified: 20220309105459 +created: 20210916194900 +modified: 20230402181229 # Sync with the official repository #* ``fossil sync -u`` +# Make sure that there is no workspace defined. +#* ``ls ..`` must not have a file ''go.work'', in no parent folder. # Make sure that all dependencies are up-to-date. #* ``cat go.mod`` # Clean up your Go workspace: #* ``go run tools/build.go clean`` (alternatively: ``make clean``). # All internal tests must succeed: @@ -30,11 +33,12 @@ # Update files in directory ''www'' #* index.wiki #* download.wiki #* changes.wiki #* plan.wiki -# Set file ''VERSION'' to the new release version +# Set file ''VERSION'' to the new release version. + It _must_ consist of three digits: MAJOR.MINOR.PATCH, even if PATCH is zero # Disable Fossil autosync mode: #* ``fossil setting autosync off`` # Commit the new release version: #* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"`` #* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''. @@ -44,12 +48,12 @@ # Create the release: #* ``go run tools/build.go release`` (alternatively: ``make release``). # Remove previous executables: #* ``fossil uv remove --glob '*-PREVVERSION*'`` # Add executables for release: -#* ``cd release`` +#* ``cd releases`` #* ``fossil uv add *.zip`` #* ``cd ..`` #* Synchronize with main repository: #* ``fossil sync -u`` # Enable autosync: #* ``fossil setting autosync on`` ADDED docs/development/20221026184300.zettel Index: docs/development/20221026184300.zettel ================================================================== --- docs/development/20221026184300.zettel +++ docs/development/20221026184300.zettel @@ -0,0 +1,14 @@ +id: 20221026184300 +title: Fuzzing Tests +role: zettel +syntax: zmk +created: 20221026184320 +modified: 20221102140156 + +The source code contains some simple [[fuzzing tests|https://go.dev/security/fuzz/]]. +You should call them regularly to make sure that the software will cope with unusual input. + +```sh +go test -fuzz=FuzzParseBlocks zettelstore.de/z/parser/draw +go test -fuzz=FuzzParseBlocks zettelstore.de/z/parser/zettelmark +``` Index: docs/manual/00000000000100.zettel ================================================================== --- docs/manual/00000000000100.zettel +++ docs/manual/00000000000100.zettel @@ -1,13 +1,14 @@ id: 00000000000100 title: Zettelstore Runtime Configuration role: configuration syntax: none -default-copyright: (c) 2020-2022 by Detlef Stern +created: 00010101000000 +default-copyright: (c) 2020-present by Detlef Stern default-license: EUPL-1.2-or-later default-visibility: public -footer-html:

Imprint / Privacy

+footer-zettel: 00001000000100 home-zettel: 00001000000000 -modified: 20220215171041 +modified: 20221205173642 site-name: Zettelstore Manual visibility: owner ADDED docs/manual/00000000025001 Index: docs/manual/00000000025001 ================================================================== --- docs/manual/00000000025001 +++ docs/manual/00000000025001 @@ -0,0 +1,7 @@ +id: 00000000025001 +title: Zettelstore User CSS +role: configuration +syntax: css +created: 20210622110143 +modified: 20220926183101 +visibility: public ADDED docs/manual/00000000025001.css Index: docs/manual/00000000025001.css ================================================================== --- docs/manual/00000000025001.css +++ docs/manual/00000000025001.css @@ -0,0 +1,2 @@ +/* User-defined CSS */ +.example { border-style: dotted !important } Index: docs/manual/00001000000000.zettel ================================================================== --- docs/manual/00001000000000.zettel +++ docs/manual/00001000000000.zettel @@ -1,11 +1,11 @@ id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk -modified: 20211027121716 +modified: 20220803183647 * [[Introduction|00001001000000]] * [[Design goals|00001002000000]] * [[Installation|00001003000000]] * [[Configuration|00001004000000]] @@ -14,9 +14,10 @@ * [[Zettelmarkup|00001007000000]] * [[Other markup languages|00001008000000]] * [[Security|00001010000000]] * [[API|00001012000000]] * [[Web user interface|00001014000000]] +* [[Tips and Tricks|00001017000000]] * [[Troubleshooting|00001018000000]] * Frequently asked questions Licensed under the EUPL-1.2-or-later. ADDED docs/manual/00001000000100.zettel Index: docs/manual/00001000000100.zettel ================================================================== --- docs/manual/00001000000100.zettel +++ docs/manual/00001000000100.zettel @@ -0,0 +1,8 @@ +id: 00001000000100 +title: Footer Zettel +role: configuration +syntax: zmk +created: 20221205173520 +modified: 20221207175927 + +[[Imprint / Privacy|/home/doc/trunk/www/impri.wiki]] Index: docs/manual/00001002000000.zettel ================================================================== --- docs/manual/00001002000000.zettel +++ docs/manual/00001002000000.zettel @@ -1,21 +1,26 @@ id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk -modified: 20211124131628 +created: 20210126175322 +modified: 20230624171152 Zettelstore supports the following design goals: ; Longevity of stored notes / zettel : Every zettel you create should be readable without the help of any tool, even without Zettelstore. : It should be not hard to write other software that works with your zettel. +: Normal zettel should be stored in a single file. + If this is not possible: at most in two files: one for the metadata, one for the content. + The only exception are [[predefined zettel|00001005090000]] stored in the Zettelstore executable. +: There is no additional database. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. - If your device is securely configured, there should be no risk that others are able to read or update your zettel. + If the computer running Zettelstore is securely configured, there should be no risk that others are able to read or update your zettel. : If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel. ; Ease of installation : If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working. : Upgrading the software is done just by replacing the executable with a newer one. ; Ease of operation @@ -28,5 +33,11 @@ : Zettelstore provides a default [[web-based user interface|00001014000000]]. Anybody can provide alternative user interfaces, e.g. for special purposes. ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. +; Security by default +: Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks. +: If you know what use are doing, Zettelstore allows you to relax some security-related preferences. + However, even in this case, the more secure way is chosen. +: The Zettelstore software uses a minimal design and uses other software dependencies only is essential needed. +: There will be no plugin mechanism, which allows external software to control the inner workings of the Zettelstore software. Index: docs/manual/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -1,11 +1,12 @@ id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk -modified: 20220419193611 +created: 20210126175322 +modified: 20221128155143 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored. An attacker that is able to change the owner can do anything. @@ -22,10 +23,28 @@ The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]]. 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). Default: ""0"" +; [!asset-dir|''asset-dir''] +: Allows to specify a directory whose files are allowed be transferred directly with the help of the web server. + The URL prefix for these files is ''/assets/''. + You can use this if you want to transfer files that are too large for a note to users. + Examples would be presentation files, PDF files, music files or video files. + + Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the case that the directory is one of the configured [[boxes|#box-uri-x]].] + + If you specify only the URL prefix, then the contents of the directory are listed to the user. + To avoid this, create an empty file in the directory named ""index.html"". + + Default: """", no asset directory is set, the URL prefix ''/assets/'' is invalid. +; [!base-url|''base-url''] +: Sets the absolute base URL for the service. + + Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start. + + Default: ""http://127.0.0.1:23123/"". ; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one : Specifies a [[box|00001004011200]] where zettel are stored. During startup __X__ is counted up, starting with one, until no key is found. This allows to configure more than one box. @@ -47,20 +66,35 @@ ; [!insecure-cookie|''insecure-cookie''] : Must be set to [[true|00001006030500]], if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). Otherwise web browser are free to ignore the authentication cookie. Default: ""false"" +; [!insecure-html|''insecure-html''] +: Allows to use HTML, e.g. within supported markup languages, even if this might introduce security-related problems. + However, HTML containing the ``