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.17.0 +0.18.0 Index: ast/block.go ================================================================== --- ast/block.go +++ ast/block.go @@ -11,11 +11,11 @@ // 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 @@ -12,35 +12,20 @@ //----------------------------------------------------------------------------- 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) } @@ -56,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? Index: ast/ref.go ================================================================== --- ast/ref.go +++ ast/ref.go @@ -15,11 +15,11 @@ import ( "net/url" "strings" - "zettelstore.de/client.fossil/api" + "t73f.de/r/zsc/api" "zettelstore.de/z/zettel/id" ) // QueryPrefix is the prefix that denotes a query expression. const QueryPrefix = api.QueryPrefix Index: ast/walk_test.go ================================================================== --- ast/walk_test.go +++ ast/walk_test.go @@ -14,53 +14,53 @@ 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() Index: auth/impl/digest.go ================================================================== --- auth/impl/digest.go +++ auth/impl/digest.go @@ -17,12 +17,12 @@ "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 Index: auth/impl/impl.go ================================================================== --- auth/impl/impl.go +++ auth/impl/impl.go @@ -18,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" @@ -90,11 +90,11 @@ } now := time.Now().Round(time.Second) sClaim := sx.MakeList( sx.Int64(kind), - sx.String(subject), + sx.MakeString(subject), sx.Int64(now.Unix()), sx.Int64(now.Add(d).Unix()), sx.Int64(ident.Zid), ) return sign(sClaim, a.secret) @@ -123,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) Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -76,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) Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -12,11 +12,11 @@ //----------------------------------------------------------------------------- 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 { Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -12,11 +12,11 @@ //----------------------------------------------------------------------------- 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" ) Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -15,11 +15,11 @@ 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" ) Index: box/box.go ================================================================== --- box/box.go +++ box/box.go @@ -19,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" ) @@ -136,11 +136,11 @@ 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. @@ -248,14 +248,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() { Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -16,11 +16,11 @@ 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" @@ -31,59 +31,71 @@ func init() { manager.Register( " comp", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { - return getCompBox(cdata.Number, cdata.Enricher), nil + return getCompBox(cdata.Number, cdata.Enricher, cdata.Mapper), nil }) } type compBox struct { log *logger.Logger number int enricher box.Enricher + mapper manager.Mapper } 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}, + id.MustParse(api.ZidWarnings): {genWarningsM, genWarningsC}, + id.MustParse(api.ZidMapping): {genMappingM, genMappingC}, } // Get returns the one program box. -func getCompBox(boxNumber int, mf box.Enricher) box.ManagedBox { +func getCompBox(boxNumber int, mf box.Enricher, mapper manager.Mapper) *compBox { return &compBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "comp").Int("boxnum", int64(boxNumber)).Child(), number: boxNumber, enricher: mf, + mapper: mapper, } } // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } func (*compBox) Location() string { return "" } -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 } @@ -160,17 +172,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 @@ -13,29 +13,24 @@ 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 @@ -13,27 +13,27 @@ 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 @@ -13,27 +13,26 @@ 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.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 @@ -13,26 +13,23 @@ 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/mapping.go Index: box/compbox/mapping.go ================================================================== --- /dev/null +++ box/compbox/mapping.go @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------- +// 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" + + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +// Zettelstore Identifier Mapping. +// +// In the first stage of migration process, it is a computed zettel showing a +// hypothetical mapping. In later stages, it will be stored as a normal zettel +// that is updated when a new zettel is created or an old zettel is deleted. + +func genMappingM(zid id.Zid) *meta.Meta { + return getTitledMeta(zid, "Zettelstore Identifier Mapping") +} + +func genMappingC(ctx context.Context, cb *compBox) []byte { + var buf bytes.Buffer + toNew, err := cb.mapper.OldToNewMapping(ctx) + if err != nil { + buf.WriteString("**Error while fetching: ") + buf.WriteString(err.Error()) + buf.WriteString("**\n") + return buf.Bytes() + } + oldZids := id.NewSetCap(len(toNew)) + for zidO := range toNew { + oldZids.Add(zidO) + } + first := true + oldZids.ForEach(func(zidO id.Zid) { + if first { + buf.WriteString("**Note**: this mapping is preliminary.\n") + buf.WriteString("It only shows you how it could look if the migration is done.\n") + buf.WriteString("Use this page to update your zettel if something strange is shown.\n") + buf.WriteString("```\n") + first = false + } + buf.WriteString(zidO.String()) + buf.WriteByte(' ') + buf.WriteString(toNew[zidO].String()) + buf.WriteByte('\n') + }) + if !first { + buf.WriteString("```") + } + return buf.Bytes() +} ADDED box/compbox/memory.go Index: box/compbox/memory.go ================================================================== --- /dev/null +++ box/compbox/memory.go @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------------- +// 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) + 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 @@ -13,41 +13,41 @@ 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 @@ -12,48 +12,39 @@ //----------------------------------------------------------------------------- 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, '/') ADDED box/compbox/warnings.go Index: box/compbox/warnings.go ================================================================== --- /dev/null +++ box/compbox/warnings.go @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------------- +// 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" + + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +func genWarningsM(zid id.Zid) *meta.Meta { + return getTitledMeta(zid, "Zettelstore Warnings") +} + +func genWarningsC(ctx context.Context, cb *compBox) []byte { + var buf bytes.Buffer + buf.WriteString("* [[Zettel without stored creation date|query:created-missing:true]]\n") + buf.WriteString("* [[Zettel with strange creation date|query:created<19700101000000]]\n") + + ws, err := cb.mapper.Warnings(ctx) + if err != nil { + buf.WriteString("**Error while fetching: ") + buf.WriteString(err.Error()) + buf.WriteString("**\n") + return buf.Bytes() + } + first := true + ws.ForEach(func(zid id.Zid) { + if first { + first = false + buf.WriteString("=== Mapper Warnings\n") + } + buf.WriteString("* [[") + buf.WriteString(zid.String()) + buf.WriteString("]]\n") + }) + + return buf.Bytes() +} Index: box/constbox/constbox.go ================================================================== --- box/constbox/constbox.go +++ box/constbox/constbox.go @@ -17,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" @@ -170,11 +170,11 @@ api.KeySyntax: meta.SyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, 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", @@ -209,11 +209,11 @@ constHeader{ api.KeyTitle: "Zettelstore Info HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", + api.KeyModified: "20240618170000", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentInfoSxn)}, id.FormTemplateZid: { constHeader{ @@ -280,11 +280,11 @@ constHeader{ api.KeyTitle: "Zettelstore Sxn Base Code", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230619132800", - api.KeyModified: "20240219144600", + api.KeyModified: "20240618170100", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityExpert, api.KeyPrecursor: string(api.ZidSxnPrelude), }, zettel.NewContent(contentBaseCodeSxn)}, @@ -422,10 +422,20 @@ 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, 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/info.sxn ================================================================== --- box/constbox/info.sxn +++ box/constbox/info.sxn @@ -25,11 +25,11 @@ ) ) (h2 "Interpreted Metadata") (table ,@(map wui-info-meta-table-row metadata)) (h2 "References") - ,@(if local-links `((h3 "Local") (ul ,@(map wui-valid-link local-links)))) + ,@(if 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/prelude.sxn ================================================================== --- box/constbox/prelude.sxn +++ box/constbox/prelude.sxn @@ -11,44 +11,26 @@ ;;; 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. - -;; Constants NIL and T -(defconst NIL ()) -(defconst T 'T) - -;; defunconst macro to define functions that are bound as a constant. -;; -;; (defunconst NAME ARGS EXPR ...) -(defmacro defunconst (name args . body) - `(begin (defun ,name ,args ,@body) (defconst ,name ,name))) +;;; 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 BINDING is a list of two elements -;; (SYMBOL EXPR) -(defmacro let (bindings . body) - `((lambda ,(map car bindings) ,@body) ,@(map cadr bindings))) - ;; let* macro ;; ;; (let* (BINDING ...) EXPR ...), where SYMBOL may occur in later bindings. (defmacro let* (bindings . body) (if (null? bindings) - `((lambda () ,@body)) - `((lambda (,(caar bindings)) - (let* ,(cdr bindings) ,@body)) - ,(cadar bindings)))) + `(begin ,@body) + `(let ((,(caar bindings) ,(cadar bindings))) + (let* ,(cdr bindings) ,@body)))) ;; cond macro ;; ;; (cond ((COND EXPR) ...)) (defmacro cond clauses @@ -55,13 +37,13 @@ (if (null? clauses) () (let* ((clause (car clauses)) (the-cond (car clause))) (if (= the-cond T) - (cadr clause) + `(begin ,@(cdr clause)) `(if ,the-cond - ,(cadr clause) + (begin ,@(cdr clause)) (cond ,@(cdr clauses))))))) ;; and macro ;; ;; (and EXPR ...) Index: box/constbox/wuicode.sxn ================================================================== --- box/constbox/wuicode.sxn +++ box/constbox/wuicode.sxn @@ -12,61 +12,56 @@ ;;;---------------------------------------------------------------------------- ;; Contains WebUI specific code, but not related to a specific template. ;; wui-list-item returns the argument as a HTML list item. -(defunconst wui-item (s) `(li ,s)) +(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. -(defunconst wui-info-meta-table-row (p) +(defun wui-info-meta-table-row (p) `(tr (td (@ (class zs-info-meta-key)) ,(car p)) (td (@ (class zs-info-meta-value)) ,(cdr p)))) -;; wui-valid-link translates a local link into a HTML link. A link is a pair -;; (valid . url). If valid is not truish, only the invalid url is returned. -(defunconst wui-valid-link (l) - (if (car l) - `(li (a (@ (href ,(cdr l))) ,(cdr l))) - `(li ,(cdr l)))) +;; 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. -(defunconst wui-link (q) - `(a (@ (href ,(cdr q))) ,(car q))) +(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. -(defunconst 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. -(defunconst 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. -(defunconst wui-item-popup-link (e) +(defun wui-item-popup-link (e) `(li (a (@ (href ,e) (target "_blank") (rel "noopener noreferrer")) ,e))) ;; wui-option-value returns a value for an HTML option element. -(defunconst 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. -(defunconst 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. -(defunconst 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. -(defunconst 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. -(defunconst wui-enc-matrix (matrix) +(defun wui-enc-matrix (matrix) `(table ,@(map (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row)))) matrix))) Index: box/dirbox/service.go ================================================================== --- box/dirbox/service.go +++ box/dirbox/service.go @@ -19,11 +19,11 @@ "io" "os" "path/filepath" "time" - "zettelstore.de/client.fossil/input" + "t73f.de/r/zsc/input" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/notify" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" @@ -360,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 @@ -18,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 @@ -18,11 +18,11 @@ "context" "fmt" "io" "strings" - "zettelstore.de/client.fossil/input" + "t73f.de/r/zsc/input" "zettelstore.de/z/box" "zettelstore.de/z/box/notify" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" Index: box/manager/anteroom.go ================================================================== --- box/manager/anteroom.go +++ box/manager/anteroom.go @@ -27,11 +27,11 @@ arZettel ) type anteroom struct { next *anteroom - waiting id.Set + waiting *id.Set curLoad int reload bool } type anteroomQueue struct { @@ -56,11 +56,11 @@ } 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) { @@ -86,17 +86,17 @@ defer ar.mx.Unlock() ar.first = &anteroom{next: nil, waiting: nil, curLoad: 0, reload: true} ar.last = ar.first } -func (ar *anteroomQueue) Reload(allZids id.Set) { +func (ar *anteroomQueue) Reload(allZids *id.Set) { ar.mx.Lock() defer ar.mx.Unlock() ar.deleteReloadedRooms() - if ns := len(allZids); ns > 0 { - ar.first = &anteroom{next: ar.first, waiting: allZids, 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 } } else { ar.first = nil @@ -122,13 +122,12 @@ if first != nil { if first.waiting == nil && first.reload { ar.removeFirst() return arReload, id.Invalid, false } - for zid := range first.waiting { - delete(first.waiting, zid) - if len(first.waiting) == 0 { + if zid, found := first.waiting.Pop(); found { + if first.waiting.IsEmpty() { ar.removeFirst() } return arZettel, zid, first.reload } ar.removeFirst() Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -42,11 +42,11 @@ return sb.String() } // CanCreateZettel returns true, if box could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { - if mgr.State() != box.StartStateStarted { + if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { @@ -56,12 +56,12 @@ } // CreateZettel creates a new zettel. func (mgr *Manager) CreateZettel(ctx context.Context, zettel 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() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) @@ -75,12 +75,12 @@ } // 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() for i, p := range mgr.boxes { var errZNF box.ErrZettelNotFound @@ -95,12 +95,12 @@ } // 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 { @@ -111,30 +111,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.Add(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 { 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 { @@ -145,12 +154,12 @@ return false } func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta") - if mgr.State() != box.StartStateStarted { - return nil, box.ErrStopped + if err := mgr.checkContinue(ctx); err != nil { + return nil, err } m, err := mgr.idxStore.GetMeta(ctx, zid) if err != nil { return nil, err @@ -163,12 +172,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) @@ -176,14 +185,14 @@ 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.ContainsOrNil(zid) { + if rejected.Contains(zid) { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") return } if _, ok := selected[zid]; ok { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected") @@ -212,11 +221,11 @@ 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 mgr.State() != box.StartStateStarted { + if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { @@ -227,12 +236,12 @@ } // 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 + if err := mgr.checkContinue(ctx); err != nil { + return err } 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 @@ -243,11 +252,11 @@ return box.ErrReadOnly } // AllowRenameZettel returns true, if box will not disallow renaming the zettel. func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { - if mgr.State() != box.StartStateStarted { + if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { @@ -259,12 +268,12 @@ } // 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 + if err := mgr.checkContinue(ctx); err != nil { + return err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for i, p := range mgr.boxes { err := p.RenameZettel(ctx, curZid, newZid) @@ -280,11 +289,11 @@ return nil } // CanDeleteZettel returns true, if box could possibly delete the given zettel. func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { - if mgr.State() != box.StartStateStarted { + if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { @@ -296,12 +305,12 @@ } // 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) Index: box/manager/collect.go ================================================================== --- box/manager/collect.go +++ box/manager/collect.go @@ -21,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() { Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.go @@ -15,39 +15,41 @@ 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 @@ -29,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 } @@ -141,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 } @@ -207,17 +208,17 @@ stWords.Add(value) } } func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { - for ref := range cData.refs { + 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) { @@ -244,10 +245,10 @@ 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 @@ -37,10 +37,18 @@ type ConnectData struct { Number int // number of the box, starting with 1. Config config.Config Enricher box.Enricher Notify chan<- box.UpdateInfo + Mapper Mapper +} + +// Mapper allows to inspect the mapping between old-style and new-style zettel identifier. +type Mapper interface { + Warnings(context.Context) (*id.Set, error) // Fetch problematic zettel identifier + + OldToNewMapping(ctx context.Context) (map[id.Zid]id.ZidN, error) } // Connect returns a handle to the specified box. func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) { if authManager.IsReadonly() { @@ -92,10 +100,11 @@ observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo propertyKeys strfun.Set // Set of property key names + zidMapper *zidMapper // Indexer data idxLog *logger.Logger idxStore store.Store idxAr *anteroomQueue @@ -140,11 +149,13 @@ idxLog: boxLog.Clone().Str("box", "index").Child(), idxStore: createIdxStore(rtConfig), idxAr: newAnteroomQueue(1000), idxReady: make(chan struct{}, 1), } - cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos} + mgr.zidMapper = NewZidMapper(mgr) + + cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos, Mapper: mgr.zidMapper} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { p, err := Connect(uri, authManager, &cdata) if err != nil { return nil, err @@ -338,11 +349,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 { @@ -354,12 +365,12 @@ } // 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() for _, bx := range mgr.boxes { @@ -369,14 +380,14 @@ } return nil } // ReIndex data of the given zettel. -func (mgr *Manager) ReIndex(_ context.Context, zid id.Zid) error { +func (mgr *Manager) ReIndex(ctx context.Context, zid id.Zid) error { mgr.mgrLog.Debug().Msg("ReIndex") - if mgr.State() != box.StartStateStarted { - return box.ErrStopped + if err := mgr.checkContinue(ctx); err != nil { + return err } mgr.infos <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} return nil } @@ -417,5 +428,12 @@ // 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() +} Index: box/manager/mapstore/mapstore.go ================================================================== --- box/manager/mapstore/mapstore.go +++ box/manager/mapstore/mapstore.go @@ -16,128 +16,137 @@ import ( "context" "fmt" "io" - "sort" + "slices" "strings" "sync" - "zettelstore.de/client.fossil/api" - "zettelstore.de/client.fossil/maps" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/maps" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) type zettelData struct { meta *meta.Meta // a local copy of the metadata, without computed keys - dead id.Slice // list of dead references in this zettel - forward id.Slice // list of forward references in this zettel - backward id.Slice // list of zettel that reference with zettel + 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.Slice - backward id.Slice + 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 stringRefs map[string]id.Slice - -type memStore struct { +type mapStore 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 + 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 &memStore{ + return &mapStore{ intern: make(map[string]string, 1024), idx: make(map[id.Zid]*zettelData), - dead: make(map[id.Zid]id.Slice), + dead: make(map[id.Zid]*id.Set), words: make(stringRefs), urls: make(stringRefs), } } -func (ms *memStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { +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 *memStore) Enrich(_ context.Context, m *meta.Meta) { +func (ms *mapStore) 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 { +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 len(zi.dead) > 0 { - m.Set(api.KeyDead, zi.dead.String()) + if !zi.dead.IsEmpty() { + m.Set(api.KeyDead, zi.dead.MetaString()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Clone()) - if len(zi.backward) > 0 { - m.Set(api.KeyBackward, zi.backward.String()) + if !zi.backward.IsEmpty() { + m.Set(api.KeyBackward, zi.backward.MetaString()) updated = true } - if len(zi.forward) > 0 { - m.Set(api.KeyForward, zi.forward.String()) - back = remRefs(back, zi.forward) + if !zi.forward.IsEmpty() { + m.Set(api.KeyForward, zi.forward.MetaString()) + back.ISubstract(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) + if !refs.backward.IsEmpty() { + m.Set(k, refs.backward.MetaString()) + back.ISubstract(refs.backward) updated = true } } - if len(back) > 0 { - m.Set(api.KeyBack, back.String()) + 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 *memStore) SearchEqual(word string) id.Set { +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.CopySlice(refs) + result = result.IUnion(refs) } if refs, ok := ms.urls[word]; ok { - result.CopySlice(refs) + result = result.IUnion(refs) } zid, err := id.Parse(word) if err != nil { return result } @@ -144,17 +153,16 @@ zi, ok := ms.idx[zid] if !ok { return result } - addBackwardZids(result, zid, zi) - 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 *memStore) SearchPrefix(prefix string) id.Set { +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 { @@ -173,19 +181,19 @@ return result } } for zid, zi := range ms.idx { if minZid <= zid && zid <= maxZid { - addBackwardZids(result, zid, zi) + 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 *memStore) SearchSuffix(suffix string) id.Set { +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 { @@ -199,19 +207,19 @@ for range l { modulo *= 10 } for zid, zi := range ms.idx { if uint64(zid)%modulo == val { - addBackwardZids(result, zid, zi) + 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 *memStore) SearchContains(s string) id.Set { +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 @@ -219,62 +227,63 @@ 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) + result = addBackwardZids(result, zid, zi) } } return result } -func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { +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.CopySlice(refs) + result.IUnion(refs) } for u, refs := range ms.urls { if !pred(u, s) { continue } - result.CopySlice(refs) + 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 addBackwardZids(result id.Set, zid id.Zid, zi *zettelData) { - // Must only be called if ms.mx is read-locked! - result.Add(zid) - result.CopySlice(zi.backward) - for _, mref := range zi.otherRefs { - result.CopySlice(mref.backward) - } -} - -func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { +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 = remRef(back, zid) + back = back.Remove(zid) } case meta.TypeIDSet: for _, val := range meta.ListFromValue(p.Value) { if zid, err := id.Parse(val); err == nil { - back = remRef(back, zid) + back = back.Remove(zid) } } } } return back } -func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { +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 { @@ -281,31 +290,31 @@ zi = &zettelData{} ziExist = false } // Is this zettel an old dead reference mentioned in other zettel? - var toCheck id.Set + var toCheck *id.Set if refs, ok := ms.dead[zidx.Zid]; ok { // These must be checked later again - toCheck = id.NewSet(refs...) + toCheck = refs delete(ms.dead, zidx.Zid) } zi.meta = m ms.updateDeadReferences(zidx, zi) ids := ms.updateForwardBackwardReferences(zidx, zi) - toCheck = toCheck.Copy(ids) + toCheck = toCheck.IUnion(ids) ids = ms.updateMetadataReferences(zidx, zi) - toCheck = toCheck.Copy(ids) + 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, @@ -320,19 +329,19 @@ return true } return strings.HasSuffix(key, meta.SuffixKeyRole) } -func (ms *memStore) internString(s string) string { +func (ms *mapStore) 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 { +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) { @@ -342,48 +351,48 @@ } } return copyM } -func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) { - // Must only be called if ms.mx is write-locked! - drefs := zidx.GetDeadRefs() - newRefs, remRefs := refsDiff(drefs, zi.dead) - zi.dead = drefs - for _, ref := range remRefs { - ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) - } - for _, ref := range newRefs { - ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) - } -} - -func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set { - // Must only be called if ms.mx is write-locked! - brefs := zidx.GetBackRefs() - newRefs, remRefs := refsDiff(brefs, zi.forward) - zi.forward = brefs - - var toCheck id.Set - for _, ref := range remRefs { - bzi := ms.getOrCreateEntry(ref) - bzi.backward = remRef(bzi.backward, zidx.Zid) - if bzi.meta == nil { - toCheck = toCheck.Add(ref) - } - } - for _, ref := range newRefs { - bzi := ms.getOrCreateEntry(ref) - bzi.backward = addRef(bzi.backward, zidx.Zid) - if bzi.meta == nil { - toCheck = toCheck.Add(ref) - } - } - return toCheck -} - -func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set { +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 @@ -391,69 +400,66 @@ ms.removeInverseMeta(zidx.Zid, key, mr.forward) } if zi.otherRefs == nil { zi.otherRefs = make(map[string]bidiRefs) } - var toCheck id.Set + var toCheck *id.Set for key, mrefs := range inverseRefs { mr := zi.otherRefs[key] - newRefs, remRefs := refsDiff(mrefs, mr.forward) + newRefs, remRefs := mr.forward.Diff(mrefs) mr.forward = mrefs zi.otherRefs[key] = mr - for _, ref := range newRefs { + 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 = addRef(bmr.backward, zidx.Zid) + 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 { - if refs, ok := srefs[word]; ok { - srefs[word] = addRef(refs, zid) - continue - } - srefs[word] = id.Slice{zid} + srefs[word] = srefs[word].Add(zid) } for _, word := range removeWords { refs, ok := srefs[word] if !ok { continue } - refs2 := remRef(refs, zid) - if len(refs2) == 0 { + refs = refs.Remove(zid) + if refs.IsEmpty() { delete(srefs, word) continue } - srefs[word] = refs2 + srefs[word] = refs } return next.Words() } -func (ms *memStore) getOrCreateEntry(zid id.Zid) *zettelData { +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 *memStore) RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set { +func (ms *mapStore) RenameZettel(_ context.Context, curZid, newZid id.Zid) *id.Set { ms.mx.Lock() defer ms.mx.Unlock() curZi, curFound := ms.idx[curZid] _, newFound := ms.idx[newZid] @@ -470,66 +476,58 @@ urls: copyStrings(ms.urls, curZi.urls, newZid), } ms.idx[newZid] = newZi toCheck := ms.doDeleteZettel(curZid) - toCheck = toCheck.CopySlice(ms.dead[newZid]) + toCheck = toCheck.IUnion(ms.dead[newZid]) delete(ms.dead, newZid) toCheck = toCheck.Add(newZid) // should update otherRefs return toCheck } func copyMeta(m *meta.Meta, newZid id.Zid) *meta.Meta { result := m.Clone() result.Zid = newZid return result } -func (ms *memStore) copyDeadReferences(curDead id.Slice) id.Slice { - // Must only be called if ms.mx is write-locked! - if l := len(curDead); l > 0 { - result := make(id.Slice, l) - for i, ref := range curDead { - result[i] = ref - ms.dead[ref] = addRef(ms.dead[ref], ref) - } - return result - } - return nil -} -func (ms *memStore) copyForward(curForward id.Slice, newZid id.Zid) id.Slice { - // Must only be called if ms.mx is write-locked! - if l := len(curForward); l > 0 { - result := make(id.Slice, l) - for i, ref := range curForward { - result[i] = ref - if fzi, found := ms.idx[ref]; found { - fzi.backward = addRef(fzi.backward, newZid) - } - } - return result - } - return nil + +func (ms *mapStore) copyDeadReferences(curDead *id.Set) *id.Set { + // Must only be called if ms.mx is write-locked! + curDead.ForEach(func(ref id.Zid) { + ms.dead[ref] = ms.dead[ref].Add(ref) + }) + return curDead.Clone() +} +func (ms *mapStore) copyForward(curForward *id.Set, newZid id.Zid) *id.Set { + // Must only be called if ms.mx is write-locked! + curForward.ForEach(func(ref id.Zid) { + if fzi, found := ms.idx[ref]; found { + fzi.backward = fzi.backward.Add(newZid) + } + + }) + return curForward.Clone() } func copyStrings(msStringMap stringRefs, curStrings []string, newZid id.Zid) []string { // Must only be called if ms.mx is write-locked! if l := len(curStrings); l > 0 { result := make([]string, l) for i, s := range curStrings { result[i] = s - msStringMap[s] = addRef(msStringMap[s], newZid) + msStringMap[s] = msStringMap[s].Add(newZid) } return result } return nil } -func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set { +func (ms *mapStore) DeleteZettel(_ context.Context, zid id.Zid) *id.Set { ms.mx.Lock() defer ms.mx.Unlock() return ms.doDeleteZettel(zid) } -func (ms *memStore) doDeleteZettel(zid id.Zid) id.Set { +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 } @@ -543,81 +541,97 @@ deleteStrings(ms.urls, zi.urls, zid) delete(ms.idx, zid) return toCheck } -func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelData) { - // Must only be called if ms.mx is write-locked! - for _, ref := range zi.dead { - if drefs, ok := ms.dead[ref]; ok { - drefs = remRef(drefs, zid) - if len(drefs) > 0 { - ms.dead[ref] = drefs - } else { - delete(ms.dead, ref) - } - } - } -} - -func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelData) id.Set { - // Must only be called if ms.mx is write-locked! - for _, ref := range zi.forward { - if fzi, ok := ms.idx[ref]; ok { - fzi.backward = remRef(fzi.backward, zid) - } - } - var toCheck id.Set - for _, ref := range zi.backward { - if bzi, ok := ms.idx[ref]; ok { - bzi.forward = remRef(bzi.forward, zid) - toCheck = toCheck.Add(ref) - } - } +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 *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { +func (ms *mapStore) removeInverseMeta(zid id.Zid, key string, forward *id.Set) { // Must only be called if ms.mx is write-locked! - for _, ref := range forward { + forward.ForEach(func(ref id.Zid) { bzi, ok := ms.idx[ref] if !ok || bzi.otherRefs == nil { - continue + return } bmr, ok := bzi.otherRefs[key] if !ok { - continue + return } - bmr.backward = remRef(bmr.backward, zid) - if len(bmr.backward) > 0 || len(bmr.forward) > 0 { + 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 } - refs2 := remRef(refs, zid) - if len(refs2) == 0 { + refs = refs.Remove(zid) + if refs.IsEmpty() { delete(msStringMap, word) continue } - msStringMap[word] = refs2 + 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 *memStore) ReadStats(st *store.Stats) { +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() @@ -624,11 +638,11 @@ ms.mxStats.Lock() st.Updates = ms.updates ms.mxStats.Unlock() } -func (ms *memStore) Dump(w io.Writer) { +func (ms *mapStore) Dump(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() io.WriteString(w, "=== Dump\n") ms.dumpIndex(w) @@ -635,11 +649,11 @@ ms.dumpDead(w) dumpStringRefs(w, "Words", "", "", ms.words) dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) } -func (ms *memStore) dumpIndex(w io.Writer) { +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)) @@ -648,26 +662,32 @@ } zids.Sort() for _, id := range zids { fmt.Fprintln(w, "=====", id) zi := ms.idx[id] - if len(zi.dead) > 0 { + if !zi.dead.IsEmpty() { fmt.Fprintln(w, "* Dead:", zi.dead) } - dumpZids(w, "* Forward:", zi.forward) - dumpZids(w, "* Backward:", zi.backward) - for k, fb := range zi.otherRefs { + 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) - dumpZids(w, "** Forward:", fb.forward) - dumpZids(w, "** Backward:", fb.backward) + 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 *memStore) dumpDead(w io.Writer) { +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)) @@ -679,32 +699,30 @@ fmt.Fprintln(w, ";", id) fmt.Fprintln(w, ":", ms.dead[id]) } } -func dumpZids(w io.Writer, prefix string, zids id.Slice) { - if len(zids) > 0 { +func dumpSet(w io.Writer, prefix string, s *id.Set) { + if !s.IsEmpty() { io.WriteString(w, prefix) - for _, zid := range zids { + 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) - sort.Strings(sl) + 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 DELETED box/manager/mapstore/refs.go Index: box/manager/mapstore/refs.go ================================================================== --- box/manager/mapstore/refs.go +++ /dev/null @@ -1,105 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package mapstore - -import ( - "slices" - - "zettelstore.de/z/zettel/id" -) - -func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { - npos, opos := 0, 0 - for npos < len(refsN) && opos < len(refsO) { - rn, ro := refsN[npos], refsO[opos] - if rn == ro { - npos++ - opos++ - continue - } - if rn < ro { - newRefs = append(newRefs, rn) - npos++ - continue - } - remRefs = append(remRefs, ro) - opos++ - } - if npos < len(refsN) { - newRefs = append(newRefs, refsN[npos:]...) - } - if opos < len(refsO) { - remRefs = append(remRefs, refsO[opos:]...) - } - return newRefs, remRefs -} - -func addRef(refs id.Slice, ref id.Zid) id.Slice { - hi := len(refs) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if r := refs[m]; r == ref { - return refs - } else if r < ref { - lo = m + 1 - } else { - hi = m - } - } - refs = slices.Insert(refs, hi, ref) - return refs -} - -func remRefs(refs, rem id.Slice) id.Slice { - if len(refs) == 0 || len(rem) == 0 { - return refs - } - result := make(id.Slice, 0, len(refs)) - rpos, dpos := 0, 0 - for rpos < len(refs) && dpos < len(rem) { - rr, dr := refs[rpos], rem[dpos] - if rr < dr { - result = append(result, rr) - rpos++ - continue - } - if dr < rr { - dpos++ - continue - } - rpos++ - dpos++ - } - if rpos < len(refs) { - result = append(result, refs[rpos:]...) - } - return result -} - -func remRef(refs id.Slice, ref id.Zid) id.Slice { - hi := len(refs) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if r := refs[m]; r == ref { - copy(refs[m:], refs[m+1:]) - refs = refs[:len(refs)-1] - return refs - } else if r < ref { - lo = m + 1 - } else { - hi = m - } - } - return refs -} DELETED box/manager/mapstore/refs_test.go Index: box/manager/mapstore/refs_test.go ================================================================== --- box/manager/mapstore/refs_test.go +++ /dev/null @@ -1,140 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package mapstore - -import ( - "testing" - - "zettelstore.de/z/zettel/id" -) - -func assertRefs(t *testing.T, i int, got, exp id.Slice) { - t.Helper() - if got == nil && exp != nil { - t.Errorf("%d: got nil, but expected %v", i, exp) - return - } - if got != nil && exp == nil { - t.Errorf("%d: expected nil, but got %v", i, got) - return - } - if len(got) != len(exp) { - t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) - return - } - for p, n := range exp { - if got := got[p]; got != id.Zid(n) { - t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) - } - } -} - -func TestRefsDiff(t *testing.T) { - t.Parallel() - testcases := []struct { - in1, in2 id.Slice - exp1, exp2 id.Slice - }{ - {nil, nil, nil, nil}, - {id.Slice{1}, nil, id.Slice{1}, nil}, - {nil, id.Slice{1}, nil, id.Slice{1}}, - {id.Slice{1}, id.Slice{1}, nil, nil}, - {id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil}, - {id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}}, - {id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}}, - } - for i, tc := range testcases { - got1, got2 := refsDiff(tc.in1, tc.in2) - assertRefs(t, i, got1, tc.exp1) - assertRefs(t, i, got2, tc.exp2) - } -} - -func TestAddRef(t *testing.T) { - t.Parallel() - testcases := []struct { - ref id.Slice - zid uint - exp id.Slice - }{ - {nil, 5, id.Slice{5}}, - {id.Slice{1}, 5, id.Slice{1, 5}}, - {id.Slice{10}, 5, id.Slice{5, 10}}, - {id.Slice{5}, 5, id.Slice{5}}, - {id.Slice{1, 10}, 5, id.Slice{1, 5, 10}}, - {id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}}, - } - for i, tc := range testcases { - got := addRef(tc.ref, id.Zid(tc.zid)) - assertRefs(t, i, got, tc.exp) - } -} - -func TestRemRefs(t *testing.T) { - t.Parallel() - testcases := []struct { - in1, in2 id.Slice - exp id.Slice - }{ - {nil, nil, nil}, - {nil, id.Slice{}, nil}, - {id.Slice{}, nil, id.Slice{}}, - {id.Slice{}, id.Slice{}, id.Slice{}}, - {id.Slice{1}, id.Slice{5}, id.Slice{1}}, - {id.Slice{10}, id.Slice{5}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{5}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{5}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}}, - {id.Slice{1}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}}, - {id.Slice{1}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}}, - } - for i, tc := range testcases { - got := remRefs(tc.in1, tc.in2) - assertRefs(t, i, got, tc.exp) - } -} - -func TestRemRef(t *testing.T) { - t.Parallel() - testcases := []struct { - ref id.Slice - zid uint - exp id.Slice - }{ - {nil, 5, nil}, - {id.Slice{}, 5, id.Slice{}}, - {id.Slice{5}, 5, id.Slice{}}, - {id.Slice{1}, 5, id.Slice{1}}, - {id.Slice{10}, 5, id.Slice{10}}, - {id.Slice{1, 5}, 5, id.Slice{1}}, - {id.Slice{5, 10}, 5, id.Slice{10}}, - {id.Slice{1, 5, 10}, 5, id.Slice{1, 10}}, - } - for i, tc := range testcases { - got := remRef(tc.ref, id.Zid(tc.zid)) - assertRefs(t, i, got, tc.exp) - } -} Index: box/manager/store/store.go ================================================================== --- box/manager/store/store.go +++ box/manager/store/store.go @@ -49,19 +49,22 @@ // 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 // RenameZettel changes all references of current zettel identifier to new // zettel identifier. - RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set + RenameZettel(_ context.Context, curZid, newZid id.Zid) *id.Set // DeleteZettel removes index data for given zettel. // Returns set of zettel identifier that must also be checked for changes. - DeleteZettel(context.Context, id.Zid) id.Set + 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. Index: box/manager/store/wordset_test.go ================================================================== --- box/manager/store/wordset_test.go +++ box/manager/store/wordset_test.go @@ -12,11 +12,11 @@ //----------------------------------------------------------------------------- package store_test import ( - "sort" + "slices" "testing" "zettelstore.de/z/box/manager/store" ) @@ -25,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 @@ -18,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. @@ -33,20 +33,18 @@ 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.Add(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 { @@ -66,30 +64,30 @@ // 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 } ADDED box/manager/zidmapper.go Index: box/manager/zidmapper.go ================================================================== --- /dev/null +++ box/manager/zidmapper.go @@ -0,0 +1,199 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern +//----------------------------------------------------------------------------- + +package manager + +import ( + "context" + "maps" + "sync" + "time" + + "zettelstore.de/z/zettel/id" +) + +// zidMapper transforms old-style zettel identifier (14 digits) into new one (4 alphanums). +// +// Since there are no new-style identifier defined, there is only support for old-style +// identifier by checking, whether they are suported as new-style or not. +// +// This will change in later versions. +type zidMapper struct { + fetcher zidfetcher + defined map[id.Zid]id.ZidN // predefined mapping, constant after creation + mx sync.RWMutex // protect toNew ... nextZidN + toNew map[id.Zid]id.ZidN // working mapping old->new + toOld map[id.ZidN]id.Zid // working mapping new->old + nextZidM id.ZidN // next zid for manual + hadManual bool + nextZidN id.ZidN // next zid for normal zettel +} + +type zidfetcher interface { + fetchZids(context.Context) (*id.Set, error) +} + +// NewZidMapper creates a new ZipMapper. +func NewZidMapper(fetcher zidfetcher) *zidMapper { + defined := map[id.Zid]id.ZidN{ + id.Invalid: id.InvalidN, + 1: id.MustParseN("0001"), // ZidVersion + 2: id.MustParseN("0002"), // ZidHost + 3: id.MustParseN("0003"), // ZidOperatingSystem + 4: id.MustParseN("0004"), // ZidLicense + 5: id.MustParseN("0005"), // ZidAuthors + 6: id.MustParseN("0006"), // ZidDependencies + 7: id.MustParseN("0007"), // ZidLog + 8: id.MustParseN("0008"), // ZidMemory + 9: id.MustParseN("0009"), // ZidSx + 10: id.MustParseN("000a"), // ZidHTTP + 11: id.MustParseN("000b"), // ZidAPI + 12: id.MustParseN("000c"), // ZidWebUI + 13: id.MustParseN("000d"), // ZidConsole + 20: id.MustParseN("000e"), // ZidBoxManager + 21: id.MustParseN("000f"), // ZidZettel + 22: id.MustParseN("000g"), // ZidIndex + 23: id.MustParseN("000h"), // ZidQuery + 90: id.MustParseN("000i"), // ZidMetadataKey + 92: id.MustParseN("000j"), // ZidParser + 96: id.MustParseN("000k"), // ZidStartupConfiguration + 100: id.MustParseN("000l"), // ZidRuntimeConfiguration + 101: id.MustParseN("000m"), // ZidDirectory + 102: id.MustParseN("000n"), // ZidWarnings + 10100: id.MustParseN("000s"), // Base HTML Template + 10200: id.MustParseN("000t"), // Login Form Template + 10300: id.MustParseN("000u"), // List Zettel Template + 10401: id.MustParseN("000v"), // Detail Template + 10402: id.MustParseN("000w"), // Info Template + 10403: id.MustParseN("000x"), // Form Template + 10404: id.MustParseN("001z"), // Rename Form Template (will be removed in the future) + 10405: id.MustParseN("000y"), // Delete Template + 10700: id.MustParseN("000z"), // Error Template + 19000: id.MustParseN("000q"), // Sxn Start Code + 19990: id.MustParseN("000r"), // Sxn Base Code + 20001: id.MustParseN("0010"), // Base CSS + 25001: id.MustParseN("0011"), // User CSS + 40001: id.MustParseN("000o"), // Generic Emoji + 59900: id.MustParseN("000p"), // Sxn Prelude + 60010: id.MustParseN("0012"), // zettel + 60020: id.MustParseN("0013"), // confguration + 60030: id.MustParseN("0014"), // role + 60040: id.MustParseN("0015"), // tag + 90000: id.MustParseN("0016"), // New Menu + 90001: id.MustParseN("0017"), // New Zettel + 90002: id.MustParseN("0018"), // New User + 90003: id.MustParseN("0019"), // New Tag + 90004: id.MustParseN("001a"), // New Role + // 100000000, // Manual -> 0020-00yz + 9999999997: id.MustParseN("00zx"), // ZidSession + 9999999998: id.MustParseN("00zy"), // ZidAppDirectory + 9999999999: id.MustParseN("00zz"), // ZidMapping + 10000000000: id.MustParseN("0100"), // ZidDefaultHome + } + toNew := maps.Clone(defined) + toOld := make(map[id.ZidN]id.Zid, len(toNew)) + for o, n := range toNew { + if _, found := toOld[n]; found { + panic("duplicate predefined zid") + } + toOld[n] = o + } + + return &zidMapper{ + fetcher: fetcher, + defined: defined, + toNew: toNew, + toOld: toOld, + nextZidM: id.MustParseN("0020"), + hadManual: false, + nextZidN: id.MustParseN("0101"), + } +} + +// isWellDefined returns true, if the given zettel identifier is predefined +// (as stated in the manual), or is part of the manual itself, or is greater than +// 19699999999999. +func (zm *zidMapper) isWellDefined(zid id.Zid) bool { + if _, found := zm.defined[zid]; found || (1000000000 <= zid && zid <= 1099999999) { + return true + } + if _, err := time.Parse("20060102150405", zid.String()); err != nil { + return false + } + return 19700000000000 <= zid +} + +// Warnings returns all zettel identifier with warnings. +func (zm *zidMapper) Warnings(ctx context.Context) (*id.Set, error) { + allZids, err := zm.fetcher.fetchZids(ctx) + if err != nil { + return nil, err + } + warnings := id.NewSet() + allZids.ForEach(func(zid id.Zid) { + if !zm.isWellDefined(zid) { + warnings = warnings.Add(zid) + } + }) + return warnings, nil +} + +func (zm *zidMapper) GetZidN(zidO id.Zid) id.ZidN { + zm.mx.RLock() + if zidN, found := zm.toNew[zidO]; found { + zm.mx.RUnlock() + return zidN + } + zm.mx.RUnlock() + + zm.mx.Lock() + defer zm.mx.Unlock() + // Double check to avoid races + if zidN, found := zm.toNew[zidO]; found { + return zidN + } + + if 1000000000 <= zidO && zidO <= 1099999999 { + if zidO == 1000000000 { + zm.hadManual = true + } + if zm.hadManual { + zidN := zm.nextZidM + zm.nextZidM++ + zm.toNew[zidO] = zidN + zm.toOld[zidN] = zidO + return zidN + } + } + + zidN := zm.nextZidN + zm.nextZidN++ + zm.toNew[zidO] = zidN + zm.toOld[zidN] = zidO + return zidN +} + +// OldToNewMapping returns the mapping of old format identifier to new format identifier. +func (zm *zidMapper) OldToNewMapping(ctx context.Context) (map[id.Zid]id.ZidN, error) { + allZids, err := zm.fetcher.fetchZids(ctx) + if err != nil { + return nil, err + } + + result := make(map[id.Zid]id.ZidN, allZids.Length()) + allZids.ForEach(func(zidO id.Zid) { + zidN := zm.GetZidN(zidO) + result[zidO] = zidN + }) + return result, nil +} Index: box/membox/membox.go ================================================================== --- box/membox/membox.go +++ box/membox/membox.go @@ -113,10 +113,11 @@ meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() + mb.notifyChanged(zid) mb.log.Trace().Zid(zid).Msg("CreateZettel") return zid, nil } Index: box/notify/entry.go ================================================================== --- box/notify/entry.go +++ box/notify/entry.go @@ -14,11 +14,11 @@ 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" ) Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -18,12 +18,12 @@ "flag" "fmt" "io" "os" - "zettelstore.de/client.fossil/api" - "zettelstore.de/client.fossil/input" + "t73f.de/r/zsc/api" + "t73f.de/r/zsc/input" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" Index: cmd/cmd_password.go ================================================================== --- cmd/cmd_password.go +++ cmd/cmd_password.go @@ -18,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/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -14,11 +14,11 @@ 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 @@ -23,12 +23,12 @@ "runtime/debug" "strconv" "strings" "time" - "zettelstore.de/client.fossil/api" - "zettelstore.de/client.fossil/input" + "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" 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,11 @@ id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk -created: 20210301190630 +created: 20210126175322 modified: 20231125185455 show-back-links: false * [[Introduction|00001001000000]] * [[Design goals|00001002000000]] 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,10 +1,11 @@ id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk +created: 20210126175322 modified: 20220119145756 === The curious user You just want to check out the Zettelstore software 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,10 +1,11 @@ id: 00001003305000 title: Enable Zettelstore to start automatically on Windows role: manual tags: #installation #manual #zettelstore syntax: zmk +created: 20211125191727 modified: 20220218125541 Windows is a complicated beast. There are several ways to automatically start Zettelstore. === Startup folder 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: 20240220190138 +modified: 20240710183532 -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,30 @@ 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 the 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"". 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 ``