Index: .fossil-settings/ignore-glob ================================================================== --- .fossil-settings/ignore-glob +++ .fossil-settings/ignore-glob @@ -1,3 +1,2 @@ bin/* releases/* -parser/pikchr/*.out Index: Makefile ================================================================== --- Makefile +++ Makefile @@ -5,27 +5,27 @@ ## ## 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,12 +11,12 @@ 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 Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.13.0 +0.20.0-dev Index: ast/ast.go ================================================================== --- ast/ast.go +++ ast/ast.go @@ -4,10 +4,13 @@ // 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 Index: ast/block.go ================================================================== --- ast/block.go +++ ast/block.go @@ -4,15 +4,18 @@ // 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/client.fossil/attrs" +import "t73f.de/r/zsc/attrs" // Definition of Block nodes. // BlockSlice is a slice of BlockNodes. type BlockSlice []BlockNode Index: ast/inline.go ================================================================== --- ast/inline.go +++ ast/inline.go @@ -4,40 +4,28 @@ // 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/client.fossil/attrs" + "t73f.de/r/zsc/attrs" ) // Definitions of inline nodes. // InlineSlice is a list of BlockNodes. type InlineSlice []InlineNode func (*InlineSlice) inlineNode() { /* Just a marker */ } -// CreateInlineSliceFromWords makes a new inline list from words, -// that will be space-separated. -func CreateInlineSliceFromWords(words ...string) InlineSlice { - inl := make(InlineSlice, 0, 2*len(words)-1) - for i, word := range words { - if i > 0 { - inl = append(inl, &SpaceNode{Lexeme: " "}) - } - inl = append(inl, &TextNode{Text: word}) - } - return inl -} - // WalkChildren walks down to the list. func (is *InlineSlice) WalkChildren(v Visitor) { for _, in := range *is { Walk(v, in) } @@ -53,27 +41,10 @@ func (*TextNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*TextNode) WalkChildren(Visitor) { /* No children*/ } -// -------------------------------------------------------------------------- - -// SpaceNode tracks inter-word space characters. -type SpaceNode struct { - Lexeme string -} - -func (*SpaceNode) inlineNode() { /* Just a marker */ } - -// WalkChildren does nothing. -func (*SpaceNode) WalkChildren(Visitor) { /* No children*/ } - -// Count returns the number of space runes. -func (sn *SpaceNode) Count() int { - return utf8.RuneCountInString(sn.Lexeme) -} - // -------------------------------------------------------------------------- // BreakNode signals a new line that must / should be interpreted as a new line break. type BreakNode struct { Hard bool // Hard line break? @@ -193,18 +164,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 @@ -4,23 +4,27 @@ // 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" + "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 invalidReference(s) { return &Reference{URL: nil, Value: s, State: RefStateInvalid} Index: ast/ref_test.go ================================================================== --- ast/ref_test.go +++ ast/ref_test.go @@ -4,10 +4,13 @@ // 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 @@ -4,10 +4,13 @@ // 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 @@ -4,66 +4,69 @@ // 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/client.fossil/attrs" + "t73f.de/r/zsc/attrs" "zettelstore.de/z/ast" ) func BenchmarkWalk(b *testing.B) { root := ast.BlockSlice{ &ast.HeadingNode{ - Inlines: ast.CreateInlineSliceFromWords("A", "Simple", "Heading"), + Inlines: ast.InlineSlice{&ast.TextNode{Text: "A Simple Heading"}}, }, &ast.ParaNode{ - Inlines: ast.CreateInlineSliceFromWords("This", "is", "the", "introduction."), + Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is the introduction."}}, }, &ast.NestedListNode{ Kind: ast.NestedListUnordered, Items: []ast.ItemSlice{ []ast.ItemNode{ &ast.ParaNode{ - Inlines: ast.CreateInlineSliceFromWords("Item", "1"), + Inlines: ast.InlineSlice{&ast.TextNode{Text: "Item 1"}}, }, }, []ast.ItemNode{ &ast.ParaNode{ - Inlines: ast.CreateInlineSliceFromWords("Item", "2"), + Inlines: ast.InlineSlice{&ast.TextNode{Text: "Item 2"}}, }, }, }, }, &ast.ParaNode{ - Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "intermediate", "text."), + Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is some intermediate text."}}, }, ast.CreateParaNode( &ast.FormatNode{ Kind: ast.FormatEmph, Attrs: attrs.Attributes(map[string]string{ "": "class", "color": "green", }), - Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "emphasized", "text."), + Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is some emphasized text."}}, }, - &ast.SpaceNode{Lexeme: " "}, + &ast.TextNode{Text: " "}, &ast.LinkNode{ Ref: &ast.Reference{Value: "http://zettelstore.de"}, - Inlines: ast.CreateInlineSliceFromWords("URL", "text."), + Inlines: ast.InlineSlice{&ast.TextNode{Text: "URL text."}}, }, ), } v := benchVisitor{} b.ResetTimer() - for n := 0; n < b.N; n++ { + for range b.N { ast.Walk(&v, &root) } } type benchVisitor struct{} Index: auth/auth.go ================================================================== --- auth/auth.go +++ auth/auth.go @@ -4,10 +4,13 @@ // 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 @@ -90,14 +93,11 @@ CanRead(user, m *meta.Meta) bool // User is allowed to write zettel. CanWrite(user, oldMeta, newMeta *meta.Meta) bool - // User is allowed to rename zettel - CanRename(user, m *meta.Meta) bool - // User is allowed to delete zettel. CanDelete(user, m *meta.Meta) bool // User is allowed to refresh box data. CanRefresh(user *meta.Meta) bool } Index: auth/cred/cred.go ================================================================== --- auth/cred/cred.go +++ auth/cred/cred.go @@ -4,10 +4,13 @@ // 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 @@ -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() } Index: auth/impl/digest.go ================================================================== --- auth/impl/digest.go +++ auth/impl/digest.go @@ -4,10 +4,13 @@ // 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 ( @@ -14,26 +17,29 @@ "bytes" "crypto" "crypto/hmac" "encoding/base64" - "zettelstore.de/sx.fossil" - "zettelstore.de/sx.fossil/sxreader" + "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 - sx.Print(&buf, claim) + _, 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()) + _, err = digest.Write(buf.Bytes()) if err != nil { return nil, err } dig := digest.Sum(nil) encDig := make([]byte, encoding.EncodedLen(len(dig))) Index: auth/impl/impl.go ================================================================== --- auth/impl/impl.go +++ auth/impl/impl.go @@ -4,10 +4,13 @@ // 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,13 +18,13 @@ "errors" "hash/fnv" "io" "time" - "zettelstore.de/client.fossil/api" - "zettelstore.de/client.fossil/sexp" - "zettelstore.de/sx.fossil" + "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/kernel" @@ -120,11 +123,11 @@ return ErrMalformedToken } if auth.TokenKind(vals[0].(sx.Int64)) != k { return ErrOtherKind } - ident := vals[1].(sx.String) + ident := vals[1].(sx.String).GetValue() if ident == "" { return ErrNoIdent } issued := time.Unix(int64(vals[2].(sx.Int64)), 0) expires := time.Unix(int64(vals[3].(sx.Int64)), 0) @@ -135,11 +138,11 @@ zid := id.Zid(vals[4].(sx.Int64)) if !zid.IsValid() { return ErrNoZid } - tokenData.Ident = ident.String() + tokenData.Ident = string(ident) tokenData.Issued = issued tokenData.Now = now tokenData.Expires = expires tokenData.Zid = zid return nil Index: auth/policy/anon.go ================================================================== --- auth/policy/anon.go +++ auth/policy/anon.go @@ -4,10 +4,13 @@ // 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 ( @@ -31,14 +34,10 @@ func (ap *anonPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return ap.pre.CanWrite(user, oldMeta, newMeta) && ap.checkVisibility(oldMeta) } -func (ap *anonPolicy) CanRename(user, m *meta.Meta) bool { - return ap.pre.CanRename(user, m) && ap.checkVisibility(m) -} - func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool { return ap.pre.CanDelete(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) CanRefresh(user *meta.Meta) bool { Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -4,10 +4,13 @@ // 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 ( @@ -22,15 +25,11 @@ "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. @@ -77,11 +76,11 @@ 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) { +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) @@ -108,11 +107,11 @@ 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 oldZettel, err := pp.box.GetZettel(ctx, zid) if err != nil { return err @@ -121,26 +120,10 @@ return pp.box.UpdateZettel(ctx, zettel) } return box.NewErrNotAllowed("Write", user, zid) } -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 { - z, err := pp.box.GetZettel(ctx, curZid) - if err != nil { - return err - } - user := server.GetUser(ctx) - if pp.policy.CanRename(user, z.Meta) { - return pp.box.RenameZettel(ctx, curZid, newZid) - } - return box.NewErrNotAllowed("Rename", user, curZid) -} - 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 { @@ -159,6 +142,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 @@ -4,16 +4,19 @@ // 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/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/auth" "zettelstore.de/z/zettel/meta" ) type defaultPolicy struct { @@ -23,11 +26,10 @@ func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true } func (*defaultPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (d *defaultPolicy) CanWrite(user, oldMeta, _ *meta.Meta) bool { return d.canChange(user, oldMeta) } -func (d *defaultPolicy) CanRename(user, m *meta.Meta) bool { return d.canChange(user, m) } func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) } func (*defaultPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } func (d *defaultPolicy) canChange(user, m *meta.Meta) bool { Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -4,16 +4,19 @@ // 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/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/zettel/meta" ) @@ -110,20 +113,10 @@ return false } return o.userCanCreate(user, newMeta) } -func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool { - if user == nil || !o.pre.CanRename(user, m) { - return false - } - if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { - return res - } - return o.userIsOwner(user) -} - func (o *ownerPolicy) CanDelete(user, m *meta.Meta) bool { if user == nil || !o.pre.CanDelete(user, m) { return false } if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { Index: auth/policy/policy.go ================================================================== --- auth/policy/policy.go +++ auth/policy/policy.go @@ -4,10 +4,13 @@ // 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 @@ -55,16 +58,12 @@ func (p *prePolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return oldMeta != nil && newMeta != nil && oldMeta.Zid == newMeta.Zid && p.post.CanWrite(user, oldMeta, newMeta) } -func (p *prePolicy) CanRename(user, m *meta.Meta) bool { - return m != nil && p.post.CanRename(user, m) -} - func (p *prePolicy) CanDelete(user, m *meta.Meta) bool { return m != nil && p.post.CanDelete(user, m) } func (p *prePolicy) CanRefresh(user *meta.Meta) bool { return p.post.CanRefresh(user) } Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -4,19 +4,22 @@ // 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/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/auth" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -54,11 +57,10 @@ ts.readonly, ts.withAuth, ts.expert, ts.simple) t.Run(name, func(tt *testing.T) { testCreate(tt, pol, ts.withAuth, ts.readonly) testRead(tt, pol, ts.withAuth, ts.expert) testWrite(tt, pol, ts.withAuth, ts.readonly, ts.expert) - testRename(tt, pol, ts.withAuth, ts.readonly, ts.expert) testDelete(tt, pol, ts.withAuth, ts.readonly, ts.expert) testRefresh(tt, pol, ts.withAuth, ts.expert, ts.simple) }) } } @@ -390,98 +392,10 @@ } for _, tc := range testCases { t.Run("Write", func(tt *testing.T) { got := pol.CanWrite(tc.user, tc.old, tc.new) if tc.exp != got { - tt.Errorf("exp=%v, but got=%v", tc.exp, got) - } - }) - } -} - -func testRename(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { - t.Helper() - anonUser := newAnon() - creator := newCreator() - reader := newReader() - writer := newWriter() - owner := newOwner() - owner2 := newOwner2() - zettel := newZettel() - expertZettel := newExpertZettel() - roFalse := newRoFalseZettel() - roTrue := newRoTrueZettel() - roReader := newRoReaderZettel() - roWriter := newRoWriterZettel() - roOwner := newRoOwnerZettel() - notAuthNotReadonly := !withAuth && !readonly - testCases := []struct { - user *meta.Meta - meta *meta.Meta - exp bool - }{ - // No meta - {anonUser, nil, false}, - {creator, nil, false}, - {reader, nil, false}, - {writer, nil, false}, - {owner, nil, false}, - {owner2, nil, false}, - // Any zettel - {anonUser, zettel, notAuthNotReadonly}, - {creator, zettel, notAuthNotReadonly}, - {reader, zettel, notAuthNotReadonly}, - {writer, zettel, notAuthNotReadonly}, - {owner, zettel, !readonly}, - {owner2, zettel, !readonly}, - // Expert zettel - {anonUser, expertZettel, notAuthNotReadonly && expert}, - {creator, expertZettel, notAuthNotReadonly && expert}, - {reader, expertZettel, notAuthNotReadonly && expert}, - {writer, expertZettel, notAuthNotReadonly && expert}, - {owner, expertZettel, !readonly && expert}, - {owner2, expertZettel, !readonly && expert}, - // No r/o zettel - {anonUser, roFalse, notAuthNotReadonly}, - {creator, roFalse, notAuthNotReadonly}, - {reader, roFalse, notAuthNotReadonly}, - {writer, roFalse, notAuthNotReadonly}, - {owner, roFalse, !readonly}, - {owner2, roFalse, !readonly}, - // Reader r/o zettel - {anonUser, roReader, false}, - {creator, roReader, false}, - {reader, roReader, false}, - {writer, roReader, notAuthNotReadonly}, - {owner, roReader, !readonly}, - {owner2, roReader, !readonly}, - // Writer r/o zettel - {anonUser, roWriter, false}, - {creator, roWriter, false}, - {reader, roWriter, false}, - {writer, roWriter, false}, - {owner, roWriter, !readonly}, - {owner2, roWriter, !readonly}, - // Owner r/o zettel - {anonUser, roOwner, false}, - {creator, roOwner, false}, - {reader, roOwner, false}, - {writer, roOwner, false}, - {owner, roOwner, false}, - {owner2, roOwner, false}, - // r/o = true zettel - {anonUser, roTrue, false}, - {creator, roTrue, false}, - {reader, roTrue, false}, - {writer, roTrue, false}, - {owner, roTrue, false}, - {owner2, roTrue, false}, - } - for _, tc := range testCases { - t.Run("Rename", func(tt *testing.T) { - got := pol.CanRename(tc.user, tc.meta) - if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } Index: auth/policy/readonly.go ================================================================== --- auth/policy/readonly.go +++ auth/policy/readonly.go @@ -4,10 +4,13 @@ // 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/zettel/meta" @@ -15,8 +18,7 @@ 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 @@ -4,10 +4,13 @@ // 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 @@ -16,11 +19,11 @@ "errors" "fmt" "io" "time" - "zettelstore.de/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/query" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -29,37 +32,34 @@ type BaseBox interface { // Location returns some information where the box is located. // Format is dependent of the box. Location() string + // GetZettel retrieves a specific zettel. + GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + + // CanDeleteZettel returns true, if box could possibly delete the given zettel. + 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) - // GetZettel retrieves a specific zettel. - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) - // CanUpdateZettel returns true, if box could possibly update the given zettel. CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool // UpdateZettel updates an existing zettel. UpdateZettel(ctx context.Context, zettel zettel.Zettel) error - - // AllowRenameZettel returns true, if box will not disallow renaming the zettel. - AllowRenameZettel(ctx context.Context, zid id.Zid) bool - - // RenameZettel changes the current Zid to a new Zid. - RenameZettel(ctx context.Context, curZid, newZid id.Zid) error - - // CanDeleteZettel returns true, if box could possibly delete the given zettel. - CanDeleteZettel(ctx context.Context, zid id.Zid) bool - - // DeleteZettel removes the zettel from the box. - DeleteZettel(ctx context.Context, zid id.Zid) error } // ZidFunc is a function that processes identifier of a zettel. type ZidFunc func(id.Zid) @@ -127,13 +127,14 @@ } // 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) + 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. @@ -143,10 +144,13 @@ // GetAllZettel retrieves a specific zettel from all managed boxes. 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. @@ -200,11 +204,12 @@ // Values for Reason const ( _ UpdateReason = iota OnReady // Box is started and fully operational OnReload // Box was reloaded - OnZettel // Something with a zettel happened + OnZettel // Something with an existing zettel happened + OnDelete // A zettel was deleted ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { Box BaseBox @@ -212,10 +217,13 @@ Zid id.Zid } // UpdateFunc is a function to be called when a change is detected. type UpdateFunc func(UpdateInfo) + +// UpdateNotifier is an UpdateFunc, but with separate values. +type UpdateNotifier func(BaseBox, id.Zid, UpdateReason) // Subject is a box that notifies observers about changes. type Subject interface { // RegisterObserver registers an observer that will be notified // if one or all zettel are found to be changed. @@ -238,14 +246,14 @@ type ctxNoEnrichType struct{} var ctxNoEnrichKey ctxNoEnrichType -// DoNotEnrich determines if the context is marked to not enrich metadata. -func DoNotEnrich(ctx context.Context) bool { +// DoEnrich determines if the context is not marked to not enrich metadata. +func DoEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) - return ok + 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() { @@ -299,19 +307,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 } +// ErrInvalidZid is returned if the zettel id is not appropriate for the box operation. +type ErrInvalidZid struct{ Zid string } -func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.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 @@ -4,20 +4,23 @@ // 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/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" @@ -27,11 +30,11 @@ ) func init() { manager.Register( " comp", - func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { + func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return getCompBox(cdata.Number, cdata.Enricher), nil }) } type compBox struct { @@ -41,24 +44,32 @@ } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta - content func(*meta.Meta) []byte + content func(context.Context, *compBox) []byte }{ - 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.ZidBoxManager): {genManagerM, genManagerC}, + 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.ZidSx): {genSxM, genSxC}, + // id.MustParse(api.ZidHTTP): {genHttpM, genHttpC}, + // id.MustParse(api.ZidAPI): {genApiM, genApiC}, + // id.MustParse(api.ZidWebUI): {genWebUiM, genWebUiC}, + // id.MustParse(api.ZidConsole): {genConsoleM, genConsoleC}, + id.MustParse(api.ZidBoxManager): {genManagerM, genManagerC}, + // id.MustParse(api.ZidIndex): {genIndexM, genIndexC}, + // id.MustParse(api.ZidQuery): {genQueryM, genQueryC}, id.MustParse(api.ZidMetadataKey): {genKeysM, genKeysC}, id.MustParse(api.ZidParser): {genParserM, genParserC}, id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC}, } // Get returns the one program box. -func getCompBox(boxNumber int, mf box.Enricher) box.ManagedBox { +func getCompBox(boxNumber int, mf box.Enricher) *compBox { return &compBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "comp").Int("boxnum", int64(boxNumber)).Child(), number: boxNumber, enricher: mf, @@ -68,34 +79,28 @@ // 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, zettel.Zettel) (id.Zid, error) { - cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel") - return id.Invalid, box.ErrReadOnly -} - -func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { +func (cb *compBox) GetZettel(ctx 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("GetZettel/Content") return zettel.Zettel{ Meta: m, - Content: zettel.NewContent(genContent(m)), + Content: zettel.NewContent(genContent(ctx, cb)), }, nil } cb.log.Trace().Msg("GetZettel/NoContent") return zettel.Zettel{Meta: m}, nil } } - cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel/Err") - return zettel.Zettel{}, box.ErrNotFound + 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 @@ -131,37 +136,17 @@ } } return nil } -func (*compBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return false } - -func (cb *compBox) UpdateZettel(context.Context, zettel.Zettel) error { - cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel") - return box.ErrReadOnly -} - -func (*compBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { - _, ok := myZettel[zid] - return !ok -} - -func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error { - err := box.ErrNotFound - if _, ok := myZettel[curZid]; ok { - err = box.ErrReadOnly - } - 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 } @@ -168,17 +153,26 @@ func (cb *compBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(myZettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } + +func getTitledMeta(zid id.Zid, title string) *meta.Meta { + m := meta.New(zid) + m.Set(api.KeyTitle, title) + return m +} func updateMeta(m *meta.Meta) { if _, ok := m.Get(api.KeySyntax); !ok { m.Set(api.KeySyntax, meta.SyntaxZmk) } m.Set(api.KeyRole, api.ValueRoleConfiguration) + if _, ok := m.Get(api.KeyCreated); !ok { + m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) + } 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 @@ -4,35 +4,33 @@ // 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" + "context" - "zettelstore.de/client.fossil/api" - "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } - m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Startup Configuration") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) - return m + return getTitledMeta(zid, "Zettelstore Startup Configuration") } -func genConfigZettelC(*meta.Meta) []byte { +func genConfigZettelC(context.Context, *compBox) []byte { var buf bytes.Buffer for i, p := range myConfig.Pairs() { if i > 0 { buf.WriteByte('\n') } Index: box/compbox/keys.go ================================================================== --- box/compbox/keys.go +++ box/compbox/keys.go @@ -4,33 +4,36 @@ // 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" + "context" "fmt" - "zettelstore.de/client.fossil/api" + "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") + m := getTitledMeta(zid, "Zettelstore Supported Metadata Keys") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } -func genKeysC(*meta.Meta) []byte { +func genKeysC(context.Context, *compBox) []byte { keys := meta.GetSortedKeyDescriptions() var buf bytes.Buffer buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { fmt.Fprintf(&buf, Index: box/compbox/log.go ================================================================== --- box/compbox/log.go +++ box/compbox/log.go @@ -4,33 +4,35 @@ // 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" + "context" - "zettelstore.de/client.fossil/api" + "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 := getTitledMeta(zid, "Zettelstore Log") 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 { +func genLogC(context.Context, *compBox) []byte { const tsFormat = "2006-01-02 15:04:05.999999" entries := kernel.Main.RetrieveLogEntries() var buf bytes.Buffer for _, entry := range entries { ts := entry.TS.Format(tsFormat) Index: box/compbox/manager.go ================================================================== --- box/compbox/manager.go +++ box/compbox/manager.go @@ -4,32 +4,32 @@ // 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" + "context" "fmt" - "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) func genManagerM(zid id.Zid) *meta.Meta { - m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Box Manager") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - return m + return getTitledMeta(zid, "Zettelstore Box Manager") } -func genManagerC(*meta.Meta) []byte { +func genManagerC(context.Context, *compBox) []byte { kvl := kernel.Main.GetServiceStatistics(kernel.BoxService) if len(kvl) == 0 { return nil } var buf bytes.Buffer ADDED box/compbox/memory.go Index: box/compbox/memory.go ================================================================== --- /dev/null +++ box/compbox/memory.go @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +// 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" + "context" + "fmt" + "os" + "runtime" + + "zettelstore.de/z/kernel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +func genMemoryM(zid id.Zid) *meta.Meta { + return getTitledMeta(zid, "Zettelstore Memory") +} + +func genMemoryC(context.Context, *compBox) []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) + fmt.Fprintf(&buf, "|CPUs|%d\n", runtime.NumCPU()) + fmt.Fprintf(&buf, "|Threads|%d\n", runtime.NumGoroutine()) + 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 @@ -4,47 +4,50 @@ // 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" + "context" "fmt" - "sort" + "slices" "strings" - "zettelstore.de/client.fossil/api" + "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") + m := getTitledMeta(zid, "Zettelstore Supported Parser") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } -func genParserC(*meta.Meta) []byte { +func genParserC(context.Context, *compBox) []byte { var buf bytes.Buffer buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Text Format?:|=Image Format?:\n") syntaxes := parser.GetSyntaxes() - sort.Strings(syntaxes) + slices.Sort(syntaxes) for _, syntax := range syntaxes { info := parser.Get(syntax) if info.Name != syntax { continue } altNames := info.AltNames - sort.Strings(altNames) + slices.Sort(altNames) fmt.Fprintf( &buf, "|%v|%v|%v|%v|%v\n", syntax, strings.Join(altNames, ", "), info.IsASTParser, info.IsTextFormat, info.IsImageFormat) } return buf.Bytes() ADDED box/compbox/sx.go Index: box/compbox/sx.go ================================================================== --- /dev/null +++ box/compbox/sx.go @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------------- +// 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" + "context" + "fmt" + + "t73f.de/r/sx" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +func genSxM(zid id.Zid) *meta.Meta { + return getTitledMeta(zid, "Zettelstore Sx Engine") +} + +func genSxC(context.Context, *compBox) []byte { + var buf bytes.Buffer + buf.WriteString("|=Name|=Value>\n") + fmt.Fprintf(&buf, "|Symbols|%d\n", sx.MakeSymbol("NIL").Factory().Size()) + return buf.Bytes() +} Index: box/compbox/version.go ================================================================== --- box/compbox/version.go +++ box/compbox/version.go @@ -4,53 +4,47 @@ // 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/client.fossil/api" + "context" + + "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) - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) - return m -} - func genVersionBuildM(zid id.Zid) *meta.Meta { - m := getVersionMeta(zid, "Zettelstore Version") + m := getTitledMeta(zid, "Zettelstore Version") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } -func genVersionBuildC(*meta.Meta) []byte { +func genVersionBuildC(context.Context, *compBox) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) } func genVersionHostM(zid id.Zid) *meta.Meta { - m := getVersionMeta(zid, "Zettelstore Host") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - return m + return getTitledMeta(zid, "Zettelstore Host") } -func genVersionHostC(*meta.Meta) []byte { +func genVersionHostC(context.Context, *compBox) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string)) } func genVersionOSM(zid id.Zid) *meta.Meta { - m := getVersionMeta(zid, "Zettelstore Operating System") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - return m + return getTitledMeta(zid, "Zettelstore Operating System") } -func genVersionOSC(*meta.Meta) []byte { +func genVersionOSC(context.Context, *compBox) []byte { goOS := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string) goArch := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string) result := make([]byte, 0, len(goOS)+len(goArch)+1) result = append(result, goOS...) result = append(result, '/') Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -1,10 +1,23 @@ +/*----------------------------------------------------------------------------- + * 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; font-family: serif; scroll-behavior: smooth; height: 100%; } body { @@ -73,44 +86,40 @@ article > * + * { margin-top: .5rem } article header { padding: 0; margin: 0; } - h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal } - h1 { font-size:1.5rem; margin:.65rem 0 } - h2 { font-size:1.25rem; margin:.70rem 0 } - 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 } + h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal; margin:.4em 0 } + h1 { font-size:1.5em } + h2 { font-size:1.25em } + h3 { font-size:1.15em } + h4 { font-size:1.05em; font-weight: bold } + h5 { font-size:1.05em } + h6 { font-size:1.05em; font-weight: lighter } + p { margin: .5em 0 0 0 } + p.zs-meta-zettel { margin-top: .5em; margin-left: .5em } li,figure,figcaption,dl { margin: 0 } - dt { margin: .5rem 0 0 0 } + dt { margin: .5em 0 0 0 } dt+dd { margin-top: 0 } - dd { margin: .5rem 0 0 2rem } + dd { margin: .5em 0 0 2em } dd > p:first-child { margin: 0 0 0 0 } blockquote { - border-left: 0.5rem solid lightgray; - padding-left: 1rem; - margin-left: 1rem; - margin-right: 2rem; - font-style: italic; - } - blockquote p { margin-bottom: .5rem } - blockquote cite { font-style: normal } + border-left: .5em solid lightgray; + padding-left: 1em; + margin-left: 1em; + margin-right: 2em; + } + blockquote p { margin-bottom: .5em } table { border-collapse: collapse; border-spacing: 0; max-width: 100%; } - thead>tr>td { border-bottom: 2px solid hsl(0, 0%, 70%); font-weight: bold } - tfoot>tr>td { border-top: 2px solid hsl(0, 0%, 70%); font-weight: bold } - td { - text-align: left; - padding: .25rem .5rem; - border-bottom: 1px solid hsl(0, 0%, 85%) - } + td, th {text-align: left; padding: .25em .5em;} + th { font-weight: bold } + thead th { border-bottom: 2px solid hsl(0, 0%, 70%) } + td { border-bottom: 1px solid hsl(0, 0%, 85%) } main form { padding: 0 .5em; margin: .5em 0 0 0; } main form:after { @@ -142,100 +151,100 @@ 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 } + a[rel~="external"]::after { content: "➚"; display: inline-block } img { max-width: 100% } img.right { float: right } ol.zs-endnotes { - padding-top: .5rem; + padding-top: .5em; border-top: 1px solid; } kbd { font-family:monospace } code,pre { font-family: monospace; font-size: 85%; } code { - padding: .1rem .2rem; + padding: .1em .2em; background: #f0f0f0; border: 1px solid #ccc; - border-radius: .25rem; + border-radius: .25em; } pre { - padding: .5rem .7rem; + padding: .5em .7em; max-width: 100%; overflow: auto; border: 1px solid #ccc; - border-radius: .5rem; + border-radius: .5em; background: #f0f0f0; } pre code { font-size: 95%; position: relative; padding: 0; border: none; } div.zs-indication { - padding: .5rem .7rem; + padding: .5em .7em; max-width: 100%; - border-radius: .5rem; + border-radius: .5em; border: 1px solid black; } div.zs-indication p:first-child { margin-top: 0 } span.zs-indication { border: 1px solid black; - border-radius: .25rem; - padding: .1rem .2rem; + border-radius: .25em; + padding: .1rem .2em; font-size: 95%; } .zs-info { background-color: lightblue; - padding: .5rem 1rem; + padding: .5em 1em; } .zs-warning { background-color: lightyellow; - padding: .5rem 1rem; + padding: .5em 1em; } .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } - td.left { text-align:left } - td.center { text-align:center } - td.right { text-align:right } + td.left, th.left { text-align:left } + td.center, th.center { text-align:center } + td.right, th.right { text-align:right } .zs-font-size-0 { font-size:75% } .zs-font-size-1 { font-size:83% } .zs-font-size-2 { font-size:100% } .zs-font-size-3 { font-size:117% } .zs-font-size-4 { font-size:150% } .zs-font-size-5 { font-size:200% } - .zs-deprecated { border-style: dashed; padding: .2rem } + .zs-deprecated { border-style: dashed; padding: .2em } .zs-meta { font-size:.75rem; color:#444; - margin-bottom:1rem; + margin-bottom:1em; } .zs-meta a { color:#444 } - h1+.zs-meta { margin-top:-1rem } - nav > details { margin-top:1rem } + h1+.zs-meta { margin-top:-1em } + nav > details { margin-top:1em } details > summary { width: 100%; background-color: #eee; font-family:sans-serif; } details > ul { margin-top:0; - padding-left:2rem; + padding-left:2em; background-color: #eee; } - footer { padding: 0 1rem } + footer { padding: 0 1em } @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } Index: box/constbox/base.sxn ================================================================== --- box/constbox/base.sxn +++ box/constbox/base.sxn @@ -1,5 +1,18 @@ +;;;---------------------------------------------------------------------------- +;;; 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"))) @@ -6,11 +19,11 @@ (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))) - ,@(if css-role-url `((link (@ (rel "stylesheet") (href ,css-role-url))))) + ,@(ROLE-DEFAULT-meta (current-binding)) (title ,title)) (body (nav (@ (class "zs-menu")) (a (@ (href ,home-url)) "Home") ,@(if with-auth @@ -37,12 +50,14 @@ (button "New") (nav (@ (class "zs-dropdown-content")) ,@(map wui-link new-zettel-links) ))) ) - (form (@ (action ,search-url)) - (input (@ (type "text") (placeholder "Search..") (name ,query-key-query)))) + (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 @@ -4,10 +4,13 @@ // 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,11 +17,11 @@ import ( "context" _ "embed" // Allow to embed file content "net/url" - "zettelstore.de/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" @@ -28,11 +31,11 @@ ) func init() { manager.Register( " const", - func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { + func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &constBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "const").Int("boxnum", int64(cdata.Number)).Child(), number: cdata.Number, zettel: constZettelMap, @@ -55,24 +58,18 @@ enricher box.Enricher } func (*constBox) Location() string { return "const:" } -func (*constBox) CanCreateZettel(context.Context) bool { return false } - -func (cb *constBox) CreateZettel(context.Context, zettel.Zettel) (id.Zid, error) { - cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel") - return id.Invalid, box.ErrReadOnly -} - func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { if z, ok := cb.zettel[zid]; ok { cb.log.Trace().Msg("GetZettel") return zettel.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil } - cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel") - return zettel.Zettel{}, box.ErrNotFound + 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 @@ -98,37 +95,17 @@ } } return nil } -func (*constBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return false } - -func (cb *constBox) UpdateZettel(context.Context, zettel.Zettel) error { - cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel") - return box.ErrReadOnly -} - -func (cb *constBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { - _, ok := cb.zettel[zid] - return !ok -} - -func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error { - err := box.ErrNotFound - if _, ok := cb.zettel[curZid]; ok { - err = box.ErrReadOnly - } - 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 } @@ -176,119 +153,136 @@ api.KeyTitle: "Zettelstore Dependencies", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityLogin, + api.KeyVisibility: api.ValueVisibilityPublic, api.KeyCreated: "20210504135842", - api.KeyModified: "20230601163100", + 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: "20230523144403", + 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: "20230527144100", + 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: "20230523212800", + api.KeyModified: "20241127170400", 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: "20230621131500", + api.KeyModified: "20241127170500", 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: "20230621132600", + 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: "20230707190246", - 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: "20230621133100", + api.KeyModified: "20241127170530", 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: "20230527224800", + api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentErrorSxn)}, - id.TemplateSxnZid: { + 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 Code for Templates", + api.KeyTitle: "Zettelstore Sxn Base Code", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230619132800", + api.KeyModified: "20241118173500", + 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(contentTemplateCodeSxn)}, + 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: "20240827143500", api.KeyVisibility: api.ValueVisibilityPublic, }, zettel.NewContent(contentBaseCSS)}, id.MustParse(api.ZidUserCSS): { constHeader{ @@ -297,19 +291,10 @@ api.KeySyntax: meta.SyntaxCSS, api.KeyCreated: "20210622110143", api.KeyVisibility: api.ValueVisibilityPublic, }, zettel.NewContent([]byte("/* User-defined CSS */"))}, - id.RoleCSSMapZid: { - constHeader{ - api.KeyTitle: "Zettelstore Role to CSS Map", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxNone, - api.KeyCreated: "20220321183214", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - zettel.NewContent(nil)}, id.EmojiZid: { constHeader{ api.KeyTitle: "Zettelstore Generic Emoji", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxGif, @@ -323,20 +308,45 @@ 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.ValueRoleZettel, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20201028185209", - api.KeyVisibility: api.ValueVisibilityCreator, + 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", @@ -346,18 +356,69 @@ meta.NewPrefix + api.KeyCredential: "", meta.NewPrefix + api.KeyUserID: "", meta.NewPrefix + api.KeyUserRole: api.ValueUserRoleReader, api.KeyVisibility: api.ValueVisibilityOwner, }, + 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: "20241213103100", + 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.MustParse(api.ZidAppDirectory): { + constHeader{ + api.KeyTitle: "Zettelstore Application Directory", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxNone, + api.KeyLang: api.ValueLangEN, + api.KeyCreated: "20240703235900", + api.KeyVisibility: api.ValueVisibilityLogin, + }, zettel.NewContent(nil)}, id.DefaultHomeZid: { constHeader{ - api.KeyTitle: "Home", - api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: meta.SyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyCreated: "20210210190757", + api.KeyTitle: "Home", + api.KeyRole: api.ValueRoleZettel, + api.KeySyntax: meta.SyntaxZmk, + api.KeyLang: api.ValueLangEN, + api.KeyCreated: "20210210190757", + api.KeyModified: "20241216105800", }, zettel.NewContent(contentHomeZettel)}, } //go:embed license.txt @@ -382,31 +443,46 @@ 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 contentTemplateCodeSxn []byte +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 Index: box/constbox/delete.sxn ================================================================== --- box/constbox/delete.sxn +++ box/constbox/delete.sxn @@ -1,5 +1,18 @@ +;;;---------------------------------------------------------------------------- +;;; 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")) Index: box/constbox/dependencies.zettel ================================================================== --- box/constbox/dependencies.zettel +++ box/constbox/dependencies.zettel @@ -128,15 +128,19 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -=== sx, zettelstore-client -These are companion projects, written by the current main developer of Zettelstore. +=== 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://zettelstore.de/sx]] -; URL & Source zettelstore-client -: [[https://zettelstore.de/client/]] +; 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. Index: box/constbox/error.sxn ================================================================== --- box/constbox/error.sxn +++ box/constbox/error.sxn @@ -1,4 +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 ) Index: box/constbox/form.sxn ================================================================== --- box/constbox/form.sxn +++ box/constbox/form.sxn @@ -1,35 +1,60 @@ +;;;---------------------------------------------------------------------------- +;;; 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") (placeholder "Title..") (value ,meta-title) (autofocus)))) + (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") (id "zs-role") (name "role") (placeholder "role..") (value ,meta-role) + (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") (placeholder "#tag") (value ,meta-tags)))) + (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") (placeholder "metakey: metavalue")) ,meta)) + (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") (id "zs-syntax") (name "syntax") (placeholder "syntax..") (value ,meta-syntax) + (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") (placeholder "Zettel content..")) ,content) + (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"))) Index: box/constbox/home.zettel ================================================================== --- box/constbox/home.zettel +++ box/constbox/home.zettel @@ -1,15 +1,14 @@ === Thank you for using Zettelstore! You will find the lastest information about Zettelstore at [[https://zettelstore.de]]. -Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. -You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading. -Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading. -Since Zettelstore is currently in a development state, every upgrade might fix some of your problems. +Check this website regularly for [[updates|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. +You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before updating. +Sometimes, you have to edit some of your Zettelstore-related zettel before updating. +Since Zettelstore is currently in a development state, every update might fix some of your problems. -If you have problems concerning Zettelstore, -do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. +If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. === Reporting errors If you have encountered an error, please include the content of the following zettel in your mail (if possible): * [[Zettelstore Version|00000000000001]]: {{00000000000001}} * [[Zettelstore Operating System|00000000000003]] Index: box/constbox/info.sxn ================================================================== --- box/constbox/info.sxn +++ box/constbox/info.sxn @@ -1,23 +1,34 @@ +;;;---------------------------------------------------------------------------- +;;; 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"))) - ,@(if (bound? 'copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy"))) - ,@(if (bound? 'version-url) `((@H " · ") (a (@ (href ,version-url)) "Version"))) - ,@(if (bound? 'child-url) `((@H " · ") (a (@ (href ,child-url)) "Child"))) - ,@(if (bound? 'folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge"))) - ,@(if (bound? 'rename-url) `((@H " · ") (a (@ (href ,rename-url)) "Rename"))) + ,@(ROLE-DEFAULT-actions (current-binding)) + ,@(if (bound? 'reindex-url) `((@H " · ") (a (@ (href ,reindex-url)) "Reindex"))) ,@(if (bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete"))) ) ) (h2 "Interpreted Metadata") - (table ,@(map wui-table-row metadata)) + (table ,@(map wui-info-meta-table-row metadata)) (h2 "References") - ,@(if local-links `((h3 "Local") (ul ,@(map wui-valid-link local-links)))) + ,@(if local-links `((h3 "Local") (ul ,@(map wui-local-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 Index: box/constbox/listzettel.sxn ================================================================== --- box/constbox/listzettel.sxn +++ box/constbox/listzettel.sxn @@ -1,16 +1,47 @@ +;;;---------------------------------------------------------------------------- +;;; 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 ,search-url)) - (input (@ (class "zs-input") (type "text") (placeholder "Search..") (name ,query-key-query) (value ,query-value)))) + (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))) - "Other encodings: " - (a (@ (href ,data-url)) "data") - ", " - (a (@ (href ,plain-url)) "plain") + ,(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"))) ) Index: box/constbox/login.sxn ================================================================== --- box/constbox/login.sxn +++ box/constbox/login.sxn @@ -1,5 +1,18 @@ +;;;---------------------------------------------------------------------------- +;;; 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 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 ================================================================== --- /dev/null +++ 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.sxn Index: box/constbox/rename.sxn ================================================================== --- box/constbox/rename.sxn +++ /dev/null @@ -1,26 +0,0 @@ -`(article - (header (h1 "Rename Zettel " ,zid)) - (p "Do you really want to rename this zettel?") - ,@(if incoming - `((div (@ (class "zs-warning")) - (h2 "Warning!") - (p "If you rename this zettel, incoming references from the following zettel will become invalid.") - (ul ,@(map wui-item-link incoming)) - )) - ) - ,@(if (and (bound? 'useless) useless) - `((div (@ (class "zs-warning")) - (h2 "Warning!") - (p "Renaming this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") - (ul ,@(map wui-item useless)) - )) - ) - (form (@ (method "POST")) - (input (@ (type "hidden") (id "curzid") (name "curzid") (value ,zid))) - (div - (label (@ (for "newzid")) "New zettel id") - (input (@ (class "zs-input") (type "text") (id "newzid") (name "newzid") (placeholder "ZID..") (value ,zid) (autofocus)))) - (div (input (@ (class "zs-primary") (type "submit") (value "Rename")))) - ) - ,(wui-meta-desc metapairs) -) ADDED box/constbox/roleconfiguration.zettel Index: box/constbox/roleconfiguration.zettel ================================================================== --- /dev/null +++ box/constbox/roleconfiguration.zettel @@ -0,0 +1,22 @@ +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. +There is some CSS code to make the default web user interface a litte bit nicer. +The default image to signal a broken image can be configured too. + +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 to 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 ================================================================== --- /dev/null +++ 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 ================================================================== --- /dev/null +++ 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 ================================================================== --- /dev/null +++ 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 ================================================================== --- /dev/null +++ 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. Index: box/constbox/wuicode.sxn ================================================================== --- box/constbox/wuicode.sxn +++ box/constbox/wuicode.sxn @@ -4,63 +4,135 @@ ;;; 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. -(define (wui-item s) `(li ,s)) - -;; wui-table-row takes a pair and translates it into a HTML table row with -;; two columns. -(define (wui-table-row p) - `(tr (td ,(car p)) (td ,(cdr p)))) - -;; wui-valid-link translates a local link into a HTML link. A link is a pair -;; (valid . url). If valid is not truish, only the invalid url is returned. -(define (wui-valid-link l) - (if (car l) - `(li (a (@ (href ,(cdr l))) ,(cdr l))) - `(li ,(cdr l)))) - -;; wui-link taks an url and returns a HTML link inside. -(define (wui-link q) - `(a (@ (href ,(cdr q))) ,(car q))) +(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-local-link translates a local link into HTML. +(defun wui-local-link (l) `(li (a (@ (href ,l )) ,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. -(define (wui-item-link q) `(li ,(wui-link q))) +(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. -(define (wui-tdata-link q) `(td ,(wui-link q))) +(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. -(define (wui-item-popup-link e) - `(li (a (@ (href ,e) (target "_blank") (rel "noopener noreferrer")) ,e))) +(defun wui-item-popup-link (e) + `(li (a (@ (href ,e) (target "_blank") (rel "external noreferrer")) ,e))) ;; wui-option-value returns a value for an HTML option element. -(define (wui-option-value v) `(option (@ (value ,v)))) +(defun wui-option-value (v) `(option (@ (value ,v)))) ;; wui-datalist returns a HTML datalist with the given HTML identifier and a ;; list of values. -(define (wui-datalist id lst) +(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. -(define (wui-pair-desc-item p) `((dt ,(car p)) (dd ,(cdr p)))) +(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. -(define (wui-meta-desc l) +(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. -(define (wui-enc-matrix matrix) +(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 ((sequel-url (binding-lookup 'sequel-url binding))) + (if (defined? sequel-url) `((@H " · ") (a (@ (href ,sequel-url)) "Sequel")))) + ,@(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))) + ) +) Index: box/constbox/zettel.sxn ================================================================== --- box/constbox/zettel.sxn +++ box/constbox/zettel.sxn @@ -1,5 +1,18 @@ +;;;---------------------------------------------------------------------------- +;;; 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 " · "))) @@ -8,27 +21,23 @@ "(" ,@(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)) - ,@(if (bound? 'copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy"))) - ,@(if (bound? 'version-url) `((@H " · ") (a (@ (href ,version-url)) "Version"))) - ,@(if (bound? 'child-url) `((@H " · ") (a (@ (href ,child-url)) "Child"))) - ,@(if (bound? 'folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge"))) + ,@(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)) - ,@(if (bound? 'meta-url) `((br) "URL: " ,(url-to-html meta-url))) - ,@(let (author (and (bound? 'meta-author) meta-author)) (if author `((br) "By " ,author))) + ,@(if prequel-refs `((br) "Prequel: " ,prequel-refs)) + ,@(ROLE-DEFAULT-heading (current-binding)) ) ) ,@content ,endnotes - ,@(if (or folge-links subordinate-links back-links successor-links) + ,@(if (or folge-links sequel-links back-links successor-links) `((nav - ,@(if folge-links `((details (@ (open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) - ,@(if subordinate-links `((details (@ (open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) - ,@(if back-links `((details (@ (open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) - ,@(if successor-links `((details (@ (open)) (summary "Successors") (ul ,@(map wui-item-link successor-links))))) + ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) + ,@(if sequel-links `((details (@ (,sequel-open)) (summary "Sequel") (ul ,@(map wui-item-link sequel-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 @@ -4,10 +4,13 @@ // 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 @@ -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 @@ -149,11 +152,11 @@ 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) } @@ -164,11 +167,11 @@ 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, @@ -198,14 +201,14 @@ for _, c := range dp.fCmds { close(c) } } -func (dp *dirBox) notifyChanged(zid id.Zid) { - if chci := dp.cdata.Notify; chci != nil { - dp.log.Trace().Zid(zid).Msg("notifyChanged") - chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} +func (dp *dirBox) notifyChanged(zid id.Zid, reason box.UpdateReason) { + if notify := dp.cdata.Notify; notify != nil { + dp.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChanged") + notify(dp, zid, reason) } } 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 @@ -239,19 +242,19 @@ err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } - dp.notifyChanged(meta.Zid) + dp.notifyChanged(meta.Zid, box.OnZettel) dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel") return meta.Zid, err } func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { - return zettel.Zettel{}, box.ErrNotFound + return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } m, c, err := dp.srvGetMetaContent(ctx, entry, zid) if err != nil { return zettel.Zettel{}, err } @@ -300,11 +303,11 @@ } 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} @@ -311,66 +314,20 @@ } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) dp.dirSrv.UpdateDirEntry(entry) err := dp.srvSetZettel(ctx, entry, zettel) if err == nil { - dp.notifyChanged(zid) + dp.notifyChanged(zid, box.OnZettel) } dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel") return err } 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 -} - -func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - if curZid == newZid { - return nil - } - curEntry := dp.dirSrv.GetDirEntry(curZid) - if !curEntry.IsValid() { - return box.ErrNotFound - } - if dp.readonly { - return box.ErrReadOnly - } - - // Check whether zettel with new ID already exists in this box. - if dp.HasZettel(ctx, newZid) { - return &box.ErrInvalidID{Zid: newZid} - } - - oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid) - if err != nil { - return err - } - - newEntry, err := dp.dirSrv.RenameDirEntry(curEntry, newZid) - if err != nil { - return err - } - oldMeta.Zid = newZid - 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(curZid) - dp.notifyChanged(newZid) - } - dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel") - return err -} - func (dp *dirBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { if dp.readonly { return false } entry := dp.dirSrv.GetDirEntry(zid) @@ -382,19 +339,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(zid) + dp.notifyChanged(zid, box.OnDelete) } 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 @@ -4,10 +4,13 @@ // 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 @@ -4,24 +4,28 @@ // 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/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" @@ -28,25 +32,25 @@ ) 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) } @@ -186,33 +188,35 @@ 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)) @@ -357,13 +360,19 @@ entry.ContentExt, entry.MetaName != "", entry.UselessFiles, ) } + +// fileMode to create a new file: user, group, and all are allowed to read and write. +// +// If you want to forbid others or the group to read or to write, you must set +// umask(1) accordingly. +const fileMode os.FileMode = 0666 // func openFileWrite(path string) (*os.File, error) { - return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode) } func writeFileZid(w io.Writer, zid id.Zid) error { _, err := io.WriteString(w, "id: ") if err == nil { Index: box/filebox/filebox.go ================================================================== --- box/filebox/filebox.go +++ box/filebox/filebox.go @@ -4,10 +4,13 @@ // 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,11 +18,11 @@ "errors" "net/url" "path/filepath" "strings" - "zettelstore.de/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" Index: box/filebox/zipbox.go ================================================================== --- box/filebox/zipbox.go +++ box/filebox/zipbox.go @@ -4,23 +4,27 @@ // 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/input" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" @@ -29,11 +33,11 @@ type zipBox struct { log *logger.Logger number int name string enricher box.Enricher - notify chan<- box.UpdateInfo + notify box.UpdateNotifier dirSrv *notify.DirService } func (zb *zipBox) Location() string { if strings.HasPrefix(zb.name, "/") { @@ -80,22 +84,14 @@ 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, zettel.Zettel) (id.Zid, error) { - err := box.ErrReadOnly - zb.log.Trace().Err(err).Msg("CreateZettel") - return id.Invalid, err -} - func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { - return zettel.Zettel{}, box.ErrNotFound + return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } reader, err := zip.OpenReader(zb.name) if err != nil { return zettel.Zettel{}, err } @@ -106,11 +102,12 @@ 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 zettel.Zettel{}, err } @@ -173,43 +170,17 @@ handle(m) } return nil } -func (*zipBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return false } - -func (zb *zipBox) UpdateZettel(context.Context, zettel.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() -} - -func (zb *zipBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { - err := box.ErrReadOnly - if curZid == newZid { - err = nil - } - curEntry := zb.dirSrv.GetDirEntry(curZid) - if !curEntry.IsValid() { - err = box.ErrNotFound - } - zb.log.Trace().Err(err).Msg("RenameZettel") - return err -} - func (*zipBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } 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 } @@ -223,13 +194,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 @@ -4,10 +4,13 @@ // 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 ( @@ -19,11 +22,11 @@ ) // 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 } @@ -42,22 +45,22 @@ _, 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 { +func GetQueryInt(u *url.URL, key string, minVal, defVal, maxVal int) int { sVal := u.Query().Get(key) if sVal == "" { - return def + return defVal } iVal, err := strconv.Atoi(sVal) if err != nil { - return def + return defVal } - if iVal < min { - return min + if iVal < minVal { + return minVal } - if iVal > max { - return max + if iVal > maxVal { + return maxVal } return iVal } Index: box/manager/anteroom.go ================================================================== --- box/manager/anteroom.go +++ box/manager/anteroom.go @@ -4,10 +4,13 @@ // 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 ( @@ -23,28 +26,26 @@ arReload arZettel ) type anteroom struct { - num uint64 next *anteroom - waiting id.Set + 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() @@ -55,65 +56,57 @@ } 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 { + if room.waiting.Contains(zid) { // Zettel is already waiting. Nothing to do. return } } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { - room.waiting.Zid(zid) + room.waiting.Add(zid) room.curLoad++ return } room := ar.makeAnteroom(zid) ar.last.next = room ar.last = room } -func (ar *anterooms) makeAnteroom(zid id.Zid) *anteroom { - ar.nextNum++ - if zid == id.Invalid { - return &anteroom{num: ar.nextNum, next: nil, waiting: nil, curLoad: 0, reload: true} - } - c := ar.maxLoad - if c == 0 { - c = 100 - } - waiting := id.NewSetCap(ar.maxLoad, zid) - 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) +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() ar.deleteReloadedRooms() - if ns := len(newZids); ns > 0 { - ar.nextNum++ - ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newZids, curLoad: ns, reload: true} + if !allZids.IsEmpty() { + ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: allZids.Length(), reload: true} if ar.first.next == nil { ar.last = ar.first } - return ar.nextNum + } else { + ar.first = nil + ar.last = nil } - - ar.first = nil - ar.last = nil - return 0 } -func (ar *anterooms) deleteReloadedRooms() { +func (ar *anteroomQueue) deleteReloadedRooms() { room := ar.first for room != nil && room.reload { room = room.next } ar.first = room @@ -120,33 +113,31 @@ 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 - } - roomNo := ar.first.num - if ar.first.waiting == nil { - ar.removeFirst() - return arReload, id.Invalid, roomNo - } - for zid := range ar.first.waiting { - delete(ar.first.waiting, zid) - if len(ar.first.waiting) == 0 { - ar.removeFirst() - } - return arZettel, zid, roomNo - } - ar.removeFirst() - return arNothing, id.Invalid, 0 -} - -func (ar *anterooms) removeFirst() { + first := ar.first + if first != nil { + if first.waiting == nil && first.reload { + ar.removeFirst() + return arReload, id.Invalid, false + } + if zid, found := first.waiting.Pop(); found { + if first.waiting.IsEmpty() { + 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 @@ -4,10 +4,13 @@ // 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,15 +19,15 @@ "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 @@ -4,10 +4,13 @@ // 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 ( @@ -28,11 +31,11 @@ func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { return "NONE" } var sb strings.Builder - for i := 0; i < len(mgr.boxes)-2; i++ { + for i := range len(mgr.boxes) - 2 { if i > 0 { sb.WriteString(", ") } sb.WriteString(mgr.boxes[i].Location()) } @@ -39,50 +42,68 @@ return sb.String() } // CanCreateZettel returns true, if box could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { + if err := mgr.checkContinue(ctx); err != nil { + return false + } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - return mgr.State() == box.StartStateStarted && 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 zettel.Zettel) (id.Zid, error) { +func (mgr *Manager) CreateZettel(ctx context.Context, ztl zettel.Zettel) (id.Zid, error) { mgr.mgrLog.Debug().Msg("CreateZettel") - if mgr.State() != box.StartStateStarted { - return id.Invalid, box.ErrStopped + if err := mgr.checkContinue(ctx); err != nil { + return id.Invalid, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - return mgr.boxes[0].CreateZettel(ctx, zettel) + if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { + ztl.Meta = mgr.cleanMetaProperties(ztl.Meta) + zid, err := box.CreateZettel(ctx, ztl) + if err == nil { + mgr.idxUpdateZettel(ctx, ztl) + } + return zid, err + } + return id.Invalid, box.ErrReadOnly } // GetZettel retrieves a specific zettel. 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 + if err := mgr.checkContinue(ctx); err != nil { + return zettel.Zettel{}, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() + return mgr.getZettel(ctx, zid) +} +func (mgr *Manager) getZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { 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 zettel.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) ([]zettel.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel") - if mgr.State() != box.StartStateStarted { - return nil, box.ErrStopped + if err := mgr.checkContinue(ctx); err != nil { + return nil, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() var result []zettel.Zettel for i, p := range mgr.boxes { @@ -93,30 +114,39 @@ } 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) { +func (mgr *Manager) FetchZids(ctx context.Context) (*id.Set, error) { mgr.mgrLog.Debug().Msg("FetchZids") - if mgr.State() != box.StartStateStarted { - return nil, box.ErrStopped + if err := mgr.checkContinue(ctx); err != nil { + return nil, err } - result := id.Set{} mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() + return mgr.fetchZids(ctx) +} +func (mgr *Manager) fetchZids(ctx context.Context) (*id.Set, error) { + numZettel := 0 + for _, p := range mgr.boxes { + var mbstats box.ManagedBoxStats + p.ReadStats(&mbstats) + numZettel += mbstats.Zettel + } + result := id.NewSetCap(numZettel) 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) }, query.AlwaysIncluded) if err != nil { return nil, err } } return result, nil } -func (mgr *Manager) HasZettel(ctx context.Context, zid id.Zid) bool { +func (mgr *Manager) hasZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrLog.Debug().Zid(zid).Msg("HasZettel") - if mgr.State() != box.StartStateStarted { + if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, bx := range mgr.boxes { @@ -125,18 +155,20 @@ } } return false } +// GetMeta returns just the metadata of the zettel with the given identifier. 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 + if err := mgr.checkContinue(ctx); err != nil { + return nil, err } m, err := mgr.idxStore.GetMeta(ctx, zid) if err != nil { + // TODO: Call GetZettel and return just metadata, in case the index is not complete. return nil, err } mgr.Enrich(ctx, m, 0) return m, nil } @@ -145,12 +177,12 @@ // criteria. The result is ordered by descending zettel id. 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") } - if mgr.State() != box.StartStateStarted { - return nil, box.ErrStopped + if err := mgr.checkContinue(ctx); err != nil { + return nil, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq) @@ -158,11 +190,11 @@ 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{} + rejected := id.NewSet() handleMeta := func(m *meta.Meta) { zid := m.Zid if rejected.Contains(zid) { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") return @@ -173,11 +205,11 @@ } 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 err2 := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err2 != nil { @@ -194,69 +226,45 @@ return result, nil } // CanUpdateZettel returns true, if box could possibly update the given zettel. func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { + if err := mgr.checkContinue(ctx); err != nil { + return false + } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - return mgr.State() == box.StartStateStarted && 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 zettel.Zettel) error { mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel") - 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) -} - -// 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() - for _, p := range mgr.boxes { - if !p.AllowRenameZettel(ctx, zid) { - return false - } - } - return true -} - -// 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") - 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++ { - mgr.boxes[j].RenameZettel(ctx, newZid, curZid) - } + if err := mgr.checkContinue(ctx); err != nil { + return err + } + return mgr.updateZettel(ctx, zettel) +} +func (mgr *Manager) updateZettel(ctx context.Context, zettel zettel.Zettel) error { + 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 nil + return box.ErrReadOnly } // 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 { + if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { @@ -268,21 +276,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 + if err := mgr.checkContinue(ctx); err != nil { + return err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) if err == nil { - return nil + mgr.idxDeleteZettel(ctx, zid) + return err } - 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 @@ -4,10 +4,13 @@ // 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 ( @@ -18,11 +21,11 @@ "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" ) type collectData struct { - refs id.Set + refs *id.Set words store.WordSet urls store.WordSet } func (data *collectData) initialize() { @@ -74,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 @@ -4,47 +4,51 @@ // 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/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/box" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { - // Calculate computed, but stored values. - if _, ok := m.Get(api.KeyCreated); !ok { + _, hasCreated := m.Get(api.KeyCreated) + if !hasCreated { m.Set(api.KeyCreated, computeCreated(m.Zid)) } - if box.DoNotEnrich(ctx) { - // Enrich is called indirectly via indexer or enrichment is not requested - // because of other reasons -> ignore this call, do not update metadata - return - } - computePublished(m) - if boxNumber > 0 { - m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) - } - mgr.idxStore.Enrich(ctx, m) + if box.DoEnrich(ctx) { + computePublished(m) + if boxNumber > 0 { + m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) + } + mgr.idxStore.Enrich(ctx, m) + } + + if !hasCreated { + m.Set(meta.KeyCreatedMissing, api.ValueTrue) + } } func computeCreated(zid id.Zid) string { if zid <= 10101000000 { - // A year 0000 is not allowed and therefore an artificaial Zid. + // A year 0000 is not allowed and therefore an artificial Zid. // In the year 0001, the month must be > 0. // In the month 000101, the day must be > 0. return "00010101000000" } seconds := zid % 100 Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -4,10 +4,13 @@ // 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 ( @@ -26,46 +29,46 @@ "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 { +func (mgr *Manager) SearchEqual(word string) *id.Set { found := mgr.idxStore.SearchEqual(word) - mgr.idxLog.Debug().Str("word", word).Int("found", int64(len(found))).Msg("SearchEqual") + mgr.idxLog.Debug().Str("word", word).Int("found", int64(found.Length())).Msg("SearchEqual") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // 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 (mgr *Manager) SearchPrefix(prefix string) id.Set { +func (mgr *Manager) SearchPrefix(prefix string) *id.Set { found := mgr.idxStore.SearchPrefix(prefix) - mgr.idxLog.Debug().Str("prefix", prefix).Int("found", int64(len(found))).Msg("SearchPrefix") + mgr.idxLog.Debug().Str("prefix", prefix).Int("found", int64(found.Length())).Msg("SearchPrefix") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // 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 (mgr *Manager) SearchSuffix(suffix string) id.Set { +func (mgr *Manager) SearchSuffix(suffix string) *id.Set { found := mgr.idxStore.SearchSuffix(suffix) - mgr.idxLog.Debug().Str("suffix", suffix).Int("found", int64(len(found))).Msg("SearchSuffix") + mgr.idxLog.Debug().Str("suffix", suffix).Int("found", int64(found.Length())).Msg("SearchSuffix") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. -func (mgr *Manager) SearchContains(s string) id.Set { +func (mgr *Manager) SearchContains(s string) *id.Set { found := mgr.idxStore.SearchContains(s) - mgr.idxLog.Debug().Str("s", s).Int("found", int64(len(found))).Msg("SearchContains") + mgr.idxLog.Debug().Str("s", s).Int("found", int64(found.Length())).Msg("SearchContains") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } @@ -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 { @@ -145,10 +141,11 @@ } case _, ok := <-timer.C: if !ok { return false } + // mgr.idxStore.Optimize() // TODO: make it less often, for example once per 10 minutes timer.Reset(timerDuration) case <-mgr.done: if !timer.Stop() { <-timer.C } @@ -158,19 +155,25 @@ } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) { var cData collectData cData.initialize() - collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) + if mustIndexZettel(zettel.Meta) { + collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) + } m := zettel.Meta zi := store.NewZettelIndex(m) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } + +func mustIndexZettel(m *meta.Meta) bool { + return m.Zid >= id.Zid(999999900) +} func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { for _, pair := range m.ComputedPairs() { descr := meta.GetDescription(pair.Key) if descr.IsProperty() { @@ -189,35 +192,49 @@ 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 mgr.HasZettel(ctx, ref) { + cData.refs.ForEach(func(ref id.Zid) { + if mgr.hasZettel(ctx, ref) { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } - } + }) zi.SetWords(cData.words) zi.SetUrls(cData.urls) } func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } - if !mgr.HasZettel(ctx, zid) { + if !mgr.hasZettel(ctx, zid) { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) @@ -224,15 +241,15 @@ return } zi.AddInverseRef(inverseKey, zid) } -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 { +func (mgr *Manager) idxCheckZettel(s *id.Set) { + s.ForEach(func(zid id.Zid) { mgr.idxAr.EnqueueZettel(zid) - } + }) } Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -4,10 +4,13 @@ // 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,14 +19,13 @@ "io" "net/url" "sync" "time" - "zettelstore.de/client.fossil/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/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" @@ -34,11 +36,11 @@ // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. Config config.Config Enricher box.Enricher - Notify chan<- box.UpdateInfo + Notify box.UpdateNotifier } // Connect returns a handle to the specified box. func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) { if authManager.IsReadonly() { @@ -77,13 +79,10 @@ 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 @@ -97,11 +96,11 @@ 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 @@ -113,10 +112,11 @@ mgr.stateMx.Lock() mgr.state = newState mgr.stateMx.Unlock() } +// State returns the box.StartState of the manager. func (mgr *Manager) State() box.StartState { mgr.stateMx.RLock() state := mgr.state mgr.stateMx.RUnlock() return state @@ -137,15 +137,16 @@ rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, idxLog: boxLog.Clone().Str("box", "index").Child(), - idxStore: memstore.New(), - idxAr: newAnterooms(1000), + idxStore: createIdxStore(rtConfig), + idxAr: newAnteroomQueue(1000), idxReady: make(chan struct{}, 1), } - cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos} + + cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.notifyChanged} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { p, err := Connect(uri, authManager, &cdata) if err != nil { return nil, err @@ -167,10 +168,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 { @@ -181,12 +186,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() @@ -208,15 +213,16 @@ if ignoreUpdate(cache, now, reason, zid) { mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored") continue } + isStarted := mgr.State() == box.StartStateStarted mgr.idxEnqueue(reason, zid) if ci.Box == nil { ci.Box = mgr } - if mgr.State() == box.StartStateStarted { + if isStarted { mgr.notifyObserver(&ci) } } case <-mgr.done: return @@ -249,12 +255,14 @@ return case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: mgr.idxAr.EnqueueZettel(zid) + case box.OnDelete: + mgr.idxAr.EnqueueZettel(zid) default: - mgr.mgrLog.Warn().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") + mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") return } select { case mgr.idxReady <- struct{}{}: default: @@ -301,10 +309,11 @@ mgr.done = make(chan struct{}) go mgr.notifier() mgr.waitBoxesAreStarted() mgr.setState(box.StartStateStarted) + mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady}) go mgr.idxIndexer() return nil } @@ -313,11 +322,11 @@ const waitTime = 10 * time.Millisecond const waitLoop = int(1 * time.Second / waitTime) for i := 1; !mgr.allBoxesStarted(); i++ { if i%waitLoop == 0 { if time.Duration(i)*waitTime > time.Minute { - mgr.mgrLog.Warn().Msg("Waiting for more than one minute to start") + 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) @@ -335,11 +344,11 @@ // 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.State() != box.StartStateStarted { + if err := mgr.checkContinue(ctx); err != nil { return } mgr.setState(box.StartStateStopping) close(mgr.done) for _, p := range mgr.boxes { @@ -351,23 +360,33 @@ } // Refresh internal box data. func (mgr *Manager) Refresh(ctx context.Context) error { mgr.mgrLog.Debug().Msg("Refresh") - if mgr.State() != box.StartStateStarted { - return box.ErrStopped + if err := mgr.checkContinue(ctx); err != nil { + return err } + mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() - mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} for _, bx := range mgr.boxes { if rb, ok := bx.(box.Refresher); ok { rb.Refresh(ctx) } } return nil } + +// ReIndex data of the given zettel. +func (mgr *Manager) ReIndex(ctx context.Context, zid id.Zid) error { + mgr.mgrLog.Debug().Msg("ReIndex") + if err := mgr.checkContinue(ctx); err != nil { + return err + } + mgr.infos <- box.UpdateInfo{Box: mgr, 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() @@ -404,5 +423,18 @@ // Dump internal data structures to a Writer. func (mgr *Manager) Dump(w io.Writer) { mgr.idxStore.Dump(w) } + +func (mgr *Manager) checkContinue(ctx context.Context) error { + if mgr.State() != box.StartStateStarted { + return box.ErrStopped + } + return ctx.Err() +} + +func (mgr *Manager) notifyChanged(bbox box.BaseBox, zid id.Zid, reason box.UpdateReason) { + if infos := mgr.infos; infos != nil { + mgr.infos <- box.UpdateInfo{Box: bbox, Reason: reason, Zid: zid} + } +} ADDED box/manager/mapstore/mapstore.go Index: box/manager/mapstore/mapstore.go ================================================================== --- /dev/null +++ box/manager/mapstore/mapstore.go @@ -0,0 +1,673 @@ +//----------------------------------------------------------------------------- +// 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" + "slices" + "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.Set // set of dead references in this zettel + forward *id.Set // set of forward references in this zettel + backward *id.Set // set 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.Set + backward *id.Set +} + +func (zd *zettelData) optimize() { + zd.dead.Optimize() + zd.forward.Optimize() + zd.backward.Optimize() + for _, bidi := range zd.otherRefs { + bidi.forward.Optimize() + bidi.backward.Optimize() + } +} + +type mapStore struct { + mx sync.RWMutex + intern map[string]string // map to intern strings + idx map[id.Zid]*zettelData + dead map[id.Zid]*id.Set // map dead refs where they occur + words stringRefs + urls stringRefs + + // Stats + mxStats sync.Mutex + updates uint64 +} +type stringRefs map[string]*id.Set + +// New returns a new memory-based index store. +func New() store.Store { + return &mapStore{ + intern: make(map[string]string, 1024), + idx: make(map[id.Zid]*zettelData), + dead: make(map[id.Zid]*id.Set), + words: make(stringRefs), + urls: make(stringRefs), + } +} + +func (ms *mapStore) 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 *mapStore) Enrich(_ context.Context, m *meta.Meta) { + if ms.doEnrich(m) { + ms.mxStats.Lock() + ms.updates++ + ms.mxStats.Unlock() + } +} + +func (ms *mapStore) 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 !zi.dead.IsEmpty() { + m.Set(api.KeyDead, zi.dead.MetaString()) + updated = true + } + back := removeOtherMetaRefs(m, zi.backward.Clone()) + if !zi.backward.IsEmpty() { + m.Set(api.KeyBackward, zi.backward.MetaString()) + updated = true + } + if !zi.forward.IsEmpty() { + m.Set(api.KeyForward, zi.forward.MetaString()) + back.ISubstract(zi.forward) + updated = true + } + for k, refs := range zi.otherRefs { + if !refs.backward.IsEmpty() { + m.Set(k, refs.backward.MetaString()) + back.ISubstract(refs.backward) + updated = true + } + } + if !back.IsEmpty() { + m.Set(api.KeyBack, back.MetaString()) + 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 *mapStore) SearchEqual(word string) *id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := id.NewSet() + if refs, ok := ms.words[word]; ok { + result = result.IUnion(refs) + } + if refs, ok := ms.urls[word]; ok { + result = result.IUnion(refs) + } + zid, err := id.Parse(word) + if err != nil { + return result + } + zi, ok := ms.idx[zid] + if !ok { + return result + } + + return addBackwardZids(result, zid, zi) +} + +// 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 *mapStore) 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 { + result = 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 *mapStore) 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 { + result = 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 *mapStore) 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) { + result = addBackwardZids(result, zid, zi) + } + } + return result +} + +func (ms *mapStore) 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.IUnion(refs) + } + for u, refs := range ms.urls { + if !pred(u, s) { + continue + } + result.IUnion(refs) + } + return result +} + +func addBackwardZids(result *id.Set, zid id.Zid, zi *zettelData) *id.Set { + // Must only be called if ms.mx is read-locked! + result = result.Add(zid) + result = result.IUnion(zi.backward) + for _, mref := range zi.otherRefs { + result = result.IUnion(mref.backward) + } + return result +} + +func removeOtherMetaRefs(m *meta.Meta, back *id.Set) *id.Set { + for _, p := range m.PairsRest() { + switch meta.Type(p.Key) { + case meta.TypeID: + if zid, err := id.Parse(p.Value); err == nil { + back = back.Remove(zid) + } + case meta.TypeIDSet: + for _, val := range meta.ListFromValue(p.Value) { + if zid, err := id.Parse(val); err == nil { + back = back.Remove(zid) + } + } + } + } + return back +} + +func (ms *mapStore) 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 = refs + delete(ms.dead, zidx.Zid) + } + + zi.meta = m + ms.updateDeadReferences(zidx, zi) + ids := ms.updateForwardBackwardReferences(zidx, zi) + toCheck = toCheck.IUnion(ids) + ids = ms.updateMetadataReferences(zidx, zi) + toCheck = toCheck.IUnion(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 + } + zi.optimize() + 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 *mapStore) internString(s string) string { + if is, found := ms.intern[s]; found { + return is + } + ms.intern[s] = s + return s +} + +func (ms *mapStore) 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 *mapStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) { + // Must only be called if ms.mx is write-locked! + drefs := zidx.GetDeadRefs() + newRefs, remRefs := zi.dead.Diff(drefs) + zi.dead = drefs + remRefs.ForEach(func(ref id.Zid) { + ms.dead[ref] = ms.dead[ref].Remove(zidx.Zid) + }) + newRefs.ForEach(func(ref id.Zid) { + ms.dead[ref] = ms.dead[ref].Add(zidx.Zid) + }) +} + +func (ms *mapStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelData) *id.Set { + // Must only be called if ms.mx is write-locked! + brefs := zidx.GetBackRefs() + newRefs, remRefs := zi.forward.Diff(brefs) + zi.forward = brefs + + var toCheck *id.Set + remRefs.ForEach(func(ref id.Zid) { + bzi := ms.getOrCreateEntry(ref) + bzi.backward = bzi.backward.Remove(zidx.Zid) + if bzi.meta == nil { + toCheck = toCheck.Add(ref) + } + }) + newRefs.ForEach(func(ref id.Zid) { + bzi := ms.getOrCreateEntry(ref) + bzi.backward = bzi.backward.Add(zidx.Zid) + if bzi.meta == nil { + toCheck = toCheck.Add(ref) + } + }) + return toCheck +} + +func (ms *mapStore) 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 := mr.forward.Diff(mrefs) + mr.forward = mrefs + zi.otherRefs[key] = mr + + newRefs.ForEach(func(ref id.Zid) { + bzi := ms.getOrCreateEntry(ref) + if bzi.otherRefs == nil { + bzi.otherRefs = make(map[string]bidiRefs) + } + bmr := bzi.otherRefs[key] + bmr.backward = bmr.backward.Add(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 { + srefs[word] = srefs[word].Add(zid) + } + for _, word := range removeWords { + refs, ok := srefs[word] + if !ok { + continue + } + refs = refs.Remove(zid) + if refs.IsEmpty() { + delete(srefs, word) + continue + } + srefs[word] = refs + } + return next.Words() +} + +func (ms *mapStore) 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 *mapStore) DeleteZettel(_ context.Context, zid id.Zid) *id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + return ms.doDeleteZettel(zid) +} + +func (ms *mapStore) 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 *mapStore) deleteDeadSources(zid id.Zid, zi *zettelData) { + // Must only be called if ms.mx is write-locked! + zi.dead.ForEach(func(ref id.Zid) { + if drefs, ok := ms.dead[ref]; ok { + if drefs = drefs.Remove(zid); drefs.IsEmpty() { + delete(ms.dead, ref) + } else { + ms.dead[ref] = drefs + } + } + }) +} + +func (ms *mapStore) deleteForwardBackward(zid id.Zid, zi *zettelData) *id.Set { + // Must only be called if ms.mx is write-locked! + zi.forward.ForEach(func(ref id.Zid) { + if fzi, ok := ms.idx[ref]; ok { + fzi.backward = fzi.backward.Remove(zid) + } + }) + + var toCheck *id.Set + zi.backward.ForEach(func(ref id.Zid) { + if bzi, ok := ms.idx[ref]; ok { + bzi.forward = bzi.forward.Remove(zid) + toCheck = toCheck.Add(ref) + } + }) + return toCheck +} + +func (ms *mapStore) removeInverseMeta(zid id.Zid, key string, forward *id.Set) { + // Must only be called if ms.mx is write-locked! + forward.ForEach(func(ref id.Zid) { + bzi, ok := ms.idx[ref] + if !ok || bzi.otherRefs == nil { + return + } + bmr, ok := bzi.otherRefs[key] + if !ok { + return + } + bmr.backward = bmr.backward.Remove(zid) + if !bmr.backward.IsEmpty() || !bmr.forward.IsEmpty() { + 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 + } + refs = refs.Remove(zid) + if refs.IsEmpty() { + delete(msStringMap, word) + continue + } + msStringMap[word] = refs + } +} + +func (ms *mapStore) Optimize() { + ms.mx.Lock() + defer ms.mx.Unlock() + + // No need to optimize ms.idx: is already done via ms.UpdateReferences + for _, dead := range ms.dead { + dead.Optimize() + } + for _, s := range ms.words { + s.Optimize() + } + for _, s := range ms.urls { + s.Optimize() + } +} + +func (ms *mapStore) 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 *mapStore) 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 *mapStore) 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 !zi.dead.IsEmpty() { + fmt.Fprintln(w, "* Dead:", zi.dead) + } + dumpSet(w, "* Forward:", zi.forward) + dumpSet(w, "* Backward:", zi.backward) + + otherRefs := make([]string, 0, len(zi.otherRefs)) + for k := range zi.otherRefs { + otherRefs = append(otherRefs, k) + } + slices.Sort(otherRefs) + for _, k := range otherRefs { + fmt.Fprintln(w, "* Meta", k) + dumpSet(w, "** Forward:", zi.otherRefs[k].forward) + dumpSet(w, "** Backward:", zi.otherRefs[k].backward) + } + dumpStrings(w, "* Words", "", "", zi.words) + dumpStrings(w, "* URLs", "[[", "]]", zi.urls) + } +} + +func (ms *mapStore) 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 dumpSet(w io.Writer, prefix string, s *id.Set) { + if !s.IsEmpty() { + io.WriteString(w, prefix) + s.ForEach(func(zid id.Zid) { + 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) + slices.Sort(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/memstore.go Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ /dev/null @@ -1,627 +0,0 @@ -//----------------------------------------------------------------------------- -// 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. -//----------------------------------------------------------------------------- - -// Package memstore stored the index in main memory. -package memstore - -import ( - "context" - "fmt" - "io" - "sort" - "strings" - "sync" - - "zettelstore.de/client.fossil/api" - "zettelstore.de/client.fossil/maps" - "zettelstore.de/z/box" - "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -type bidiRefs struct { - forward id.Slice - backward id.Slice -} - -type zettelData struct { - meta *meta.Meta - dead id.Slice - forward id.Slice - backward id.Slice - otherRefs map[string]bidiRefs - words []string - urls []string -} - -type stringRefs map[string]id.Slice - -type memStore struct { - mx sync.RWMutex - 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 { - return zi.meta.Clone(), nil - } - return nil, box.ErrNotFound -} - -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.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.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.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 - } - 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 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 *zettelData) { - // Must only be called if ms.mx is read-locked! - result.Zid(zid) - result.AddSlice(zi.backward) - for _, mref := range zi.otherRefs { - 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 { - m := ms.makeMeta(zidx) - ms.mx.Lock() - defer ms.mx.Unlock() - 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) - 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()) - - // 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, "-role") -} - -func (ms *memStore) internString(s string) string { - if is, found := ms.intern[s]; found { - return is - } - ms.intern[s] = s - return s -} - -func (ms *memStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta { - origM := zidx.GetMeta() - copyM := meta.New(origM.Zid) - for _, p := range origM.Pairs() { - key := ms.internString(p.Key) - if isInternableValue(key) { - copyM.Set(key, ms.internString(p.Value)) - } else if key == api.KeyBoxNumber || !meta.IsComputed(key) { - copyM.Set(key, p.Value) - } - } - return copyM -} - -func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) { - // Must only be called if ms.mx is write-locked! - drefs := zidx.GetDeadRefs() - newRefs, remRefs := refsDiff(drefs, zi.dead) - zi.dead = drefs - for _, ref := range remRefs { - 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) { - // 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 *zettelData) { - // 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) - } - 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.getEntry(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 - } - ms.removeInverseMeta(zidx.Zid, key, remRefs) - } -} - -func updateWordSet(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) getEntry(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) 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.otherRefs) > 0 { - for key, mrefs := range zi.otherRefs { - ms.removeInverseMeta(zid, key, mrefs.forward) - } - } - ms.deleteWords(zid, zi.words) - delete(ms.idx, zid) - return toCheck -} - -func (ms *memStore) deleteDeadSources(zid id.Zid, zi *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! - 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.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 (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.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]) - } -} DELETED box/manager/memstore/refs.go Index: box/manager/memstore/refs.go ================================================================== --- box/manager/memstore/refs.go +++ /dev/null @@ -1,100 +0,0 @@ -//----------------------------------------------------------------------------- -// 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. -//----------------------------------------------------------------------------- - -package memstore - -import "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 = 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 +++ /dev/null @@ -1,137 +0,0 @@ -//----------------------------------------------------------------------------- -// 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. -//----------------------------------------------------------------------------- - -package memstore - -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) - } -} Index: box/manager/store/store.go ================================================================== --- box/manager/store/store.go +++ box/manager/store/store.go @@ -4,10 +4,13 @@ // 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 @@ -46,17 +49,20 @@ // 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 + UpdateReferences(context.Context, *ZettelIndex) *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 + DeleteZettel(context.Context, id.Zid) *id.Set + + // Optimize removes unneeded space. + Optimize() // ReadStats populates st with store statistics. ReadStats(st *Stats) // Dump the content to a Writer. Dump(io.Writer) } Index: box/manager/store/wordset.go ================================================================== --- box/manager/store/wordset.go +++ box/manager/store/wordset.go @@ -4,10 +4,13 @@ // 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 @@ -4,16 +4,19 @@ // 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 ( - "sort" + "slices" "testing" "zettelstore.de/z/box/manager/store" ) @@ -22,11 +25,11 @@ return false } if len(got) == 0 { return len(exp) == 0 } - sort.Strings(got) + slices.Sort(got) for i, w := range exp { if w != got[i] { return false } } Index: box/manager/store/zettel.go ================================================================== --- box/manager/store/zettel.go +++ box/manager/store/zettel.go @@ -4,10 +4,13 @@ // 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 ( @@ -15,15 +18,15 @@ "zettelstore.de/z/zettel/meta" ) // ZettelIndex contains all index data of a zettel. type ZettelIndex struct { - 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 + 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. @@ -30,63 +33,61 @@ func NewZettelIndex(m *meta.Meta) *ZettelIndex { return &ZettelIndex{ Zid: m.Zid, meta: m, backrefs: id.NewSet(), - inverseRefs: make(map[string]id.Set), + 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) -} +func (zi *ZettelIndex) AddBackRef(zid id.Zid) { zi.backrefs.Add(zid) } // 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) AddInverseRef(key string, zid id.Zid) { if zids, ok := zi.inverseRefs[key]; ok { - zids.Zid(zid) + zids.Add(zid) return } 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 } // GetDeadRefs returns all dead references as a sorted list. -func (zi *ZettelIndex) GetDeadRefs() id.Slice { return zi.deadrefs.Sorted() } +func (zi *ZettelIndex) GetDeadRefs() *id.Set { return zi.deadrefs } // 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.Set { return zi.backrefs } // 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 { +func (zi *ZettelIndex) GetInverseRefs() map[string]*id.Set { if len(zi.inverseRefs) == 0 { return nil } - result := make(map[string]id.Slice, len(zi.inverseRefs)) + result := make(map[string]*id.Set, len(zi.inverseRefs)) for key, refs := range zi.inverseRefs { - result[key] = refs.Sorted() + result[key] = refs } return result } // 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 } Index: box/membox/membox.go ================================================================== --- box/membox/membox.go +++ box/membox/membox.go @@ -4,10 +4,13 @@ // 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 @@ -49,13 +52,13 @@ mx sync.RWMutex // Protects the following fields zettel map[id.Zid]zettel.Zettel curBytes int } -func (mb *memBox) notifyChanged(zid id.Zid) { - if chci := mb.cdata.Notify; chci != nil { - chci <- box.UpdateInfo{Box: mb, Reason: box.OnZettel, Zid: zid} +func (mb *memBox) notifyChanged(zid id.Zid, reason box.UpdateReason) { + if notify := mb.cdata.Notify; notify != nil { + notify(mb, zid, reason) } } func (mb *memBox) Location() string { return mb.u.String() @@ -110,21 +113,22 @@ meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(zid) + + mb.notifyChanged(zid, box.OnZettel) mb.log.Trace().Zid(zid).Msg("CreateZettel") return zid, 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.ErrNotFound + return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } z.Meta = z.Meta.Clone() mb.log.Trace().Msg("GetZettel") return z, nil } @@ -178,11 +182,11 @@ } 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 { @@ -195,43 +199,15 @@ zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(m.Zid) + mb.notifyChanged(m.Zid, box.OnZettel) mb.log.Trace().Msg("UpdateZettel") return nil } -func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true } - -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 - } - - // Check that there is no zettel with newZid - if _, ok = mb.zettel[newZid]; ok { - mb.mx.Unlock() - return &box.ErrInvalidID{Zid: newZid} - } - - meta := zettel.Meta.Clone() - meta.Zid = newZid - zettel.Meta = meta - mb.zettel[newZid] = zettel - delete(mb.zettel, curZid) - mb.mx.Unlock() - mb.notifyChanged(curZid) - mb.notifyChanged(newZid) - mb.log.Trace().Msg("RenameZettel") - return nil -} - func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { mb.mx.RLock() _, ok := mb.zettel[zid] mb.mx.RUnlock() return ok @@ -240,16 +216,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(zid) + mb.notifyChanged(zid, box.OnDelete) 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 @@ -4,20 +4,22 @@ // 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 ( "errors" "fmt" "path/filepath" "regexp" - "strings" "sync" "zettelstore.de/z/box" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" @@ -38,10 +40,11 @@ // dsWorking --directory missing--> dsMissing // dsMissing --last list notification--> dsWorking // --Stop--> dsStopping type DirServiceState uint8 +// Constants for DirServiceState const ( DsCreated DirServiceState = iota DsStarting // Reading inital scan DsWorking // Initial scan complete, fully operational DsMissing // Directory is missing @@ -52,26 +55,26 @@ type DirService struct { box box.ManagedBox log *logger.Logger dirPath string notifier Notifier - infos chan<- box.UpdateInfo + infos box.UpdateNotifier mx sync.RWMutex // protects status, entries 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(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService { +func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, notify box.UpdateNotifier) *DirService { return &DirService{ box: box, log: log, notifier: notifier, - infos: chci, + infos: notify, state: DsCreated, } } // State the current service state. @@ -181,40 +184,10 @@ } ds.entries[entry.Zid] = &entry return nil } -// RenameDirEntry replaces an existing directory entry with a new one. -func (ds *DirService) RenameDirEntry(oldEntry *DirEntry, newZid id.Zid) (DirEntry, error) { - ds.mx.Lock() - 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} - } - oldZid := oldEntry.Zid - newEntry := DirEntry{ - Zid: newZid, - MetaName: renameFilename(oldEntry.MetaName, oldZid, newZid), - ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid), - ContentExt: oldEntry.ContentExt, - // Duplicates must not be set, because duplicates will be deleted - } - delete(ds.entries, oldZid) - ds.entries[newZid] = &newEntry - return newEntry, nil -} - -func renameFilename(name string, curID, newID id.Zid) string { - if cur := curID.String(); strings.HasPrefix(name, cur) { - name = newID.String() + name[len(cur):] - } - return name -} - // DeleteDirEntry removes a entry from the directory. func (ds *DirService) DeleteDirEntry(zid id.Zid) error { ds.mx.Lock() defer ds.mx.Unlock() if ds.entries == nil { @@ -225,12 +198,12 @@ } func (ds *DirService) updateEvents(newEntries entrySet) { // Something may panic. Ensure a running service. defer func() { - if r := recover(); r != nil { - kernel.Main.LogRecover("DirectoryService", r) + if ri := recover(); ri != nil { + kernel.Main.LogRecover("DirectoryService", ri) go ds.updateEvents(newEntries) } }() for ev := range ds.notifier.Events() { @@ -255,11 +228,11 @@ switch ev.Op { case Error: newEntries = nil if state != DsMissing { - ds.log.Warn().Err(ev.Err).Msg("Notifier confused") + ds.log.Error().Err(ev.Err).Msg("Notifier confused") } case Make: newEntries = make(entrySet) case List: if ev.Name == "" { @@ -286,21 +259,21 @@ case Update: ds.mx.Lock() zid := ds.onUpdateFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { - ds.notifyChange(zid) + ds.notifyChange(zid, box.OnZettel) } case Delete: ds.mx.Lock() zid := ds.onDeleteFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { - ds.notifyChange(zid) + ds.notifyChange(zid, box.OnDelete) } default: - ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") + ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") } return newEntries, true } func getNewZids(entries entrySet) id.Slice { @@ -311,18 +284,18 @@ return zids } func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) { for _, zid := range zids { - ds.notifyChange(zid) + ds.notifyChange(zid, box.OnZettel) 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(zid) + ds.notifyChange(zid, box.OnDelete) } } func (ds *DirService) onDestroyDirectory() { ds.mx.Lock() @@ -329,11 +302,11 @@ entries := ds.entries ds.entries = nil ds.state = DsMissing ds.mx.Unlock() for zid := range entries { - ds.notifyChange(zid) + ds.notifyChange(zid, box.OnDelete) } } var validFileName = regexp.MustCompile(`^(\d{14})`) @@ -371,13 +344,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 } @@ -600,11 +573,11 @@ return newLen < oldLen } return newExt < oldExt } -func (ds *DirService) notifyChange(zid id.Zid) { - if chci := ds.infos; chci != nil { - ds.log.Trace().Zid(zid).Msg("notifyChange") - chci <- box.UpdateInfo{Box: ds.box, Reason: box.OnZettel, Zid: zid} +func (ds *DirService) notifyChange(zid id.Zid, reason box.UpdateReason) { + if notify := ds.infos; notify != nil { + ds.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChange") + notify(ds.box, zid, reason) } } Index: box/notify/directory_test.go ================================================================== --- box/notify/directory_test.go +++ box/notify/directory_test.go @@ -4,10 +4,13 @@ // 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 ( Index: box/notify/entry.go ================================================================== --- box/notify/entry.go +++ box/notify/entry.go @@ -4,18 +4,21 @@ // 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/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/parser" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -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 != "" { Index: box/notify/fsdir.go ================================================================== --- box/notify/fsdir.go +++ box/notify/fsdir.go @@ -4,10 +4,13 @@ // 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), @@ -165,11 +167,11 @@ } 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 Index: box/notify/helper.go ================================================================== --- box/notify/helper.go +++ box/notify/helper.go @@ -4,10 +4,13 @@ // 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 @@ -4,10 +4,13 @@ // 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 Index: box/notify/simpledir.go ================================================================== --- box/notify/simpledir.go +++ box/notify/simpledir.go @@ -4,10 +4,13 @@ // 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 ( Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -4,10 +4,13 @@ // 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 @@ "flag" "fmt" "io" "os" - "zettelstore.de/client.fossil/api" + "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" ) @@ -38,14 +41,14 @@ context.Background(), zettel.Zettel{ Meta: m, Content: zettel.NewContent(inp.Src[inp.Pos:]), }, - m.GetDefault(api.KeySyntax, meta.SyntaxZmk), + 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 @@ -4,10 +4,13 @@ // 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,11 +18,11 @@ "fmt" "os" "golang.org/x/term" - "zettelstore.de/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/auth/cred" "zettelstore.de/z/zettel/id" ) // ---------- Subcommand: password ------------------------------------------- Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -4,10 +4,13 @@ // 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 ( @@ -64,16 +67,18 @@ 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) @@ -88,12 +93,10 @@ webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir)) } // Web user interface if !authManager.IsReadonly() { - 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)) @@ -101,11 +104,11 @@ 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(&ucQuery)) + 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, ucGetZettel, ucGetAllZettel, &ucQuery)) @@ -113,17 +116,16 @@ // API webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler()) webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) - webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery)) + 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('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) - webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) } if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } Index: cmd/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -4,18 +4,21 @@ // 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/client.fossil/maps" + "t73f.de/r/zsc/maps" "zettelstore.de/z/logger" ) // Command stores information about commands / sub-commands. type Command struct { Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -4,10 +4,13 @@ // 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 ( @@ -20,18 +23,18 @@ "runtime/debug" "strconv" "strings" "time" - "zettelstore.de/client.fossil/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/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" @@ -157,21 +160,22 @@ const ( keyAdminPort = "admin-port" keyAssetDir = "asset-dir" keyBaseURL = "base-url" + keyBoxOneURI = kernel.BoxURIs + "1" 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" - keyBoxOneURI = kernel.BoxURIs + "1" keyReadOnly = "read-only-mode" + keyRuntimeProfiling = "runtime-profiling" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" keyVerbose = "verbose-mode" ) @@ -204,13 +208,15 @@ break } err = setConfigValue(err, kernel.BoxService, key, val) } - err = setConfigValue(err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML)) + err = setConfigValue( + err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML)) - err = setConfigValue(err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) + err = setConfigValue( + err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) if val, found := cfg.Get(keyBaseURL); found { err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val) } if val, found := cfg.Get(keyURLPrefix); found { err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val) @@ -222,10 +228,11 @@ } err = setConfigValue( err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) err = setConfigValue( err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) + err = setConfigValue(err, kernel.WebService, kernel.WebProfiling, debugMode || cfg.GetBool(keyRuntimeProfiling)) if val, found := cfg.Get(keyAssetDir); found { err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val) } return err == nil } @@ -232,11 +239,11 @@ func setConfigValue(err error, subsys kernel.Service, key string, val any) error { if err == nil { err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val)) if err != nil { - kernel.Main.GetKernelLogger().Fatal().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration") + kernel.Main.GetKernelLogger().Error().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration") } } return err } Index: cmd/register.go ================================================================== --- cmd/register.go +++ cmd/register.go @@ -4,10 +4,13 @@ // 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 Index: cmd/zettelstore/main.go ================================================================== --- cmd/zettelstore/main.go +++ cmd/zettelstore/main.go @@ -4,10 +4,13 @@ // 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 @@ -16,11 +19,11 @@ "zettelstore.de/z/cmd" ) // Version variable. Will be filled by build process. -var version string = "" +var version string func main() { exitCode := cmd.Main("Zettelstore", version) os.Exit(exitCode) } Index: collect/collect.go ================================================================== --- collect/collect.go +++ collect/collect.go @@ -4,10 +4,13 @@ // 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 @@ -4,10 +4,13 @@ // 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 @@ -4,10 +4,13 @@ // 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 +++ /dev/null @@ -1,50 +0,0 @@ -//----------------------------------------------------------------------------- -// 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. -//----------------------------------------------------------------------------- - -// 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 @@ -4,10 +4,13 @@ // 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 @@ -17,12 +20,16 @@ "zettelstore.de/z/zettel/meta" ) // Key values that are supported by Config.Get const ( - KeyFooterZettel = "footer-zettel" - KeyHomeZettel = "home-zettel" + KeyFooterZettel = "footer-zettel" + KeyHomeZettel = "home-zettel" + KeyShowBackLinks = "show-back-links" + KeyShowFolgeLinks = "show-folge-links" + KeyShowSequelLinks = "show-sequel-links" + KeyShowSuccessorLinks = "show-successor-links" // api.KeyLang ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { Index: docs/development/00010000000000.zettel ================================================================== --- docs/development/00010000000000.zettel +++ docs/development/00010000000000.zettel @@ -1,10 +1,11 @@ id: 00010000000000 title: Developments Notes role: zettel syntax: zmk created: 00010101000000 -modified: 20221026184905 +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,11 +1,11 @@ id: 20210916193200 title: Required Software role: zettel syntax: zmk created: 20210916193200 -modified: 20230405150541 +modified: 20241213124936 The following software must be installed: * A current, supported [[release of Go|https://go.dev/doc/devel/release]], * [[Fossil|https://fossil-scm.org/]], @@ -15,14 +15,15 @@ ```sh export PATH=$PATH:/usr/local/go/bin export PATH=$PATH:$(go env GOPATH)/bin ``` -The internal build tool need the following software. -It can be installed / updated via the build tool itself: ``go run tools/build.go tools``. +The internal build tool needs the following software tools. +They 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``, +* [[revive|https://revive.run]] via ``go install github.com/mgechev/revive@vlatest``, * [[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,54 +1,54 @@ id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk created: 20210916194900 -modified: 20230402181229 +modified: 20241213125640 -# Sync with the official repository +# Sync with the official repository: #* ``fossil sync -u`` -# Make sure that there is no workspace defined. +# 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. +# 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''. +#* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/z'' for those zettel that are accessible only in ''expert-mode'' #* Try to resolve other error messages and warnings #* Warnings about empty content can be ignored # On every development platform, the box with 10.000 zettel must run, with ''-race'' enabled: -#* ``go run -race cmd/zettelstore/main.go run -d DIR``. +#* ``go run -race cmd/zettelstore/main.go run -d DIR`` # Create a development release: -#* ``go run tools/build.go release`` (alternatively: ``make release``). +#* ``go run tools/build.go release`` (alternatively: ``make release``) # On every platform (esp. macOS), the box with 10.000 zettel must run properly: #* ``./zettelstore -d DIR`` -# Update files in directory ''www'' -#* index.wiki -#* download.wiki -#* changes.wiki -#* plan.wiki +# Update files in directory ''www'': +#* ''index.wiki'' +#* ''download.wiki'' +#* ''changes.wiki'' +#* ''plan.wiki'' # Set file ''VERSION'' to the new release version. - It _must_ consist of three digits: MAJOR.MINOR.PATCH, even if PATCH is zero + It **must** consists of three numbers: ''MAJOR.MINOR.PATCH'', even if ''PATCH'' is zero. # Disable Fossil autosync mode: #* ``fossil setting autosync off`` # Commit the new release version: #* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"`` #* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''. - Otherwise client will not be able to import ''zettelkasten.de/z''. + Otherwise client software will not be able to import ''zettelstore.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 releases`` #* ``fossil uv add *.zip`` ADDED docs/development/20231218181900.zettel Index: docs/development/20231218181900.zettel ================================================================== --- /dev/null +++ docs/development/20231218181900.zettel @@ -0,0 +1,116 @@ +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'' +* 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,10 +1,10 @@ id: 00000000000100 title: Zettelstore Runtime Configuration role: configuration syntax: none -created: 00010101000000 +created: 20210126175322 default-copyright: (c) 2020-present by Detlef Stern default-license: EUPL-1.2-or-later default-visibility: public footer-zettel: 00001000000100 home-zettel: 00001000000000 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: 20210126175322 +modified: 20241128141924 +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 ================================================================== --- /dev/null +++ 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/00001000000002.zettel Index: docs/manual/00001000000002.zettel ================================================================== --- /dev/null +++ docs/manual/00001000000002.zettel @@ -0,0 +1,7 @@ +id: 00001000000002 +title: manual +role: role +syntax: zmk +created: 20231128184200 + +Zettel with the role ""manual"" contain the manual of the zettelstore. Index: docs/manual/00001001000000.zettel ================================================================== --- docs/manual/00001001000000.zettel +++ docs/manual/00001001000000.zettel @@ -1,25 +1,17 @@ id: 00001001000000 title: Introduction to the Zettelstore role: manual tags: #introduction #manual #zettelstore syntax: zmk - -[[Personal knowledge -management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] is -about collecting, classifying, storing, searching, retrieving, assessing, -evaluating, and sharing knowledge as a daily activity. Personal knowledge -management is done by most people, not necessarily as part of their main -business. It is essential for knowledge workers, like students, researchers, -lecturers, software developers, scientists, engineers, architects, to name -a few. Many hobbyists build up a significant amount of knowledge, even if the -do not need to think for a living. Personal knowledge management can be seen as -a prerequisite for many kinds of collaboration. - -Zettelstore is a software that collects and relates your notes (""zettel"") -to represent and enhance your knowledge. It helps with many tasks of personal -knowledge management by explicitly supporting the ""[[Zettelkasten -method|https://en.wikipedia.org/wiki/Zettelkasten]]"". The method is based on -creating many individual notes, each with one idea or information, that are -related to each other. Since knowledge is typically build up gradually, one -major focus is a long-term store of these notes, hence the name -""Zettelstore"". +created: 20210126175322 +modified: 20240710184612 + +[[Personal knowledge management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] involves collecting, classifying, storing, searching, retrieving, assessing, evaluating, and sharing knowledge as a daily activity. +It's done by most individuals, not necessarily as part of their main business. +It's essential for knowledge workers, such as students, researchers, lecturers, software developers, scientists, engineers, architects, etc. +Many hobbyists build up a significant amount of knowledge, even if they do not need to think for a living. +Personal knowledge management can be seen as a prerequisite for many kinds of collaboration. + +Zettelstore is software that collects and relates your notes (""zettel"") to represent and enhance your knowledge, supporting the ""[[Zettelkasten method|https://en.wikipedia.org/wiki/Zettelkasten]]"". +The method is based on creating many individual notes, each with one idea or piece of information, that is related to each other. +Since knowledge is typically built up gradually, one major focus is a long-term store of these notes, hence the name ""Zettelstore"". Index: docs/manual/00001003000000.zettel ================================================================== --- docs/manual/00001003000000.zettel +++ docs/manual/00001003000000.zettel @@ -1,17 +1,19 @@ id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk -modified: 20220119145756 +created: 20210126175322 +modified: 20241213101917 === The curious user You just want to check out the Zettelstore software * Grab the appropriate executable and copy it into any directory -* Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software. If you encounter problem, please take a look on the [[Troubleshooting|00001018000000]] page.] +* Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software. + If you encounter a problem, please take a look on the [[Troubleshooting|00001018000000]] page.] * A sub-directory ""zettel"" will be created in the directory where you put the executable. It will contain your future zettel. * Open the URI [[http://localhost:23123]] with your web browser. It will present you a mostly empty Zettelstore. There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information. Index: docs/manual/00001003300000.zettel ================================================================== --- docs/manual/00001003300000.zettel +++ docs/manual/00001003300000.zettel @@ -1,10 +1,11 @@ id: 00001003300000 title: Zettelstore installation for the intermediate user role: manual tags: #installation #manual #zettelstore syntax: zmk +created: 20211125191727 modified: 20220114175754 You already tried the Zettelstore software and now you want to use it permanently. Zettelstore should start automatically when you log into your computer. Index: docs/manual/00001003305000.zettel ================================================================== --- docs/manual/00001003305000.zettel +++ docs/manual/00001003305000.zettel @@ -1,11 +1,12 @@ id: 00001003305000 title: Enable Zettelstore to start automatically on Windows role: manual tags: #installation #manual #zettelstore syntax: zmk -modified: 20220218125541 +created: 20211125191727 +modified: 20241213103259 Windows is a complicated beast. There are several ways to automatically start Zettelstore. === Startup folder @@ -32,11 +33,11 @@ The Windows Task scheduler allows you to start Zettelstore as an background task. This is both an advantage and a disadvantage. -On the plus side, Zettelstore runs in the background, and it does not disturbs you. +On the plus side, Zettelstore runs in the background, and it does not disturb you. All you have to do is to open your web browser, enter the appropriate URL, and there you go. On the negative side, you will not be notified when you enter the wrong data in the Task scheduler and Zettelstore fails to start. This can be mitigated by first using the command line prompt to start Zettelstore with the appropriate options. Once everything works, you can register Zettelstore to be automatically started by the task scheduler. @@ -69,11 +70,11 @@ {{00001003305112}} The next steps are the trickiest. -If you did not created a startup configuration file, then create an action that starts a program. +If you did not create a startup configuration file, then create an action that starts a program. Enter the file path where you placed the Zettelstore executable. The ""Browse ..."" button helps you with that.[^I store my Zettelstore executable in the sub-directory ''bin'' of my home directory.] It is essential that you also enter a directory, which serves as the environment for your zettelstore. The (sub-) directory ''zettel'', which will contain your zettel, will be placed in this directory. Index: docs/manual/00001003310000.zettel ================================================================== --- docs/manual/00001003310000.zettel +++ docs/manual/00001003310000.zettel @@ -1,10 +1,11 @@ id: 00001003310000 title: Enable Zettelstore to start automatically on macOS role: manual tags: #installation #manual #zettelstore syntax: zmk +created: 20220114181521 modified: 20220119124635 There are several ways to automatically start Zettelstore. * [[Login Items|#login-items]] Index: docs/manual/00001003315000.zettel ================================================================== --- docs/manual/00001003315000.zettel +++ docs/manual/00001003315000.zettel @@ -1,10 +1,11 @@ id: 00001003315000 title: Enable Zettelstore to start automatically on Linux role: manual tags: #installation #manual #zettelstore syntax: zmk +created: 20220114181521 modified: 20220307104944 Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore. * One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]]. Index: docs/manual/00001003600000.zettel ================================================================== --- docs/manual/00001003600000.zettel +++ docs/manual/00001003600000.zettel @@ -1,10 +1,11 @@ id: 00001003600000 title: Installation of Zettelstore on a server role: manual tags: #installation #manual #zettelstore syntax: zmk +created: 20211125191727 modified: 20211125185833 You want to provide a shared Zettelstore that can be used from your various devices. Installing Zettelstore as a Linux service is not that hard. Index: docs/manual/00001004000000.zettel ================================================================== --- docs/manual/00001004000000.zettel +++ docs/manual/00001004000000.zettel @@ -1,10 +1,11 @@ id: 00001004000000 title: Configuration of Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk +created: 20210126175322 modified: 20210510153233 There are some levels to change the behavior and/or the appearance of Zettelstore. # The first level is the way to start Zettelstore services and to manage it via command line (and, in part, via a graphical user interface). Index: docs/manual/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -2,40 +2,39 @@ title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20221128155143 +modified: 20240926144803 -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. +The configuration file, specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. +These cannot be stored in a [[configuration zettel|00001004020000]] because they are needed before Zettelstore can start or because of security reasons. +For example, Zettelstore needs to know in advance on which network address it must listen or where zettel are stored. An attacker that is able to change the owner can do anything. -Therefore only the owner of the computer on which Zettelstore runs can change this information. +Therefore, only the owner of the computer on which Zettelstore runs can change this information. The file for startup configuration must be created via a text editor in advance. The syntax of the configuration file is the same as for any zettel metadata. The following keys are supported: ; [!admin-port|''admin-port''] : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. - A value of ""0"" (the default) disables the administrator console. + A value of ""0"" (the default) disables it. The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]]. On most operating systems, the value must be greater than ""1024"" unless you start Zettelstore with the full privileges of a system administrator (which is not recommended). Default: ""0"" ; [!asset-dir|''asset-dir''] : Allows to specify a directory whose files are allowed be transferred directly with the help of the web server. The URL prefix for these files is ''/assets/''. - You can use this if you want to transfer files that are too large for a note to users. - Examples would be presentation files, PDF files, music files or video files. + You can use this if you want to transfer files that are too large for a zettel, such as presentation, PDF, music or video files. + + Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the very special case that the directory is one of the configured [[boxes|#box-uri-x]].] - Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the case that the directory is one of the configured [[boxes|#box-uri-x]].] - - If you specify only the URL prefix, then the contents of the directory are listed to the user. + If you specify only the URL prefix in your web client, the contents of the directory are listed. To avoid this, create an empty file in the directory named ""index.html"". Default: """", no asset directory is set, the URL prefix ''/assets/'' is invalid. ; [!base-url|''base-url''] : Sets the absolute base URL for the service. @@ -43,31 +42,31 @@ Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start. Default: ""http://127.0.0.1:23123/"". ; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one : Specifies a [[box|00001004011200]] where zettel are stored. - During startup __X__ is counted up, starting with one, until no key is found. - This allows to configure more than one box. + During startup, __X__ is incremented, starting with one, until no key is found. + This allows to configuring than one box. If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ""dir://.zettel"". In this case, even a key ''box-uri-2'' will be ignored. ; [!debug-mode|''debug-mode''] -: Allows to debug the Zettelstore software (mostly used by the developers) if set to [[true|00001006030500]] +: If set to [[true|00001006030500]], allows to debug the Zettelstore software (mostly used by Zettelstore developers). Disables any timeout values of the internal web server and does not send some security-related data. Sets [[''log-level''|#log-level]] to ""debug"". + Enables [[''runtime-profiling''|#runtime-profiling]]. Do not enable it for a production server. Default: ""false"" ; [!default-dir-box-type|''default-dir-box-type''] -: Specifies the default value for the (sub-) type of [[directory boxes|00001004011400#type]]. - Zettel are typically stored in such boxes. +: Specifies the default value for the (sub-)type of [[directory boxes|00001004011400#type]], in which Zettel are typically stored. Default: ""notify"" ; [!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. +: Must be set to [[true|00001006030500]] if authentication is enabled and Zettelstore is not accessible via HTTPS (but via HTTP). + Otherwise web browsers 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 ``