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,3 +1,2 @@ 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,31 +1,31 @@ -## 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. -.PHONY: check relcheck api build release clean +.PHONY: check relcheck api version build release clean check: - go run tools/build.go check + go run tools/check/check.go relcheck: - go run tools/build.go relcheck + go run tools/check/check.go -r api: - go run tools/build.go testapi + go run tools/testapi/testapi.go version: - @echo $(shell go run tools/build.go version) + @echo $(shell go run tools/build/build.go version) build: - go run tools/build.go build + go run tools/build/build.go build release: - go run tools/build.go release + go run tools/build/build.go release clean: - go run tools/build.go clean + go run tools/clean/clean.go Index: README.md ================================================================== --- README.md +++ README.md @@ -11,16 +11,16 @@ To get an initial impression, take a look at the [manual](https://zettelstore.de/manual/). It is a live example of the zettelstore software, running in read-only mode. -[Zettelstore Client](https://zettelstore.de/client) provides client -software to access Zettelstore via its API more easily, [Zettelstore +[Zettelstore Client](https://t73f.de/r/zsc) provides client software to access +Zettelstore via its API more easily, [Zettelstore Contrib](https://zettelstore.de/contrib) contains contributed software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. 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.7.0 +0.18.0-dev Index: ast/ast.go ================================================================== --- ast/ast.go +++ ast/ast.go @@ -1,31 +1,34 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree for parsed zettel content. 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 } Index: ast/block.go ================================================================== --- ast/block.go +++ ast/block.go @@ -1,18 +1,21 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast -import "zettelstore.de/c/attrs" +import "t73f.de/r/zsc/attrs" // Definition of Block nodes. // BlockSlice is a slice of BlockNodes. type BlockSlice []BlockNode @@ -88,15 +91,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 @@ -284,14 +282,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,21 +1,24 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast import ( "unicode/utf8" - "zettelstore.de/c/attrs" + "t73f.de/r/zsc/attrs" ) // Definitions of inline nodes. // InlineSlice is a list of BlockNodes. @@ -53,22 +56,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 @@ -205,18 +196,19 @@ type FormatKind int // Constants for FormatCode const ( _ FormatKind = iota - FormatEmph // Emphasized text. - FormatStrong // Strongly emphasized text. - FormatInsert // Inserted text. - FormatDelete // Deleted text. - FormatSuper // Superscripted text. - FormatSub // SubscriptedText. - FormatQuote // Quoted text. - FormatSpan // Generic inline container. + FormatEmph // Emphasized text + FormatStrong // Strongly emphasized text + FormatInsert // Inserted text + FormatDelete // Deleted text + FormatSuper // Superscripted text + FormatSub // SubscriptedText + FormatQuote // Quoted text + FormatMark // Marked text + FormatSpan // Generic inline container ) func (*FormatNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the formatted text. Index: ast/ref.go ================================================================== --- ast/ref.go +++ ast/ref.go @@ -1,30 +1,34 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast import ( "net/url" "strings" - "zettelstore.de/z/domain/id" + "t73f.de/r/zsc/api" + "zettelstore.de/z/zettel/id" ) // QueryPrefix is the prefix that denotes a query expression. -const QueryPrefix = "query:" +const QueryPrefix = api.QueryPrefix // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { - if s == "" || s == "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} } @@ -39,20 +43,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 Index: ast/ref_test.go ================================================================== --- ast/ref_test.go +++ ast/ref_test.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast_test import ( Index: ast/walk.go ================================================================== --- ast/walk.go +++ ast/walk.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package ast // Visitor is a visitor for walking the AST. Index: ast/walk_test.go ================================================================== --- ast/walk_test.go +++ ast/walk_test.go @@ -1,21 +1,24 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package ast_test import ( "testing" - "zettelstore.de/c/attrs" + "t73f.de/r/zsc/attrs" "zettelstore.de/z/ast" ) func BenchmarkWalk(b *testing.B) { root := ast.BlockSlice{ @@ -59,13 +62,13 @@ }, ), } v := benchVisitor{} b.ResetTimer() - for n := 0; n < b.N; n++ { + for range b.N { ast.Walk(&v, &root) } } type benchVisitor struct{} func (bv *benchVisitor) Visit(ast.Node) ast.Visitor { return bv } Index: auth/auth.go ================================================================== --- auth/auth.go +++ auth/auth.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package auth provides services for authentification / authorization. package auth @@ -14,12 +17,12 @@ import ( "time" "zettelstore.de/z/box" "zettelstore.de/z/config" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "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. @@ -40,12 +43,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 Index: auth/cred/cred.go ================================================================== --- auth/cred/cred.go +++ auth/cred/cred.go @@ -1,23 +1,26 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cred provides some function for handling credentials. package cred 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 +45,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,89 @@ +//----------------------------------------------------------------------------- +// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2023-present Detlef Stern +//----------------------------------------------------------------------------- + +package impl + +import ( + "bytes" + "crypto" + "crypto/hmac" + "encoding/base64" + + "t73f.de/r/sx" + "t73f.de/r/sx/sxreader" +) + +var encoding = base64.RawURLEncoding + +const digestAlg = crypto.SHA384 + +func sign(claim sx.Object, secret []byte) ([]byte, error) { + var buf bytes.Buffer + _, err := sx.Print(&buf, claim) + if err != nil { + return nil, err + } + 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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package impl provides services for authentification / authorization. package impl @@ -15,20 +18,20 @@ "errors" "hash/fnv" "io" "time" - "github.com/pascaldekloe/jwt" - - "zettelstore.de/c/api" + "t73f.de/r/sx" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/sexp" "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/zettel/id" + "zettelstore.de/z/zettel/meta" ) type myAuth struct { readonly bool owner id.Zid @@ -65,11 +68,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. @@ -84,68 +88,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.MakeString(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).GetValue() 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 = string(ident) + 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 { Index: auth/policy/anon.go ================================================================== --- auth/policy/anon.go +++ auth/policy/anon.go @@ -1,21 +1,24 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- 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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( @@ -14,23 +17,19 @@ "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/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( - 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(box, pol), pol } // polBox implements a policy box. @@ -53,33 +52,37 @@ 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) { +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 := server.GetUser(ctx) - if pp.policy.CanRead(user, zettel.Meta) { - return zettel, nil + 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 @@ -89,41 +92,33 @@ 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", server.GetUser(ctx), id.Invalid) -} - -func (pp *polBox) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { +func (pp *polBox) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { user := server.GetUser(ctx) canRead := pp.policy.CanRead q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) - return pp.box.SelectMeta(ctx, q) + 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 := 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) } @@ -130,16 +125,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 := server.GetUser(ctx) - if pp.policy.CanRename(user, meta) { + if pp.policy.CanRename(user, z.Meta) { return pp.box.RenameZettel(ctx, curZid, newZid) } return box.NewErrNotAllowed("Rename", user, curZid) } @@ -146,16 +141,16 @@ 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 := server.GetUser(ctx) - if pp.policy.CanDelete(user, meta) { + if pp.policy.CanDelete(user, z.Meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } @@ -163,6 +158,14 @@ user := server.GetUser(ctx) if pp.policy.CanRefresh(user) { return pp.box.Refresh(ctx) } return box.NewErrNotAllowed("Refresh", user, id.Invalid) +} +func (pp *polBox) ReIndex(ctx context.Context, zid id.Zid) error { + user := server.GetUser(ctx) + if pp.policy.CanRefresh(user) { + // If a user is allowed to refresh all data, it it also allowed to re-index a zettel. + return pp.box.ReIndex(ctx, zid) + } + return box.NewErrNotAllowed("ReIndex", user, zid) } Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -1,21 +1,24 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( - "zettelstore.de/c/api" + "t73f.de/r/zsc/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,25 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( - "zettelstore.de/c/api" + "t73f.de/r/zsc/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,22 +1,25 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. 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,25 +1,28 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "fmt" "testing" - "zettelstore.de/c/api" + "t73f.de/r/zsc/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,25 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- 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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box @@ -14,45 +17,27 @@ 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" + "t73f.de/r/zsc/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. // Format is dependent of the box. Location() string - // 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) - // 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) - - // CanUpdateZettel returns true, if box could possibly update the given zettel. - CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool - - // UpdateZettel updates an existing zettel. - UpdateZettel(ctx context.Context, zettel domain.Zettel) error + GetZettel(ctx context.Context, zid id.Zid) (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. @@ -62,10 +47,26 @@ CanDeleteZettel(ctx context.Context, zid id.Zid) bool // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } + +// WriteBox is a box that can create / update zettel content. +type WriteBox interface { + // 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 zettel.Zettel) (id.Zid, error) + + // CanUpdateZettel returns true, if box could possibly update the given zettel. + CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool + + // UpdateZettel updates an existing zettel. + UpdateZettel(ctx context.Context, zettel zettel.Zettel) error +} // ZidFunc is a function that processes identifier of a zettel. type ZidFunc func(id.Zid) // MetaFunc is a function that processes metadata of a zettel. @@ -72,10 +73,13 @@ 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, query.RetrievePredicate) error // Apply metadata of every zettel to the given function, if predicate returns true. @@ -91,15 +95,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) } @@ -111,25 +133,30 @@ } // Box is to be used outside the box package and its descendants. type Box interface { BaseBox + WriteBox // 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, q *query.Query) ([]*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 + + // ReIndex one zettel to update its index data. + ReIndex(context.Context, id.Zid) error } // Stats record stattistics about a box. type Stats struct { // ReadOnly indicates that boxes cannot be modified. @@ -181,17 +208,18 @@ type UpdateReason uint8 // Values for Reason const ( _ UpdateReason = iota + OnReady // Box is started and fully operational OnReload // Box was reloaded 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. @@ -225,10 +253,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 @@ -273,44 +309,21 @@ 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() } // 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,31 +1,34 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" - "zettelstore.de/c/api" + "t73f.de/r/zsc/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/query" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func init() { manager.Register( " comp", @@ -47,10 +50,11 @@ }{ id.MustParse(api.ZidVersion): {genVersionBuildM, genVersionBuildC}, id.MustParse(api.ZidHost): {genVersionHostM, genVersionHostC}, id.MustParse(api.ZidOperatingSystem): {genVersionOSM, genVersionOSC}, id.MustParse(api.ZidLog): {genLogM, genLogC}, + id.MustParse(api.ZidMemory): {genMemoryM, genMemoryC}, id.MustParse(api.ZidBoxManager): {genManagerM, genManagerC}, id.MustParse(api.ZidMetadataKey): {genKeysM, genKeysC}, id.MustParse(api.ZidParser): {genParserM, genParserC}, id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC}, } @@ -68,52 +72,37 @@ // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } func (*compBox) Location() string { return "" } -func (*compBox) CanCreateZettel(context.Context) bool { return false } - -func (cb *compBox) CreateZettel(context.Context, domain.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 + 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("ApplyMeta") + 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 { @@ -140,37 +129,32 @@ } } return nil } -func (*compBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } - -func (cb *compBox) UpdateZettel(context.Context, domain.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 +164,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,24 +1,27 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "t73f.de/r/zsc/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 Index: box/compbox/keys.go ================================================================== --- box/compbox/keys.go +++ box/compbox/keys.go @@ -1,25 +1,28 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "fmt" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "t73f.de/r/zsc/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") @@ -32,9 +35,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,32 +1,35 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "t73f.de/r/zsc/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)) + m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout)) 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,25 +1,28 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "fmt" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "t73f.de/r/zsc/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") ADDED box/compbox/memory.go Index: box/compbox/memory.go ================================================================== --- box/compbox/memory.go +++ box/compbox/memory.go @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2024-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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2024-present Detlef Stern +//----------------------------------------------------------------------------- + +package compbox + +import ( + "bytes" + "fmt" + "os" + "runtime" + + "t73f.de/r/zsc/api" + "zettelstore.de/z/kernel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +func genMemoryM(zid id.Zid) *meta.Meta { + if myConfig == nil { + return nil + } + m := meta.New(zid) + m.Set(api.KeyTitle, "Zettelstore Memory") + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) + m.Set(api.KeyVisibility, api.ValueVisibilityExpert) + return m +} + +func genMemoryC(*meta.Meta) []byte { + pageSize := os.Getpagesize() + var m runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&m) + + var buf bytes.Buffer + buf.WriteString("|=Name|=Value>\n") + fmt.Fprintf(&buf, "|Page Size|%d\n", pageSize) + fmt.Fprintf(&buf, "|Pages|%d\n", m.HeapSys/uint64(pageSize)) + fmt.Fprintf(&buf, "|Heap Objects|%d\n", m.HeapObjects) + fmt.Fprintf(&buf, "|Heap Sys (KiB)|%d\n", m.HeapSys/1024) + fmt.Fprintf(&buf, "|Heap Inuse (KiB)|%d\n", m.HeapInuse/1024) + debug := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool) + if debug { + for i, bysize := range m.BySize { + fmt.Fprintf(&buf, "|Size %2d: %d|%d - %d → %d\n", + i, bysize.Size, bysize.Mallocs, bysize.Frees, bysize.Mallocs-bysize.Frees) + } + } + return buf.Bytes() +} Index: box/compbox/parser.go ================================================================== --- box/compbox/parser.go +++ box/compbox/parser.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( @@ -14,15 +17,15 @@ "bytes" "fmt" "sort" "strings" - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "t73f.de/r/zsc/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") @@ -31,11 +34,11 @@ 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 { @@ -42,10 +45,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,25 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( - "zettelstore.de/c/api" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "t73f.de/r/zsc/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) Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -1,5 +1,19 @@ +/*----------------------------------------------------------------------------- + * 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 + * under this license. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2020-present Detlef Stern + *----------------------------------------------------------------------------- + */ + *,*::before,*::after { box-sizing: border-box; } html { font-size: 1rem; @@ -81,10 +95,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 } + p.zs-meta-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 } @@ -136,12 +151,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; @@ -182,11 +202,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 { 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,63 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +`(@@@@ +(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-binding)) + (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) + ))) + ) + (search (form (@ (action ,search-url)) + (input (@ (type "search") (inputmode "search") (name ,query-key-query) + (title "General search field, with same behaviour as search field in search result list") + (placeholder "Search..") (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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package constbox puts zettel inside the executable. package constbox @@ -14,19 +17,19 @@ import ( "context" _ "embed" // Allow to embed file content "net/url" - "zettelstore.de/c/api" + "t73f.de/r/zsc/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/query" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" ) func init() { manager.Register( " const", @@ -43,11 +46,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 @@ -55,33 +58,23 @@ enricher box.Enricher } 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) { - 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 + 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 { @@ -102,37 +95,32 @@ } } return nil } -func (*constBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } - -func (cb *constBox) UpdateZettel(context.Context, domain.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,224 +128,313 @@ 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)}, - id.MustParse(api.ZidAuthors): { - constHeader{ - api.KeyTitle: "Zettelstore Contributors", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyCreated: "20210504135842", - api.KeyLang: api.ValueLangEN, - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityLogin, - }, - domain.NewContent(contentContributors)}, - id.MustParse(api.ZidDependencies): { - constHeader{ - api.KeyTitle: "Zettelstore Dependencies", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityLogin, - api.KeyCreated: "20210504135842", - api.KeyModified: "20220824161200", - }, - domain.NewContent(contentDependencies)}, - id.BaseTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore Base HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyCreated: "20210504135842", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentBaseMustache)}, - id.LoginTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore Login Form HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyCreated: "20200804111624", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentLoginMustache)}, - id.ZettelTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore Zettel HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyCreated: "20200804111624", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentZettelMustache)}, - id.InfoTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore Info HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyCreated: "20200804111624", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentInfoMustache)}, - id.ContextTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore Context HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyCreated: "20210218181140", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentContextMustache)}, - id.FormTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore Form HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyCreated: "20200804111624", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentFormMustache)}, - id.RenameTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore Rename Form HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyCreated: "20200804111624", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentRenameMustache)}, - id.DeleteTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore Delete HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyCreated: "20200804111624", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentDeleteMustache)}, - id.ListTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore List Zettel HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyCreated: "20200804111624", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentListZettelMustache)}, - id.ErrorTemplateZid: { - constHeader{ - api.KeyTitle: "Zettelstore Error HTML Template", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: syntaxTemplate, - api.KeyCreated: "20210305133215", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(contentErrorMustache)}, - id.MustParse(api.ZidBaseCSS): { - constHeader{ - api.KeyTitle: "Zettelstore Base CSS", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: "css", - api.KeyCreated: "20200804111624", - api.KeyVisibility: api.ValueVisibilityPublic, - }, - domain.NewContent(contentBaseCSS)}, - id.MustParse(api.ZidUserCSS): { - constHeader{ - api.KeyTitle: "Zettelstore User CSS", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: "css", - 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.KeyCreated: "20220321183214", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - domain.NewContent(nil)}, - id.EmojiZid: { - constHeader{ - api.KeyTitle: "Zettelstore Generic Emoji", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxGif, - api.KeyReadOnly: api.ValueTrue, - api.KeyCreated: "20210504175807", - api.KeyVisibility: api.ValueVisibilityPublic, - }, - domain.NewContent(contentEmoji)}, - id.TOCNewTemplateZid: { - constHeader{ - api.KeyTitle: "New Menu", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyCreated: "20210217161829", - api.KeyVisibility: api.ValueVisibilityCreator, - }, - domain.NewContent(contentNewTOCZettel)}, - id.MustParse(api.ZidTemplateNewZettel): { - constHeader{ - api.KeyTitle: "New Zettel", - api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: api.ValueSyntaxZmk, - api.KeyCreated: "20201028185209", - api.KeyVisibility: api.ValueVisibilityCreator, - }, - domain.NewContent(nil)}, - id.MustParse(api.ZidTemplateNewUser): { - constHeader{ - api.KeyTitle: "New User", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: api.ValueSyntaxNone, + zettel.NewContent(contentLicense)}, + id.MustParse(api.ZidAuthors): { + constHeader{ + api.KeyTitle: "Zettelstore Contributors", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20210504135842", + api.KeyLang: api.ValueLangEN, + api.KeyReadOnly: api.ValueTrue, + api.KeyVisibility: api.ValueVisibilityLogin, + }, + zettel.NewContent(contentContributors)}, + id.MustParse(api.ZidDependencies): { + constHeader{ + api.KeyTitle: "Zettelstore Dependencies", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxZmk, + api.KeyLang: api.ValueLangEN, + api.KeyReadOnly: api.ValueTrue, + api.KeyVisibility: api.ValueVisibilityPublic, + api.KeyCreated: "20210504135842", + api.KeyModified: "20240418095500", + }, + zettel.NewContent(contentDependencies)}, + id.BaseTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore Base HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20230510155100", + api.KeyModified: "20240219145300", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentBaseSxn)}, + id.LoginTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore Login Form HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20240219145200", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentLoginSxn)}, + id.ZettelTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore Zettel HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20230510155300", + api.KeyModified: "20240219145100", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentZettelSxn)}, + id.InfoTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore Info HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20240219145200", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentInfoSxn)}, + id.FormTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore Form HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20240219145200", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentFormSxn)}, + id.RenameTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore Rename Form HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20240219145200", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentRenameSxn)}, + id.DeleteTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore Delete HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20240219145200", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentDeleteSxn)}, + id.ListTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore List Zettel HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20230704122100", + api.KeyModified: "20240219145200", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentListZettelSxn)}, + id.ErrorTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore Error HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20210305133215", + api.KeyModified: "20240219145200", + 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.KeyModified: "20240219145200", + api.KeyVisibility: api.ValueVisibilityExpert, + api.KeyPrecursor: string(api.ZidSxnBase), + }, + 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: "20240219144600", + api.KeyReadOnly: api.ValueTrue, + api.KeyVisibility: api.ValueVisibilityExpert, + api.KeyPrecursor: string(api.ZidSxnPrelude), + }, + zettel.NewContent(contentBaseCodeSxn)}, + id.PreludeSxnZid: { + constHeader{ + api.KeyTitle: "Zettelstore Sxn Prelude", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20231006181700", + api.KeyModified: "20240222121200", + api.KeyReadOnly: api.ValueTrue, + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentPreludeSxn)}, + id.MustParse(api.ZidBaseCSS): { + constHeader{ + api.KeyTitle: "Zettelstore Base CSS", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxCSS, + api.KeyCreated: "20200804111624", + api.KeyModified: "20231129112800", + api.KeyVisibility: api.ValueVisibilityPublic, + }, + zettel.NewContent(contentBaseCSS)}, + id.MustParse(api.ZidUserCSS): { + constHeader{ + api.KeyTitle: "Zettelstore User CSS", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxCSS, + api.KeyCreated: "20210622110143", + api.KeyVisibility: api.ValueVisibilityPublic, + }, + zettel.NewContent([]byte("/* User-defined CSS */"))}, + id.EmojiZid: { + constHeader{ + api.KeyTitle: "Zettelstore Generic Emoji", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxGif, + api.KeyReadOnly: api.ValueTrue, + api.KeyCreated: "20210504175807", + api.KeyVisibility: api.ValueVisibilityPublic, + }, + zettel.NewContent(contentEmoji)}, + id.TOCNewTemplateZid: { + constHeader{ + api.KeyTitle: "New Menu", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxZmk, + api.KeyLang: api.ValueLangEN, + api.KeyCreated: "20210217161829", + api.KeyModified: "20231129111800", + api.KeyVisibility: api.ValueVisibilityCreator, + }, + zettel.NewContent(contentNewTOCZettel)}, + id.MustParse(api.ZidTemplateNewZettel): { + constHeader{ + api.KeyTitle: "New Zettel", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20201028185209", + api.KeyModified: "20230929132900", + meta.NewPrefix + api.KeyRole: api.ValueRoleZettel, + api.KeyVisibility: api.ValueVisibilityCreator, + }, + zettel.NewContent(nil)}, + id.MustParse(api.ZidTemplateNewRole): { + constHeader{ + api.KeyTitle: "New Role", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20231129110800", + meta.NewPrefix + api.KeyRole: api.ValueRoleRole, + meta.NewPrefix + api.KeyTitle: "", + api.KeyVisibility: api.ValueVisibilityCreator, + }, + zettel.NewContent(nil)}, + id.MustParse(api.ZidTemplateNewTag): { + constHeader{ + api.KeyTitle: "New Tag", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20230929132400", + meta.NewPrefix + api.KeyRole: api.ValueRoleTag, + meta.NewPrefix + api.KeyTitle: "#", + api.KeyVisibility: api.ValueVisibilityCreator, + }, + zettel.NewContent(nil)}, + id.MustParse(api.ZidTemplateNewUser): { + constHeader{ + api.KeyTitle: "New User", + api.KeyRole: api.ValueRoleConfiguration, + 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.MustParse(api.ZidRoleZettelZettel): { + constHeader{ + api.KeyTitle: api.ValueRoleZettel, + api.KeyRole: api.ValueRoleRole, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20231129161400", + api.KeyLang: api.ValueLangEN, + api.KeyVisibility: api.ValueVisibilityLogin, + }, + zettel.NewContent(contentRoleZettel)}, + id.MustParse(api.ZidRoleConfigurationZettel): { + constHeader{ + api.KeyTitle: api.ValueRoleConfiguration, + api.KeyRole: api.ValueRoleRole, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20231129162800", + api.KeyLang: api.ValueLangEN, + api.KeyVisibility: api.ValueVisibilityLogin, + }, + zettel.NewContent(contentRoleConfiguration)}, + id.MustParse(api.ZidRoleRoleZettel): { + constHeader{ + api.KeyTitle: api.ValueRoleRole, + api.KeyRole: api.ValueRoleRole, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20231129162900", + api.KeyLang: api.ValueLangEN, + api.KeyVisibility: api.ValueVisibilityLogin, + }, + zettel.NewContent(contentRoleRole)}, + id.MustParse(api.ZidRoleTagZettel): { + constHeader{ + api.KeyTitle: api.ValueRoleTag, + api.KeyRole: api.ValueRoleRole, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20231129162000", + api.KeyLang: api.ValueLangEN, + api.KeyVisibility: api.ValueVisibilityLogin, + }, + zettel.NewContent(contentRoleTag)}, id.DefaultHomeZid: { constHeader{ api.KeyTitle: "Home", api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: api.ValueSyntaxZmk, + api.KeySyntax: meta.SyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyCreated: "20210210190757", }, - domain.NewContent(contentHomeZettel)}, + zettel.NewContent(contentHomeZettel)}, } //go:embed license.txt var contentLicense []byte @@ -365,46 +442,64 @@ 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 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 prelude.sxn +var contentPreludeSxn []byte //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte //go:embed newtoc.zettel var contentNewTOCZettel []byte + +//go:embed rolezettel.zettel +var contentRoleZettel []byte + +//go:embed roleconfiguration.zettel +var contentRoleConfiguration []byte + +//go:embed rolerole.zettel +var contentRoleRole []byte + +//go:embed roletag.zettel +var contentRoleTag []byte //go:embed home.zettel var contentHomeZettel []byte DELETED box/constbox/context.mustache Index: box/constbox/context.mustache ================================================================== --- box/constbox/context.mustache +++ box/constbox/context.mustache @@ -1,11 +0,0 @@ -
-

{{Title}}

-
-InfoBackwardBothForward -· Depth:{{#Depths}} {{{Text}}}{{/Depths}} -
-
-{{{Content}}} 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,39 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +`(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,117 +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. -``` - -=== gopikchr -; URL & Source -: [[https://github.com/gopikchr/gopikchr]] -; License -: MIT License -; Remarks -: Author is [[Zellyn Hunter|https://github.com/zellyn]], he wrote a blog post [[gopikchr: a yakshave|https://zellyn.com/2022/01/gopikchr-a-yakshave/]] about his work. -: Gopikchr was incorporated into the source code of Zettelstore, moving it into package ''zettelstore.de/z/parser/pikchr''. - Later, the source code was changed to adapt it to the needs of Zettelstore. - For details, read README.txt in the appropriate source code folder. -``` -MIT License - -Copyright (c) 2022 gopikchr - -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. -``` - -=== 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]] @@ -209,5 +127,20 @@ 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, SxWebs, Webs, Zettelstore-Client +These are companion projects, written by the main developer of Zettelstore. +They are published under the same license, [[EUPL v1.2, or later|00000000000004]]. + +; URL & Source Sx +: [[https://t73f.de/r/sx]] +; URL & Source SxWebs +: [[https://t73f.de/r/sxwebs]] +; URL & Source Webs +: [[https://t73f.de/r/webs]] +; URL & Source Zettelstore-Client +: [[https://t73f.de/r/zsc]] +; 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,17 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +`(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,63 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +`(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") + (title "Title of this zettel") + (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") (pattern "\\w*") (id "zs-role") (name "role") + (title "One word, letters and digits, but no spaces, to set the main role of the zettel.") + (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") + (title "Tags/keywords to categorize the zettel. Each tags is a word that begins with a '#' character; they are separated by spaces") + (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") + (title "Additional metadata about the zettel") + (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") (pattern "\\w*") (id "zs-syntax") (name "syntax") + (title "Syntax/format of zettel content below, one word, letters and digits, no spaces.") + (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") + (title "Zettel content, according to the given syntax") + (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,71 +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}} -{{#HasQueryLinks}} -

Queries

- -{{/HasQueryLinks}} -{{#HasExtLinks}} -

External

- -{{/HasExtLinks}} -

Unlinked

-{{{UnLinksContent}}} -
- - -
-

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,48 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +`(article + (header (h1 "Information for Zettel " ,zid) + (p + (a (@ (href ,web-url)) "Web") + (@H " · ") (a (@ (href ,context-url)) "Context") + (@H " / ") (a (@ (href ,context-full-url)) "Full") + ,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit"))) + ,@(ROLE-DEFAULT-actions (current-binding)) + ,@(if (bound? 'reindex-url) `((@H " · ") (a (@ (href ,reindex-url)) "Reindex"))) + ,@(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-info-meta-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/listzettel.mustache Index: box/constbox/listzettel.mustache ================================================================== --- box/constbox/listzettel.mustache +++ box/constbox/listzettel.mustache @@ -1,7 +0,0 @@ -
-

{{Title}}

-
-
- -
-{{{Content}}} ADDED box/constbox/listzettel.sxn Index: box/constbox/listzettel.sxn ================================================================== --- box/constbox/listzettel.sxn +++ box/constbox/listzettel.sxn @@ -0,0 +1,50 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +`(article + (header (h1 ,heading)) + (search (form (@ (action ,search-url)) + (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query) + (title "Contains the search that leads to the list below. You're allowed to modify it") + (placeholder "Search..") (value ,query-value) (dir "auto"))))) + ,@(if (bound? 'tag-zettel) + `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel)) + ) + ,@(if (bound? 'create-tag-zettel) + `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel)) + ) + ,@(if (bound? 'role-zettel) + `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel)) + ) + ,@(if (bound? 'create-role-zettel) + `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel)) + ) + ,@content + ,@endnotes + (form (@ (action ,(if (bound? 'create-url) create-url))) + ,(if (bound? 'data-url) + `(@L "Other encodings" + ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ") + (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,27 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +`(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")))) + ) +) Index: box/constbox/newtoc.zettel ================================================================== --- box/constbox/newtoc.zettel +++ box/constbox/newtoc.zettel @@ -1,4 +1,6 @@ This zettel lists all zettel that should act as a template for new zettel. These zettel will be included in the ""New"" menu of the WebUI. * [[New Zettel|00000000090001]] +* [[New Role|00000000090004]] +* [[New Tag|00000000090003]] * [[New User|00000000090002]] ADDED box/constbox/prelude.sxn Index: box/constbox/prelude.sxn ================================================================== --- box/constbox/prelude.sxn +++ box/constbox/prelude.sxn @@ -0,0 +1,62 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +;;; This zettel contains sxn definitions that are independent of specific +;;; subsystems, such as WebUI, API, or other. It just contains generic code to +;;; be used in all places. It asumes that the symbols NIL and T are defined. + +;; not macro +(defmacro not (x) `(if ,x NIL T)) + +;; not= macro, to negate an equivalence +(defmacro not= args `(not (= ,@args))) + +;; let* macro +;; +;; (let* (BINDING ...) EXPR ...), where SYMBOL may occur in later bindings. +(defmacro let* (bindings . body) + (if (null? bindings) + `(begin ,@body) + `(let ((,(caar bindings) ,(cadar bindings))) + (let* ,(cdr bindings) ,@body)))) + +;; cond macro +;; +;; (cond ((COND EXPR) ...)) +(defmacro cond clauses + (if (null? clauses) + () + (let* ((clause (car clauses)) + (the-cond (car clause))) + (if (= the-cond T) + `(begin ,@(cdr clause)) + `(if ,the-cond + (begin ,@(cdr clause)) + (cond ,@(cdr clauses))))))) + +;; and macro +;; +;; (and EXPR ...) +(defmacro and args + (cond ((null? args) T) + ((null? (cdr args)) (car args)) + (T `(if ,(car args) (and ,@(cdr args)))))) + + +;; or macro +;; +;; (or EXPR ...) +(defmacro or args + (cond ((null? args) NIL) + ((null? (cdr args)) (car args)) + (T `(if ,(car args) T (or ,@(cdr args)))))) 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,42 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +`(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") (inputmode "numeric") (id "newzid") (name "newzid") + (pattern "\\d{14}") + (title "New zettel identifier, must be unique") + (placeholder "ZID..") (value ,zid) (autofocus)))) + (div (input (@ (class "zs-primary") (type "submit") (value "Rename")))) + ) + ,(wui-meta-desc metapairs) +) ADDED box/constbox/roleconfiguration.zettel Index: box/constbox/roleconfiguration.zettel ================================================================== --- box/constbox/roleconfiguration.zettel +++ box/constbox/roleconfiguration.zettel @@ -0,0 +1,20 @@ +Zettel with role ""configuration"" are used within Zettelstore to manage and to show the current configuration of the software. + +Typically, there are some public zettel that show the license of this software, its dependencies, some CSS code to make the default web user interface a litte bit nicer, and the defult image to singal a broken image. + +Other zettel are only visible if an user has authenticated itself, or if there is no authentication enabled. +In this case, one additional configuration zettel is the zettel containing the version number of this software. +Other zettel are showing the supported metadata keys and supported syntax values. +Zettel that allow to configure the menu of template to create new zettel are also using the role ""configuration"". + +Most important is the zettel that contains the runtime configuration. +You may change its metadata value to change the behaviour of the software. + +One configuration is the ""expert mode"". +If enabled, and if you are authorized so see them, you will discover some more zettel. +For example, HTML templates to customize the default web user interface, to show the application log, to see statistics about zettel boxes, to show the host name and it operating system, and many more. + +You are allowed to add your own configuration zettel, for example if you want to customize the look and feel of zettel by placing relevant data into your own zettel. + +By default, user zettel (for authentification) use also the role ""configuration"". +However, you are allowed to change this. ADDED box/constbox/rolerole.zettel Index: box/constbox/rolerole.zettel ================================================================== --- box/constbox/rolerole.zettel +++ box/constbox/rolerole.zettel @@ -0,0 +1,10 @@ +A zettel with the role ""role"" describes a specific role. +The described role must be the title of such a zettel. + +This zettel is such a zettel, as it describes the meaning of the role ""role"". +Therefore it has the title ""role"" too. +If you like, this zettel is a meta-role. + +You are free to create your own role-describing zettel. +For example, you want to document the intended meaning of the role. +You might also be interested to describe needed metadata so that some software is enabled to analyse or to process your zettel. ADDED box/constbox/roletag.zettel Index: box/constbox/roletag.zettel ================================================================== --- box/constbox/roletag.zettel +++ box/constbox/roletag.zettel @@ -0,0 +1,6 @@ +A zettel with role ""tag"" is a zettel that describes specific tag. +The tag name must be the title of such a zettel. + +Such zettel are similar to this specific zettel: this zettel describes zettel with a role ""tag"". +These zettel with the role ""tag"" describe specific tags. +These might form a hierarchy of meta-tags (and meta-roles). ADDED box/constbox/rolezettel.zettel Index: box/constbox/rolezettel.zettel ================================================================== --- box/constbox/rolezettel.zettel +++ box/constbox/rolezettel.zettel @@ -0,0 +1,7 @@ +A zettel with the role ""zettel"" is typically used to document your own thoughts. +Such zettel are the main reason to use the software Zettelstore. + +The only predefined zettel with the role ""zettel"" is the [[default home zettel|00010000000000]], which contains some welcome information. + +You are free to change this. +In this case you should modify this zettel too, so that it reflects your own use of zettel with the role ""zettel"". ADDED box/constbox/start.sxn Index: box/constbox/start.sxn ================================================================== --- box/constbox/start.sxn +++ box/constbox/start.sxn @@ -0,0 +1,17 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +;;; 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,143 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +;; Contains WebUI specific code, but not related to a specific template. + +;; wui-list-item returns the argument as a HTML list item. +(defun wui-item (s) `(li ,s)) + +;; wui-info-meta-table-row takes a pair and translates it into a HTML table row +;; with two columns. +(defun wui-info-meta-table-row (p) + `(tr (td (@ (class zs-info-meta-key)) ,(car p)) (td (@ (class zs-info-meta-value)) ,(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. +(defun 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. +(defun 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. +(defun 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. +(defun 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. +(defun 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. +(defun wui-option-value (v) `(option (@ (value ,v)))) + +;; wui-datalist returns a HTML datalist with the given HTML identifier and a +;; list of values. +(defun 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. +(defun 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. +(defun 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. +(defun 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". +(defvar 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. +(defun ROLE-DEFAULT-meta (binding) + `(,@(let* ((meta-role (binding-lookup 'meta-role binding)) + (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. +(defvar ACTION-SEPARATOR '(@H " · ")) + +;; ROLE-DEFAULT-actions returns the default text for actions. +(defun ROLE-DEFAULT-actions (binding) + `(,@(let ((copy-url (binding-lookup 'copy-url binding))) + (if (defined? copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))) + ,@(let ((version-url (binding-lookup 'version-url binding))) + (if (defined? version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))) + ,@(let ((child-url (binding-lookup 'child-url binding))) + (if (defined? child-url) `((@H " · ") (a (@ (href ,child-url)) "Child")))) + ,@(let ((folge-url (binding-lookup 'folge-url binding))) + (if (defined? folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))) + ) +) + +;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag". +(defun ROLE-tag-actions (binding) + `(,@(ROLE-DEFAULT-actions binding) + ,@(let ((title (binding-lookup 'title binding))) + (if (and (defined? title) title) + `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "tags:" title)))) "Zettel")) + ) + ) + ) +) + +;; ROLE-role-actions returns an additional action "Zettel" for zettel with role "role". +(defun ROLE-role-actions (binding) + `(,@(ROLE-DEFAULT-actions binding) + ,@(let ((title (binding-lookup 'title binding))) + (if (and (defined? title) title) + `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "role:" 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. +(defun ROLE-DEFAULT-heading (binding) + `(,@(let ((meta-url (binding-lookup 'meta-url binding))) + (if (defined? meta-url) `((br) "URL: " ,(url-to-html meta-url)))) + ,@(let ((urls (binding-lookup 'urls binding))) + (if (defined? urls) + (map (lambda (u) `(@L (br) ,(car u) ": " ,(url-to-html (cdr u)))) urls) + ) + ) + ,@(let ((meta-author (binding-lookup 'meta-author binding))) + (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,42 +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}} -{{#Author}}
By {{Author}}{{/Author}} -
-
-{{{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,43 @@ +;;;---------------------------------------------------------------------------- +;;; 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. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +`(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-binding)) + ,@(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-binding)) + ) + ) + ,@content + ,endnotes + ,@(if (or folge-links subordinate-links back-links successor-links) + `((nav + ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) + ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) + ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) + ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links))))) + )) + ) +) Index: box/dirbox/dirbox.go ================================================================== --- box/dirbox/dirbox.go +++ box/dirbox/dirbox.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox @@ -20,16 +23,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/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 @@ -87,11 +90,11 @@ dirNotifySimple dirNotifyFS ) func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec { - for count := 0; count < 2; count++ { + for range 2 { switch notifyType { case kernel.BoxDirTypeNotify: return dirNotifyFS case kernel.BoxDirTypeSimple: return dirNotifySimple @@ -126,16 +129,34 @@ } 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) - for i := uint32(0); i < dp.fSrvs; i++ { + for i := range dp.fSrvs { cc := make(chan fileCmd) go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc) dp.fCmds = append(dp.fCmds, cc) } @@ -146,15 +167,16 @@ notifier, err = notify.NewSimpleDirNotifier(dp.log.Clone().Str("notify", "simple").Child(), dp.dir) default: notifier, err = notify.NewFSDirNotifier(dp.log.Clone().Str("notify", "fs").Child(), dp.dir) } if err != nil { - dp.log.Fatal().Err(err).Msg("Unable to create directory supervisor") + dp.log.Error().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 +201,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 +224,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,44 +242,31 @@ err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } - dp.notifyChanged(box.OnZettel, 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) 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") @@ -282,23 +291,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 +314,17 @@ } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) dp.dirSrv.UpdateDirEntry(entry) err := dp.srvSetZettel(ctx, entry, zettel) if err == nil { - dp.notifyChanged(box.OnZettel, 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 +334,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 +355,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.OnZettel, curZid) - dp.notifyChanged(box.OnZettel, newZid) + dp.notifyChanged(curZid) + dp.notifyChanged(newZid) } dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel") return err } @@ -376,19 +385,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.OnZettel, 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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package dirbox import "testing" @@ -30,11 +33,11 @@ } } } func TestMakePrime(t *testing.T) { - for i := uint32(0); i < 1500; i++ { + for i := range uint32(1500) { np := makePrime(i) if np < i { t.Errorf("makePrime(%d) < %d", i, np) continue } Index: box/dirbox/service.go ================================================================== --- box/dirbox/service.go +++ box/dirbox/service.go @@ -1,52 +1,56 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package dirbox import ( "context" + "fmt" "io" "os" "path/filepath" "time" + "t73f.de/r/zsc/input" "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") + log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started") for cmd := range cmds { - cmd.run(log, dirPath) + cmd.run(dirPath) } - log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped") + log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped") } type fileCmd interface { - run(*logger.Logger, string) + run(string) } const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing. // COMMAND: srvGetMeta ---------------------------------------- @@ -73,23 +77,22 @@ type resGetMeta struct { meta *meta.Meta err error } -func (cmd *fileGetMeta) run(log *logger.Logger, dirPath string) { +func (cmd *fileGetMeta) run(dirPath string) { var m *meta.Meta var err error entry := cmd.entry zid := entry.Zid if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { - log.Panic().Zid(zid).Msg("No meta, no content in getMeta") - } - if entry.HasMetaInContent() { + err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid) + } else if entry.HasMetaInContent() { m, _, err = parseMetaContentFile(zid, filepath.Join(dirPath, contentName)) } else { m = filebox.CalcDefaultMeta(zid, contentExt) } } else { @@ -126,11 +129,11 @@ meta *meta.Meta content []byte err error } -func (cmd *fileGetMetaContent) run(log *logger.Logger, dirPath string) { +func (cmd *fileGetMetaContent) run(dirPath string) { var m *meta.Meta var content []byte var err error entry := cmd.entry @@ -138,13 +141,12 @@ contentName := entry.ContentName contentExt := entry.ContentExt contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" || contentExt == "" { - log.Panic().Zid(zid).Msg("No meta, no content in getMetaContent") - } - if entry.HasMetaInContent() { + err = fmt.Errorf("no meta, no content in getMetaContent, zid=%v", zid) + } else if entry.HasMetaInContent() { m, content, err = parseMetaContentFile(zid, contentPath) } else { m = filebox.CalcDefaultMeta(zid, contentExt) content, err = os.ReadFile(contentPath) } @@ -166,11 +168,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,38 +183,40 @@ } } 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) { +func (cmd *fileSetZettel) run(dirPath string) { + var err error entry := cmd.entry zid := entry.Zid contentName := entry.ContentName m := cmd.zettel.Meta content := cmd.zettel.Content.AsBytes() metaName := entry.MetaName if metaName == "" { if contentName == "" { - log.Panic().Zid(zid).Msg("No meta, no content in setZettel") - } - contentPath := filepath.Join(dirPath, contentName) - if entry.HasMetaInContent() { - err := writeZettelFile(contentPath, m, content) - cmd.rc <- err - return - } - err := writeFileContent(contentPath, content) + err = fmt.Errorf("no meta, no content in setZettel, zid=%v", zid) + } else { + contentPath := filepath.Join(dirPath, contentName) + if entry.HasMetaInContent() { + err = writeZettelFile(contentPath, m, content) + cmd.rc <- err + return + } + err = writeFileContent(contentPath, content) + } cmd.rc <- err return } - err := writeMetaFile(filepath.Join(dirPath, metaName), m) + err = writeMetaFile(filepath.Join(dirPath, metaName), m) if err == nil && contentName != "" { err = writeFileContent(filepath.Join(dirPath, contentName), content) } cmd.rc <- err } @@ -235,13 +239,11 @@ func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error { zettelFile, err := openFileWrite(contentPath) if err != nil { return err } - if err == nil { - err = writeMetaHeader(zettelFile, m) - } + err = writeMetaHeader(zettelFile, m) if err == nil { _, err = zettelFile.Write(content) } if err1 := zettelFile.Close(); err == nil { err = err1 @@ -298,21 +300,22 @@ entry *notify.DirEntry rc chan<- resDeleteZettel } type resDeleteZettel = error -func (cmd *fileDeleteZettel) run(log *logger.Logger, dirPath string) { +func (cmd *fileDeleteZettel) run(dirPath string) { var err error entry := cmd.entry contentName := entry.ContentName contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" { - log.Panic().Zid(entry.Zid).Msg("No meta, no content in getMetaContent") + err = fmt.Errorf("no meta, no content in deleteZettel, zid=%v", entry.Zid) + } else { + err = os.Remove(contentPath) } - err = os.Remove(contentPath) } else { if contentName != "" { err = os.Remove(contentPath) } err1 := os.Remove(filepath.Join(dirPath, metaName)) Index: box/filebox/filebox.go ================================================================== --- box/filebox/filebox.go +++ box/filebox/filebox.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package filebox provides boxes that are stored in a file. package filebox @@ -15,16 +18,16 @@ "errors" "net/url" "path/filepath" "strings" - "zettelstore.de/c/api" + "t73f.de/r/zsc/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,31 +1,35 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package filebox import ( "archive/zip" "context" + "fmt" "io" "strings" + "t73f.de/r/zsc/input" "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/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 +43,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 +81,21 @@ 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) { - 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 @@ -90,15 +102,16 @@ var inMeta bool contentName := entry.ContentName if metaName := entry.MetaName; metaName == "" { if contentName == "" { - zb.log.Panic().Zid(zid).Msg("No meta, no content in zipBox.GetZettel") + err = fmt.Errorf("no meta, no content in getZettel, zid=%v", zid) + return zettel.Zettel{}, err } 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,39 +119,28 @@ 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 + 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") @@ -168,18 +170,10 @@ handle(m) } return nil } -func (*zipBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } - -func (zb *zipBox) UpdateZettel(context.Context, domain.Zettel) error { - err := box.ErrReadOnly - zb.log.Trace().Err(err).Msg("UpdateZettel") - return err -} - func (zb *zipBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { entry := zb.dirSrv.GetDirEntry(zid) return !entry.IsValid() } @@ -188,11 +182,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 +194,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 } @@ -218,13 +212,12 @@ var inMeta bool if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { - zb.log.Panic().Zid(zid).Msg("No meta, no content in getMeta") - } - if entry.HasMetaInContent() { + err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid) + } else if entry.HasMetaInContent() { m, err = readZipMetaFile(reader, zid, contentName) } else { m = CalcDefaultMeta(zid, contentExt) } } else { Index: box/helper.go ================================================================== --- box/helper.go +++ box/helper.go @@ -1,27 +1,32 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- 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 - for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout) + for range 90 { // Must be completed within 9 seconds (less than web/server.writeTimeout) zid := id.New(withSeconds) found, err := testZid(zid) if err != nil { return id.Invalid, err } @@ -32,5 +37,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,21 +1,24 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "sync" - "zettelstore.de/z/domain/id" + "zettelstore.de/z/zettel/id" ) type arAction int const ( @@ -23,108 +26,87 @@ arReload 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) EnqueueZettel(zid id.Zid) { +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, arZettel) + 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 } if _, ok := room.waiting[zid]; ok { - // Zettel is already waiting. + // Zettel is already waiting. Nothing to do. return } } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { - room.waiting[zid] = arZettel + room.waiting.Add(zid) room.curLoad++ return } - room := ar.makeAnteroom(zid, arZettel) + room := ar.makeAnteroom(zid) ar.last.next = room ar.last = room } -func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom { - c := ar.maxLoad - if c == 0 { - c = 100 - } - waiting := make(map[id.Zid]arAction, c) - waiting[zid] = action - ar.nextNum++ - return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false} -} - -func (ar *anterooms) Reset() { - ar.mx.Lock() - defer ar.mx.Unlock() - ar.first = ar.makeAnteroom(id.Invalid, arReload) +func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom { + if zid == id.Invalid { + panic(zid) + } + waiting := id.NewSetCap(max(ar.maxLoad, 100), zid) + return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false} +} + +func (ar *anteroomQueue) Reset() { + ar.mx.Lock() + defer ar.mx.Unlock() + ar.first = &anteroom{next: nil, waiting: nil, curLoad: 0, reload: true} ar.last = ar.first } -func (ar *anterooms) Reload(newZids id.Set) uint64 { +func (ar *anteroomQueue) Reload(allZids id.Set) { ar.mx.Lock() defer ar.mx.Unlock() - newWaiting := createWaitingSet(newZids) ar.deleteReloadedRooms() - if ns := len(newWaiting); ns > 0 { - ar.nextNum++ - ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newWaiting, curLoad: ns} + if ns := len(allZids); ns > 0 { + ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: ns, reload: true} if ar.first.next == nil { ar.last = ar.first } - return ar.nextNum - } - - ar.first = nil - ar.last = nil - return 0 -} - -func createWaitingSet(zids id.Set) map[id.Zid]arAction { - waitingSet := make(map[id.Zid]arAction, len(zids)) - for zid := range zids { - if zid.IsValid() { - waitingSet[zid] = arZettel - } - } - return waitingSet -} - -func (ar *anterooms) deleteReloadedRooms() { + } else { + ar.first = nil + ar.last = nil + } +} + +func (ar *anteroomQueue) deleteReloadedRooms() { room := ar.first for room != nil && room.reload { room = room.next } ar.first = room @@ -131,24 +113,32 @@ if room == nil { ar.last = nil } } -func (ar *anterooms) Dequeue() (arAction, id.Zid, uint64) { +func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, bool) { 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 + first := ar.first + if first != nil { + if first.waiting == nil && first.reload { + ar.removeFirst() + return arReload, id.Invalid, false + } + for zid := range first.waiting { + delete(first.waiting, zid) + if len(first.waiting) == 0 { + ar.removeFirst() + } + return arZettel, zid, first.reload + } + ar.removeFirst() + } + return arNothing, id.Invalid, false +} + +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,30 +1,33 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- 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 := newAnteroomQueue(2) ar.EnqueueZettel(id.Zid(1)) - action, zid, rno := ar.Dequeue() - 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, lastReload := ar.Dequeue() + if zid != id.Zid(1) || action != arZettel || lastReload { + t.Errorf("Expected arZettel/1/false, but got %v/%v/%v", action, zid, lastReload) } _, zid, _ = ar.Dequeue() if zid != id.Invalid { t.Errorf("Expected invalid Zid, but got %v", zid) } @@ -50,11 +53,11 @@ } } func TestReset(t *testing.T) { t.Parallel() - ar := newAnterooms(1) + 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) @@ -83,11 +86,11 @@ 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 = 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) } @@ -94,13 +97,13 @@ 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 = 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,181 +1,189 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- 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/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 - for i := 0; i < len(mgr.boxes)-2; i++ { + var sb strings.Builder + for i := range len(mgr.boxes) - 2 { 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 { + if mgr.State() != box.StartStateStarted { + return false + } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - return mgr.started && mgr.boxes[0].CanCreateZettel(ctx) + if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { + return box.CanCreateZettel(ctx) + } + return false } // 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 } - return mgr.boxes[0].CreateZettel(ctx, zettel) + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { + zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) + zid, err := box.CreateZettel(ctx, zettel) + if err == nil { + mgr.idxUpdateZettel(ctx, zettel) + } + return zid, err + } + return id.Invalid, box.ErrReadOnly } // 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, q *query.Query) ([]*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", q.String()).Msg("SelectMeta") } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if !mgr.started { + if mgr.State() != box.StartStateStarted { return nil, box.ErrStopped } + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() - compSearch := q.RetrieveAndCompile(mgr) - selected := metaMap{} + 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.Contains(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") @@ -183,59 +191,67 @@ } if compSearch.PreMatch(m) && term.Match(m) { selected[zid] = m mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match") } else { - rejected.Zid(zid) + rejected.Add(zid) mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject") } } for _, p := range mgr.boxes { - if err := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err != nil { - return nil, err + 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 q.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 { + if mgr.State() != box.StartStateStarted { + return false + } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - return mgr.started && mgr.boxes[0].CanUpdateZettel(ctx, zettel) + if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { + return box.CanUpdateZettel(ctx, zettel) + } + return false + } // 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() { - if mgr.propertyKeys.Has(p.Key) { - zettel.Meta.Delete(p.Key) - } - } - return mgr.boxes[0].UpdateZettel(ctx, zettel) + if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { + zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) + if err := box.UpdateZettel(ctx, zettel); err != nil { + return err + } + mgr.idxUpdateZettel(ctx, zettel) + return nil + } + return box.ErrReadOnly } // 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 } } @@ -243,34 +259,36 @@ } // 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) { - for j := 0; j < i; j++ { + var errZNF box.ErrZettelNotFound + if err != nil && !errors.As(err, &errZNF) { + for j := range i { mgr.boxes[j].RenameZettel(ctx, newZid, curZid) } return err } } + mgr.idxRenameZettel(ctx, curZid, newZid) 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 } } @@ -278,21 +296,34 @@ } // 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 { + mgr.idxDeleteZettel(ctx, zid) 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} +} + +// Remove all (computed) properties from metadata before storing the zettel. +func (mgr *Manager) cleanMetaProperties(m *meta.Meta) *meta.Meta { + result := m.Clone() + for _, p := range result.ComputedPairsRest() { + if mgr.propertyKeys.Has(p.Key) { + result.Delete(p.Key) + } + } + return result } Index: box/manager/collect.go ================================================================== --- box/manager/collect.go +++ box/manager/collect.go @@ -1,38 +1,39 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager 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,13 +48,10 @@ 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: @@ -79,8 +77,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,25 +1,28 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "context" "strconv" - "zettelstore.de/c/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/box" - "zettelstore.de/z/domain/id" - "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) { @@ -32,11 +35,13 @@ // Enrich is called indirectly via indexer or enrichment is not requested // because of other reasons -> ignore this call, do not update metadata return } computePublished(m) - m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) + if boxNumber > 0 { + m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) + } mgr.idxStore.Enrich(ctx, m) } func computeCreated(zid id.Zid) string { if zid <= 10101000000 { @@ -59,26 +64,33 @@ 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 } - zid /= 100 - month := zid % 100 if month < 1 { month = 1 } if month > 12 { month = 12 } - year := zid / 100 + switch month { case 1, 3, 5, 7, 8, 10, 12: if day > 31 { - day = 32 + day = 31 } case 4, 6, 9, 11: if day > 30 { day = 30 } @@ -91,12 +103,11 @@ if day > 29 { day = 29 } } } - created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds - return created.String() + return month, day } func computePublished(m *meta.Meta) { if _, ok := m.Get(api.KeyPublished); ok { return Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( @@ -16,16 +19,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 +76,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 @@ -91,25 +94,21 @@ } } } func (mgr *Manager) idxWorkService(ctx context.Context) { - var roomNum uint64 var start time.Time for { - switch action, zid, arRoomNum := mgr.idxAr.Dequeue(); action { + switch action, zid, lastReload := mgr.idxAr.Dequeue(); action { case arNothing: return case arReload: mgr.idxLog.Debug().Msg("reload") - roomNum = 0 zids, err := mgr.FetchZids(ctx) if err == nil { start = time.Now() - if rno := mgr.idxAr.Reload(zids); rno > 0 { - roomNum = rno - } + mgr.idxAr.Reload(zids) mgr.idxMx.Lock() mgr.idxLastReload = time.Now().Local() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } @@ -117,24 +116,21 @@ mgr.idxLog.Debug().Zid(zid).Msg("zettel") zettel, err := mgr.GetZettel(ctx, zid) if err != nil { // 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) + mgr.idxDeleteZettel(ctx, zid) continue } mgr.idxLog.Trace().Zid(zid).Msg("update") + mgr.idxUpdateZettel(ctx, zettel) mgr.idxMx.Lock() - if arRoomNum == roomNum { + if lastReload { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() - mgr.idxUpdateZettel(ctx, zettel) } } } func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { @@ -155,17 +151,17 @@ 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(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) } @@ -189,51 +185,69 @@ case meta.TypeURL: if _, err := url.Parse(pair.Value); err == nil { cData.urls.Add(pair.Value) } default: - for _, word := range strfun.NormalizeWords(pair.Value) { - cData.words.Add(word) + if descr.Type.IsSet { + for _, val := range meta.ListFromValue(pair.Value) { + idxCollectMetaValue(cData.words, val) + } + } else { + idxCollectMetaValue(cData.words, pair.Value) } } } } + +func idxCollectMetaValue(stWords store.WordSet, value string) { + if words := strfun.NormalizeWords(value); len(words) > 0 { + for _, word := range words { + stWords.Add(word) + } + } else { + stWords.Add(value) + } +} 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) idxRenameZettel(ctx context.Context, curZid, newZid id.Zid) { + toCheck := mgr.idxStore.RenameZettel(ctx, curZid, newZid) + mgr.idxCheckZettel(toCheck) } -func (mgr *Manager) idxDeleteZettel(zid id.Zid) { - toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid) +func (mgr *Manager) idxDeleteZettel(ctx context.Context, zid id.Zid) { + toCheck := mgr.idxStore.DeleteZettel(ctx, zid) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCheckZettel(s id.Set) { for zid := range s { mgr.idxAr.EnqueueZettel(zid) } } Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager @@ -16,21 +19,20 @@ "io" "net/url" "sync" "time" - "zettelstore.de/c/maps" "zettelstore.de/z/auth" "zettelstore.de/z/box" - "zettelstore.de/z/box/manager/memstore" + "zettelstore.de/z/box/manager/mapstore" "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. @@ -77,18 +79,16 @@ panic(scheme) } registry[scheme] = create } -// GetSchemes returns all registered scheme, ordered by scheme string. -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 +96,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)) @@ -123,12 +136,12 @@ rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, idxLog: boxLog.Clone().Str("box", "index").Child(), - idxStore: memstore.New(), - idxAr: newAnterooms(10), + idxStore: createIdxStore(rtConfig), + 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 { @@ -153,10 +166,14 @@ cdata.Number++ boxes = append(boxes, constbox, compbox) mgr.boxes = boxes return mgr, nil } + +func createIdxStore(_ config.Config) store.Store { + return mapstore.New() +} // RegisterObserver registers an observer that will be notified // if a zettel was found to be changed. func (mgr *Manager) RegisterObserver(f box.UpdateFunc) { if f != nil { @@ -167,12 +184,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 +210,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,15 +246,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.OnZettel: mgr.idxAr.EnqueueZettel(zid) default: + mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") return } select { case mgr.idxReady <- struct{}{}: default: @@ -252,73 +275,112 @@ // 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.Info().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") - mgr.mgrMx.Lock() - defer mgr.mgrMx.Unlock() - if !mgr.started { + if mgr.State() != box.StartStateStarted { return box.ErrStopped } mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} + mgr.mgrMx.Lock() + defer mgr.mgrMx.Unlock() for _, bx := range mgr.boxes { if rb, ok := bx.(box.Refresher); ok { rb.Refresh(ctx) } } return nil } + +// ReIndex data of the given zettel. +func (mgr *Manager) ReIndex(_ context.Context, zid id.Zid) error { + mgr.mgrLog.Debug().Msg("ReIndex") + if mgr.State() != box.StartStateStarted { + return box.ErrStopped + } + mgr.infos <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} + return nil +} // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { mgr.mgrLog.Debug().Msg("ReadStats") mgr.mgrMx.RLock() ADDED box/manager/mapstore/mapstore.go Index: box/manager/mapstore/mapstore.go ================================================================== --- box/manager/mapstore/mapstore.go +++ box/manager/mapstore/mapstore.go @@ -0,0 +1,717 @@ +//----------------------------------------------------------------------------- +// 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 +// under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package mapstore stored the index in main memory via a Go map. +package mapstore + +import ( + "context" + "fmt" + "io" + "sort" + "strings" + "sync" + + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/maps" + "zettelstore.de/z/box" + "zettelstore.de/z/box/manager/store" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +type zettelData struct { + meta *meta.Meta // a local copy of the metadata, without computed keys + dead id.Slice // list of dead references in this zettel + forward id.Slice // list of forward references in this zettel + backward id.Slice // list of zettel that reference with zettel + otherRefs map[string]bidiRefs + words []string // list of words of this zettel + urls []string // list of urls of this zettel +} + +type bidiRefs struct { + forward id.Slice + backward id.Slice +} + +type stringRefs map[string]id.Slice + +type memStore struct { + 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{ + 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.mxStats.Lock() + ms.updates++ + ms.mxStats.Unlock() + } +} + +func (ms *memStore) doEnrich(m *meta.Meta) bool { + ms.mx.RLock() + defer ms.mx.RUnlock() + zi, ok := ms.idx[m.Zid] + if !ok { + return false + } + var updated bool + if len(zi.dead) > 0 { + m.Set(api.KeyDead, zi.dead.String()) + updated = true + } + 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.otherRefs { + if len(refs.backward) > 0 { + m.Set(k, refs.backward.String()) + back = remRefs(back, refs.backward) + updated = true + } + } + if len(back) > 0 { + m.Set(api.KeyBack, back.String()) + 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. +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.CopySlice(refs) + } + if refs, ok := ms.urls[word]; ok { + result.CopySlice(refs) + } + zid, err := id.Parse(word) + if err != nil { + return result + } + zi, ok := ms.idx[zid] + if !ok { + return result + } + + addBackwardZids(result, zid, zi) + return result +} + +// SearchPrefix returns all zettel that have a word with the given prefix. +// The prefix must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchPrefix(prefix string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := ms.selectWithPred(prefix, strings.HasPrefix) + l := len(prefix) + if l > 14 { + 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) + } + } + return result +} + +// SearchSuffix returns all zettel that have a word with the given suffix. +// The suffix must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchSuffix(suffix string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := ms.selectWithPred(suffix, strings.HasSuffix) + l := len(suffix) + if l > 14 { + return result + } + val, err := id.ParseUint(suffix) + if err != nil { + return result + } + modulo := uint64(1) + for range l { + modulo *= 10 + } + for zid, zi := range ms.idx { + if uint64(zid)%modulo == val { + addBackwardZids(result, zid, zi) + } + } + return result +} + +// SearchContains returns all zettel that contains the given string. +// The string must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchContains(s string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := ms.selectWithPred(s, strings.Contains) + if len(s) > 14 { + return result + } + if _, err := id.ParseUint(s); err != nil { + return result + } + for zid, zi := range ms.idx { + if strings.Contains(zid.String(), s) { + addBackwardZids(result, zid, zi) + } + } + return result +} + +func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { + // Must only be called if ms.mx is read-locked! + result := id.NewSet() + for word, refs := range ms.words { + if !pred(word, s) { + continue + } + result.CopySlice(refs) + } + for u, refs := range ms.urls { + if !pred(u, s) { + continue + } + result.CopySlice(refs) + } + return result +} + +func addBackwardZids(result id.Set, zid id.Zid, zi *zettelData) { + // Must only be called if ms.mx is read-locked! + 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() { + switch meta.Type(p.Key) { + case meta.TypeID: + if zid, err := id.Parse(p.Value); err == nil { + back = remRef(back, zid) + } + case meta.TypeIDSet: + for _, val := range meta.ListFromValue(p.Value) { + if zid, err := id.Parse(val); err == nil { + back = remRef(back, zid) + } + } + } + } + return back +} + +func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + m := ms.makeMeta(zidx) + zi, ziExist := ms.idx[zidx.Zid] + if !ziExist || zi == nil { + zi = &zettelData{} + ziExist = false + } + + // Is this zettel an old dead reference mentioned in other zettel? + var toCheck id.Set + if refs, ok := ms.dead[zidx.Zid]; ok { + // These must be checked later again + toCheck = id.NewSet(refs...) + delete(ms.dead, zidx.Zid) + } + + zi.meta = m + ms.updateDeadReferences(zidx, zi) + ids := ms.updateForwardBackwardReferences(zidx, zi) + toCheck = toCheck.Copy(ids) + ids = ms.updateMetadataReferences(zidx, zi) + toCheck = toCheck.Copy(ids) + zi.words = updateStrings(zidx.Zid, ms.words, zi.words, zidx.GetWords()) + zi.urls = updateStrings(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) + + // Check if zi must be inserted into ms.idx + if !ziExist { + ms.idx[zidx.Zid] = zi + } + + return toCheck +} + +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, meta.SuffixKeyRole) +} + +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 { + ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) + } + for _, ref := range newRefs { + ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) + } +} + +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.getOrCreateEntry(ref) + bzi.backward = remRef(bzi.backward, zidx.Zid) + if bzi.meta == nil { + toCheck = toCheck.Add(ref) + } + } + for _, ref := range newRefs { + 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 *zettelData) id.Set { + // Must only be called if ms.mx is write-locked! + inverseRefs := zidx.GetInverseRefs() + for key, mr := range zi.otherRefs { + if _, ok := inverseRefs[key]; ok { + continue + } + ms.removeInverseMeta(zidx.Zid, key, mr.forward) + } + if zi.otherRefs == nil { + zi.otherRefs = make(map[string]bidiRefs) + } + var toCheck id.Set + for key, mrefs := range inverseRefs { + mr := zi.otherRefs[key] + newRefs, remRefs := refsDiff(mrefs, mr.forward) + mr.forward = mrefs + zi.otherRefs[key] = mr + + for _, ref := range newRefs { + bzi := ms.getOrCreateEntry(ref) + if bzi.otherRefs == nil { + bzi.otherRefs = make(map[string]bidiRefs) + } + bmr := bzi.otherRefs[key] + bmr.backward = addRef(bmr.backward, zidx.Zid) + bzi.otherRefs[key] = bmr + if bzi.meta == nil { + toCheck = toCheck.Add(ref) + } + } + ms.removeInverseMeta(zidx.Zid, key, remRefs) + } + return toCheck +} + +func updateStrings(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { + newWords, removeWords := next.Diff(prev) + for _, word := range newWords { + if refs, ok := srefs[word]; ok { + srefs[word] = addRef(refs, zid) + continue + } + srefs[word] = id.Slice{zid} + } + for _, word := range removeWords { + refs, ok := srefs[word] + if !ok { + continue + } + refs2 := remRef(refs, zid) + if len(refs2) == 0 { + delete(srefs, word) + continue + } + srefs[word] = refs2 + } + return next.Words() +} + +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 := &zettelData{} + ms.idx[zid] = zi + return zi +} + +func (ms *memStore) RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + + curZi, curFound := ms.idx[curZid] + _, newFound := ms.idx[newZid] + if !curFound || newFound { + return nil + } + newZi := &zettelData{ + meta: copyMeta(curZi.meta, newZid), + dead: ms.copyDeadReferences(curZi.dead), + forward: ms.copyForward(curZi.forward, newZid), + backward: nil, // will be done through tocheck + otherRefs: nil, // TODO: check if this will be done through toCheck + words: copyStrings(ms.words, curZi.words, newZid), + urls: copyStrings(ms.urls, curZi.urls, newZid), + } + + ms.idx[newZid] = newZi + toCheck := ms.doDeleteZettel(curZid) + toCheck = toCheck.CopySlice(ms.dead[newZid]) + delete(ms.dead, newZid) + toCheck = toCheck.Add(newZid) // should update otherRefs + return toCheck +} +func copyMeta(m *meta.Meta, newZid id.Zid) *meta.Meta { + result := m.Clone() + result.Zid = newZid + return result +} +func (ms *memStore) copyDeadReferences(curDead id.Slice) id.Slice { + // Must only be called if ms.mx is write-locked! + if l := len(curDead); l > 0 { + result := make(id.Slice, l) + for i, ref := range curDead { + result[i] = ref + ms.dead[ref] = addRef(ms.dead[ref], ref) + } + return result + } + return nil +} +func (ms *memStore) copyForward(curForward id.Slice, newZid id.Zid) id.Slice { + // Must only be called if ms.mx is write-locked! + if l := len(curForward); l > 0 { + result := make(id.Slice, l) + for i, ref := range curForward { + result[i] = ref + if fzi, found := ms.idx[ref]; found { + fzi.backward = addRef(fzi.backward, newZid) + } + } + return result + } + return nil +} +func copyStrings(msStringMap stringRefs, curStrings []string, newZid id.Zid) []string { + // Must only be called if ms.mx is write-locked! + if l := len(curStrings); l > 0 { + result := make([]string, l) + for i, s := range curStrings { + result[i] = s + msStringMap[s] = addRef(msStringMap[s], newZid) + } + return result + } + return nil +} + +func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + return ms.doDeleteZettel(zid) +} + +func (ms *memStore) doDeleteZettel(zid id.Zid) id.Set { + // Must only be called if ms.mx is write-locked! + zi, ok := ms.idx[zid] + if !ok { + return nil + } + + ms.deleteDeadSources(zid, zi) + toCheck := ms.deleteForwardBackward(zid, zi) + for key, mrefs := range zi.otherRefs { + ms.removeInverseMeta(zid, key, mrefs.forward) + } + deleteStrings(ms.words, zi.words, zid) + deleteStrings(ms.urls, zi.urls, zid) + delete(ms.idx, zid) + return toCheck +} + +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 { + ms.dead[ref] = drefs + } else { + delete(ms.dead, ref) + } + } + } +} + +func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelData) id.Set { + // Must only be called if ms.mx is write-locked! + for _, ref := range zi.forward { + if fzi, ok := ms.idx[ref]; ok { + fzi.backward = remRef(fzi.backward, zid) + } + } + var toCheck id.Set + for _, ref := range zi.backward { + if bzi, ok := ms.idx[ref]; ok { + bzi.forward = remRef(bzi.forward, zid) + toCheck = 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.otherRefs == nil { + continue + } + bmr, ok := bzi.otherRefs[key] + if !ok { + continue + } + bmr.backward = remRef(bmr.backward, zid) + if len(bmr.backward) > 0 || len(bmr.forward) > 0 { + bzi.otherRefs[key] = bmr + } else { + delete(bzi.otherRefs, key) + if len(bzi.otherRefs) == 0 { + bzi.otherRefs = nil + } + } + } +} + +func deleteStrings(msStringMap stringRefs, curStrings []string, zid id.Zid) { + // Must only be called if ms.mx is write-locked! + for _, word := range curStrings { + refs, ok := msStringMap[word] + if !ok { + continue + } + refs2 := remRef(refs, zid) + if len(refs2) == 0 { + delete(msStringMap, word) + continue + } + msStringMap[word] = refs2 + } +} + +func (ms *memStore) ReadStats(st *store.Stats) { + ms.mx.RLock() + st.Zettel = len(ms.idx) + 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() + + io.WriteString(w, "=== Dump\n") + ms.dumpIndex(w) + ms.dumpDead(w) + dumpStringRefs(w, "Words", "", "", ms.words) + dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) +} + +func (ms *memStore) dumpIndex(w io.Writer) { + if len(ms.idx) == 0 { + return + } + io.WriteString(w, "==== Zettel Index\n") + zids := make(id.Slice, 0, len(ms.idx)) + for id := range ms.idx { + zids = append(zids, id) + } + zids.Sort() + for _, id := range zids { + fmt.Fprintln(w, "=====", id) + zi := ms.idx[id] + 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.otherRefs { + fmt.Fprintln(w, "* Meta", k) + dumpZids(w, "** Forward:", fb.forward) + dumpZids(w, "** Backward:", fb.backward) + } + dumpStrings(w, "* Words", "", "", zi.words) + dumpStrings(w, "* URLs", "[[", "]]", zi.urls) + } +} + +func (ms *memStore) dumpDead(w io.Writer) { + if len(ms.dead) == 0 { + return + } + fmt.Fprintf(w, "==== Dead References\n") + zids := make(id.Slice, 0, len(ms.dead)) + for id := range ms.dead { + zids = append(zids, id) + } + zids.Sort() + for _, id := range zids { + fmt.Fprintln(w, ";", id) + fmt.Fprintln(w, ":", ms.dead[id]) + } +} + +func dumpZids(w io.Writer, prefix string, zids id.Slice) { + if len(zids) > 0 { + io.WriteString(w, prefix) + for _, zid := range zids { + io.WriteString(w, " ") + w.Write(zid.Bytes()) + } + fmt.Fprintln(w) + } +} + +func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { + if len(slice) > 0 { + sl := make([]string, len(slice)) + copy(sl, slice) + sort.Strings(sl) + fmt.Fprintln(w, title) + for _, s := range sl { + fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) + } + } + +} + +func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { + if len(srefs) == 0 { + return + } + fmt.Fprintln(w, "====", title) + for _, s := range maps.Keys(srefs) { + fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) + fmt.Fprintln(w, ":", srefs[s]) + } +} ADDED box/manager/mapstore/refs.go Index: box/manager/mapstore/refs.go ================================================================== --- box/manager/mapstore/refs.go +++ box/manager/mapstore/refs.go @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +// 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 +// under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern +//----------------------------------------------------------------------------- + +package mapstore + +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] + if rn == ro { + npos++ + opos++ + continue + } + if rn < ro { + newRefs = append(newRefs, rn) + npos++ + continue + } + remRefs = append(remRefs, ro) + opos++ + } + if npos < len(refsN) { + newRefs = append(newRefs, refsN[npos:]...) + } + if opos < len(refsO) { + remRefs = append(remRefs, refsO[opos:]...) + } + return newRefs, remRefs +} + +func addRef(refs id.Slice, ref id.Zid) id.Slice { + hi := len(refs) + for lo := 0; lo < hi; { + m := lo + (hi-lo)/2 + if r := refs[m]; r == ref { + return refs + } else if r < ref { + lo = m + 1 + } else { + hi = m + } + } + refs = slices.Insert(refs, hi, ref) + return refs +} + +func remRefs(refs, rem id.Slice) id.Slice { + if len(refs) == 0 || len(rem) == 0 { + return refs + } + result := make(id.Slice, 0, len(refs)) + rpos, dpos := 0, 0 + for rpos < len(refs) && dpos < len(rem) { + rr, dr := refs[rpos], rem[dpos] + if rr < dr { + result = append(result, rr) + rpos++ + continue + } + if dr < rr { + dpos++ + continue + } + rpos++ + dpos++ + } + if rpos < len(refs) { + result = append(result, refs[rpos:]...) + } + return result +} + +func remRef(refs id.Slice, ref id.Zid) id.Slice { + hi := len(refs) + for lo := 0; lo < hi; { + m := lo + (hi-lo)/2 + if r := refs[m]; r == ref { + copy(refs[m:], refs[m+1:]) + refs = refs[:len(refs)-1] + return refs + } else if r < ref { + lo = m + 1 + } else { + hi = m + } + } + return refs +} ADDED box/manager/mapstore/refs_test.go Index: box/manager/mapstore/refs_test.go ================================================================== --- box/manager/mapstore/refs_test.go +++ box/manager/mapstore/refs_test.go @@ -0,0 +1,140 @@ +//----------------------------------------------------------------------------- +// 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 +// under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern +//----------------------------------------------------------------------------- + +package mapstore + +import ( + "testing" + + "zettelstore.de/z/zettel/id" +) + +func assertRefs(t *testing.T, i int, got, exp id.Slice) { + t.Helper() + if got == nil && exp != nil { + t.Errorf("%d: got nil, but expected %v", i, exp) + return + } + if got != nil && exp == nil { + t.Errorf("%d: expected nil, but got %v", i, got) + return + } + if len(got) != len(exp) { + t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) + return + } + for p, n := range exp { + if got := got[p]; got != id.Zid(n) { + t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) + } + } +} + +func TestRefsDiff(t *testing.T) { + t.Parallel() + testcases := []struct { + in1, in2 id.Slice + exp1, exp2 id.Slice + }{ + {nil, nil, nil, nil}, + {id.Slice{1}, nil, id.Slice{1}, nil}, + {nil, id.Slice{1}, nil, id.Slice{1}}, + {id.Slice{1}, id.Slice{1}, nil, nil}, + {id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil}, + {id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}}, + {id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}}, + } + for i, tc := range testcases { + got1, got2 := refsDiff(tc.in1, tc.in2) + assertRefs(t, i, got1, tc.exp1) + assertRefs(t, i, got2, tc.exp2) + } +} + +func TestAddRef(t *testing.T) { + t.Parallel() + testcases := []struct { + ref id.Slice + zid uint + exp id.Slice + }{ + {nil, 5, id.Slice{5}}, + {id.Slice{1}, 5, id.Slice{1, 5}}, + {id.Slice{10}, 5, id.Slice{5, 10}}, + {id.Slice{5}, 5, id.Slice{5}}, + {id.Slice{1, 10}, 5, id.Slice{1, 5, 10}}, + {id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}}, + } + for i, tc := range testcases { + got := addRef(tc.ref, id.Zid(tc.zid)) + assertRefs(t, i, got, tc.exp) + } +} + +func TestRemRefs(t *testing.T) { + t.Parallel() + testcases := []struct { + in1, in2 id.Slice + exp id.Slice + }{ + {nil, nil, nil}, + {nil, id.Slice{}, nil}, + {id.Slice{}, nil, id.Slice{}}, + {id.Slice{}, id.Slice{}, id.Slice{}}, + {id.Slice{1}, id.Slice{5}, id.Slice{1}}, + {id.Slice{10}, id.Slice{5}, id.Slice{10}}, + {id.Slice{1, 5}, id.Slice{5}, id.Slice{1}}, + {id.Slice{5, 10}, id.Slice{5}, id.Slice{10}}, + {id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}}, + {id.Slice{1}, id.Slice{2, 5}, id.Slice{1}}, + {id.Slice{10}, id.Slice{2, 5}, id.Slice{10}}, + {id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}}, + {id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}}, + {id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}}, + {id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}}, + {id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}}, + {id.Slice{1}, id.Slice{5, 9}, id.Slice{1}}, + {id.Slice{10}, id.Slice{5, 9}, id.Slice{10}}, + {id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}}, + {id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}}, + {id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}}, + {id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}}, + {id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}}, + } + for i, tc := range testcases { + got := remRefs(tc.in1, tc.in2) + assertRefs(t, i, got, tc.exp) + } +} + +func TestRemRef(t *testing.T) { + t.Parallel() + testcases := []struct { + ref id.Slice + zid uint + exp id.Slice + }{ + {nil, 5, nil}, + {id.Slice{}, 5, id.Slice{}}, + {id.Slice{5}, 5, id.Slice{}}, + {id.Slice{1}, 5, id.Slice{1}}, + {id.Slice{10}, 5, id.Slice{10}}, + {id.Slice{1, 5}, 5, id.Slice{1}}, + {id.Slice{5, 10}, 5, id.Slice{10}}, + {id.Slice{1, 5, 10}, 5, id.Slice{1, 10}}, + } + for i, tc := range testcases { + got := remRef(tc.ref, id.Zid(tc.zid)) + assertRefs(t, i, got, tc.exp) + } +} DELETED box/manager/memstore/memstore.go Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ box/manager/memstore/memstore.go @@ -1,598 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 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 memstore stored the index in main memory. -package memstore - -import ( - "context" - "fmt" - "io" - "sort" - "strings" - "sync" - - "zettelstore.de/c/api" - "zettelstore.de/c/maps" - "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" -) - -type metaRefs 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 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 - - // Stats - 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), - } -} - -func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) { - if ms.doEnrich(m) { - ms.mx.Lock() - ms.updates++ - ms.mx.Unlock() - } -} - -func (ms *memStore) doEnrich(m *meta.Meta) bool { - ms.mx.RLock() - defer ms.mx.RUnlock() - zi, ok := ms.idx[m.Zid] - if !ok { - return false - } - var updated bool - if len(zi.dead) > 0 { - m.Set(api.KeyDead, zi.dead.String()) - updated = true - } - back := removeOtherMetaRefs(m, zi.backward.Copy()) - 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 { - if len(refs.backward) > 0 { - m.Set(k, refs.backward.String()) - back = remRefs(back, refs.backward) - updated = true - } - } - 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. -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) - } - if refs, ok := ms.urls[word]; ok { - result.AddSlice(refs) - } - zid, err := id.Parse(word) - if err != nil { - return result - } - zi, ok := ms.idx[zid] - if !ok { - return result - } - - addBackwardZids(result, zid, zi) - return result -} - -// SearchPrefix returns all zettel that have a word with the given prefix. -// The prefix must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchPrefix(prefix string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(prefix, strings.HasPrefix) - l := len(prefix) - if l > 14 { - return result - } - minZid, err := id.Parse(prefix + "00000000000000"[:13-l] + "1") - if err != nil { - return result - } - maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) - if err != nil { - return result - } - for zid, zi := range ms.idx { - if minZid <= zid && zid <= maxZid { - addBackwardZids(result, zid, zi) - } - } - return result -} - -// SearchSuffix returns all zettel that have a word with the given suffix. -// The suffix must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchSuffix(suffix string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(suffix, strings.HasSuffix) - l := len(suffix) - if l > 14 { - return result - } - val, err := id.ParseUint(suffix) - if err != nil { - return result - } - modulo := uint64(1) - for i := 0; i < l; i++ { - modulo *= 10 - } - for zid, zi := range ms.idx { - if uint64(zid)%modulo == val { - addBackwardZids(result, zid, zi) - } - } - return result -} - -// SearchContains returns all zettel that contains the given string. -// The string must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchContains(s string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(s, strings.Contains) - if len(s) > 14 { - return result - } - if _, err := id.ParseUint(s); err != nil { - return result - } - for zid, zi := range ms.idx { - if strings.Contains(zid.String(), s) { - addBackwardZids(result, zid, zi) - } - } - return result -} - -func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { - // Must only be called if ms.mx is read-locked! - result := id.NewSet() - for word, refs := range ms.words { - if !pred(word, s) { - continue - } - result.AddSlice(refs) - } - for u, refs := range ms.urls { - if !pred(u, s) { - continue - } - result.AddSlice(refs) - } - return result -} - -func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) { - // 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) - } -} - -func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { - for _, p := range m.PairsRest() { - switch meta.Type(p.Key) { - case meta.TypeID: - if zid, err := id.Parse(p.Value); err == nil { - back = remRef(back, zid) - } - case meta.TypeIDSet: - for _, val := range meta.ListFromValue(p.Value) { - if zid, err := id.Parse(val); err == nil { - back = remRef(back, zid) - } - } - } - } - return back -} - -func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { - ms.mx.Lock() - defer ms.mx.Unlock() - zi, ziExist := ms.idx[zidx.Zid] - if !ziExist || zi == nil { - zi = &zettelIndex{} - ziExist = false - } - - // Is this zettel an old dead reference mentioned in other zettel? - var toCheck id.Set - if refs, ok := ms.dead[zidx.Zid]; ok { - // These must be checked later again - toCheck = id.NewSet(refs...) - delete(ms.dead, zidx.Zid) - } - - ms.updateDeadReferences(zidx, zi) - ms.updateForwardBackwardReferences(zidx, zi) - ms.updateMetadataReferences(zidx, zi) - 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() { - ms.idx[zidx.Zid] = zi - } - - return toCheck -} - -func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) { - // 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 { - ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) - } - for _, ref := range newRefs { - ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) - } -} - -func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) { - // Must only be called if ms.mx is write-locked! - brefs := zidx.GetBackRefs() - newRefs, remRefs := refsDiff(brefs, zi.forward) - zi.forward = brefs - for _, ref := range remRefs { - bzi := ms.getEntry(ref) - bzi.backward = remRef(bzi.backward, zidx.Zid) - } - for _, ref := range newRefs { - bzi := ms.getEntry(ref) - bzi.backward = addRef(bzi.backward, zidx.Zid) - } -} - -func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) { - // Must only be called if ms.mx is write-locked! - metarefs := zidx.GetMetaRefs() - for key, mr := range zi.meta { - if _, ok := metarefs[key]; ok { - continue - } - ms.removeInverseMeta(zidx.Zid, key, mr.forward) - } - if zi.meta == nil { - zi.meta = make(map[string]metaRefs) - } - for key, mrefs := range metarefs { - mr := zi.meta[key] - newRefs, remRefs := refsDiff(mrefs, mr.forward) - mr.forward = mrefs - zi.meta[key] = mr - - for _, ref := range newRefs { - bzi := ms.getEntry(ref) - if bzi.meta == nil { - bzi.meta = make(map[string]metaRefs) - } - bmr := bzi.meta[key] - bmr.backward = addRef(bmr.backward, zidx.Zid) - bzi.meta[key] = bmr - } - ms.removeInverseMeta(zidx.Zid, key, remRefs) - } -} - -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 - } - srefs[word] = id.Slice{zid} - } - for _, word := range removeWords { - refs, ok := srefs[word] - if !ok { - continue - } - refs2 := remRef(refs, zid) - if len(refs2) == 0 { - delete(srefs, word) - continue - } - 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 { - // Must only be called if ms.mx is write-locked! - if zi, ok := ms.idx[zid]; ok { - return zi - } - zi := &zettelIndex{} - ms.idx[zid] = zi - return zi -} - -func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set { - ms.mx.Lock() - defer ms.mx.Unlock() - - zi, ok := ms.idx[zid] - if !ok { - return nil - } - - ms.deleteDeadSources(zid, zi) - toCheck := ms.deleteForwardBackward(zid, zi) - if len(zi.meta) > 0 { - for key, mrefs := range zi.meta { - 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) { - // 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 { - ms.dead[ref] = drefs - } else { - delete(ms.dead, ref) - } - } - } -} - -func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) 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) - } - } - for _, ref := range zi.backward { - if bzi, ok := ms.idx[ref]; ok { - bzi.forward = remRef(bzi.forward, zid) - if toCheck == nil { - toCheck = id.NewSet() - } - toCheck.Zid(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 { - continue - } - bmr, ok := bzi.meta[key] - if !ok { - continue - } - bmr.backward = remRef(bmr.backward, zid) - if len(bmr.backward) > 0 || len(bmr.forward) > 0 { - bzi.meta[key] = bmr - } else { - delete(bzi.meta, key) - if len(bzi.meta) == 0 { - bzi.meta = nil - } - } - } -} - -func (ms *memStore) deleteWords(zid id.Zid, words []string) { - // Must only be called if ms.mx is write-locked! - for _, word := range words { - refs, ok := ms.words[word] - if !ok { - continue - } - refs2 := remRef(refs, zid) - if len(refs2) == 0 { - delete(ms.words, word) - continue - } - ms.words[word] = refs2 - } -} - -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() -} - -func (ms *memStore) Dump(w io.Writer) { - ms.mx.RLock() - defer ms.mx.RUnlock() - - io.WriteString(w, "=== Dump\n") - ms.dumpIndex(w) - ms.dumpDead(w) - dumpStringRefs(w, "Words", "", "", ms.words) - dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) -} - -func (ms *memStore) dumpIndex(w io.Writer) { - if len(ms.idx) == 0 { - return - } - io.WriteString(w, "==== Zettel Index\n") - zids := make(id.Slice, 0, len(ms.idx)) - for id := range ms.idx { - zids = append(zids, id) - } - zids.Sort() - for _, id := range zids { - fmt.Fprintln(w, "=====", id) - zi := ms.idx[id] - 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 { - fmt.Fprintln(w, "* Meta", k) - dumpZids(w, "** Forward:", fb.forward) - dumpZids(w, "** Backward:", fb.backward) - } - dumpStrings(w, "* Words", "", "", zi.words) - dumpStrings(w, "* URLs", "[[", "]]", zi.urls) - } -} - -func (ms *memStore) dumpDead(w io.Writer) { - if len(ms.dead) == 0 { - return - } - fmt.Fprintf(w, "==== Dead References\n") - zids := make(id.Slice, 0, len(ms.dead)) - for id := range ms.dead { - zids = append(zids, id) - } - zids.Sort() - for _, id := range zids { - fmt.Fprintln(w, ";", id) - fmt.Fprintln(w, ":", ms.dead[id]) - } -} - -func dumpZids(w io.Writer, prefix string, zids id.Slice) { - if len(zids) > 0 { - io.WriteString(w, prefix) - for _, zid := range zids { - io.WriteString(w, " ") - w.Write(zid.Bytes()) - } - fmt.Fprintln(w) - } -} - -func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { - if len(slice) > 0 { - sl := make([]string, len(slice)) - copy(sl, slice) - sort.Strings(sl) - fmt.Fprintln(w, title) - for _, s := range sl { - fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) - } - } - -} - -func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { - if len(srefs) == 0 { - return - } - fmt.Fprintln(w, "====", title) - for _, s := range maps.Keys(srefs) { - fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) - fmt.Fprintln(w, ":", srefs[s]) - } -} DELETED box/manager/memstore/refs.go Index: box/manager/memstore/refs.go ================================================================== --- box/manager/memstore/refs.go +++ box/manager/memstore/refs.go @@ -1,100 +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. -//----------------------------------------------------------------------------- - -package memstore - -import "zettelstore.de/z/domain/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] - if rn == ro { - npos++ - opos++ - continue - } - if rn < ro { - newRefs = append(newRefs, rn) - npos++ - continue - } - remRefs = append(remRefs, ro) - opos++ - } - if npos < len(refsN) { - newRefs = append(newRefs, refsN[npos:]...) - } - if opos < len(refsO) { - remRefs = append(remRefs, refsO[opos:]...) - } - return newRefs, remRefs -} - -func addRef(refs id.Slice, ref id.Zid) id.Slice { - hi := len(refs) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if r := refs[m]; r == ref { - return refs - } else if r < ref { - lo = m + 1 - } else { - hi = m - } - } - refs = append(refs, id.Invalid) - copy(refs[hi+1:], refs[hi:]) - refs[hi] = ref - return refs -} - -func remRefs(refs, rem id.Slice) id.Slice { - if len(refs) == 0 || len(rem) == 0 { - return refs - } - result := make(id.Slice, 0, len(refs)) - rpos, dpos := 0, 0 - for rpos < len(refs) && dpos < len(rem) { - rr, dr := refs[rpos], rem[dpos] - if rr < dr { - result = append(result, rr) - rpos++ - continue - } - if dr < rr { - dpos++ - continue - } - rpos++ - dpos++ - } - if rpos < len(refs) { - result = append(result, refs[rpos:]...) - } - return result -} - -func remRef(refs id.Slice, ref id.Zid) id.Slice { - hi := len(refs) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if r := refs[m]; r == ref { - copy(refs[m:], refs[m+1:]) - refs = refs[:len(refs)-1] - return refs - } else if r < ref { - lo = m + 1 - } else { - hi = m - } - } - return refs -} DELETED box/manager/memstore/refs_test.go Index: box/manager/memstore/refs_test.go ================================================================== --- box/manager/memstore/refs_test.go +++ box/manager/memstore/refs_test.go @@ -1,137 +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. -//----------------------------------------------------------------------------- - -package memstore - -import ( - "testing" - - "zettelstore.de/z/domain/id" -) - -func assertRefs(t *testing.T, i int, got, exp id.Slice) { - t.Helper() - if got == nil && exp != nil { - t.Errorf("%d: got nil, but expected %v", i, exp) - return - } - if got != nil && exp == nil { - t.Errorf("%d: expected nil, but got %v", i, got) - return - } - if len(got) != len(exp) { - t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) - return - } - for p, n := range exp { - if got := got[p]; got != id.Zid(n) { - t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) - } - } -} - -func TestRefsDiff(t *testing.T) { - t.Parallel() - testcases := []struct { - in1, in2 id.Slice - exp1, exp2 id.Slice - }{ - {nil, nil, nil, nil}, - {id.Slice{1}, nil, id.Slice{1}, nil}, - {nil, id.Slice{1}, nil, id.Slice{1}}, - {id.Slice{1}, id.Slice{1}, nil, nil}, - {id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil}, - {id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}}, - {id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}}, - } - for i, tc := range testcases { - got1, got2 := refsDiff(tc.in1, tc.in2) - assertRefs(t, i, got1, tc.exp1) - assertRefs(t, i, got2, tc.exp2) - } -} - -func TestAddRef(t *testing.T) { - t.Parallel() - testcases := []struct { - ref id.Slice - zid uint - exp id.Slice - }{ - {nil, 5, id.Slice{5}}, - {id.Slice{1}, 5, id.Slice{1, 5}}, - {id.Slice{10}, 5, id.Slice{5, 10}}, - {id.Slice{5}, 5, id.Slice{5}}, - {id.Slice{1, 10}, 5, id.Slice{1, 5, 10}}, - {id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}}, - } - for i, tc := range testcases { - got := addRef(tc.ref, id.Zid(tc.zid)) - assertRefs(t, i, got, tc.exp) - } -} - -func TestRemRefs(t *testing.T) { - t.Parallel() - testcases := []struct { - in1, in2 id.Slice - exp id.Slice - }{ - {nil, nil, nil}, - {nil, id.Slice{}, nil}, - {id.Slice{}, nil, id.Slice{}}, - {id.Slice{}, id.Slice{}, id.Slice{}}, - {id.Slice{1}, id.Slice{5}, id.Slice{1}}, - {id.Slice{10}, id.Slice{5}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{5}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{5}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}}, - {id.Slice{1}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}}, - {id.Slice{1}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}}, - } - for i, tc := range testcases { - got := remRefs(tc.in1, tc.in2) - assertRefs(t, i, got, tc.exp) - } -} - -func TestRemRef(t *testing.T) { - t.Parallel() - testcases := []struct { - ref id.Slice - zid uint - exp id.Slice - }{ - {nil, 5, nil}, - {id.Slice{}, 5, id.Slice{}}, - {id.Slice{5}, 5, id.Slice{}}, - {id.Slice{1}, 5, id.Slice{1}}, - {id.Slice{10}, 5, id.Slice{10}}, - {id.Slice{1, 5}, 5, id.Slice{1}}, - {id.Slice{5, 10}, 5, id.Slice{10}}, - {id.Slice{1, 5, 10}, 5, id.Slice{1, 10}}, - } - for i, tc := range testcases { - got := remRef(tc.ref, id.Zid(tc.zid)) - assertRefs(t, i, got, tc.exp) - } -} Index: box/manager/store/store.go ================================================================== --- box/manager/store/store.go +++ box/manager/store/store.go @@ -1,25 +1,28 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store import ( "context" "io" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" "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. @@ -38,16 +41,23 @@ // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { 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. // Returns set of zettel identifier that must also be checked for changes. UpdateReferences(context.Context, *ZettelIndex) id.Set + + // RenameZettel changes all references of current zettel identifier to new + // zettel identifier. + RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set // DeleteZettel removes index data for given zettel. // Returns set of zettel identifier that must also be checked for changes. DeleteZettel(context.Context, id.Zid) id.Set Index: box/manager/store/wordset.go ================================================================== --- box/manager/store/wordset.go +++ box/manager/store/wordset.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store // WordSet contains the set of all words, with the count of their occurrences. Index: box/manager/store/wordset_test.go ================================================================== --- box/manager/store/wordset_test.go +++ box/manager/store/wordset_test.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store_test import ( Index: box/manager/store/zettel.go ================================================================== --- box/manager/store/zettel.go +++ box/manager/store/zettel.go @@ -1,87 +1,90 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- 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 +91,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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package membox stores zettel volatile in main memory. package membox @@ -16,16 +19,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/query" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" ) func init() { manager.Register( "mem", @@ -46,27 +48,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 +92,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,36 +113,32 @@ meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(box.OnZettel, 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) 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() @@ -156,11 +163,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 +178,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 +198,11 @@ zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(box.OnZettel, 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 +210,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.OnZettel, curZid) - mb.notifyChanged(box.OnZettel, 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 +243,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.OnZettel, 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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -17,68 +20,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/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 +100,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 { @@ -177,11 +192,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,71 +225,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.OnZettel, zid) - } - case Delete: - ds.mx.Lock() - zid := ds.onDeleteFileEvent(ds.entries, ev.Name) - ds.mx.Unlock() - if zid != id.Invalid { - ds.notifyChange(box.OnZettel, zid) - } - 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.Error().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.Error().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 { @@ -283,29 +314,29 @@ return zids } func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) { for _, zid := range zids { - ds.notifyChange(box.OnZettel, 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.OnZettel, 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.OnZettel, zid) + ds.notifyChange(zid) } } var validFileName = regexp.MustCompile(`^(\d{14})`) @@ -343,13 +374,13 @@ return id.Invalid } entry := fetchdirEntry(entries, zid) dupName1, dupName2 := ds.updateEntry(entry, name) if dupName1 != "" { - ds.log.Warn().Str("name", dupName1).Msg("Duplicate content (is ignored)") + ds.log.Info().Str("name", dupName1).Msg("Duplicate content (is ignored)") if dupName2 != "" { - ds.log.Warn().Str("name", dupName2).Msg("Duplicate content (is ignored)") + ds.log.Info().Str("name", dupName2).Msg("Duplicate content (is ignored)") } return id.Invalid } return zid } @@ -550,12 +581,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) { @@ -569,11 +603,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,28 +1,31 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- 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/pikchr" // Allow to use pikchr 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 @@ -47,15 +50,20 @@ } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats - api.ValueSyntaxZmk, "pikchr", "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,25 +1,28 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "path/filepath" - "zettelstore.de/c/api" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "t73f.de/r/zsc/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 +49,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 == "" { @@ -57,11 +60,11 @@ e.MetaName = e.calcBaseName(contentName) } return } - syntax := m.GetDefault(api.KeySyntax, "") + syntax := m.GetDefault(api.KeySyntax, meta.DefaultSyntax) ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax) metaName := e.MetaName eimc := extIsMetaAndContent(ext) if eimc { if metaName != "" { @@ -78,11 +81,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 +100,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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -53,18 +56,17 @@ Str("path", absPath).Err(err). Msg("Unable to access Zettel directory and its parent directory") watcher.Close() return nil, err } - log.Warn(). - Str("parentDir", absParentDir).Err(errParent). + log.Info().Str("parentDir", absParentDir).Err(errParent). Msg("Parent of Zettel directory cannot be supervised") - log.Warn().Str("path", absPath). + log.Info().Str("path", absPath). Msg("Zettelstore might not detect a deletion or movement of the Zettel directory") } else if err != nil { // Not a problem, if container is not available. It might become available later. - log.Warn().Err(err).Str("path", absPath).Msg("Zettel directory not available") + log.Info().Err(err).Str("path", absPath).Msg("Zettel directory currently not available") } fsdn := &fsdirNotifier{ log: log, events: make(chan Event), @@ -92,109 +94,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 } func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool { - const deleteFsDirOps = fsnotify.Remove | fsnotify.Rename - - if ev.Op&deleteFsDirOps != 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 } - } else if ev.Op&fsnotify.Create != 0 { + return true + } + + 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") + fsdn.log.Error().Err(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) - } else { - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("Directory processed") } + + 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 { - const deleteFsFileOps = fsnotify.Remove - const updateFsFileOps = fsnotify.Create | fsnotify.Write | fsnotify.Rename - - if ev.Op&updateFsFileOps != 0 { + 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") - select { - case fsdn.events <- Event{Op: Update, Name: filepath.Base(ev.Name)}: - case <-fsdn.done: - return false - } - } else if ev.Op&deleteFsFileOps != 0 { - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted") - select { - case fsdn.events <- Event{Op: Delete, Name: filepath.Base(ev.Name)}: - case <-fsdn.done: - return false - } - } else { - fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File processed") + 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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -15,15 +18,10 @@ "os" "zettelstore.de/z/logger" ) -// MakeMetaFilename builds the name of the file containing metadata. -func MakeMetaFilename(basename string) string { - return basename //+ ".meta" -} - // EntryFetcher return a list of (file) names of an directory. type EntryFetcher interface { Fetch() ([]string, error) } Index: box/notify/notify.go ================================================================== --- box/notify/notify.go +++ box/notify/notify.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package notify provides some notification services to be used by box services. package notify @@ -33,19 +36,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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -42,20 +45,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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( @@ -15,17 +18,17 @@ "flag" "fmt" "io" "os" - "zettelstore.de/c/api" - "zettelstore.de/z/domain" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/input" "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) { @@ -34,18 +37,18 @@ if m == nil { return 2, err } z := parser.ParseZettel( context.Background(), - domain.Zettel{ + 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.DefaultSyntax), nil, ) - encdr := encoder.Create(api.Encoder(enc)) + encdr := encoder.Create(api.Encoder(enc), &encoder.CreateParameter{Lang: m.GetDefault(api.KeyLang, api.ValueLangEN)}) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) return 2, nil } _, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata) Index: cmd/cmd_password.go ================================================================== --- cmd/cmd_password.go +++ cmd/cmd_password.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( @@ -15,13 +18,13 @@ "fmt" "os" "golang.org/x/term" - "zettelstore.de/c/api" + "t73f.de/r/zsc/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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( @@ -16,16 +19,16 @@ "net/http" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" - "zettelstore.de/z/domain/meta" "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) { @@ -50,97 +53,81 @@ func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) kern := kernel.Main webLog := kern.GetLogger(kernel.WebService) + + 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) + ucQuery := usecase.NewQuery(protectedBoxManager) + ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery) + ucQuery.SetEvaluate(&ucEvaluate) + ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery) + ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery) + ucListSyntax := usecase.NewListSyntax(protectedBoxManager) + ucListRoles := usecase.NewListRoles(protectedBoxManager) + ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager) + ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager) + ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager) + ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager) + ucReIndex := usecase.NewReIndex(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) - - var getUser getUserImpl - logAuth := kern.GetLogger(kernel.AuthService) - logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) - ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, authManager, boxManager) - ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager) - ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager) - ucGetMeta := usecase.NewGetMeta(protectedBoxManager) - ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) - ucGetZettel := usecase.NewGetZettel(protectedBoxManager) - ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) - ucListMeta := usecase.NewListMeta(protectedBoxManager) - ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta, ucListMeta) - ucListSyntax := usecase.NewListSyntax(protectedBoxManager) - ucListRoles := usecase.NewListRoles(protectedBoxManager) - ucListTags := usecase.NewListTags(protectedBoxManager) - ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig) - ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager) - ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager) - ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager) - ucUnlinkedRefs := usecase.NewUnlinkedReferences(protectedBoxManager, rtConfig) - ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager) - ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) + 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, &ucEvaluate)) - webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler( - &ucEvaluate, ucGetMeta)) + webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex)) + 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.AddListRoute('q', server.MethodGet, a.MakeQueryHandler(ucListMeta)) - 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, &ucTagZettel, &ucRoleZettel, &ucReIndex)) + 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() { Index: cmd/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -1,21 +1,24 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( "flag" - "zettelstore.de/c/maps" + "t73f.de/r/zsc/maps" "zettelstore.de/z/logger" ) // Command stores information about commands / sub-commands. type Command struct { @@ -46,11 +49,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 Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -1,20 +1,22 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( "crypto/sha256" - "errors" "flag" "fmt" "net" "net/url" "os" @@ -21,23 +23,23 @@ "runtime/debug" "strconv" "strings" "time" - "zettelstore.de/c/api" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/input" "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() { @@ -87,19 +89,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) @@ -107,31 +109,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 { @@ -147,20 +145,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) { @@ -174,10 +163,11 @@ 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" @@ -187,76 +177,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") + err = setConfigValue(err, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel") for i := 1; ; i++ { key := kernel.BoxURIs + strconv.Itoa(i) val, found := cfg.Get(key) if !found { break } - ok = setConfigValue(ok, kernel.BoxService, key, val) + err = setConfigValue(err, kernel.BoxService, key, val) } + + err = setConfigValue(err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML)) - ok = setConfigValue( - ok, kernel.WebService, kernel.WebListenAddress, - cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) + err = setConfigValue(err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) if val, found := cfg.Get(keyBaseURL); found { - ok = setConfigValue(ok, kernel.WebService, kernel.WebBaseURL, val) + err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val) } if val, found := cfg.Get(keyURLPrefix); found { - ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, val) + err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val) } - ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) - ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) + 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) + err = setConfigValue(err, 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, "")) + 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 { - ok = setConfigValue(ok, kernel.WebService, kernel.WebAssetDir, val) - } - - 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.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().Error().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 { @@ -266,13 +252,13 @@ 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 @@ -305,11 +291,11 @@ ) 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) @@ -317,11 +303,11 @@ } // runSimple is called, when the user just starts the software via a double click // 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) Index: cmd/register.go ================================================================== --- cmd/register.go +++ cmd/register.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cmd provides command generic functions. package cmd @@ -17,17 +20,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/pikchr" // Allow to use PIC/Pikchr 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,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package main is the starting point for the zettelstore command. package main Index: collect/collect.go ================================================================== --- collect/collect.go +++ collect/collect.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect Index: collect/collect_test.go ================================================================== --- collect/collect_test.go +++ collect/collect_test.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package collect_test provides some unit test for collectors. package collect_test Index: collect/order.go ================================================================== --- collect/order.go +++ collect/order.go @@ -1,13 +1,16 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect DELETED collect/split.go Index: collect/split.go ================================================================== --- collect/split.go +++ collect/split.go @@ -1,50 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 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 collect provides functions to collect items from a syntax tree. -package collect - -import ( - "zettelstore.de/z/ast" - "zettelstore.de/z/strfun" -) - -// DivideReferences divides the given list of rederences into zettel, local, and external References. -func DivideReferences(all []*ast.Reference) (zettel, local, external []*ast.Reference) { - if len(all) == 0 { - return nil, nil, nil - } - - mapZettel := make(strfun.Set) - mapLocal := make(strfun.Set) - mapExternal := make(strfun.Set) - for _, ref := range all { - if ref.State == ast.RefStateSelf { - continue - } - if ref.IsZettel() { - zettel = appendRefToList(zettel, mapZettel, ref) - } else if ref.IsExternal() { - external = appendRefToList(external, mapExternal, ref) - } else { - local = appendRefToList(local, mapLocal, ref) - } - } - return zettel, local, external -} - -func appendRefToList(reflist []*ast.Reference, refSet strfun.Set, ref *ast.Reference) []*ast.Reference { - s := ref.String() - if !refSet.Has(s) { - reflist = append(reflist, ref) - refSet.Set(s) - } - return reflist -} Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -1,30 +1,36 @@ //----------------------------------------------------------------------------- -// 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 // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package config provides functions to retrieve runtime configuration data. package config import ( "context" - "zettelstore.de/z/domain/id" - "zettelstore.de/z/domain/meta" + "zettelstore.de/z/zettel/meta" ) // Key values that are supported by Config.Get const ( - KeyFooterHTML = "footer-html" + KeyFooterZettel = "footer-zettel" + KeyHomeZettel = "home-zettel" + KeyShowBackLinks = "show-back-links" + KeyShowFolgeLinks = "show-folge-links" + KeyShowSubordinateLinks = "show-subordinate-links" + KeyShowSuccessorLinks = "show-successor-links" // api.KeyLang - KeyMarkerExternal = "marker-external" ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig @@ -37,14 +43,14 @@ 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 @@ -61,5 +67,43 @@ GetExpertMode() bool // GetVisibility returns the visibility value of the metadata. GetVisibility(m *meta.Meta) meta.Visibility } + +// 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 false +} Index: docs/development/00010000000000.zettel ================================================================== --- docs/development/00010000000000.zettel +++ docs/development/00010000000000.zettel @@ -1,8 +1,11 @@ id: 00010000000000 title: Developments Notes role: zettel syntax: zmk -modified: 20210916194954 +created: 00010101000000 +modified: 20231218182020 * [[Required Software|20210916193200]] +* [[Fuzzing tests|20221026184300]] * [[Checklist for Release|20210916194900]] +* [[Development tools|20231218181900]] 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: 20231213194509 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/devtools/devtools.go``. + +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,23 +1,24 @@ id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk -modified: 20220309105459 +created: 20210916194900 +modified: 20231213194631 # 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``). +#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``). # All internal tests must succeed: -#* ``go run tools/build.go relcheck`` (alternatively: ``make relcheck``). +#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``). # The API tests must succeed on every development platform: -#* ``go run tools/build.go testapi`` (alternatively: ``make api``). +#* ``go run tools/testapi/testapi.go`` (alternatively: ``make api``). # Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual: #* ``go run -race cmd/zettelstore/main.go run -d docs/manual`` #* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt`` #* Check all ""Error: 404 Not Found"" #* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''. @@ -41,18 +42,18 @@ # 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''. Otherwise client will not be able to import ''zettelkasten.de/z''. # Clean up your Go workspace: -#* ``go run tools/build.go clean`` (alternatively: ``make clean``). +#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``). # Create the release: -#* ``go run tools/build.go release`` (alternatively: ``make release``). +#* ``go run tools/build/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 +``` ADDED docs/development/20231218181900.zettel Index: docs/development/20231218181900.zettel ================================================================== --- docs/development/20231218181900.zettel +++ docs/development/20231218181900.zettel @@ -0,0 +1,117 @@ +id: 20231218181900 +title: Development tools +role: zettel +syntax: zmk +created: 20231218181956 +modified: 20231218184500 + +The source code contains some tools to assist the development of Zettelstore. +These are located in the ''tools'' directory. + +Most tool support the generic option ``-v``, which log internal activities. + +Some of the tools can be called easier by using ``make``, that reads in a provided ''Makefile''. + +=== Check +The ""check"" tool automates some testing activities. +It is called via the command line: +``` +# go run tools/check/check.go +``` +There is an additional option ``-r`` to check in advance of a release. + +The following checks are executed: +* Execution of unit tests, like ``go test ./...`` +* Analyze the source code for general problems, as in ``go vet ./...`` +* Tries to find shadowed variable, via ``shadow ./...`` +* Performs some additional checks on the source code, via ``staticcheck ./...`` +* Checks the usage of function parameters and usage of return values, via ``unparam ./...``. + In case the option ''-r'' is set, the check includes exported functions and internal tests. +* In case option ''-r'' is set, the source code is checked against the vulnerability database, via ``govulncheck ./...`` + +Please note, that most of the tools above are not automatically installed in a standard Go distribution. +Use the command ""devtools"" to install them. + +=== Devtools +The following command installs all needed tools: +``` +# go run tooles/devtools/devtools.go +``` +It will also automatically update these tools. + +=== TestAPI +The following command will perform some high-level tests: +```sh +# go run tools/testapi/testapi.go +``` +Basically, a Zettelstore will be started and then API calls will be made to simulate some typical activities with the Zettelstore. + +If a Zettelstore is already running on port 23123, this Zettelstore will be used instead. +Even if the API test should clean up later, some zettel might stay created if a test fails. +This feature is used, if you want to have more control on the running Zettelstore. +You should start it with the following command: +```sh +# go run -race cmd/zettelstore/main.go run -c testdata/testbox/19700101000000.zettel +``` +This allows you to debug failing API tests. + +=== HTMLlint +The following command will check the generated HTML code for validity: +```sh +# go run tools/htmllint/htmllint.go +``` +In addition, you might specify the URL od a running Zettelstore. +Otherwise ''http://localhost:23123'' is used. + +This command fetches first the list of all zettel. +This list is used to check the generated HTML code (''ZID'' is the paceholder for the zettel identification): + +* Check all zettel HTML encodings, via the path ''/z/ZID?enc=html&part=zettel'' +* Check all zettel web views, via the path ''/h/ZID'' +* The info page of all zettel is checked, via path ''/i/ZID'' +* A subset of max. 100 zettel will be checked for the validity of their edit page, via ''/e/ZID'' +* 10 random zettel are checked for a valid create form, via ''/c/ZID'' +* The zettel rename form will be checked for 100 zettel, via ''/b/ZID'' +* A maximum of 200 random zettel are checked for a valid delete dialog, via ''/d/ZID'' + +Depending on the selected Zettelstore, the command might take a long time. + +You can shorten the time, if you disable any zettel query in the footer. + +=== Build +The ""build"" tool allows to build the software, either for tests or for a release. + +The following command will create a Zettelstore executable for the architecture of the current computer: +```sh +# go tools/build/build.go build +``` +You will find the executable in the ''bin'' directory. + +A full release will be build in the directory ''releases'', containing ZIP files for the computer architectures ""Linux/amd64"", ""Linux/arm"", ""MacOS/arm64"", ""MacOS/amd64"", and ""Windows/amd64"". +In addition, the manual is also build as a ZIP file: +```sh +# go run tools/build/build.go release +``` + +If you just want the ZIP file with the manual, please use: +```sh +# go run tools/build/build.go manual +``` + +In case you want to check the version of the Zettelstore to be build, use: +```sh +# go run tools/build/build.go version +``` + +=== Clean +To remove the directories ''bin'' and ''releases'', as well as all cached Go libraries used by Zettelstore, execute: +```sh +# go run tools/clean/clean.go +``` + +Internally, the following commands are executed +```sh +# rm -rf bin releases +# go clean ./... +# go clean -cache -modcache -testcache +``` 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,13 @@ id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk -modified: 20220803183647 +created: 20210301190630 +modified: 20231125185455 +show-back-links: false * [[Introduction|00001001000000]] * [[Design goals|00001002000000]] * [[Installation|00001003000000]] * [[Configuration|00001004000000]] @@ -17,7 +19,9 @@ * [[API|00001012000000]] * [[Web user interface|00001014000000]] * [[Tips and Tricks|00001017000000]] * [[Troubleshooting|00001018000000]] * Frequently asked questions + +Version: {{00001000000001}}. Licensed under the EUPL-1.2-or-later. ADDED docs/manual/00001000000001.zettel Index: docs/manual/00001000000001.zettel ================================================================== --- docs/manual/00001000000001.zettel +++ docs/manual/00001000000001.zettel @@ -0,0 +1,8 @@ +id: 00001000000001 +title: Manual Version +role: configuration +syntax: zmk +created: 20231002142915 +modified: 20231002142948 + +To be set by build tool. 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 @@ -2,11 +2,11 @@ title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20220914183434 +modified: 20240220190138 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. @@ -66,22 +66,37 @@ ; [!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 ``