Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From version-0.0.13 To version-0.0.12
2021-06-07
| ||
09:11 | Increase version to 0.0.14-dev to begin next development cycle ... (check-in: 7dd6f4dd5c user: stern tags: trunk) | |
2021-06-01
| ||
12:35 | Version 0.0.13 ... (check-in: 11d9b6da63 user: stern tags: trunk, release, version-0.0.13) | |
10:14 | Log output while starting Command Line Server ... (check-in: 968a91bbaa user: stern tags: trunk) | |
2021-04-17
| ||
12:34 | Increase version to 0.0.13-dev to begin next development cycle ... (check-in: 5f0c8f2d4c user: stern tags: trunk) | |
2021-04-16
| ||
16:16 | Version 0.0.12 ... (check-in: 86f8bc8a70 user: stern tags: trunk, release, version-0.0.12) | |
16:08 | Show default dir place type in startup values zettel ... (check-in: 8269a7cbc4 user: stern tags: trunk) | |
Changes to VERSION.
|
| | | 1 | 0.0.12 |
Changes to ast/ast.go.
︙ | ︙ | |||
79 80 81 82 83 84 85 | } // RefState indicates the state of the reference. type RefState int // Constants for RefState const ( | | | 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | } // RefState indicates the state of the reference. type RefState int // Constants for RefState const ( RefStateInvalid RefState = iota // Invalid Referende RefStateZettel // Reference to an internal zettel RefStateSelf // Reference to same zettel with a fragment RefStateFound // Reference to an existing internal zettel RefStateBroken // Reference to a non-existing internal zettel RefStateHosted // Reference to local hosted non-Zettel, without URL change RefStateBased // Reference to local non-Zettel, to be prefixed RefStateExternal // Reference to external material |
︙ | ︙ |
Changes to ast/ref.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "net/url" "zettelstore.de/z/domain/id" ) // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { | | < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | "net/url" "zettelstore.de/z/domain/id" ) // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { if s == "" { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } if state, ok := localState(s); ok { if state == RefStateBased { s = s[1:] } u, err := url.Parse(s) |
︙ | ︙ |
Changes to ast/ref_test.go.
︙ | ︙ | |||
44 45 46 47 48 49 50 | testcases := []struct { link string isZettel bool isExternal bool isLocal bool }{ {"", false, false, false}, | < | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | testcases := []struct { link string isZettel bool isExternal bool isLocal bool }{ {"", false, false, false}, {"http://zettelstore.de/z/ast", false, true, false}, {"12345678901234", true, false, false}, {"12345678901234#local", true, false, false}, {"http://12345678901234", false, true, false}, {"http://zettelstore.de/z/12345678901234", false, true, false}, {"http://zettelstore.de/12345678901234", false, true, false}, {"/12345678901234", false, false, true}, |
︙ | ︙ |
Deleted auth/auth.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted auth/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to auth/policy/anon.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorization policies. package policy import ( | < < | > > | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorization policies. package policy import ( "zettelstore.de/z/domain/meta" ) type anonPolicy struct { simpleMode bool expertMode func() bool getVisibility func(*meta.Meta) meta.Visibility pre Policy } func (ap *anonPolicy) CanCreate(user, newMeta *meta.Meta) bool { return ap.pre.CanCreate(user, newMeta) } func (ap *anonPolicy) CanRead(user, m *meta.Meta) bool { |
︙ | ︙ | |||
39 40 41 42 43 44 45 | } func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool { return ap.pre.CanDelete(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) checkVisibility(m *meta.Meta) bool { | > > > | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | } func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool { return ap.pre.CanDelete(user, m) && ap.checkVisibility(m) } func (ap *anonPolicy) checkVisibility(m *meta.Meta) bool { switch ap.getVisibility(m) { case meta.VisibilitySimple: return ap.simpleMode || ap.expertMode() case meta.VisibilityExpert: return ap.expertMode() } return true } |
Changes to auth/policy/default.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( | | | < < | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/meta" ) type defaultPolicy struct{} func (d *defaultPolicy) CanCreate(user, newMeta *meta.Meta) bool { return true } func (d *defaultPolicy) CanRead(user, m *meta.Meta) bool { return true } func (d *defaultPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return d.canChange(user, oldMeta) } func (d *defaultPolicy) CanRename(user, m *meta.Meta) bool { return d.canChange(user, m) } |
︙ | ︙ | |||
38 39 40 41 42 43 44 | // See owner.go:CanWrite. // No authentication: check for owner-like restriction, because the user // acts as an owner return metaRo != meta.ValueUserRoleOwner && !meta.BoolValue(metaRo) } | | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // See owner.go:CanWrite. // No authentication: check for owner-like restriction, because the user // acts as an owner return metaRo != meta.ValueUserRoleOwner && !meta.BoolValue(metaRo) } userRole := runtime.GetUserRole(user) switch metaRo { case meta.ValueUserRoleReader: return userRole > meta.UserRoleReader case meta.ValueUserRoleWriter: return userRole > meta.UserRoleWriter case meta.ValueUserRoleOwner: return userRole > meta.UserRoleOwner } return !meta.BoolValue(metaRo) } |
Changes to auth/policy/owner.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( | | | > | < > | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) type ownerPolicy struct { expertMode func() bool isOwner func(id.Zid) bool getVisibility func(*meta.Meta) meta.Visibility pre Policy } func (o *ownerPolicy) CanCreate(user, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanCreate(user, newMeta) { return false } return o.userIsOwner(user) || o.userCanCreate(user, newMeta) } func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool { if runtime.GetUserRole(user) == meta.UserRoleReader { return false } if role, ok := newMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { return false } return true } func (o *ownerPolicy) CanRead(user, m *meta.Meta) bool { // No need to call o.pre.CanRead(user, meta), because it will always return true. // Both the default and the readonly policy allow to read a zettel. vis := o.getVisibility(m) if res, ok := o.checkVisibility(user, vis); ok { return res } return o.userIsOwner(user) || o.userCanRead(user, m, vis) } func (o *ownerPolicy) userCanRead(user, m *meta.Meta, vis meta.Visibility) bool { switch vis { case meta.VisibilityOwner, meta.VisibilitySimple, meta.VisibilityExpert: return false case meta.VisibilityPublic: return true } if user == nil { return false } |
︙ | ︙ | |||
74 75 76 77 78 79 80 | meta.KeyUserRole, } func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) { return false } | | | | | > | | | | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | meta.KeyUserRole, } func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) { return false } vis := o.getVisibility(oldMeta) if res, ok := o.checkVisibility(user, vis); ok { return res } if o.userIsOwner(user) { return true } if !o.userCanRead(user, oldMeta, vis) { return false } if role, ok := oldMeta.Get(meta.KeyRole); ok && role == meta.ValueRoleUser { // Here we know, that user.Zid == newMeta.Zid (because of userCanRead) and // user.Zid == newMeta.Zid (because oldMeta.Zid == newMeta.Zid) for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false } } return true } if runtime.GetUserRole(user) == meta.UserRoleReader { return false } return o.userCanCreate(user, newMeta) } func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool { if user == nil || !o.pre.CanRename(user, m) { return false } if res, ok := o.checkVisibility(user, o.getVisibility(m)); ok { return res } return o.userIsOwner(user) } func (o *ownerPolicy) CanDelete(user, m *meta.Meta) bool { if user == nil || !o.pre.CanDelete(user, m) { return false } if res, ok := o.checkVisibility(user, o.getVisibility(m)); ok { return res } return o.userIsOwner(user) } func (o *ownerPolicy) checkVisibility(user *meta.Meta, vis meta.Visibility) (bool, bool) { switch vis { case meta.VisibilitySimple, meta.VisibilityExpert: return o.userIsOwner(user) && o.expertMode(), true } return false, false } func (o *ownerPolicy) userIsOwner(user *meta.Meta) bool { if user == nil { return false } if o.isOwner(user.Zid) { return true } if val, ok := user.Get(meta.KeyUserRole); ok && val == meta.ValueUserRoleOwner { return true } return false } |
Changes to auth/policy/place.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "context" | < < < | < < > | > > > > | | | < | | < | | | | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "context" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/search" "zettelstore.de/z/web/session" ) // PlaceWithPolicy wraps the given place inside a policy place. func PlaceWithPolicy( place place.Place, simpleMode bool, withAuth func() bool, isReadOnlyMode bool, expertMode func() bool, isOwner func(id.Zid) bool, getVisibility func(*meta.Meta) meta.Visibility, ) (place.Place, Policy) { pol := newPolicy(simpleMode, withAuth, isReadOnlyMode, expertMode, isOwner, getVisibility) return newPlace(place, pol), pol } // polPlace implements a policy place. type polPlace struct { place place.Place policy Policy } // newPlace creates a new policy place. func newPlace(place place.Place, policy Policy) place.Place { return &polPlace{ place: place, policy: policy, } } func (pp *polPlace) Location() string { return pp.place.Location() } func (pp *polPlace) CanCreateZettel(ctx context.Context) bool { return pp.place.CanCreateZettel(ctx) } func (pp *polPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { user := session.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.place.CreateZettel(ctx, zettel) } return id.Invalid, place.NewErrNotAllowed("Create", user, id.Invalid) } func (pp *polPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { zettel, err := pp.place.GetZettel(ctx, zid) if err != nil { return domain.Zettel{}, err } user := session.GetUser(ctx) if pp.policy.CanRead(user, zettel.Meta) { return zettel, nil } return domain.Zettel{}, place.NewErrNotAllowed("GetZettel", user, zid) } func (pp *polPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.place.GetMeta(ctx, zid) if err != nil { return nil, err } user := session.GetUser(ctx) if pp.policy.CanRead(user, m) { return m, nil } return nil, place.NewErrNotAllowed("GetMeta", user, zid) } func (pp *polPlace) FetchZids(ctx context.Context) (id.Set, error) { return nil, place.NewErrNotAllowed("fetch-zids", session.GetUser(ctx), id.Invalid) } func (pp *polPlace) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { user := session.GetUser(ctx) canRead := pp.policy.CanRead s = s.AddPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) return pp.place.SelectMeta(ctx, s) } func (pp *polPlace) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return pp.place.CanUpdateZettel(ctx, zettel) } func (pp *polPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { zid := zettel.Meta.Zid user := session.GetUser(ctx) if !zid.IsValid() { return &place.ErrInvalidID{Zid: zid} } // Write existing zettel oldMeta, err := pp.place.GetMeta(ctx, zid) if err != nil { return err |
︙ | ︙ | |||
129 130 131 132 133 134 135 | } func (pp *polPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { meta, err := pp.place.GetMeta(ctx, curZid) if err != nil { return err } | | | < < < < | 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | } func (pp *polPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { meta, err := pp.place.GetMeta(ctx, curZid) if err != nil { return err } user := session.GetUser(ctx) if pp.policy.CanRename(user, meta) { return pp.place.RenameZettel(ctx, curZid, newZid) } return place.NewErrNotAllowed("Rename", user, curZid) } func (pp *polPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return pp.place.CanDeleteZettel(ctx, zid) } func (pp *polPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { meta, err := pp.place.GetMeta(ctx, zid) if err != nil { return err } user := session.GetUser(ctx) if pp.policy.CanDelete(user, meta) { return pp.place.DeleteZettel(ctx, zid) } return place.NewErrNotAllowed("Delete", user, zid) } func (pp *polPlace) ReadStats(st *place.Stats) { pp.place.ReadStats(st) } |
Changes to auth/policy/policy.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( | | | > | > > > > | > > > > > > > > > > > > | > > > > > > > | | | | > | < > | | > > | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | // under this license. //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Policy is an interface for checking access authorization. type Policy interface { // User is allowed to create a new zettel. CanCreate(user, newMeta *meta.Meta) bool // User is allowed to read zettel CanRead(user, m *meta.Meta) bool // User is allowed to write zettel. CanWrite(user, oldMeta, newMeta *meta.Meta) bool // User is allowed to rename zettel CanRename(user, m *meta.Meta) bool // User is allowed to delete zettel CanDelete(user, m *meta.Meta) bool } // newPolicy creates a policy based on given constraints. func newPolicy( simpleMode bool, withAuth func() bool, isReadOnlyMode bool, expertMode func() bool, isOwner func(id.Zid) bool, getVisibility func(*meta.Meta) meta.Visibility, ) Policy { var pol Policy if isReadOnlyMode { pol = &roPolicy{} } else { pol = &defaultPolicy{} } if withAuth() { pol = &ownerPolicy{ expertMode: expertMode, isOwner: isOwner, getVisibility: getVisibility, pre: pol, } } else { pol = &anonPolicy{ simpleMode: simpleMode, expertMode: expertMode, getVisibility: getVisibility, pre: pol, } } return &prePolicy{pol} } type prePolicy struct { post Policy } func (p *prePolicy) CanCreate(user, newMeta *meta.Meta) bool { return newMeta != nil && p.post.CanCreate(user, newMeta) } func (p *prePolicy) CanRead(user, m *meta.Meta) bool { |
︙ | ︙ |
Changes to auth/policy/policy_test.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "fmt" "testing" | < > | > | > | > | > | > | > | > | > | < | > > > > | > > > > > | | | | | | | < < | < | | | | < < < < < < < < < < < < < < < < < < < < < < < < < | > > | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "fmt" "testing" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { testScene := []struct { simple bool readonly bool withAuth bool expert bool }{ {true, true, true, true}, {true, true, true, false}, {true, true, false, true}, {true, true, false, false}, {true, false, true, true}, {true, false, true, false}, {true, false, false, true}, {true, false, false, false}, {false, true, true, true}, {false, true, true, false}, {false, true, false, true}, {false, true, false, false}, {false, false, true, true}, {false, false, true, false}, {false, false, false, true}, {false, false, false, false}, } for _, ts := range testScene { var authFunc func() bool if ts.withAuth { authFunc = withAuth } else { authFunc = withoutAuth } var expertFunc func() bool if ts.expert { expertFunc = expertMode } else { expertFunc = noExpertMode } pol := newPolicy(ts.simple, authFunc, ts.readonly, expertFunc, isOwner, getVisibility) name := fmt.Sprintf("simple=%v/readonly=%v/withauth=%v/expert=%v", ts.simple, ts.readonly, ts.withAuth, ts.expert) t.Run(name, func(tt *testing.T) { testCreate(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) testRead(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) testWrite(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) testRename(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) testDelete(tt, pol, ts.simple, ts.withAuth, ts.readonly, ts.expert) }) } } func withAuth() bool { return true } func withoutAuth() bool { return false } func expertMode() bool { return true } func noExpertMode() bool { return false } func isOwner(zid id.Zid) bool { return zid == ownerZid } func getVisibility(m *meta.Meta) meta.Visibility { if vis, ok := m.Get(meta.KeyVisibility); ok { switch vis { case meta.ValueVisibilityPublic: return meta.VisibilityPublic case meta.ValueVisibilityOwner: return meta.VisibilityOwner case meta.ValueVisibilityExpert: return meta.VisibilityExpert case meta.ValueVisibilitySimple: return meta.VisibilitySimple } } return meta.VisibilityLogin } func testCreate(t *testing.T, pol Policy, simple, withAuth, readonly, isExpert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() |
︙ | ︙ | |||
139 140 141 142 143 144 145 | if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } | | > | 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testRead(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() simpleZettel := newSimpleZettel() userZettel := newUserZettel() testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta |
︙ | ︙ | |||
193 194 195 196 197 198 199 200 201 202 203 204 205 206 | {owner2, ownerZettel, true}, // Expert zettel {anonUser, expertZettel, !withAuth && expert}, {reader, expertZettel, !withAuth && expert}, {writer, expertZettel, !withAuth && expert}, {owner, expertZettel, expert}, {owner2, expertZettel, expert}, // Other user zettel {anonUser, userZettel, !withAuth}, {reader, userZettel, !withAuth}, {writer, userZettel, !withAuth}, {owner, userZettel, true}, {owner2, userZettel, true}, // Own user zettel | > > > > > > | 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | {owner2, ownerZettel, true}, // Expert zettel {anonUser, expertZettel, !withAuth && expert}, {reader, expertZettel, !withAuth && expert}, {writer, expertZettel, !withAuth && expert}, {owner, expertZettel, expert}, {owner2, expertZettel, expert}, // Simple expert zettel {anonUser, simpleZettel, !withAuth && (simple || expert)}, {reader, simpleZettel, !withAuth && (simple || expert)}, {writer, simpleZettel, !withAuth && (simple || expert)}, {owner, simpleZettel, (!withAuth && simple) || expert}, {owner2, simpleZettel, (!withAuth && simple) || expert}, // Other user zettel {anonUser, userZettel, !withAuth}, {reader, userZettel, !withAuth}, {writer, userZettel, !withAuth}, {owner, userZettel, true}, {owner2, userZettel, true}, // Own user zettel |
︙ | ︙ | |||
217 218 219 220 221 222 223 | if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } | | > < | 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testWrite(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() simpleZettel := newSimpleZettel() userZettel := newUserZettel() writerNew := writer.Clone() writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, "")) roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() testCases := []struct { user *meta.Meta old *meta.Meta new *meta.Meta exp bool }{ // No old and new meta |
︙ | ︙ | |||
269 270 271 272 273 274 275 | // Old an new zettel have different zettel identifier {anonUser, zettel, publicZettel, false}, {reader, zettel, publicZettel, false}, {writer, zettel, publicZettel, false}, {owner, zettel, publicZettel, false}, {owner2, zettel, publicZettel, false}, // Overwrite a normal zettel | | | | | | | | | | | | | > > > > > > | | | | | | | 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | // Old an new zettel have different zettel identifier {anonUser, zettel, publicZettel, false}, {reader, zettel, publicZettel, false}, {writer, zettel, publicZettel, false}, {owner, zettel, publicZettel, false}, {owner2, zettel, publicZettel, false}, // Overwrite a normal zettel {anonUser, zettel, zettel, !withAuth && !readonly}, {reader, zettel, zettel, !withAuth && !readonly}, {writer, zettel, zettel, !readonly}, {owner, zettel, zettel, !readonly}, {owner2, zettel, zettel, !readonly}, // Public zettel {anonUser, publicZettel, publicZettel, !withAuth && !readonly}, {reader, publicZettel, publicZettel, !withAuth && !readonly}, {writer, publicZettel, publicZettel, !readonly}, {owner, publicZettel, publicZettel, !readonly}, {owner2, publicZettel, publicZettel, !readonly}, // Login zettel {anonUser, loginZettel, loginZettel, !withAuth && !readonly}, {reader, loginZettel, loginZettel, !withAuth && !readonly}, {writer, loginZettel, loginZettel, !readonly}, {owner, loginZettel, loginZettel, !readonly}, {owner2, loginZettel, loginZettel, !readonly}, // Owner zettel {anonUser, ownerZettel, ownerZettel, !withAuth && !readonly}, {reader, ownerZettel, ownerZettel, !withAuth && !readonly}, {writer, ownerZettel, ownerZettel, !withAuth && !readonly}, {owner, ownerZettel, ownerZettel, !readonly}, {owner2, ownerZettel, ownerZettel, !readonly}, // Expert zettel {anonUser, expertZettel, expertZettel, !withAuth && !readonly && expert}, {reader, expertZettel, expertZettel, !withAuth && !readonly && expert}, {writer, expertZettel, expertZettel, !withAuth && !readonly && expert}, {owner, expertZettel, expertZettel, !readonly && expert}, {owner2, expertZettel, expertZettel, !readonly && expert}, // Simple expert zettel {anonUser, simpleZettel, expertZettel, !withAuth && !readonly && (simple || expert)}, {reader, simpleZettel, expertZettel, !withAuth && !readonly && (simple || expert)}, {writer, simpleZettel, expertZettel, !withAuth && !readonly && (simple || expert)}, {owner, simpleZettel, expertZettel, !readonly && ((!withAuth && simple) || expert)}, {owner2, simpleZettel, expertZettel, !readonly && ((!withAuth && simple) || expert)}, // Other user zettel {anonUser, userZettel, userZettel, !withAuth && !readonly}, {reader, userZettel, userZettel, !withAuth && !readonly}, {writer, userZettel, userZettel, !withAuth && !readonly}, {owner, userZettel, userZettel, !readonly}, {owner2, userZettel, userZettel, !readonly}, // Own user zettel {reader, reader, reader, !readonly}, {writer, writer, writer, !readonly}, {owner, owner, owner, !readonly}, {owner2, owner2, owner2, !readonly}, // Writer cannot change importand metadata of its own user zettel {writer, writer, writerNew, !withAuth && !readonly}, // No r/o zettel {anonUser, roFalse, roFalse, !withAuth && !readonly}, {reader, roFalse, roFalse, !withAuth && !readonly}, {writer, roFalse, roFalse, !readonly}, {owner, roFalse, roFalse, !readonly}, {owner2, roFalse, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, roReader, false}, {reader, roReader, roReader, false}, {writer, roReader, roReader, !readonly}, |
︙ | ︙ | |||
352 353 354 355 356 357 358 | if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } | | > < | | | | | | > > > > > > | | | | | 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 | if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testRename(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() simpleZettel := newSimpleZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel {anonUser, zettel, !withAuth && !readonly}, {reader, zettel, !withAuth && !readonly}, {writer, zettel, !withAuth && !readonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, !withAuth && !readonly && expert}, {reader, expertZettel, !withAuth && !readonly && expert}, {writer, expertZettel, !withAuth && !readonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // Simple expert zettel {anonUser, simpleZettel, !withAuth && !readonly && (simple || expert)}, {reader, simpleZettel, !withAuth && !readonly && (simple || expert)}, {writer, simpleZettel, !withAuth && !readonly && (simple || expert)}, {owner, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, {owner2, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, // No r/o zettel {anonUser, roFalse, !withAuth && !readonly}, {reader, roFalse, !withAuth && !readonly}, {writer, roFalse, !withAuth && !readonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {reader, roReader, false}, {writer, roReader, !withAuth && !readonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {reader, roWriter, false}, {writer, roWriter, false}, {owner, roWriter, !readonly}, |
︙ | ︙ | |||
431 432 433 434 435 436 437 | if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } | | > < | | | | | | > > > > > > | | | | | 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 | if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } } func testDelete(t *testing.T, pol Policy, simple, withAuth, readonly, expert bool) { t.Helper() anonUser := newAnon() reader := newReader() writer := newWriter() owner := newOwner() owner2 := newOwner2() zettel := newZettel() expertZettel := newExpertZettel() simpleZettel := newSimpleZettel() roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() testCases := []struct { user *meta.Meta meta *meta.Meta exp bool }{ // No meta {anonUser, nil, false}, {reader, nil, false}, {writer, nil, false}, {owner, nil, false}, {owner2, nil, false}, // Any zettel {anonUser, zettel, !withAuth && !readonly}, {reader, zettel, !withAuth && !readonly}, {writer, zettel, !withAuth && !readonly}, {owner, zettel, !readonly}, {owner2, zettel, !readonly}, // Expert zettel {anonUser, expertZettel, !withAuth && !readonly && expert}, {reader, expertZettel, !withAuth && !readonly && expert}, {writer, expertZettel, !withAuth && !readonly && expert}, {owner, expertZettel, !readonly && expert}, {owner2, expertZettel, !readonly && expert}, // Simple expert zettel {anonUser, simpleZettel, !withAuth && !readonly && (simple || expert)}, {reader, simpleZettel, !withAuth && !readonly && (simple || expert)}, {writer, simpleZettel, !withAuth && !readonly && (simple || expert)}, {owner, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, {owner2, simpleZettel, !readonly && ((!withAuth && simple) || expert)}, // No r/o zettel {anonUser, roFalse, !withAuth && !readonly}, {reader, roFalse, !withAuth && !readonly}, {writer, roFalse, !withAuth && !readonly}, {owner, roFalse, !readonly}, {owner2, roFalse, !readonly}, // Reader r/o zettel {anonUser, roReader, false}, {reader, roReader, false}, {writer, roReader, !withAuth && !readonly}, {owner, roReader, !readonly}, {owner2, roReader, !readonly}, // Writer r/o zettel {anonUser, roWriter, false}, {reader, roWriter, false}, {writer, roWriter, false}, {owner, roWriter, !readonly}, |
︙ | ︙ | |||
577 578 579 580 581 582 583 584 585 586 587 588 589 590 | return m } func newExpertZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Expert Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func newRoFalseZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "No r/o Zettel") m.Set(meta.KeyReadOnly, "false") return m } | > > > > > > | 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 | return m } func newExpertZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Expert Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func newSimpleZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Simple Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilitySimple) return m } func newRoFalseZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "No r/o Zettel") m.Set(meta.KeyReadOnly, "false") return m } |
︙ | ︙ |
Added auth/token/token.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package token provides some function for handling auth token. package token import ( "errors" "time" "github.com/pascaldekloe/jwt" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) const reqHash = jwt.HS512 // ErrNoUser signals that the meta data has no role value 'user'. var ErrNoUser = errors.New("auth: meta is no user") // ErrNoIdent signals that the 'ident' key is missing. var ErrNoIdent = errors.New("auth: missing ident") // ErrOtherKind signals that the token was defined for another token kind. var ErrOtherKind = errors.New("auth: wrong token kind") // ErrNoZid signals that the 'zid' key is missing. var ErrNoZid = errors.New("auth: missing zettel id") // Kind specifies for which application / usage a token is/was requested. type Kind int // Allowed values of token kind const ( _ Kind = iota KindJSON KindHTML ) // GetToken returns a token to be used for authentification. func GetToken(ident *meta.Meta, d time.Duration, kind Kind) ([]byte, error) { if role, ok := ident.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser { return nil, ErrNoUser } subject, ok := ident.Get(meta.KeyUserID) if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) claims := jwt.Claims{ Registered: jwt.Registered{ Subject: subject, Expires: jwt.NewNumericTime(now.Add(d)), Issued: jwt.NewNumericTime(now), }, Set: map[string]interface{}{ "zid": ident.Zid.String(), "_tk": int(kind), }, } token, err := claims.HMACSign(reqHash, startup.Secret()) if err != nil { return nil, err } return token, nil } // ErrTokenExpired signals an exired token var ErrTokenExpired = errors.New("auth: token expired") // Data contains some important elements from a token. type Data struct { Token []byte Now time.Time Issued time.Time Expires time.Time Ident string Zid id.Zid } // CheckToken checks the validity of the token and returns relevant data. func CheckToken(token []byte, k Kind) (Data, error) { h, err := jwt.NewHMAC(reqHash, startup.Secret()) if err != nil { return Data{}, err } claims, err := h.Check(token) if err != nil { return Data{}, err } now := time.Now().Round(time.Second) expires := claims.Expires.Time() if expires.Before(now) { return Data{}, ErrTokenExpired } ident := claims.Subject if ident == "" { return Data{}, ErrNoIdent } if zidS, ok := claims.Set["zid"].(string); ok { if zid, err := id.Parse(zidS); err == nil { if kind, ok := claims.Set["_tk"].(float64); ok { if Kind(kind) == k { return Data{ Token: token, Now: now, Issued: claims.Issued.Time(), Expires: expires, Ident: ident, Zid: zid, }, nil } } return Data{}, ErrOtherKind } } return Data{}, ErrNoZid } |
Added cmd/cmd_config.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "fmt" "zettelstore.de/z/config/startup" ) // ---------- Subcommand: config --------------------------------------------- func cmdConfig(*flag.FlagSet) (int, error) { fmtVersion() fmt.Println("Stores") fmt.Printf(" Read-only mode = %v\n", startup.IsReadOnlyMode()) fmt.Println("Web") fmt.Printf(" Listen address = %q\n", startup.ListenAddress()) fmt.Printf(" URL prefix = %q\n", startup.URLPrefix()) if startup.WithAuth() { fmt.Println("Auth") fmt.Printf(" Owner = %v\n", startup.Owner()) fmt.Printf(" Secure cookie = %v\n", startup.SecureCookie()) fmt.Printf(" Persistent cookie = %v\n", startup.PersistentCookie()) htmlLifetime, apiLifetime := startup.TokenLifetime() fmt.Printf(" HTML lifetime = %v\n", htmlLifetime) fmt.Printf(" API lifetime = %v\n", apiLifetime) } return 0, nil } |
Changes to cmd/cmd_file.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "flag" "fmt" "io" "os" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) // ---------- Subcommand: file ----------------------------------------------- | > | | | | | < | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | import ( "flag" "fmt" "io" "os" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) // ---------- Subcommand: file ----------------------------------------------- func cmdFile(fs *flag.FlagSet) (int, error) { format := fs.Lookup("t").Value.String() meta, inp, err := getInput(fs.Args()) if meta == nil { return 2, err } z := parser.ParseZettel( domain.Zettel{ Meta: meta, Content: domain.NewContent(inp.Src[inp.Pos:]), }, runtime.GetSyntax(meta), ) enc := encoder.Create(format, &encoder.Environment{Lang: runtime.GetLang(meta)}) if enc == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", format) return 2, nil } _, err = enc.WriteZettel(os.Stdout, z, format != "raw") if err != nil { return 2, err |
︙ | ︙ |
Changes to cmd/cmd_password.go.
︙ | ︙ | |||
20 21 22 23 24 25 26 | "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ---------- Subcommand: password ------------------------------------------- | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ---------- Subcommand: password ------------------------------------------- func cmdPassword(fs *flag.FlagSet) (int, error) { if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "User name and user zettel identification missing") return 2, nil } if fs.NArg() == 1 { fmt.Fprintln(os.Stderr, "User zettel identification missing") return 2, nil |
︙ | ︙ |
Changes to cmd/cmd_run.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 15 16 | // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "net/http" | > | | < | > > < | | < | > | | < < < < | | > > | > | | | > > > > > > > > > > | > > | < > > | | < | | | | | | | > | | | | | | | | > | | | | | > | > | | | > | | | | | > | | | > | | | | | | | > | | | | | | | | | | < < | < | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "log" "net/http" "zettelstore.de/z/auth/policy" "zettelstore.de/z/config/runtime" "zettelstore.de/z/config/startup" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/router" "zettelstore.de/z/web/server" "zettelstore.de/z/web/session" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { fs.String("c", defConfigfile, "configuration file") fs.Uint("p", 23123, "port number") fs.String("d", "", "zettel directory") fs.Bool("r", false, "system-wide read-only mode") fs.Bool("v", false, "verbose mode") fs.Bool("debug", false, "debug mode") } func enableDebug(fs *flag.FlagSet, srv *server.Server) { if dbg := fs.Lookup("debug"); dbg != nil && dbg.Value.String() == "true" { srv.SetDebug() } } func runFunc(fs *flag.FlagSet) (int, error) { listenAddr := startup.ListenAddress() readonlyMode := startup.IsReadOnlyMode() logBeforeRun(listenAddr, readonlyMode) handler := setupRouting(startup.PlaceManager(), readonlyMode) srv := server.New(listenAddr, handler) enableDebug(fs, srv) if err := srv.Run(); err != nil { return 1, err } return 0, nil } func logBeforeRun(listenAddr string, readonlyMode bool) { v := startup.GetVersion() log.Printf("%v %v (%v@%v/%v)", v.Prog, v.Build, v.GoVersion, v.Os, v.Arch) log.Println("Licensed under the latest version of the EUPL (European Union Public License)") log.Printf("Listening on %v", listenAddr) log.Printf("Zettel location [%v]", startup.PlaceManager().Location()) if readonlyMode { log.Println("Read-only mode") } } func setupRouting(mgr place.Manager, readonlyMode bool) http.Handler { var up place.Place = mgr pp, pol := policy.PlaceWithPolicy( up, startup.IsSimple(), startup.WithAuth, readonlyMode, runtime.GetExpertMode, startup.IsOwner, runtime.GetVisibility) te := webui.NewTemplateEngine(mgr, pol) ucAuthenticate := usecase.NewAuthenticate(up) ucGetMeta := usecase.NewGetMeta(pp) ucGetZettel := usecase.NewGetZettel(pp) ucParseZettel := usecase.NewParseZettel(ucGetZettel) ucListMeta := usecase.NewListMeta(pp) ucListRoles := usecase.NewListRole(pp) ucListTags := usecase.NewListTags(pp) ucZettelContext := usecase.NewZettelContext(pp) router := router.NewRouter() router.Handle("/", webui.MakeGetRootHandler(te, pp)) router.AddListRoute('a', http.MethodGet, webui.MakeGetLoginHandler(te)) router.AddListRoute('a', http.MethodPost, adapter.MakePostLoginHandler( api.MakePostLoginHandlerAPI(ucAuthenticate), webui.MakePostLoginHandlerHTML(te, ucAuthenticate))) router.AddListRoute('a', http.MethodPut, api.MakeRenewAuthHandler()) router.AddZettelRoute('a', http.MethodGet, webui.MakeGetLogoutHandler(te)) if !readonlyMode { router.AddZettelRoute('b', http.MethodGet, webui.MakeGetRenameZettelHandler( te, ucGetMeta)) router.AddZettelRoute('b', http.MethodPost, webui.MakePostRenameZettelHandler( te, usecase.NewRenameZettel(pp))) router.AddZettelRoute('c', http.MethodGet, webui.MakeGetCopyZettelHandler( te, ucGetZettel, usecase.NewCopyZettel())) router.AddZettelRoute('c', http.MethodPost, webui.MakePostCreateZettelHandler( te, usecase.NewCreateZettel(pp))) router.AddZettelRoute('d', http.MethodGet, webui.MakeGetDeleteZettelHandler( te, ucGetZettel)) router.AddZettelRoute('d', http.MethodPost, webui.MakePostDeleteZettelHandler( te, usecase.NewDeleteZettel(pp))) router.AddZettelRoute('e', http.MethodGet, webui.MakeEditGetZettelHandler( te, ucGetZettel)) router.AddZettelRoute('e', http.MethodPost, webui.MakeEditSetZettelHandler( te, usecase.NewUpdateZettel(pp))) router.AddZettelRoute('f', http.MethodGet, webui.MakeGetFolgeZettelHandler( te, ucGetZettel, usecase.NewFolgeZettel())) router.AddZettelRoute('f', http.MethodPost, webui.MakePostCreateZettelHandler( te, usecase.NewCreateZettel(pp))) router.AddZettelRoute('g', http.MethodGet, webui.MakeGetNewZettelHandler( te, ucGetZettel, usecase.NewNewZettel())) router.AddZettelRoute('g', http.MethodPost, webui.MakePostCreateZettelHandler( te, usecase.NewCreateZettel(pp))) } router.AddListRoute('f', http.MethodGet, webui.MakeSearchHandler( te, usecase.NewSearch(pp), ucGetMeta, ucGetZettel)) router.AddListRoute('h', http.MethodGet, webui.MakeListHTMLMetaHandler( te, ucListMeta, ucListRoles, ucListTags)) router.AddZettelRoute('h', http.MethodGet, webui.MakeGetHTMLZettelHandler( te, ucParseZettel, ucGetMeta)) router.AddZettelRoute('i', http.MethodGet, webui.MakeGetInfoHandler( te, ucParseZettel, ucGetMeta)) router.AddZettelRoute('j', http.MethodGet, webui.MakeZettelContextHandler(te, ucZettelContext)) router.AddZettelRoute('l', http.MethodGet, api.MakeGetLinksHandler(ucParseZettel)) router.AddZettelRoute('o', http.MethodGet, api.MakeGetOrderHandler( usecase.NewZettelOrder(pp, ucParseZettel))) router.AddListRoute('r', http.MethodGet, api.MakeListRoleHandler(ucListRoles)) router.AddListRoute('t', http.MethodGet, api.MakeListTagsHandler(ucListTags)) router.AddZettelRoute('y', http.MethodGet, api.MakeZettelContextHandler(ucZettelContext)) router.AddListRoute('z', http.MethodGet, api.MakeListMetaHandler( usecase.NewListMeta(pp), ucGetMeta, ucParseZettel)) router.AddZettelRoute('z', http.MethodGet, api.MakeGetZettelHandler( ucParseZettel, ucGetMeta)) return session.NewHandler(router, usecase.NewGetUserByZid(up)) } |
Changes to cmd/cmd_run_simple.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package cmd import ( "flag" "fmt" "os" "strings" | > | | | < | | > | | | | | < | > > > | > > | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | //----------------------------------------------------------------------------- package cmd import ( "flag" "fmt" "log" "os" "strings" "zettelstore.de/z/config/startup" "zettelstore.de/z/web/server" ) func flgSimpleRun(fs *flag.FlagSet) { fs.String("d", "", "zettel directory") } func runSimpleFunc(*flag.FlagSet) (int, error) { listenAddr := startup.ListenAddress() readonlyMode := startup.IsReadOnlyMode() logBeforeRun(listenAddr, readonlyMode) if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { log.Println() log.Println("--------------------------") log.Printf("Open your browser and enter the following URL:") log.Println() log.Printf(" http://localhost%v", listenAddr[idx:]) } handler := setupRouting(startup.PlaceManager(), readonlyMode) srv := server.New(listenAddr, handler) if err := srv.Run(); err != nil { return 1, err } return 0, nil } // runSimple is called, when the user just starts the software via a double click // or via a simple call ``./zettelstore`` on the command line. func runSimple() { dir := "./zettel" if err := os.MkdirAll(dir, 0750); err != nil { fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err) os.Exit(1) } executeCommand("run-simple", "-d", dir) } |
Changes to cmd/command.go.
1 | //----------------------------------------------------------------------------- | | < < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "sort" ) // Command stores information about commands / sub-commands. type Command struct { Name string // command name as it appears on the command line Func CommandFunc // function that executes a command Places bool // if true then places will be set up Simple bool // Should start in simple mode Flags func(*flag.FlagSet) // function to set up flag.FlagSet flags *flag.FlagSet // flags that belong to the command } // CommandFunc is the function that executes the command. // It accepts the parsed command line parameters. // It returns the exit code and an error. type CommandFunc func(*flag.FlagSet) (int, error) // GetFlags return the flag.FlagSet defined for the command. func (c *Command) GetFlags() *flag.FlagSet { return c.flags } var commands = make(map[string]Command) // RegisterCommand registers the given command. |
︙ | ︙ |
Changes to cmd/main.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( | | | < < | | | | | | | < | | | | > > < | > > > > > > > > > > > > | | | | | < < < | < < < < < < | | | | > > > | | < | < < < | < < | < < < < < < < < < < < | | > > | | < > | | | < | < < | | | | | < < | | > > > | < < < < < < < < < < > > > > > | < > > | | | | < < < < | | < > | | < < < < < < < | < < < < < < < < < < | < < | | | | | | < | > | > | > | | > < | < | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "context" "flag" "fmt" "log" "os" "strings" "zettelstore.de/z/config/runtime" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/index/indexer" "zettelstore.de/z/input" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/place/progplace" ) const ( defConfigfile = ".zscfg" ) func init() { RegisterCommand(Command{ Name: "help", Func: func(*flag.FlagSet) (int, error) { fmt.Println("Available commands:") for _, name := range List() { fmt.Printf("- %q\n", name) } return 0, nil }, }) RegisterCommand(Command{ Name: "version", Func: func(*flag.FlagSet) (int, error) { fmtVersion() return 0, nil }, }) RegisterCommand(Command{ Name: "run", Func: runFunc, Places: true, Flags: flgRun, }) RegisterCommand(Command{ Name: "run-simple", Func: runSimpleFunc, Places: true, Simple: true, Flags: flgSimpleRun, }) RegisterCommand(Command{ Name: "config", Func: cmdConfig, Flags: flgRun, }) RegisterCommand(Command{ Name: "file", Func: cmdFile, Flags: func(fs *flag.FlagSet) { fs.String("t", "html", "target output format") }, }) RegisterCommand(Command{ Name: "password", Func: cmdPassword, }) } func fmtVersion() { version := startup.GetVersion() fmt.Printf("%v (%v/%v) running on %v (%v/%v)\n", version.Prog, version.Build, version.GoVersion, version.Hostname, version.Os, version.Arch) } func getConfig(fs *flag.FlagSet) (cfg *meta.Meta) { var configFile string if configFlag := fs.Lookup("c"); configFlag != nil { configFile = configFlag.Value.String() } else { configFile = defConfigfile } content, err := os.ReadFile(configFile) if err != nil { cfg = meta.New(id.Invalid) } else { cfg = meta.NewFromInput(id.Invalid, input.NewInput(string(content))) } fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": cfg.Set(startup.KeyListenAddress, "127.0.0.1:"+flg.Value.String()) case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } cfg.Set(startup.KeyPlaceOneURI, val) case "r": cfg.Set(startup.KeyReadOnlyMode, flg.Value.String()) case "v": cfg.Set(startup.KeyVerbose, flg.Value.String()) } }) return cfg } func setupOperations(cfg *meta.Meta, withPlaces, simple bool) error { var mgr place.Manager var idx index.Indexer if withPlaces { err := raiseFdLimit() if err != nil { log.Println("Raising some limitions did not work:", err) log.Println("Prepare to encounter errors. Most of them can be mitigated. See the manual for details") cfg.Set(startup.KeyDefaultDirPlaceType, startup.ValueDirPlaceTypeSimple) } startup.SetupStartupConfig(cfg) idx = indexer.New() filter := index.NewMetaFilter(idx) mgr, err = manager.New(getPlaces(cfg), cfg.GetBool(startup.KeyReadOnlyMode), filter) if err != nil { return err } } else { startup.SetupStartupConfig(cfg) } startup.SetupStartupService(mgr, idx, simple) if withPlaces { if err := mgr.Start(context.Background()); err != nil { fmt.Fprintln(os.Stderr, "Unable to start zettel place") return err } runtime.SetupConfiguration(mgr) progplace.Setup(cfg, mgr, idx) idx.Start(mgr) } return nil } func getPlaces(cfg *meta.Meta) []string { var result []string = nil for cnt := 1; ; cnt++ { key := fmt.Sprintf("place-%v-uri", cnt) uri, ok := cfg.Get(key) if !ok || uri == "" { if cnt > 1 { break } uri = "dir:./zettel" } result = append(result, uri) } return result } func cleanupOperations(withPlaces bool) error { if withPlaces { startup.Indexer().Stop() if err := startup.PlaceManager().Stop(context.Background()); err != nil { fmt.Fprintln(os.Stderr, "Unable to stop zettel place") return err } } return nil } func executeCommand(name string, args ...string) { command, ok := Get(name) if !ok { fmt.Fprintf(os.Stderr, "Unknown command %q\n", name) os.Exit(1) } fs := command.GetFlags() if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "%s: unable to parse flags: %v %v\n", name, args, err) os.Exit(1) } cfg := getConfig(fs) if err := setupOperations(cfg, command.Places, command.Simple); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) os.Exit(2) } exitCode, err := command.Func(fs) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } if err := cleanupOperations(command.Places); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } if exitCode != 0 { os.Exit(exitCode) } } // Main is the real entrypoint of the zettelstore. func Main(progName, buildVersion string) { startup.SetupVersion(progName, buildVersion) if len(os.Args) <= 1 { runSimple() } else { executeCommand(os.Args[1], os.Args[2:]...) } } |
Changes to cmd/register.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | import ( _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/jsonenc" // Allow to use JSON encoder. _ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder. _ "zettelstore.de/z/encoder/rawenc" // Allow to use raw encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. | < < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import ( _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/jsonenc" // Allow to use JSON encoder. _ "zettelstore.de/z/encoder/nativeenc" // Allow to use native encoder. _ "zettelstore.de/z/encoder/rawenc" // Allow to use raw encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. _ "zettelstore.de/z/place/constplace" // Allow to use global internal place. _ "zettelstore.de/z/place/dirplace" // Allow to use directory place. _ "zettelstore.de/z/place/fileplace" // Allow to use file place. _ "zettelstore.de/z/place/memplace" // Allow to use memory place. ) |
Changes to collect/split.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" // DivideReferences divides the given list of rederences into zettel, local, and external References. | | | | | | > > > > > > > > | | | | | > > | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" // DivideReferences divides the given list of rederences into zettel, local, and external References. func DivideReferences(all []*ast.Reference, duplicates bool) (zettel, local, external []*ast.Reference) { if len(all) == 0 { return nil, nil, nil } mapZettel := make(map[string]bool) mapLocal := make(map[string]bool) mapExternal := make(map[string]bool) for _, ref := range all { if ref.State == ast.RefStateSelf { continue } if ref.IsZettel() { zettel = appendRefToList(zettel, mapZettel, ref, duplicates) } else if ref.IsExternal() { external = appendRefToList(external, mapExternal, ref, duplicates) } else { local = appendRefToList(local, mapLocal, ref, duplicates) } } return zettel, local, external } func appendRefToList( reflist []*ast.Reference, refSet map[string]bool, ref *ast.Reference, duplicates bool, ) []*ast.Reference { if duplicates { reflist = append(reflist, ref) } else { s := ref.String() if _, ok := refSet[s]; !ok { reflist = append(reflist, ref) refSet[s] = true } } return reflist } |
Deleted config/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added config/runtime/meta.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package runtime provides functions to retrieve runtime configuration data. package runtime import ( "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/meta" ) var mapDefaultKeys = map[string]func() string{ meta.KeyCopyright: GetDefaultCopyright, meta.KeyLang: GetDefaultLang, meta.KeyLicense: GetDefaultLicense, meta.KeyRole: GetDefaultRole, meta.KeySyntax: GetDefaultSyntax, meta.KeyTitle: GetDefaultTitle, } // AddDefaultValues enriches the given meta data with its default values. func AddDefaultValues(m *meta.Meta) *meta.Meta { result := m for k, f := range mapDefaultKeys { if _, ok := result.Get(k); !ok { if result == m { result = m.Clone() } if val := f(); len(val) > 0 || m.Type(k) == meta.TypeEmpty { result.Set(k, val) } } } return result } // GetTitle returns the value of the "title" key of the given meta. If there // is no such value, GetDefaultTitle is returned. func GetTitle(m *meta.Meta) string { if syntax, ok := m.Get(meta.KeyTitle); ok && len(syntax) > 0 { return syntax } return GetDefaultTitle() } // GetRole returns the value of the "role" key of the given meta. If there // is no such value, GetDefaultRole is returned. func GetRole(m *meta.Meta) string { if syntax, ok := m.Get(meta.KeyRole); ok && len(syntax) > 0 { return syntax } return GetDefaultRole() } // GetSyntax returns the value of the "syntax" key of the given meta. If there // is no such value, GetDefaultSyntax is returned. func GetSyntax(m *meta.Meta) string { if syntax, ok := m.Get(meta.KeySyntax); ok && len(syntax) > 0 { return syntax } return GetDefaultSyntax() } // GetLang returns the value of the "lang" key of the given meta. If there is // no such value, GetDefaultLang is returned. func GetLang(m *meta.Meta) string { if lang, ok := m.Get(meta.KeyLang); ok && len(lang) > 0 { return lang } return GetDefaultLang() } // GetVisibility returns the visibility value, or "login" if none is given. func GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(meta.KeyVisibility); ok { if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } } return GetDefaultVisibility() } // GetUserRole role returns the user role of the given user zettel. func GetUserRole(user *meta.Meta) meta.UserRole { if user == nil { if startup.WithAuth() { return meta.UserRoleUnknown } return meta.UserRoleOwner } if startup.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(meta.KeyUserRole); ok { if ur := meta.GetUserRole(val); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } |
Added config/runtime/runtime.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package runtime provides functions to retrieve runtime configuration data. package runtime import ( "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/stock" ) // --- Configuration zettel -------------------------------------------------- var configStock stock.Stock // SetupConfiguration enables the configuration data. func SetupConfiguration(mgr place.Manager) { if configStock != nil { panic("configStock already set") } configStock = stock.NewStock(mgr) if err := configStock.Subscribe(id.ConfigurationZid); err != nil { panic(err) } } // getConfigurationMeta returns the meta data of the configuration zettel. func getConfigurationMeta() *meta.Meta { if configStock == nil { panic("configStock not set") } return configStock.GetMeta(id.ConfigurationZid) } // GetDefaultTitle returns the current value of the "default-title" key. func GetDefaultTitle() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if title, ok := config.Get(meta.KeyDefaultTitle); ok { return title } } } return "Untitled" } // GetDefaultSyntax returns the current value of the "default-syntax" key. func GetDefaultSyntax() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if syntax, ok := config.Get(meta.KeyDefaultSyntax); ok { return syntax } } } return meta.ValueSyntaxZmk } // GetDefaultRole returns the current value of the "default-role" key. func GetDefaultRole() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if role, ok := config.Get(meta.KeyDefaultRole); ok { return role } } } return meta.ValueRoleZettel } // GetDefaultLang returns the current value of the "default-lang" key. func GetDefaultLang() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if lang, ok := config.Get(meta.KeyDefaultLang); ok { return lang } } } return "en" } // GetDefaultCopyright returns the current value of the "default-copyright" key. func GetDefaultCopyright() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if copyright, ok := config.Get(meta.KeyDefaultCopyright); ok { return copyright } } } return "" } // GetDefaultLicense returns the current value of the "default-license" key. func GetDefaultLicense() string { if configStock != nil { if config := getConfigurationMeta(); config != nil { if license, ok := config.Get(meta.KeyDefaultLicense); ok { return license } } } return "" } // GetExpertMode returns the current value of the "expert-mode" key func GetExpertMode() bool { if config := getConfigurationMeta(); config != nil { if mode, ok := config.Get(meta.KeyExpertMode); ok { return meta.BoolValue(mode) } } return false } // GetSiteName returns the current value of the "site-name" key. func GetSiteName() string { if config := getConfigurationMeta(); config != nil { if name, ok := config.Get(meta.KeySiteName); ok { return name } } return "Zettelstore" } // GetHomeZettel returns the value of the "home-zettel" key. func GetHomeZettel() id.Zid { if config := getConfigurationMeta(); config != nil { if start, ok := config.Get(meta.KeyHomeZettel); ok { if startID, err := id.Parse(start); err == nil { return startID } } } return id.DefaultHomeZid } // GetDefaultVisibility returns the default value for zettel visibility. func GetDefaultVisibility() meta.Visibility { if config := getConfigurationMeta(); config != nil { if value, ok := config.Get(meta.KeyDefaultVisibility); ok { if vis := meta.GetVisibility(value); vis != meta.VisibilityUnknown { return vis } } } return meta.VisibilityLogin } // GetYAMLHeader returns the current value of the "yaml-header" key. func GetYAMLHeader() bool { if config := getConfigurationMeta(); config != nil { return config.GetBool(meta.KeyYAMLHeader) } return false } // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. func GetZettelFileSyntax() []string { if config := getConfigurationMeta(); config != nil { return config.GetListOrNil(meta.KeyZettelFileSyntax) } return nil } // GetMarkerExternal returns the current value of the "marker-external" key. func GetMarkerExternal() string { if config := getConfigurationMeta(); config != nil { if html, ok := config.Get(meta.KeyMarkerExternal); ok { return html } } return "➚" } // GetFooterHTML returns HTML code that should be embedded into the footer // of each WebUI page. func GetFooterHTML() string { if config := getConfigurationMeta(); config != nil { if data, ok := config.Get(meta.KeyFooterHTML); ok { return data } } return "" } // GetListPageSize returns the maximum length of a list to be returned in WebUI. // A value less or equal to zero signals no limit. func GetListPageSize() int { if config := getConfigurationMeta(); config != nil { if value, ok := config.GetNumber(meta.KeyListPageSize); ok && value > 0 { return value } } return 0 } |
Added config/startup/startup.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package startup provides functions to retrieve startup configuration data. package startup import ( "hash/fnv" "io" "strconv" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" ) var config struct { // Set in SetupStartupConfig verbose bool readonlyMode bool urlPrefix string listenAddress string defaultDirPlaceType string owner id.Zid withAuth bool secret []byte insecCookie bool persistCookie bool htmlLifetime time.Duration apiLifetime time.Duration // Set in SetupStartupService simple bool // was started without run command manager place.Manager indexer index.Indexer } // Predefined keys for startup zettel const ( KeyDefaultDirPlaceType = "default-dir-place-type" KeyInsecureCookie = "insecure-cookie" KeyListenAddress = "listen-addr" KeyOwner = "owner" KeyPersistentCookie = "persistent-cookie" KeyPlaceOneURI = "place-1-uri" KeyReadOnlyMode = "read-only-mode" KeyTokenLifetimeHTML = "token-lifetime-html" KeyTokenLifetimeAPI = "token-lifetime-api" KeyURLPrefix = "url-prefix" KeyVerbose = "verbose" ) // Important values for some keys. const ( ValueDirPlaceTypeNotify = "notify" ValueDirPlaceTypeSimple = "simple" ) // SetupStartupConfig initializes the startup data with content of config file. func SetupStartupConfig(cfg *meta.Meta) { if config.urlPrefix != "" { panic("startup.config already set") } config.verbose = cfg.GetBool(KeyVerbose) config.readonlyMode = cfg.GetBool(KeyReadOnlyMode) config.urlPrefix = cfg.GetDefault(KeyURLPrefix, "/") if prefix, ok := cfg.Get(KeyURLPrefix); ok && len(prefix) > 0 && prefix[0] == '/' && prefix[len(prefix)-1] == '/' { config.urlPrefix = prefix } else { config.urlPrefix = "/" } if val, ok := cfg.Get(KeyListenAddress); ok { config.listenAddress = val // TODO: check for valid string } else { config.listenAddress = "127.0.0.1:23123" } if defaultType, ok := cfg.Get(KeyDefaultDirPlaceType); ok { switch defaultType { case ValueDirPlaceTypeNotify: case ValueDirPlaceTypeSimple: default: defaultType = ValueDirPlaceTypeNotify } config.defaultDirPlaceType = defaultType } else { config.defaultDirPlaceType = ValueDirPlaceTypeNotify } config.owner = id.Invalid if owner, ok := cfg.Get(KeyOwner); ok { if zid, err := id.Parse(owner); err == nil { config.owner = zid config.withAuth = true } } if config.withAuth { config.insecCookie = cfg.GetBool(KeyInsecureCookie) config.persistCookie = cfg.GetBool(KeyPersistentCookie) config.secret = calcSecret(cfg) config.htmlLifetime = getDuration( cfg, KeyTokenLifetimeHTML, 1*time.Hour, 1*time.Minute, 30*24*time.Hour) config.apiLifetime = getDuration( cfg, KeyTokenLifetimeAPI, 10*time.Minute, 0, 1*time.Hour) } } // SetupStartupService initializes the startup data with internal services. func SetupStartupService(manager place.Manager, idx index.Indexer, simple bool) { if config.urlPrefix == "" { panic("startup.config not set") } config.simple = simple && !config.withAuth config.manager = manager config.indexer = idx } func calcSecret(cfg *meta.Meta) []byte { h := fnv.New128() if secret, ok := cfg.Get("secret"); ok { io.WriteString(h, secret) } io.WriteString(h, version.Prog) io.WriteString(h, version.Build) io.WriteString(h, version.Hostname) io.WriteString(h, version.Os) io.WriteString(h, version.Arch) return h.Sum(nil) } func getDuration( cfg *meta.Meta, key string, defDur, minDur, maxDur time.Duration) time.Duration { if s, ok := cfg.Get(key); ok && len(s) > 0 { if d, err := strconv.ParseUint(s, 10, 64); err == nil { secs := time.Duration(d) * time.Minute if secs < minDur { return minDur } if secs > maxDur { return maxDur } return secs } } return defDur } // IsSimple returns true if Zettelstore was not started with command "run" // and authentication is disabled. func IsSimple() bool { return config.simple } // IsVerbose returns whether the system should be more chatty about its operations. func IsVerbose() bool { return config.verbose } // IsReadOnlyMode returns whether the system is in read-only mode or not. func IsReadOnlyMode() bool { return config.readonlyMode } // URLPrefix returns the configured prefix to be used when providing URL to // the service. func URLPrefix() string { return config.urlPrefix } // ListenAddress returns the string that specifies the the network card and the ip port // where the server listens for requests func ListenAddress() string { return config.listenAddress } // DefaultDirPlaceType returns the default value for a directory place type. func DefaultDirPlaceType() string { return config.defaultDirPlaceType } // WithAuth returns true if user authentication is enabled. func WithAuth() bool { return config.withAuth } // SecureCookie returns whether the web app should set cookies to secure mode. func SecureCookie() bool { return config.withAuth && !config.insecCookie } // PersistentCookie returns whether the web app should set persistent cookies // (instead of temporary). func PersistentCookie() bool { return config.persistCookie } // Owner returns the zid of the zettelkasten's owner. // If there is no owner defined, the value ZettelID(0) is returned. func Owner() id.Zid { return config.owner } // IsOwner returns true, if the given user is the owner of the Zettelstore. func IsOwner(zid id.Zid) bool { return zid.IsValid() && zid == config.owner } // Secret returns the interal application secret. It is typically used to // encrypt session values. func Secret() []byte { return config.secret } // TokenLifetime return the token lifetime for the web/HTML access and for the // API access. If lifetime for API access is equal to zero, no API access is // possible. func TokenLifetime() (htmlLifetime, apiLifetime time.Duration) { return config.htmlLifetime, config.apiLifetime } // PlaceManager returns the managing place. func PlaceManager() place.Manager { return config.manager } // Indexer returns the current indexer. func Indexer() index.Indexer { return config.indexer } |
Added config/startup/version.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package startup provides functions to retrieve startup configuration data. package startup import ( "os" "runtime" ) // Version describes all elements of a software version. type Version struct { Prog string // Name of the software Build string // Representation of build process Hostname string // Host name a reported by the kernel GoVersion string // Version of go Os string // GOOS Arch string // GOARCH // More to come } var version Version // SetupVersion initializes the version data. func SetupVersion(progName, buildVersion string) { version.Prog = progName if buildVersion == "" { version.Build = "unknown" } else { version.Build = buildVersion } if hn, err := os.Hostname(); err == nil { version.Hostname = hn } else { version.Hostname = "*unknown host*" } version.GoVersion = runtime.Version() version.Os = runtime.GOOS version.Arch = runtime.GOARCH } // GetVersion returns the current software version data. func GetVersion() Version { return version } |
Changes to docs/manual/00001004000000.zettel.
1 2 3 4 5 | id: 00001004000000 title: Configuration of Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk | < | | < < < | < | > | | | > | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001004000000 title: Configuration of Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk There are two levels to change the behavior and/or the appearance of Zettelstore. The first level is the configuration that is needed to start the services provided by Zettelstore. For example, this includes the URI under which your Zettelstore is accessible. * [[Zettelstore start-up configuration|00001004010000]] The second level is configuring the running Zettelstore. For example, you can configure the default language of your Zettelstore. * [[Configure a running Zettelstore|00001004020000]] The third level is the way to start Zettelstore services and to manage it. * [[Command line parameters|00001004050000]] |
Changes to docs/manual/00001004010000.zettel.
1 | id: 00001004010000 | | < | | < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | id: 00001004010000 title: Zettelstore start-up configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some start-up 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 placed. 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. The file for start-up 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: ; [!default-dir-place-type]''default-dir-place-type'' : Specifies the default value for the (sub-) type of [[directory places|00001004011400#type]]. Zettel are typically stored in such places. Default: ''notify'' ; [!insecure-cookie]''insecure-cookie'' : Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). |
︙ | ︙ | |||
48 49 50 51 52 53 54 | On these devices, the operating system is free to stop the web browser and to remove temporary cookies. Therefore, an authenticated user will be logged off. If ''true'', a persistent cookie is used. Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds. Default: ''false'' | | | | | | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | On these devices, the operating system is free to stop the web browser and to remove temporary cookies. Therefore, an authenticated user will be logged off. If ''true'', a persistent cookie is used. Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds. Default: ''false'' ; [!place-X-uri]''place-//X//-uri'', where //X// is a number greater or equal to one : Specifies a [[place|00001004011200]] where zettel are stored. During start-up //X// is counted, starting with one, until no key is found. This allows to configure more than one place. If no ''place-1-uri'' key is given, the overall effect will be the same as if only ''place-1-uri'' was specified with the value ''dir://.zettel''. In this case, even a key ''place-2-uri'' will be ignored. ; [!read-only-mode]''read-only-mode'' : Puts the Zettelstore web service into a read-only mode. No changes are possible. Default: false. ; [!token-lifetime-api]''token-lifetime-api'', [!token-lifetime-html]''token-lifetime-html'' : Define lifetime of access tokens in minutes. Values are only valid if authentication is enabled, i.e. key ''owner'' is set. |
︙ | ︙ |
Changes to docs/manual/00001004011200.zettel.
1 2 3 4 5 | id: 00001004011200 title: Zettelstore places role: manual tags: #configuration #manual #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001004011200 title: Zettelstore places role: manual tags: #configuration #manual #zettelstore syntax: zmk A Zettelstore must store its zettel somehow and somewhere. In most cases you want to store your zettel as files in a directory. Under certain circumstances you may want to store your zettel in other places. An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore. They are stored within the software itself. In another situation you may want to store your zettel volatile, e.g. if you want to provide a sandbox for experimenting. To cope with these (and more) situations, you configure Zettelstore to use one or more places. This is done via the ''place-X-uri'' keys of the [[start-up configuration|00001004010000#place-X-uri]] (X is a number). Places are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses. The following place URIs are supported: ; ''dir:\//DIR'' : Specifies a directory where zettel files are stored. ''DIR'' is the file path. |
︙ | ︙ | |||
32 33 34 35 36 37 38 | You can create such a ZIP file, if you zip a directory full of zettel files. This place is always read-only. ; ''mem:'' : Stores all its zettel in volatile memory. If you stop the Zettelstore, all changes are lost. | | | | > > > | | | 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | You can create such a ZIP file, if you zip a directory full of zettel files. This place is always read-only. ; ''mem:'' : Stores all its zettel in volatile memory. If you stop the Zettelstore, all changes are lost. All places that you configure via the ''store-X-uri'' keys form a chain of places. If a zettel should be retrieved, a search starts in the place specified with the ''place-1-uri'' key, then ''place-2-uri'' and so on. If a zettel is created or changed, it is always stored in the place specified with the ''place-1-uri'' key. This allows to overwrite zettel from other places, e.g. the predefined zettel. If you did not configure the place of the predefined zettel (''const:'') they will automatically be appended as a last place. Otherwise Zettelstore will not work in certain situations. If you use the ''mem:'' place, where zettel are stored in volatile memory, it makes only sense if you configure it as ''place-1-uri''. Such a place will be empty when Zettelstore starts and only the place 1 will receive updates. You must make sure that your computer has enough RAM to store all zettel. |
Changes to docs/manual/00001004011400.zettel.
1 2 3 4 5 | id: 00001004011400 title: Configure file directory places role: manual tags: #configuration #manual #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001004011400 title: Configure file directory places role: manual tags: #configuration #manual #zettelstore syntax: zmk Under certain circumstances, it is preferable to further configure a file directory place. This is done by appending query parameters after the base place URI ''dir:\//DIR''. The following parameters are supported: |= Parameter:|Description|Default value:| |
︙ | ︙ | |||
37 38 39 40 41 42 43 | When the parameter ''type'' is set to ""notify"", Zettelstore automatically detects changes to zettel files that originates from other software. It is done on a ""best-effort"" basis. Under certain circumstances it is possible that Zettelstore does not detect a change done by another software. To cope with this unlikely, but still possible event, Zettelstore re-scans periodically the file directory. The time interval is configured by the ''rescan'' parameter, e.g. ``` | | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | When the parameter ''type'' is set to ""notify"", Zettelstore automatically detects changes to zettel files that originates from other software. It is done on a ""best-effort"" basis. Under certain circumstances it is possible that Zettelstore does not detect a change done by another software. To cope with this unlikely, but still possible event, Zettelstore re-scans periodically the file directory. The time interval is configured by the ''rescan'' parameter, e.g. ``` place-1-uri: dir:///home/zettel?rescan=300 ``` This makes Zettelstore to re-scan the directory ''/home/zettel/'' every 300 seconds, i.e. 5 minutes. For a Zettelstore with many zettel, re-scanning the directory may take a while, especially if it is stored on a remote computer (e.g. via CIFS/SMB or NFS). In this case, you should adjust the parameter value. Please note that a directory re-scan invalidates all internal data of a Zettelstore. |
︙ | ︙ | |||
73 74 75 76 77 78 79 | For various reasons, the value should be a prime number, with a maximum value of 1499. === Readonly Sometimes you may want to provide zettel from a file directory place, but you want to disallow any changes. If you provide the query parameter ''readonly'' (with or without a corresponding value), the place will disallow any changes. ``` | | | 72 73 74 75 76 77 78 79 80 81 | For various reasons, the value should be a prime number, with a maximum value of 1499. === Readonly Sometimes you may want to provide zettel from a file directory place, but you want to disallow any changes. If you provide the query parameter ''readonly'' (with or without a corresponding value), the place will disallow any changes. ``` place-1-uri: dir:///home/zettel?readonly ``` If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory places will be in read-only mode too, even if not explicitly configured. |
Changes to docs/manual/00001004050000.zettel.
1 2 3 4 5 | id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | < > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk Zettelstore is not just a web service that provides services of a zettelkasten. It allows to some tasks to be executed at the command line. Typically, the task (""sub-command"") will be given at the command line as the first parameter. If no parameter is given, the Zettelstore is called as ``` zettelstore ``` This is equivalent to call it this way: ```sh mkdir -p ./zettel zettelstore run -d ./zettel -c ./.zscfg ``` Typically this is done by starting Zettelstore via a graphical user interface by double-clicking to its file icon. === Sub-commands * [[``zettelstore help``|00001004050200]] lists all available sub-commands. * [[``zettelstore version``|00001004050400]] to display version information of Zettelstore. * [[``zettelstore config``|00001004050600]] to show the currently active [[configuration|00001004000000]]. * [[``zettelstore run``|00001004051000]] to start the web-based Zettelstore service. * [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by a double.click in your GUI. * [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services. * [[``zettelstore password``|00001004051400]] to calculate data for user authentication. |
Added docs/manual/00001004050600.zettel.
> > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001004050600 title: The ''config'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk precursor: 00001004050000 Shows the Zettelstore configuration, for debugging purposes. Currently, only the [[start-up configuration|00001004010000]] is shown. This sub-command uses the same command line parameters as [[``zettelstore run``|00001004051000]]. An example for an unconfigured Zettelstore: ``` # zettelstore config Zettelstore (v0.0.4/go1.15) running on mycomputer (linux/amd64) Stores Read only = false Web Listen Addr = "127.0.0.1:23123" URL prefix = "/" ``` The first line is identical to the output of the [[``zettelkasten version``|00001004050400]] sub-command. |
Changes to docs/manual/00001004051000.zettel.
1 2 3 4 5 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | < < < < | | | | | | | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk precursor: 00001004050000 === ``zettelstore run`` This starts the web service. ``` zettelstore run [-c CONFIGFILE] [-d DIR] [-p PORT] [-r] [-v] ``` ; ''-c CONFIGFILE'' : Specifies ''CONFIGFILE'' as a file, where [[start-up configuration data|00001004010000]] is read. It is ignored, when the given file is not available, nor readable. Default: ''./.zscfg''. (''.\\.zscfg'' on Windows)), where ''.'' denotes the ""current directory"". ; ''-d DIR'' : Specifies ''DIR'' as the directory that contains all zettel. Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"". ; [!debug]''-debug'' : Allows better debugging of the internal web server by disabling any timeout values. You should specify this only as a developer. Especially do not enable it for a production server. [[https://blog.cloudflare.com/exposing-go-on-the-internet/#timeouts]] contains a good explanation for the usefulness of sensitive timeout values. ; ''-p PORT'' : Specifies the integer value ''PORT'' as the TCP port, where the Zettelstore service listens for requests. Default: 23123. Zettelstore listens only on ''127.0.0.1'', e.g. only requests from the current computer will be processed. If you want to listen on network card to process requests from other computer, please use ''listen-addr'' of the configuration file as described below. ; ''-r'' : Puts the Zettelstore in read-only mode. No changes are possible via the web interface / via the API. This allows to publish your content without any risks of unauthorized changes. ; ''-v'' : Be more verbose in writing logs. Writes the start-up configuration to stderr. Command line options take precedence over configuration file options. |
Deleted docs/manual/00001004100000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001004101000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001005000000.zettel.
︙ | ︙ | |||
18 19 20 21 22 23 24 | Zettelstore becomes extensible by external software. For example, a more sophisticated web interface could be build, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel. === Where zettel are stored Your zettel are stored as files in a specific directory. If you have not explicitly specified the directory, a default directory will be used. | | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | Zettelstore becomes extensible by external software. For example, a more sophisticated web interface could be build, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel. === Where zettel are stored Your zettel are stored as files in a specific directory. If you have not explicitly specified the directory, a default directory will be used. The directory has to be specified at [[start-up time|00001004010000]]. Nested directories are not supported (yet). Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]]. If you create a new zettel via the web interface or the API, the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss''). This allows zettel to be sorted naturally by creation time. Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences. |
︙ | ︙ |
Changes to docs/manual/00001005090000.zettel.
1 2 3 4 5 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk | < | | | | | > < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore | [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore | [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore | [[00000000000006]] | Zettelstore Environment Values | Contains environmental data of Zettelstore executable | [[00000000000008]] | Zettelstore Runtime Values | Contains values that reflect the inner working; see [[here|https://golang.org/pkg/runtime/]] for a technical description of these values | [[00000000000018]] | Zettelstore Indexer | Provides some statistics about the index process | [[00000000000020]] | Zettelstore Place Manager | Contains some statistics about zettel places | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more | [[00000000000096]] | Zettelstore Start-up Configuration | Contains the effective values of the [[start-up configuration|00001004010000]] | [[00000000000098]] | Zettelstore Start-up Values | Contains all values computed from the [[start-up configuration|00001004010000]] | [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]] | [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view | [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]] | [[00000000010300]] | Zettelstore List Meta HTML Template | Used when displaying a list of zettel | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel | [[00000000010402]] | Zettelstore Info HTML Templöate | Layout for the information view of a specific zettel | [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text | [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]] | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel | [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles | [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists | [[00000000020001]] | Zettelstore Base CSS | CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]"" | [[00000000090002]] | New User | Template for a new zettel with role ""[[user|00001006020100#user]]"" | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. **Important:** The identifier may change until a stable version of the software is released. |
Changes to docs/manual/00001006034500.zettel.
1 2 3 4 5 | id: 00001006034500 title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | id: 00001006034500 title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk Values of this type denote a point in time. === Allowed values Must be a sequence of 14 digits (""0""--""9"") (same as an [[Identifier|00001006032000]]), with the restriction that is conforms to the pattern ""YYYYMMDDhhmmss"". * YYYY is the year, * MM is the month, * DD is the day, * hh is the hour, * mm is the minute, * ss is the second. === Match operator A value matches an timestampvalue, if the first value is the prefix of the timestamp value. For example, ""202102"" matches ""20210212143200"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. If both values are timestamp values, this works well because both have the same length. |
Changes to docs/manual/00001007031000.zettel.
1 2 | id: 00001007031000 title: Zettelmarkup: Tables | < | | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007031000 title: Zettelmarkup: Tables tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual Tables are used to show some data in a two-dimenensional fashion. In zettelmarkup, table are not specified explicitly, but by entering //table rows//. Therefore, a table can be seen as a sequence of table rows. A table row is nothing as a sequence of //table cells//. The length of a table is the number of table rows, the width of a table is the maximum length of its rows. |
︙ | ︙ | |||
27 28 29 30 31 32 33 | will be rendered in HTML as: :::example | a1 | a2 | a3| | b1 | b2 | b3 | c1 | c2 ::: | < < | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | will be rendered in HTML as: :::example | a1 | a2 | a3| | b1 | b2 | b3 | c1 | c2 ::: If any cell in the first row of a table contains an equal sing character (""''=''"", ''U+003D'') as the very first character, then this first row will be interpreted as a //table header// row. For example: ```zmk | a1 | a2 |= a3| | b1 | b2 | b3 | c1 | c2 ``` will be rendered in HTML as: :::example | a1 | a2 |= a3| | b1 | b2 | b3 | c1 | c2 ::: Inside a header row, you can specify the alignment of each header cell by a given character as the last character of a cell. The alignment of a header cell determines the alignment of every cell in the same column. The following characters specify the alignment: * the colon character (""'':''"", ''U+003A'') forces a centered aligment, * the less-than sign character (""''<''"", ''U+0060'') specifies an alignment to the left, * the greater-than sign character (""''>''"", ''U+0062'') will produce right aligned cells. |
︙ | ︙ | |||
67 68 69 70 71 72 73 | will be rendered in HTML as: :::example |=Left<|Right>|Center:|Default |123456|123456|123456|123456| |123|123|123|123 ::: | < < < < < < < < < < < < < < < < < < | 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | will be rendered in HTML as: :::example |=Left<|Right>|Center:|Default |123456|123456|123456|123456| |123|123|123|123 ::: To specify the alignment of an individual cell, you can enter these characters for alignment as the first character of that cell. For example: ```zmk |=Left<|Right>|Center:|Default |>R|:C|<L |123456|123456|123456|123456| |123|123|123|123 ``` will be rendered in HTML as: :::example |=Left<|Right>|Center:|Default |>R|:C|<L |123456|123456|123456|123456| |123|123|123|123 ::: |
Changes to docs/manual/00001007040200.zettel.
1 2 | id: 00001007040200 title: Zettelmarkup: Literal-like formatting | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | id: 00001007040200 title: Zettelmarkup: Literal-like formatting tags: #manual #zettelmarkup #zettelstore syntax: zmk role: manual There are some reasons to mark text that should be rendered as uninterpreted: * Mark text as literal, sometimes as part of a program. * Mark text as input you give into a computer via a keyboard. * Mark text as output from some computer, e.g. shown at the command line. === Literal text Literal text somehow relates to [[verbatim blocks|00001007030500]]: their content should not be interpreted further, but may be rendered special. It is specified by two grave accent characters (""''`''"", ''U+0060''), followed by the text, followed by again two grave accent characters, optionally followed by an [[attribute|00001007050000]] specification. Similar to the verbatim block, the literal element allows also a modifier letter grave accent (""''ˋ''"", ''U+02CB'') as an alternative to the grave accent character[^On some devices, such as an iPhone / iPad, a grave accent character is harder to enter and is often confused with a modifier letter grave accent.]. However, all four characters must be the same. The literal element supports the default attribute: when given, all spaces in the text are rendered in HTML as open box characters (""''␣''"", ''U+2423''). The use of a generic attribute allwos to specify a (programming) language that controls syntax colouring when rendered in HTML. If you want to specify a grave accent character in the text, either use modifier grave accent characters as delimiters for the element, or place a backslash character before the grave accent character you want to use inside the element. If you want to enter a backslash character, you need to enter two of these. Examples: * ``\`\`abc def\`\``` is rendered in HTML as ::``abc def``::{=example}. * ``\`\`abc def\`\`{-}`` is rendered in HTML as ::``abc def``{-}::{=example}. * ``\`\`abc\\\`def\`\``` is rendered in HTML as ::``abc\`def``::{=example}. * ``\`\`abc\\\\def\`\``` is rendered in HTML as ::``abc\\def``::{=example}. |
︙ | ︙ |
Changes to docs/manual/00001007040300.zettel.
︙ | ︙ | |||
55 56 57 58 59 60 61 | If the text is given, it will be interpreted as an alternative textual representation, to help persons with some visual disabilities. [[Attributes|00001007050000]] are supported. They must follow the last right curly bracket character immediately. One prominent example is to specify an explicit title attribute that is shown on certain web browsers when the zettel is rendered in HTML: | | | | < < | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | If the text is given, it will be interpreted as an alternative textual representation, to help persons with some visual disabilities. [[Attributes|00001007050000]] are supported. They must follow the last right curly bracket character immediately. One prominent example is to specify an explicit title attribute that is shown on certain web browsers when the zettel is rendered in HTML: %%Example: %%``{{External link|00000000030001}}{title=External width=30}`` is rendered as ::{{External link|00000000030001}}{title=External width=30}::{=example}. === Footnotes A footnote begins with a left square bracket, followed by a circumflex accent (""''^''"", ''U+005E''), followed by some text, and ends with a right square bracket. Example: |
︙ | ︙ |
Changes to docs/manual/00001008000000.zettel.
1 2 3 4 5 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk | < > > < < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk [[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content. Zettelstore is quite agnostic with respect to markup languages. Of course, Zettelmarkup plays an important role. However, with the exception of zettel titles, you can use any (markup) language that is supported: * Markdown * Images: GIF, PNG, JPEG, SVG * CSS * HTML template data * Plain text, not further interpreted The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which language should be used. If it is not given, the key ""''default-syntax''"" will be used (specified in the [[configuration zettel|00001004020000#default-syntax]]). The following syntax values are supported: ; [!css]''css'' : A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML. ; [!gif]''gif''; [!jpeg]''jpeg''; [!jpg]''jpg''; [!png]''png'' : The formats for pixel graphics. Typically the data is stored in a separate file and the syntax is given in the ''.meta'' file. ; [!markdown]''markdown'', [!md]''md'' : For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]]. Since the world of Markdown is so diverse, a [[CommonMark|https://commonmark.org]] parser is used. See [[Use Markdown as the main markup language of Zettelstore|00001008010000]]. ; [!mustache]''mustache'' : A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML. ; [!none]''none'' : Only the metadata of a zettel is ""parsed"". Useful for displaying the full metadata. The [[runtime configuration zettel|00000000000100]] uses this syntax. The zettel content is ignored. |
︙ | ︙ |
Changes to docs/manual/00001008010000.zettel.
1 | id: 00001008010000 | | < | < < < | | | < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | id: 00001008010000 title: Use Markdown as the main markup language of Zettelstore tags: #manual #markdown #zettelstore syntax: zmk role: manual If you are customized to use Markdown as your markup language, you can configure Zettelstore to support your decision. Just add the key ''default-syntax'' with a value of ''md'' or ''markdown'' to the [[configuration zettel|00000000000100]]. Whether to use ''md'' or ''markdown'' is not just a matter to taste, but also depends on the value of ''zettel-file-syntax'' and, to some degree, on the value of ''yaml-header''. All key are described [[here|00001004020000]]. If you set ''yaml-header'' to true, then new content is always stored in a file with the extension ''.zettel''. Otherwise ''zettel-file-syntax'' lists all syntax values, where its content should be stored in a file with the extension ''.zettel''. If neither ''yaml-header'' nor ''zettel-file-syntax'' is set, new content is stored in a file where its file name extension is the same as the syntax value of that zettel. In this case it makes a difference, whether you specify ''md'' or ''markdown''. If you specify the syntax ''md'', your content will be stored in a file with the ''.md'' extension. Similar for the syntax ''markdown''. If you want to process the files that store the zettel content, e.g. with some other Markdown tools, this may be important. Not every Markdown tool allows both file extensions. BTW, metadata is stored in a file with the extension ''.meta'', if neither ''yaml-header'' nor ''zettel-file-syntax'' is set. |
Changes to docs/manual/00001010000000.zettel.
1 2 | id: 00001010000000 title: Security | < > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | id: 00001010000000 title: Security tags: #configuration #manual #security #zettelstore syntax: zmk role: manual Your zettel could contain sensitive content. You probably want to ensure that only authorized person can read and/or modify them. Zettelstore ensures this in various ways. === Local first The Zettelstore is designed to run on your local computer. If you do not configure it in other ways, no person from another computer can connect to your Zettelstore. You must explicitly configure it to allow access from other computers. In the case that your own multiple computers, you do not have to access the Zettelstore remotely. You could install Zettelstore on each computer and set-up some software to synchronize your zettel. Since zettel are stored as ordinary files, this task could be done in various ways. === Read-only You can start the Zettelstore in an read-only mode. Nobody, not even you as the owner of the Zettelstore, can change something via its interfaces[^However, as an owner, you have access to the files that store the zettel. If you modify the files, these changes will be reflected via its interfaces.]. You enable read-only mode through the key ''readonly'' in the [[start-up configuration zettel|00001004010000]] or with the ''-r'' option of the ``zettelstore run`` sub-command. === Authentication The Zettelstore can be configured that a user must authenticate itself to gain access to the content. * [[How to enable authentication|00001010040100]] * [[How to add a new user|00001010040200]] * [[How users are authenticated|00001010040400]] (some technical background) |
︙ | ︙ | |||
56 57 58 59 60 61 62 | Otherwise, an eavesdropper could fetch sensible data, such as passwords or precious content that is not for the public. The Zettelstore itself does not encrypt messages. But you can put a server in front of it, which is able to handle encryption. Most generic web server software do allow this. To enforce encryption, [[authentication sessions|00001010040700]] are marked as secure by default. | | | 56 57 58 59 60 61 62 63 64 65 66 | Otherwise, an eavesdropper could fetch sensible data, such as passwords or precious content that is not for the public. The Zettelstore itself does not encrypt messages. But you can put a server in front of it, which is able to handle encryption. Most generic web server software do allow this. To enforce encryption, [[authentication sessions|00001010040700]] are marked as secure by default. If you still want to access the Zettelstore remotely without encryption, you must change the start-up configuration. Otherwise, authentication will not work. * [[Use a server for encryption|00001010090100]] |
Changes to docs/manual/00001010040100.zettel.
1 2 | id: 00001010040100 title: Enable authentication | < > | | | 1 2 3 4 5 6 7 8 9 | id: 00001010040100 title: Enable authentication tags: #authentication #configuration #manual #security #zettelstore syntax: zmk role: manual To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner. Then you must reference this zettel within the [[start-up configuration|00001004010000#owner]] under the key ''owner''. Once the start-up configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled. |
Changes to docs/manual/00001010040700.zettel.
1 2 | id: 00001010040700 title: Access token | < > | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | id: 00001010040700 title: Access token tags: #authentication #configuration #manual #security #zettelstore syntax: zmk role: manual If an user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller. Otherwise the user will not be recognized by Zettelstore. If the user was authenticated via the web interface, the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]]. When the web browser is closed, theses cookies are not saved. If you want web browser to store the cookie as long as lifetime of that token, the owner must set ''persistent-cookie'' of the [[start-up configuration|00001004010000]] to ''true''. If the web browser remains inactive for a period, the user will be automatically logged off, because each access token has a limited lifetime. The maximum length of this period is specified by the ''token-lifetime-html'' value of the start-up configuration. Every time a web page is displayed, a fresh token is created and stored inside the cookie. If the user was authenticated via the API, the access token will be returned as the content of the response. Typically, the lifetime of this token is more short term, e.g. 10 minutes. It is specified by the ''token-timeout-api'' value of the start-up configuration. If you need more time, you can either [[re-authenticate|00001012050200]] the user or use an API call to [[renew the access token|00001012050400]]. If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), your must set the ''insecure-cookie'' value of the start-up configuration to ''true''. In most cases, such a scenario is not recommended, because user name and password will be transferred as plain text. You could make use of such scenario if you know all parties that access the local network where you access the Zettelstore. |
Changes to docs/manual/00001010070200.zettel.
1 2 3 4 5 | id: 00001010070200 title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk | < > > > > > | | | < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | id: 00001010070200 title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk For every zettel you can specify under which condition the zettel is visible to others. This is controlled with the metadata key [[''visibility''|00001006020000#visibility]]. The following values are supported: ; [!public]""public"" : The zettel is visible to everybody, even if the user is not authenticated. ; [!login]""login"" : Only an authenticated user can access the zettel. This is the default value for [[''default-visibility''|00001004020000#default-visibility]]. ; [!owner]""owner"" : Only the owner of the Zettelstore can access the zettel. This is for zettel with sensitive content, e.g. the [[configuration zettel|00001004020000]] or the various zettel that contains the templates for rendering zettel in HTML. ; [!expert]""expert"" : Only the owner of the Zettelstore can access the zettel, if runtime configuration [[''expert-mode''|00001004020000#expert-mode]] is set to a boolean true value. This is for zettel with sensitive content that might irritate the owner. Computed zettel with internal runtime information are examples for such a zettel. ; [!simple-expert]""simple-expert"" : The owner of the Zettelstore can access the zettel, if expert mode is enabled, or if authentication is disabled and the Zettelstore is started without any command. The reason for this is to show all computed zettel to an user that started the Zettestore in simple mode. Many computed zettel should be given in error reporting and a new user might not be able to enable expert mode. When you install a Zettelstore, only two zettel have visibility ""public"". The first zettel is the zettel that contains CSS for displaying the web interface. This is to ensure that the web interface looks nice even for not authenticated users. The other zettel is the zettel containing the [[version|00000000000001]] of the Zettelstore. Please note: if authentication is not enabled, every user has the same rights as the owner of a Zettelstore. This is also true, if the Zettelstore runs additionally in [[read-only mode|00001004010000#read-only-mode]]. In this case, the [[runtime configuration zettel|00001004020000]] is shown (its visibility is ""owner""). The [[start-up configuration|00001004010000]] is not shown, because the associated computed zettel with identifier ''00000000000098'' is stored with the visibility ""expert"". If you want to show such a zettel, you must set ''expert-mode'' to true. |
Changes to docs/manual/00001010090100.zettel.
1 2 3 4 5 | id: 00001010090100 title: External server to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001010090100 title: External server to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption. === Public-key encryption To enable encryption, you probably use some kind of encryption keys. In most cases, you need to deploy a ""public-key encryption"" process, where your side publish a public encryption key that only works with a corresponding private decryption key. Technically, this is not trivial. |
︙ | ︙ | |||
55 56 57 58 59 60 61 62 63 64 65 | If you want to add some additional content on the server, you could change the configuration as follows: ``` zettelstore.de { file_server * { root /var/www/html } route /manual/* { reverse_proxy localhost:23123 } } ``` | > | | | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | If you want to add some additional content on the server, you could change the configuration as follows: ``` zettelstore.de { file_server * { root /var/www/html } route /manual/* { uri strip_prefix /manual reverse_proxy localhost:23123 } } ``` This will forwards requests with the prefix ""/manual"" to the running Zettelstore. All other requests will be handled by Caddy itself. In this case you must specify the start-up configuration key ''url-prefix'' with the value ""/manual"". This is to allow the Zettelstore to give you the correct URLs with the given prefix. |
Changes to docs/manual/00001012050200.zettel.
1 2 3 4 5 6 7 8 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]]. This token has to be used for other API calls. | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]]. This token has to be used for other API calls. It is valid for a relatively short amount of time, as configured with the key ''token-timeout-api'' of the [[start-up configuration|00001004010000]] (typically 10 minutes). The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the [[endpoint|00001012920000]] ''/a'' with a POST request: ```sh # curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` |
︙ | ︙ |
Changes to docs/manual/00001012051800.zettel.
1 2 3 4 5 | id: 00001012051800 title: API: Shape the list of zettel metadata with filter options role: manual tags: #api #manual #zettelstore syntax: zmk | < | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001012051800 title: API: Shape the list of zettel metadata with filter options role: manual tags: #api #manual #zettelstore syntax: zmk In most cases, it is not essential to list //all// zettel. Typically, you are interested only in a subset of the zettel maintained by your Zettelstore. This is done by adding some query parameters to the general ''GET /z'' request. === Filter Every query parameter that does //not// begin with the low line character (""_"", ''U+005F'') is treated as the name of a [[metadata|00001006010000]] key. |
︙ | ︙ | |||
21 22 23 24 25 26 27 | {"list":[{"id":"00001012921000","url":"/z/00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","url":"/z/00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","url":"/z/00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ... ``` However, if you want all zettel that does //not// match a given value, you must prefix the value with the exclamation mark character (""!"", ''U+0021''). For example, if you want to retrieve all zettel that do not contain the string ""API"" in their title, your request will be: ```sh # curl 'http://127.0.0.1:23123/z?title=!API' | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | {"list":[{"id":"00001012921000","url":"/z/00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","url":"/z/00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","url":"/z/00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ... ``` However, if you want all zettel that does //not// match a given value, you must prefix the value with the exclamation mark character (""!"", ''U+0021''). For example, if you want to retrieve all zettel that do not contain the string ""API"" in their title, your request will be: ```sh # curl 'http://127.0.0.1:23123/z?title=!API' {"list":[{"id":"00010000000000","url":"/z/00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00000000000001 00000000000003 00000000000096 00000000000098 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","url":"/z/00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}}, ... ``` The empty query parameter values matches all zettel that contain the given metadata key. Similar, if you specify just the exclamation mark character as a query parameter value, only those zettel match that does //not// contain the given metadata key. For example ``curl 'http://localhost:23123/z?back=!&backward='`` returns all zettel that are reachable via other zettel, but also references these zettel. === Output only specific parts of a zettel |
︙ | ︙ | |||
61 62 63 64 65 66 67 | {"list":[{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012050400","url":"/z/00001012050400"}]} ``` === General filter The query parameter ""''_s''"" allows to provide a string for a full-text search of all zettel. The search string will be normalized according to Unicode NKFD, ignoring everything except letters and numbers. | < < < < < < < < < < < < < < < < < < < < < < < | | 60 61 62 63 64 65 66 67 68 69 70 | {"list":[{"id":"00001012050200","url":"/z/00001012050200"},{"id":"00001012050400","url":"/z/00001012050400"}]} ``` === General filter The query parameter ""''_s''"" allows to provide a string for a full-text search of all zettel. The search string will be normalized according to Unicode NKFD, ignoring everything except letters and numbers. You are allowed to specify this query parameter more than once. All results will be intersected, i.e. a zettel will be included into the list if both of the provided values match. This parameter loosely resembles the search box of the web user interface. |
Changes to docs/manual/00001012920000.zettel.
1 2 3 4 5 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk All API endpoints conform to the pattern ''[PREFIX]/LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is an optional URL prefix, configured via the ''url-prefix'' [[start-up configuration|00001004010000]], ; ''LETTER'' : is a single letter that specifies the ressource type, ; ''ZETTEL-ID'' : is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]]. The following letters are currently in use: |
︙ | ︙ |
Changes to domain/id/id.go.
︙ | ︙ | |||
24 25 26 27 28 29 30 | type Zid uint64 // Some important ZettelIDs. // Note: if you change some values, ensure that you also change them in the // constant place. They are mentioned there literally, because these // constants are not available there. const ( | | < < < < < < < < < < < | | | < < < < < < < < < < < < < < < < | | > > > | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | type Zid uint64 // Some important ZettelIDs. // Note: if you change some values, ensure that you also change them in the // constant place. They are mentioned there literally, because these // constants are not available there. const ( Invalid = Zid(0) // Invalid is a Zid that will never be valid ConfigurationZid = Zid(100) // WebUI HTML templates are in the range 10000..19999 BaseTemplateZid = Zid(10100) LoginTemplateZid = Zid(10200) ListTemplateZid = Zid(10300) ZettelTemplateZid = Zid(10401) InfoTemplateZid = Zid(10402) FormTemplateZid = Zid(10403) RenameTemplateZid = Zid(10404) DeleteTemplateZid = Zid(10405) ContextTemplateZid = Zid(10406) RolesTemplateZid = Zid(10500) TagsTemplateZid = Zid(10600) ErrorTemplateZid = Zid(10700) // WebUI CSS pages are in the range 20000..29999 BaseCSSZid = Zid(20001) // WebUI JS pages are in the range 30000..39999 // Range 90000...99999 is reserved for zettel templates TOCNewTemplateZid = Zid(90000) TemplateNewZettelZid = Zid(90001) TemplateNewUserZid = Zid(90002) DefaultHomeZid = Zid(10000000000) ) const maxZid = 99999999999999 // Parse interprets a string as a zettel identification and // returns its integer value. func Parse(s string) (Zid, error) { if len(s) != 14 { return Invalid, strconv.ErrSyntax } res, err := strconv.ParseUint(s, 10, 47) if err != nil { return Invalid, err } if res == 0 { return Invalid, strconv.ErrRange } return Zid(res), nil } const digits = "0123456789" // String converts the zettel identification to a string of 14 digits. // Only defined for valid ids. |
︙ | ︙ |
Changes to domain/id/id_test.go.
︙ | ︙ | |||
49 50 51 52 53 54 55 | "i=%d: zid=%v does not format to %q, but to %q", i, sid, zid, s) } } invalidIDs := []string{ "", "0", "a", "00000000000000", | < > | | | | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | "i=%d: zid=%v does not format to %q, but to %q", i, sid, zid, s) } } invalidIDs := []string{ "", "0", "a", "00000000000000", "000000000000000", "99999999999999a", "20200310T195100", } for i, zid := range invalidIDs { if _, err := id.Parse(zid); err == nil { t.Errorf("i=%d: zid=%q is valid, but should not be", i, zid) } } } |
Changes to domain/id/set.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 | // NewSet returns a new set of identifier with the given initial values. func NewSet(zids ...Zid) Set { l := len(zids) if l < 8 { l = 8 } result := make(Set, l) | > | > < < < < < < | | > | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | // NewSet returns a new set of identifier with the given initial values. func NewSet(zids ...Zid) Set { l := len(zids) if l < 8 { l = 8 } result := make(Set, l) for _, zid := range zids { result[zid] = true } return result } // NewSetCap returns a new set of identifier with the given capacity and initial values. func NewSetCap(c int, zids ...Zid) Set { l := len(zids) if c < l { c = l } if c < 8 { c = 8 } result := make(Set, c) for _, zid := range zids { result[zid] = true } return result } // Sorted returns the set as a sorted slice of zettel identifier. func (s Set) Sorted() Slice { if l := len(s); l > 0 { result := make(Slice, 0, l) for zid := range s { |
︙ | ︙ | |||
75 76 77 78 79 80 81 | otherInSet, otherOk := other[zid] if !otherInSet || !otherOk { delete(s, zid) } } return s } | < < < < < < < < < < < < | 72 73 74 75 76 77 78 | otherInSet, otherOk := other[zid] if !otherInSet || !otherOk { delete(s, zid) } } return s } |
Changes to domain/id/set_test.go.
︙ | ︙ | |||
57 58 59 60 61 62 63 | } got = id.NewSet(sl2...).Intersect(id.NewSet(sl1...)).Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Intersect(%v) should be %v, but got %v", i, sl2, sl1, tc.exp, got) } } } | < < < < < < < < < < < < < < < < < < < < < < < < < | 57 58 59 60 61 62 63 | } got = id.NewSet(sl2...).Intersect(id.NewSet(sl1...)).Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Intersect(%v) should be %v, but got %v", i, sl2, sl1, tc.exp, got) } } } |
Changes to domain/meta/meta.go.
︙ | ︙ | |||
151 152 153 154 155 156 157 | // Important values for some keys. const ( ValueRoleConfiguration = "configuration" ValueRoleUser = "user" ValueRoleZettel = "zettel" ValueSyntaxNone = "none" | < < < > | 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | // Important values for some keys. const ( ValueRoleConfiguration = "configuration" ValueRoleUser = "user" ValueRoleZettel = "zettel" ValueSyntaxNone = "none" ValueSyntaxZmk = "zmk" ValueTrue = "true" ValueFalse = "false" ValueUserRoleReader = "reader" ValueUserRoleWriter = "writer" ValueUserRoleOwner = "owner" ValueVisibilityExpert = "expert" ValueVisibilityOwner = "owner" ValueVisibilityLogin = "login" ValueVisibilityPublic = "public" ValueVisibilitySimple = "simple-expert" ) // Meta contains all meta-data of a zettel. type Meta struct { Zid id.Zid pairs map[string]string YamlSep bool |
︙ | ︙ |
Changes to domain/meta/values.go.
1 | //----------------------------------------------------------------------------- | | | 1 2 3 4 5 6 7 8 9 | //----------------------------------------------------------------------------- // Copyright (c) 2020 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. //----------------------------------------------------------------------------- |
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityLogin VisibilityOwner VisibilityExpert ) var visMap = map[string]Visibility{ ValueVisibilityPublic: VisibilityPublic, ValueVisibilityLogin: VisibilityLogin, ValueVisibilityOwner: VisibilityOwner, ValueVisibilityExpert: VisibilityExpert, } // GetVisibility returns the visibility value of the given string func GetVisibility(val string) Visibility { if vis, ok := visMap[val]; ok { return vis | > > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityLogin VisibilityOwner VisibilitySimple VisibilityExpert ) var visMap = map[string]Visibility{ ValueVisibilityPublic: VisibilityPublic, ValueVisibilityLogin: VisibilityLogin, ValueVisibilityOwner: VisibilityOwner, ValueVisibilitySimple: VisibilitySimple, ValueVisibilityExpert: VisibilityExpert, } // GetVisibility returns the visibility value of the given string func GetVisibility(val string) Visibility { if vis, ok := visMap[val]; ok { return vis |
︙ | ︙ |
Changes to encoder/htmlenc/block.go.
︙ | ︙ | |||
52 53 54 55 56 57 58 | v.b.WriteByte('\n') } v.b.WriteString("-->\n") } case ast.VerbatimHTML: for _, line := range vn.Lines { | < | < < < < < < < < < < < < < < < < < < | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | v.b.WriteByte('\n') } v.b.WriteString("-->\n") } case ast.VerbatimHTML: for _, line := range vn.Lines { v.b.WriteStrings(line, "\n") } default: panic(fmt.Sprintf("Unknown verbatim code %v", vn.Code)) } } var specialSpanAttr = map[string]bool{ "example": true, "note": true, "tip": true, "important": true, "caution": true, "warning": true, |
︙ | ︙ |
Changes to encoder/htmlenc/inline.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "fmt" "strconv" "strings" "zettelstore.de/z/ast" | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "fmt" "strconv" "strings" "zettelstore.de/z/ast" ) // VisitText writes text content. func (v *visitor) VisitText(tn *ast.TextNode) { v.writeHTMLEscaped(tn.Text) } |
︙ | ︙ | |||
250 251 252 253 254 255 256 | v.b.WriteByte('>') v.acceptInlineSlice(ins) v.b.WriteString("</span>") } var langQuotes = map[string][2]string{ | | | | | 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | v.b.WriteByte('>') v.acceptInlineSlice(ins) v.b.WriteString("</span>") } var langQuotes = map[string][2]string{ "en": {"“", "”"}, "de": {"„", "“"}, "fr": {"« ", " »"}, } func getQuotes(lang string) (string, string) { langFields := strings.FieldsFunc(lang, func(r rune) bool { return r == '-' || r == '_' }) for len(langFields) > 0 { langSup := strings.Join(langFields, "-") quotes, ok := langQuotes[langSup] |
︙ | ︙ | |||
298 299 300 301 302 303 304 | case ast.LiteralOutput: v.writeLiteral("<samp", "</samp>", ln.Attrs, ln.Text) case ast.LiteralComment: v.b.WriteString("<!-- ") v.writeHTMLEscaped(ln.Text) // writeCommentEscaped v.b.WriteString(" -->") case ast.LiteralHTML: | < | < | 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | case ast.LiteralOutput: v.writeLiteral("<samp", "</samp>", ln.Attrs, ln.Text) case ast.LiteralComment: v.b.WriteString("<!-- ") v.writeHTMLEscaped(ln.Text) // writeCommentEscaped v.b.WriteString(" -->") case ast.LiteralHTML: v.b.WriteString(ln.Text) default: panic(fmt.Sprintf("Unknown literal code %v", ln.Code)) } } func (v *visitor) writeLiteral(codeS, codeE string, attrs *ast.Attributes, text string) { oldVisible := v.visibleSpace |
︙ | ︙ |
Changes to encoder/zmkenc/zmkenc.go.
︙ | ︙ | |||
268 269 270 271 272 273 274 | if i < len(tn.Text)-1 { s := tn.Text[i : i+2] if _, ok := escapeSeqs[s]; ok { v.b.WriteString(tn.Text[last:i]) for j := 0; j < len(s); j++ { v.b.WriteBytes('\\', s[j]) } | < | 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | if i < len(tn.Text)-1 { s := tn.Text[i : i+2] if _, ok := escapeSeqs[s]; ok { v.b.WriteString(tn.Text[last:i]) for j := 0; j < len(s); j++ { v.b.WriteBytes('\\', s[j]) } last = i + 1 continue } } } v.b.WriteString(tn.Text[last:]) } |
︙ | ︙ |
Changes to go.mod.
1 2 3 4 5 6 7 | module zettelstore.de/z go 1.16 require ( github.com/fsnotify/fsnotify v1.4.9 github.com/pascaldekloe/jwt v1.10.0 | | | 1 2 3 4 5 6 7 8 9 10 11 12 | module zettelstore.de/z go 1.16 require ( github.com/fsnotify/fsnotify v1.4.9 github.com/pascaldekloe/jwt v1.10.0 github.com/yuin/goldmark v1.3.5 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 golang.org/x/text v0.3.6 ) |
Changes to go.sum.
1 2 3 4 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs= github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A= | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs= github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A= github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= |
︙ | ︙ |
Added index/filter.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package index allows to search for metadata and content. package index import ( "context" "zettelstore.de/z/domain/meta" ) // Remover is used to remove some metadata before they are stored in a place. type Remover interface { // Remove removes computed properties from the given metadata. // It is called by the manager place before meta data is updated. Remove(ctx context.Context, m *meta.Meta) } // MetaFilter is used by places to filter and set computed metadata value. type MetaFilter interface { Enricher Remover } type metaFilter struct { index Indexer properties map[string]bool // Set of property key names } // NewMetaFilter creates a new meta filter. func NewMetaFilter(idx Indexer) MetaFilter { properties := make(map[string]bool) for _, kd := range meta.GetSortedKeyDescriptions() { if kd.IsProperty() { properties[kd.Name] = true } } return &metaFilter{ index: idx, properties: properties, } } func (mf *metaFilter) Enrich(ctx context.Context, m *meta.Meta) { computePublished(m) mf.index.Enrich(ctx, m) } func computePublished(m *meta.Meta) { if _, ok := m.Get(meta.KeyPublished); ok { return } if modified, ok := m.Get(meta.KeyModified); ok { if _, ok = meta.TimeValue(modified); ok { m.Set(meta.KeyPublished, modified) return } } zid := m.Zid.String() if _, ok := meta.TimeValue(zid); ok { m.Set(meta.KeyPublished, zid) return } // Neither the zettel was modified nor the zettel identifer contains a valid // timestamp. In this case do not set the "published" property. } func (mf *metaFilter) Remove(ctx context.Context, m *meta.Meta) { for _, p := range m.PairsRest(true) { if mf.properties[p.Key] { m.Delete(p.Key) } } } |
Added index/index.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package index allows to search for metadata and content. package index import ( "context" "io" "time" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place/change" ) // Enricher is used to update metadata by adding new properties. type Enricher interface { // Enrich computes additional properties and updates the given metadata. // It is typically called by zettel reading methods. Enrich(ctx context.Context, m *meta.Meta) } // Selector is used to select zettel identifier based on selection criteria. type Selector interface { // Select all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD. Select(word string) id.Set // Select all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD. SelectPrefix(prefix string) id.Set // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD. SelectContains(s string) id.Set } // NoEnrichContext will signal an enricher that nothing has to be done. // This is useful for an Indexer, but also for some place.Place calls, when // just the plain metadata is needed. func NoEnrichContext(ctx context.Context) context.Context { return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey) } type ctxNoEnrichType struct{} var ctxNoEnrichKey ctxNoEnrichType // DoNotEnrich determines if the context is marked to not enrich metadata. func DoNotEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) return ok } // Port contains all the used functions to access zettel to be indexed. type Port interface { RegisterObserver(change.Func) FetchZids(context.Context) (id.Set, error) GetMeta(context.Context, id.Zid) (*meta.Meta, error) GetZettel(context.Context, id.Zid) (domain.Zettel, error) } // Indexer contains all the functions of an index. type Indexer interface { Enricher Selector // Start the index. It will read all zettel and store index data for later retrieval. Start(Port) // Stop the index. No zettel are read any more, but the current index data // can stil be retrieved. Stop() // ReadStats populates st with indexer statistics. ReadStats(st *IndexerStats) } // IndexerStats records statistics about the indexer. type IndexerStats struct { // LastReload stores the timestamp when a full re-index was done. LastReload time.Time // IndexesSinceReload counts indexing a zettel since the full re-index. IndexesSinceReload uint64 // DurLastIndex is the duration of the last index run. This could be a // full re-index or a re-index of a single zettel. DurLastIndex time.Duration // Store records statistics about the underlying index store. Store StoreStats } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { Enricher Selector // UpdateReferences for a specific zettel. // Returns set of zettel identifier that must also be checked for changes. UpdateReferences(context.Context, *ZettelIndex) id.Set // DeleteZettel removes index data for given zettel. // Returns set of zettel identifier that must also be checked for changes. DeleteZettel(context.Context, id.Zid) id.Set // ReadStats populates st with store statistics. ReadStats(st *StoreStats) // Write the content to a Writer. Write(io.Writer) } // StoreStats records statistics about the store. type StoreStats struct { // Zettel is the number of zettel managed by the indexer. Zettel int // Updates count the number of metadata updates. Updates uint64 // Words count the different words stored in the store. Words uint64 } |
Added index/indexer/anteroom.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package indexer allows to search for metadata and content. package indexer import ( "sync" "zettelstore.de/z/domain/id" ) type arAction int const ( arNothing arAction = iota arReload arUpdate arDelete ) type anteroom struct { next *anteroom waiting map[id.Zid]arAction curLoad int reload bool } type anterooms struct { mx sync.Mutex first *anteroom last *anteroom maxLoad int } func newAnterooms(maxLoad int) *anterooms { return &anterooms{maxLoad: maxLoad} } func (ar *anterooms) Enqueue(zid id.Zid, action arAction) { if !zid.IsValid() || action == arNothing || action == arReload { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { ar.first = ar.makeAnteroom(zid, action) ar.last = ar.first return } for room := ar.first; room != nil; room = room.next { if room.reload { continue // Do not place zettel in reload room } a, ok := room.waiting[zid] if !ok { continue } switch action { case a: return case arUpdate: room.waiting[zid] = action case arDelete: room.waiting[zid] = action } return } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { room.waiting[zid] = action room.curLoad++ return } room := ar.makeAnteroom(zid, action) ar.last.next = room ar.last = room } func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom { c := ar.maxLoad if c == 0 { c = 100 } waiting := make(map[id.Zid]arAction, c) waiting[zid] = action return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false} } func (ar *anterooms) Reset() { ar.mx.Lock() defer ar.mx.Unlock() ar.first = ar.makeAnteroom(id.Invalid, arReload) ar.last = ar.first } func (ar *anterooms) Reload(delZids id.Slice, newZids id.Set) { ar.mx.Lock() defer ar.mx.Unlock() delWaiting := createWaitingSlice(delZids, arDelete) newWaiting := createWaitingSet(newZids, arUpdate) ar.deleteReloadedRooms() if ds := len(delWaiting); ds > 0 { if ns := len(newWaiting); ns > 0 { roomNew := &anteroom{next: ar.first, waiting: newWaiting, curLoad: ns, reload: true} ar.first = &anteroom{next: roomNew, waiting: delWaiting, curLoad: ds, reload: true} if roomNew.next == nil { ar.last = roomNew } return } ar.first = &anteroom{next: ar.first, waiting: delWaiting, curLoad: ds} if ar.first.next == nil { ar.last = ar.first } return } if ns := len(newWaiting); ns > 0 { ar.first = &anteroom{next: ar.first, waiting: newWaiting, curLoad: ns} if ar.first.next == nil { ar.last = ar.first } return } ar.first = nil ar.last = nil } func createWaitingSlice(zids id.Slice, action arAction) map[id.Zid]arAction { waitingSet := make(map[id.Zid]arAction, len(zids)) for _, zid := range zids { if zid.IsValid() { waitingSet[zid] = action } } return waitingSet } func createWaitingSet(zids id.Set, action arAction) map[id.Zid]arAction { waitingSet := make(map[id.Zid]arAction, len(zids)) for zid := range zids { if zid.IsValid() { waitingSet[zid] = action } } return waitingSet } func (ar *anterooms) deleteReloadedRooms() { room := ar.first for room != nil && room.reload { room = room.next } ar.first = room if room == nil { ar.last = nil } } func (ar *anterooms) Dequeue() (arAction, id.Zid) { ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { return arNothing, id.Invalid } for zid, action := range ar.first.waiting { delete(ar.first.waiting, zid) if len(ar.first.waiting) == 0 { ar.first = ar.first.next if ar.first == nil { ar.last = nil } } return action, zid } return arNothing, id.Invalid } |
Added index/indexer/anteroom_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package indexer allows to search for metadata and content. package indexer import ( "testing" "zettelstore.de/z/domain/id" ) func TestSimple(t *testing.T) { ar := newAnterooms(2) ar.Enqueue(id.Zid(1), arUpdate) action, zid := ar.Dequeue() if zid != id.Zid(1) || action != arUpdate { t.Errorf("Expected 1/arUpdate, but got %v/%v", zid, action) } action, zid = ar.Dequeue() if zid != id.Invalid && action != arDelete { t.Errorf("Expected invalid Zid, but got %v", zid) } ar.Enqueue(id.Zid(1), arUpdate) ar.Enqueue(id.Zid(2), arUpdate) if ar.first != ar.last { t.Errorf("Expected one room, but got more") } ar.Enqueue(id.Zid(3), arUpdate) if ar.first == ar.last { t.Errorf("Expected more than one room, but got only one") } count := 0 for ; count < 1000; count++ { action, _ := ar.Dequeue() if action == arNothing { break } } if count != 3 { t.Errorf("Expected 3 dequeues, but got %v", count) } } func TestReset(t *testing.T) { ar := newAnterooms(1) ar.Enqueue(id.Zid(1), arUpdate) ar.Reset() action, zid := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } ar.Reload(id.Slice{2}, id.NewSet(3, 4)) ar.Enqueue(id.Zid(5), arUpdate) ar.Enqueue(id.Zid(5), arDelete) ar.Enqueue(id.Zid(5), arDelete) ar.Enqueue(id.Zid(5), arUpdate) if ar.first == ar.last || ar.first.next == ar.last || ar.first.next.next != ar.last { t.Errorf("Expected 3 rooms") } action, zid = ar.Dequeue() if zid != id.Zid(2) || action != arDelete { t.Errorf("Expected 2/arDelete, but got %v/%v", zid, action) } action, zid1 := ar.Dequeue() if action != arUpdate { t.Errorf("Expected arUpdate, but got %v", action) } action, zid2 := ar.Dequeue() if action != arUpdate { t.Errorf("Expected arUpdate, but got %v", action) } if !(zid1 == id.Zid(3) && zid2 == id.Zid(4) || zid1 == id.Zid(4) && zid2 == id.Zid(3)) { t.Errorf("Zids must be 3 or 4, but got %v/%v", zid1, zid2) } action, zid = ar.Dequeue() if zid != id.Zid(5) || action != arUpdate { t.Errorf("Expected 5/arUpdate, but got %v/%v", zid, action) } action, zid = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Reload(nil, id.NewSet(id.Zid(6))) action, zid = ar.Dequeue() if zid != id.Zid(6) || action != arUpdate { t.Errorf("Expected 6/arUpdate, but got %v/%v", zid, action) } action, zid = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Reload(id.Slice{7}, nil) action, zid = ar.Dequeue() if zid != id.Zid(7) || action != arDelete { t.Errorf("Expected 7/arDelete, but got %v/%v", zid, action) } action, zid = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Enqueue(id.Zid(8), arUpdate) ar.Reload(nil, nil) action, zid = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } } |
Added index/indexer/collect.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package indexer allows to search for metadata and content. package indexer import ( "zettelstore.de/z/ast" "zettelstore.de/z/domain/id" "zettelstore.de/z/index" "zettelstore.de/z/strfun" ) func collectZettelIndexData(zn *ast.ZettelNode, refs id.Set, words index.WordSet) { ixv := ixVisitor{refs: refs, words: words} ast.NewTopDownTraverser(&ixv).VisitBlockSlice(zn.Ast) } func collectInlineIndexData(ins ast.InlineSlice, refs id.Set, words index.WordSet) { ixv := ixVisitor{refs: refs, words: words} ast.NewTopDownTraverser(&ixv).VisitInlineSlice(ins) } type ixVisitor struct { refs id.Set words index.WordSet } // VisitVerbatim collects the verbatim text in the word set. func (lv *ixVisitor) VisitVerbatim(vn *ast.VerbatimNode) { for _, line := range vn.Lines { lv.addText(line) } } // VisitRegion does nothing. func (lv *ixVisitor) VisitRegion(rn *ast.RegionNode) {} // VisitHeading does nothing. func (lv *ixVisitor) VisitHeading(hn *ast.HeadingNode) {} // VisitHRule does nothing. func (lv *ixVisitor) VisitHRule(hn *ast.HRuleNode) {} // VisitList does nothing. func (lv *ixVisitor) VisitNestedList(ln *ast.NestedListNode) {} // VisitDescriptionList does nothing. func (lv *ixVisitor) VisitDescriptionList(dn *ast.DescriptionListNode) {} // VisitPara does nothing. func (lv *ixVisitor) VisitPara(pn *ast.ParaNode) {} // VisitTable does nothing. func (lv *ixVisitor) VisitTable(tn *ast.TableNode) {} // VisitBLOB does nothing. func (lv *ixVisitor) VisitBLOB(bn *ast.BLOBNode) {} // VisitText collects the text in the word set. func (lv *ixVisitor) VisitText(tn *ast.TextNode) { lv.addText(tn.Text) } // VisitTag collects the tag name in the word set. func (lv *ixVisitor) VisitTag(tn *ast.TagNode) { lv.addText(tn.Tag) } // VisitSpace does nothing. func (lv *ixVisitor) VisitSpace(sn *ast.SpaceNode) {} // VisitBreak does nothing. func (lv *ixVisitor) VisitBreak(bn *ast.BreakNode) {} // VisitLink collects the given link as a reference. func (lv *ixVisitor) VisitLink(ln *ast.LinkNode) { ref := ln.Ref if ref == nil || !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { lv.refs[zid] = true } } // VisitImage collects the image links as a reference. func (lv *ixVisitor) VisitImage(in *ast.ImageNode) { ref := in.Ref if ref == nil || !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { lv.refs[zid] = true } } // VisitCite does nothing. func (lv *ixVisitor) VisitCite(cn *ast.CiteNode) {} // VisitFootnote does nothing. func (lv *ixVisitor) VisitFootnote(fn *ast.FootnoteNode) {} // VisitMark does nothing. func (lv *ixVisitor) VisitMark(mn *ast.MarkNode) {} // VisitFormat does nothing. func (lv *ixVisitor) VisitFormat(fn *ast.FormatNode) {} // VisitLiteral collects the literal words in the word set. func (lv *ixVisitor) VisitLiteral(ln *ast.LiteralNode) { lv.addText(ln.Text) } func (lv *ixVisitor) addText(s string) { for _, word := range strfun.NormalizeWords(s) { lv.words[word] = lv.words[word] + 1 } } |
Added index/indexer/indexer.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package indexer allows to search for metadata and content. package indexer import ( "context" "log" "runtime/debug" "sync" "time" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/index/memstore" "zettelstore.de/z/parser" "zettelstore.de/z/place/change" "zettelstore.de/z/strfun" ) type indexer struct { store index.Store ar *anterooms ready chan struct{} // Signal a non-empty anteroom to background task done chan struct{} // Stop background task observe bool started bool // Stats data mx sync.RWMutex lastReload time.Time sinceReload uint64 durLastIndex time.Duration } // New creates a new indexer. func New() index.Indexer { return &indexer{ store: memstore.New(), ar: newAnterooms(10), ready: make(chan struct{}, 1), } } func (idx *indexer) observer(ci change.Info) { switch ci.Reason { case change.OnReload: idx.ar.Reset() case change.OnUpdate: idx.ar.Enqueue(ci.Zid, arUpdate) case change.OnDelete: idx.ar.Enqueue(ci.Zid, arDelete) default: return } select { case idx.ready <- struct{}{}: default: } } func (idx *indexer) Start(p index.Port) { if idx.started { panic("Index already started") } idx.done = make(chan struct{}) if !idx.observe { p.RegisterObserver(idx.observer) idx.observe = true } idx.ar.Reset() // Ensure an initial index run go idx.indexer(p) idx.started = true } func (idx *indexer) Stop() { if !idx.started { panic("Index already stopped") } close(idx.done) idx.started = false } // Enrich reads all properties in the index and updates the metadata. func (idx *indexer) Enrich(ctx context.Context, m *meta.Meta) { if index.DoNotEnrich(ctx) { // Enrich is called indirectly via indexer or enrichment is not requested // because of other reasons -> ignore this call, do not update meta data return } idx.store.Enrich(ctx, m) } // Select all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD. func (idx *indexer) Select(word string) id.Set { return idx.store.Select(word) } // Select all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD. func (idx *indexer) SelectPrefix(prefix string) id.Set { return idx.store.SelectPrefix(prefix) } // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD. func (idx *indexer) SelectContains(s string) id.Set { return idx.store.SelectContains(s) } // ReadStats populates st with indexer statistics. func (idx *indexer) ReadStats(st *index.IndexerStats) { idx.mx.RLock() st.LastReload = idx.lastReload st.IndexesSinceReload = idx.sinceReload st.DurLastIndex = idx.durLastIndex idx.mx.RUnlock() idx.store.ReadStats(&st.Store) } type indexerPort interface { getMetaPort FetchZids(ctx context.Context) (id.Set, error) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) } // indexer runs in the background and updates the index data structures. // This is the main service of the indexer. func (idx *indexer) indexer(p indexerPort) { // Something may panic. Ensure a running indexer. defer func() { if r := recover(); r != nil { log.Println("recovered from:", r) debug.PrintStack() go idx.indexer(p) } }() timerDuration := 15 * time.Second timer := time.NewTimer(timerDuration) ctx := index.NoEnrichContext(context.Background()) for { start := time.Now() if idx.workService(ctx, p) { idx.mx.Lock() idx.durLastIndex = time.Since(start) idx.mx.Unlock() } if !idx.sleepService(timer, timerDuration) { return } } } func (idx *indexer) workService(ctx context.Context, p indexerPort) bool { changed := false for { switch action, zid := idx.ar.Dequeue(); action { case arNothing: return changed case arReload: zids, err := p.FetchZids(ctx) if err == nil { idx.ar.Reload(nil, zids) idx.mx.Lock() idx.lastReload = time.Now() idx.sinceReload = 0 idx.mx.Unlock() } case arUpdate: changed = true idx.mx.Lock() idx.sinceReload++ idx.mx.Unlock() zettel, err := p.GetZettel(ctx, zid) if err != nil { // TODO: on some errors put the zid into a "try later" set continue } idx.updateZettel(ctx, zettel, p) case arDelete: changed = true idx.mx.Lock() idx.sinceReload++ idx.mx.Unlock() idx.deleteZettel(zid) } } } func (idx *indexer) sleepService(timer *time.Timer, timerDuration time.Duration) bool { select { case _, ok := <-idx.ready: if !ok { return false } case _, ok := <-timer.C: if !ok { return false } timer.Reset(timerDuration) case _, ok := <-idx.done: if !ok { if !timer.Stop() { <-timer.C } return false } } return true } type getMetaPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } func (idx *indexer) updateZettel(ctx context.Context, zettel domain.Zettel, p getMetaPort) { m := zettel.Meta if m.GetBool(meta.KeyNoIndex) { // Zettel maybe in index toCheck := idx.store.DeleteZettel(ctx, m.Zid) idx.checkZettel(toCheck) return } refs := id.NewSet() words := make(index.WordSet) collectZettelIndexData(parser.ParseZettel(zettel, ""), refs, words) zi := index.NewZettelIndex(m.Zid) for _, pair := range m.Pairs(false) { descr := meta.GetDescription(pair.Key) if descr.IsComputed() { continue } switch descr.Type { case meta.TypeID: updateValue(ctx, descr.Inverse, pair.Value, p, zi) case meta.TypeIDSet: for _, val := range meta.ListFromValue(pair.Value) { updateValue(ctx, descr.Inverse, val, p, zi) } case meta.TypeZettelmarkup: collectInlineIndexData(parser.ParseMetadata(pair.Value), refs, words) default: for _, word := range strfun.NormalizeWords(pair.Value) { words[word] = words[word] + 1 } } } for ref := range refs { if _, err := p.GetMeta(ctx, ref); err == nil { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } } zi.SetWords(words) toCheck := idx.store.UpdateReferences(ctx, zi) idx.checkZettel(toCheck) } func updateValue(ctx context.Context, inverse string, value string, p getMetaPort, zi *index.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } if _, err := p.GetMeta(ctx, zid); err != nil { zi.AddDeadRef(zid) return } if inverse == "" { zi.AddBackRef(zid) return } zi.AddMetaRef(inverse, zid) } func (idx *indexer) deleteZettel(zid id.Zid) { toCheck := idx.store.DeleteZettel(context.Background(), zid) idx.checkZettel(toCheck) } func (idx *indexer) checkZettel(s id.Set) { for zid := range s { idx.ar.Enqueue(zid, arUpdate) } } |
Added index/memstore/memstore.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "context" "fmt" "io" "strings" "sync" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" ) type metaRefs struct { forward id.Slice backward id.Slice } type zettelIndex struct { dead id.Slice forward id.Slice backward id.Slice meta map[string]metaRefs words []string } func (zi *zettelIndex) isEmpty() bool { if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 { return false } return zi.meta == nil || len(zi.meta) == 0 } type memStore struct { mx sync.RWMutex idx map[id.Zid]*zettelIndex dead map[id.Zid]id.Slice // map dead refs where they occur words map[string]id.Slice // Stats updates uint64 } // New returns a new memory-based index store. func New() index.Store { return &memStore{ idx: make(map[id.Zid]*zettelIndex), dead: make(map[id.Zid]id.Slice), words: make(map[string]id.Slice), } } func (ms *memStore) Enrich(ctx context.Context, m *meta.Meta) { if ms.doEnrich(ctx, m) { ms.mx.Lock() ms.updates++ ms.mx.Unlock() } } func (ms *memStore) doEnrich(ctx context.Context, 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(meta.KeyDead, zi.dead.String()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Copy()) if len(zi.backward) > 0 { m.Set(meta.KeyBackward, zi.backward.String()) updated = true } if len(zi.forward) > 0 { m.Set(meta.KeyForward, zi.forward.String()) back = remRefs(back, zi.forward) updated = true } if len(zi.meta) > 0 { for k, refs := range zi.meta { if len(refs.backward) > 0 { m.Set(k, refs.backward.String()) back = remRefs(back, refs.backward) updated = true } } } if len(back) > 0 { m.Set(meta.KeyBack, back.String()) updated = true } return updated } // Select all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD. func (ms *memStore) Select(word string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() if refs, ok := ms.words[word]; ok { return id.NewSet(refs...) } return nil } // Select all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD. func (ms *memStore) SelectPrefix(prefix string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() return ms.selectWithPred(prefix, strings.HasPrefix) } // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD. func (ms *memStore) SelectContains(s string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() return ms.selectWithPred(s, strings.Contains) } func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { result := id.NewSet() for word, refs := range ms.words { if !pred(word, s) { continue } for _, ref := range refs { result[ref] = true } } return result } func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { for _, p := range m.PairsRest(false) { switch meta.Type(p.Key) { case meta.TypeID: if zid, err := id.Parse(p.Value); err == nil { back = remRef(back, zid) } case meta.TypeIDSet: for _, val := range meta.ListFromValue(p.Value) { if zid, err := id.Parse(val); err == nil { back = remRef(back, zid) } } } } return back } func (ms *memStore) UpdateReferences(ctx context.Context, zidx *index.ZettelIndex) id.Set { ms.mx.Lock() defer ms.mx.Unlock() zi, ziExist := ms.idx[zidx.Zid] if !ziExist || zi == nil { zi = &zettelIndex{} ziExist = false } // Is this zettel an old dead reference mentioned in other zettel? var toCheck id.Set if refs, ok := ms.dead[zidx.Zid]; ok { // These must be checked later again toCheck = id.NewSet(refs...) delete(ms.dead, zidx.Zid) } ms.updateDeadReferences(zidx, zi) ms.updateForwardBackwardReferences(zidx, zi) ms.updateMetadataReferences(zidx, zi) ms.updateWords(zidx, zi) // Check if zi must be inserted into ms.idx if !ziExist && !zi.isEmpty() { ms.idx[zidx.Zid] = zi } return toCheck } func (ms *memStore) updateDeadReferences(zidx *index.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! drefs := zidx.GetDeadRefs() newRefs, remRefs := refsDiff(drefs, zi.dead) zi.dead = drefs for _, ref := range remRefs { ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) } for _, ref := range newRefs { ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) } } func (ms *memStore) updateForwardBackwardReferences(zidx *index.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! brefs := zidx.GetBackRefs() newRefs, remRefs := refsDiff(brefs, zi.forward) zi.forward = brefs for _, ref := range remRefs { bzi := ms.getEntry(ref) bzi.backward = remRef(bzi.backward, zidx.Zid) } for _, ref := range newRefs { bzi := ms.getEntry(ref) bzi.backward = addRef(bzi.backward, zidx.Zid) } } func (ms *memStore) updateMetadataReferences(zidx *index.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! metarefs := zidx.GetMetaRefs() for key, mr := range zi.meta { if _, ok := metarefs[key]; ok { continue } ms.removeInverseMeta(zidx.Zid, key, mr.forward) } if zi.meta == nil { zi.meta = make(map[string]metaRefs) } for key, mrefs := range metarefs { mr := zi.meta[key] newRefs, remRefs := refsDiff(mrefs, mr.forward) mr.forward = mrefs zi.meta[key] = mr for _, ref := range newRefs { bzi := ms.getEntry(ref) if bzi.meta == nil { bzi.meta = make(map[string]metaRefs) } bmr := bzi.meta[key] bmr.backward = addRef(bmr.backward, zidx.Zid) bzi.meta[key] = bmr } ms.removeInverseMeta(zidx.Zid, key, remRefs) } } func (ms *memStore) updateWords(zidx *index.ZettelIndex, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! words := zidx.GetWords() newWords, removeWords := words.Diff(zi.words) for _, word := range newWords { if refs, ok := ms.words[word]; ok { ms.words[word] = addRef(refs, zidx.Zid) continue } ms.words[word] = id.Slice{zidx.Zid} } for _, word := range removeWords { refs, ok := ms.words[word] if !ok { continue } refs2 := remRef(refs, zidx.Zid) if len(refs2) == 0 { delete(ms.words, word) continue } ms.words[word] = refs2 } zi.words = words.Words() } func (ms *memStore) getEntry(zid id.Zid) *zettelIndex { // Must only be called if ms.mx is write-locked! if zi, ok := ms.idx[zid]; ok { return zi } zi := &zettelIndex{} ms.idx[zid] = zi return zi } func (ms *memStore) DeleteZettel(ctx context.Context, zid id.Zid) id.Set { ms.mx.Lock() defer ms.mx.Unlock() zi, ok := ms.idx[zid] if !ok { return nil } ms.deleteDeadSources(zid, zi) toCheck := ms.deleteForwardBackward(zid, zi) if len(zi.meta) > 0 { for key, mrefs := range zi.meta { ms.removeInverseMeta(zid, key, mrefs.forward) } } ms.deleteWords(zid, zi.words) delete(ms.idx, zid) return toCheck } func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) { // Must only be called if ms.mx is write-locked! for _, ref := range zi.dead { if drefs, ok := ms.dead[ref]; ok { drefs = remRef(drefs, zid) if len(drefs) > 0 { ms.dead[ref] = drefs } else { delete(ms.dead, ref) } } } } func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set { // Must only be called if ms.mx is write-locked! var toCheck id.Set for _, ref := range zi.forward { if fzi, ok := ms.idx[ref]; ok { fzi.backward = remRef(fzi.backward, zid) } } for _, ref := range zi.backward { if bzi, ok := ms.idx[ref]; ok { bzi.forward = remRef(bzi.forward, zid) if toCheck == nil { toCheck = id.NewSet() } toCheck[ref] = true } } return toCheck } func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { // Must only be called if ms.mx is write-locked! for _, ref := range forward { bzi, ok := ms.idx[ref] if !ok || bzi.meta == nil { continue } bmr, ok := bzi.meta[key] if !ok { continue } bmr.backward = remRef(bmr.backward, zid) if len(bmr.backward) > 0 || len(bmr.forward) > 0 { bzi.meta[key] = bmr } else { delete(bzi.meta, key) if len(bzi.meta) == 0 { bzi.meta = nil } } } } func (ms *memStore) deleteWords(zid id.Zid, words []string) { // Must only be called if ms.mx is write-locked! for _, word := range words { refs, ok := ms.words[word] if !ok { continue } refs2 := remRef(refs, zid) if len(refs2) == 0 { delete(ms.words, word) continue } ms.words[word] = refs2 } } func (ms *memStore) ReadStats(st *index.StoreStats) { ms.mx.RLock() st.Zettel = len(ms.idx) st.Updates = ms.updates st.Words = uint64(len(ms.words)) ms.mx.RUnlock() } func (ms *memStore) Write(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() zids := make(id.Slice, 0, len(ms.idx)) for id := range ms.idx { zids = append(zids, id) } zids.Sort() for _, id := range zids { fmt.Fprintln(w, id) zi := ms.idx[id] fmt.Fprintln(w, "-", zi.dead) writeZidsLn(w, ">", zi.forward) writeZidsLn(w, "<", zi.backward) if zi.meta == nil { fmt.Fprintln(w, "*NIL") } else if len(zi.meta) == 0 { fmt.Fprintln(w, "*(0)") } else { for k, fb := range zi.meta { fmt.Fprintln(w, "*", k) writeZidsLn(w, "]", fb.forward) writeZidsLn(w, "[", fb.backward) } } } zids = make(id.Slice, 0, len(ms.dead)) for id := range ms.dead { zids = append(zids, id) } zids.Sort() for _, id := range zids { fmt.Fprintln(w, "~", id, ms.dead[id]) } } func writeZidsLn(w io.Writer, prefix string, zids id.Slice) { io.WriteString(w, prefix) for _, zid := range zids { io.WriteString(w, " ") w.Write(zid.Bytes()) } fmt.Fprintln(w) } |
Added index/memstore/refs.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "zettelstore.de/z/domain/id" ) func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { npos, opos := 0, 0 for npos < len(refsN) && opos < len(refsO) { rn, ro := refsN[npos], refsO[opos] if rn == ro { npos++ opos++ continue } if rn < ro { newRefs = append(newRefs, rn) npos++ continue } remRefs = append(remRefs, ro) opos++ } if npos < len(refsN) { newRefs = append(newRefs, refsN[npos:]...) } if opos < len(refsO) { remRefs = append(remRefs, refsO[opos:]...) } return newRefs, remRefs } func addRef(refs id.Slice, ref id.Zid) id.Slice { if len(refs) == 0 { return append(refs, ref) } for i, r := range refs { if r == ref { return refs } if r > ref { return append(refs[:i], append(id.Slice{ref}, refs[i:]...)...) } } return append(refs, ref) } func remRefs(refs id.Slice, 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 { for i, r := range refs { if r == ref { return append(refs[:i], refs[i+1:]...) } if r > ref { return refs } } return refs } |
Added index/memstore/refs_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "testing" "zettelstore.de/z/domain/id" ) func assertRefs(t *testing.T, i int, got, exp id.Slice) { t.Helper() if got == nil && exp != nil { t.Errorf("%d: got nil, but expected %v", i, exp) return } if got != nil && exp == nil { t.Errorf("%d: expected nil, but got %v", i, got) return } if len(got) != len(exp) { t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) return } for p, n := range exp { if got := got[p]; got != id.Zid(n) { t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) } } } func TestRefsDiff(t *testing.T) { 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) { 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) { 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) { 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) } } |
Added index/wordset.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package index allows to search for metadata and content. package index // WordSet contains the set of all words, with the count of their occurrences. type WordSet map[string]int // Words gives the slice of all words in the set. func (ws WordSet) Words() []string { if len(ws) == 0 { return nil } words := make([]string, 0, len(ws)) for w := range ws { words = append(words, w) } return words } // Diff calculates the word slice to be added and to be removed from oldWords // to get the given word set. func (ws WordSet) Diff(oldWords []string) (newWords, removeWords []string) { if len(ws) == 0 { return nil, oldWords } if len(oldWords) == 0 { return ws.Words(), nil } oldSet := make(WordSet, len(oldWords)) for _, ow := range oldWords { if _, ok := ws[ow]; ok { oldSet[ow] = 1 continue } removeWords = append(removeWords, ow) } for w := range ws { if _, ok := oldSet[w]; ok { continue } newWords = append(newWords, w) } return newWords, removeWords } |
Added index/wordset_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package index allows to search for metadata and content. package index_test import ( "sort" "testing" "zettelstore.de/z/index" ) func equalWordList(exp, got []string) bool { if len(exp) != len(got) { return false } if len(got) == 0 { return len(exp) == 0 } sort.Strings(got) for i, w := range exp { if w != got[i] { return false } } return true } func TestWordsWords(t *testing.T) { testcases := []struct { words index.WordSet exp []string }{ {nil, nil}, {index.WordSet{}, nil}, {index.WordSet{"a": 1, "b": 2}, []string{"a", "b"}}, } for i, tc := range testcases { got := tc.words.Words() if !equalWordList(tc.exp, got) { t.Errorf("%d: %v.Words() == %v, but got %v", i, tc.words, tc.exp, got) } } } func TestWordsDiff(t *testing.T) { testcases := []struct { cur index.WordSet old []string expN, expR []string }{ {nil, nil, nil, nil}, {index.WordSet{}, []string{}, nil, nil}, {index.WordSet{"a": 1}, []string{}, []string{"a"}, nil}, {index.WordSet{"a": 1}, []string{"b"}, []string{"a"}, []string{"b"}}, {index.WordSet{}, []string{"b"}, nil, []string{"b"}}, {index.WordSet{"a": 1}, []string{"a"}, nil, nil}, } for i, tc := range testcases { gotN, gotR := tc.cur.Diff(tc.old) if !equalWordList(tc.expN, gotN) { t.Errorf("%d: %v.Diff(%v)->new %v, but got %v", i, tc.cur, tc.old, tc.expN, gotN) } if !equalWordList(tc.expR, gotR) { t.Errorf("%d: %v.Diff(%v)->rem %v, but got %v", i, tc.cur, tc.old, tc.expR, gotR) } } } |
Added index/zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package index allows to search for metadata and content. package index import ( "zettelstore.de/z/domain/id" ) // ZettelIndex contains all index data of a zettel. type ZettelIndex struct { Zid id.Zid // zid of the indexed zettel backrefs id.Set // set of back references metarefs map[string]id.Set // references to inverse keys deadrefs id.Set // set of dead references words WordSet } // NewZettelIndex creates a new zettel index. func NewZettelIndex(zid id.Zid) *ZettelIndex { return &ZettelIndex{ Zid: zid, backrefs: id.NewSet(), metarefs: make(map[string]id.Set), deadrefs: id.NewSet(), } } // AddBackRef adds a reference to a zettel where the current zettel links to // without any more information. func (zi *ZettelIndex) AddBackRef(zid id.Zid) { zi.backrefs[zid] = true } // AddMetaRef adds a named reference to a zettel. On that zettel, the given // metadata key should point back to the current zettel. func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) { if zids, ok := zi.metarefs[key]; ok { zids[zid] = true return } zi.metarefs[key] = id.NewSet(zid) } // AddDeadRef adds a dead reference to a zettel. func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { zi.deadrefs[zid] = true } // SetWords sets the words to the given value. func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } // GetDeadRefs returns all dead references as a sorted list. func (zi *ZettelIndex) GetDeadRefs() id.Slice { return zi.deadrefs.Sorted() } // GetBackRefs returns all back references as a sorted list. func (zi *ZettelIndex) GetBackRefs() id.Slice { return zi.backrefs.Sorted() } // GetMetaRefs returns all meta references as a map of strings to a sorted list of references func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice { if len(zi.metarefs) == 0 { return nil } result := make(map[string]id.Slice, len(zi.metarefs)) for key, refs := range zi.metarefs { result[key] = refs.Sorted() } return result } // GetWords returns a reference to the WordSet. It must not be modified. func (zi *ZettelIndex) GetWords() WordSet { return zi.words } |
Deleted kernel/impl/auth.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/cfg.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/cmd.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/config.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/core.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/place.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/server.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/impl/web.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted kernel/kernel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to parser/markdown/markdown.go.
︙ | ︙ | |||
410 411 412 413 414 415 416 | if title := string(node.Title); len(title) > 0 { attrs = attrs.Set("title", cleanText(title, true)) } return ast.InlineSlice{ &ast.LinkNode{ Ref: ref, Inlines: p.acceptInlineSlice(node), | < | 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | if title := string(node.Title); len(title) > 0 { attrs = attrs.Set("title", cleanText(title, true)) } return ast.InlineSlice{ &ast.LinkNode{ Ref: ref, Inlines: p.acceptInlineSlice(node), Attrs: attrs, }, } } func (p *mdP) acceptImage(node *gmAst.Image) ast.InlineSlice { ref := ast.ParseReference(cleanText(string(node.Destination), true)) |
︙ | ︙ | |||
437 438 439 440 441 442 443 | func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice { ins := p.acceptInlineSlice(node) var sb strings.Builder _, err := p.textEnc.WriteInlines(&sb, ins) if err != nil { panic(err) | < < < | | < | 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 | func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice { ins := p.acceptInlineSlice(node) var sb strings.Builder _, err := p.textEnc.WriteInlines(&sb, ins) if err != nil { panic(err) //return ins } return ast.InlineSlice{ &ast.TextNode{ Text: sb.String(), }, } } func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) ast.InlineSlice { url := node.URL(p.source) if node.AutoLinkType == gmAst.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) { url = append([]byte("mailto:"), url...) } ref := ast.ParseReference(cleanText(string(url), false)) label := node.Label(p.source) if len(label) == 0 { label = url } return ast.InlineSlice{ &ast.LinkNode{ Ref: ref, Inlines: ast.InlineSlice{&ast.TextNode{Text: string(label)}}, Attrs: nil, //TODO }, } } func (p *mdP) acceptRawHTML(node *gmAst.RawHTML) ast.InlineSlice { segs := make([]string, 0, node.Segments.Len()) |
︙ | ︙ |
Changes to parser/parser.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package parser provides a generic interface to a range of different parsers. package parser import ( "log" "zettelstore.de/z/ast" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package parser provides a generic interface to a range of different parsers. package parser import ( "log" "zettelstore.de/z/ast" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser/cleaner" ) // Info describes a single parser. |
︙ | ︙ | |||
78 79 80 81 82 83 84 | // ParseMetadata parses a string as Zettelmarkup, resulting in an inline slice. // Typically used to parse the title or other metadata of type Zettelmarkup. func ParseMetadata(title string) ast.InlineSlice { return ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk) } // ParseZettel parses the zettel based on the syntax. | | | | 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | // ParseMetadata parses a string as Zettelmarkup, resulting in an inline slice. // Typically used to parse the title or other metadata of type Zettelmarkup. func ParseMetadata(title string) ast.InlineSlice { return ParseInlines(input.NewInput(title), meta.ValueSyntaxZmk) } // ParseZettel parses the zettel based on the syntax. func ParseZettel(zettel domain.Zettel, syntax string) *ast.ZettelNode { m := zettel.Meta inhMeta := runtime.AddDefaultValues(m) if syntax == "" { syntax, _ = inhMeta.Get(meta.KeySyntax) } parseMeta := inhMeta if syntax == meta.ValueSyntaxNone { parseMeta = m } |
︙ | ︙ |
Changes to parser/zettelmark/block.go.
︙ | ︙ | |||
545 546 547 548 549 550 551 | } } } // parseRow parse one table row. func (cp *zmkP) parseRow() (res ast.BlockNode, success bool) { inp := cp.inp | < < < < | 545 546 547 548 549 550 551 552 553 554 555 556 557 558 | } } } // parseRow parse one table row. func (cp *zmkP) parseRow() (res ast.BlockNode, success bool) { inp := cp.inp row := ast.TableRow{} for { inp.Next() cell := cp.parseCell() if cell != nil { row = append(row, cell) } |
︙ | ︙ |
Changes to parser/zettelmark/inline.go.
︙ | ︙ | |||
185 186 187 188 189 190 191 | if !ok { return "", nil, false } if inp.Ch == '|' { // First part must be inline text if pos == inp.Pos { // [[| or {{| return "", nil, false } | < < | < | 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | if !ok { return "", nil, false } if inp.Ch == '|' { // First part must be inline text if pos == inp.Pos { // [[| or {{| return "", nil, false } inp.SetPos(pos) ins = cp.parseReferenceInline() inp.Next() pos = inp.Pos } else if hasSpace { return "", nil, false } inp.SetPos(pos) |
︙ | ︙ | |||
220 221 222 223 224 225 226 | inp := cp.inp for { switch inp.Ch { case input.EOS: return false, false case '\n', '\r', ' ': hasSpace = true | | < > | < < | > | > > > > > > | | 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | inp := cp.inp for { switch inp.Ch { case input.EOS: return false, false case '\n', '\r', ' ': hasSpace = true case '|', closeCh: return hasSpace, true } inp.Next() } } func (cp *zmkP) parseReferenceInline() (ins ast.InlineSlice) { for { switch cp.inp.Ch { case input.EOS, '|': return ins } in := cp.parseInline() ins = append(ins, in) } } func (cp *zmkP) readReferenceToClose(closeCh rune) bool { inp := cp.inp for { switch inp.Ch { |
︙ | ︙ |
Changes to parser/zettelmark/zettelmark_test.go.
︙ | ︙ | |||
149 150 151 152 153 154 155 | {"[[b%%c|a]]", "(PARA [[b {% c|a]]})"}, {"[[b|a]", "(PARA [[b|a])"}, {"[[b\nc|a]]", "(PARA (LINK a b SB c))"}, {"[[b c|a#n]]", "(PARA (LINK a#n b SP c))"}, {"[[a]]go", "(PARA (LINK a a) go)"}, {"[[a]]{go}", "(PARA (LINK a a)[ATTR go])"}, {"[[[[a]]|b]]", "(PARA (LINK [[a [[a) |b]])"}, | < < < < | 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | {"[[b%%c|a]]", "(PARA [[b {% c|a]]})"}, {"[[b|a]", "(PARA [[b|a])"}, {"[[b\nc|a]]", "(PARA (LINK a b SB c))"}, {"[[b c|a#n]]", "(PARA (LINK a#n b SP c))"}, {"[[a]]go", "(PARA (LINK a a) go)"}, {"[[a]]{go}", "(PARA (LINK a a)[ATTR go])"}, {"[[[[a]]|b]]", "(PARA (LINK [[a [[a) |b]])"}, }) } func TestCite(t *testing.T) { checkTcs(t, TestCases{ {"[@", "(PARA [@)"}, {"[@]", "(PARA [@])"}, |
︙ | ︙ | |||
526 527 528 529 530 531 532 | checkTcs(t, TestCases{ {"|", "(TAB (TR))"}, {"|a", "(TAB (TR (TD a)))"}, {"|a|", "(TAB (TR (TD a)))"}, {"|a| ", "(TAB (TR (TD a)(TD)))"}, {"|a|b", "(TAB (TR (TD a)(TD b)))"}, {"|a|b\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, | < < | 522 523 524 525 526 527 528 529 530 531 532 533 534 535 | checkTcs(t, TestCases{ {"|", "(TAB (TR))"}, {"|a", "(TAB (TR (TD a)))"}, {"|a|", "(TAB (TR (TD a)))"}, {"|a| ", "(TAB (TR (TD a)(TD)))"}, {"|a|b", "(TAB (TR (TD a)(TD b)))"}, {"|a|b\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, }) } func TestBlockAttr(t *testing.T) { checkTcs(t, TestCases{ {":::go\n:::", "(SPAN)[ATTR =go]"}, {":::go=\n:::", "(SPAN)[ATTR =go]"}, |
︙ | ︙ |
Added place/change/change.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package change provides definition for place changes. package change import ( "zettelstore.de/z/domain/id" ) // Reason gives an indication, why the ObserverFunc was called. type Reason int // Values for Reason const ( _ Reason = iota OnReload // Place was reloaded OnUpdate // A zettel was created or changed OnDelete // A zettel was removed ) // Info contains all the data about a changed zettel. type Info struct { Reason Reason Zid id.Zid } // Func is a function to be called when a change is detected. type Func func(Info) // Subject is a place that notifies observers about changes. type Subject interface { // RegisterObserver registers an observer that will be notified // if one or all zettel are found to be changed. RegisterObserver(Func) } |
Changes to place/constplace/base.mustache.
1 2 3 4 5 6 7 | <!DOCTYPE html> <html{{#Lang}} lang="{{Lang}}"{{/Lang}}> <head> <meta charset="utf-8"> <meta name="referrer" content="no-referrer"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Zettelstore"> | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <!DOCTYPE html> <html{{#Lang}} lang="{{Lang}}"{{/Lang}}> <head> <meta charset="utf-8"> <meta name="referrer" content="no-referrer"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Zettelstore"> {{{MetaHeader}}} <link rel="stylesheet" href="{{{StylesheetURL}}}"> <title>{{Title}}</title> </head> <body> <nav class="zs-menu"> <a href="{{{HomeURL}}}">Home</a> |
︙ | ︙ |
Changes to place/constplace/constplace.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "context" _ "embed" // Allow to embed file content "net/url" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register( " const", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { | > | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | "context" _ "embed" // Allow to embed file content "net/url" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register( " const", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { return &constPlace{zettel: constZettelMap, filter: cdata.Filter}, nil }) } type constHeader map[string]string func makeMeta(zid id.Zid, h constHeader) *meta.Meta { m := meta.New(zid) for k, v := range h { m.Set(k, v) } return m } type constZettel struct { header constHeader content domain.Content } type constPlace struct { zettel map[id.Zid]constZettel filter index.MetaFilter } func (cp *constPlace) Location() string { return "const:" } func (cp *constPlace) CanCreateZettel(ctx context.Context) bool { return false } |
︙ | ︙ | |||
83 84 85 86 87 88 89 | } return result, nil } func (cp *constPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { for zid, zettel := range cp.zettel { m := makeMeta(zid, zettel.header) | | | 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | } return result, nil } func (cp *constPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { for zid, zettel := range cp.zettel { m := makeMeta(zid, zettel.header) cp.filter.Enrich(ctx, m) if match(m) { res = append(res, m) } } return res, nil } |
︙ | ︙ | |||
119 120 121 122 123 124 125 | func (cp *constPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := cp.zettel[zid]; ok { return place.ErrReadOnly } return place.ErrNotFound } | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | func (cp *constPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := cp.zettel[zid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (cp *constPlace) ReadStats(st *place.Stats) { st.ReadOnly = true st.Zettel = len(cp.zettel) } const syntaxTemplate = "mustache" var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ meta.KeyTitle: "Zettelstore Runtime Configuration", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityOwner, meta.KeySyntax: meta.ValueSyntaxNone, meta.KeyNoIndex: meta.ValueTrue, }, ""}, id.BaseTemplateZid: { constHeader{ meta.KeyTitle: "Zettelstore Base HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeySyntax: syntaxTemplate, meta.KeyNoIndex: meta.ValueTrue, |
︙ | ︙ | |||
283 284 285 286 287 288 289 | meta.KeyTitle: "Zettelstore Base CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeySyntax: "css", meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentBaseCSS)}, | < < < < < < < < < < | 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | meta.KeyTitle: "Zettelstore Base CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeySyntax: "css", meta.KeyNoIndex: meta.ValueTrue, }, domain.NewContent(contentBaseCSS)}, id.TOCNewTemplateZid: { constHeader{ meta.KeyTitle: "New Menu", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, }, domain.NewContent(contentNewTOCZettel)}, id.TemplateNewZettelZid: { constHeader{ meta.KeyTitle: "New Zettel", meta.KeyRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, |
︙ | ︙ | |||
322 323 324 325 326 327 328 | }, ""}, id.DefaultHomeZid: { constHeader{ meta.KeyTitle: "Home", meta.KeyRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, | < < < < < < < < < < | 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | }, ""}, id.DefaultHomeZid: { constHeader{ meta.KeyTitle: "Home", meta.KeyRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, }, domain.NewContent(contentHomeZettel)}, } //go:embed base.mustache var contentBaseMustache string //go:embed login.mustache var contentLoginMustache string //go:embed zettel.mustache |
︙ | ︙ | |||
375 376 377 378 379 380 381 | //go:embed error.mustache var contentErrorMustache string //go:embed base.css var contentBaseCSS string | < < < | 326 327 328 329 330 331 332 333 334 335 336 337 | //go:embed error.mustache var contentErrorMustache string //go:embed base.css var contentBaseCSS string //go:embed newtoc.zettel var contentNewTOCZettel string //go:embed home.zettel var contentHomeZettel string |
Deleted place/constplace/contributors.zettel.
|
| < < < < < < < < |
Deleted place/constplace/dependencies.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/constplace/emoji_spin.gif.
cannot compute difference between binary files
Changes to place/constplace/home.zettel.
1 2 3 4 5 6 7 8 9 10 11 12 13 | === Thank you for using Zettelstore! You will find the lastest information about Zettelstore at [[https://zettelstore.de]]. Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading. Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading. Since Zettelstore is currently in a development state, every upgrade might fix some of your problems. To check for versions, there is a zettel with the [[current version|00000000000001]] of your Zettelstore. If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. === Reporting errors | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | === Thank you for using Zettelstore! You will find the lastest information about Zettelstore at [[https://zettelstore.de]]. Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading. Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading. Since Zettelstore is currently in a development state, every upgrade might fix some of your problems. To check for versions, there is a zettel with the [[current version|00000000000001]] of your Zettelstore. If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. === Reporting errors If you have encountered an error, please include the content of the following zettel in your mail: * [[Zettelstore Version|00000000000001]] * [[Zettelstore Operating System|00000000000003]] * [[Zettelstore Startup Configuration|00000000000096]] * [[Zettelstore Startup Values|00000000000098]] * [[Zettelstore Runtime Configuration|00000000000100]] Additionally, you have to describe, what you have done before that error occurs and what you have expected instead. Please do not forget to include the error message, if there is one. Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"". |
︙ | ︙ |
Changes to place/constplace/info.mustache.
︙ | ︙ | |||
13 14 15 16 17 18 19 | <table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table> {{#HasLinks}} <h2>References</h2> {{#HasLocLinks}} <h3>Local</h3> <ul> {{#LocLinks}} | | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table> {{#HasLinks}} <h2>References</h2> {{#HasLocLinks}} <h3>Local</h3> <ul> {{#LocLinks}} <li><a href="{{{.}}}">{{.}}</a></li> {{/LocLinks}} </ul> {{/HasLocLinks}} {{#HasExtLinks}} <h3>External</h3> <ul> {{#ExtLinks}} |
︙ | ︙ |
Deleted place/constplace/license.txt.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to place/dirplace/dirplace.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Package dirplace provides a directory-based zettel place. package dirplace import ( "context" | < > > | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | //----------------------------------------------------------------------------- // Package dirplace provides a directory-based zettel place. package dirplace import ( "context" "net/url" "os" "path/filepath" "strconv" "strings" "sync" "time" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/change" "zettelstore.de/z/place/dirplace/directory" "zettelstore.de/z/place/fileplace" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { path := getDirPath(u) if _, err := os.Stat(path); os.IsNotExist(err) { return nil, err } dirSrvSpec, defWorker, maxWorker := getDirSrvInfo(u.Query().Get("type")) dp := dirPlace{ location: u.String(), readonly: getQueryBool(u, "readonly"), cdata: *cdata, |
︙ | ︙ | |||
132 133 134 135 136 137 138 | err := dirSrv.Stop() for _, c := range dp.fCmds { close(c) } return err } | | | | 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | err := dirSrv.Stop() for _, c := range dp.fCmds { close(c) } return err } func (dp *dirPlace) notifyChanged(reason change.Reason, zid id.Zid) { if dp.mustNotify { if chci := dp.cdata.Notify; chci != nil { chci <- change.Info{Reason: reason, Zid: zid} } } } func (dp *dirPlace) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function var sum uint32 = 2166136261 ^ uint32(zid) |
︙ | ︙ | |||
173 174 175 176 177 178 179 | meta.Zid = entry.Zid dp.updateEntryFromMeta(entry, meta) err = setZettel(dp, entry, zettel) if err == nil { dp.dirSrv.UpdateEntry(entry) } | | | 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | meta.Zid = entry.Zid dp.updateEntryFromMeta(entry, meta) err = setZettel(dp, entry, zettel) if err == nil { dp.dirSrv.UpdateEntry(entry) } dp.notifyChanged(change.OnUpdate, meta.Zid) return meta.Zid, err } func (dp *dirPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { entry, err := dp.dirSrv.GetEntry(zid) if err != nil || !entry.IsValid() { return domain.Zettel{}, place.ErrNotFound |
︙ | ︙ | |||
230 231 232 233 234 235 236 | for _, entry := range entries { m, err1 := getMeta(dp, entry, entry.Zid) err = err1 if err != nil { continue } dp.cleanupMeta(ctx, m) | | | 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | for _, entry := range entries { m, err1 := getMeta(dp, entry, entry.Zid) err = err1 if err != nil { continue } dp.cleanupMeta(ctx, m) dp.cdata.Filter.Enrich(ctx, m) if match(m) { res = append(res, m) } } if err != nil { return nil, err |
︙ | ︙ | |||
272 273 274 275 276 277 278 | if !meta.Equal(defaultMeta, true) { dp.updateEntryFromMeta(entry, meta) dp.dirSrv.UpdateEntry(entry) } } err = setZettel(dp, entry, zettel) if err == nil { | | | | | | 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | if !meta.Equal(defaultMeta, true) { dp.updateEntryFromMeta(entry, meta) dp.dirSrv.UpdateEntry(entry) } } err = setZettel(dp, entry, zettel) if err == nil { dp.notifyChanged(change.OnUpdate, meta.Zid) } return err } func (dp *dirPlace) updateEntryFromMeta(entry *directory.Entry, meta *meta.Meta) { entry.MetaSpec, entry.ContentExt = calcSpecExt(meta) basePath := filepath.Join(dp.dir, entry.Zid.String()) if entry.MetaSpec == directory.MetaSpecFile { entry.MetaPath = basePath + ".meta" } entry.ContentPath = basePath + "." + entry.ContentExt entry.Duplicates = false } func calcSpecExt(m *meta.Meta) (directory.MetaSpec, string) { if m.YamlSep { return directory.MetaSpecHeader, "zettel" } syntax := m.GetDefault(meta.KeySyntax, "bin") switch syntax { case meta.ValueSyntaxNone, meta.ValueSyntaxZmk: return directory.MetaSpecHeader, "zettel" } for _, s := range runtime.GetZettelFileSyntax() { if s == syntax { return directory.MetaSpecHeader, "zettel" } } return directory.MetaSpecFile, syntax } |
︙ | ︙ | |||
350 351 352 353 354 355 356 | if err = setZettel(dp, &newEntry, newZettel); err != nil { // "Rollback" rename. No error checking... dp.dirSrv.RenameEntry(&newEntry, curEntry) return err } err = deleteZettel(dp, curEntry, curZid) if err == nil { | | | | 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | if err = setZettel(dp, &newEntry, newZettel); err != nil { // "Rollback" rename. No error checking... dp.dirSrv.RenameEntry(&newEntry, curEntry) return err } err = deleteZettel(dp, curEntry, curZid) if err == nil { dp.notifyChanged(change.OnDelete, curZid) dp.notifyChanged(change.OnUpdate, newZid) } return err } func (dp *dirPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { if dp.readonly { return false |
︙ | ︙ | |||
376 377 378 379 380 381 382 | entry, err := dp.dirSrv.GetEntry(zid) if err != nil || !entry.IsValid() { return nil } dp.dirSrv.DeleteEntry(zid) err = deleteZettel(dp, entry, zid) if err == nil { | | | | | | 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | entry, err := dp.dirSrv.GetEntry(zid) if err != nil || !entry.IsValid() { return nil } dp.dirSrv.DeleteEntry(zid) err = deleteZettel(dp, entry, zid) if err == nil { dp.notifyChanged(change.OnDelete, zid) } return err } func (dp *dirPlace) ReadStats(st *place.Stats) { st.ReadOnly = dp.readonly st.Zettel, _ = dp.dirSrv.NumEntries() } func (dp *dirPlace) cleanupMeta(ctx context.Context, m *meta.Meta) { if role, ok := m.Get(meta.KeyRole); !ok || role == "" { m.Set(meta.KeyRole, runtime.GetDefaultRole()) } if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { m.Set(meta.KeySyntax, runtime.GetDefaultSyntax()) } } func renamePath(path string, curID, newID id.Zid) string { dir, file := filepath.Split(path) if cur := curID.String(); strings.HasPrefix(file, cur) { file = newID.String() + file[len(cur):] return filepath.Join(dir, file) } return path } |
Changes to place/dirplace/makedir.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package dirplace provides a directory-based zettel place. package dirplace import ( | | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // under this license. //----------------------------------------------------------------------------- // Package dirplace provides a directory-based zettel place. package dirplace import ( "zettelstore.de/z/config/startup" "zettelstore.de/z/place/dirplace/notifydir" "zettelstore.de/z/place/dirplace/simpledir" ) func getDirSrvInfo(dirType string) (directoryServiceSpec, int, int) { for count := 0; count < 2; count++ { switch dirType { case startup.ValueDirPlaceTypeNotify: return dirSrvNotify, 7, 1499 case startup.ValueDirPlaceTypeSimple: return dirSrvSimple, 1, 1 default: dirType = startup.DefaultDirPlaceType() } } panic("unable to set default dir place type: " + dirType) } func (dp *dirPlace) setupDirService() { switch dp.dirSrvSpec { |
︙ | ︙ |
Changes to place/dirplace/notifydir/notifydir.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "time" "zettelstore.de/z/domain/id" | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // Package notifydir manages the notified directory part of a dirstore. package notifydir import ( "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/place/change" "zettelstore.de/z/place/dirplace/directory" ) // notifyService specifies a directory scan service. type notifyService struct { dirPath string rescanTime time.Duration done chan struct{} cmds chan dirCmd infos chan<- change.Info } // NewService creates a new directory service. func NewService(directoryPath string, rescanTime time.Duration, chci chan<- change.Info) directory.Service { srv := ¬ifyService{ dirPath: directoryPath, rescanTime: rescanTime, cmds: make(chan dirCmd), infos: chci, } return srv |
︙ | ︙ | |||
62 63 64 65 66 67 68 | // Stop stops the directory service. func (srv *notifyService) Stop() error { close(srv.done) srv.done = nil return nil } | | | | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | // Stop stops the directory service. func (srv *notifyService) Stop() error { close(srv.done) srv.done = nil return nil } func (srv *notifyService) notifyChange(reason change.Reason, zid id.Zid) { if chci := srv.infos; chci != nil { chci <- change.Info{Reason: reason, Zid: zid} } } // NumEntries returns the number of managed zettel. func (srv *notifyService) NumEntries() (int, error) { resChan := make(chan resNumEntries) srv.cmds <- &cmdNumEntries{resChan} |
︙ | ︙ |
Changes to place/dirplace/notifydir/service.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "log" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/place" "zettelstore.de/z/place/dirplace/directory" ) // ping sends every tick a signal to reload the directory list func ping(tick chan<- struct{}, rescanTime time.Duration, done <-chan struct{}) { ticker := time.NewTicker(rescanTime) defer close(tick) | > | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "log" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/place" "zettelstore.de/z/place/change" "zettelstore.de/z/place/dirplace/directory" ) // ping sends every tick a signal to reload the directory list func ping(tick chan<- struct{}, rescanTime time.Duration, done <-chan struct{}) { ticker := time.NewTicker(rescanTime) defer close(tick) |
︙ | ︙ | |||
108 109 110 111 112 113 114 | curMap = newMap newMap = nil if ready != nil { ready <- len(curMap) close(ready) ready = nil } | | | | | 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | curMap = newMap newMap = nil if ready != nil { ready <- len(curMap) close(ready) ready = nil } srv.notifyChange(change.OnReload, id.Invalid) case fileStatusError: log.Println("DIRPLACE", "ERROR", ev.err) case fileStatusUpdate: srv.processFileUpdateEvent(ev, curMap, newMap) case fileStatusDelete: srv.processFileDeleteEvent(ev, curMap, newMap) } case cmd, ok := <-srv.cmds: if ok { cmd.run(curMap) } } } } func (srv *notifyService) processFileUpdateEvent(ev *fileEvent, curMap, newMap dirMap) { if newMap != nil { dirMapUpdate(newMap, ev) } else { dirMapUpdate(curMap, ev) srv.notifyChange(change.OnUpdate, ev.zid) } } func (srv *notifyService) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) { if newMap != nil { deleteFromMap(newMap, ev) } else { deleteFromMap(curMap, ev) srv.notifyChange(change.OnDelete, ev.zid) } } type dirCmd interface { run(m dirMap) } |
︙ | ︙ |
Changes to place/fileplace/fileplace.go.
︙ | ︙ | |||
26 27 28 29 30 31 32 | func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { path := getFilepathFromURL(u) ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { return nil, errors.New("unknown extension '" + ext + "' in place URL: " + u.String()) } | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { path := getFilepathFromURL(u) ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { return nil, errors.New("unknown extension '" + ext + "' in place URL: " + u.String()) } return &zipPlace{name: path, filter: cdata.Filter}, nil }) } func getFilepathFromURL(u *url.URL) string { name := u.Opaque if name == "" { name = u.Path |
︙ | ︙ |
Changes to place/fileplace/zipplace.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | "io" "regexp" "strings" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/place" "zettelstore.de/z/search" ) var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } type zipEntry struct { metaName string contentName string contentExt string // (normalized) file extension of zettel content metaInHeader bool } type zipPlace struct { | > | | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | "io" "regexp" "strings" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/input" "zettelstore.de/z/place" "zettelstore.de/z/search" ) var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } type zipEntry struct { metaName string contentName string contentExt string // (normalized) file extension of zettel content metaInHeader bool } type zipPlace struct { name string filter index.MetaFilter zettel map[id.Zid]*zipEntry // no lock needed, because read-only after creation } func (zp *zipPlace) Location() string { if strings.HasPrefix(zp.name, "/") { return "file://" + zp.name } return "file:" + zp.name |
︙ | ︙ | |||
175 176 177 178 179 180 181 | } defer reader.Close() for zid, entry := range zp.zettel { m, err := readZipMeta(reader, zid, entry) if err != nil { continue } | | | 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | } defer reader.Close() for zid, entry := range zp.zettel { m, err := readZipMeta(reader, zid, entry) if err != nil { continue } zp.filter.Enrich(ctx, m) if match(m) { res = append(res, m) } } return res, nil } |
︙ | ︙ | |||
212 213 214 215 216 217 218 | func (zp *zipPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := zp.zettel[zid]; ok { return place.ErrReadOnly } return place.ErrNotFound } | | | 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | func (zp *zipPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := zp.zettel[zid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (zp *zipPlace) ReadStats(st *place.Stats) { st.ReadOnly = true st.Zettel = len(zp.zettel) } func readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *zipEntry) (m *meta.Meta, err error) { var inMeta bool if entry.metaInHeader { |
︙ | ︙ |
Changes to place/helper.go.
︙ | ︙ | |||
29 30 31 32 33 34 35 | if found { return zid, nil } // TODO: do not wait here unconditionally. time.Sleep(100 * time.Millisecond) withSeconds = true } | | | 29 30 31 32 33 34 35 36 37 | if found { return zid, nil } // TODO: do not wait here unconditionally. time.Sleep(100 * time.Millisecond) withSeconds = true } return id.Invalid, ErrTimeout } |
Deleted place/manager/anteroom.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/anteroom_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/collect.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/enrich.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/indexer.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to place/manager/manager.go.
1 2 3 4 5 6 7 8 9 10 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- | | < > > < | | | | | < | | | > | > > | > > > < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various places of a Zettelstore. package manager import ( "context" "log" "net/url" "runtime/debug" "sort" "strings" "sync" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" "zettelstore.de/z/place/change" "zettelstore.de/z/search" ) // ConnectData contains all administration related values. type ConnectData struct { Filter index.MetaFilter Notify chan<- change.Info } // Connect returns a handle to the specified place func Connect(rawURL string, readonlyMode bool, cdata *ConnectData) (place.ManagedPlace, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err } if u.Scheme == "" { u.Scheme = "dir" } if readonlyMode { // TODO: the following is wrong under some circumstances: // 1. fragment is set if q := u.Query(); len(q) == 0 { rawURL += "?readonly" } else if _, ok := q["readonly"]; !ok { rawURL += "&readonly" } if u, err = url.Parse(rawURL); err != nil { return nil, err } } if create, ok := registry[u.Scheme]; ok { return create(u, cdata) |
︙ | ︙ | |||
85 86 87 88 89 90 91 | } sort.Strings(result) return result } // Manager is a coordinating place. type Manager struct { | | | < | > | | | | < | < < < < < < < < < < < | < < < < < < < < < | < < | | | | 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | } sort.Strings(result) return result } // Manager is a coordinating place. type Manager struct { mx sync.RWMutex started bool subplaces []place.ManagedPlace filter index.MetaFilter observers []change.Func mxObserver sync.RWMutex done chan struct{} infos chan change.Info } // New creates a new managing place. func New(placeURIs []string, readonlyMode bool, filter index.MetaFilter) (*Manager, error) { mgr := &Manager{ filter: filter, infos: make(chan change.Info, len(placeURIs)*10), } cdata := ConnectData{Filter: filter, Notify: mgr.infos} subplaces := make([]place.ManagedPlace, 0, len(placeURIs)+2) for _, uri := range placeURIs { p, err := Connect(uri, readonlyMode, &cdata) if err != nil { return nil, err } if p != nil { subplaces = append(subplaces, p) } } |
︙ | ︙ | |||
150 151 152 153 154 155 156 | subplaces = append(subplaces, constplace, progplace) mgr.subplaces = subplaces return mgr, nil } // RegisterObserver registers an observer that will be notified // if a zettel was found to be changed. | | | | | | > | | < < < < | | > | | | | | | < | | | < < < < | < | > | > | > > > | | | < < < | < | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > < < | < | | < < < < < < < < < < < < | < | < < | 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 | subplaces = append(subplaces, constplace, progplace) mgr.subplaces = subplaces return mgr, nil } // RegisterObserver registers an observer that will be notified // if a zettel was found to be changed. func (mgr *Manager) RegisterObserver(f change.Func) { if f != nil { mgr.mxObserver.Lock() mgr.observers = append(mgr.observers, f) mgr.mxObserver.Unlock() } } func (mgr *Manager) notifyObserver(ci change.Info) { mgr.mxObserver.RLock() observers := mgr.observers mgr.mxObserver.RUnlock() for _, ob := range observers { ob(ci) } } func notifier(notify change.Func, infos <-chan change.Info, done <-chan struct{}) { // The call to notify may panic. Ensure a running notifier. defer func() { if r := recover(); r != nil { log.Println("recovered from:", r) debug.PrintStack() go notifier(notify, infos, done) } }() for { select { case ci, ok := <-infos: if ok { notify(ci) } case _, ok := <-done: if !ok { return } } } } // Location returns some information where the place is located. func (mgr *Manager) Location() string { if len(mgr.subplaces) < 2 { return mgr.subplaces[0].Location() } var sb strings.Builder for i := 0; i < len(mgr.subplaces)-2; i++ { if i > 0 { sb.WriteString(", ") } sb.WriteString(mgr.subplaces[i].Location()) } return sb.String() } // Start the place. Now all other functions of the place are allowed. // Starting an already started place is not allowed. func (mgr *Manager) Start(ctx context.Context) error { mgr.mx.Lock() if mgr.started { mgr.mx.Unlock() return place.ErrStarted } for i := len(mgr.subplaces) - 1; i >= 0; i-- { ssi, ok := mgr.subplaces[i].(place.StartStopper) if !ok { continue } err := ssi.Start(ctx) if err == nil { continue } for j := i + 1; j < len(mgr.subplaces); j++ { if ssj, ok := mgr.subplaces[j].(place.StartStopper); ok { ssj.Stop(ctx) } } mgr.mx.Unlock() return err } mgr.done = make(chan struct{}) go notifier(mgr.notifyObserver, mgr.infos, mgr.done) mgr.started = true mgr.mx.Unlock() mgr.infos <- change.Info{Reason: change.OnReload, Zid: id.Invalid} return nil } // Stop the started place. Now only the Start() function is allowed. func (mgr *Manager) Stop(ctx context.Context) error { mgr.mx.Lock() defer mgr.mx.Unlock() if !mgr.started { return place.ErrStopped } close(mgr.done) mgr.done = nil var err error for _, p := range mgr.subplaces { if ss, ok := p.(place.StartStopper); ok { if err1 := ss.Stop(ctx); err1 != nil && err == nil { err = err1 } } } mgr.started = false return err } // CanCreateZettel returns true, if place could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { mgr.mx.RLock() defer mgr.mx.RUnlock() return mgr.started && mgr.subplaces[0].CanCreateZettel(ctx) } // CreateZettel creates a new zettel. func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return id.Invalid, place.ErrStopped } return mgr.subplaces[0].CreateZettel(ctx, zettel) } // GetZettel retrieves a specific zettel. func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return domain.Zettel{}, place.ErrStopped } for _, p := range mgr.subplaces { if z, err := p.GetZettel(ctx, zid); err != place.ErrNotFound { if err == nil { mgr.filter.Enrich(ctx, z.Meta) } return z, err } } return domain.Zettel{}, place.ErrNotFound } // GetMeta retrieves just the meta data of a specific zettel. func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return nil, place.ErrStopped } for _, p := range mgr.subplaces { if m, err := p.GetMeta(ctx, zid); err != place.ErrNotFound { if err == nil { mgr.filter.Enrich(ctx, m) } return m, err } } return nil, place.ErrNotFound } // FetchZids returns the set of all zettel identifer managed by the place. func (mgr *Manager) FetchZids(ctx context.Context) (result id.Set, err error) { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return nil, place.ErrStopped } for _, p := range mgr.subplaces { zids, err := p.FetchZids(ctx) if err != nil { return nil, err } if result == nil { result = zids } else if len(result) <= len(zids) { for zid := range result { zids[zid] = true } result = zids } else { for zid := range zids { result[zid] = true } } } return result, nil } // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. func (mgr *Manager) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return nil, place.ErrStopped } var result []*meta.Meta match := s.CompileMatch(startup.Indexer()) for _, p := range mgr.subplaces { selected, err := p.SelectMeta(ctx, match) if err != nil { return nil, err } sort.Slice(selected, func(i, j int) bool { return selected[i].Zid > selected[j].Zid }) if len(result) == 0 { result = selected } else { result = place.MergeSorted(result, selected) } } if s == nil { return result, nil } return s.Sort(result), nil } // CanUpdateZettel returns true, if place could possibly update the given zettel. func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { mgr.mx.RLock() defer mgr.mx.RUnlock() return mgr.started && mgr.subplaces[0].CanUpdateZettel(ctx, zettel) } // UpdateZettel updates an existing zettel. func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return place.ErrStopped } zettel.Meta = zettel.Meta.Clone() mgr.filter.Remove(ctx, zettel.Meta) return mgr.subplaces[0].UpdateZettel(ctx, zettel) } // AllowRenameZettel returns true, if place will not disallow renaming the zettel. func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return false } for _, p := range mgr.subplaces { if !p.AllowRenameZettel(ctx, zid) { return false } } return true } // RenameZettel changes the current zid to a new zid. func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return place.ErrStopped } for i, p := range mgr.subplaces { if err := p.RenameZettel(ctx, curZid, newZid); err != nil && err != place.ErrNotFound { for j := 0; j < i; j++ { mgr.subplaces[j].RenameZettel(ctx, newZid, curZid) } return err } } return nil } // CanDeleteZettel returns true, if place could possibly delete the given zettel. func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return false } for _, p := range mgr.subplaces { if p.CanDeleteZettel(ctx, zid) { return true } } return false } // DeleteZettel removes the zettel from the place. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { mgr.mx.RLock() defer mgr.mx.RUnlock() if !mgr.started { return place.ErrStopped } for _, p := range mgr.subplaces { if err := p.DeleteZettel(ctx, zid); err != place.ErrNotFound && err != place.ErrReadOnly { return err } } return place.ErrNotFound } // ReadStats populates st with place statistics func (mgr *Manager) ReadStats(st *place.Stats) { subStats := make([]place.Stats, len(mgr.subplaces)) for i, p := range mgr.subplaces { p.ReadStats(&subStats[i]) } st.ReadOnly = true sumZettel := 0 for _, sst := range subStats { if !sst.ReadOnly { st.ReadOnly = false } sumZettel += sst.Zettel } st.Zettel = sumZettel } // NumPlaces returns the number of managed places. func (mgr *Manager) NumPlaces() int { return len(mgr.subplaces) } |
Deleted place/manager/memstore/memstore.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/memstore/refs.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/memstore/refs_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/place.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/store/store.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/store/wordset.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/store/wordset_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/manager/store/zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to place/memplace/memplace.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | "net/url" "sync" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { return &memPlace{u: u, cdata: *cdata}, nil }) } type memPlace struct { u *url.URL cdata manager.ConnectData zettel map[id.Zid]domain.Zettel mx sync.RWMutex } | > | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | "net/url" "sync" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/change" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { return &memPlace{u: u, cdata: *cdata}, nil }) } type memPlace struct { u *url.URL cdata manager.ConnectData zettel map[id.Zid]domain.Zettel mx sync.RWMutex } func (mp *memPlace) notifyChanged(reason change.Reason, zid id.Zid) { if chci := mp.cdata.Notify; chci != nil { chci <- change.Info{Reason: reason, Zid: zid} } } func (mp *memPlace) Location() string { return mp.u.String() } |
︙ | ︙ | |||
76 77 78 79 80 81 82 | return id.Invalid, err } meta := zettel.Meta.Clone() meta.Zid = zid zettel.Meta = meta mp.zettel[zid] = zettel mp.mx.Unlock() | | | 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | return id.Invalid, err } meta := zettel.Meta.Clone() meta.Zid = zid zettel.Meta = meta mp.zettel[zid] = zettel mp.mx.Unlock() mp.notifyChanged(change.OnUpdate, zid) return zid, nil } func (mp *memPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { mp.mx.RLock() zettel, ok := mp.zettel[zid] mp.mx.RUnlock() |
︙ | ︙ | |||
116 117 118 119 120 121 122 | } func (mp *memPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) { result := make([]*meta.Meta, 0, len(mp.zettel)) mp.mx.RLock() for _, zettel := range mp.zettel { m := zettel.Meta.Clone() | | | 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | } func (mp *memPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) { result := make([]*meta.Meta, 0, len(mp.zettel)) mp.mx.RLock() for _, zettel := range mp.zettel { m := zettel.Meta.Clone() mp.cdata.Filter.Enrich(ctx, m) if match(m) { result = append(result, m) } } mp.mx.RUnlock() return result, nil } |
︙ | ︙ | |||
138 139 140 141 142 143 144 | meta := zettel.Meta.Clone() if !meta.Zid.IsValid() { return &place.ErrInvalidID{Zid: meta.Zid} } zettel.Meta = meta mp.zettel[meta.Zid] = zettel mp.mx.Unlock() | | | 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | meta := zettel.Meta.Clone() if !meta.Zid.IsValid() { return &place.ErrInvalidID{Zid: meta.Zid} } zettel.Meta = meta mp.zettel[meta.Zid] = zettel mp.mx.Unlock() mp.notifyChanged(change.OnUpdate, meta.Zid) return nil } func (mp *memPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return true } func (mp *memPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { mp.mx.Lock() |
︙ | ︙ | |||
164 165 166 167 168 169 170 | meta := zettel.Meta.Clone() meta.Zid = newZid zettel.Meta = meta mp.zettel[newZid] = zettel delete(mp.zettel, curZid) mp.mx.Unlock() | | | | | | 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | meta := zettel.Meta.Clone() meta.Zid = newZid zettel.Meta = meta mp.zettel[newZid] = zettel delete(mp.zettel, curZid) mp.mx.Unlock() mp.notifyChanged(change.OnDelete, curZid) mp.notifyChanged(change.OnUpdate, newZid) return nil } func (mp *memPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { mp.mx.RLock() _, ok := mp.zettel[zid] mp.mx.RUnlock() return ok } func (mp *memPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { mp.mx.Lock() if _, ok := mp.zettel[zid]; !ok { mp.mx.Unlock() return place.ErrNotFound } delete(mp.zettel, zid) mp.mx.Unlock() mp.notifyChanged(change.OnDelete, zid) return nil } func (mp *memPlace) ReadStats(st *place.Stats) { st.ReadOnly = false mp.mx.RLock() st.Zettel = len(mp.zettel) mp.mx.RUnlock() } |
Changes to place/place.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package place provides a generic interface to zettel places. package place import ( "context" "errors" "fmt" | < < > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Package place provides a generic interface to zettel places. package place import ( "context" "errors" "fmt" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place/change" "zettelstore.de/z/search" ) // BasePlace is implemented by all Zettel places. type BasePlace interface { // Location returns some information where the place is located. // Format is dependent of the place. |
︙ | ︙ | |||
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | RenameZettel(ctx context.Context, curZid, newZid id.Zid) error // CanDeleteZettel returns true, if place could possibly delete the given zettel. CanDeleteZettel(ctx context.Context, zid id.Zid) bool // DeleteZettel removes the zettel from the place. DeleteZettel(ctx context.Context, zid id.Zid) error } // ManagedPlace is the interface of managed places. type ManagedPlace interface { BasePlace // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) | > > > | < < | < | | | 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | RenameZettel(ctx context.Context, curZid, newZid id.Zid) error // CanDeleteZettel returns true, if place could possibly delete the given zettel. CanDeleteZettel(ctx context.Context, zid id.Zid) bool // DeleteZettel removes the zettel from the place. DeleteZettel(ctx context.Context, zid id.Zid) error // ReadStats populates st with place statistics ReadStats(st *Stats) } // ManagedPlace is the interface of managed places. type ManagedPlace interface { BasePlace // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, match search.MetaMatchFunc) ([]*meta.Meta, error) } // Stats records statistics about the place. type Stats struct { // ReadOnly indicates that the places cannot be changed ReadOnly bool // Zettel is the number of zettel managed by the place. Zettel int } |
︙ | ︙ | |||
97 98 99 100 101 102 103 | // Place is a place to be used outside the place package and its descendants. type Place interface { BasePlace // SelectMeta returns a list of metadata that comply to the given selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | // Place is a place to be used outside the place package and its descendants. type Place interface { BasePlace // SelectMeta returns a list of metadata that comply to the given selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // Manager is a place-managing place. type Manager interface { Place StartStopper change.Subject // NumPlaces returns the number of managed places. NumPlaces() int } // ErrNotAllowed is returned if the caller is not allowed to perform the operation. type ErrNotAllowed struct { Op string User *meta.Meta Zid id.Zid |
︙ | ︙ | |||
240 241 242 243 244 245 246 | return fmt.Sprintf( "operation %q not allowed for user %v/%v", err.Op, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid.String()) } | | > | > > | | | 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | return fmt.Sprintf( "operation %q not allowed for user %v/%v", err.Op, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid.String()) } // IsErrNotAllowed return true, if the error is of type ErrNotAllowed. func IsErrNotAllowed(err error) bool { _, ok := err.(*ErrNotAllowed) return ok } // ErrStarted is returned when trying to start an already started place. var ErrStarted = errors.New("place is already started") // ErrStopped is returned if calling methods on a place that was not started. var ErrStopped = errors.New("place is stopped") // ErrReadOnly is returned if there is an attepmt to write to a read-only place. var ErrReadOnly = errors.New("read-only place") // ErrNotFound is returned if a zettel was not found in the place. var ErrNotFound = errors.New("zettel not found") // ErrTimeout is returned if a place operation takes too long. // One example: if calculating a new zettel identifier takes too long. var ErrTimeout = errors.New("timeout") // ErrInvalidID is returned if the zettel id is not appropriate for the place operation. type ErrInvalidID struct{ Zid id.Zid } func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() } |
Changes to place/progplace/config.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // under this license. //----------------------------------------------------------------------------- // Package progplace provides zettel that inform the user about the internal Zettelstore state. package progplace import ( "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { | > > | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | // under this license. //----------------------------------------------------------------------------- // Package progplace provides zettel that inform the user about the internal Zettelstore state. package progplace import ( "fmt" "strings" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myPlace.startConfig == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Startup Configuration") m.Set(meta.KeyVisibility, meta.ValueVisibilitySimple) return m } func genConfigZettelC(m *meta.Meta) string { var sb strings.Builder for i, p := range myPlace.startConfig.Pairs(false) { if i > 0 { sb.WriteByte('\n') } sb.WriteString("; ''") sb.WriteString(p.Key) sb.WriteString("''") if p.Value != "" { sb.WriteString("\n: ``") for _, r := range p.Value { if r == '`' { sb.WriteByte('\\') } sb.WriteRune(r) } sb.WriteString("``") } } return sb.String() } func genConfigM(zid id.Zid) *meta.Meta { if myPlace.startConfig == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Startup Values") m.Set(meta.KeyRole, meta.ValueRoleConfiguration) m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) m.Set(meta.KeyVisibility, meta.ValueVisibilitySimple) m.Set(meta.KeyReadOnly, meta.ValueTrue) return m } func genConfigC(m *meta.Meta) string { var sb strings.Builder sb.WriteString("|=Name|=Value>\n") fmt.Fprintf(&sb, "|Simple|%v\n", startup.IsSimple()) fmt.Fprintf(&sb, "|Verbose|%v\n", startup.IsVerbose()) fmt.Fprintf(&sb, "|Read-only|%v\n", startup.IsReadOnlyMode()) fmt.Fprintf(&sb, "|URL prefix|%v\n", startup.URLPrefix()) // There must be a space before the next "%v". Listen address may start with a ":" fmt.Fprintf(&sb, "|Listen address| %v\n", startup.ListenAddress()) fmt.Fprintf(&sb, "|Authentication enabled|%v\n", startup.WithAuth()) fmt.Fprintf(&sb, "|Secure cookie|%v\n", startup.SecureCookie()) fmt.Fprintf(&sb, "|Persistent Cookie|%v\n", startup.PersistentCookie()) html, api := startup.TokenLifetime() fmt.Fprintf(&sb, "|API Token lifetime|%v\n", api) fmt.Fprintf(&sb, "|HTML Token lifetime|%v\n", html) fmt.Fprintf(&sb, "|Default directory place type|%v", startup.DefaultDirPlaceType()) return sb.String() } |
Added place/progplace/env.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package progplace provides zettel that inform the user about the internal Zettelstore state. package progplace import ( "fmt" "os" "sort" "strings" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genEnvironmentM(zid id.Zid) *meta.Meta { if myPlace.startConfig == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Environment Values") return m } func genEnvironmentC(*meta.Meta) string { workDir, err := os.Getwd() if err != nil { workDir = err.Error() } execName, err := os.Executable() if err != nil { execName = err.Error() } envs := os.Environ() sort.Strings(envs) var sb strings.Builder sb.WriteString("|=Name|=Value>\n") fmt.Fprintf(&sb, "|Working directory| %v\n", workDir) fmt.Fprintf(&sb, "|Executable| %v\n", execName) fmt.Fprintf(&sb, "|Build with| %v\n", startup.GetVersion().GoVersion) sb.WriteString("=== Environment\n") sb.WriteString("|=Key>|=Value<\n") for _, env := range envs { if pos := strings.IndexByte(env, '='); pos >= 0 && pos < len(env) { fmt.Fprintf(&sb, "| %v| %v\n", env[:pos], env[pos+1:]) } else { fmt.Fprintf(&sb, "| %v\n", env) } } return sb.String() } |
Added place/progplace/indexer.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package progplace provides zettel that inform the user about the internal // Zettelstore state. package progplace import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" ) func genIndexerM(zid id.Zid) *meta.Meta { if myPlace.indexer == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Indexer") return m } func genIndexerC(*meta.Meta) string { ixer := myPlace.indexer var stats index.IndexerStats ixer.ReadStats(&stats) var sb strings.Builder sb.WriteString("|=Name|=Value>\n") fmt.Fprintf(&sb, "|Zettel| %v\n", stats.Store.Zettel) fmt.Fprintf(&sb, "|Last re-index| %v\n", stats.LastReload.Format("2006-01-02 15:04:05 -0700 MST")) fmt.Fprintf(&sb, "|Indexes since last re-index| %v\n", stats.IndexesSinceReload) fmt.Fprintf(&sb, "|Duration last index| %vms\n", stats.DurLastIndex.Milliseconds()) fmt.Fprintf(&sb, "|Zettel enrichments| %v\n", stats.Store.Updates) fmt.Fprintf(&sb, "|Indexed words| %v\n", stats.Store.Words) return sb.String() } |
Changes to place/progplace/manager.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" | | > > > | | | > | < > | < > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" ) func genManagerM(zid id.Zid) *meta.Meta { if myPlace.manager == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Place Manager") return m } func genManagerC(*meta.Meta) string { mgr := myPlace.manager var stats place.Stats mgr.ReadStats(&stats) var sb strings.Builder sb.WriteString("|=Name|=Value>\n") fmt.Fprintf(&sb, "|Read-only| %v\n", stats.ReadOnly) fmt.Fprintf(&sb, "|Zettel| %v\n", stats.Zettel) fmt.Fprintf(&sb, "|Sub-places| %v\n", mgr.NumPlaces()) return sb.String() } |
Changes to place/progplace/progplace.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import ( "context" "net/url" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register( " prog", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { | > | > > > > > > | > | > > > | | | | | > | < > > > | | | > > > | | | > > > | | < < | > > > > > > > | > > > | | | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | import ( "context" "net/url" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register( " prog", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { return getPlace(cdata.Filter), nil }) } type ( zettelGen struct { meta func(id.Zid) *meta.Meta content func(*meta.Meta) string } progPlace struct { zettel map[id.Zid]zettelGen filter index.MetaFilter startConfig *meta.Meta manager place.Manager indexer index.Indexer } ) var myPlace *progPlace // Get returns the one program place. func getPlace(mf index.MetaFilter) place.ManagedPlace { if myPlace == nil { myPlace = &progPlace{ zettel: map[id.Zid]zettelGen{ id.Zid(1): {genVersionBuildM, genVersionBuildC}, id.Zid(2): {genVersionHostM, genVersionHostC}, id.Zid(3): {genVersionOSM, genVersionOSC}, id.Zid(6): {genEnvironmentM, genEnvironmentC}, id.Zid(8): {genRuntimeM, genRuntimeC}, id.Zid(18): {genIndexerM, genIndexerC}, id.Zid(20): {genManagerM, genManagerC}, id.Zid(90): {genKeysM, genKeysC}, id.Zid(96): {genConfigZettelM, genConfigZettelC}, id.Zid(98): {genConfigM, genConfigC}, }, filter: mf, } } return myPlace } // Setup remembers important values. func Setup(startConfig *meta.Meta, manager place.Manager, idx index.Indexer) { if myPlace == nil { panic("progplace.getPlace not called") } if myPlace.startConfig != nil || myPlace.manager != nil { panic("progplace.Setup already called") } myPlace.startConfig = startConfig.Clone() myPlace.manager = manager myPlace.indexer = idx } func (pp *progPlace) Location() string { return "" } func (pp *progPlace) CanCreateZettel(ctx context.Context) bool { return false } func (pp *progPlace) CreateZettel( ctx context.Context, zettel domain.Zettel) (id.Zid, error) { return id.Invalid, place.ErrReadOnly } func (pp *progPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { if gen, ok := pp.zettel[zid]; ok && gen.meta != nil { if m := gen.meta(zid); m != nil { updateMeta(m) if genContent := gen.content; genContent != nil { return domain.Zettel{ Meta: m, Content: domain.NewContent(genContent(m)), }, nil } return domain.Zettel{Meta: m}, nil } } return domain.Zettel{}, place.ErrNotFound } func (pp *progPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { if gen, ok := pp.zettel[zid]; ok { if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) return m, nil } } } return nil, place.ErrNotFound } func (pp *progPlace) FetchZids(ctx context.Context) (id.Set, error) { result := id.NewSetCap(len(pp.zettel)) for zid, gen := range pp.zettel { if genMeta := gen.meta; genMeta != nil { if genMeta(zid) != nil { result[zid] = true } } } return result, nil } func (pp *progPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { for zid, gen := range pp.zettel { if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) pp.filter.Enrich(ctx, m) if match(m) { res = append(res, m) } |
︙ | ︙ | |||
126 127 128 129 130 131 132 | } func (pp *progPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { return place.ErrReadOnly } func (pp *progPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { | | | | | | < | 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | } func (pp *progPlace) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { return place.ErrReadOnly } func (pp *progPlace) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { _, ok := pp.zettel[zid] return !ok } func (pp *progPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if _, ok := pp.zettel[curZid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (pp *progPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return false } func (pp *progPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if _, ok := pp.zettel[zid]; ok { return place.ErrReadOnly } return place.ErrNotFound } func (pp *progPlace) ReadStats(st *place.Stats) { st.ReadOnly = true st.Zettel = len(pp.zettel) } func updateMeta(m *meta.Meta) { m.Set(meta.KeyNoIndex, meta.ValueTrue) m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) m.Set(meta.KeyRole, meta.ValueRoleConfiguration) m.Set(meta.KeyReadOnly, meta.ValueTrue) if _, ok := m.Get(meta.KeyVisibility); !ok { m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) } } |
Added place/progplace/runtime.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package progplace provides zettel that inform the user about the internal Zettelstore state. package progplace import ( "fmt" "runtime/metrics" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func genRuntimeM(zid id.Zid) *meta.Meta { if myPlace.startConfig == nil { return nil } m := meta.New(zid) m.Set(meta.KeyTitle, "Zettelstore Runtime Metrics") return m } func genRuntimeC(*meta.Meta) string { var samples []metrics.Sample all := metrics.All() for _, d := range all { if d.Kind == metrics.KindFloat64Histogram { continue } samples = append(samples, metrics.Sample{Name: d.Name}) } metrics.Read(samples) var sb strings.Builder sb.WriteString("|=Name|=Value>\n") i := 0 for _, d := range all { if d.Kind == metrics.KindFloat64Histogram { continue } descr := d.Description if pos := strings.IndexByte(descr, '.'); pos > 0 { descr = descr[:pos] } fmt.Fprintf(&sb, "|%s|", descr) value := samples[i].Value i++ switch value.Kind() { case metrics.KindUint64: fmt.Fprintf(&sb, "%v", value.Uint64()) case metrics.KindFloat64: fmt.Fprintf(&sb, "%v", value.Float64()) case metrics.KindFloat64Histogram: sb.WriteString("???") case metrics.KindBad: sb.WriteString("BAD") default: fmt.Fprintf(&sb, "(unexpected metric kind: %v)", value.Kind()) } sb.WriteByte('\n') } return sb.String() } |
Changes to place/progplace/version.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package progplace provides zettel that inform the user about the internal Zettelstore state. package progplace import ( "fmt" | | | | | | < < | < < > | < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // Package progplace provides zettel that inform the user about the internal Zettelstore state. package progplace import ( "fmt" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func getVersionMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, title) m.Set(meta.KeyVisibility, meta.ValueVisibilitySimple) return m } func genVersionBuildM(zid id.Zid) *meta.Meta { m := getVersionMeta(zid, "Zettelstore Version") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func genVersionBuildC(*meta.Meta) string { return startup.GetVersion().Build } func genVersionHostM(zid id.Zid) *meta.Meta { return getVersionMeta(zid, "Zettelstore Host") } func genVersionHostC(*meta.Meta) string { return startup.GetVersion().Hostname } func genVersionOSM(zid id.Zid) *meta.Meta { return getVersionMeta(zid, "Zettelstore Operating System") } func genVersionOSC(*meta.Meta) string { v := startup.GetVersion() return fmt.Sprintf("%v/%v", v.Os, v.Arch) } |
Added place/stock/stock.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package stock allows to get zettel without reading it from a place. package stock import ( "context" "sync" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place/change" ) // Place is a place that is used by a stock. type Place interface { change.Subject // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) } // Stock allow to get subscribed zettel without reading it from a place. type Stock interface { Subscribe(zid id.Zid) error GetZettel(zid id.Zid) domain.Zettel GetMeta(zid id.Zid) *meta.Meta } // NewStock creates a new stock that operates on the given place. func NewStock(place Place) Stock { stock := &defaultStock{ place: place, subs: make(map[id.Zid]domain.Zettel), } place.RegisterObserver(stock.observe) return stock } type defaultStock struct { place Place subs map[id.Zid]domain.Zettel mxSubs sync.RWMutex } // observe tracks all changes the place signals. func (s *defaultStock) observe(ci change.Info) { if ci.Reason == change.OnReload { go func() { s.mxSubs.Lock() defer s.mxSubs.Unlock() for zid := range s.subs { s.update(zid) } }() return } s.mxSubs.RLock() defer s.mxSubs.RUnlock() if _, found := s.subs[ci.Zid]; found { go func() { s.mxSubs.Lock() defer s.mxSubs.Unlock() s.update(ci.Zid) }() } } func (s *defaultStock) update(zid id.Zid) { if zettel, err := s.place.GetZettel(context.Background(), zid); err == nil { s.subs[zid] = zettel return } } // Subscribe adds a zettel to the stock. func (s *defaultStock) Subscribe(zid id.Zid) error { s.mxSubs.Lock() defer s.mxSubs.Unlock() if _, found := s.subs[zid]; found { return nil } zettel, err := s.place.GetZettel(context.Background(), zid) if err != nil { return err } s.subs[zid] = zettel return nil } // GetZettel returns the zettel with the given zid, if in stock, else an empty zettel func (s *defaultStock) GetZettel(zid id.Zid) domain.Zettel { s.mxSubs.RLock() defer s.mxSubs.RUnlock() return s.subs[zid] } // GetZettel returns the zettel Meta with the given zid, if in stock, else nil. func (s *defaultStock) GetMeta(zid id.Zid) *meta.Meta { s.mxSubs.RLock() zettel, ok := s.subs[zid] s.mxSubs.RUnlock() if ok { return zettel.Meta } return nil } |
Changes to search/print.go.
︙ | ︙ | |||
85 86 87 88 89 90 91 | for j, val := range values { if j > 0 { io.WriteString(w, " AND") } if val.negate { io.WriteString(w, " NOT") } | < < | < < < < < < < < < < < | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | for j, val := range values { if j > 0 { io.WriteString(w, " AND") } if val.negate { io.WriteString(w, " NOT") } io.WriteString(w, " MATCH ") if val.value == "" { io.WriteString(w, "ANY") } else { io.WriteString(w, val.value) } } } |
︙ | ︙ |
Changes to search/search.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package search provides a zettel search. package search import ( "math/rand" "sort" | < | | < < < < < < < < < < < < < < < < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // Package search provides a zettel search. package search import ( "math/rand" "sort" "sync" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" ) // MetaMatchFunc is a function determine whethe some metadata should be filtered or not. type MetaMatchFunc func(*meta.Meta) bool // Search specifies a mechanism for selecting zettel. type Search struct { mx sync.RWMutex // Protects other attributes |
︙ | ︙ | |||
61 62 63 64 65 66 67 | } type expTagValues map[string][]expValue // RandomOrder is a pseudo metadata key that selects a random order. const RandomOrder = "_random" | < < < < < < < < < < < | < | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | } type expTagValues map[string][]expValue // RandomOrder is a pseudo metadata key that selects a random order. const RandomOrder = "_random" type expValue struct { value string negate bool } // AddExpr adds a match expression to the filter. func (s *Search) AddExpr(key, val string, negate bool) *Search { if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() if key == "" { s.search = append(s.search, expValue{value: val, negate: negate}) } else if s.tags == nil { s.tags = expTagValues{key: {{value: val, negate: negate}}} } else { s.tags[key] = append(s.tags[key], expValue{value: val, negate: negate}) } return s } // SetNegate changes the filter to reverse its selection. func (s *Search) SetNegate() *Search { if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() |
︙ | ︙ | |||
238 239 240 241 242 243 244 | if order := s.order; order != "" && meta.IsComputed(order) { return true } return false } // CompileMatch returns a function to match meta data based on filter specification. | | > | > | 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | if order := s.order; order != "" && meta.IsComputed(order) { return true } return false } // CompileMatch returns a function to match meta data based on filter specification. func (s *Search) CompileMatch(selector index.Selector) MetaMatchFunc { if s == nil { return filterNone } s.mx.Lock() defer s.mx.Unlock() compMeta := compileFilter(s.tags) //compSearch := createSearchAllFunc(s.search) compSearch := compileSearch(selector, s.search) if preMatch := s.preMatch; preMatch != nil { return compilePreMatch(preMatch, compMeta, compSearch, s.negate) } return compileNoPreMatch(compMeta, compSearch, s.negate) } func filterNone(m *meta.Meta) bool { return true } |
︙ | ︙ |
Changes to search/selector.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package search provides a zettel search. package search // This file is about "compiling" a search expression into a function. import ( | < < < | < | < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < | < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < | < | | > > > > > > > > > > > > > > > > > > | > < | < | < < < | < | < < < < | < < | > > | | < < > | | < < | < < | < > | < < < | | | < < < < < < < < < < < < < < < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | // Package search provides a zettel search. package search // This file is about "compiling" a search expression into a function. import ( "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/strfun" ) func compileSearch(selector index.Selector, search []expValue) MetaMatchFunc { poss, negs := normalizeSearchValues(search) if len(poss) == 0 { if len(negs) == 0 { return nil } return makeNegOnlySearch(selector, negs) } if len(negs) == 0 { return makePosOnlySearch(selector, poss) } return makePosNegSearch(selector, poss, negs) } func normalizeSearchValues(search []expValue) (positives, negatives []string) { posSet := make(map[string]bool) negSet := make(map[string]bool) for _, val := range search { for _, word := range strfun.NormalizeWords(val.value) { if val.negate { if _, ok := negSet[word]; !ok { negSet[word] = true negatives = append(negatives, word) } } else { if _, ok := posSet[word]; !ok { posSet[word] = true positives = append(positives, word) } } } } return positives, negatives } func makePosOnlySearch(selector index.Selector, poss []string) MetaMatchFunc { return func(m *meta.Meta) bool { ids := retrieveZids(selector, poss) _, ok := ids[m.Zid] return ok } } func makeNegOnlySearch(selector index.Selector, negs []string) MetaMatchFunc { return func(m *meta.Meta) bool { ids := retrieveZids(selector, negs) _, ok := ids[m.Zid] return !ok } } func makePosNegSearch(selector index.Selector, poss, negs []string) MetaMatchFunc { return func(m *meta.Meta) bool { idsPos := retrieveZids(selector, poss) _, okPos := idsPos[m.Zid] idsNeg := retrieveZids(selector, negs) _, okNeg := idsNeg[m.Zid] return okPos && !okNeg } } func retrieveZids(selector index.Selector, words []string) id.Set { var result id.Set for i, word := range words { ids := selector.SelectContains(word) if i == 0 { result = ids continue } result = result.Intersect(ids) } return result } |
Changes to strfun/slugify.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package strfun provides some string functions. package strfun import ( | < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < < < | < < < < < < | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | // under this license. //----------------------------------------------------------------------------- // Package strfun provides some string functions. package strfun import ( "unicode" "golang.org/x/text/unicode/norm" ) var ( useUnicode = []*unicode.RangeTable{ unicode.Letter, unicode.Number, } ignoreUnicode = []*unicode.RangeTable{ unicode.Mark, unicode.Sk, unicode.Lm, } ) // Slugify returns a string that can be used as part of an URL func Slugify(s string) string { result := make([]rune, 0, len(s)) addDash := false for _, r := range norm.NFKD.String(s) { if unicode.IsOneOf(useUnicode, r) { result = append(result, unicode.ToLower(r)) addDash = true } else if !unicode.IsOneOf(ignoreUnicode, r) && addDash { result = append(result, '-') addDash = false } } if i := len(result) - 1; i >= 0 && result[i] == '-' { result = result[:i] } return string(result) } // NormalizeWords produces a word list that is normalized for better searching. func NormalizeWords(s string) []string { result := make([]string, 0, 1) word := make([]rune, 0, len(s)) for _, r := range norm.NFKD.String(s) { if unicode.IsOneOf(useUnicode, r) { word = append(word, unicode.ToLower(r)) } else if !unicode.IsOneOf(ignoreUnicode, r) && len(word) > 0 { result = append(result, string(word)) word = word[:0] } } if len(word) > 0 { result = append(result, string(word)) } return result } |
Changes to strfun/slugify_test.go.
︙ | ︙ | |||
47 48 49 50 51 52 53 | } func TestNormalizeWord(t *testing.T) { tests := []struct { in string exp []string }{ | < < < | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | } func TestNormalizeWord(t *testing.T) { tests := []struct { in string exp []string }{ {"simple test", []string{"simple", "test"}}, {"I'm a go developer", []string{"i", "m", "a", "go", "developer"}}, {"-!->simple test<-!-", []string{"simple", "test"}}, {"äöüÄÖÜß", []string{"aouaouß"}}, {"\"aèf", []string{"aef"}}, {"a#b", []string{"a", "b"}}, {"*", []string{}}, |
︙ | ︙ |
Changes to strfun/strfun.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package strfun provides some string functions. package strfun import ( "strings" "unicode" | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Package strfun provides some string functions. package strfun import ( "strings" "unicode" ) // TrimSpaceRight returns a slice of the string s, with all trailing white space removed, // as defined by Unicode. func TrimSpaceRight(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace) } |
Changes to strfun/strfun_test.go.
︙ | ︙ | |||
44 45 46 47 48 49 50 | for i, tc := range testcases { got := strfun.TrimSpaceRight(tc.in) if got != tc.exp { t.Errorf("%d/%q: expected %q, got %q", i, tc.in, tc.exp, got) } } } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 44 45 46 47 48 49 50 | for i, tc := range testcases { got := strfun.TrimSpaceRight(tc.in) if got != tc.exp { t.Errorf("%d/%q: expected %q, got %q", i, tc.in, tc.exp, got) } } } |
Changes to template/spec_test.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 | // located. See file LICENSE. //----------------------------------------------------------------------------- package template_test import ( "encoding/json" | < | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // located. See file LICENSE. //----------------------------------------------------------------------------- package template_test import ( "encoding/json" "os" "path/filepath" "sort" "testing" "zettelstore.de/z/template" ) |
︙ | ︙ | |||
180 181 182 183 184 185 186 | } return filepath.Join(curDir, "..", "testdata", "mustache") } func TestSpec(t *testing.T) { root := getRoot() if _, err := os.Stat(root); err != nil { | | | 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | } return filepath.Join(curDir, "..", "testdata", "mustache") } func TestSpec(t *testing.T) { root := getRoot() if _, err := os.Stat(root); err != nil { if os.IsNotExist(err) { t.Fatalf("Could not find the mustache testdata folder at %s'", root) } t.Fatal(err) } paths, err := filepath.Glob(filepath.Join(root, "*.json")) if err != nil { |
︙ | ︙ |
Changes to testdata/content/table/20200618140700.zettel.
1 2 3 | title: Testtable |h1>|=h2|h3:| | < | 1 2 3 4 5 | title: Testtable |h1>|=h2|h3:| |<c1|c2|:c3| |f1|f2|=f3 |
Changes to tests/markdown_test.go.
︙ | ︙ | |||
39 40 41 42 43 44 45 | StartLine int `json:"start_line"` EndLine int `json:"end_line"` Section string `json:"section"` } // exceptions lists all CommonMark tests that should not be tested for identical HTML output var exceptions = []string{ | | < < | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | StartLine int `json:"start_line"` EndLine int `json:"end_line"` Section string `json:"section"` } // exceptions lists all CommonMark tests that should not be tested for identical HTML output var exceptions = []string{ " - foo\n - bar\n\t - baz\n", // 9 "- foo\n - bar\n - baz\n - boo\n", // 264 "10) foo\n - bar\n", // 266 "- # Foo\n- Bar\n ---\n baz\n", // 270 "- foo\n\n- bar\n\n\n- baz\n", // 276 "- foo\n - bar\n - baz\n\n\n bim\n", // 277 "1. a\n\n 2. b\n\n 3. c\n", // 281 "1. a\n\n 2. b\n\n 3. c\n", // 283 |
︙ | ︙ |
Changes to tests/regression_test.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 | "net/url" "os" "path/filepath" "strings" "testing" "zettelstore.de/z/ast" | | | < | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | "net/url" "os" "path/filepath" "strings" "testing" "zettelstore.de/z/ast" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/jsonenc" _ "zettelstore.de/z/encoder/nativeenc" |
︙ | ︙ | |||
46 47 48 49 50 51 52 | func getFilePlaces(wd string, kind string) (root string, places []place.ManagedPlace) { root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind)) entries, err := os.ReadDir(root) if err != nil { panic(err) } | | < < < < | > > > > | | | < < < < | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | func getFilePlaces(wd string, kind string) (root string, places []place.ManagedPlace) { root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind)) entries, err := os.ReadDir(root) if err != nil { panic(err) } cdata := manager.ConnectData{Filter: &noFilter{}, Notify: nil} for _, entry := range entries { if entry.IsDir() { place, err := manager.Connect( "dir://"+filepath.Join(root, entry.Name())+"?type="+startup.ValueDirPlaceTypeSimple, false, &cdata, ) if err != nil { panic(err) } places = append(places, place) } } return root, places } type noFilter struct{} func (nf *noFilter) Enrich(ctx context.Context, m *meta.Meta) {} func (nf *noFilter) Remove(ctx context.Context, m *meta.Meta) {} func trimLastEOL(s string) string { if lastPos := len(s) - 1; lastPos >= 0 && s[lastPos] == '\n' { return s[:lastPos] } return s } |
︙ | ︙ | |||
123 124 125 126 127 128 129 | zmkEncoder := encoder.Create("zmk", nil) var sb strings.Builder zmkEncoder.WriteBlocks(&sb, zn.Ast) gotFirst := sb.String() sb.Reset() newZettel := parser.ParseZettel(domain.Zettel{ | | | 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | zmkEncoder := encoder.Create("zmk", nil) var sb strings.Builder zmkEncoder.WriteBlocks(&sb, zn.Ast) gotFirst := sb.String() sb.Reset() newZettel := parser.ParseZettel(domain.Zettel{ Meta: zn.Meta, Content: domain.NewContent("\n" + gotFirst)}, "") zmkEncoder.WriteBlocks(&sb, newZettel.Ast) gotSecond := sb.String() sb.Reset() if gotFirst != gotSecond { t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond) } |
︙ | ︙ | |||
157 158 159 160 161 162 163 | panic(err) } for _, meta := range metaList { zettel, err := p.GetZettel(context.Background(), meta.Zid) if err != nil { panic(err) } | | | 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | panic(err) } for _, meta := range metaList { zettel, err := p.GetZettel(context.Background(), meta.Zid) if err != nil { panic(err) } z := parser.ParseZettel(zettel, "") for _, format := range formats { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) { resultName := filepath.Join(wd, "result", "content", placeName, z.Zid.String()+"."+format) checkBlocksFile(st, resultName, z, format) }) } t.Run(fmt.Sprintf("%s::%d", p.Location(), meta.Zid), func(st *testing.T) { |
︙ | ︙ | |||
210 211 212 213 214 215 216 | panic(err) } for _, meta := range metaList { zettel, err := p.GetZettel(context.Background(), meta.Zid) if err != nil { panic(err) } | | < < < < < < < < < < < < < < < < < < < < < | 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | panic(err) } for _, meta := range metaList { zettel, err := p.GetZettel(context.Background(), meta.Zid) if err != nil { panic(err) } z := parser.ParseZettel(zettel, "") for _, format := range formats { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, format), func(st *testing.T) { resultName := filepath.Join(wd, "result", "meta", placeName, z.Zid.String()+"."+format) checkMetaFile(st, resultName, z, format) }) } } if err := ss.Stop(context.Background()); err != nil { panic(err) } } func TestMetaRegression(t *testing.T) { wd, err := os.Getwd() if err != nil { panic(err) } root, places := getFilePlaces(wd, "meta") for _, p := range places { checkMetaPlace(t, p, wd, getPlaceName(p, root)) } } |
Changes to tools/build.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // Package main provides a command to build and run the software. package main import ( "archive/zip" "bytes" "flag" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "regexp" "strings" "time" | > < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // Package main provides a command to build and run the software. package main import ( "archive/zip" "bytes" "errors" "flag" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "regexp" "strings" "time" ) func executeCommand(env []string, name string, arg ...string) (string, error) { if verbose { if len(env) > 0 { for i, e := range env { fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e) |
︙ | ︙ | |||
56 57 58 59 60 61 62 | return "", err } return strings.TrimFunc(string(content), func(r rune) bool { return r <= ' ' }), nil } | | | < | < < < | | < | | < > | > > > | < > | | < < | < < < > > > > > > | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | return "", err } return strings.TrimFunc(string(content), func(r rune) bool { return r <= ' ' }), nil } var fossilHash = regexp.MustCompile(`\[[0-9a-fA-F]+\]`) var dirtyPrefixes = []string{"DELETED", "ADDED", "UPDATED", "CONFLICT", "EDITED", "RENAMED"} const dirtySuffix = "-dirty" func readFossilVersion() (string, error) { s, err := executeCommand(nil, "fossil", "timeline", "--limit", "1") if err != nil { return "", err } hash := fossilHash.FindString(s) if len(hash) < 3 { return "", errors.New("no fossil hash found") } hash = hash[1 : len(hash)-1] s, err = executeCommand(nil, "fossil", "status") if err != nil { return "", err } for _, line := range splitLines(s) { for _, prefix := range dirtyPrefixes { if strings.HasPrefix(line, prefix) { return hash + dirtySuffix, nil } } } return hash, nil } func splitLines(s string) []string { return strings.FieldsFunc(s, func(r rune) bool { return r == '\n' || r == '\r' }) } func getVersionData() (string, string) { base, err := readVersionFile() if err != nil { base = "dev" } fossil, err := readFossilVersion() |
︙ | ︙ | |||
141 142 143 144 145 146 147 | } return checkFossilExtra() } func checkGoTest() error { out, err := executeCommand(nil, "go", "test", "./...") if err != nil { | | | 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | } return checkFossilExtra() } func checkGoTest() error { out, err := executeCommand(nil, "go", "test", "./...") if err != nil { for _, line := range splitLines(out) { if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") { continue } fmt.Fprintln(os.Stderr, line) } } return err |
︙ | ︙ | |||
204 205 206 207 208 209 210 | out, err := executeCommand(nil, "fossil", "extra") if err != nil { fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'") return err } if len(out) > 0 { fmt.Fprint(os.Stderr, "Warning: unversioned file(s):") | | | 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | out, err := executeCommand(nil, "fossil", "extra") if err != nil { fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'") return err } if len(out) > 0 { fmt.Fprint(os.Stderr, "Warning: unversioned file(s):") for i, extra := range splitLines(out) { if i > 0 { fmt.Fprint(os.Stderr, ",") } fmt.Fprintf(os.Stderr, " %q", extra) } fmt.Fprintln(os.Stderr) } |
︙ | ︙ |
Changes to usecase/authenticate.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package usecase import ( "context" "math/rand" "time" | | | < | < | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | package usecase import ( "context" "math/rand" "time" "zettelstore.de/z/auth/cred" "zettelstore.de/z/auth/token" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" ) // AuthenticatePort is the interface used by this use case. type AuthenticatePort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // Authenticate is the data for this use case. type Authenticate struct { port AuthenticatePort ucGetUser GetUser } // NewAuthenticate creates a new use case. func NewAuthenticate(port AuthenticatePort) Authenticate { return Authenticate{ port: port, ucGetUser: NewGetUser(port), } } // Run executes the use case. func (uc Authenticate) Run(ctx context.Context, ident, credential string, d time.Duration, k token.Kind) ([]byte, error) { identMeta, err := uc.ucGetUser.Run(ctx, ident) defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond) if identMeta == nil || err != nil { compensateCompare() return nil, err } if hashCred, ok := identMeta.Get(meta.KeyCredential); ok { ok, err := cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential) if err != nil { return nil, err } if ok { token, err := token.GetToken(identMeta, d, k) if err != nil { return nil, err } return token, nil } return nil, nil } |
︙ | ︙ |
Changes to usecase/create_zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" | | < | | | < < < | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/strfun" ) // CreateZettelPort is the interface used by this use case. type CreateZettelPort interface { // CreateZettel creates a new zettel. CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) } // CreateZettel is the data for this use case. type CreateZettel struct { port CreateZettelPort } // NewCreateZettel creates a new use case. func NewCreateZettel(port CreateZettelPort) CreateZettel { return CreateZettel{port: port} } // Run executes the use case. func (uc CreateZettel) Run(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { m := zettel.Meta if m.Zid.IsValid() { return m.Zid, nil // TODO: new error: already exists } if title, ok := m.Get(meta.KeyTitle); !ok || title == "" { m.Set(meta.KeyTitle, runtime.GetDefaultTitle()) } if role, ok := m.Get(meta.KeyRole); !ok || role == "" { m.Set(meta.KeyRole, runtime.GetDefaultRole()) } if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { m.Set(meta.KeySyntax, runtime.GetDefaultSyntax()) } m.YamlSep = runtime.GetYAMLHeader() zettel.Content = domain.Content(strfun.TrimSpaceRight(zettel.Content.AsString())) return uc.port.CreateZettel(ctx, zettel) } |
Changes to usecase/folge_zettel.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( | | | < | < | | | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // under this license. //----------------------------------------------------------------------------- // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // FolgeZettel is the data for this use case. type FolgeZettel struct{} // NewFolgeZettel creates a new use case. func NewFolgeZettel() FolgeZettel { return FolgeZettel{} } // Run executes the use case. func (uc FolgeZettel) Run(origZettel domain.Zettel) domain.Zettel { origMeta := origZettel.Meta m := meta.New(id.Invalid) if title, ok := origMeta.Get(meta.KeyTitle); ok { if len(title) > 0 { title = "Folge of " + title } else { title = "Folge" } m.Set(meta.KeyTitle, title) } m.Set(meta.KeyRole, runtime.GetRole(origMeta)) m.Set(meta.KeyTags, origMeta.GetDefault(meta.KeyTags, "")) m.Set(meta.KeySyntax, runtime.GetSyntax(origMeta)) m.Set(meta.KeyPrecursor, origMeta.Zid.String()) return domain.Zettel{Meta: m, Content: ""} } |
Changes to usecase/get_user.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" | | | < | | | > > > | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/search" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- // GetUserPort is the interface used by this use case. type GetUserPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // GetUser is the data for this use case. type GetUser struct { port GetUserPort } // NewGetUser creates a new use case. func NewGetUser(port GetUserPort) GetUser { return GetUser{port: port} } // Run executes the use case. func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { if !startup.WithAuth() { return nil, nil } ctx = index.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. identMeta, err := uc.port.GetMeta(ctx, startup.Owner()) if err == nil && identMeta.GetDefault(meta.KeyUserID, "") == ident { if role, ok := identMeta.Get(meta.KeyRole); !ok || role != meta.ValueRoleUser { return nil, nil } return identMeta, nil } // Owner was not found or has another ident. Try via list search. var s *search.Search s = s.AddExpr(meta.KeyRole, meta.ValueRoleUser, false) s = s.AddExpr(meta.KeyUserID, ident, false) metaList, err := uc.port.SelectMeta(ctx, s) if err != nil { return nil, err } if len(metaList) < 1 { return nil, nil } |
︙ | ︙ | |||
84 85 86 87 88 89 90 | } // NewGetUserByZid creates a new use case. func NewGetUserByZid(port GetUserByZidPort) GetUserByZid { return GetUserByZid{port: port} } | | | | | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | } // NewGetUserByZid creates a new use case. func NewGetUserByZid(port GetUserByZidPort) GetUserByZid { return GetUserByZid{port: port} } // Run executes the use case. func (uc GetUserByZid) Run(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { userMeta, err := uc.port.GetMeta(index.NoEnrichContext(ctx), zid) if err != nil { return nil, err } if val, ok := userMeta.Get(meta.KeyUserID); !ok || val != ident { return nil, nil } return userMeta, nil } |
Changes to usecase/list_role.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package usecase import ( "context" "sort" "zettelstore.de/z/domain/meta" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package usecase import ( "context" "sort" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/search" ) // ListRolePort is the interface used by this use case. type ListRolePort interface { // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) |
︙ | ︙ | |||
34 35 36 37 38 39 40 | // NewListRole creates a new use case. func NewListRole(port ListRolePort) ListRole { return ListRole{port: port} } // Run executes the use case. func (uc ListRole) Run(ctx context.Context) ([]string, error) { | | | 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | // NewListRole creates a new use case. func NewListRole(port ListRolePort) ListRole { return ListRole{port: port} } // Run executes the use case. func (uc ListRole) Run(ctx context.Context) ([]string, error) { metas, err := uc.port.SelectMeta(index.NoEnrichContext(ctx), nil) if err != nil { return nil, err } roles := make(map[string]bool, 8) for _, m := range metas { if role, ok := m.Get(meta.KeyRole); ok && role != "" { roles[role] = true |
︙ | ︙ |
Changes to usecase/list_tags.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/meta" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/search" ) // ListTagsPort is the interface used by this use case. type ListTagsPort interface { // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) |
︙ | ︙ | |||
36 37 38 39 40 41 42 | } // TagData associates tags with a list of all zettel meta that use this tag type TagData map[string][]*meta.Meta // Run executes the use case. func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) { | | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | } // TagData associates tags with a list of all zettel meta that use this tag type TagData map[string][]*meta.Meta // Run executes the use case. func (uc ListTags) Run(ctx context.Context, minCount int) (TagData, error) { metas, err := uc.port.SelectMeta(index.NoEnrichContext(ctx), nil) if err != nil { return nil, err } result := make(TagData) for _, m := range metas { if tl, ok := m.GetList(meta.KeyTags); ok && len(tl) > 0 { for _, t := range tl { |
︙ | ︙ |
Changes to usecase/parse_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/ast" | < < | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/ast" "zettelstore.de/z/domain/id" "zettelstore.de/z/parser" ) // ParseZettel is the data for this use case. type ParseZettel struct { getZettel GetZettel } // NewParseZettel creates a new use case. func NewParseZettel(getZettel GetZettel) ParseZettel { return ParseZettel{getZettel: getZettel} } // Run executes the use case. func (uc ParseZettel) Run( ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { zettel, err := uc.getZettel.Run(ctx, zid) if err != nil { return nil, err } return parser.ParseZettel(zettel, syntax), nil } |
Changes to usecase/rename_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" ) // RenameZettelPort is the interface used by this use case. type RenameZettelPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) |
︙ | ︙ | |||
43 44 45 46 47 48 49 | // NewRenameZettel creates a new use case. func NewRenameZettel(port RenameZettelPort) RenameZettel { return RenameZettel{port: port} } // Run executes the use case. func (uc RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error { | | | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | // NewRenameZettel creates a new use case. func NewRenameZettel(port RenameZettelPort) RenameZettel { return RenameZettel{port: port} } // Run executes the use case. func (uc RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error { noEnrichCtx := index.NoEnrichContext(ctx) if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil { return err } if newZid == curZid { // Nothing to do return nil } if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil { return &ErrZidInUse{Zid: newZid} } return uc.port.RenameZettel(ctx, curZid, newZid) } |
Changes to usecase/search.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/meta" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package usecase provides (business) use cases for the zettelstore. package usecase import ( "context" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/search" ) // SearchPort is the interface used by this use case. type SearchPort interface { // SelectMeta returns all zettel meta data that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) |
︙ | ︙ | |||
34 35 36 37 38 39 40 | func NewSearch(port SearchPort) Search { return Search{port: port} } // Run executes the use case. func (uc Search) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { | | | 34 35 36 37 38 39 40 41 42 43 44 | func NewSearch(port SearchPort) Search { return Search{port: port} } // Run executes the use case. func (uc Search) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { ctx = index.NoEnrichContext(ctx) } return uc.port.SelectMeta(ctx, s) } |
Changes to usecase/update_zettel.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "context" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "context" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/strfun" ) // UpdateZettelPort is the interface used by this use case. type UpdateZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) |
︙ | ︙ | |||
39 40 41 42 43 44 45 | func NewUpdateZettel(port UpdateZettelPort) UpdateZettel { return UpdateZettel{port: port} } // Run executes the use case. func (uc UpdateZettel) Run(ctx context.Context, zettel domain.Zettel, hasContent bool) error { m := zettel.Meta | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | func NewUpdateZettel(port UpdateZettelPort) UpdateZettel { return UpdateZettel{port: port} } // Run executes the use case. func (uc UpdateZettel) Run(ctx context.Context, zettel domain.Zettel, hasContent bool) error { m := zettel.Meta oldZettel, err := uc.port.GetZettel(index.NoEnrichContext(ctx), m.Zid) if err != nil { return err } if zettel.Equal(oldZettel, false) { return nil } m.SetNow(meta.KeyModified) |
︙ | ︙ |
Deleted web/adapter/api/api.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/api/get_links.go.
︙ | ︙ | |||
37 38 39 40 41 42 43 | Local []string `json:"local"` External []string `json:"external"` } `json:"images"` Cites []string `json:"cites"` } // MakeGetLinksHandler creates a new API handler to return links to other material. | | < | | | | | | | | | | | | | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | Local []string `json:"local"` External []string `json:"external"` } `json:"images"` Cites []string `json:"cites"` } // MakeGetLinksHandler creates a new API handler to return links to other material. func MakeGetLinksHandler(parseZettel usecase.ParseZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() zn, err := parseZettel.Run(r.Context(), zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } summary := collect.References(zn) kind := getKindFromValue(q.Get("kind")) matter := getMatterFromValue(q.Get("matter")) if !validKindMatter(kind, matter) { adapter.BadRequest(w, "Invalid kind/matter") return } outData := jsonGetLinks{ ID: zid.String(), URL: adapter.NewURLBuilder('z').SetZid(zid).String(), } if kind&kindLink != 0 { setupLinkJSONRefs(summary, matter, &outData) } if kind&kindImage != 0 { setupImageJSONRefs(summary, matter, &outData) } if kind&kindCite != 0 { outData.Cites = stringCites(summary.Cites) } w.Header().Set(adapter.ContentType, format2ContentType("json")) enc := json.NewEncoder(w) enc.SetEscapeHTML(false) enc.Encode(&outData) } } func setupLinkJSONRefs(summary collect.Summary, matter matterType, outData *jsonGetLinks) { if matter&matterIncoming != 0 { outData.Links.Incoming = []jsonIDURL{} } zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Links, false) if matter&matterOutgoing != 0 { outData.Links.Outgoing = idURLRefs(zetRefs) } if matter&matterLocal != 0 { outData.Links.Local = stringRefs(locRefs) } if matter&matterExternal != 0 { outData.Links.External = stringRefs(extRefs) } } func setupImageJSONRefs(summary collect.Summary, matter matterType, outData *jsonGetLinks) { zetRefs, locRefs, extRefs := collect.DivideReferences(summary.Images, false) if matter&matterOutgoing != 0 { outData.Images.Outgoing = idURLRefs(zetRefs) } if matter&matterLocal != 0 { outData.Images.Local = stringRefs(locRefs) } if matter&matterExternal != 0 { outData.Images.External = stringRefs(extRefs) } } func idURLRefs(refs []*ast.Reference) []jsonIDURL { result := make([]jsonIDURL, 0, len(refs)) for _, ref := range refs { path := ref.URL.Path ub := adapter.NewURLBuilder('z').AppendPath(path) if fragment := ref.URL.Fragment; len(fragment) > 0 { ub.SetFragment(fragment) } result = append(result, jsonIDURL{ID: path, URL: ub.String()}) } return result } |
︙ | ︙ |
Changes to web/adapter/api/get_order.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetOrderHandler creates a new API handler to return zettel references // of a given zettel. | | < | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetOrderHandler creates a new API handler to return zettel references // of a given zettel. func MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() start, metas, err := zettelOrder.Run(r.Context(), zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } writeMetaList(w, start, metas) } } |
Changes to web/adapter/api/get_role_list.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 | "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/jsonenc" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListRoleHandler creates a new HTTP handler for the use case "list some zettel". | | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/jsonenc" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListRoleHandler creates a new HTTP handler for the use case "list some zettel". func MakeListRoleHandler(listRole usecase.ListRole) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { roleList, err := listRole.Run(r.Context()) if err != nil { adapter.ReportUsecaseError(w, err) return } |
︙ | ︙ |
Changes to web/adapter/api/get_tags_list.go.
︙ | ︙ | |||
20 21 22 23 24 25 26 | "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/jsonenc" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListTagsHandler creates a new HTTP handler for the use case "list some zettel". | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/jsonenc" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListTagsHandler creates a new HTTP handler for the use case "list some zettel". func MakeListTagsHandler(listTags usecase.ListTags) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min")) tagData, err := listTags.Run(r.Context(), iMinCount) if err != nil { adapter.ReportUsecaseError(w, err) return } |
︙ | ︙ |
Changes to web/adapter/api/get_zettel.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( | < < | | > | | > > | | | | < < > > > | > | | | > > > > > > > > > | > > > > | > > > > > > > > > > | > > > | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetZettelHandler creates a new HTTP handler to return a rendered zettel. func MakeGetZettelHandler( parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() format := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) if format == "raw" { ctx = index.NoEnrichContext(ctx) } zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { adapter.ReportUsecaseError(w, err) return } part := getPart(q, partZettel) switch format { case "json", "djson": if part == partUnknown { adapter.BadRequest(w, "Unknown _part parameter") return } w.Header().Set(adapter.ContentType, format2ContentType(format)) if format != "djson" { err = writeJSONZettel(w, zn, part) } else { err = writeDJSONZettel(ctx, w, zn, part, partZettel, getMeta) } if err != nil { adapter.InternalServerError(w, "Write D/JSON", err) } return } env := encoder.Environment{ LinkAdapter: adapter.MakeLinkAdapter(ctx, 'z', getMeta, part.DefString(partZettel), format), ImageAdapter: adapter.MakeImageAdapter(), CiteAdapter: nil, Lang: runtime.GetLang(zn.InhMeta), Xhtml: false, MarkerExternal: "", NewWindow: false, IgnoreMeta: map[string]bool{meta.KeyLang: true}, } switch part { case partZettel: inhMeta := false if format != "raw" { w.Header().Set(adapter.ContentType, format2ContentType(format)) inhMeta = true } enc := encoder.Create(format, &env) if enc == nil { err = adapter.ErrNoSuchFormat } else { _, err = enc.WriteZettel(w, zn, inhMeta) } case partMeta: w.Header().Set(adapter.ContentType, format2ContentType(format)) if format == "raw" { // Don't write inherited meta data, just the raw err = writeMeta(w, zn.Meta, format, nil) } else { err = writeMeta(w, zn.InhMeta, format, nil) } case partContent: if format == "raw" { if ct, ok := syntax2contentType(runtime.GetSyntax(zn.Meta)); ok { w.Header().Add(adapter.ContentType, ct) } } else { w.Header().Set(adapter.ContentType, format2ContentType(format)) } err = writeContent(w, zn, format, &env) default: adapter.BadRequest(w, "Unknown _part parameter") return } if err != nil { if err == adapter.ErrNoSuchFormat { adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in format %q", zid.String(), format)) return } adapter.InternalServerError(w, "Get zettel", err) } } } |
Changes to web/adapter/api/get_zettel_context.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() |
︙ | ︙ | |||
39 40 41 42 43 44 45 | } ctx := r.Context() metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { adapter.ReportUsecaseError(w, err) return } | | | 39 40 41 42 43 44 45 46 47 48 | } ctx := r.Context() metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { adapter.ReportUsecaseError(w, err) return } writeMetaList(w, metaList[0], metaList[1:]) } } |
Changes to web/adapter/api/get_zettel_list.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 | // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" | > | | | < < < < | | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | // Package api provides api handlers for web requests. package api import ( "fmt" "net/http" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel". func MakeListMetaHandler( listMeta usecase.ListMeta, getMeta usecase.GetMeta, parseZettel usecase.ParseZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() s := adapter.GetSearch(q, false) format := adapter.GetFormat(r, q, encoder.GetDefaultFormat()) part := getPart(q, partMeta) ctx1 := ctx if format == "html" || (!s.HasComputedMetaKey() && (part == partID || part == partContent)) { ctx1 = index.NoEnrichContext(ctx1) } metaList, err := listMeta.Run(ctx1, s) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set(adapter.ContentType, format2ContentType(format)) switch format { case "html": renderListMetaHTML(w, metaList) case "json", "djson": renderListMetaXJSON(ctx, w, metaList, format, part, partMeta, getMeta, parseZettel) case "native", "raw", "text", "zmk": adapter.NotImplemented(w, fmt.Sprintf("Zettel list in format %q not yet implemented", format)) default: adapter.BadRequest(w, fmt.Sprintf("Zettel list not available in format %q", format)) } } } func renderListMetaHTML(w http.ResponseWriter, metaList []*meta.Meta) { env := encoder.Environment{Interactive: true} buf := encoder.NewBufWriter(w) buf.WriteStrings("<html lang=\"", runtime.GetDefaultLang(), "\">\n<body>\n<ul>\n") for _, m := range metaList { title := m.GetDefault(meta.KeyTitle, "") htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), "html", &env) if err != nil { adapter.InternalServerError(w, "Format HTML inlines", err) return } buf.WriteStrings( "<li><a href=\"", adapter.NewURLBuilder('z').SetZid(m.Zid).AppendQuery("_format", "html").String(), "\">", htmlTitle, "</a></li>\n") } buf.WriteString("</ul>\n</body>\n</html>") buf.Flush() } |
Changes to web/adapter/api/json.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "context" "encoding/json" "io" "net/http" "zettelstore.de/z/ast" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) | > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "context" "encoding/json" "io" "net/http" "zettelstore.de/z/ast" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) |
︙ | ︙ | |||
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | } type jsonContent struct { ID string `json:"id"` URL string `json:"url"` Encoding string `json:"encoding"` Content interface{} `json:"content"` } func encodedContent(content domain.Content) (string, interface{}) { if content.IsBinary() { return "base64", content.AsBytes() } return "", content.AsString() } var ( djsonMetaHeader = []byte(",\"meta\":") djsonContentHeader = []byte(",\"content\":") djsonHeader1 = []byte("{\"id\":\"") djsonHeader2 = []byte("\",\"url\":\"") djsonHeader3 = []byte("?_format=") djsonHeader4 = []byte("\"") djsonFooter = []byte("}") ) | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > | > > < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | > > > > > > > > > > > | > | | | < | | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | } type jsonContent struct { ID string `json:"id"` URL string `json:"url"` Encoding string `json:"encoding"` Content interface{} `json:"content"` } func writeJSONZettel(w http.ResponseWriter, z *ast.ZettelNode, part partType) error { var outData interface{} idData := jsonIDURL{ ID: z.Zid.String(), URL: adapter.NewURLBuilder('z').SetZid(z.Zid).String(), } switch part { case partZettel: encoding, content := encodedContent(z.Content) outData = jsonZettel{ ID: idData.ID, URL: idData.URL, Meta: z.InhMeta.Map(), Encoding: encoding, Content: content, } case partMeta: outData = jsonMeta{ ID: idData.ID, URL: idData.URL, Meta: z.InhMeta.Map(), } case partContent: encoding, content := encodedContent(z.Content) outData = jsonContent{ ID: idData.ID, URL: idData.URL, Encoding: encoding, Content: content, } case partID: outData = idData default: panic(part) } return encodeJSONData(w, outData, false) } func encodedContent(content domain.Content) (string, interface{}) { if content.IsBinary() { return "base64", content.AsBytes() } return "", content.AsString() } func writeDJSONZettel( ctx context.Context, w http.ResponseWriter, z *ast.ZettelNode, part, defPart partType, getMeta usecase.GetMeta, ) (err error) { switch part { case partZettel: err = writeDJSONHeader(w, z.Zid) if err == nil { err = writeDJSONMeta(w, z) } if err == nil { err = writeDJSONContent(ctx, w, z, part, defPart, getMeta) } case partMeta: err = writeDJSONHeader(w, z.Zid) if err == nil { err = writeDJSONMeta(w, z) } case partContent: err = writeDJSONHeader(w, z.Zid) if err == nil { err = writeDJSONContent(ctx, w, z, part, defPart, getMeta) } case partID: writeDJSONHeader(w, z.Zid) default: panic(part) } if err == nil { _, err = w.Write(djsonFooter) } return err } var ( djsonMetaHeader = []byte(",\"meta\":") djsonContentHeader = []byte(",\"content\":") djsonHeader1 = []byte("{\"id\":\"") djsonHeader2 = []byte("\",\"url\":\"") djsonHeader3 = []byte("?_format=") djsonHeader4 = []byte("\"") djsonFooter = []byte("}") ) func writeDJSONHeader(w http.ResponseWriter, zid id.Zid) error { _, err := w.Write(djsonHeader1) if err == nil { _, err = w.Write(zid.Bytes()) } if err == nil { _, err = w.Write(djsonHeader2) } if err == nil { _, err = io.WriteString(w, adapter.NewURLBuilder('z').SetZid(zid).String()) } if err == nil { _, err = w.Write(djsonHeader3) if err == nil { _, err = io.WriteString(w, "djson") } } if err == nil { _, err = w.Write(djsonHeader4) } return err } func writeDJSONMeta(w io.Writer, z *ast.ZettelNode) error { _, err := w.Write(djsonMetaHeader) if err == nil { err = writeMeta(w, z.InhMeta, "djson", nil) } return err } func writeDJSONContent( ctx context.Context, w io.Writer, z *ast.ZettelNode, part, defPart partType, getMeta usecase.GetMeta, ) (err error) { _, err = w.Write(djsonContentHeader) if err == nil { err = writeContent(w, z, "djson", &encoder.Environment{ LinkAdapter: adapter.MakeLinkAdapter(ctx, 'z', getMeta, part.DefString(defPart), "djson"), ImageAdapter: adapter.MakeImageAdapter()}) } return err } var ( jsonListHeader = []byte("{\"list\":[") jsonListSep = []byte{','} jsonListFooter = []byte("]}") ) var setJSON = map[string]bool{"json": true} func renderListMetaXJSON( ctx context.Context, w http.ResponseWriter, metaList []*meta.Meta, format string, part, defPart partType, getMeta usecase.GetMeta, parseZettel usecase.ParseZettel, ) { var readZettel bool switch part { case partZettel, partContent: readZettel = true case partMeta, partID: readZettel = false default: adapter.BadRequest(w, "Unknown _part parameter") return } isJSON := setJSON[format] _, err := w.Write(jsonListHeader) for i, m := range metaList { if err != nil { break } if i > 0 { _, err = w.Write(jsonListSep) } if err != nil { break } var zn *ast.ZettelNode if readZettel { z, err1 := parseZettel.Run(ctx, m.Zid, "") if err1 != nil { err = err1 break } zn = z } else { zn = &ast.ZettelNode{ Meta: m, Content: "", Zid: m.Zid, InhMeta: runtime.AddDefaultValues(m), Ast: nil, } } if isJSON { err = writeJSONZettel(w, zn, part) } else { err = writeDJSONZettel(ctx, w, zn, part, defPart, getMeta) } } if err == nil { _, err = w.Write(jsonListFooter) } if err != nil { adapter.InternalServerError(w, "Get list", err) } } func writeContent( w io.Writer, zn *ast.ZettelNode, format string, env *encoder.Environment) error { enc := encoder.Create(format, env) if enc == nil { return adapter.ErrNoSuchFormat } _, err := enc.WriteContent(w, zn) return err } func writeMeta( w io.Writer, m *meta.Meta, format string, env *encoder.Environment) error { enc := encoder.Create(format, env) if enc == nil { return adapter.ErrNoSuchFormat } _, err := enc.WriteMeta(w, m) return err } func encodeJSONData(w http.ResponseWriter, data interface{}, addHeader bool) error { w.Header().Set(adapter.ContentType, format2ContentType("json")) enc := json.NewEncoder(w) enc.SetEscapeHTML(false) return enc.Encode(data) } func writeMetaList(w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error { outData := jsonMetaList{ ID: m.Zid.String(), URL: adapter.NewURLBuilder('z').SetZid(m.Zid).String(), Meta: m.Map(), List: make([]jsonMeta, len(metaList)), } for i, m := range metaList { outData.List[i].ID = m.Zid.String() outData.List[i].URL = adapter.NewURLBuilder('z').SetZid(m.Zid).String() outData.List[i].Meta = m.Map() } return encodeJSONData(w, outData, true) } |
Changes to web/adapter/api/login.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package api import ( "encoding/json" "net/http" "time" | > | > | | | > > > | > > > | > > | | | | | < | | | | | | | | | | > > | > > | > > | | < < > | | | | | | | | > | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | package api import ( "encoding/json" "net/http" "time" "zettelstore.de/z/auth/token" "zettelstore.de/z/config/startup" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakePostLoginHandlerAPI creates a new HTTP handler to authenticate the given user via API. func MakePostLoginHandlerAPI(auth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !startup.WithAuth() { w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) return } _, apiDur := startup.TokenLifetime() authenticateViaJSON(auth, w, r, apiDur) } } func authenticateViaJSON( auth usecase.Authenticate, w http.ResponseWriter, r *http.Request, authDuration time.Duration, ) { token, err := authenticateForJSON(auth, w, r, authDuration) if err != nil { adapter.ReportUsecaseError(w, err) return } if token == nil { w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, string(token), authDuration) } func authenticateForJSON( auth usecase.Authenticate, w http.ResponseWriter, r *http.Request, authDuration time.Duration, ) ([]byte, error) { ident, cred, ok := adapter.GetCredentialsViaForm(r) if !ok { if ident, cred, ok = r.BasicAuth(); !ok { return nil, nil } } token, err := auth.Run(r.Context(), ident, cred, authDuration, token.KindJSON) return token, err } func writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) { je := json.NewEncoder(w) je.Encode(struct { Token string `json:"access_token"` Type string `json:"token_type"` Expires int `json:"expires_in"` }{ Token: token, Type: "Bearer", Expires: int(lifetime / time.Second), }) } // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. func MakeRenewAuthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() auth := session.GetAuthData(ctx) if auth == nil || auth.Token == nil || auth.User == nil { adapter.BadRequest(w, "Not authenticated") return } totalLifetime := auth.Expires.Sub(auth.Issued) currentLifetime := auth.Now.Sub(auth.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, string(auth.Token), totalLifetime-currentLifetime) return } // Toke is a little bit aged. Create a new one _, apiDur := startup.TokenLifetime() token, err := token.GetToken(auth.User, apiDur, token.KindJSON) if err != nil { adapter.ReportUsecaseError(w, err) return } w.Header().Set(adapter.ContentType, format2ContentType("json")) writeJSONToken(w, string(token), apiDur) } } |
Changes to web/adapter/encoding.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 | import ( "context" "errors" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" | > | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import ( "context" "errors" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/place" "zettelstore.de/z/usecase" ) // ErrNoSuchFormat signals an unsupported encoding format var ErrNoSuchFormat = errors.New("no such format") // FormatInlines returns a string representation of the inline slice. func FormatInlines(is ast.InlineSlice, format string, env *encoder.Environment) (string, error) { |
︙ | ︙ | |||
41 42 43 44 45 46 47 | } return content.String(), nil } // MakeLinkAdapter creates an adapter to change a link node during encoding. func MakeLinkAdapter( ctx context.Context, | < < | | | < < < < < < < < < < < | > > > > > > > > > > > > > > | | | < < < | | | | < < < < < < < < < < < < | | > | | | > | 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | } return content.String(), nil } // MakeLinkAdapter creates an adapter to change a link node during encoding. func MakeLinkAdapter( ctx context.Context, key byte, getMeta usecase.GetMeta, part, format string, ) func(*ast.LinkNode) ast.InlineNode { return func(origLink *ast.LinkNode) ast.InlineNode { origRef := origLink.Ref if origRef == nil { return origLink } if origRef.State == ast.RefStateBased { newLink := *origLink newRef := ast.ParseReference(startup.URLPrefix() + origRef.Value[1:]) newRef.State = ast.RefStateHosted newLink.Ref = newRef return &newLink } if origRef.State != ast.RefStateZettel { return origLink } zid, err := id.Parse(origRef.URL.Path) if err != nil { panic(err) } _, err = getMeta.Run(index.NoEnrichContext(ctx), zid) if place.IsErrNotAllowed(err) { return &ast.FormatNode{ Code: ast.FormatSpan, Attrs: origLink.Attrs, Inlines: origLink.Inlines, } } var newRef *ast.Reference if err == nil { newRef = ast.ParseReference(adaptZettelReference(key, zid, part, format, origRef.URL.EscapedFragment())) newRef.State = ast.RefStateFound } else { newRef = ast.ParseReference(origRef.Value) newRef.State = ast.RefStateBroken } newLink := *origLink newLink.Ref = newRef return &newLink } } func adaptZettelReference(key byte, zid id.Zid, part, format, fragment string) string { u := NewURLBuilder(key).SetZid(zid) if part != "" { u.AppendQuery("_part", part) } if format != "" { u.AppendQuery("_format", format) } if fragment != "" { u.SetFragment(fragment) } return u.String() } // MakeImageAdapter creates an adapter to change an image node during encoding. func MakeImageAdapter() func(*ast.ImageNode) ast.InlineNode { return func(origImage *ast.ImageNode) ast.InlineNode { if origImage.Ref == nil || origImage.Ref.State != ast.RefStateZettel { return origImage } newImage := *origImage zid, err := id.Parse(newImage.Ref.Value) if err != nil { panic(err) } newImage.Ref = ast.ParseReference( NewURLBuilder('z').SetZid(zid).AppendQuery("_part", "content").AppendQuery( "_format", "raw").String()) newImage.Ref.State = ast.RefStateFound return &newImage } } |
Changes to web/adapter/request.go.
︙ | ︙ | |||
132 133 134 135 136 137 138 | func getQueryKeys(forSearch bool) (string, string, string, string, string, string) { if forSearch { return "sort", "order", "offset", "limit", "negate", "s" } return "_sort", "_order", "_offset", "_limit", "_negate", "_s" } | | > > > > | | > | | 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | func getQueryKeys(forSearch bool) (string, string, string, string, string, string) { if forSearch { return "sort", "order", "offset", "limit", "negate", "s" } return "_sort", "_order", "_offset", "_limit", "_negate", "_s" } func setCleanedQueryValues(filter *search.Search, key string, values []string) *search.Search { for _, val := range values { val = strings.TrimSpace(val) if len(val) > 0 && val[0] == '!' { filter = filter.AddExpr(key, val[1:], true) } else { filter = filter.AddExpr(key, val, false) } } return filter } |
Changes to web/adapter/response.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( | < | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( "fmt" "log" "net/http" "zettelstore.de/z/place" "zettelstore.de/z/usecase" ) |
︙ | ︙ | |||
45 46 47 48 49 50 51 | if err == place.ErrNotFound { return http.StatusNotFound, http.StatusText(http.StatusNotFound) } if err1, ok := err.(*place.ErrNotAllowed); ok { return http.StatusForbidden, err1.Error() } if err1, ok := err.(*place.ErrInvalidID); ok { | | | | | | | | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | if err == place.ErrNotFound { return http.StatusNotFound, http.StatusText(http.StatusNotFound) } if err1, ok := err.(*place.ErrNotAllowed); ok { return http.StatusForbidden, err1.Error() } if err1, ok := err.(*place.ErrInvalidID); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context.", err1.Zid) } if err1, ok := err.(*usecase.ErrZidInUse); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use.", err1.Zid) } if err1, ok := err.(*ErrBadRequest); ok { return http.StatusBadRequest, err1.Text } if err == place.ErrStopped { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v.", err) } if err == place.ErrTimeout { return http.StatusLoopDetected, "Zettelstore operation took too long" } return http.StatusInternalServerError, err.Error() } |
Added web/adapter/urlbuilder.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package adapter provides handlers for web requests. package adapter import ( "net/url" "strings" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" ) type urlQuery struct{ key, val string } // URLBuilder should be used to create zettelstore URLs. type URLBuilder struct { key byte path []string query []urlQuery fragment string } // NewURLBuilder creates a new URLBuilder. func NewURLBuilder(key byte) *URLBuilder { return &URLBuilder{key: key} } // Clone an URLBuilder func (ub *URLBuilder) Clone() *URLBuilder { cpy := new(URLBuilder) cpy.key = ub.key if len(ub.path) > 0 { cpy.path = make([]string, 0, len(ub.path)) cpy.path = append(cpy.path, ub.path...) } if len(ub.query) > 0 { cpy.query = make([]urlQuery, 0, len(ub.query)) cpy.query = append(cpy.query, ub.query...) } cpy.fragment = ub.fragment return cpy } // SetZid sets the zettel identifier. func (ub *URLBuilder) SetZid(zid id.Zid) *URLBuilder { if len(ub.path) > 0 { panic("Cannot add Zid") } ub.path = append(ub.path, zid.String()) return ub } // AppendPath adds a new path element func (ub *URLBuilder) AppendPath(p string) *URLBuilder { ub.path = append(ub.path, p) return ub } // AppendQuery adds a new query parameter func (ub *URLBuilder) AppendQuery(key, value string) *URLBuilder { ub.query = append(ub.query, urlQuery{key, value}) return ub } // ClearQuery removes all query parameters. func (ub *URLBuilder) ClearQuery() *URLBuilder { ub.query = nil ub.fragment = "" return ub } // SetFragment stores the fragment func (ub *URLBuilder) SetFragment(s string) *URLBuilder { ub.fragment = s return ub } // String produces a string value. func (ub *URLBuilder) String() string { var sb strings.Builder sb.WriteString(startup.URLPrefix()) if ub.key != '/' { sb.WriteByte(ub.key) } for _, p := range ub.path { sb.WriteByte('/') sb.WriteString(url.PathEscape(p)) } if len(ub.fragment) > 0 { sb.WriteByte('#') sb.WriteString(ub.fragment) } for i, q := range ub.query { if i == 0 { sb.WriteByte('?') } else { sb.WriteByte('&') } sb.WriteString(q.key) sb.WriteByte('=') sb.WriteString(url.QueryEscape(q.val)) } return sb.String() } |
Changes to web/adapter/webui/create_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package webui import ( "context" "fmt" "net/http" | | > > > > > | > | | > > > | > | | > > | > > | | | | | | | | > | | | | | | | | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | package webui import ( "context" "fmt" "net/http" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeGetCopyZettelHandler creates a new HTTP handler to display the // HTML edit view of a copied zettel. func MakeGetCopyZettelHandler( te *TemplateEngine, getZettel usecase.GetZettel, copyZettel usecase.CopyZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() origZettel, err := getOrigZettel(ctx, w, r, getZettel, "Copy") if err != nil { te.reportError(ctx, w, err) return } renderZettelForm(w, r, te, copyZettel.Run(origZettel), "Copy Zettel", "Copy Zettel") } } // MakeGetFolgeZettelHandler creates a new HTTP handler to display the // HTML edit view of a follow-up zettel. func MakeGetFolgeZettelHandler( te *TemplateEngine, getZettel usecase.GetZettel, folgeZettel usecase.FolgeZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() origZettel, err := getOrigZettel(ctx, w, r, getZettel, "Folge") if err != nil { te.reportError(ctx, w, err) return } renderZettelForm(w, r, te, folgeZettel.Run(origZettel), "Folge Zettel", "Folgezettel") } } // MakeGetNewZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. func MakeGetNewZettelHandler( te *TemplateEngine, getZettel usecase.GetZettel, newZettel usecase.NewZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() origZettel, err := getOrigZettel(ctx, w, r, getZettel, "New") if err != nil { te.reportError(ctx, w, err) return } m := origZettel.Meta title := parser.ParseInlines(input.NewInput(runtime.GetTitle(m)), meta.ValueSyntaxZmk) textTitle, err := adapter.FormatInlines(title, "text", nil) if err != nil { te.reportError(ctx, w, err) return } env := encoder.Environment{Lang: runtime.GetLang(m)} htmlTitle, err := adapter.FormatInlines(title, "html", &env) if err != nil { te.reportError(ctx, w, err) return } renderZettelForm(w, r, te, newZettel.Run(origZettel), textTitle, htmlTitle) } } func getOrigZettel( ctx context.Context, w http.ResponseWriter, r *http.Request, getZettel usecase.GetZettel, op string, ) (domain.Zettel, error) { if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { return domain.Zettel{}, adapter.NewErrBadRequest( fmt.Sprintf("%v zettel not possible in format %q", op, format)) } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { return domain.Zettel{}, place.ErrNotFound } origZettel, err := getZettel.Run(index.NoEnrichContext(ctx), zid) if err != nil { return domain.Zettel{}, place.ErrNotFound } return origZettel, nil } func renderZettelForm( w http.ResponseWriter, r *http.Request, te *TemplateEngine, zettel domain.Zettel, title, heading string, ) { ctx := r.Context() user := session.GetUser(ctx) m := zettel.Meta var base baseData te.makeBaseData(ctx, runtime.GetLang(m), title, user, &base) te.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: heading, MetaTitle: runtime.GetTitle(m), MetaTags: m.GetDefault(meta.KeyTags, ""), MetaRole: runtime.GetRole(m), MetaSyntax: runtime.GetSyntax(m), MetaPairsRest: m.PairsRest(false), IsTextContent: !zettel.Content.IsBinary(), Content: zettel.Content.AsString(), }) } // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func MakePostCreateZettelHandler(te *TemplateEngine, createZettel usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zettel, hasContent, err := parseZettelForm(r, id.Invalid) if err != nil { te.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read form data")) return } if !hasContent { te.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing")) return } newZid, err := createZettel.Run(r.Context(), zettel) if err != nil { te.reportError(ctx, w, err) return } redirectFound(w, r, adapter.NewURLBuilder('h').SetZid(newZid)) } } |
Changes to web/adapter/webui/delete_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" | | > | > > > | | | | | | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func MakeGetDeleteZettelHandler( te *TemplateEngine, getZettel usecase.GetZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { te.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Delete zettel not possible in format %q", format))) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { te.reportError(ctx, w, place.ErrNotFound) return } zettel, err := getZettel.Run(ctx, zid) if err != nil { te.reportError(ctx, w, err) return } user := session.GetUser(ctx) m := zettel.Meta var base baseData te.makeBaseData(ctx, runtime.GetLang(m), "Delete Zettel "+m.Zid.String(), user, &base) te.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair }{ Zid: zid.String(), MetaPairs: m.Pairs(true), }) } } // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. func MakePostDeleteZettelHandler(te *TemplateEngine, deleteZettel usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { te.reportError(ctx, w, place.ErrNotFound) return } if err := deleteZettel.Run(r.Context(), zid); err != nil { te.reportError(ctx, w, err) return } redirectFound(w, r, adapter.NewURLBuilder('/')) } } |
Changes to web/adapter/webui/edit_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" | | > > > | | | | | | | | | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | // Package webui provides web-UI handlers for web requests. package webui import ( "fmt" "net/http" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. func MakeEditGetZettelHandler( te *TemplateEngine, getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { te.reportError(ctx, w, place.ErrNotFound) return } zettel, err := getZettel.Run(index.NoEnrichContext(ctx), zid) if err != nil { te.reportError(ctx, w, err) return } if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { te.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Edit zettel %q not possible in format %q", zid, format))) return } user := session.GetUser(ctx) m := zettel.Meta var base baseData te.makeBaseData(ctx, runtime.GetLang(m), "Edit Zettel", user, &base) te.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: base.Title, MetaTitle: m.GetDefault(meta.KeyTitle, ""), MetaRole: m.GetDefault(meta.KeyRole, ""), MetaTags: m.GetDefault(meta.KeyTags, ""), MetaSyntax: m.GetDefault(meta.KeySyntax, ""), MetaPairsRest: m.PairsRest(false), IsTextContent: !zettel.Content.IsBinary(), Content: zettel.Content.AsString(), }) } } // MakeEditSetZettelHandler creates a new HTTP handler to store content of // an existing zettel. func MakeEditSetZettelHandler(te *TemplateEngine, updateZettel usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { te.reportError(ctx, w, place.ErrNotFound) return } zettel, hasContent, err := parseZettelForm(r, zid) if err != nil { te.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form")) return } if err := updateZettel.Run(r.Context(), zettel, hasContent); err != nil { te.reportError(ctx, w, err) return } redirectFound(w, r, adapter.NewURLBuilder('h').SetZid(zid)) } } |
Changes to web/adapter/webui/get_info.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "fmt" "net/http" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/collect" | | > > > | > > | | | | | | | > | | | | | | | | | | | | | | | < < < < < | < | | > < | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | import ( "fmt" "net/http" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/collect" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) type metaDataInfo struct { Key string Value string } type matrixElement struct { Text string HasURL bool URL string } type matrixLine struct { Elements []matrixElement } // MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". func MakeGetInfoHandler( te *TemplateEngine, parseZettel usecase.ParseZettel, getMeta usecase.GetMeta, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() if format := adapter.GetFormat(r, q, "html"); format != "html" { te.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Zettel info not available in format %q", format))) return } zid, err := id.Parse(r.URL.Path[1:]) if err != nil { te.reportError(ctx, w, place.ErrNotFound) return } zn, err := parseZettel.Run(ctx, zid, q.Get("syntax")) if err != nil { te.reportError(ctx, w, err) return } summary := collect.References(zn) locLinks, extLinks := splitLocExtLinks(append(summary.Links, summary.Images...)) lang := runtime.GetLang(zn.InhMeta) env := encoder.Environment{Lang: lang} pairs := zn.Meta.Pairs(true) metaData := make([]metaDataInfo, len(pairs)) getTitle := makeGetTitle(ctx, getMeta, &env) for i, p := range pairs { var html strings.Builder writeHTMLMetaValue(&html, zn.Meta, p.Key, getTitle, &env) metaData[i] = metaDataInfo{p.Key, html.String()} } endnotes, err := formatBlocks(nil, "html", &env) if err != nil { endnotes = "" } textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle) user := session.GetUser(ctx) var base baseData te.makeBaseData(ctx, lang, textTitle, user, &base) canCopy := base.CanCreate && !zn.Content.IsBinary() te.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { Zid string WebURL string ContextURL string CanWrite bool EditURL string CanFolge bool FolgeURL string CanCopy bool CopyURL string CanRename bool RenameURL string CanDelete bool DeleteURL string MetaData []metaDataInfo HasLinks bool HasLocLinks bool LocLinks []string HasExtLinks bool ExtLinks []string ExtNewWindow string Matrix []matrixLine Endnotes string }{ Zid: zid.String(), WebURL: adapter.NewURLBuilder('h').SetZid(zid).String(), ContextURL: adapter.NewURLBuilder('j').SetZid(zid).String(), CanWrite: te.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: adapter.NewURLBuilder('e').SetZid(zid).String(), CanFolge: base.CanCreate && !zn.Content.IsBinary(), FolgeURL: adapter.NewURLBuilder('f').SetZid(zid).String(), CanCopy: canCopy, CopyURL: adapter.NewURLBuilder('c').SetZid(zid).String(), CanRename: te.canRename(ctx, user, zn.Meta), RenameURL: adapter.NewURLBuilder('b').SetZid(zid).String(), CanDelete: te.canDelete(ctx, user, zn.Meta), DeleteURL: adapter.NewURLBuilder('d').SetZid(zid).String(), MetaData: metaData, HasLinks: len(extLinks)+len(locLinks) > 0, HasLocLinks: len(locLinks) > 0, LocLinks: locLinks, HasExtLinks: len(extLinks) > 0, ExtLinks: extLinks, ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0), Matrix: infoAPIMatrix(zid), Endnotes: endnotes, }) } } func splitLocExtLinks(links []*ast.Reference) (locLinks, extLinks []string) { if len(links) == 0 { return nil, nil } for _, ref := range links { if ref.State == ast.RefStateSelf { continue } if ref.IsZettel() { continue } else if ref.IsExternal() { extLinks = append(extLinks, ref.String()) } else { locLinks = append(locLinks, ref.String()) } } return locLinks, extLinks } func infoAPIMatrix(zid id.Zid) []matrixLine { formats := encoder.GetFormats() defFormat := encoder.GetDefaultFormat() parts := []string{"zettel", "meta", "content"} matrix := make([]matrixLine, 0, len(parts)) u := adapter.NewURLBuilder('z').SetZid(zid) for _, part := range parts { row := make([]matrixElement, 0, len(formats)+1) row = append(row, matrixElement{part, false, ""}) for _, format := range formats { u.AppendQuery("_part", part) if format != defFormat { u.AppendQuery("_format", format) |
︙ | ︙ |
Changes to web/adapter/webui/get_zettel.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "bytes" "net/http" "strings" "zettelstore.de/z/ast" | | > > > > | | | | | | | | | | | | | | > | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | import ( "bytes" "net/http" "strings" "zettelstore.de/z/ast" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/encfun" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". func MakeGetHTMLZettelHandler( te *TemplateEngine, parseZettel usecase.ParseZettel, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { te.reportError(ctx, w, place.ErrNotFound) return } syntax := r.URL.Query().Get("syntax") zn, err := parseZettel.Run(ctx, zid, syntax) if err != nil { te.reportError(ctx, w, err) return } lang := runtime.GetLang(zn.InhMeta) envHTML := encoder.Environment{ LinkAdapter: adapter.MakeLinkAdapter(ctx, 'h', getMeta, "", ""), ImageAdapter: adapter.MakeImageAdapter(), CiteAdapter: nil, Lang: lang, Xhtml: false, MarkerExternal: runtime.GetMarkerExternal(), NewWindow: true, IgnoreMeta: map[string]bool{meta.KeyTitle: true, meta.KeyLang: true}, } metaHeader, err := formatMeta(zn.InhMeta, "html", &envHTML) if err != nil { te.reportError(ctx, w, err) return } htmlTitle, err := adapter.FormatInlines( encfun.MetaAsInlineSlice(zn.InhMeta, meta.KeyTitle), "html", &envHTML) if err != nil { te.reportError(ctx, w, err) return } htmlContent, err := formatBlocks(zn.Ast, "html", &envHTML) if err != nil { te.reportError(ctx, w, err) return } textTitle := encfun.MetaAsText(zn.InhMeta, meta.KeyTitle) user := session.GetUser(ctx) roleText := zn.Meta.GetDefault(meta.KeyRole, "*") tags := buildTagInfos(zn.Meta) getTitle := makeGetTitle(ctx, getMeta, &encoder.Environment{Lang: lang}) extURL, hasExtURL := zn.Meta.Get(meta.KeyURL) backLinks := formatBackLinks(zn.InhMeta, getTitle) var base baseData te.makeBaseData(ctx, lang, textTitle, user, &base) base.MetaHeader = metaHeader canCopy := base.CanCreate && !zn.Content.IsBinary() te.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct { HTMLTitle string CanWrite bool EditURL string Zid string InfoURL string RoleText string RoleURL string |
︙ | ︙ | |||
101 102 103 104 105 106 107 | ExtURL string ExtNewWindow string Content string HasBackLinks bool BackLinks []simpleLink }{ HTMLTitle: htmlTitle, | | | | | | | | | | | | 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | ExtURL string ExtNewWindow string Content string HasBackLinks bool BackLinks []simpleLink }{ HTMLTitle: htmlTitle, CanWrite: te.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: adapter.NewURLBuilder('e').SetZid(zid).String(), Zid: zid.String(), InfoURL: adapter.NewURLBuilder('i').SetZid(zid).String(), RoleText: roleText, RoleURL: adapter.NewURLBuilder('h').AppendQuery("role", roleText).String(), HasTags: len(tags) > 0, Tags: tags, CanCopy: canCopy, CopyURL: adapter.NewURLBuilder('c').SetZid(zid).String(), CanFolge: base.CanCreate && !zn.Content.IsBinary(), FolgeURL: adapter.NewURLBuilder('f').SetZid(zid).String(), FolgeRefs: formatMetaKey(zn.InhMeta, meta.KeyFolge, getTitle), PrecursorRefs: formatMetaKey(zn.InhMeta, meta.KeyPrecursor, getTitle), ExtURL: extURL, HasExtURL: hasExtURL, ExtNewWindow: htmlAttrNewWindow(envHTML.NewWindow && hasExtURL), Content: htmlContent, HasBackLinks: len(backLinks) > 0, BackLinks: backLinks, }) |
︙ | ︙ | |||
153 154 155 156 157 158 159 | _, err := enc.WriteMeta(&content, m) if err != nil { return "", err } return content.String(), nil } | | | | | | | | 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | _, err := enc.WriteMeta(&content, m) if err != nil { return "", err } return content.String(), nil } func buildTagInfos(m *meta.Meta) []simpleLink { var tagInfos []simpleLink if tags, ok := m.GetList(meta.KeyTags); ok { ub := adapter.NewURLBuilder('h') tagInfos = make([]simpleLink, len(tags)) for i, tag := range tags { tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery("tags", tag).String()} ub.ClearQuery() } } return tagInfos } func formatMetaKey(m *meta.Meta, key string, getTitle getTitleFunc) string { if _, ok := m.Get(key); ok { var buf bytes.Buffer writeHTMLMetaValue(&buf, m, key, getTitle, nil) return buf.String() } return "" } func formatBackLinks(m *meta.Meta, getTitle getTitleFunc) []simpleLink { values, ok := m.GetList(meta.KeyBack) if !ok || len(values) == 0 { return nil } result := make([]simpleLink, 0, len(values)) for _, val := range values { zid, err := id.Parse(val) if err != nil { continue } if title, found := getTitle(zid, "text"); found > 0 { url := adapter.NewURLBuilder('h').SetZid(zid).String() if title == "" { result = append(result, simpleLink{Text: val, URL: url}) } else { result = append(result, simpleLink{Text: title, URL: url}) } } } return result } |
Changes to web/adapter/webui/home.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" | < > > > > | | | | | | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "net/http" "zettelstore.de/z/config/runtime" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) type getRootStore interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // MakeGetRootHandler creates a new HTTP handler to show the root URL. func MakeGetRootHandler(te *TemplateEngine, s getRootStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if r.URL.Path != "/" { te.reportError(ctx, w, place.ErrNotFound) return } homeZid := runtime.GetHomeZettel() if homeZid != id.DefaultHomeZid { if _, err := s.GetMeta(ctx, homeZid); err == nil { redirectFound(w, r, adapter.NewURLBuilder('h').SetZid(homeZid)) return } homeZid = id.DefaultHomeZid } _, err := s.GetMeta(ctx, homeZid) if err == nil { redirectFound(w, r, adapter.NewURLBuilder('h').SetZid(homeZid)) return } if place.IsErrNotAllowed(err) && startup.WithAuth() && session.GetUser(ctx) == nil { redirectFound(w, r, adapter.NewURLBuilder('a')) return } redirectFound(w, r, adapter.NewURLBuilder('h')) } } |
Changes to web/adapter/webui/htmlmeta.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" | < > | | | | | | | | | | | > > | > > | > > | | | | | | | | | > > | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "fmt" "io" "net/url" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) var space = []byte{' '} func writeHTMLMetaValue(w io.Writer, m *meta.Meta, key string, getTitle getTitleFunc, env *encoder.Environment) { switch kt := m.Type(key); kt { case meta.TypeBool: writeHTMLBool(w, key, m.GetBool(key)) case meta.TypeCredential: writeCredential(w, m.GetDefault(key, "???c")) case meta.TypeEmpty: writeEmpty(w, m.GetDefault(key, "???e")) case meta.TypeID: writeIdentifier(w, m.GetDefault(key, "???i"), getTitle) case meta.TypeIDSet: if l, ok := m.GetList(key); ok { writeIdentifierSet(w, l, getTitle) } case meta.TypeNumber: writeNumber(w, m.GetDefault(key, "???n")) case meta.TypeString: writeString(w, m.GetDefault(key, "???s")) case meta.TypeTagSet: if l, ok := m.GetList(key); ok { writeTagSet(w, key, l) } case meta.TypeTimestamp: if ts, ok := m.GetTime(key); ok { writeTimestamp(w, ts) } case meta.TypeURL: writeURL(w, m.GetDefault(key, "???u")) case meta.TypeWord: writeWord(w, key, m.GetDefault(key, "???w")) case meta.TypeWordSet: if l, ok := m.GetList(key); ok { writeWordSet(w, key, l) } case meta.TypeZettelmarkup: writeZettelmarkup(w, m.GetDefault(key, "???z"), env) case meta.TypeUnknown: writeUnknown(w, m.GetDefault(key, "???u")) default: strfun.HTMLEscape(w, m.GetDefault(key, "???w"), false) fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key) } } func writeHTMLBool(w io.Writer, key string, val bool) { if val { writeLink(w, key, "true", "True") } else { writeLink(w, key, "false", "False") } } func writeCredential(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func writeEmpty(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func writeIdentifier(w io.Writer, val string, getTitle func(id.Zid, string) (string, int)) { zid, err := id.Parse(val) if err != nil { strfun.HTMLEscape(w, val, false) return } title, found := getTitle(zid, "text") switch { case found > 0: if title == "" { fmt.Fprintf( w, "<a href=\"%v\">%v</a>", adapter.NewURLBuilder('h').SetZid(zid), zid, ) } else { fmt.Fprintf( w, "<a href=\"%v\" title=\"%v\">%v</a>", adapter.NewURLBuilder('h').SetZid(zid), title, zid, ) } case found == 0: fmt.Fprintf(w, "<s>%v</s>", val) case found < 0: io.WriteString(w, val) } } func writeIdentifierSet(w io.Writer, vals []string, getTitle func(id.Zid, string) (string, int)) { for i, val := range vals { if i > 0 { w.Write(space) } writeIdentifier(w, val, getTitle) } } func writeNumber(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func writeString(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func writeUnknown(w io.Writer, val string) { strfun.HTMLEscape(w, val, false) } func writeTagSet(w io.Writer, key string, tags []string) { for i, tag := range tags { if i > 0 { w.Write(space) } writeLink(w, key, tag, tag) } } func writeTimestamp(w io.Writer, ts time.Time) { io.WriteString(w, ts.Format("2006-01-02 15:04:05")) } func writeURL(w io.Writer, val string) { u, err := url.Parse(val) if err != nil { strfun.HTMLEscape(w, val, false) return } fmt.Fprintf(w, "<a href=\"%v\">", u) strfun.HTMLEscape(w, val, false) io.WriteString(w, "</a>") } func writeWord(w io.Writer, key, word string) { writeLink(w, key, word, word) } func writeWordSet(w io.Writer, key string, words []string) { for i, word := range words { if i > 0 { w.Write(space) } writeWord(w, key, word) } } func writeZettelmarkup(w io.Writer, val string, env *encoder.Environment) { title, err := adapter.FormatInlines(parser.ParseMetadata(val), "html", env) if err != nil { strfun.HTMLEscape(w, val, false) return } io.WriteString(w, title) } func writeLink(w io.Writer, key, value, text string) { fmt.Fprintf( w, "<a href=\"%v?%v=%v\">", adapter.NewURLBuilder('h'), url.QueryEscape(key), url.QueryEscape(value)) strfun.HTMLEscape(w, text, false) io.WriteString(w, "</a>") } type getTitleFunc func(id.Zid, string) (string, int) func makeGetTitle(ctx context.Context, getMeta usecase.GetMeta, env *encoder.Environment) getTitleFunc { return func(zid id.Zid, format string) (string, int) { m, err := getMeta.Run(index.NoEnrichContext(ctx), zid) if err != nil { if place.IsErrNotAllowed(err) { return "", -1 } return "", 0 } astTitle := parser.ParseMetadata(m.GetDefault(meta.KeyTitle, "")) title, err := adapter.FormatInlines(astTitle, format, env) if err == nil { return title, 1 } return "", 1 } } |
Changes to web/adapter/webui/lists.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | "context" "net/http" "net/url" "sort" "strconv" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/search" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of // zettel as HTML. | > > > | > | | | > | | | < | | | > > > > | > | | | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | "context" "net/http" "net/url" "sort" "strconv" "strings" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/search" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of // zettel as HTML. func MakeListHTMLMetaHandler( te *TemplateEngine, listMeta usecase.ListMeta, listRole usecase.ListRole, listTags usecase.ListTags, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() switch query.Get("_l") { case "r": renderWebUIRolesList(w, r, te, listRole) case "t": renderWebUITagsList(w, r, te, listTags) default: renderWebUIZettelList(w, r, te, listMeta) } } } func renderWebUIZettelList( w http.ResponseWriter, r *http.Request, te *TemplateEngine, listMeta usecase.ListMeta) { query := r.URL.Query() s := adapter.GetSearch(query, false) ctx := r.Context() title := listTitleSearch("Filter", s) renderWebUIMetaList( ctx, w, te, title, s, func(s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { ctx = index.NoEnrichContext(ctx) } return listMeta.Run(ctx, s) }, func(offset int) string { return newPageURL('h', query, offset, "_offset", "_limit") }) } type roleInfo struct { Text string URL string } func renderWebUIRolesList( w http.ResponseWriter, r *http.Request, te *TemplateEngine, listRole usecase.ListRole, ) { ctx := r.Context() roleList, err := listRole.Run(ctx) if err != nil { adapter.ReportUsecaseError(w, err) return } roleInfos := make([]roleInfo, 0, len(roleList)) for _, r := range roleList { roleInfos = append( roleInfos, roleInfo{r, adapter.NewURLBuilder('h').AppendQuery("role", r).String()}) } user := session.GetUser(ctx) var base baseData te.makeBaseData(ctx, runtime.GetDefaultLang(), runtime.GetSiteName(), user, &base) te.renderTemplate(ctx, w, id.RolesTemplateZid, &base, struct { Roles []roleInfo }{ Roles: roleInfos, }) } type countInfo struct { |
︙ | ︙ | |||
108 109 110 111 112 113 114 | count int Count string Size string } var fontSizes = [...]int{75, 83, 100, 117, 150, 200} | > > > > | > | | | | 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | count int Count string Size string } var fontSizes = [...]int{75, 83, 100, 117, 150, 200} func renderWebUITagsList( w http.ResponseWriter, r *http.Request, te *TemplateEngine, listTags usecase.ListTags, ) { ctx := r.Context() iMinCount, _ := strconv.Atoi(r.URL.Query().Get("min")) tagData, err := listTags.Run(ctx, iMinCount) if err != nil { te.reportError(ctx, w, err) return } user := session.GetUser(ctx) tagsList := make([]tagInfo, 0, len(tagData)) countMap := make(map[int]int) baseTagListURL := adapter.NewURLBuilder('h') for tag, ml := range tagData { count := len(ml) countMap[count]++ tagsList = append( tagsList, tagInfo{tag, baseTagListURL.AppendQuery("tags", tag).String(), count, "", ""}) baseTagListURL.ClearQuery() |
︙ | ︙ | |||
146 147 148 149 150 151 152 | for i := 0; i < len(tagsList); i++ { count := tagsList[i].count tagsList[i].Count = strconv.Itoa(count) tagsList[i].Size = strconv.Itoa(countMap[count]) } var base baseData | | | | > < | > | | | | | | | | | | | | | | | | < | | | | | | | | | | | | | | | | | | | 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 | for i := 0; i < len(tagsList); i++ { count := tagsList[i].count tagsList[i].Count = strconv.Itoa(count) tagsList[i].Size = strconv.Itoa(countMap[count]) } var base baseData te.makeBaseData(ctx, runtime.GetDefaultLang(), runtime.GetSiteName(), user, &base) minCounts := make([]countInfo, 0, len(countList)) for _, c := range countList { sCount := strconv.Itoa(c) minCounts = append(minCounts, countInfo{sCount, base.ListTagsURL + "&min=" + sCount}) } te.renderTemplate(ctx, w, id.TagsTemplateZid, &base, struct { ListTagsURL string MinCounts []countInfo Tags []tagInfo }{ ListTagsURL: base.ListTagsURL, MinCounts: minCounts, Tags: tagsList, }) } // MakeSearchHandler creates a new HTTP handler for the use case "search". func MakeSearchHandler( te *TemplateEngine, ucSearch usecase.Search, getMeta usecase.GetMeta, getZettel usecase.GetZettel, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() s := adapter.GetSearch(query, true) if s == nil { redirectFound(w, r, adapter.NewURLBuilder('h')) return } ctx := r.Context() title := listTitleSearch("Search", s) renderWebUIMetaList( ctx, w, te, title, s, func(s *search.Search) ([]*meta.Meta, error) { if !s.HasComputedMetaKey() { ctx = index.NoEnrichContext(ctx) } return ucSearch.Run(ctx, s) }, func(offset int) string { return newPageURL('f', query, offset, "offset", "limit") }) } } // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func MakeZettelContextHandler(te *TemplateEngine, getContext usecase.ZettelContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { te.reportError(ctx, w, place.ErrNotFound) return } q := r.URL.Query() dir := usecase.ParseZCDirection(q.Get("dir")) depth := getIntParameter(q, "depth", 5) limit := getIntParameter(q, "limit", 200) metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { te.reportError(ctx, w, err) return } metaLinks, err := buildHTMLMetaList(metaList) if err != nil { adapter.InternalServerError(w, "Build HTML meta list", err) return } depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"} depthLinks := make([]simpleLink, len(depths)) depthURL := adapter.NewURLBuilder('j').SetZid(zid) for i, depth := range depths { depthURL.ClearQuery() switch dir { case usecase.ZettelContextBackward: depthURL.AppendQuery("dir", "backward") case usecase.ZettelContextForward: depthURL.AppendQuery("dir", "forward") } depthURL.AppendQuery("depth", depth) depthLinks[i].Text = depth depthLinks[i].URL = depthURL.String() } var base baseData user := session.GetUser(ctx) te.makeBaseData(ctx, runtime.GetDefaultLang(), runtime.GetSiteName(), user, &base) te.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct { Title string InfoURL string Depths []simpleLink Start simpleLink Metas []simpleLink }{ Title: "Zettel Context", InfoURL: adapter.NewURLBuilder('i').SetZid(zid).String(), Depths: depthLinks, Start: metaLinks[0], Metas: metaLinks[1:], }) } } func getIntParameter(q url.Values, key string, minValue int) int { val, ok := adapter.GetInteger(q, key) if !ok || val < 0 { return minValue } return val } func renderWebUIMetaList( ctx context.Context, w http.ResponseWriter, te *TemplateEngine, title string, s *search.Search, ucMetaList func(sorter *search.Search) ([]*meta.Meta, error), pageURL func(int) string) { var metaList []*meta.Meta var err error var prevURL, nextURL string if lps := runtime.GetListPageSize(); lps > 0 { if s.GetLimit() < lps { s.SetLimit(lps + 1) } metaList, err = ucMetaList(s) if err != nil { te.reportError(ctx, w, err) return } if offset := s.GetOffset(); offset > 0 { offset -= lps if offset < 0 { offset = 0 } prevURL = pageURL(offset) } if len(metaList) >= s.GetLimit() { nextURL = pageURL(s.GetOffset() + lps) metaList = metaList[:len(metaList)-1] } } else { metaList, err = ucMetaList(s) if err != nil { te.reportError(ctx, w, err) return } } user := session.GetUser(ctx) metas, err := buildHTMLMetaList(metaList) if err != nil { te.reportError(ctx, w, err) return } var base baseData te.makeBaseData(ctx, runtime.GetDefaultLang(), runtime.GetSiteName(), user, &base) te.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { Title string Metas []simpleLink HasPrevNext bool HasPrev bool PrevURL string HasNext bool NextURL string }{ Title: title, Metas: metas, HasPrevNext: len(prevURL) > 0 || len(nextURL) > 0, HasPrev: len(prevURL) > 0, PrevURL: prevURL, HasNext: len(nextURL) > 0, NextURL: nextURL, }) } func listTitleSearch(prefix string, s *search.Search) string { if s == nil { return runtime.GetSiteName() } var sb strings.Builder sb.WriteString(prefix) if s != nil { sb.WriteString(": ") s.Print(&sb) } return sb.String() } func newPageURL(key byte, query url.Values, offset int, offsetKey, limitKey string) string { urlBuilder := adapter.NewURLBuilder(key) for key, values := range query { if key != offsetKey && key != limitKey { for _, val := range values { urlBuilder.AppendQuery(key, val) } } } if offset > 0 { urlBuilder.AppendQuery(offsetKey, strconv.Itoa(offset)) } return urlBuilder.String() } // buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering. func buildHTMLMetaList(metaList []*meta.Meta) ([]simpleLink, error) { defaultLang := runtime.GetDefaultLang() metas := make([]simpleLink, 0, len(metaList)) for _, m := range metaList { var lang string if val, ok := m.Get(meta.KeyLang); ok { lang = val } else { lang = defaultLang } title, _ := m.Get(meta.KeyTitle) env := encoder.Environment{Lang: lang, Interactive: true} htmlTitle, err := adapter.FormatInlines(parser.ParseMetadata(title), "html", &env) if err != nil { return nil, err } metas = append(metas, simpleLink{ Text: htmlTitle, URL: adapter.NewURLBuilder('h').SetZid(m.Zid).String(), }) } return metas, nil } |
Changes to web/adapter/webui/login.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 16 17 | //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "net/http" | > > > | > > | | | | | | | | > > > > > > > > > > > > | | | | | | | | | | | | | | | | | | | < | > > > > > > | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "fmt" "net/http" "time" "zettelstore.de/z/auth/token" "zettelstore.de/z/config/runtime" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeGetLoginHandler creates a new HTTP handler to display the HTML login view. func MakeGetLoginHandler(te *TemplateEngine) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { renderLoginForm(session.ClearToken(r.Context(), w), w, te, false) } } func renderLoginForm(ctx context.Context, w http.ResponseWriter, te *TemplateEngine, retry bool) { var base baseData te.makeBaseData(ctx, runtime.GetDefaultLang(), "Login", nil, &base) te.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct { Title string Retry bool }{ Title: base.Title, Retry: retry, }) } // MakePostLoginHandlerHTML creates a new HTTP handler to authenticate the given user. func MakePostLoginHandlerHTML(te *TemplateEngine, auth usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !startup.WithAuth() { redirectFound(w, r, adapter.NewURLBuilder('/')) return } htmlDur, _ := startup.TokenLifetime() authenticateViaHTML(te, auth, w, r, htmlDur) } } func authenticateViaHTML( te *TemplateEngine, auth usecase.Authenticate, w http.ResponseWriter, r *http.Request, authDuration time.Duration, ) { ctx := r.Context() ident, cred, ok := adapter.GetCredentialsViaForm(r) if !ok { te.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read login form")) return } token, err := auth.Run(ctx, ident, cred, authDuration, token.KindHTML) if err != nil { te.reportError(ctx, w, err) return } if token == nil { renderLoginForm(session.ClearToken(ctx, w), w, te, true) return } session.SetToken(w, token, authDuration) redirectFound(w, r, adapter.NewURLBuilder('/')) } // MakeGetLogoutHandler creates a new HTTP handler to log out the current user func MakeGetLogoutHandler(te *TemplateEngine) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { te.reportError(r.Context(), w, adapter.NewErrBadRequest( fmt.Sprintf("Logout not possible in format %q", format))) return } session.ClearToken(r.Context(), w) redirectFound(w, r, adapter.NewURLBuilder('/')) } } |
Changes to web/adapter/webui/rename_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package webui import ( "fmt" "net/http" "strings" | | > > | | | | | | | | | | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | package webui import ( "fmt" "net/http" "strings" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the // HTML rename view of a zettel. func MakeGetRenameZettelHandler( te *TemplateEngine, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { te.reportError(ctx, w, place.ErrNotFound) return } m, err := getMeta.Run(ctx, zid) if err != nil { te.reportError(ctx, w, err) return } if format := adapter.GetFormat(r, r.URL.Query(), "html"); format != "html" { te.reportError(ctx, w, adapter.NewErrBadRequest( fmt.Sprintf("Rename zettel %q not possible in format %q", zid.String(), format))) return } user := session.GetUser(ctx) var base baseData te.makeBaseData(ctx, runtime.GetLang(m), "Rename Zettel "+zid.String(), user, &base) te.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair }{ Zid: zid.String(), MetaPairs: m.Pairs(true), }) } } // MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel. func MakePostRenameZettelHandler(te *TemplateEngine, renameZettel usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() curZid, err := id.Parse(r.URL.Path[1:]) if err != nil { te.reportError(ctx, w, place.ErrNotFound) return } if err = r.ParseForm(); err != nil { te.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form")) return } if formCurZid, err1 := id.Parse( r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid { te.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form")) return } newZid, err := id.Parse(strings.TrimSpace(r.PostFormValue("newzid"))) if err != nil { te.reportError(ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", newZid))) return } if err := renameZettel.Run(r.Context(), curZid, newZid); err != nil { te.reportError(ctx, w, err) return } redirectFound(w, r, adapter.NewURLBuilder('h').SetZid(newZid)) } } |
Changes to web/adapter/webui/response.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package webui provides web-UI handlers for web requests. package webui import ( "net/http" | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 | // Package webui provides web-UI handlers for web requests. package webui import ( "net/http" "zettelstore.de/z/web/adapter" ) func redirectFound(w http.ResponseWriter, r *http.Request, ub *adapter.URLBuilder) { http.Redirect(w, r, ub.String(), http.StatusFound) } |
Added web/adapter/webui/template.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "bytes" "context" "log" "net/http" "sync" "zettelstore.de/z/auth/policy" "zettelstore.de/z/auth/token" "zettelstore.de/z/collect" "zettelstore.de/z/config/runtime" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/index" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/place/change" "zettelstore.de/z/template" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/session" ) type templatePlace interface { CanCreateZettel(ctx context.Context) bool GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool AllowRenameZettel(ctx context.Context, zid id.Zid) bool CanDeleteZettel(ctx context.Context, zid id.Zid) bool } // TemplateEngine is the way to render HTML templates. type TemplateEngine struct { place templatePlace templateCache map[id.Zid]*template.Template mxCache sync.RWMutex policy policy.Policy stylesheetURL string homeURL string listZettelURL string listRolesURL string listTagsURL string withAuth bool loginURL string searchURL string } // NewTemplateEngine creates a new TemplateEngine. func NewTemplateEngine(mgr place.Manager, pol policy.Policy) *TemplateEngine { te := &TemplateEngine{ place: mgr, policy: pol, stylesheetURL: adapter.NewURLBuilder('z').SetZid( id.BaseCSSZid).AppendQuery("_format", "raw").AppendQuery( "_part", "content").String(), homeURL: adapter.NewURLBuilder('/').String(), listZettelURL: adapter.NewURLBuilder('h').String(), listRolesURL: adapter.NewURLBuilder('h').AppendQuery("_l", "r").String(), listTagsURL: adapter.NewURLBuilder('h').AppendQuery("_l", "t").String(), withAuth: startup.WithAuth(), loginURL: adapter.NewURLBuilder('a').String(), searchURL: adapter.NewURLBuilder('f').String(), } te.observe(change.Info{Reason: change.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(te.observe) return te } func (te *TemplateEngine) observe(ci change.Info) { te.mxCache.Lock() if ci.Reason == change.OnReload || ci.Zid == id.BaseTemplateZid { te.templateCache = make(map[id.Zid]*template.Template, len(te.templateCache)) } else { delete(te.templateCache, ci.Zid) } te.mxCache.Unlock() } func (te *TemplateEngine) cacheSetTemplate(zid id.Zid, t *template.Template) { te.mxCache.Lock() te.templateCache[zid] = t te.mxCache.Unlock() } func (te *TemplateEngine) cacheGetTemplate(zid id.Zid) (*template.Template, bool) { te.mxCache.RLock() t, ok := te.templateCache[zid] te.mxCache.RUnlock() return t, ok } func (te *TemplateEngine) canCreate(ctx context.Context, user *meta.Meta) bool { m := meta.New(id.Invalid) return te.policy.CanCreate(user, m) && te.place.CanCreateZettel(ctx) } func (te *TemplateEngine) canWrite( ctx context.Context, user, meta *meta.Meta, content domain.Content) bool { return te.policy.CanWrite(user, meta, meta) && te.place.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content}) } func (te *TemplateEngine) canRename(ctx context.Context, user, m *meta.Meta) bool { return te.policy.CanRename(user, m) && te.place.AllowRenameZettel(ctx, m.Zid) } func (te *TemplateEngine) canDelete(ctx context.Context, user, m *meta.Meta) bool { return te.policy.CanDelete(user, m) && te.place.CanDeleteZettel(ctx, m.Zid) } func (te *TemplateEngine) getTemplate( ctx context.Context, templateID id.Zid) (*template.Template, error) { if t, ok := te.cacheGetTemplate(templateID); ok { return t, nil } realTemplateZettel, err := te.place.GetZettel(ctx, templateID) if err != nil { return nil, err } t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil) if err == nil { // t.SetErrorOnMissing() te.cacheSetTemplate(templateID, t) } return t, err } type simpleLink struct { Text string URL string } type baseData struct { Lang string MetaHeader string StylesheetURL string Title string HomeURL string WithUser bool WithAuth bool UserIsValid bool UserZettelURL string UserIdent string UserLogoutURL string LoginURL string ListZettelURL string ListRolesURL string ListTagsURL string CanCreate bool NewZettelURL string NewZettelLinks []simpleLink SearchURL string Content string FooterHTML string } func (te *TemplateEngine) makeBaseData( ctx context.Context, lang, title string, user *meta.Meta, data *baseData) { var ( newZettelLinks []simpleLink userZettelURL string userIdent string userLogoutURL string ) canCreate := te.canCreate(ctx, user) if canCreate { newZettelLinks = te.fetchNewTemplates(ctx, user) } userIsValid := user != nil if userIsValid { userZettelURL = adapter.NewURLBuilder('h').SetZid(user.Zid).String() userIdent = user.GetDefault(meta.KeyUserID, "") userLogoutURL = adapter.NewURLBuilder('a').SetZid(user.Zid).String() } data.Lang = lang data.StylesheetURL = te.stylesheetURL data.Title = title data.HomeURL = te.homeURL data.WithAuth = te.withAuth data.WithUser = data.WithAuth data.UserIsValid = userIsValid data.UserZettelURL = userZettelURL data.UserIdent = userIdent data.UserLogoutURL = userLogoutURL data.LoginURL = te.loginURL data.ListZettelURL = te.listZettelURL data.ListRolesURL = te.listRolesURL data.ListTagsURL = te.listTagsURL data.CanCreate = canCreate data.NewZettelLinks = newZettelLinks data.SearchURL = te.searchURL data.FooterHTML = runtime.GetFooterHTML() } // htmlAttrNewWindow eturns HTML attribute string for opening a link in a new window. // If hasURL is false an empty string is returned. func htmlAttrNewWindow(hasURL bool) string { if hasURL { return " target=\"_blank\" ref=\"noopener noreferrer\"" } return "" } func (te *TemplateEngine) fetchNewTemplates(ctx context.Context, user *meta.Meta) []simpleLink { ctx = index.NoEnrichContext(ctx) menu, err := te.place.GetZettel(ctx, id.TOCNewTemplateZid) if err != nil { return nil } zn := parser.ParseZettel(menu, "") refs := collect.Order(zn) result := make([]simpleLink, 0, len(refs)) for _, ref := range refs { zid, err := id.Parse(ref.URL.Path) if err != nil { continue } m, err := te.place.GetMeta(ctx, zid) if err != nil { continue } if !te.policy.CanRead(user, m) { continue } title := runtime.GetTitle(m) astTitle := parser.ParseInlines(input.NewInput(runtime.GetTitle(m)), meta.ValueSyntaxZmk) env := encoder.Environment{Lang: runtime.GetLang(m)} menuTitle, err := adapter.FormatInlines(astTitle, "html", &env) if err != nil { menuTitle, err = adapter.FormatInlines(astTitle, "text", nil) if err != nil { menuTitle = title } } result = append(result, simpleLink{ Text: menuTitle, URL: adapter.NewURLBuilder('g').SetZid(m.Zid).String(), }) } return result } func (te *TemplateEngine) renderTemplate( ctx context.Context, w http.ResponseWriter, templateID id.Zid, base *baseData, data interface{}) { te.renderTemplateStatus(ctx, w, http.StatusOK, templateID, base, data) } func (te *TemplateEngine) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { log.Printf("%v: %v", text, err) } user := session.GetUser(ctx) var base baseData te.makeBaseData(ctx, "en", "Error", user, &base) te.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct { ErrorTitle string ErrorText string }{ ErrorTitle: http.StatusText(code), ErrorText: text, }) } func (te *TemplateEngine) renderTemplateStatus( ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, base *baseData, data interface{}) { bt, err := te.getTemplate(ctx, id.BaseTemplateZid) if err != nil { adapter.InternalServerError(w, "Unable to get base template", err) return } t, err := te.getTemplate(ctx, templateID) if err != nil { adapter.InternalServerError(w, "Unable to get template", err) return } if user := session.GetUser(ctx); user != nil { htmlLifetime, _ := startup.TokenLifetime() if tok, err1 := token.GetToken(user, htmlLifetime, token.KindHTML); err1 == nil { session.SetToken(w, tok, htmlLifetime) } } var content bytes.Buffer err = t.Render(&content, data) if err == nil { base.Content = content.String() w.Header().Set(adapter.ContentType, "text/html; charset=utf-8") w.WriteHeader(code) err = bt.Render(w, base) } if err != nil { log.Println("Unable to render template", err) } } |
Deleted web/adapter/webui/webui.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added web/router/router.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package router provides a router for web requests. package router import ( "net/http" "regexp" ) type ( methodHandler map[string]http.Handler routingTable map[byte]methodHandler ) // Router handles all routing for zettelstore. type Router struct { minKey byte maxKey byte reURL *regexp.Regexp tables [2]routingTable mux *http.ServeMux } const ( indexList = 0 indexZettel = 1 ) // NewRouter creates a new, empty router with the given root handler. func NewRouter() *Router { router := &Router{ minKey: 255, maxKey: 0, reURL: regexp.MustCompile("^$"), mux: http.NewServeMux(), } router.tables[indexList] = make(routingTable) router.tables[indexZettel] = make(routingTable) return router } func (rt *Router) addRoute(key byte, httpMethod string, handler http.Handler, index int) { // Set minKey and maxKey; re-calculate regexp. if key < rt.minKey || rt.maxKey < key { if key < rt.minKey { rt.minKey = key } if rt.maxKey < key { rt.maxKey = key } rt.reURL = regexp.MustCompile( "^/(?:([" + string(rt.minKey) + "-" + string(rt.maxKey) + "])(?:/(?:([0-9]{14})/?)?)?)$") } mh, hasKey := rt.tables[index][key] if !hasKey { mh = make(methodHandler) rt.tables[index][key] = mh } mh[httpMethod] = handler if httpMethod == http.MethodGet { if _, hasHead := rt.tables[index][key][http.MethodHead]; !hasHead { rt.tables[index][key][http.MethodHead] = handler } } } // AddListRoute adds a route for the given key and HTTP method to work with a list. func (rt *Router) AddListRoute(key byte, httpMethod string, handler http.Handler) { rt.addRoute(key, httpMethod, handler, indexList) } // AddZettelRoute adds a route for the given key and HTTP method to work with a zettel. func (rt *Router) AddZettelRoute(key byte, httpMethod string, handler http.Handler) { rt.addRoute(key, httpMethod, handler, indexZettel) } // Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics. func (rt *Router) Handle(pattern string, handler http.Handler) { rt.mux.Handle(pattern, handler) } func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { match := rt.reURL.FindStringSubmatch(r.URL.Path) if len(match) == 3 { key := match[1][0] index := indexZettel if match[2] == "" { index = indexList } if mh, ok := rt.tables[index][key]; ok { if handler, ok := mh[r.Method]; ok { r.URL.Path = "/" + match[2] handler.ServeHTTP(w, r) return } http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } } rt.mux.ServeHTTP(w, r) } |
Deleted web/server/impl/http.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/server/impl/impl.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/server/impl/router.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/server/impl/urlbuilder.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/server/server.go.
1 | //----------------------------------------------------------------------------- | | | > > > > | < < | | < < | | | | | | < < | > > | | < < < < | < | > > | > > > > < | | > > | > | < < | < < < | | | | > > > < > > | < < | < < > < > > | > | | < < > | | | | | | > > < < | | | > | < < < < < | < | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package server provides a web server. package server import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" ) // Server timeout values const ( shutdownTimeout = 5 * time.Second readTimeout = 5 * time.Second writeTimeout = 10 * time.Second idleTimeout = 120 * time.Second ) // Server is a HTTP server. type Server struct { *http.Server waitShutdown chan struct{} } // New creates a new HTTP server object. func New(addr string, handler http.Handler) *Server { if addr == "" { addr = ":http" } srv := &Server{ Server: &http.Server{ Addr: addr, Handler: handler, // See: https://blog.cloudflare.com/exposing-go-on-the-internet/ ReadTimeout: readTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, }, waitShutdown: make(chan struct{}), } return srv } // SetDebug enables debugging goroutines that are started by the server. // Basically, just the timeout values are reset. This method should be called // before running the server. func (srv *Server) SetDebug() { srv.ReadTimeout = 0 srv.WriteTimeout = 0 srv.IdleTimeout = 0 } // Run starts the web server and wait for its completion. func (srv *Server) Run() error { waitInterrupt := make(chan os.Signal) waitError := make(chan error) signal.Notify(waitInterrupt, os.Interrupt, syscall.SIGTERM) go func() { select { case <-waitInterrupt: case <-srv.waitShutdown: } ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() log.Println("Stopping Zettelstore...") if err := srv.Shutdown(ctx); err != nil { waitError <- err return } waitError <- nil }() if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { return err } return <-waitError } // Stop the web server. func (srv *Server) Stop() { close(srv.waitShutdown) } |
Added web/session/session.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package session provides utilities for using sessions. package session import ( "context" "net/http" "strings" "time" "zettelstore.de/z/auth/token" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" ) const sessionName = "zsession" // SetToken sets the session cookie for later user identification. func SetToken(w http.ResponseWriter, token []byte, d time.Duration) { cookie := http.Cookie{ Name: sessionName, Value: string(token), Path: startup.URLPrefix(), Secure: startup.SecureCookie(), HttpOnly: true, SameSite: http.SameSiteStrictMode, } if startup.PersistentCookie() && d > 0 { cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC() } http.SetCookie(w, &cookie) } // ClearToken invalidates the session cookie by sending an empty one. func ClearToken(ctx context.Context, w http.ResponseWriter) context.Context { if w != nil { SetToken(w, nil, 0) } return updateContext(ctx, nil, nil) } // Handler enriches the request context with optional user information. type Handler struct { next http.Handler getUserByZid usecase.GetUserByZid } // NewHandler creates a new handler. func NewHandler(next http.Handler, getUserByZid usecase.GetUserByZid) *Handler { return &Handler{ next: next, getUserByZid: getUserByZid, } } type ctxKeyType struct{} var ctxKey ctxKeyType // AuthData stores all relevant authentication data for a context. type AuthData struct { User *meta.Meta Token []byte Now time.Time Issued time.Time Expires time.Time } // GetAuthData returns the full authentication data from the context. func GetAuthData(ctx context.Context) *AuthData { data, ok := ctx.Value(ctxKey).(*AuthData) if ok { return data } return nil } // GetUser returns the user meta data from the context, if there is one. Else return nil. func GetUser(ctx context.Context) *meta.Meta { if data := GetAuthData(ctx); data != nil { return data.User } return nil } func updateContext( ctx context.Context, user *meta.Meta, data *token.Data) context.Context { if data == nil { return context.WithValue(ctx, ctxKey, &AuthData{User: user}) } return context.WithValue( ctx, ctxKey, &AuthData{ User: user, Token: data.Token, Now: data.Now, Issued: data.Issued, Expires: data.Expires, }) } // ServeHTTP processes one HTTP request. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { k := token.KindJSON t := getHeaderToken(r) if t == nil { k = token.KindHTML t = getSessionToken(r) } if t == nil { h.next.ServeHTTP(w, r) return } tokenData, err := token.CheckToken(t, k) if err != nil { h.next.ServeHTTP(w, r) return } ctx := r.Context() user, err := h.getUserByZid.Run(ctx, tokenData.Zid, tokenData.Ident) if err != nil { h.next.ServeHTTP(w, r) return } h.next.ServeHTTP(w, r.WithContext(updateContext(ctx, user, &tokenData))) } func getSessionToken(r *http.Request) []byte { cookie, err := r.Cookie(sessionName) if err != nil { return nil } return []byte(cookie.Value) } func getHeaderToken(r *http.Request) []byte { h := r.Header["Authorization"] if h == nil { return nil } // “Multiple message-header fields with the same field-name MAY be // present in a message if and only if the entire field-value for that // header field is defined as a comma-separated list.” // — “Hypertext Transfer Protocol” RFC 2616, subsection 4.2 auth := strings.Join(h, ", ") const prefix = "Bearer " // RFC 2617, subsection 1.2 defines the scheme token as case-insensitive. if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { return nil } return []byte(auth[len(prefix):]) } |
Changes to www/changes.wiki.
1 2 | <title>Change Log</title> | < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 | <title>Change Log</title> <a name="0_0_13"></a> <h2>Changes for Version 0.0.13 (pending)</h2> <a name="0_0_12"></a> <h2>Changes for Version 0.0.12 (2021-04-16)</h2> * Raise the per-process limit of open files on macOS to 1.048.576. This allows most macOS users to use at least 500.000 zettel. That should be enough for the near future. (major) |
︙ | ︙ |
Changes to www/download.wiki.
1 2 3 4 5 6 7 8 9 10 11 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> Build: <code>v0.0.12</code> (2021-04-16). * [/uv/zettelstore-0.0.12-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.0.12-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.0.12-windows-amd64.zip|Windows] (amd64) * [/uv/zettelstore-0.0.12-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.0.12-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) Unzip the appropriate file, install and execute Zettelstore according to the manual. <h2>Zettel for the manual</h2> As a starter, you can download the zettel for the manual [/uv/manual-0.0.11.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file place to read the zettel directly from the ZIP file. |
Changes to www/index.wiki.
︙ | ︙ | |||
13 14 15 16 17 18 19 | It is a live example of the zettelstore software, running in read-only mode. The software, including the manual, is licensed under the [/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)]. [https://twitter.com/zettelstore|Stay tuned]… <hr> | | | | | | | | > | | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | It is a live example of the zettelstore software, running in read-only mode. The software, including the manual, is licensed under the [/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)]. [https://twitter.com/zettelstore|Stay tuned]… <hr> <h3>Latest Release: 0.0.12 (2021-04-16)</h3> * [./download.wiki|Download] * [./changes.wiki#0_0_12|Change Summary] * [/timeline?p=version-0.0.12&bt=version-0.0.11&y=ci|Check-ins for version 0.0.12], [/vdiff?to=version-0.0.12&from=version-0.0.11|content diff] * [/timeline?df=version-0.0.12&y=ci|Check-ins derived from the 0.0.12 release], [/vdiff?from=version-0.0.12&to=trunk|content diff] * [./plan.wiki|Limitations and planned Improvements] * [/timeline?t=release|Timeline of all past releases] <hr> <h2>Build instructions</h2> Just install [https://fossil-scm.org|Fossil], [https://golang.org/dl/|Go] and some Go-based tools. Please read the [./build.md|instructions] for details. * [/dir?ci=trunk|Source Code] * [/download|Download the source code] as a Tarball or a ZIP file (you must [/login|login] as user "anonymous"). |
Changes to www/plan.wiki.
|
| | < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <title>Limitations and planned Improvements</title> Here is a list of some shortcomings of Zettelstore. They are planned to be solved. <h3>Serious limitations</h3> * Content with binary data (e.g. a GIF, PNG, or JPG file) cannot be created nor modified via the standard web interface. As a workaround, you should place your file into the directory where your zettel are stored. Make sure that the file name starts with unique 14 digits that make up the zettel identifier. * Automatic lists and transclusions are not supported in Zettelmarkup. * … <h3>Smaller limitations</h3> * Quoted attribute values are not yet supported in Zettelmarkup: <code>{key="value with space"}</code>. * The <tt>file</tt> sub-command currently does not support output format “json”. * The horizontal tab character (<tt>U+0009</tt>) is not supported. * Missing support for citation keys. * … <h3>Planned improvements</h3> * Support for mathematical content is missing, e.g. <code>$$F(x) &= \\int^a_b \\frac{1}{3}x^3$$</code>. * Render zettel in [https://pandoc.org|pandoc's] JSON version of their native AST to make pandoc an external renderer for Zettelstore. * … |