Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From v0.21.0 To v0.22.0
2025-07-07
| ||
08:54 | Version 0.22.0 ... (Leaf check-in: 5336da12eb user: stern tags: trunk, release, v0.22.0) | |
07:31 | Update dependencies ... (check-in: 46803d88ec user: stern tags: trunk) | |
2025-04-26
| ||
15:13 | WebUI: move context link from info page to zettel page ... (check-in: 7ae5e31c4a user: stern tags: trunk) | |
2025-04-17
| ||
15:29 | Version 0.21.0 ... (check-in: 7220c2d479 user: stern tags: trunk, release, v0.21.0) | |
09:24 | Update to zsc, to fix a panic ... (check-in: b3283fc6d6 user: stern tags: trunk) | |
Changes to VERSION.
|
| | | 1 | 0.22.0 |
Changes to cmd/cmd_run.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "context" "flag" "net/http" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter/api" "zettelstore.de/z/internal/web/adapter/webui" "zettelstore.de/z/internal/web/server" | > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | "context" "flag" "net/http" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter/api" "zettelstore.de/z/internal/web/adapter/webui" "zettelstore.de/z/internal/web/server" |
︙ | ︙ | |||
51 52 53 54 55 56 57 | kernel.Main.WaitForShutdown() return exitCode, err } func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) kern := kernel.Main | | | | | | | | | | | | | | 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 | kernel.Main.WaitForShutdown() return exitCode, err } func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) kern := kernel.Main webLogger := kern.GetLogger(kernel.WebService) var getUser getUserImpl authLogger := kern.GetLogger(kernel.AuthService) ucLogger := kern.GetLogger(kernel.CoreService) ucGetUser := usecase.NewGetUser(authManager, boxManager) ucAuthenticate := usecase.NewAuthenticate(authLogger, authManager, &ucGetUser) ucIsAuth := usecase.NewIsAuthenticated(ucLogger, &getUser, authManager) ucCreateZettel := usecase.NewCreateZettel(ucLogger, rtConfig, protectedBoxManager) ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) ucGetReferences := usecase.NewGetReferences() ucQuery := usecase.NewQuery(protectedBoxManager) ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery) ucQuery.SetEvaluate(&ucEvaluate) ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery) ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery) ucListSyntax := usecase.NewListSyntax(protectedBoxManager) ucListRoles := usecase.NewListRoles(protectedBoxManager) ucDelete := usecase.NewDeleteZettel(ucLogger, protectedBoxManager) ucUpdate := usecase.NewUpdateZettel(ucLogger, protectedBoxManager) ucRefresh := usecase.NewRefresh(ucLogger, protectedBoxManager) ucReIndex := usecase.NewReIndex(ucLogger, protectedBoxManager) ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) a := api.New( webLogger.With("system", "WEBAPI"), webSrv, authManager, authManager, rtConfig, authPolicy) wui := webui.New( webLogger.With("system", "WEBUI"), webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate) webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" { const assetPrefix = "/assets/" webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir)))) webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir)) |
︙ | ︙ | |||
132 133 134 135 136 137 138 | if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } } type getUserImpl struct{} | | | 133 134 135 136 137 138 139 140 | if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } } type getUserImpl struct{} func (*getUserImpl) GetCurrentUser(ctx context.Context) *meta.Meta { return user.GetCurrentUser(ctx) } |
Changes to cmd/command.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( "flag" "maps" "slices" | > < < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( "flag" "log/slog" "maps" "slices" ) // 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 Simple bool // Operate in simple-mode |
︙ | ︙ | |||
48 49 50 51 52 53 54 | if cmd.Name == "" || cmd.Func == nil { panic("Required command values missing") } if _, ok := commands[cmd.Name]; ok { panic("Command already registered: " + cmd.Name) } cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError) | | | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | if cmd.Name == "" || cmd.Func == nil { panic("Required command values missing") } if _, ok := commands[cmd.Name]; ok { panic("Command already registered: " + cmd.Name) } cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError) cmd.flags.String("l", slog.LevelInfo.String(), "log level specification") if cmd.SetFlags != nil { cmd.SetFlags(cmd.flags) } commands[cmd.Name] = cmd } |
︙ | ︙ |
Changes to cmd/main.go.
︙ | ︙ | |||
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 | // Package cmd provides the commands to call Zettelstore from the command line. package cmd import ( "crypto/sha256" "flag" "fmt" "net" "net/url" "os" "runtime/debug" "strconv" "strings" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/impl" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/compbox" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" | > | | 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 | // Package cmd provides the commands to call Zettelstore from the command line. package cmd import ( "crypto/sha256" "flag" "fmt" "log/slog" "net" "net/url" "os" "runtime/debug" "strconv" "strings" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/impl" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/compbox" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/web/server" ) const strRunSimple = "run-simple" func init() { RegisterCommand(Command{ |
︙ | ︙ | |||
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | keyBoxOneURI = kernel.BoxURIs + "1" keyDebug = "debug-mode" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" keyInsecureHTML = "insecure-html" keyListenAddr = "listen-addr" keyLogLevel = "log-level" keyMaxRequestSize = "max-request-size" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" keyReadOnly = "read-only-mode" keyRuntimeProfiling = "runtime-profiling" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" keyVerbose = "verbose-mode" ) func setServiceConfig(cfg *meta.Meta) bool { debugMode := cfg.GetBool(keyDebug) | > > > | | | 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 | keyBoxOneURI = kernel.BoxURIs + "1" keyDebug = "debug-mode" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" keyInsecureHTML = "insecure-html" keyListenAddr = "listen-addr" keyLogLevel = "log-level" keyLoopbackIdent = "loopback-ident" keyLoopbackZid = "loopback-zid" keyMaxRequestSize = "max-request-size" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" keyReadOnly = "read-only-mode" keyRuntimeProfiling = "runtime-profiling" keySxNesting = "sx-max-nesting" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" keyVerbose = "verbose-mode" ) func setServiceConfig(cfg *meta.Meta) bool { debugMode := cfg.GetBool(keyDebug) if debugMode && kernel.Main.GetKernelLogLevel() > slog.LevelDebug { kernel.Main.SetLogLevel(logging.LevelString(slog.LevelDebug)) } if logLevel, found := cfg.Get(keyLogLevel); found { kernel.Main.SetLogLevel(string(logLevel)) } err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode) err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) if val, found := cfg.Get(keyAdminPort); found { |
︙ | ︙ | |||
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 | } err = setConfigValue( err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML)) err = setConfigValue( err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) if val, found := cfg.Get(keyBaseURL); found { err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val) } if val, found := cfg.Get(keyURLPrefix); found { err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val) } err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) if val, found := cfg.Get(keyMaxRequestSize); found { err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val) } err = setConfigValue( err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) err = setConfigValue( err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) err = setConfigValue(err, kernel.WebService, kernel.WebProfiling, debugMode || cfg.GetBool(keyRuntimeProfiling)) if val, found := cfg.Get(keyAssetDir); found { err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val) } return err == nil } func setConfigValue(err error, subsys kernel.Service, key string, val any) error { if err == nil { | > > > > > | < | > | 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 | } err = setConfigValue( err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML)) err = setConfigValue( err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) err = setConfigValue(err, kernel.WebService, kernel.WebLoopbackIdent, cfg.GetDefault(keyLoopbackIdent, "")) err = setConfigValue(err, kernel.WebService, kernel.WebLoopbackZid, cfg.GetDefault(keyLoopbackZid, "")) if val, found := cfg.Get(keyBaseURL); found { err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val) } if val, found := cfg.Get(keyURLPrefix); found { err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val) } err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) if val, found := cfg.Get(keyMaxRequestSize); found { err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val) } err = setConfigValue( err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) err = setConfigValue( err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) err = setConfigValue(err, kernel.WebService, kernel.WebProfiling, debugMode || cfg.GetBool(keyRuntimeProfiling)) if val, found := cfg.Get(keyAssetDir); found { err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val) } if val, found := cfg.Get(keySxNesting); found { err = setConfigValue(err, kernel.WebService, kernel.WebSxMaxNesting, val) } return err == nil } func setConfigValue(err error, subsys kernel.Service, key string, val any) error { if err == nil { if err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val)); err != nil { kernel.Main.GetKernelLogger().Error("Unable to set configuration", "key", key, "value", val, "err", err) } } return err } func executeCommand(name string, args ...string) int { command, ok := Get(name) |
︙ | ︙ | |||
293 294 295 296 297 298 299 | func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error { setupRouting(srv, plMgr, authMgr, rtConfig) return nil }, ) if command.Simple { | | > > > | 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error { setupRouting(srv, plMgr, authMgr, rtConfig) return nil }, ) if command.Simple { if err := kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true"); err != nil { kern.GetKernelLogger().Error("unable to set simple-mode", "err", err) return 1 } } kern.Start(command.Header, command.LineServer, filename) exitCode, err := command.Func(fs) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kern.Shutdown(true) |
︙ | ︙ | |||
331 332 333 334 335 336 337 338 | fullVersion := info.revision if info.dirty { fullVersion += "-dirty" } kernel.Main.Setup(progName, fullVersion, info.time) flag.Parse() if *cpuprofile != "" || *memprofile != "" { if *cpuprofile != "" { | > | | > > > > > | > > > | 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 | fullVersion := info.revision if info.dirty { fullVersion += "-dirty" } kernel.Main.Setup(progName, fullVersion, info.time) flag.Parse() if *cpuprofile != "" || *memprofile != "" { var err error if *cpuprofile != "" { err = kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile) } else { err = kernel.Main.StartProfiling(kernel.ProfileHead, *memprofile) } if err != nil { kernel.Main.GetKernelLogger().Error("start profiling", "err", err) return 1 } defer func() { if err = kernel.Main.StopProfiling(); err != nil { kernel.Main.GetKernelLogger().Error("stop profiling", "err", err) } }() } args := flag.Args() if len(args) == 0 { return runSimple() } return executeCommand(args[0], args[1:]...) } |
︙ | ︙ |
Changes to docs/development/20210916194900.zettel.
1 2 3 4 5 | id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk created: 20210916194900 | | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk created: 20210916194900 modified: 20250702180238 # Sync with the official repository: #* ``fossil sync -u`` # Make sure that there is no workspace defined: #* ``ls ..`` must not have a file ''go.work'', in no parent folder. # Make sure that all dependencies are up-to-date: #* ``cat go.mod`` # Ensure clean indirect dependencies: #* ``go mod graph | modgraphviz | dot -Tpdf -o /tmp/zsgraph.pdf`` #* Grey boxes signal outdated dependencies # Clean up your Go workspace: #* ``go run tools/clean/clean.go`` (alternatively: ``make clean``) # All internal tests must succeed: #* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``) # The API tests must succeed on every development platform: #* ``go run tools/testapi/testapi.go`` (alternatively: ``make api``) # Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual: |
︙ | ︙ |
Changes to docs/manual/00001002000000.zettel.
1 2 3 4 5 6 | id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250602181324 Zettelstore supports the following design goals: ; Longevity of stored notes / zettel : Every zettel you create should be readable without the help of any tool, even without Zettelstore. : It should not be hard to write other software that works with your zettel. : Normal zettel should be stored in a single file. If this is not possible: at most in two files: one for the metadata, one for the content. The only exceptions are [[predefined zettel|00001005090000]] stored in the Zettelstore executable. : There is no additional database. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. |
︙ | ︙ | |||
35 36 37 38 39 40 41 | ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. ; Security by default : Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks. : If you know what you are doing, Zettelstore allows you to relax some security-related preferences. However, even in this case, the more secure way is chosen. | | | 35 36 37 38 39 40 41 42 43 | ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. ; Security by default : Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks. : If you know what you are doing, Zettelstore allows you to relax some security-related preferences. However, even in this case, the more secure way is chosen. : Zettelstore features a minimal design and relies on external software only when absolutely necessary. : There will be no plugin mechanism, which allows external software to control the inner workings of the Zettelstore software. |
Changes to docs/manual/00001003300000.zettel.
1 2 3 4 5 6 | id: 00001003300000 title: Zettelstore installation for the intermediate user role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20211125191727 | | | | 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 | id: 00001003300000 title: Zettelstore installation for the intermediate user role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20211125191727 modified: 20250627152419 You have already tried the Zettelstore software and now you want to use it permanently. Zettelstore should start automatically when you log into your computer. * Grab the appropriate executable and copy it into the appropriate directory * If you want to place your zettel into another directory, or if you want more than one [[Zettelstore box|00001004011200]], or if you want to [[enable authentication|00001010040100]], or if you want to tweak your Zettelstore in some other way, create an appropriate [[startup configuration file|00001004010000]]. * If you created a startup configuration file, you need to test it: ** Start a command line prompt for your operating system. ** Navigate to the directory, where you placed the Zettelstore executable. In most cases, this is done by the command ``cd DIR``, where ''DIR'' denotes the directory, where you placed the executable. ** Start the Zettelstore: *** On Windows execute the command ``zettelstore.exe run -c CONFIG_FILE`` *** On macOS execute the command ``./zettelstore run -c CONFIG_FILE`` *** On Linux execute the command ``./zettelstore run -c CONFIG_FILE`` ** In all cases, ''CONFIG_FILE'' must be replaced with the name of the file where you wrote the startup configuration. ** If you encounter some error messages, update the startup configuration, and try again. * Depending on your operating system, there are different ways to register Zettelstore to start automatically: ** [[Windows|00001003305000]] ** [[macOS|00001003310000]] ** [[Linux|00001003315000]] A word of caution: Never expose Zettelstore directly to the Internet. As a personal service, Zettelstore is not designed to handle all aspects of the open web. For instance, it lacks support for certificate handling, which is necessary for encrypted HTTP connections. To ensure security, [[install Zettelstore on a server|00001003600000]] and place it behind a proxy server designed for Internet exposure. For more details, see: [[External server to encrypt message transport|00001010090100]]. |
Changes to docs/manual/00001003305000.zettel.
1 2 3 4 5 6 | id: 00001003305000 title: Enable Zettelstore to start automatically on Windows role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20211125191727 | | | | | 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: 00001003305000 title: Enable Zettelstore to start automatically on Windows role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20211125191727 modified: 20250701130205 Windows is a complicated beast. There are several ways to automatically start Zettelstore. === Startup folder One way is to use the [[autostart folder|https://support.microsoft.com/en-us/windows/configure-startup-applications-in-windows-115a420a-0bff-4a6f-90e0-1934c844e473]]. Open the folder where you have placed in the Explorer. Create a shortcut file for the Zettelstore executable. There are some ways to do this: * Execute a right-click on the executable, and choose the menu entry ""Create shortcut"", * Execute a right-click on the executable, and then click Send To > Desktop (Create shortcut). * Drag the executable to your Desktop with pressing the ''Alt''-Key. If you have created the shortcut file, you must move it into the Startup folder. Press the Windows logo key and the key ''R'', type ''shell:startup''. Select the OK button. This will open the Startup folder. Move the shortcut file into this folder. The next time you log into your computer, Zettelstore will be started automatically. However, it remains visible, at least in the task bar. You can modify the behavior by changing some properties of the shortcut file. === Task scheduler The Windows Task scheduler allows you to start Zettelstore as a background task. This is both an advantage and a disadvantage. On the plus side, Zettelstore runs in the background, and it does not disturb you. All you have to do is to open your web browser, enter the appropriate URL, and there you go. On the negative side, you will not be notified when you enter the wrong data in the Task scheduler and Zettelstore fails to start. |
︙ | ︙ | |||
109 110 111 112 113 114 115 | Under some circumstances, Windows asks for permission and you have to enter your password. As the last step, you could run the freshly created task manually. Open your browser, enter the appropriate URL and use your Zettelstore. In case of errors, the task will most likely stop immediately. Make sure that all data you have entered is valid. | | | 109 110 111 112 113 114 115 116 117 118 119 120 | Under some circumstances, Windows asks for permission and you have to enter your password. As the last step, you could run the freshly created task manually. Open your browser, enter the appropriate URL and use your Zettelstore. In case of errors, the task will most likely stop immediately. Make sure that all data you have entered is valid. Do not forget to check the content of the startup configuration file. Use the command prompt to debug your configuration. Sometimes, for example when your computer was in stand-by and it wakes up, these tasks are not started. In this case execute the task scheduler and run the task manually. |
Changes to docs/manual/00001003315000.zettel.
1 2 3 4 5 6 | id: 00001003315000 title: Enable Zettelstore to start automatically on Linux role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20220114181521 | | | | | | | 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 | id: 00001003315000 title: Enable Zettelstore to start automatically on Linux role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20220114181521 modified: 20250701135817 Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore. * One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]]. ** See below for a lighter alternative. * If you are using the [[Gnome Desktop|https://www.gnome.org/]], you could use the tool [[GNOME Tweaks|https://gitlab.gnome.org/GNOME/gnome-tweaks]]. It allows you to specify applications that should run on startup / login. * [[KDE|https://kde.org/]] provides a system setting to [[autostart|https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/autostart/]] applications. * [[Xfce|https://xfce.org/]] allows to specify [[autostart applications|https://docs.xfce.org/xfce/xfce4-session/preferences#application_autostart]]. * [[LXDE|https://www.lxde.org/]] uses ""LXSession Edit"" to allow users to specify autostart applications. If you're using a different desktop environment, try searching for its name together with the word ""autostart"". Yet another way is to make use of the middleware that is provided. Many Linux distributions make use of [[systemd|https://systemd.io/]], which allows to start processes on behalf of a user. On the command line, adapt the following script to your own needs and execute it: ``` # mkdir -p "$HOME/.config/systemd/user" # cd "$HOME/.config/systemd/user" |
︙ | ︙ |
Changes to docs/manual/00001004010000.zettel.
1 2 3 4 5 6 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250627155145 The configuration file, specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These cannot be stored in a [[configuration zettel|00001004020000]] because they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore needs to know in advance on which network address it must listen or where zettel are stored. An attacker that is able to change the owner can do anything. Therefore, only the owner of the computer on which Zettelstore runs can change this information. |
︙ | ︙ | |||
41 42 43 44 45 46 47 | Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start. Default: ""http://127.0.0.1:23123/"". ; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one : Specifies a [[box|00001004011200]] where zettel are stored. During startup, __X__ is incremented, starting with one, until no key is found. | | | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start. Default: ""http://127.0.0.1:23123/"". ; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one : Specifies a [[box|00001004011200]] where zettel are stored. During startup, __X__ is incremented, starting with one, until no key is found. This allows you to configure than one box. If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ""dir://.zettel"". In this case, even a key ''box-uri-2'' will be ignored. ; [!debug-mode|''debug-mode''] : If set to [[true|00001006030500]], allows to debug the Zettelstore software (mostly used by Zettelstore developers). Disables any timeout values of the internal web server and does not send some security-related data. Sets [[''log-level''|#log-level]] to ""debug"". |
︙ | ︙ | |||
91 92 93 94 95 96 97 98 99 100 101 102 103 104 | Default: ""info"". Examples: ""error"" will produce just error messages (e.g. no ""info"" messages). ""error;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing error messages for all other components. When you are familiar with operating the Zettelstore, you might set the level to ""error"" to receive fewer noisy messages from it. ; [!max-request-size|''max-request-size''] : It limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources. The minimum value is 1024. Default: 16777216 (16 MiB). ; [!owner|''owner''] : [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore. | > > > > > > > > > > > > | 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 | Default: ""info"". Examples: ""error"" will produce just error messages (e.g. no ""info"" messages). ""error;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing error messages for all other components. When you are familiar with operating the Zettelstore, you might set the level to ""error"" to receive fewer noisy messages from it. ; [!loopback-ident|''loopback-ident''], [!loopback-zid|''loopback-zid''] : These keys are effective only if [[authentication is enabled|00001010000000]]. They must specify the user ID and zettel ID of a [[user zettel|00001010040200]]. When these keys are set and an HTTP request originates from the loopback device, no further authentication is required. The loopback device typically uses the IP address ''127.0.0.1'' (IPv4) or ''::1'' (IPv6). This configuration allows client software running on the same computer as the Zettelstore to access it through its API or web user interface. However, this setup is not recommended if the Zettelstore is running on a computer shared with untrusted or unknown users. Default: (empty string)/00000000000000 ; [!max-request-size|''max-request-size''] : It limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources. The minimum value is 1024. Default: 16777216 (16 MiB). ; [!owner|''owner''] : [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore. |
︙ | ︙ | |||
113 114 115 116 117 118 119 | Therefore, an authenticated user will be logged off. If ""true"", a persistent cookie is used. Its lifetime exceeds the lifetime of the authentication token by 30 seconds (see option ''token-lifetime-html''). Default: ""false"" ; [!read-only-mode|''read-only-mode''] | | > > > > > > | 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 | Therefore, an authenticated user will be logged off. If ""true"", a persistent cookie is used. Its lifetime exceeds the lifetime of the authentication token by 30 seconds (see option ''token-lifetime-html''). Default: ""false"" ; [!read-only-mode|''read-only-mode''] : If set to a [[true value|00001006030500]] the Zettelstore service will beputs into a read-only mode. No changes are possible. Default: ""false"". ; [!runtime-profiling|''runtime-profiling''] : A boolean value that enables a web interface to obtain [[runtime profiling information|00001004010200]]. Default: ""false"", but it is set to ""true"" if [[''debug-mode''|#debug-mode]]Â is enabled. In this case, it cannot be disabled. ; [!secret|''secret''] : A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be altered by some external unfriendly party. The string must have a length of at least 16 bytes. This value is only needed to be set if [[authentication is enabled|00001010040100]] by setting the key [[''owner''|#owner]] to some user identification value. ; [!sx-max-nesting|''sx-max-nesting''] : Limits nesting of template evaluation to the given value. This is used to prevent the application from crashing when an error occurs in a template zettel of the Web User Interface. Default: ""32768"" ; [!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. ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]]. Default: ""10"". |
︙ | ︙ |
Changes to docs/manual/00001004011200.zettel.
1 2 3 4 5 6 | id: 00001004011200 title: Zettelstore boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | | 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 | id: 00001004011200 title: Zettelstore boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250627162212 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 elsewhere. An example is 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 __boxes__. This is done via the ''box-uri-X'' keys of the [[startup configuration|00001004010000#box-uri-X]] (X is a number). Boxes are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses. The following box URIs are supported: ; [!dir|''dir://DIR''] : Specifies a directory where zettel files are stored. ''DIR'' is the file path. Although it is possible to use relative file paths, such as ''./zettel'' (→ URI is ''dir://.zettel''), it is preferable to use absolute file paths, e.g. ''/home/user/zettel''. The directory must exist before starting the Zettelstore[^There is one exception: when Zettelstore is [[started without any parameter|00001004050000]], e.g. via double-clicking its icon, a directory called ''./zettel'' will be created.]. It is possible to [[configure|00001004011400]] a directory box. ; [!file|''file:FILE.zip'' or ''file:///path/to/file.zip''] : Specifies a ZIP file which contains files that store zettel. You can create such a ZIP file, if you zip a directory full of zettel files. This box is always read-only. ; [!mem|''mem:''] : Stores all its zettel in volatile memory. If you stop the Zettelstore, all changes are lost. To limit usage of volatile memory, you should [[configure|00001004011600]] this type of box, although the default values might be valid for your use case. All boxes that you configure via the ''box-uri-X'' keys form a chain of boxes. If a zettel should be retrieved, a search starts in the box specified with the ''box-uri-2'' key, then ''box-uri-3'' and so on. If a zettel is created or changed, it is always stored in the box specified with the ''box-uri-1'' key. This allows to overwrite zettel from other boxes, e.g. the predefined zettel. If you use the ''mem:'' box, where zettel are stored in volatile memory, it only makes sense if you configure it as ''box-uri-1''. Such a box will be empty when Zettelstore starts, and only the first box will receive updates. You must ensure that your computer has enough RAM to store all zettel. |
Changes to docs/manual/00001004011400.zettel.
1 2 3 4 5 6 | id: 00001004011400 title: Configure file directory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | | | 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 | id: 00001004011400 title: Configure file directory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250701135923 Under certain circumstances, it is preferable to further configure a file directory box. This is done by appending query parameters after the base box URI ''dir:\//DIR''. The following parameters are supported: |= Parameter:|Description|Default value:| |type|(Sub-) Type of the directory service|(value of ""[[default-dir-box-type|00001004010000#default-dir-box-type]]"") |worker|Number of workers that can access the directory in parallel|7 |readonly|Allow only operations that do not create or change zettel|n/a === Type On some operating systems, Zettelstore tries to detect changes to zettel files outside of Zettelstore's control[^This includes Linux, Windows, and macOS.]. On other operating systems, this may not be possible due to technical limitations. Automatic detection of external changes is also not possible, if zettel files are put on an external service, such as a file server accessed via SMB/CIFS or NFS. To cope with this uncertainty, Zettelstore provides various internal implementations of a directory box. The default values should match the needs of different users, as explained in the [[installation part|00001003000000]] of this manual. The following values are supported: ; simple : Is not able to detect external changes. Works on all platforms. Is a little slower than other implementations (up to three times). ; notify : Automatically detect external changes. Tries to optimize performance, at a little cost of main memory (RAM). === Worker Internally, Zettelstore parallels concurrent requests for a zettel or its metadata. The number of parallel activities is configured by the ''worker'' parameter. A computer contains a limited number of internal processing units (CPUs). Their number ranges from 1 up to (currently) 128, for example in larger server environments. Zettelstore typically runs on systems with 1 to 8 CPUs. Access to zettel files is ultimately managed by the underlying operating system. Depending on the hardware and the type of directory box, only a limited number of parallel accesses is desirable. On smaller hardware[^In comparison to a normal desktop or laptop computer], such as the [[Raspberry Pi Zero|https://www.raspberrypi.com/products/raspberry-pi-zero/]], a smaller value might be appropriate. Every worker needs some amount of main memory (RAM) and some amount of processing power. On bigger hardware, with some fast file services, a bigger value could result in higher performance, if needed. For various reasons, the value should be a prime number. The software might enforce this restriction by selecting the next prime number of a specified non-prime value. The default value is 7, the minimum value is 1, the maximum value is 1499. |
︙ | ︙ |
Changes to docs/manual/00001004011600.zettel.
1 2 3 4 5 6 | id: 00001004011600 title: Configure memory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20220307112918 | | | | | | < | | 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: 00001004011600 title: Configure memory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20220307112918 modified: 20250627155851 Under most circumstances, it is preferable to further configure a memory box. This is done by appending query parameters after the base box URI ''mem:''. The following parameters are supported: |= Parameter:|Description|Default value:|Maximum value: |max-bytes|Maximum number of bytes the box will store|65535|1073741824 (1 GiB) |max-zettel|Maximum number of zettel|127|65535 The default values are somewhat arbitrary but applicable to many use cases. While the number of zettel should be easily calculable by a user, the number of bytes might be a bit more difficult to determine. Metadata consumes 6 bytes for the zettel identifier, plus one byte for the separator for each metadata value, in addition to the length of the key and data. The size of the content is its size in bytes. For text content, this corresponds to the number of bytes in its UTF-8 encoding. If one of the limits is exceeded, Zettelstore will return an error indicated by the HTTP status code 507. |
Changes to docs/manual/00001004020000.zettel.
1 2 3 4 5 6 | id: 00001004020000 title: Configure a running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001004020000 title: Configure a running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250701130707 show-back-links: false You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]]. This zettel is called __configuration zettel__. The following metadata keys change the appearance / behavior of Zettelstore. Some of them can be overwritten in a [[user zettel|00001010040200]], a subset of those may be overwritten in the zettel that is currently used. See the full list of [[metadata that may be overwritten|00001004020200]]. ; [!default-copyright|''default-copyright''] : Copyright value to be used when rendering content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''copyright''. Default: (the empty string). |
︙ | ︙ | |||
49 50 51 52 53 54 55 | : Language to be used when displaying content. Default: ""en"". This value is used as a default value, if it is not set in a user's zettel or in a zettel. It is also used to specify the language for all non-zettel content, e.g. lists or search results. | | | | | | 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 | : Language to be used when displaying content. Default: ""en"". This value is used as a default value, if it is not set in a user's zettel or in a zettel. It is also used to specify the language for all non-zettel content, e.g. lists or search results. Use values according to the language definition of [[RFC-5646|https://datatracker.ietf.org/doc/html/rfc5646]]. ; [!lists-menu-zettel|''lists-menu-zettel''] : Identifier of the zettel that specifies entries of the ""Lists"" menu (in the [[Web user interface|00001014000000]]). Every list item with a [[link|00001007040310]] is translated into a menu entry. If not given or if the identifier does not identify a zettel, or the zettel is not accessible for the current user, the zettel with the identifier ''00000000080001'' is used. May be [[overwritten|00001004020200]] in a user zettel. Default: ""00000000080001"". ; [!max-transclusions|''max-transclusions''] : Maximum number of indirect transclusion. This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]]. Default: ""1024"". ; [!show-back-links|''show-back-links''], [!show-folge-links|''show-folge-links''], [!show-sequel-links|''show-sequel-links''], [!show-subordinate-links|''show-subordinate-links''] : When displaying a zettel in the web user interface, references to other zettel are normally shown below the content of the zettel. This affects the metadata keys [[''back''|00001006020000#back]], [[''folge''|00001006020000#folge]], [[''sequel''|00001006020000#sequel]], and [[''subordinates''|00001006020000#subordinates]]. These configuration keys may be used to show, not to show, or to close the list of referenced zettel. Allowed values are: ""false"" (will not show the list), ""close"" (will show the list closed), and ""open"" / """" (will show the list). Default: """". May be [[overwritten|00001004020200]] in a user zettel, so that setting will only affect the given user. Alternatively, it may be overwritten in a zettel, so that the setting will affect only the given zettel. This zettel is an example of a zettel that sets ''show-back-links'' to ""false"". ; [!site-name|''site-name''] : Name of the Zettelstore instance. Will be used when displaying some lists. Default: ""Zettelstore"". |
︙ | ︙ |
Changes to docs/manual/00001004020200.zettel.
1 2 3 4 5 6 | id: 00001004020200 title: Runtime configuration data that may be user specific or zettel specific role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20221205155521 | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001004020200 title: Runtime configuration data that may be user specific or zettel specific role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20221205155521 modified: 20250626114354 Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]]. A subset of those may be overwritten in the zettel that is currently used. This allows to specify user specific or zettel specific behavior. The following metadata keys are supported to provide a more specific behavior: |=Key|User:|Zettel:|Remarks |[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N| |[[''home-zettel''|00001004020000#home-zettel]]|Y|N| |[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful |[[''lists-menu-zettel''|00001004020000#lists-menu-zettel]]|Y|N| |[[''show-back-links''|00001004020000#show-back-links]]|Y|Y| |[[''show-folge-links''|00001004020000#show-folge-links]]|Y|Y| |[[''show-sequel-links''|00001004020000#show-sequel-links]]|Y|Y| |[[''show-subordinate-links''|00001004020000#show-subordinate-links]]|Y|Y| |
Changes to docs/manual/00001004050000.zettel.
1 2 3 4 5 6 | id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 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: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250627160307 Zettelstore is not just a service that provides services of a zettelkasten. It allows 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 calling 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 its file icon. === Sub-commands * [[``zettelstore help``|00001004050200]] lists all available sub-commands. |
︙ | ︙ |
Changes to docs/manual/00001004050400.zettel.
1 2 3 4 5 6 | id: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | 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: 00001004050400 title: The ''version'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250701125959 Emits some information about the Zettelstore version. This allows you to check whether your installed Zettelstore is up to date. The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, and an indication about the operating system and the processor architecture of that computer. The build version information is a string like ''1.0.2+351ae138b4''. The part ""1.0.2"" is the release version. ""+351ae138b4"" is a code uniquely identifying the version to the developer. Everything after the release version is optional, e.g. ""1.4.3"" is a valid build version information also. Example: ``` # zettelstore version Zettelstore 1.0.2+351ae138b4 (go1.16.5@linux/amd64) Licensed under the latest version of the EUPL (European Union Public License) ``` In this example, Zettelstore is running in the released version ""1.0.2"" and was compiled using [[Go, version 1.16.5|https://go.dev/doc/go1.16]]. The software was built for running under a Linux operating system with an ""amd64"" processor. The build version is also stored in the public, [[predefined|00001005090000]] zettel titled ""[[Zettelstore Version|00000000000001]]"". However, to access this zettel, you need a [[running zettelstore|00001004051000]]. |
Changes to docs/manual/00001004051000.zettel.
1 2 3 4 5 6 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 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: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250627160733 === ``zettelstore run`` This starts the web service. ``` zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v] ``` ; [!a|''-a PORT''] : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. See the explanation of [[''admin-port''|00001004010000#admin-port]] for more details. ; [!c|''-c CONFIGFILE''] : Specifies ''CONFIGFILE'' as a file, where [[startup configuration data|00001004010000]] is read. It is ignored if the given file is not available or not readable. Default: tries to read the following files in the ""current directory"": ''zettelstore.cfg'', ''zsconfig.txt'', ''zscfg.txt'', ''_zscfg'', and ''.zscfg''. ; [!d|''-d DIR''] : Specifies ''DIR'' as the directory that contains all zettel. Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"". ; [!debug|''-debug''] |
︙ | ︙ |
Changes to docs/manual/00001004051100.zettel.
1 2 3 4 5 6 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | < | | | | 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 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250627161010 === ``zettelstore run-simple`` This sub-command is implicitly called when a user starts Zettelstore by double-clicking its GUI icon. It is a simplified variant of the [[''run'' sub-command|00001004051000]]. First, this sub-command checks if it can read a [[Zettelstore startup configuration|00001004010000]] file by trying the [[default values|00001004051000#c]]. If this is the case, ''run-simple'' just continues as the [[''run'' sub-command|00001004051000]], but ignores any command line options (including ''-d DIR'').[^This allows a [[curious user|00001003000000]] to become an intermediate user.] If no startup configuration is found, the sub-command only allows specifying a zettel directory. The directory will be created automatically if it does not exist. This differs from the ''run'' sub-command, where the directory must already exist. Unlike the ''run'' sub-command, no other command-line parameters are allowed. ``` zettelstore run-simple [-d DIR] ``` ; [!d|''-d DIR''] : Specifies ''DIR'' as the directory that contains all zettel. |
︙ | ︙ |
Changes to docs/manual/00001004059900.zettel.
1 2 3 4 5 6 | id: 00001004059900 title: Command line flags for profiling the application role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20211122170506 | | | | 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: 00001004059900 title: Command line flags for profiling the application role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20211122170506 modified: 20250627161347 If you want to measure potential bottlenecks within the software Zettelstore, there are two [[command line|00001004050000]] flags for enabling the measurement (also called __profiling__): ; ''-cpuprofile FILE'' : Enables CPU profiling. ''FILE'' must be the name of the file where the data is stored. ; ''-memprofile FILE'' : Enables memory profiling. ''FILE'' must be the name of the file where the data is stored. Normally, profiling will stop when you stop the software Zettelstore. The given ''FILE'' can be used to analyze the data via the tool ``go tool pprof FILE``. Please notice that ''-cpuprofile'' takes precedence over ''-memprofile''. You cannot measure both. You also can use the [[administrator console|00001004100000]] to start and stop profiling manually for an already running Zettelstore. |
Changes to docs/manual/00001004101000.zettel.
1 2 3 4 5 6 | id: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210510141304 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210510141304 modified: 20250701130043 ; [!bye|''bye''] : Closes the connection to the administrator console. ; [!config|''config SERVICE''] : Displays all valid configuration keys for the given service. If a key ends with the hyphen-minus character (""''-''"", U+002D), the key denotes a list value. |
︙ | ︙ | |||
48 49 50 51 52 53 54 | ``log-level NAME`` shows log level for the given service or for the kernel. ``log-level NAME VALUE`` sets the log level for the given service or for the kernel. ''VALUE'' is either the name of the log level or its numerical value. ; [!metrics|''metrics''] : Displays some values that reflect the inner workings of Zettelstore. | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | ``log-level NAME`` shows log level for the given service or for the kernel. ``log-level NAME VALUE`` sets the log level for the given service or for the kernel. ''VALUE'' is either the name of the log level or its numerical value. ; [!metrics|''metrics''] : Displays some values that reflect the inner workings of Zettelstore. See [[here|https://pkg.go.dev/runtime/metrics]] for a technical description of these values. ; [!next-config|''next-config''] : Displays next configuration data. It will be the current configuration, if the corresponding services is restarted. ``next-config`` shows all next configuration data. ``next-config SERVICE`` shows only the next configuration data of the given service. |
︙ | ︙ |
Changes to docs/manual/00001005000000.zettel.
1 2 3 4 5 6 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250702180913 Zettelstore is a software application that manages your zettel. Since each zettel must be readable without any special tools, most of them are stored as ordinary files within specific directories. Typically, both file names and file content must follow certain rules so that Zettelstore can manage them properly. If you add, delete, or modify zettel files with other tools, e.g. a text editor, Zettelstore will monitor these changes. Zettelstore provides additional services to the user. Via the built-in [[web user interface|00001014000000]] you can work with zettel in various ways. For example, you are able to list zettel, to create new zettel, to edit them, or to delete them. You can view zettel details and relations between zettel. In addition, Zettelstore provides an ""application programming interface"" ([[API|00001012000000]]) that allows other software to communicate with the Zettelstore. |
︙ | ︙ | |||
27 28 29 30 31 32 33 | The directory has to be specified at [[startup 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 user interface|00001014000000]] or via the [[API|00001012053200]], 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. | | | | 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 | The directory has to be specified at [[startup 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 user interface|00001014000000]] or via the [[API|00001012053200]], 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 is that they consist of 14 digits, you are free to use other digit sequences. The [[configuration zettel|00001004020000]] is one prominent example, as well as these manual zettel. You can create these special zettel by manually renaming the underlying zettel files. It is allowed that the file name contains other characters after the 14 digits. These are ignored by Zettelstore. Two filename extensions are used by Zettelstore: # ''.zettel'' is a format that stores metadata and content together in one file, # the empty file extension is used, when the content must be stored in its own file, e.g. image data; in this case, the filename contains just the 14 digits of the zettel identifier, and optional characters except the period ''"."''. Other filename extensions are used to determine the ""syntax"" of a zettel. This allows to use other content within the Zettelstore, e.g. images or HTML templates. For example, you want to store an important figure in the Zettelstore that is encoded as a ''.png'' file. Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stored. The solution is a meta-file with the same zettel identifier, but without a filename extension. Zettelstore recognizes this situation and reads in both files for the one zettel containing the figure. It maintains this relationship as long as these files exist. In the case of some textual zettel content, you may not want to store the metadata and the zettel content in two separate files. Here the ''.zettel'' extension will signal that the metadata and the zettel content will be stored in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator""). === Predefined zettel Zettelstore contains some [[predefined zettel|00001005090000]] to work properly. The [[configuration zettel|00001004020000]] is one example. To render the built-in [[web user interface|00001014000000]], some templates are used, as well as a [[layout specification in CSS|00000000020001]]. |
︙ | ︙ | |||
79 80 81 82 83 84 85 | * [[List of predefined zettel|00001005090000]] === Boxes: alternative ways to store zettel As described above, a zettel may be stored either as a file inside a directory or within the Zettelstore software itself. Zettelstore allows other ways to store zettel by providing an abstraction called __box__.[^Formerly, zettel were stored physically in boxes, often made of wood.] | | | | | < < < | 79 80 81 82 83 84 85 86 87 88 89 | * [[List of predefined zettel|00001005090000]] === Boxes: alternative ways to store zettel As described above, a zettel may be stored either as a file inside a directory or within the Zettelstore software itself. Zettelstore allows other ways to store zettel by providing an abstraction called __box__.[^Formerly, zettel were stored physically in boxes, often made of wood.] A file directory that stores zettel is called a ""directory box"". Alternatively, zettel can also be stored in a ZIP file, known as a ""file box"". For testing purposes, zettel can be stored in volatile memory (called __RAM__). This method is referred to as a ""memory box"". |
Changes to docs/manual/00001005090000.zettel.
1 2 3 4 5 6 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20250627163000 The following table lists all predefined zettel with their purpose. The content of most[^To be more exact: zettel with an identifier greater or equal ''00000999999900'' will have their content indexed.] of these zettel will not be indexed by Zettelstore. You will not find zettel when searched for some content, e.g. ""[[query:european]]"" will not find the [[Zettelstore License|00000000000004]]. However, metadata is always indexed, e.g. ""[[query:title:license]]"" will find the Zettelstore License zettel. |
︙ | ︙ | |||
23 24 25 26 27 28 29 | | [[00000000000009]] | Zettelstore Log | Lists the last 8192 log messages | [[00000000000010]] | Zettelstore Memory | Some statistics about main memory usage | [[00000000000011]] | Zettelstore Sx Engine | Statistics about the [[Sx|https://t73f.de/r/sx]] engine, which interprets symbolic expressions | [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the index process | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more | [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore | [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]] | | | | 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 | | [[00000000000009]] | Zettelstore Log | Lists the last 8192 log messages | [[00000000000010]] | Zettelstore Memory | Some statistics about main memory usage | [[00000000000011]] | Zettelstore Sx Engine | Statistics about the [[Sx|https://t73f.de/r/sx]] engine, which interprets symbolic expressions | [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the index process | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more | [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore | [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]] | [[00000000000100]] | Zettelstore Runtime Configuration | Allows you 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 Zettel 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 Template | 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 | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel | [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message | [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates | [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn functions to build the templates | [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid | [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]"" | [[00000000060020]] | configuration | [[Role zettel|00001012051800]] for the role ""[[configuration|00001006020100#configuration]]"" | [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]"" | [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]"" | [[00000000080001]] | Lists Menu | Default items of the ""Lists"" menu; see [[lists-menu-zettel|00001004020000#lists-menu-zettel]] for customization options | [[00000000090000]] | New Menu | Contains items that should be in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100#zettel]]"" | [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]] | [[00000000090003]] | New Tag | Template for a new [[tag zettel|00001006020100#tag]] |
︙ | ︙ |
Changes to docs/manual/00001006010000.zettel.
1 2 3 4 5 6 | id: 00001006010000 title: Syntax of Metadata role: manual tags: #manual #syntax #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001006010000 title: Syntax of Metadata role: manual tags: #manual #syntax #zettelstore syntax: zmk created: 20210126175322 modified: 20250701130629 The metadata of a zettel is a collection of key-value pairs. The syntax roughly resembles the internal header of an email ([[RFC5322|https://datatracker.ietf.org/doc/html/rfc5322]]). The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"", U+002D) is also allowed. It begins at the first position of a new line. Uppercase letters of a key are translated to their lowercase equivalence. A key is separated from its value either by * a colon character (""'':''""), |
︙ | ︙ |
Changes to docs/manual/00001006020000.zettel.
1 2 3 4 5 6 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 modified: 20250701130724 Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore. See the [[computed list of supported metadata keys|00000000000090]] for details. Most keys conform to a [[type|00001006030000]]. ; [!author|''author''] |
︙ | ︙ | |||
48 49 50 51 52 53 54 | : A user-entered time stamp that document the point in time when the zettel should expire. When a zettel expires, Zettelstore does nothing. It is up to you to define required actions. ''expire'' is just a documentation. You could define a query and execute it regularly, for example [[query:expire? ORDER expire]]. Alternatively, a Zettelstore client software could define some actions when it detects expired zettel. ; [!folge|''folge''] | > | > | < < < < | | > | 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 | : A user-entered time stamp that document the point in time when the zettel should expire. When a zettel expires, Zettelstore does nothing. It is up to you to define required actions. ''expire'' is just a documentation. You could define a query and execute it regularly, for example [[query:expire? ORDER expire]]. Alternatively, a Zettelstore client software could define some actions when it detects expired zettel. ; [!folge|''folge''] : Is a property that contains identifiers of all zettel that reference this zettel via the [[''precursor''|#precursor]] value. It is used to reference __folge zettel__, those that develop the idea of the current zettel a bit further, following a ""train of thought."" ; [!folge-role|''folge-role''] : Specifies a suggested [[''role''|#role]] the zettel should use in the future, if zettel currently has a preliminary role. ; [!forward|''forward''] : Property that contains all references that identify another zettel within the content of the zettel. ; [!id|''id''] : Contains the [[zettel identifier|00001006050000]], as given by the Zettelstore. It cannot be set manually, because it is a computed value. ; [!lang|''lang''] : Language for the zettel. Mostly used for HTML rendering of the zettel. If not given, the value ''lang'' from the zettel of the [[current user|00001010040200]] will be used. If that value is also not available, it is read from the [[configuration zettel|00001004020000#lang]] will be used. Use values according to the language definition of [[RFC-5646|https://datatracker.ietf.org/doc/html/rfc5646]]. ; [!license|''license''] : Defines a license string that will be rendered. If not given, the value ''default-license'' from the [[configuration zettel|00001004020000#default-license]] will be used. ; [!modified|''modified''] : Date and time when a zettel was modified through Zettelstore. If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value. This is a computed value. There is no need to set it via Zettelstore. ; [!precursor|''precursor''] : References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel. Basically the inverse of key [[''folge''|#folge]]. ; [!prequel|''prequel''] : Specifies one or more zettel that are conceptually a prequel zettel. These are zettel that occurred somehow before the current zettel. Basically the inverse of key [[''sequel''|#sequel]]. ; [!published|''published''] : This property contains the timestamp of the last modification / creation of the zettel. If [[''modified''|#modified]] is set with a valid timestamp, it contains the its value. Otherwise, if [[''created''|#created]] is set with a valid timestamp, it contains the its value. Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used. In all other cases, this property is not set. |
︙ | ︙ | |||
106 107 108 109 110 111 112 113 114 | ; [!role|''role''] : Defines the role of the zettel. Can be used for selecting zettel. See [[supported zettel roles|00001006020100]]. If not given, it is ignored. ; [!sequel|''sequel''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''prequel''|#prequel]] value. ; [!subordinates|''subordinates''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''superior''|#superior]] value. | > > < < < < < < | 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | ; [!role|''role''] : Defines the role of the zettel. Can be used for selecting zettel. See [[supported zettel roles|00001006020100]]. If not given, it is ignored. ; [!sequel|''sequel''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''prequel''|#prequel]] value. A __sequel zettel__ acts as a branching thought of the current zettel. ; [!subordinates|''subordinates''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''superior''|#superior]] value. ; [!summary|''summary''] : Summarizes the content of the zettel using plain text. ; [!superior|''superior''] : Specifies a zettel that is conceptually a superior zettel. This might be a more abstract zettel, or a zettel that should be higher in a hierarchy. ; [!syntax|''syntax''] : Specifies the syntax that should be used for interpreting the zettel. |
︙ | ︙ |
Changes to docs/manual/00001006020400.zettel.
1 2 3 4 5 6 | id: 00001006020400 title: Supported values for metadata key ''read-only'' role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001006020400 title: Supported values for metadata key ''read-only'' role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 modified: 20250602181627 A zettel can be marked as read-only, if it contains a metadata value for key [[''read-only''|00001006020000#read-only]]. If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel, depending on their [[user role|00001010070300]]. Otherwise, the read-only mark is just a binary value. === No authentication |
︙ | ︙ | |||
22 23 24 25 26 27 28 | If the metadata value is the same as an explicit [[user role|00001010070300]], users with that role (or a role with lower rights) are not allowed to modify the zettel. ; ""reader"" : Neither an unauthenticated user nor a user with role ""reader"" is allowed to modify the zettel. Users with role ""writer"" or the owner itself still can modify the zettel. ; ""writer"" | | | 22 23 24 25 26 27 28 29 30 31 32 33 34 | If the metadata value is the same as an explicit [[user role|00001010070300]], users with that role (or a role with lower rights) are not allowed to modify the zettel. ; ""reader"" : Neither an unauthenticated user nor a user with role ""reader"" is allowed to modify the zettel. Users with role ""writer"" or the owner itself still can modify the zettel. ; ""writer"" : Neither an unauthenticated user nor users with roles ""reader"" or ""writer"" are allowed to modify the zettel. Only the owner of the Zettelstore can modify the zettel. If the metadata value is something else (one of the values ""true"" or ""owner"" is recommended), no user is allowed to modify the zettel through the [[web user interface|00001014000000]]. However, if the zettel is accessible as a file in a [[directory box|00001004011400]], the zettel could be modified using an external editor. Typically the owner of a Zettelstore has such access. |
Changes to docs/manual/00001006050000.zettel.
1 2 3 4 5 6 | id: 00001006050000 title: Zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | 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: 00001006050000 title: Zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250627130018 Each zettel is given a unique identifier. To some degree, the zettel identifier is part of the metadata. Basically, the identifier is given by the [[Zettelstore|00001005000000]] software. Every zettel identifier consists of 14 digits. They resemble a timestamp: the first four digits could represent the year, the next two represent the month, followed by day, hour, minute, and second. This allows zettel to be ordered chronologically in a canonical way. In most cases the zettel identifier is the timestamp when the zettel was created. However, the Zettelstore software just checks for exactly 14 digits. Anybody is free to assign a ""non-timestamp"" identifier to a zettel, e.g. with a month part of ""35"" or with ""99"" as the last two digits. Some zettel identifiers are [[reserved|00001006055000]] and should not be used otherwise. All identifiers of zettel initially provided by an empty Zettelstore begin with ""000000"", except the home zettel ''00010000000000''. Zettel identifiers of this manual have been chosen to begin with ""000010"". A zettel can have any identifier that contains 14 digits and that is not in use by another zettel managed by the same Zettelstore. |
Changes to docs/manual/00001006055000.zettel.
1 2 3 4 5 6 | id: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210721105704 | | | | | > | | | 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 | id: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210721105704 modified: 20250627152022 [[Zettel identifiers|00001006050000]] are typically created based on the current date and time. By renaming the underlying zettel files, you can provide any sequence of 14 digits. To make things easier, you must not use zettel identifier that begin with four zeroes (''0000''). All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel is ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.]. Zettel identifiers of this manual have been chosen to begin with ''000010''. However, some external applications may need at least one defined zettel identifier to work properly. Zettel [[Zettelstore Application Directory|00000999999999]] (''00000999999999'') can be used to associate a name to a zettel identifier. For example, if your application is named ""app"", you create a metadata key ''app-zid''. Its value is the zettel identifier of the zettel that configures your application. === Reserved Zettel Identifiers |= From | To | Description | 00000000000000 | 00000000000000 | This is an invalid zettel identifier | 00000000000001 | 00000999999999 | [[Predefined zettel|00001005090000]] | 00001000000000 | 00001099999999 | This [[Zettelstore manual|00001000000000]] | 00001100000000 | 00008999999999 | Reserved, do not use | 00009000000000 | 00009999999999 | Reserved for applications |
︙ | ︙ |
Changes to docs/manual/00001007000000.zettel.
1 2 3 4 5 6 | id: 00001007000000 title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | | 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 | id: 00001007000000 title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250627161609 Zettelmarkup is a rich plain-text based markup language for writing zettel content. Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel. Zettelmarkup supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer. Zettelmarkup can be much easier parsed / consumed by a software compared to other markup languages. Writing a parser for [[Markdown|https://daringfireball.net/projects/markdown/syntax]] is quite challenging. [[CommonMark|00001008010500]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation. Zettelmarkup follows a few simple principles that anyone familiar with software development should be able to understand and implement. Zettelmarkup is a markup language on its own. This is in contrast to Markdown, which is basically a super-set of HTML: every HTML document is a valid Markdown document.[^To be precise: the content of the ``<body>`` of each HTML document is a valid Markdown document.] While HTML is a markup language that will probably last for a long time, it cannot be easily translated to other formats, such as PDF, JSON, or LaTeX. Additionally, it is allowed to embed other languages into HTML, such as CSS or even JavaScript. This could create problems with longevity as well as security problems. Zettelmarkup is a rich markup language, but it focuses on relatively short zettel content. It allows embedding other content, simple tables, quotations, description lists, and images. It provides a broad range of inline formatting, including __emphasized__, **strong**, ~~deleted~~{-} and >>inserted>> text. Footnotes[^like this] are supported, links to other zettel and to external material, as well as citation keys. Zettelmarkup allows you to include content from other zettel and to embed the results of a search query. Zettelmarkup might be seen as a proprietary markup language. But if you want to use [[Markdown/CommonMark|00001008010000]] and you need support for footnotes or tables, you'll end up with proprietary extensions. However, the Zettelstore supports CommonMark as a zettel syntax, so you can mix both Zettelmarkup zettel and CommonMark zettel in one store to get the best of both worlds. * [[General principles|00001007010000]] * [[Basic definitions|00001007020000]] |
︙ | ︙ |
Changes to docs/manual/00001007030000.zettel.
1 2 3 4 5 6 | id: 00001007030000 title: Zettelmarkup: Block-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | | | | | 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 | id: 00001007030000 title: Zettelmarkup: Block-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250627132222 Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line. There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs. === Lists In Zettelmarkup, lists themselves are not specified, but list items. A sequence of list items is considered as a list. [[Description lists|00001007030100]] contain two different item types: the term to be described and the description itself. These cannot be combined with other lists. Ordered lists, unordered lists, and quotation lists can be combined into [[nested lists|00001007030200]]. === One-line blocks * [[Headings|00001007030300]] allow you to structure the content of a zettel. * The [[horizontal rule|00001007030400]] signals a thematic break * A [[transclusion|00001007031100]] embeds the content of one zettel into another. === Line-range blocks This kind of block encompasses at least two lines. To be useful, it encompasses more lines. It begins with at least three identical characters at the start of the first line. It ends at the line that contains at least the same number of these identical characters, starting at the first position of that line. This allows line-range blocks to be nested. Additionally, all other block elements are allowed within line-range blocks. * [[Verbatim blocks|00001007030500]] do not interpret their content, * [[Quotation blocks|00001007030600]] specify a block-length quotations, * [[Verse blocks|00001007030700]] allow to enter poetry, lyrics and similar text, where line endings are important, * [[Region blocks|00001007030800]] just mark regions, e.g. for common formatting, * [[Comment blocks|00001007030900]] allow you to enter text that will be ignored when rendered. * [[Evaluation blocks|00001007031300]] specify some content to be evaluated by either Zettelstore or external software. * [[Math-mode blocks|00001007031400]] can be used to enter mathematical formulas / equations. * [[Inline-Zettel blocks|00001007031200]] provide a mechanism to specify zettel content with a new syntax without creating a new zettel. === Tables Tables, similar to lists, are not specified explicitly. A sequence of table rows is considered a [[table|00001007031000]]. A table row itself is a sequence of table cells. === Paragraphs Any line that does not conform to another blocks-structured element begins a paragraph. This has the implication that a mistyped syntax element for a block element will be part of the paragraph. For example: ```zmk = Heading Some text follows. ``` will be rendered in HTML as :::example = Heading Some text follows. ::: This is because headings require at least three equal sign characters. A paragraph is essentially a sequence of [[inline-structured elements|00001007040000]]. Inline-structured elements can span more than one line. Paragraphs are separated by empty lines. If you want to specify a second paragraph inside a list item, or if you want to continue a paragraph on a second and more line within a list item, you must begin the paragraph with a certain number of space characters. The number of space characters depends on the kind of a list and the relevant nesting level. A line that begins with a space character and which is outside of a list or does not contain the right number of space characters is considered to be part of a paragraph. |
Changes to docs/manual/00001007031200.zettel.
1 2 3 4 5 6 | id: 00001007031200 title: Zettelmarkup: Inline-Zettel Block role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20220201142439 | | | | | 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: 00001007031200 title: Zettelmarkup: Inline-Zettel Block role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20220201142439 modified: 20250627131204 An inline-zettel block allows to specify some content with another syntax without creating a new zettel. This is useful, for example, if you want to embed some [[Markdown|00001008010500]] content, because you are too lazy to translate Markdown into Zettelmarkup. Another example is to specify HTML code to use it for some kind of web front-end framework. Like all other [[line-range blocks|00001007030000#line-range-blocks]], an inline-zettel block begins with at least three identical characters, starting at the first position of a line. For inline-zettel blocks, the at-sign character (""''@''"", U+0040) is used. You can add some [[attributes|00001007050000]] to the beginning line of a verbatim block, following the initiating characters. The inline-zettel block uses the attribute key ""syntax"" to specify the [[syntax|00001008000000]] of the inline-zettel. Alternatively, you can use the generic attribute to specify the syntax value. If no value is provided, ""[[text|00001008000000#text]]"" is assumed. Any other character in this first line will be ignored. Text following the beginning line will not be interpreted until a line starts with at least the same number of identical at-sign characters as those in the beginning line. This allows entering at-sign characters in the text that should not be interpreted at this level. Some examples: ```zmk @@@markdown A link to [this](00001007031200) zettel. @@@ ``` |
︙ | ︙ | |||
48 49 50 51 52 53 54 | :::example @@@html <h1>H1 Heading</h1> Alea iacta est @@@ ::: :::note | | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | :::example @@@html <h1>H1 Heading</h1> Alea iacta est @@@ ::: :::note Please note: some HTML code will not be fully rendered due to possible security implications. This includes HTML lines that contain a ''<script>'' tag or an ''<iframe>'' tag. ::: Of course, you do not need to switch the syntax and you are allowed to nest inline-zettel blocks: ```zmk @@@@zmk 1st level inline @@@zmk 2nd level inline |
︙ | ︙ |
Changes to docs/manual/00001007040310.zettel.
1 2 3 4 5 6 | id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 modified: 20250701130556 There are two kinds of links, regardless of links to (internal) other zettel or to (external) material. Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and end with two consecutive right square bracket characters (""'']''"", U+005D). If the content starts with more than two left square bracket characters, all but the last two will be treated as text. The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", U+007C): ``[[text|linkspecification]]``. The text is a sequence of [[inline elements|00001007040000]]. |
︙ | ︙ | |||
28 29 30 31 32 33 34 | A link specification starting with one slash character (""''/''"", U+002F), or one or two full stop characters (""''.''"", U+002E) followed by a slash character, will be interpreted as a local reference, called __hosted reference__. Such references will be interpreted relative to the web server hosting the Zettelstore. If a link specification begins with two slash characters (called __based reference__), it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]]. | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | A link specification starting with one slash character (""''/''"", U+002F), or one or two full stop characters (""''.''"", U+002E) followed by a slash character, will be interpreted as a local reference, called __hosted reference__. Such references will be interpreted relative to the web server hosting the Zettelstore. If a link specification begins with two slash characters (called __based reference__), it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]]. To specify some material outside the Zettelstore, just use a normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://datatracker.ietf.org/doc/html/rfc3986]]. === Other topics If the link references another zettel, and this zettel is not readable for the current user, e.g. because of missing access rights, then only the associated text is presented. A zettel reference to a non-existing zettel, i.e. using a legal zettel identifier that does not identify an existing zettel, the link text will be shown strikethrough. For example, ''[[Missing zettel|99999999999999]]'' will be rendered in HTML as ::[[Missing zettel|99999999999999]]::{=example}. |
Changes to docs/manual/00001007040322.zettel.
1 2 3 4 5 6 | id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 modified: 20250701130320 Image content is assumed, if a URL is used or if the referenced zettel contains an image. Supported formats are: * Portable Network Graphics (""PNG""), as defined by [[RFC\ 2083|https://datatracker.ietf.org/doc/html/rfc2083]]. * Graphics Interchange Format (""GIF"), as defined by [[https://www.w3.org/Graphics/GIF/spec-gif89a.txt]]. * JPEG / JPG, defined by the __Joint Photographic Experts Group__. * Scalable Vector Graphics (SVG), defined by [[https://www.w3.org/Graphics/SVG/]] * WebP, defined by [[Google|https://developers.google.com/speed/webp]] If the text is given, it will be interpreted as an alternative textual representation, to help persons with some visual disabilities. |
︙ | ︙ |
Changes to docs/manual/00001007050100.zettel.
1 2 3 4 5 6 | id: 00001007050100 title: Zettelmarkup: Supported Attribute Values for Natural Languages role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001007050100 title: Zettelmarkup: Supported Attribute Values for Natural Languages role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250701130735 With an [[attribute|00001007050000]] it is possible to specify the natural language of a text region. This is important, if you want to render your markup into an environment, where this is significant. HTML is such an environment. To specify the language within an attribute, you must use the key ''lang''. The language itself is specified according to the language definition of [[RFC-5646|https://datatracker.ietf.org/doc/html/rfc5646]]. Examples: * ``{lang=en}`` for the english language * ``{lang=en-us}`` for the english dialect spoken in the United States of America * ``{lang=de}`` for the german language * ``{lang=de-at}`` for the german language dialect spoken in Austria * ``{lang=de-de}`` for the german language dialect spoken in Germany |
︙ | ︙ |
Changes to docs/manual/00001007700000.zettel.
1 2 3 4 5 6 | id: 00001007700000 title: Query Expression role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 | | | | > | | 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: 00001007700000 title: Query Expression role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 modified: 20250627150916 A query expression allows you to search for specific zettel and perform actions on them. You may select zettel based on a list of [[identifiers|00001006050000]], a query directive, a full-text search, specific metadata values, or any combination of these. A query expression consists of an optional __[[zettel identifier list|00001007710000]]__, zero or more __[[query directives|00001007720000]]__, an optional __[[search expression|00001007701000]]__, and an optional __[[action list|00001007770000]]__. The latter two are separated by a vertical bar character (""''|''"", U+007C). A query expression follows a [[formal syntax|00001007780000]]. * [[List of zettel identifier|00001007710000]] * [[Query directives|00001007720000]] ** [[Context directive|00001007720300]] ** [[Thread Directive|00001007720500]] ** [[Ident directive|00001007720600]] ** [[Items directive|00001007720900]] ** [[Unlinked directive|00001007721200]] * [[Search expression|00001007701000]] ** [[Search term|00001007702000]] ** [[Search operator|00001007705000]] ** [[Search value|00001007706000]] * [[Action list|00001007770000]] Here are [[some examples|00001007790000]] thar can be used to manage a Zettelstore: {{{00001007790000}}} |
Changes to docs/manual/00001007720000.zettel.
1 2 3 4 5 6 | id: 00001007720000 title: Query Directives role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707203135 | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001007720000 title: Query Directives role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707203135 modified: 20250624163147 A query directive transforms a list of zettel identifier into a list of zettel identifiert. It is only valid if a list of zettel identifier is specified at the beginning of the query expression. Otherwise the text of the directive is interpreted as a search expression. For example, ''CONTEXT'' is interpreted as a full-text search for the word ""context"". Every query directive therefore consumes a list of zettel, and it produces a list of zettel according to the specific directive. * [[Context directive|00001007720300]] * [[Thread Directive|00001007720500]] * [[Ident directive|00001007720600]] * [[Items directive|00001007720900]] * [[Unlinked directive|00001007721200]] |
Changes to docs/manual/00001007720300.zettel.
1 2 3 4 5 6 | id: 00001007720300 title: Query: Context Directive role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707204706 | | > > > | > > > > > | > | | < | | | | | | 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 | id: 00001007720300 title: Query: Context Directive role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707204706 modified: 20250626174255 A context directive calculates the __context__ of a list of zettel identifier. It starts with the keyword ''CONTEXT''. Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters. These are: * ''FULL'': additionally search for zettel with the same tags, * ''BACKWARD'': search for context only though backward links, * ''FORWARD'': search for context only through forward links, * ''DIRECTED'': search for context only in the same direction as the one that led to the current zettel. If the zettel is one of the initial zettel, search in both directions. * ''COST'': one or more space characters, and a positive integer: set the maximum __cost__ (default: 17), * ''MAX'': one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200). * ''MIN'': one or more space characters, and a positive integer: set the minimum number of context zettel (default: 0). Takes precedence over ''COST'' and ''MAX''. If neither ''BACKWARD'' nor ''FORWARD'' is specified, the search for context zettel will follow both backward and forward links. If both ''BACKWARD'' and ''FORWARD'' are specified, the search for context zettel will be performed as ''DIRECTED''. Internally, ''DIRECTED'' is just a shorthand for specifying both ''BACKWARD'' and ''FORWARD''. If any of the three direction specifiers ''BACKWARD'', ''FORWARD'', and ''DIRECTED'' is specified more than once, parsing of the thread directive is stopped. All following text is then interpreted either as other directives or as a search term. The cost of a context zettel is calculated iteratively: * Each of the specified zettel has a cost of 1.0. * Every zettel directly referenced by a specified zettel has a maximum cost of 4.0. * A zettel found as a single folge zettel or single precursor zettel inherits the cost of the originating zettel, plus 0.2. * A zettel found as a single sequel zettel or single prequel zettel inherits the cost of the originating zettel, plus 1.0. * A zettel discovered via another type of link, without being part of a [[set of zettel identifiers|00001006032500]], inherits the cost of the originating zettel, plus 2.0. * A zettel that is part of a set of zettel identifiers, inherits the cost of the originating zettel, plus one of the four costs above, multiplied by a value that grows roughly in a linear-logarithmic fashion based on the size of the set. * A zettel sharing the same tag inherits the cost of the originating zettel, plus a linear-logarithmic value based on the number of zettel sharing that tag. If a zettel shares more than one tag with the originating zettel, a 90% discount is applied per additional tag. This rules applies only if the ''FULL'' directive has been specified. The maximum cost is only checked for all zettel that are not directly reachable from the initial, specified list of zettel. This ensures that initial zettel that have only a highly used tag, will also produce some context zettel. Despite its possibly complicated structure, this algorithm ensures in practice that the zettel context is a list of zettel, where the first elements are ""near"" to the specified zettel and the last elements are more ""distant"" to the specified zettel. It also penalties zettel that acts as a ""hub"" to other zettel, to make it more likely that only relevant zettel appear on the context list. |
︙ | ︙ |
Added docs/manual/00001007720500.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | id: 00001007720500 title: Query: Thread Directive role: manual tags: #manual #search #zettelstore syntax: zmk created: 20250624162823 modified: 20250626174233 A thread directive is similar to the [[context directive|00001007720300]] by calculating zettel associated with a list of zettel identifier. There are three thread directives, beginning with the keywords ''FOLGE'', ''SEQUEL'', and ''THREAD''. With the help of the ''FOLGE'' directive, you can list all zettel that belong to the same ""train of thought"". It is possible to list all folge zettel only or only precursor zettel. Additionally, you can specify the maximum number to be listed. A ''FOLGE'' directive considers only the metadata keys only metadata keys [[''folge''|00001006020000#folge]] and [[''precursor''|00001006020000#precursor]]. Similarly, the ''SEQUEL'' directive allows you to list all ""branching ideas"" You can limit the search to only sequel or only prequel zettel, and you can also restrict the result size. The ''SEQUEL'' directive works exclusively with the metadata keys [[''sequel''|00001006020000#sequel]] and [[''prequel''|00001006020000#prequel]]. The ''THREAD'' directive combines the functionality of the other two directives. It lists all zettel (or a subset) that can be reached via the four metadata keys mentioned. Optionally yyou may specify additional parameters after the directive keyword, separated by space characters. These include: * ''BACKWARD'': Search for zettel only via backward links, i.e. ''precursor'' and/or ''prequel'' * ''FORWARD'': Search for zettel only via forward links, i.e. ''folge'' and/or ''sequel'' * ''DIRECTED'': Search for zettel only in the same direction as the one that led to the current zettel. If the zettel is one of the initial zettel, search in both directions. ''DIRECTED'' is equivalent to ''FORWARD'' if the current zettel is a starting point of a ''folge'' and/or ''sequel'' sequence. It is equivalent to ''BACKWARD'' at the end of such a sequence (or at the beginning of a ''precursor'' and/or ''prequel'' sequence). * ''MAX'': Followed by one or more space characters and a positive integer, sets the maximum number of resulting Zettel (default: 0 = unlimited) If neither BACKWARD nor FORWARD is specified, the search includes both backward and forward links. If both ''BACKWARD'' and ''FORWARD'' are specified, the search for context zettel will be performed as ''DIRECTED''. Internally, ''DIRECTED'' is just a shorthand for specifying both ''BACKWARD'' and ''FORWARD''. If any of the three direction specifiers ''BACKWARD'', ''FORWARD'', and ''DIRECTED'' is specified more than once, parsing of the thread directive is stopped. All following text is then interpreted either as other directives or as a search term. The resulting list is sorted by the relative distance to the originating zettel. These directives may be specified only once as a query directive. A second occurence of ''FOLGE'', ''SEQUEL'' or ''THREAD'' is interpreted as a [[search expression|00001007701000]]. |
Changes to docs/manual/00001007780000.zettel.
1 2 3 4 5 6 | id: 00001007780000 title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 | | | > > > > > > > | 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 | id: 00001007780000 title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 modified: 20250626172412 ``` QueryExpression := ZettelList? QueryDirective* SearchExpression ActionExpression? ZettelList := (ZID (SPACE+ ZID)*)?. ZID := '0'+ ('1' .. '9'') DIGIT* | ('1' .. '9') DIGIT*. QueryDirective := ContextDirective | ThreadDirective | IdentDirective | ItemsDirective | UnlinkedDirective. ContextDirective := "CONTEXT" (SPACE+ ContextDetail)*. ContextDetail := "FULL" | "BACKWARD" | "FORWARD" | "DIRECTED" | "COST" SPACE+ PosInt | "MAX" SPACE+ PosInt | "MIN" SPACE+ PosInt. ThreadDirective := ("FOLGE" | "SEQUEL" | "THREAD") (SPACE+ ThreadDetail)*. ThreadDetail := "BACKWARD" | "FORWARD" | "DIRECTED" | "MAX" SPACE+ PosInt. IdentDirective := IDENT. ItemsDirective := ITEMS. UnlinkedDirective := UNLINKED (SPACE+ PHRASE SPACE+ Word)*. SearchExpression := SearchTerm (SPACE+ SearchTerm)*. SearchTerm := SearchOperator? SearchValue | SearchKey SearchOperator SearchValue? | SearchKey ExistOperator |
︙ | ︙ |
Changes to docs/manual/00001007790000.zettel.
1 2 3 4 5 6 | id: 00001007790000 title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 | | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001007790000 title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 modified: 20250627151159 |= Query Expression |= Meaning | [[query:role:configuration]] | All zettel that contain configuration data for the Zettelstore | [[query:ORDER REVERSE created LIMIT 40]] | 40 recently created zettel | [[query:ORDER REVERSE published LIMIT 40]] | 40 recently updated zettel | [[query:PICK 40]] | 40 random zettel, ordered by zettel identifier | [[query:dead?]] | Zettel with invalid / dead links | [[query:backward!? precursor!?]] | Zettel that are not referenced by other zettel | [[query:tags!?]] | Zettel without tags | [[query:expire? ORDER expire]] | All zettel with an expiration date, ordered from the nearest to the latest | [[query:00001007700000 CONTEXT]] | Zettel within the context of the [[given zettel|00001007700000]] | [[query:00001012051200 FOLGE]] | ""Train of thought"" of the [[given zettel|00001012051200]] | [[query:PICK 1 | REDIRECT]] | Redirect to a random zettel |
Changes to docs/manual/00001010040100.zettel.
1 2 3 4 5 6 | id: 00001010040100 title: Enable authentication role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001010040100 title: Enable authentication role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20250627131456 show-back-links: close To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner. Then you must reference this zettel within the [[startup configuration|00001004010000#owner]] under the key ''owner''. Once the startup configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled. Please note that you must also set key ''secret'' of the [[startup configuration|00001004010000#secret]] to a random string (minimum length: 16 bytes) to secure the data exchanged with a client system. |
Changes to docs/manual/00001010070200.zettel.
1 2 3 4 5 6 | id: 00001010070200 title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 | | | | | | 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 | id: 00001010070200 title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20250627134004 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]]. ; [!creator|""creator""] : Only an authenticated user who is allowed to create new zettel can access the zettel. ; [!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|00001006030500]]. This applies to zettel with sensitive content that might irritate the owner. Computed zettel containing internal runtime information are examples of such zettel. When you install a Zettelstore, only [[some zettel|query:visibility:public]] have visibility ""public"". One is the zettel that contains [[CSS|00000000020001]] for displaying the [[web user interface|00001014000000]]. This is to ensure that the web interface looks nice even for not authenticated users. Another is the zettel containing the Zettelstore [[license|00000000000004]]. The [[default image|00000000040001]], used if an image reference is invalid, is also publicly visible. |
︙ | ︙ |
Changes to docs/manual/00001010090100.zettel.
1 2 3 4 5 6 | id: 00001010090100 title: External server to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001010090100 title: External server to encrypt message transport role: manual tags: #configuration #encryption #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20250701125905 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 publishes a public encryption key that only works with a corresponding private decryption key. Technically, this is not trivial. Any client who wants to communicate with your Zettelstore must trust the public encryption key. Otherwise the client cannot be sure that it is communication with your Zettelstore. This problem is solved in part with [[Let's Encrypt|https://letsencrypt.org/]], ""a free, automated, and open certificate authority (CA), run for the public’s benefit. It is a service provided by the [[Internet Security Research Group|https://www.abetterinternet.org/]]"". |
︙ | ︙ | |||
34 35 36 37 38 39 40 | * [[Caddy|https://caddyserver.com/]]: see below for details. Other software is also possible. There exists software dedicated for this task of handling the encryption part. Some examples: * [[stunnel|https://www.stunnel.org/]] (""a proxy designed to add TLS encryption functionality to existing clients and servers without any changes in the programs' code."") | | | 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | * [[Caddy|https://caddyserver.com/]]: see below for details. Other software is also possible. There exists software dedicated for this task of handling the encryption part. Some examples: * [[stunnel|https://www.stunnel.org/]] (""a proxy designed to add TLS encryption functionality to existing clients and servers without any changes in the programs' code."") * [[Traefik|https://traefik.io/]]: set-up a [[router|https://doc.traefik.io/traefik/routing/routers/]]. === Example configuration for Caddy For the inexperienced owner of a Zettelstore, [[Caddy|https://caddyserver.com/]] is a good option[^In fact, the [[server-based installation procedure|00001003000000]] of Zettelstore was inspired by Caddy.]. Caddy has the capability to automatically fetch appropriately encryption key from Let's Encrypt, without any further configuration. The only requirement of doing this is that the server must be publicly accessible. Here is the base configuration for the main site for Zettelstore at [[https://zettelstore.de/]]: |
︙ | ︙ | |||
60 61 62 63 64 65 66 | root /var/www/html } route /manual/* { reverse_proxy localhost:23123 } } ``` | | | 60 61 62 63 64 65 66 67 68 69 70 71 | root /var/www/html } route /manual/* { reverse_proxy localhost:23123 } } ``` This will forward 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 [[startup configuration key ''url-prefix''|00001004010000#url-prefix]] with the value ""/manual/"". This is to allow Zettelstore to ignore the prefix while reading web requests and to give the correct URLs with the given prefix when sending a web response. |
Changes to docs/manual/00001012000000.zettel.
1 2 3 4 5 6 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250627124452 The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore. Most integration with other systems and services is performed via the API. The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore. === Background The API is HTTP-based and uses plain text and [[symbolic expressions|00001012930000]] as its main encoding formats for exchanging messages between a Zettelstore and its client software. There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use. |
︙ | ︙ |
Changes to docs/manual/00001012050200.zettel.
1 2 3 4 5 6 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | 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 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250701134955 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-lifetime-api'' of the [[startup configuration|00001004010000#token-lifetime-api]] (typically 10 minutes). The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://datatracker.ietf.org/doc/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 ("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA2MiwiaWF0IjoxNjgxMzA0MDAyLCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kdF8PdiL50gIPkRD3ovgR6nUXR0-80EKAXcY2zVYgYvryF09iXnNR3zrvYnGzdrArMcnvAYqVvuXtqhQj2jG9g" 600) ``` Some tools, like [[curl|https://curl.se/]], also allow to specify user identification and password as part of the URL: ```sh # curl -X POST http://IDENT:PASSWORD@127.0.0.1:23123/a ("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4NiwiaWF0IjoxNjgxMzA0MDI2LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kZd3prYc79dt9efDsrYVHtKrjWyOWvfByjeeUB3hf_vs43V3SNJqmb8k-zTHVNWOK0-5orVPrg2tIAqbXqmkhg" 600) ``` If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data: ```sh |
︙ | ︙ |
Changes to docs/manual/00001012050600.zettel.
1 2 3 4 5 6 | id: 00001012050600 title: API: Provide an access token role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001012050600 title: API: Provide an access token role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250701134911 The [[authentication process|00001012050200]] provides you with an [[access token|00001012921000]]. Most API calls need such an access token, so that they know the identity of the caller. You send the access token in the ""Authorization"" request header field, as described in [[RFC 6750, section 2.1|https://datatracker.ietf.org/doc/html/rfc6750#section-2.1]]. You need to use the ""Bearer"" authentication scheme to transmit the access token. For example (in plain text HTTP): ``` GET /z HTTP/1.0 Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg ``` Note, that there is exactly one space character (""'' ''{-}"", U+0020) between the string ""Bearer"" and the access token: ``Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.ey...``{-}. If you use the [[curl|https://curl.se/]] tool, you can use the ''-H'' command line parameter to set this header field. |
Changes to docs/manual/00001012051400.zettel.
1 2 3 4 5 6 | id: 00001012051400 title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012051400 title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 modified: 20250602182055 precursor: 00001012051200 The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally specify some actions. A [[query|00001007700000]] consists of an optional [[search expression|00001007700000#search-expression]] and an optional [[list of actions|00001007700000#action-list]] (described below). If no search expression is provided, all zettel are selected. Similarly, if no valid action is specified, or the action list is empty, the list of all selected zettel metadata is returned. |
︙ | ︙ | |||
54 55 56 57 58 59 60 | An implicit precondition is that the zettel must contain the given metadata key. For a metadata key like [[''title''|00001006020000#title]], which have a default value, this precondition should always be true. But the situation is different for a key like [[''url''|00001006020000#url]]. Both ``curl 'http://localhost:23123/z?q=url%3A'`` and ``curl 'http://localhost:23123/z?q=url%3A!'`` may result in an empty list. | | | | | 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 | An implicit precondition is that the zettel must contain the given metadata key. For a metadata key like [[''title''|00001006020000#title]], which have a default value, this precondition should always be true. But the situation is different for a key like [[''url''|00001006020000#url]]. Both ``curl 'http://localhost:23123/z?q=url%3A'`` and ``curl 'http://localhost:23123/z?q=url%3A!'`` may result in an empty list. As an example for a query action, to list all roles used in the Zettelstore, send an HTTP GET request to the endpoint ''/z?q=|role''. ```sh # curl 'http://127.0.0.1:23123/z?q=|role' configuration 00001000000100 00000000090002 00000000090000 00000000040001 00000000025001 00000000020001 00000000000100 00000000000092 00000000000090 00000000000006 00000000000005 00000000000004 00000000000001 manual 00001018000000 00001017000000 00001014000000 00001012921200 00001012921000 00001012920800 00001012920588 00001012920584 00001012920582 00001012920522 00001012920519 00001012920516 00001012920513 00001012920510 00001012920503 00001012920500 00001012920000 00001012080500 00001012080200 00001012080100 00001012070500 00001012054600 00001012054400 00001012054200 00001012054000 00001012053900 00001012053800 00001012053600 00001012053500 00001012053400 00001012053300 00001012053200 00001012051400 00001012051200 00001012050600 00001012050400 00001012050200 00001012000000 00001010090100 00001010070600 00001010070400 00001010070300 00001010070200 00001010040700 00001010040400 00001010040200 00001010040100 00001010000000 00001008050000 00001008010500 00001008010000 00001008000000 00001007990000 00001007906000 00001007903000 00001007900000 00001007800000 00001007790000 00001007780000 00001007706000 00001007705000 00001007702000 00001007700000 00001007050200 00001007050100 00001007050000 00001007040350 00001007040340 00001007040330 00001007040324 00001007040322 00001007040320 00001007040310 00001007040300 00001007040200 00001007040100 00001007040000 00001007031400 00001007031300 00001007031200 00001007031140 00001007031110 00001007031100 00001007031000 00001007030900 00001007030800 00001007030700 00001007030600 00001007030500 00001007030400 00001007030300 00001007030200 00001007030100 00001007030000 00001007020000 00001007010000 00001007000000 00001006055000 00001006050000 00001006036500 00001006036000 00001006035500 00001006035000 00001006034500 00001006034000 00001006033500 00001006033000 00001006032500 00001006032000 00001006031500 00001006031000 00001006030500 00001006030000 00001006020400 00001006020100 00001006020000 00001006010000 00001006000000 00001005090000 00001005000000 00001004101000 00001004100000 00001004059900 00001004059700 00001004051400 00001004051200 00001004051100 00001004051000 00001004050400 00001004050200 00001004050000 00001004020200 00001004020000 00001004011600 00001004011400 00001004011200 00001004010000 00001004000000 00001003600000 00001003315000 00001003310000 00001003305000 00001003300000 00001003000000 00001002000000 00001001000000 00001000000000 zettel 00010000000000 00000000090001 ``` The result is a text file. The first word, separated by a horizontal tab (U+0009) contains the role name. The rest of the line consists of zettel identifier, where the corresponding zettel have this role. Zettel identifiers are separated by a space character (U+0020). Please note that the list is **not** sorted by the role name, so the same request might result in a different order. If you want a sorted list, you could sort it on the command line (``curl 'http://127.0.0.1:23123/z?q=|role' | sort``) or within the software that made the call to the Zettelstore. Of course, this list can also be returned as a data object: ```sh # curl 'http://127.0.0.1:23123/z?q=|role&enc=data' (aggregate "role" (query "| role") (human "| role") ("zettel" 10000000000 90001) ("configuration" 6 100 1000000100 20001 90 25001 92 4 40001 1 90000 5 90002) ("manual" 1008050000 1007031110 1008000000 1012920513 1005000000 1012931800 1010040700 1012931000 1012053600 1006050000 1012050200 1012000000 1012070500 1012920522 1006032500 1006020100 1007906000 1007030300 1012051400 1007040350 1007040324 1007706000 1012931900 1006030500 1004050200 1012054400 1007700000 1004050000 1006020000 1007030400 1012080100 1012920510 1007790000 1010070400 1005090000 1004011400 1006033000 1012930500 1001000000 1007010000 1006020400 1007040300 1010070300 1008010000 1003305000 1006030000 1006034000 1012054200 1012080200 1004010000 1003300000 1006032000 1003310000 1004059700 1007031000 1003600000 1004000000 1007030700 1007000000 1006055000 1007050200 1006036000 1012050600 1006000000 1012053900 1012920500 1004050400 1007031100 1007040340 1007020000 1017000000 1012053200 1007030600 1007040320 1003315000 1012054000 1014000000 1007030800 1010000000 1007903000 1010070200 1004051200 1007040330 1004051100 1004051000 1007050100 1012080500 1012053400 1006035500 1012054600 1004100000 1010040200 1012920000 1012920525 1004051400 1006031500 1012921200 1008010500 1012921000 1018000000 1012051200 1010040100 1012931200 1012920516 1007040310 1007780000 1007030200 1004101000 1012920800 1007030100 1007040200 1012053500 1007040000 1007040322 1007031300 1007031140 1012931600 1012931400 1004059900 1003000000 1006036500 1004020200 1010040400 1006033500 1000000000 1012053300 1007990000 1010090100 1007900000 1007030500 1004011600 1012930000 1007030900 1004020000 1007030000 1010070600 1007040100 1007800000 1012050400 1006010000 1007705000 1007702000 1007050000 1002000000 1007031200 1006035000 1006031000 1006034500 1004011200 1007031400 1012920519)) ``` The data object starts with the symbol ''aggregate'' to signal a different format compared to ''meta-list'' above. Then a string follows, which specifies the key on which the aggregate was performed. ''query'' and ''human'' have the same meaning as above. Then comes the result list of aggregates. Each aggregate starts with a string of the aggregate value, in this case the role value, followed by a list of zettel identifier, denoting zettel which have the given role value. Similarly, to list all tags used in the Zettelstore, send an HTTP GET request to the endpoint ''/z?q=|tags''. If successful, the output is a data object: ```sh # curl 'http://127.0.0.1:23123/z?q=|tags&enc=data' (aggregate "tags" (query "| tags") (human "| tags") ("#zettel" 1006034500 1006034000 1006031000 1006020400 1006033500 1006036500 1006032500 1006020100 1006031500 1006030500 1006035500 1006033000 1006020000 1006036000 1006030000 1006032000 1006035000) ("#reference" 1006034500 1006034000 1007800000 1012920500 1006031000 1012931000 1006020400 1012930000 1006033500 1012920513 1007050100 1012920800 1007780000 1012921000 1012920510 1007990000 1006036500 1006032500 1006020100 1012931400 1012931800 1012920516 1012931600 1012920525 1012931200 1006031500 1012931900 1012920000 1005090000 1012920522 1006030500 1007050200 1012921200 1006035500 1012920519 1006033000 1006020000 1006036000 1006030000 1006032000 1012930500 1006035000) ("#graphic" 1008050000) ("#search" 1007700000 1007705000 1007790000 1007780000 1007702000 1007706000 1007031140) ("#installation" 1003315000 1003310000 1003000000 1003305000 1003300000 1003600000) ("#zettelmarkup" 1007900000 1007030700 1007031300 1007030600 1007800000 1007000000 1007031400 1007040100 1007030300 1007031200 1007040350 1007030400 1007030900 1007050100 1007040000 1007030500 1007903000 1007040200 1007040330 1007990000 1007040320 1007050000 1007040310 1007031100 1007040340 1007020000 1007031110 1007031140 1007040324 1007030800 1007031000 1007030000 1007010000 1007906000 1007050200 1007030100 1007030200 1007040300 1007040322) ("#design" 1005000000 1006000000 1002000000 1006050000 1006055000) ("#markdown" 1008010000 1008010500) ("#goal" 1002000000) ("#syntax" 1006010000) ... ``` |
︙ | ︙ | |||
108 109 110 111 112 113 114 | __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. ; ''MAXn'' (parameter) : Emit only those values with at most __n__ aggregated values. __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. ; ''KEYS'' (aggregate) : Emit a list of all metadata keys, together with the number of zettel having the key. ; ''REDIRECT'' (aggregate) | | | 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. ; ''MAXn'' (parameter) : Emit only those values with at most __n__ aggregated values. __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. ; ''KEYS'' (aggregate) : Emit a list of all metadata keys, together with the number of zettel having the key. ; ''REDIRECT'' (aggregate) : Performs an HTTP redirect to the first selected zettel, using HTTP status code 302. The zettel identifier is in the body. ; ''REINDEX'' (aggregate) : Updates the internal search index for the selected zettel, roughly similar to the [[refresh|00001012080500]] API call. It is not really an aggregate, since it is used only for its side effect. It is allowed to specify another aggregate. ; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or [[TagSet|00001006034000]] (aggregates) : Emit an aggregate of the given metadata key. |
︙ | ︙ |
Changes to docs/manual/00001012054200.zettel.
1 2 3 4 5 6 | id: 00001012054200 title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 | | | | | | | 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: 00001012054200 title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 modified: 20250627125627 Updating metadata and content of a zettel is technically quite similar to [[creating a new one|00001012053200]]. In both cases, you must provide the data for the new or updated zettel in the body of the HTTP request. One difference is the endpoint. The [[endpoint|00001012920000]] to update a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. You must send an HTTP PUT request to that endpoint. The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line. This is the same format used for storing zettel within a directory box. [[directory box|00001006010000]]. ``` # curl -X PUT --data $'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200 ``` === Data input Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''. |
︙ | ︙ | |||
33 34 35 36 37 38 39 | ; ''400'' : Request was not valid. For example, the request body was not valid. ; ''403'' : You are not allowed to delete the given zettel. ; ''404'' : Zettel not found. | | | 33 34 35 36 37 38 39 40 | ; ''400'' : Request was not valid. For example, the request body was not valid. ; ''403'' : You are not allowed to delete the given zettel. ; ''404'' : Zettel not found. You probably used a zettel identifier that does not exist in the Zettelstore. |
Changes to docs/manual/00001012920000.zettel.
1 2 3 4 5 6 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20250627125818 All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is the URL prefix (default: ""/""), configured via the ''url-prefix'' [[startup configuration|00001004010000]], ; ''LETTER'' : is a single letter that specifies the resource type, ; ''ZETTEL-ID'' : is an optional 14-digit string that uniquely [[identifies a zettel|00001006050000]]. The following letters are currently in use: |= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic | ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate | | PUT: [[renew access token|00001012050400]] | | ''r'' | | GET: [[references|00001012053800]] | **R**eference |
︙ | ︙ |
Changes to docs/manual/00001012921000.zettel.
1 2 3 4 5 6 | id: 00001012921000 title: API: Structure of an access token role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001012921000 title: API: Structure of an access token role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20250701134823 If the [[authentication process|00001012050200]] was successful, an access token with some additional data is returned. The same is true, if the access token was [[renewed|00001012050400]]. The response is structured as a [[symbolic expression|00001012930000]] list, with the following elements: # The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://datatracker.ietf.org/doc/html/rfc6750]] # The token itself, which is technically the string representation of a [[symbolic expression|00001012930500]] containing relevant data, plus a check sum. #* The symbolic expression has the form ''(KIND USERNAME NOW EXPIRE Z-ID)'' #* ''KIND'' is ''0'' for an API access, ''1'' if it created for the Web user interface. #* ''USERNAME'' is the user name of the user. #* ''NOW'' is a timestamp of the current time. #* ''EXPIRE'' is the timestamp when the access token expires. #* ''Z-ID'' is the zettel identifier of the user zettel. |
︙ | ︙ |
Changes to docs/manual/00001012921200.zettel.
1 2 3 4 5 6 | id: 00001012921200 title: API: Encoding of Zettel Access Rights role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20220201173115 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012921200 title: API: Encoding of Zettel Access Rights role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20220201173115 modified: 20250602181802 Various API calls return a symbolic expression list ''(rights N)'', with ''N'' as a number, that encodes the access rights the user currently has. ''N'' is an integer number between 0 and 62.[^Not all values in this range are used.] The value ""0"" signals that something went wrong internally while determining the access rights. A value of ""1"" says, that the current user has no access right for the given zettel. |
︙ | ︙ | |||
36 37 38 39 40 41 42 | # The next right is the right to update a zettel (16 > 10, but 8 < 10). The new value of the rights value is now 2 (10-8). # The last right is the right to create a new zettel. The rights value is now zero, the algorithm ends. In practice, not every rights value will occur. A Zettelstore in [[read-only mode|00001010000000#read-only]]Â will always return the value 4. | | | 36 37 38 39 40 41 42 43 44 45 46 | # The next right is the right to update a zettel (16 > 10, but 8 < 10). The new value of the rights value is now 2 (10-8). # The last right is the right to create a new zettel. The rights value is now zero, the algorithm ends. In practice, not every rights value will occur. A Zettelstore in [[read-only mode|00001010000000#read-only]]Â will always return the value 4. Similarly, a Zettelstore that you started with a [[double-click|00001003000000]] will return either the value ""6"" (read and update) or the value ""62"" (all operations are allowed). If you have added an additional [[user|00001010040200]] to your Zettelstore, this might change. The access rights are calculated depending on [[enabled authentication|00001010040100]], on the [[user role|00001010070300]] of the current user, on [[visibility rules|00001010070200]] for a given zettel and on the [[read-only status|00001006020400]] for the zettel. |
Changes to docs/manual/00001012930000.zettel.
1 2 3 4 5 6 | id: 00001012930000 title: Symbolic Expression role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20230403145644 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001012930000 title: Symbolic Expression role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20230403145644 modified: 20250627124639 A symbolic expression (also called __s-expression__) is a notation of a list-based tree. Inner nodes are lists of arbitrary length, while outer nodes are either primitive values (also called __atoms__) or the empty list. A symbolic expression is either * a primitive value (__atom__), or * a list of the form __(E,,1,, E,,2,, … E,,n,,)__, where __E,,i,,__ is itself a symbolic expression, separated by space characters. An atom is a number, a string, or a symbol. |
︙ | ︙ |
Changes to docs/manual/00001012930500.zettel.
1 2 3 4 5 6 | id: 00001012930500 title: Syntax of Symbolic Expressions role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20230403151127 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012930500 title: Syntax of Symbolic Expressions role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20230403151127 modified: 20250627124837 === Syntax of lists A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029). A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms. Internally, lists are composed of __cells__. A cell allows to store two values. |
︙ | ︙ | |||
29 30 31 32 33 34 35 | v v v +-------+ +-------+ +-------+ | Elem1 | | Elem2 | | ElemN | +-------+ +-------+ +-------+ ~~~ ''V'' is a placeholder for a value, ''N'' is the reference to the next cell (also known as the rest / tail of the list). | | | 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | v v v +-------+ +-------+ +-------+ | Elem1 | | Elem2 | | ElemN | +-------+ +-------+ +-------+ ~~~ ''V'' is a placeholder for a value, ''N'' is the reference to the next cell (also known as the rest / tail of the list). The list above is represented as a symbolic expression in the form ''(Elem1 Elem2 ... ElemN)'' An improper list will have a non-__nil__ reference to an atom as the very last element ~~~draw +---+---+ +---+---+ +---+---+ | V | N +-->| V | N +--> -->| V | V | +-+-+---+ +-+-+---+ +-+-+-+-+ |
︙ | ︙ |
Changes to docs/manual/00001017000000.zettel.
1 2 3 4 5 6 | id: 00001017000000 title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk created: 20220803170112 | | | | | | | | | | | | | 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 | id: 00001017000000 title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk created: 20220803170112 modified: 20250627133713 === Welcome Zettel * **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users. In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel. Zettelstore only allows specifying one [[''home-zettel''|00001004020000#home-zettel]]. * **Solution 1:** *# Create a new zettel with all your references to internal, non-public zettel. Let's assume this zettel receives the zettel identifier ''20220803182600''. *# Create the zettel that will serve as the starting zettel for your users. It must use the [[Zettelmarkup|00001008000000#zmk]] syntax, i.e. the syntax metadata must be set to ''zmk''. If needed, set the runtime configuration [[''home-zettel''|00001004020000#home-zettel]] to the identifier of this zettel. *# At the beginning of the start zettel, add the following [[Zettelmarkup|00001007000000]] text in a separate paragraph: ``{{{20220803182600}}}`` (you have to adapt to the actual value of the zettel identifier for your non-public home zettel). * **Discussion:** As stated in the description for a [[transclusion|00001007031100]], a transclusion will be ignored, if the transcluded zettel is not visible to the current user. In effect, the transclusion statement (above paragraph that contained ''{{{...}}}'') is ignored when rendering the zettel. * **Solution 2:** Set a user-specific value by adding metadata ''home-zettel'' to the [[user zettel|00001010040200]]. * **Discussion:** A value for ''home-zettel'' is first searched for in the user zettel of the currently authenticated user. Only if it is not found there, the value is looked up in the runtime configuration zettel. If multiple users should share the same home zettel, its zettel identifier must be set in all relevant user zettel. === Role-specific Layout of Zettel in Web User Interface (WebUI) [!role-css] * **Problem:** You want to add some CSS when displaying zettel of a specific [[role|00001006020000#role]]. For example, you might want to add a yellow background color for all [[configuration|00001006020100#configuration]] zettel. Or you want a multi-column layout. * **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Sxn Start Code|00000000019000]]"" (its identifier is ''00000000019000''). This zettel is the starting point for Sxn code, where you can place a definition for a variable named ""CSS-ROLE-map"". But first, create a zettel containing the needed CSS: give it any title, its role is preferably ""configuration"" (but this is not a must). Its [[''syntax''|00001006020000#syntax]]Â must be set to ""[[css|00001008000000#css]]"". The content must contain the role-specific CSS code, for example ``body {background-color: #FFFFD0}``for a background in a light yellow color. Let's assume, the newly created CSS zettel got the identifier ''20220825200100''. Now, you have to map this freshly created zettel to a role, for example ""zettel"". Since you have enabled ''expert-mode'', you are allowed to modify the zettel ""[[Zettelstore Sxn Start Code|00000000019000]]"". Add the following code to the Sxn Start Code zettel: ``(set! CSS-ROLE-map '(("zettel" . "20220825200100")))``. In general, the mapping must follow the pattern: ``(ROLE . ID)``, where ''ROLE'' is a placeholder for the role, and ''ID'' is the identifier of the zettel containing CSS code. For example, if you also want the role ""configuration"" to be rendered using that CSS, the code should look like this: ``(set! CSS-ROLE-map '(("zettel" . "20220825200100") ("configuration" . "20220825200100")))``. * **Discussion:** you have to ensure that the CSS zettel is accessible to the intended audience of the zettel with the given role. For example, if you made a zettel with a specific role publicly visible, the CSS zettel must also have a [[''visibility: public''|00001010070200]] metadata entry. === Zettel synchronization with iCloud (Apple) * **Problem:** You use Zettelstore on various macOS computers and you want to use the same set of zettel across all computers. * **Solution:** Place your zettel in an iCloud folder. To configure Zettelstore to use the folder, you must specify its location within your directory structure as [[''box-uri-X''|00001004010000#box-uri-x]] (replace ''X'' with an appropriate number). Your iCloud folder is typically placed in the folder ''~/Library/Mobile Documents/com~apple~CloudDocs''. |
︙ | ︙ |
Changes to docs/manual/00001018000000.zettel.
1 2 3 4 5 6 | id: 00001018000000 title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk created: 20211027105921 | | | | 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 | id: 00001018000000 title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk created: 20211027105921 modified: 20250627130445 This page lists some problems and their solutions that may occur when using your Zettelstore. === Installation * **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer. Therefore, it will not start Zettelstore. ** **Solution:** Press the ''Ctrl'' key while opening the context menu of the Zettelstore executable with a right-click. A dialog is then opened where you can acknowledge that you understand the possible risks when you start Zettelstore. This dialog is only presented once for a given Zettelstore executable. * **Problem:** When you double-click on the Zettelstore executable icon, Windows complains that Zettelstore is an application from an unknown developer. ** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allow to start Zettelstore. === Authentication * **Problem:** [[Authentication is enabled|00001010040100]] for a local running Zettelstore and there is a valid [[user zettel|00001010040200]] for the owner. But entering user name and password at the [[web user interface|00001014000000]] seems to be ignored, while entering a wrong password will result in an error message. ** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using a URL with schema ''http://'', and not ''https://'', for example ''http://localhost:23123/''. The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http://'' schema. To be secure by default, the Zettelstore will not work in an insecure environment. ** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in your [[startup configuration|00001004010000#insecure-cookie]] file. ** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''https://'' schema. === Working with Zettel Files * **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes does not detect the change. |
︙ | ︙ |
Changes to go.mod.
1 2 3 4 5 6 | module zettelstore.de/z go 1.24 require ( github.com/fsnotify/fsnotify v1.9.0 | | | | < | | | | | | > | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | module zettelstore.de/z go 1.24 require ( github.com/fsnotify/fsnotify v1.9.0 github.com/yuin/goldmark v1.7.12 golang.org/x/crypto v0.39.0 golang.org/x/term v0.32.0 t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb t73f.de/r/sxwebs v0.0.0-20250707071704-c44197610ee4 t73f.de/r/webs v0.0.0-20250707071548-227f3e99db55 t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455 t73f.de/r/zsc v0.0.0-20250707072124-be388711ad2a t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7 ) require ( golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect ) |
Changes to go.sum.
1 2 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= | | | | | | | | | | | | | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY= github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb h1:cYvTOpaJinh/EPB7i8nx7PtT7hniuSP+NZr74P9U+fE= t73f.de/r/sx v0.0.0-20250707071435-95b82f7d24bb/go.mod h1:uglbFdRHlcpQVVyCNh4Fd7jbKo8alGBCjRp0aZv8IIg= t73f.de/r/sxwebs v0.0.0-20250707071704-c44197610ee4 h1:WbT8qQAQjqx1S0syci6yWi+YiNQFFYVBkOSfSYIRid4= t73f.de/r/sxwebs v0.0.0-20250707071704-c44197610ee4/go.mod h1:zSel+qtBHV9NglxDHlFFPwvaetlZh4H0pLyd7OkpkpQ= t73f.de/r/webs v0.0.0-20250707071548-227f3e99db55 h1:gl4XpbrzrtAFDY+4V9bixLTUruhzEcwvfKiZi1NqJY4= t73f.de/r/webs v0.0.0-20250707071548-227f3e99db55/go.mod h1:b8/5E5Pe6WSWqh+T+sxLO5ZLiGVkuL5tgh86kx2OAIg= t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455 h1:TFRPPexX2WrwuF03hC+Be2ONx2bPzMMBlNDn0rk88eI= t73f.de/r/zero v0.0.0-20250703105709-bb38976d4455/go.mod h1:Ovx7CYsjz45BNuIEMGZfqA7NdQxERydJqUGnOBoQaXQ= t73f.de/r/zsc v0.0.0-20250707072124-be388711ad2a h1:p12BTQ8TdKZy6GcCRgVINKSeoMz0wBPcNG7ssYvLNrc= t73f.de/r/zsc v0.0.0-20250707072124-be388711ad2a/go.mod h1:JnkeoahGBxNK0gDcTnAIfDP2rAP33BtnAU1/rCEewC8= t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7 h1:ERxpb1Hqln+NXoZDK6sqjmX3BzeoLO+O64f4bK0B6dk= t73f.de/r/zsx v0.0.0-20250707071920-5e29047e4db7/go.mod h1:64/AjQ1GnEBoBhXI1D0bDMGDj7JCbtZUTT3WoA7kS0s= |
Changes to internal/auth/impl/impl.go.
︙ | ︙ | |||
55 56 57 58 59 60 61 | kernel.CoreGoArch, kernel.CoreVersion, } func calcSecret(extSecret string) []byte { h := fnv.New128() if extSecret != "" { | | | | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | kernel.CoreGoArch, kernel.CoreVersion, } func calcSecret(extSecret string) []byte { h := fnv.New128() if extSecret != "" { _, _ = io.WriteString(h, extSecret) } for _, key := range configKeys { _, _ = io.WriteString(h, kernel.Main.GetConfig(kernel.CoreService, key).(string)) } return h.Sum(nil) } // IsReadonly returns true, if the systems is configured to run in read-only-mode. func (a *myAuth) IsReadonly() bool { return a.readonly } |
︙ | ︙ |
Changes to internal/auth/policy/box.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 | "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/query" | > < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) // BoxWithPolicy wraps the given box inside a policy box. func BoxWithPolicy(manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig) (box.Box, auth.Policy) { pol := newPolicy(manager, authConfig) return newBox(box, pol), pol |
︙ | ︙ | |||
53 54 55 56 57 58 59 | } func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } func (pp *polBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { | | | | | | | | 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 | } func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } func (pp *polBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { user := user.GetCurrentUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.box.CreateZettel(ctx, zettel) } return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid) } func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { z, err := pp.box.GetZettel(ctx, zid) if err != nil { return zettel.Zettel{}, err } user := user.GetCurrentUser(ctx) if pp.policy.CanRead(user, z.Meta) { return z, nil } return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) } func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { return pp.box.GetAllZettel(ctx, zid) } func (pp *polBox) FetchZids(ctx context.Context) (*idset.Set, error) { return nil, box.NewErrNotAllowed("fetch-zids", user.GetCurrentUser(ctx), id.Invalid) } func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { return nil, err } user := user.GetCurrentUser(ctx) if pp.policy.CanRead(user, m) { return m, nil } return nil, box.NewErrNotAllowed("GetMeta", user, zid) } func (pp *polBox) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { user := user.GetCurrentUser(ctx) canRead := pp.policy.CanRead q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) return pp.box.SelectMeta(ctx, metaSeq, q) } func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { return pp.box.CanUpdateZettel(ctx, zettel) } func (pp *polBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { zid := zettel.Meta.Zid user := user.GetCurrentUser(ctx) if !zid.IsValid() { return box.ErrInvalidZid{Zid: zid.String()} } // Write existing zettel oldZettel, err := pp.box.GetZettel(ctx, zid) if err != nil { return err |
︙ | ︙ | |||
129 130 131 132 133 134 135 | } func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error { z, err := pp.box.GetZettel(ctx, zid) if err != nil { return err } | | | | | 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 | } func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error { z, err := pp.box.GetZettel(ctx, zid) if err != nil { return err } user := user.GetCurrentUser(ctx) if pp.policy.CanDelete(user, z.Meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } func (pp *polBox) Refresh(ctx context.Context) error { user := user.GetCurrentUser(ctx) if pp.policy.CanRefresh(user) { return pp.box.Refresh(ctx) } return box.NewErrNotAllowed("Refresh", user, id.Invalid) } func (pp *polBox) ReIndex(ctx context.Context, zid id.Zid) error { user := user.GetCurrentUser(ctx) if pp.policy.CanRefresh(user) { // If a user is allowed to refresh all data, it it also allowed to re-index a zettel. return pp.box.ReIndex(ctx, zid) } return box.NewErrNotAllowed("ReIndex", user, zid) } |
Added internal/auth/user/user.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 | //----------------------------------------------------------------------------- // Copyright (c) 2025-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2025-present Detlef Stern //----------------------------------------------------------------------------- // Package user provides services for working with user data. package user import ( "context" "time" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" ) // 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 { if ctx != nil { if data, ok := ctx.Value(ctxKeyTypeSession{}).(*AuthData); ok { return data } } return nil } // GetCurrentUser returns the metadata of the current user, or nil if there is no one. func GetCurrentUser(ctx context.Context) *meta.Meta { if data := GetAuthData(ctx); data != nil { return data.User } return nil } // ctxKeyTypeSession is just an additional type to make context value retrieval unambiguous. type ctxKeyTypeSession struct{} // UpdateContext enriches the given context with some data of the current user. func UpdateContext(ctx context.Context, user *meta.Meta, data *auth.TokenData) context.Context { if data == nil { return context.WithValue(ctx, ctxKeyTypeSession{}, &AuthData{User: user}) } return context.WithValue( ctx, ctxKeyTypeSession{}, &AuthData{ User: user, Token: data.Token, Now: data.Now, Issued: data.Issued, Expires: data.Expires, }) } |
Changes to internal/box/box.go.
︙ | ︙ | |||
237 238 239 240 241 242 243 | Enrich(ctx context.Context, m *meta.Meta, boxNumber int) } // NoEnrichContext will signal an enricher that nothing has to be done. // This is useful for an Indexer, but also for some box.Box calls, when // just the plain metadata is needed. func NoEnrichContext(ctx context.Context) context.Context { | | < < | | 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | Enrich(ctx context.Context, m *meta.Meta, boxNumber int) } // NoEnrichContext will signal an enricher that nothing has to be done. // This is useful for an Indexer, but also for some box.Box calls, when // just the plain metadata is needed. func NoEnrichContext(ctx context.Context) context.Context { return context.WithValue(ctx, ctxNoEnrichType{}, ctx) } type ctxNoEnrichType struct{} // DoEnrich determines if the context is not marked to not enrich metadata. func DoEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichType{}).(*ctxNoEnrichType) return !ok } // NoEnrichQuery provides a context that signals not to enrich, if the query does not need this. func NoEnrichQuery(ctx context.Context, q *query.Query) context.Context { if q.EnrichNeeded() { return ctx |
︙ | ︙ |
Changes to internal/box/compbox/compbox.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/kernel" | > | | | 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 | //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "context" "log/slog" "net/url" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) func init() { manager.Register( " comp", func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return getCompBox(cdata.Number, cdata.Enricher), nil }) } type compBox struct { logger *slog.Logger number int enricher box.Enricher } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta |
︙ | ︙ | |||
66 67 68 69 70 71 72 | id.ZidParser: {genParserM, genParserC}, id.ZidStartupConfiguration: {genConfigZettelM, genConfigZettelC}, } // Get returns the one program box. func getCompBox(boxNumber int, mf box.Enricher) *compBox { return &compBox{ | | < | | | | | | 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 | id.ZidParser: {genParserM, genParserC}, id.ZidStartupConfiguration: {genConfigZettelM, genConfigZettelC}, } // Get returns the one program box. func getCompBox(boxNumber int, mf box.Enricher) *compBox { return &compBox{ logger: kernel.Main.GetLogger(kernel.BoxService).With("box", "comp", "boxnum", boxNumber), number: boxNumber, enricher: mf, } } // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } func (*compBox) Location() string { return "" } func (cb *compBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { if gen, ok := myZettel[zid]; ok && gen.meta != nil { if m := gen.meta(zid); m != nil { updateMeta(m) if genContent := gen.content; genContent != nil { logging.LogTrace(cb.logger, "GetZettel/Content") return zettel.Zettel{ Meta: m, Content: zettel.NewContent(genContent(ctx, cb)), }, nil } logging.LogTrace(cb.logger, "GetZettel/NoContent") return zettel.Zettel{Meta: m}, nil } } err := box.ErrZettelNotFound{Zid: zid} logging.LogTrace(cb.logger, "GetZettel/Err", "err", err) return zettel.Zettel{}, err } func (*compBox) HasZettel(_ context.Context, zid id.Zid) bool { _, found := myZettel[zid] return found } func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { logging.LogTrace(cb.logger, "ApplyZid", "entries", len(myZettel)) for zid, gen := range myZettel { if !constraint(zid) { continue } if genMeta := gen.meta; genMeta != nil { if genMeta(zid) != nil { handle(zid) } } } return nil } func (cb *compBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { logging.LogTrace(cb.logger, "ApplyMeta", "entries", len(myZettel)) for zid, gen := range myZettel { if !constraint(zid) { continue } if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) |
︙ | ︙ | |||
143 144 145 146 147 148 149 | func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { if _, ok := myZettel[zid]; ok { err = box.ErrReadOnly } else { err = box.ErrZettelNotFound{Zid: zid} } | | | | 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { if _, ok := myZettel[zid]; ok { err = box.ErrReadOnly } else { err = box.ErrZettelNotFound{Zid: zid} } logging.LogTrace(cb.logger, "DeleteZettel", "err", err) return err } func (cb *compBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(myZettel) logging.LogTrace(cb.logger, "ReadStats", "zettel", st.Zettel) } func getTitledMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, meta.Value(title)) return m } |
︙ | ︙ |
Changes to internal/box/compbox/log.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 42 | import ( "bytes" "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/kernel" ) func genLogM(zid id.Zid) *meta.Meta { m := getTitledMeta(zid, "Zettelstore Log") m.Set(meta.KeySyntax, meta.ValueSyntaxText) m.Set(meta.KeyModified, meta.Value(kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout))) return m } func genLogC(context.Context, *compBox) []byte { const tsFormat = "2006-01-02 15:04:05.999999" entries := kernel.Main.RetrieveLogEntries() var buf bytes.Buffer for _, entry := range entries { ts := entry.TS.Format(tsFormat) buf.WriteString(ts) for j := len(ts); j < len(tsFormat); j++ { buf.WriteByte('0') } buf.WriteByte(' ') | > | > > | 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 | import ( "bytes" "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logging" ) func genLogM(zid id.Zid) *meta.Meta { m := getTitledMeta(zid, "Zettelstore Log") m.Set(meta.KeySyntax, meta.ValueSyntaxText) m.Set(meta.KeyModified, meta.Value(kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout))) return m } func genLogC(context.Context, *compBox) []byte { const tsFormat = "2006-01-02 15:04:05.999999" entries := kernel.Main.RetrieveLogEntries() var buf bytes.Buffer for _, entry := range entries { ts := entry.TS.Format(tsFormat) buf.WriteString(ts) for j := len(ts); j < len(tsFormat); j++ { buf.WriteByte('0') } buf.WriteByte(' ') buf.WriteString(logging.LevelStringPad(entry.Level)) buf.WriteByte(' ') buf.WriteString(entry.Prefix) buf.WriteByte(' ') buf.WriteString(entry.Message) buf.WriteByte(' ') buf.WriteString(entry.Details) buf.WriteByte('\n') } return buf.Bytes() } |
Changes to internal/box/compbox/sx.go.
︙ | ︙ | |||
26 27 28 29 30 31 32 | func genSxM(zid id.Zid) *meta.Meta { return getTitledMeta(zid, "Zettelstore Sx Engine") } func genSxC(context.Context, *compBox) []byte { var buf bytes.Buffer buf.WriteString("|=Name|=Value>\n") | > > > | > > > > | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | func genSxM(zid id.Zid) *meta.Meta { return getTitledMeta(zid, "Zettelstore Sx Engine") } func genSxC(context.Context, *compBox) []byte { var buf bytes.Buffer buf.WriteString("|=Name|=Value>\n") numSymbols := 0 for pkg := range sx.AllPackages() { if size := pkg.Size(); size > 0 { fmt.Fprintf(&buf, "|Symbols in package %q|%d\n", pkg.Name(), size) numSymbols += size } } fmt.Fprintf(&buf, "|All symbols|%d\n", numSymbols) return buf.Bytes() } |
Changes to internal/box/constbox/base.sxn.
︙ | ︙ | |||
17 18 19 20 21 22 23 | (meta (@ (charset "utf-8"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0"))) (meta (@ (name "generator") (content "Zettelstore"))) (meta (@ (name "format-detection") (content "telephone=no"))) ,@META-HEADER (link (@ (rel "stylesheet") (href ,css-base-url))) (link (@ (rel "stylesheet") (href ,css-user-url))) | | > | | 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 | (meta (@ (charset "utf-8"))) (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0"))) (meta (@ (name "generator") (content "Zettelstore"))) (meta (@ (name "format-detection") (content "telephone=no"))) ,@META-HEADER (link (@ (rel "stylesheet") (href ,css-base-url))) (link (@ (rel "stylesheet") (href ,css-user-url))) ,@(ROLE-DEFAULT-meta (current-frame)) ,@(let* ((frame (current-frame))(rem (resolve-symbol 'ROLE-EXTRA-meta frame))) (if (defined? rem) (rem frame))) (title ,title)) (body (nav (@ (class "zs-menu")) (a (@ (href ,home-url)) "Home") ,@(if with-auth `((div (@ (class "zs-dropdown")) (button "User") (nav (@ (class "zs-dropdown-content")) ,@(if user-is-valid `((a (@ (href ,user-zettel-url)) ,user-ident) (a (@ (href ,logout-url)) "Logout")) `((a (@ (href ,login-url)) "Login")) ) ))) ) (div (@ (class "zs-dropdown")) (button "Lists") (nav (@ (class "zs-dropdown-content")) ,@list-urls ,@(if (symbol-bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh"))) )) ,@(if new-zettel-links `((div (@ (class "zs-dropdown")) (button "New") (nav (@ (class "zs-dropdown-content")) ,@(map wui-link new-zettel-links) ))) |
︙ | ︙ |
Changes to internal/box/constbox/constbox.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // Package constbox puts zettel inside the executable. package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/kernel" | > | | < | | | | | | | | 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 | // Package constbox puts zettel inside the executable. package constbox import ( "context" _ "embed" // Allow to embed file content "log/slog" "net/url" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) func init() { manager.Register( " const", func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &constBox{ logger: kernel.Main.GetLogger(kernel.BoxService).With("box", "const", "boxnum", cdata.Number), number: cdata.Number, zettel: constZettelMap, enricher: cdata.Enricher, }, nil }) } type constHeader map[string]string type constZettel struct { header constHeader content zettel.Content } type constBox struct { logger *slog.Logger number int zettel map[id.Zid]constZettel enricher box.Enricher } func (*constBox) Location() string { return "const:" } func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { if z, ok := cb.zettel[zid]; ok { logging.LogTrace(cb.logger, "GetZettel") return zettel.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil } err := box.ErrZettelNotFound{Zid: zid} logging.LogTrace(cb.logger, "GetZettel/Err", "err", err) return zettel.Zettel{}, err } func (cb *constBox) HasZettel(_ context.Context, zid id.Zid) bool { _, found := cb.zettel[zid] return found } func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { logging.LogTrace(cb.logger, "ApplyZid", "entries", len(cb.zettel)) for zid := range cb.zettel { if constraint(zid) { handle(zid) } } return nil } func (cb *constBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { logging.LogTrace(cb.logger, "ApplyMeta", "entries", len(cb.zettel)) for zid, zettel := range cb.zettel { if constraint(zid) { m := meta.NewWithData(zid, zettel.header) cb.enricher.Enrich(ctx, m, cb.number) handle(m) } } return nil } func (*constBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { if _, ok := cb.zettel[zid]; ok { err = box.ErrReadOnly } else { err = box.ErrZettelNotFound{Zid: zid} } logging.LogTrace(cb.logger, "DeleteZettel", logging.Err(err)) return err } func (cb *constBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(cb.zettel) logging.LogTrace(cb.logger, "ReadStats", "zettel", st.Zettel) } var constZettelMap = map[id.Zid]constZettel{ id.ZidConfiguration: { constHeader{ meta.KeyTitle: "Zettelstore Runtime Configuration", meta.KeyRole: meta.ValueRoleConfiguration, |
︙ | ︙ | |||
153 154 155 156 157 158 159 | meta.KeyTitle: "Zettelstore Dependencies", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeyCreated: "20210504135842", | | | | | | | | | 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 | meta.KeyTitle: "Zettelstore Dependencies", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeyCreated: "20210504135842", meta.KeyModified: "20250623150400", }, zettel.NewContent(contentDependencies)}, id.ZidBaseTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Base HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230510155100", meta.KeyModified: "20250623131400", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentBaseSxn)}, id.ZidLoginTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Login Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentLoginSxn)}, id.ZidZettelTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230510155300", meta.KeyModified: "20250626113800", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentZettelSxn)}, id.ZidInfoTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Info HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20250624160000", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentInfoSxn)}, id.ZidFormTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20250612180300", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentFormSxn)}, id.ZidDeleteTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Delete HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20250612180200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentDeleteSxn)}, id.ZidListTemplate: { constHeader{ meta.KeyTitle: "Zettelstore List Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230704122100", meta.KeyModified: "20250612180200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentListZettelSxn)}, id.ZidErrorTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Error HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, |
︙ | ︙ | |||
253 254 255 256 257 258 259 | zettel.NewContent(contentStartCodeSxn)}, id.ZidSxnBase: { constHeader{ meta.KeyTitle: "Zettelstore Sxn Base Code", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230619132800", | | < < < < < < < < < < < < | | 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | zettel.NewContent(contentStartCodeSxn)}, id.ZidSxnBase: { constHeader{ meta.KeyTitle: "Zettelstore Sxn Base Code", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230619132800", meta.KeyModified: "20250624200200", meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentBaseCodeSxn)}, id.ZidBaseCSS: { constHeader{ meta.KeyTitle: "Zettelstore Base CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxCSS, meta.KeyCreated: "20200804111624", meta.KeyModified: "20240827143500", |
︙ | ︙ | |||
466 467 468 469 470 471 472 | //go:embed start.sxn var contentStartCodeSxn []byte //go:embed wuicode.sxn var contentBaseCodeSxn []byte | < < < | 454 455 456 457 458 459 460 461 462 463 464 465 466 467 | //go:embed start.sxn var contentStartCodeSxn []byte //go:embed wuicode.sxn var contentBaseCodeSxn []byte //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte //go:embed menu_lists.zettel |
︙ | ︙ |
Changes to internal/box/constbox/delete.sxn.
︙ | ︙ | |||
23 24 25 26 27 28 29 | ,@(if incoming `((div (@ (class "zs-warning")) (h2 "Warning!") (p "If you delete this zettel, incoming references from the following zettel will become invalid.") (ul ,@(map wui-item-link incoming)) )) ) | | | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | ,@(if incoming `((div (@ (class "zs-warning")) (h2 "Warning!") (p "If you delete this zettel, incoming references from the following zettel will become invalid.") (ul ,@(map wui-item-link incoming)) )) ) ,@(if (and (symbol-bound? 'useless) useless) `((div (@ (class "zs-warning")) (h2 "Warning!") (p "Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") (ul ,@(map wui-item useless)) )) ) ,(wui-meta-desc metapairs) |
︙ | ︙ |
Changes to internal/box/constbox/dependencies.zettel.
︙ | ︙ | |||
126 127 128 129 130 131 132 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` | | > > | 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 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` === Sx, SxWebs, Webs, Zero, Zettelstore-Client, Zsx These are companion projects, written by the main developer of Zettelstore. They are published under the same license, [[EUPL v1.2, or later|00000000000004]]. ; URL & Source Sx : [[https://t73f.de/r/sx]] ; URL & Source SxWebs : [[https://t73f.de/r/sxwebs]] ; URL & Source Webs : [[https://t73f.de/r/webs]] ; URL & Source Zero : [[https://t73f.de/r/zero]] ; URL & Source Zettelstore-Client : [[https://t73f.de/r/zsc]] ; URL & Source Zsx : [[https://t73f.de/r/zsx]] ; License: : European Union Public License, version 1.2 (EUPL v1.2), or later. |
Changes to internal/box/constbox/form.sxn.
︙ | ︙ | |||
43 44 45 46 47 48 49 | (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-syntax") (name "syntax") (title "Syntax/format of zettel content below, one word, letters and digits, no spaces.") (placeholder "syntax..") (value ,meta-syntax) (dir "auto") ,@(if syntax-data '((list "zs-syntax-data"))) )) ,@(wui-datalist "zs-syntax-data" syntax-data) ) | | | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-syntax") (name "syntax") (title "Syntax/format of zettel content below, one word, letters and digits, no spaces.") (placeholder "syntax..") (value ,meta-syntax) (dir "auto") ,@(if syntax-data '((list "zs-syntax-data"))) )) ,@(wui-datalist "zs-syntax-data" syntax-data) ) ,@(if (symbol-bound? 'content) `((div (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "ⓘ"))) (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") (title "Zettel content, according to the given syntax") (placeholder "Zettel content..") (dir "auto")) ,content) )) ) |
︙ | ︙ |
Changes to internal/box/constbox/info.sxn.
︙ | ︙ | |||
11 12 13 14 15 16 17 | ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 "Information for Zettel " ,zid) (p (a (@ (href ,web-url)) "Web") | | | > | > > > | > | | | 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 | ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 "Information for Zettel " ,zid) (p (a (@ (href ,web-url)) "Web") ,@(if (symbol-bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit"))) (@H " · ") (a (@ (href ,context-full-url)) "Full Context") ,@(if (symbol-bound? 'thread-query-url) `((@H " · [") (a (@ (href ,thread-query-url)) "Thread") ,@(if (symbol-bound? 'folge-query-url) `((@H ", ") (a (@ (href ,folge-query-url)) "Folge"))) ,@(if (symbol-bound? 'sequel-query-url) `((@H ", ") (a (@ (href ,sequel-query-url)) "Sequel"))) (@H "]"))) ,@(ROLE-DEFAULT-actions (current-frame)) ,@(let* ((frame (current-frame))(rea (resolve-symbol 'ROLE-EXTRA-actions frame))) (if (defined? rea) (rea frame))) ,@(if (symbol-bound? 'reindex-url) `((@H " · ") (a (@ (href ,reindex-url)) "Reindex"))) ,@(if (symbol-bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete"))) ) ) (h2 "Interpreted Metadata") (table ,@(map wui-info-meta-table-row metadata)) (h2 "References") ,@(if local-links `((h3 "Local") (ul ,@(map wui-local-link local-links)))) ,@(if query-links `((h3 "Queries") (ul ,@(map wui-item-link query-links)))) |
︙ | ︙ |
Changes to internal/box/constbox/listzettel.sxn.
︙ | ︙ | |||
13 14 15 16 17 18 19 | `(article (header (h1 ,heading)) (search (form (@ (action ,search-url)) (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query) (title "Contains the search that leads to the list below. You're allowed to modify it") (placeholder "Search..") (value ,query-value) (dir "auto"))))) | | | | | | | | | 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 | `(article (header (h1 ,heading)) (search (form (@ (action ,search-url)) (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query) (title "Contains the search that leads to the list below. You're allowed to modify it") (placeholder "Search..") (value ,query-value) (dir "auto"))))) ,@(if (symbol-bound? 'tag-zettel) `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel)) ) ,@(if (symbol-bound? 'create-tag-zettel) `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel)) ) ,@(if (symbol-bound? 'role-zettel) `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel)) ) ,@(if (symbol-bound? 'create-role-zettel) `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel)) ) ,@content ,@endnotes (form (@ (action ,(if (symbol-bound? 'create-url) create-url))) ,(if (symbol-bound? 'data-url) `(@L "Other encodings" ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ") (a (@ (href ,data-url)) "data") ", " (a (@ (href ,plain-url)) "plain") ) ) ,@(if (symbol-bound? 'create-url) `((input (@ (type "hidden") (name ,query-key-query) (value ,query-value))) (input (@ (type "hidden") (name ,query-key-seed) (value ,seed))) (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel"))) ) ) ) ) |
Deleted internal/box/constbox/prelude.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to internal/box/constbox/wuicode.sxn.
︙ | ︙ | |||
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | ;; wui-enc-matrix returns the HTML table of all encodings and parts. (defun wui-enc-matrix (matrix) `(table ,@(map (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row)))) matrix))) ;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel ;; identifier. It is used in the base template to update the metadata of the ;; HTML page to include some role specific CSS code. ;; Referenced in function "ROLE-DEFAULT-meta". (defvar CSS-ROLE-map '()) ;; ROLE-DEFAULT-meta returns some metadata for the base template. Any role ;; specific code should include the returned list of this function. | > > > > > > > | | | | < < | | | < | | < | | | | | | 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 | ;; wui-enc-matrix returns the HTML table of all encodings and parts. (defun wui-enc-matrix (matrix) `(table ,@(map (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row)))) matrix))) ;; wui-optional-link puts the text into a link, if symbol is defined. Otherwise just return the text (defun wui-optional-link (text url-sym) (let ((url (resolve-symbol url-sym))) (if (defined? url) `(a (@ (href ,(resolve-symbol url-sym))) ,text) text))) ;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel ;; identifier. It is used in the base template to update the metadata of the ;; HTML page to include some role specific CSS code. ;; Referenced in function "ROLE-DEFAULT-meta". (defvar CSS-ROLE-map '()) ;; ROLE-DEFAULT-meta returns some metadata for the base template. Any role ;; specific code should include the returned list of this function. (defun ROLE-DEFAULT-meta (frame) `(,@(let* ((meta-role (resolve-symbol 'meta-role frame)) (entry (assoc CSS-ROLE-map meta-role))) (if (pair? entry) `((link (@ (rel "stylesheet") (href ,(zid-content-path (cdr entry)))))) ) ) ) ) ;; ACTION-SEPARATOR defines a HTML value that separates actions links. (defvar ACTION-SEPARATOR '(@H " · ")) ;; ROLE-DEFAULT-actions returns the default text for actions. (defun ROLE-DEFAULT-actions (frame) `(,@(let ((copy-url (resolve-symbol 'copy-url frame))) (if (defined? copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))) ,@(let ((sequel-url (resolve-symbol 'sequel-url frame))) (if (defined? sequel-url) `((@H " · ") (a (@ (href ,sequel-url)) "Sequel")))) ,@(let ((folge-url (resolve-symbol 'folge-url frame))) (if (defined? folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))) ) ) ;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag". (defun ROLE-tag-actions (frame) `(,@(let ((title (resolve-symbol 'title frame))) (if (and (defined? title) title) `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "tags:" title)))) "Zettel")) ) ) ) ) ;; ROLE-role-actions returns an additional action "Zettel" for zettel with role "role". (defun ROLE-role-actions (frame) `(,@(let ((title (resolve-symbol 'title frame))) (if (and (defined? title) title) `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "role:" title)))) "Zettel")) ) ) ) ) ;; ROLE-DEFAULT-heading returns the default text for headings, below the ;; references of a zettel. In most cases it should be called from an ;; overwriting function. (defun ROLE-DEFAULT-heading (frame) `(,@(let ((meta-url (resolve-symbol 'meta-url frame))) (if (defined? meta-url) `((br) "URL: " ,(url-to-html meta-url)))) ,@(let ((urls (resolve-symbol 'urls frame))) (if (defined? urls) (map (lambda (u) `(@L (br) ,(car u) ": " ,(url-to-html (cdr u)))) urls) ) ) ,@(let ((meta-author (resolve-symbol 'meta-author frame))) (if (and (defined? meta-author) meta-author) `((br) "By " ,meta-author))) ) ) |
Changes to internal/box/constbox/zettel.sxn.
︙ | ︙ | |||
11 12 13 14 15 16 17 | ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 ,heading) (div (@ (class "zs-meta")) | | | | > > | > < | | | > | > > > | | > > | | 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 | ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article (header (h1 ,heading) (div (@ (class "zs-meta")) ,@(if (symbol-bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " · "))) ,zid (@H " · ") (a (@ (href ,info-url)) "Info") (@H " · ") "(" ,@(if (symbol-bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role))) ,@(if (and (symbol-bound? 'folge-role-url) (symbol-bound? 'meta-folge-role)) `((@H " → ") (a (@ (href ,folge-role-url)) ,meta-folge-role))) ")" ,@(if tag-refs `((@H " · ") ,@tag-refs)) (@H " · ") (a (@ (href ,context-url)) "Context") ,@(if (symbol-bound? 'thread-query-url) `((@H " · ") (a (@ (href ,thread-query-url)) "Thread"))) ,@(ROLE-DEFAULT-actions (current-frame)) ,@(let* ((frame (current-frame))(rea (resolve-symbol 'ROLE-EXTRA-actions frame))) (if (defined? rea) (rea frame))) ,@(if superior-refs `((br) "Superior: " ,superior-refs)) ,@(if prequel-refs `((br) ,(wui-optional-link "Prequel" 'sequel-query-url) ": " ,prequel-refs)) ,@(if precursor-refs `((br) ,(wui-optional-link "Precursor" 'folge-query-url) ": " ,precursor-refs)) ,@(ROLE-DEFAULT-heading (current-frame)) ,@(let* ((frame (current-frame))(reh (resolve-symbol 'ROLE-EXTRA-heading frame))) (if (defined? reh) (reh frame))) ) ) ,@content ,endnotes ,@(if (or folge-links sequel-links back-links subordinate-links) `((nav ,@(if folge-links `((details (@ (,folge-open)) (summary ,(wui-optional-link "Folgezettel" 'folge-query-url)) (ul ,@(map wui-item-link folge-links))))) ,@(if sequel-links `((details (@ (,sequel-open)) (summary ,(wui-optional-link "Sequel" 'sequel-query-url)) (ul ,@(map wui-item-link sequel-links))))) ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) )) ) ) |
Changes to internal/box/dirbox/dirbox.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // Package dirbox provides a directory-based zettel box. package dirbox import ( "context" "errors" "net/url" "os" "path/filepath" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/box/notify" "zettelstore.de/z/internal/kernel" | > | | | | | | 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 | // Package dirbox provides a directory-based zettel box. package dirbox import ( "context" "errors" "log/slog" "net/url" "os" "path/filepath" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/box/notify" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { var logger *slog.Logger if krnl := kernel.Main; krnl != nil { logger = krnl.GetLogger(kernel.BoxService).With("box", "dir", "boxnum", cdata.Number) } path := getDirPath(u) if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return nil, err } dp := dirBox{ logger: logger, number: cdata.Number, location: u.String(), readonly: box.GetQueryBool(u, "readonly"), cdata: *cdata, dir: path, notifySpec: getDirSrvInfo(logger, u.Query().Get("type")), fSrvs: makePrime(uint32(box.GetQueryInt(u, "worker", 1, 7, 1499))), } return &dp, nil }) } func makePrime(n uint32) uint32 { |
︙ | ︙ | |||
88 89 90 91 92 93 94 | const ( _ notifyTypeSpec = iota dirNotifyAny dirNotifySimple dirNotifyFS ) | | | | | 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 | const ( _ notifyTypeSpec = iota dirNotifyAny dirNotifySimple dirNotifyFS ) func getDirSrvInfo(logger *slog.Logger, notifyType string) notifyTypeSpec { for range 2 { switch notifyType { case kernel.BoxDirTypeNotify: return dirNotifyFS case kernel.BoxDirTypeSimple: return dirNotifySimple default: notifyType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string) } } logger.Error("Unable to set notify type, using a default", "notifyType", notifyType) return dirNotifySimple } func getDirPath(u *url.URL) string { if u.Opaque != "" { return filepath.Clean(u.Opaque) } return filepath.Clean(u.Path) } // dirBox uses a directory to store zettel as files. type dirBox struct { logger *slog.Logger number int location string readonly bool cdata manager.ConnectData dir string notifySpec notifyTypeSpec dirSrv *notify.DirService |
︙ | ︙ | |||
153 154 155 156 157 158 159 | func (dp *dirBox) Start(context.Context) error { dp.mxCmds.Lock() defer dp.mxCmds.Unlock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) for i := range dp.fSrvs { cc := make(chan fileCmd) | | | | | | | | | 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 | func (dp *dirBox) Start(context.Context) error { dp.mxCmds.Lock() defer dp.mxCmds.Unlock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) for i := range dp.fSrvs { cc := make(chan fileCmd) go fileService(i, dp.logger.With("sub", "file", "fn", i), dp.dir, cc) dp.fCmds = append(dp.fCmds, cc) } var notifier notify.Notifier var err error switch dp.notifySpec { case dirNotifySimple: notifier, err = notify.NewSimpleDirNotifier(dp.logger.With("notify", "simple"), dp.dir) default: notifier, err = notify.NewFSDirNotifier(dp.logger.With("notify", "fs"), dp.dir) } if err != nil { dp.logger.Error("Unable to create directory supervisor", "err", err) dp.stopFileServices() return err } dp.dirSrv = notify.NewDirService( dp, dp.logger.With("sub", "dirsrv"), notifier, dp.cdata.Notify, ) dp.dirSrv.Start() return nil } func (dp *dirBox) Refresh(_ context.Context) { dp.dirSrv.Refresh() logging.LogTrace(dp.logger, "Refresh") } func (dp *dirBox) Stop(_ context.Context) { dirSrv := dp.dirSrv dp.dirSrv = nil if dirSrv != nil { dirSrv.Stop() } dp.stopFileServices() } func (dp *dirBox) stopFileServices() { for _, c := range dp.fCmds { close(c) } } func (dp *dirBox) notifyChanged(zid id.Zid, reason box.UpdateReason) { if notify := dp.cdata.Notify; notify != nil { logging.LogTrace(dp.logger, "notifyChanged", "zid", zid, "reason", reason) notify(dp, zid, reason) } } func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function sum := 2166136261 ^ uint32(zid) |
︙ | ︙ | |||
242 243 244 245 246 247 248 | dp.updateEntryFromMetaContent(&entry, meta, zettel.Content) err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } dp.notifyChanged(meta.Zid, box.OnZettel) | | | | | | | 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 | dp.updateEntryFromMetaContent(&entry, meta, zettel.Content) err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } dp.notifyChanged(meta.Zid, box.OnZettel) logging.LogTrace(dp.logger, "CreateZettel", logging.Err(err), "zid", meta.Zid) return meta.Zid, err } func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } m, c, err := dp.srvGetMetaContent(ctx, entry, zid) if err != nil { return zettel.Zettel{}, err } zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(c)} logging.LogTrace(dp.logger, "GetZettel", "zid", zid) return zettel, nil } func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool { return dp.dirSrv.GetDirEntry(zid).IsValid() } func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) logging.LogTrace(dp.logger, "ApplyZid", "entries", len(entries)) for _, entry := range entries { handle(entry.Zid) } return nil } func (dp *dirBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) logging.LogTrace(dp.logger, "ApplyMeta", "entries", len(entries)) // The following loop could be parallelized if needed for performance. for _, entry := range entries { m, err := dp.srvGetMeta(ctx, entry, entry.Zid) if err != nil { logging.LogTrace(dp.logger, "ApplyMeta/getMeta", "err", err) return err } dp.cdata.Enricher.Enrich(ctx, m, dp.number) handle(m) } return nil } |
︙ | ︙ | |||
310 311 312 313 314 315 316 | } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { // Existing zettel, but new in this box. entry = ¬ify.DirEntry{Zid: zid} } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) | | > > > | | | 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 | } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { // Existing zettel, but new in this box. entry = ¬ify.DirEntry{Zid: zid} } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) err := dp.dirSrv.UpdateDirEntry(entry) if err != nil { return err } err = dp.srvSetZettel(ctx, entry, zettel) if err == nil { dp.notifyChanged(zid, box.OnZettel) } logging.LogTrace(dp.logger, "UpdateZettel", "zid", zid, logging.Err(err)) return err } func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content zettel.Content) { entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax) } |
︙ | ︙ | |||
348 349 350 351 352 353 354 | if err != nil { return nil } err = dp.srvDeleteZettel(ctx, entry, zid) if err == nil { dp.notifyChanged(zid, box.OnDelete) } | | | | 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | if err != nil { return nil } err = dp.srvDeleteZettel(ctx, entry, zid) if err == nil { dp.notifyChanged(zid, box.OnDelete) } logging.LogTrace(dp.logger, "DeleteZettel", "zid", zid, logging.Err(err)) return err } func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = dp.readonly st.Zettel = dp.dirSrv.NumDirEntries() logging.LogTrace(dp.logger, "ReadStats", "zettel", st.Zettel) } |
Changes to internal/box/dirbox/service.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package dirbox import ( "context" "fmt" "io" "os" "path/filepath" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/box/filebox" "zettelstore.de/z/internal/box/notify" "zettelstore.de/z/internal/kernel" | > > < | | | | | 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 | package dirbox import ( "context" "fmt" "io" "log/slog" "os" "path/filepath" "time" "t73f.de/r/zero/oso" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/box/filebox" "zettelstore.de/z/internal/box/notify" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/zettel" ) func fileService(i uint32, logger *slog.Logger, dirPath string, cmds <-chan fileCmd) { // Something may panic. Ensure a running service. defer func() { if ri := recover(); ri != nil { kernel.Main.LogRecover("FileService", ri) go fileService(i, logger, dirPath, cmds) } }() logger.Debug("File service started", "i", i, "dirpath", dirPath) for cmd := range cmds { cmd.run(dirPath) } logger.Debug("File service stopped", "i", i, "dirpath", dirPath) } type fileCmd interface { run(string) } const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing. |
︙ | ︙ | |||
205 206 207 208 209 210 211 | } else { contentPath := filepath.Join(dirPath, contentName) if entry.HasMetaInContent() { err = writeZettelFile(contentPath, m, content) cmd.rc <- err return } | | | > > | > | < | < < < < | | > | < | < < < < | | | < < | < | < < < | < < < | | < | 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 | } else { contentPath := filepath.Join(dirPath, contentName) if entry.HasMetaInContent() { err = writeZettelFile(contentPath, m, content) cmd.rc <- err return } err = writeFileContent(contentPath, makeTempPrefix(zid), content) } cmd.rc <- err return } err = writeMetaFile(filepath.Join(dirPath, metaName), m) if err == nil && contentName != "" { err = writeFileContent(filepath.Join(dirPath, contentName), makeTempPrefix(zid), content) } cmd.rc <- err } func makeTempPrefix(zid id.Zid) string { return "tmp-" + zid.String() } func writeMetaFile(metaPath string, m *meta.Meta) error { metaFile, err := oso.SafeWriteWith(metaPath, makeTempPrefix(m.Zid)) if err != nil { return err } defer metaFile.RollbackIfNeeded() writeFileZid(metaFile, m.Zid) _, _ = m.WriteComputed(metaFile) return metaFile.Close() } func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error { zettelFile, err := oso.SafeWriteWith(contentPath, makeTempPrefix(m.Zid)) if err != nil { return err } defer zettelFile.RollbackIfNeeded() writeMetaHeader(zettelFile, m) _, _ = zettelFile.Write(content) return zettelFile.Close() } var ( newline = []byte{'\n'} yamlSep = []byte{'-', '-', '-', '\n'} ) func writeMetaHeader(w io.Writer, m *meta.Meta) { if m.YamlSep { _, _ = w.Write(yamlSep) } writeFileZid(w, m.Zid) _, _ = m.WriteComputed(w) if m.YamlSep { _, _ = w.Write(yamlSep) } else { _, _ = w.Write(newline) } } // COMMAND: srvDeleteZettel ---------------------------------------- // // Deletes an existing zettel. func (dp *dirBox) srvDeleteZettel(ctx context.Context, entry *notify.DirEntry, zid id.Zid) error { |
︙ | ︙ | |||
360 361 362 363 364 365 366 | entry.Zid, entry.ContentExt, entry.MetaName != "", entry.UselessFiles, ) } | < < < < < < < < < < | | < | < | | | > > > | | | < < < | < < < < | | 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | entry.Zid, entry.ContentExt, entry.MetaName != "", entry.UselessFiles, ) } func writeFileZid(w io.Writer, zid id.Zid) { _, _ = io.WriteString(w, "id: ") _, _ = w.Write(zid.Bytes()) _, _ = io.WriteString(w, "\n") } func writeFileContent(path, prefix string, content []byte) error { f, err := oso.SafeWriteWith(path, prefix) if err != nil { return err } defer f.RollbackIfNeeded() _, _ = f.Write(content) return f.Close() } |
Changes to internal/box/filebox/filebox.go.
︙ | ︙ | |||
32 33 34 35 36 37 38 | manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getFilepathFromURL(u) ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String()) } return &zipBox{ | | < | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getFilepathFromURL(u) ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { return nil, errors.New("unknown extension '" + ext + "' in box URL: " + u.String()) } return &zipBox{ logger: kernel.Main.GetLogger(kernel.BoxService).With("box", "zip", "boxnum", cdata.Number), number: cdata.Number, name: path, enricher: cdata.Enricher, notify: cdata.Notify, }, nil }) } |
︙ | ︙ |
Changes to internal/box/filebox/zipbox.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package filebox import ( "archive/zip" "context" "fmt" "io" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/notify" | > | | | 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 | package filebox import ( "archive/zip" "context" "fmt" "io" "log/slog" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/notify" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) type zipBox struct { logger *slog.Logger number int name string enricher box.Enricher notify box.UpdateNotifier dirSrv *notify.DirService } |
︙ | ︙ | |||
66 67 68 69 70 71 72 | } func (zb *zipBox) Start(context.Context) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } | | > > | | | | | 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 | } func (zb *zipBox) Start(context.Context) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } if err = reader.Close(); err != nil { return err } zipNotifier := notify.NewSimpleZipNotifier(zb.logger, zb.name) zb.dirSrv = notify.NewDirService(zb, zb.logger, zipNotifier, zb.notify) zb.dirSrv.Start() return nil } func (zb *zipBox) Refresh(_ context.Context) { zb.dirSrv.Refresh() logging.LogTrace(zb.logger, "Refresh") } func (zb *zipBox) Stop(context.Context) { zb.dirSrv.Stop() zb.dirSrv = nil } func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } reader, err := zip.OpenReader(zb.name) if err != nil { return zettel.Zettel{}, err } defer func() { _ = reader.Close() }() var m *meta.Meta var src []byte var inMeta bool contentName := entry.ContentName if metaName := entry.MetaName; metaName == "" { |
︙ | ︙ | |||
130 131 132 133 134 135 136 | if err != nil { return zettel.Zettel{}, err } } } CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) | | | < | | | | | 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 | if err != nil { return zettel.Zettel{}, err } } } CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) logging.LogTrace(zb.logger, "GetZettel", "zid", zid) return zettel.Zettel{Meta: m, Content: zettel.NewContent(src)}, nil } func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool { return zb.dirSrv.GetDirEntry(zid).IsValid() } func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := zb.dirSrv.GetDirEntries(constraint) logging.LogTrace(zb.logger, "ApplyZid", "entries", len(entries)) for _, entry := range entries { handle(entry.Zid) } return nil } func (zb *zipBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } entries := zb.dirSrv.GetDirEntries(constraint) logging.LogTrace(zb.logger, "ApplyMeta", "entries", len(entries)) for _, entry := range entries { if !constraint(entry.Zid) { continue } m, err2 := zb.readZipMeta(reader, entry.Zid, entry) if err2 != nil { continue } zb.enricher.Enrich(ctx, m, zb.number) handle(m) } return reader.Close() } func (*zipBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrReadOnly entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { err = box.ErrZettelNotFound{Zid: zid} } logging.LogTrace(zb.logger, "DeleteZettel", "err", err) return err } func (zb *zipBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = zb.dirSrv.NumDirEntries() logging.LogTrace(zb.logger, "ReadStats", "zettel", st.Zettel) } func (zb *zipBox) readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *notify.DirEntry) (m *meta.Meta, err error) { var inMeta bool if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt |
︙ | ︙ | |||
222 223 224 225 226 227 228 | } func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) { f, err := reader.Open(name) if err != nil { return nil, err } | > | > > > | | 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | } func readZipFileContent(reader *zip.ReadCloser, name string) ([]byte, error) { f, err := reader.Open(name) if err != nil { return nil, err } data, err := io.ReadAll(f) err2 := f.Close() if err == nil { err = err2 } return data, err } |
Changes to internal/box/manager/box.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 26 27 28 29 30 31 32 | "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) // Conatains all box.Box related functions // Location returns some information where the box is located. | > | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) // Conatains all box.Box related functions // Location returns some information where the box is located. |
︙ | ︙ | |||
55 56 57 58 59 60 61 | return box.CanCreateZettel(ctx) } return false } // CreateZettel creates a new zettel. func (mgr *Manager) CreateZettel(ctx context.Context, ztl zettel.Zettel) (id.Zid, error) { | | | | 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 | return box.CanCreateZettel(ctx) } return false } // CreateZettel creates a new zettel. func (mgr *Manager) CreateZettel(ctx context.Context, ztl zettel.Zettel) (id.Zid, error) { mgr.mgrLogger.Debug("CreateZettel") if err := mgr.checkContinue(ctx); err != nil { return id.Invalid, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { ztl.Meta = mgr.cleanMetaProperties(ztl.Meta) zid, err := box.CreateZettel(ctx, ztl) if err == nil { mgr.idxUpdateZettel(ctx, ztl) } return zid, err } return id.Invalid, box.ErrReadOnly } // GetZettel retrieves a specific zettel. func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { mgr.mgrLogger.Debug("GetZettel", "zid", zid) if err := mgr.checkContinue(ctx); err != nil { return zettel.Zettel{}, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.getZettel(ctx, zid) } |
︙ | ︙ | |||
97 98 99 100 101 102 103 | } } return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } // GetAllZettel retrieves a specific zettel from all managed boxes. func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { | | | | 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 | } } return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } // GetAllZettel retrieves a specific zettel from all managed boxes. func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { mgr.mgrLogger.Debug("GetAllZettel", "zid", zid) if err := mgr.checkContinue(ctx); err != nil { return nil, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() var result []zettel.Zettel for i, p := range mgr.boxes { if z, err := p.GetZettel(ctx, zid); err == nil { mgr.Enrich(ctx, z.Meta, i+1) result = append(result, z) } } return result, nil } // FetchZids returns the set of all zettel identifer managed by the box. func (mgr *Manager) FetchZids(ctx context.Context) (*idset.Set, error) { mgr.mgrLogger.Debug("FetchZids") if err := mgr.checkContinue(ctx); err != nil { return nil, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.fetchZids(ctx) } |
︙ | ︙ | |||
141 142 143 144 145 146 147 | return nil, err } } return result, nil } func (mgr *Manager) hasZettel(ctx context.Context, zid id.Zid) bool { | | | < | < | | | | | | | | 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 | return nil, err } } return result, nil } func (mgr *Manager) hasZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrLogger.Debug("HasZettel", "zid", zid) if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, bx := range mgr.boxes { if bx.HasZettel(ctx, zid) { return true } } return false } // GetMeta returns just the metadata of the zettel with the given identifier. func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { mgr.mgrLogger.Debug("GetMeta", "zid", zid) if err := mgr.checkContinue(ctx); err != nil { return nil, err } m, err := mgr.idxStore.GetMeta(ctx, zid) if err != nil { // TODO: Call GetZettel and return just metadata, in case the index is not complete. return nil, err } mgr.Enrich(ctx, m, 0) return m, nil } // 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, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { mgr.mgrLogger.Debug("SelectMeta", "query", q) if err := mgr.checkContinue(ctx); err != nil { return nil, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq) if result := compSearch.Result(); result != nil { logging.LogTrace(mgr.mgrLogger, "found without ApplyMeta", "count", len(result)) return result, nil } selected := map[id.Zid]*meta.Meta{} for _, term := range compSearch.Terms { rejected := idset.New() handleMeta := func(m *meta.Meta) { zid := m.Zid if rejected.Contains(zid) { logging.LogTrace(mgr.mgrLogger, "SelectMeta/alreadyRejected", "zid", zid) return } if _, ok := selected[zid]; ok { logging.LogTrace(mgr.mgrLogger, "SelectMeta/alreadySelected", "zid", zid) return } if compSearch.PreMatch(m) && term.Match(m) { selected[zid] = m logging.LogTrace(mgr.mgrLogger, "SelectMeta/match", "zid", zid) } else { rejected.Add(zid) logging.LogTrace(mgr.mgrLogger, "SelectMeta/reject", "zid", zid) } } for _, p := range mgr.boxes { if err2 := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err2 != nil { return nil, err2 } } } result := make([]*meta.Meta, 0, len(selected)) for _, m := range selected { result = append(result, m) } result = compSearch.AfterSearch(result) logging.LogTrace(mgr.mgrLogger, "found with ApplyMeta", "count", len(result)) return result, nil } // CanUpdateZettel returns true, if box could possibly update the given zettel. func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { return box.CanUpdateZettel(ctx, zettel) } return false } // UpdateZettel updates an existing zettel. func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { mgr.mgrLogger.Debug("UpdateZettel", "zid", zettel.Meta.Zid) if err := mgr.checkContinue(ctx); err != nil { return err } return mgr.updateZettel(ctx, zettel) } func (mgr *Manager) updateZettel(ctx context.Context, zettel zettel.Zettel) error { if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { |
︙ | ︙ | |||
275 276 277 278 279 280 281 | } } return false } // DeleteZettel removes the zettel from the box. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { | | | 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | } } return false } // DeleteZettel removes the zettel from the box. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { mgr.mgrLogger.Debug("DeleteZettel", "zid", zid) if err := mgr.checkContinue(ctx); err != nil { return err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) |
︙ | ︙ |
Changes to internal/box/manager/collect.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- package manager import ( "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box/manager/store" | > < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | //----------------------------------------------------------------------------- package manager import ( "strings" zerostrings "t73f.de/r/zero/strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box/manager/store" ) type collectData struct { refs *idset.Set words store.WordSet urls store.WordSet } |
︙ | ︙ | |||
57 58 59 60 61 62 63 | case *ast.LiteralNode: data.addText(string(n.Content)) } return data } func (data *collectData) addText(s string) { | | | 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | case *ast.LiteralNode: data.addText(string(n.Content)) } return data } func (data *collectData) addText(s string) { for _, word := range zerostrings.NormalizeWords(s) { data.words.Add(word) } } func (data *collectData) addRef(ref *ast.Reference) { if ref == nil { return |
︙ | ︙ |
Changes to internal/box/manager/indexer.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "context" | < > | | | | | < < | | < < | | < < | | < < | 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 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "context" "net/url" "time" "t73f.de/r/zero/strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager/store" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/zettel" ) // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchEqual(word string) *idset.Set { found := mgr.idxStore.SearchEqual(word) mgr.idxLogger.Debug("SearchEqual", "word", word, "found", found.Length()) logging.LogTrace(mgr.idxLogger, "IDs", "ids", found) return found } // SearchPrefix returns all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchPrefix(prefix string) *idset.Set { found := mgr.idxStore.SearchPrefix(prefix) mgr.idxLogger.Debug("SearchPrefix", "prefix", prefix, "found", found.Length()) logging.LogTrace(mgr.idxLogger, "IDs", "ids", found) return found } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchSuffix(suffix string) *idset.Set { found := mgr.idxStore.SearchSuffix(suffix) mgr.idxLogger.Debug("SearchSuffix", "suffix", suffix, "found", found.Length()) logging.LogTrace(mgr.idxLogger, "IDs", "ids", found) return found } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchContains(s string) *idset.Set { found := mgr.idxStore.SearchContains(s) mgr.idxLogger.Debug("SearchContains", "s", s, "found", found.Length()) logging.LogTrace(mgr.idxLogger, "IDs", "ids", found) return found } // idxIndexer runs in the background and updates the index data structures. // This is the main service of the idxIndexer. func (mgr *Manager) idxIndexer() { // Something may panic. Ensure a running indexer. |
︙ | ︙ | |||
100 101 102 103 104 105 106 | func (mgr *Manager) idxWorkService(ctx context.Context) { var start time.Time for { switch action, zid, lastReload := mgr.idxAr.Dequeue(); action { case arNothing: return case arReload: | | | | | | 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 | func (mgr *Manager) idxWorkService(ctx context.Context) { var start time.Time for { switch action, zid, lastReload := mgr.idxAr.Dequeue(); action { case arNothing: return case arReload: mgr.idxLogger.Debug("reload") zids, err := mgr.FetchZids(ctx) if err == nil { start = time.Now() mgr.idxAr.Reload(zids) mgr.idxMx.Lock() mgr.idxLastReload = time.Now().Local() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } case arZettel: mgr.idxLogger.Debug("zettel", "zid", zid) zettel, err := mgr.GetZettel(ctx, zid) if err != nil { // Zettel was deleted or is not accessible b/c of other reasons logging.LogTrace(mgr.idxLogger, "delete", "zid", zid) mgr.idxDeleteZettel(ctx, zid) continue } logging.LogTrace(mgr.idxLogger, "update", "zid", zid) mgr.idxUpdateZettel(ctx, zettel) mgr.idxMx.Lock() if lastReload { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() |
︙ | ︙ | |||
201 202 203 204 205 206 207 | idxCollectMetaValue(cData.words, string(val)) } } } } func idxCollectMetaValue(stWords store.WordSet, value string) { | | | 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | idxCollectMetaValue(cData.words, string(val)) } } } } func idxCollectMetaValue(stWords store.WordSet, value string) { if words := strings.NormalizeWords(value); len(words) > 0 { for _, word := range words { stWords.Add(word) } } else { stWords.Add(value) } } |
︙ | ︙ |
Changes to internal/box/manager/manager.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "io" "net/url" "sync" "time" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager/mapstore" "zettelstore.de/z/internal/box/manager/store" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" | > | | 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 | // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "io" "log/slog" "net/url" "sync" "time" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager/mapstore" "zettelstore.de/z/internal/box/manager/store" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logging" ) // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. Config config.Config Enricher box.Enricher |
︙ | ︙ | |||
80 81 82 83 84 85 86 | panic(scheme) } registry[scheme] = create } // Manager is a coordinating box. type Manager struct { | | | | | | | 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 | panic(scheme) } registry[scheme] = create } // Manager is a coordinating box. type Manager struct { mgrLogger *slog.Logger stateMx sync.RWMutex state box.StartState mgrMx sync.RWMutex rtConfig config.Config boxes []box.ManagedBox observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo propertyKeys *set.Set[string] // Set of property key names // Indexer data idxLogger *slog.Logger idxStore store.Store idxAr *anteroomQueue idxReady chan struct{} // Signal a non-empty anteroom to background task // Indexer stats data idxMx sync.RWMutex idxLastReload time.Time idxDurReload time.Duration idxSinceReload uint64 } |
︙ | ︙ | |||
128 129 130 131 132 133 134 | descrs := meta.GetSortedKeyDescriptions() propertyKeys := set.New[string]() for _, kd := range descrs { if kd.IsProperty() { propertyKeys.Add(kd.Name) } } | | | | | | | | 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 | descrs := meta.GetSortedKeyDescriptions() propertyKeys := set.New[string]() for _, kd := range descrs { if kd.IsProperty() { propertyKeys.Add(kd.Name) } } boxLogger := kernel.Main.GetLogger(kernel.BoxService) mgr := &Manager{ mgrLogger: boxLogger.With("box", "manager"), rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, idxLogger: boxLogger.With("box", "index"), idxStore: createIdxStore(rtConfig), idxAr: newAnteroomQueue(1000), idxReady: make(chan struct{}, 1), } cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.notifyChanged} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { p, err := Connect(uri, authManager, &cdata) if err != nil { |
︙ | ︙ | |||
200 201 202 203 204 205 206 | for { select { case ci, ok := <-mgr.infos: if ok { now := time.Now() if len(cache) > 1 && tsLastEvent.Add(10*time.Second).Before(now) { // Cache contains entries and is definitely outdated | | | | | 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | for { select { case ci, ok := <-mgr.infos: if ok { now := time.Now() if len(cache) > 1 && tsLastEvent.Add(10*time.Second).Before(now) { // Cache contains entries and is definitely outdated logging.LogTrace(mgr.mgrLogger, "clean destutter cache") cache = destutterCache{} } tsLastEvent = now reason, zid := ci.Reason, ci.Zid mgr.mgrLogger.Debug("notifier", "reason", reason, "zid", zid) if ignoreUpdate(cache, now, reason, zid) { logging.LogTrace(mgr.mgrLogger, "notifier ignored", "reason", reason, "zid", zid) continue } isStarted := mgr.State() == box.StartStateStarted mgr.idxEnqueue(reason, zid) if ci.Box == nil { ci.Box = mgr |
︙ | ︙ | |||
257 258 259 260 261 262 263 | case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: mgr.idxAr.EnqueueZettel(zid) case box.OnDelete: mgr.idxAr.EnqueueZettel(zid) default: | | | 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: mgr.idxAr.EnqueueZettel(zid) case box.OnDelete: mgr.idxAr.EnqueueZettel(zid) default: mgr.mgrLogger.Error("Unknown notification reason", "reason", reason, "zid", zid) return } select { case mgr.idxReady <- struct{}{}: default: } } |
︙ | ︙ | |||
321 322 323 324 325 326 327 | func (mgr *Manager) waitBoxesAreStarted() { const waitTime = 10 * time.Millisecond const waitLoop = int(1 * time.Second / waitTime) for i := 1; !mgr.allBoxesStarted(); i++ { if i%waitLoop == 0 { if time.Duration(i)*waitTime > time.Minute { | | | | 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | func (mgr *Manager) waitBoxesAreStarted() { const waitTime = 10 * time.Millisecond const waitLoop = int(1 * time.Second / waitTime) for i := 1; !mgr.allBoxesStarted(); i++ { if i%waitLoop == 0 { if time.Duration(i)*waitTime > time.Minute { mgr.mgrLogger.Info("Waiting for more than one minute to start") } else { logging.LogTrace(mgr.mgrLogger, "Wait for boxes to start") } } time.Sleep(waitTime) } } func (mgr *Manager) allBoxesStarted() bool { |
︙ | ︙ | |||
358 359 360 361 362 363 364 | } } mgr.setState(box.StartStateStopped) } // Refresh internal box data. func (mgr *Manager) Refresh(ctx context.Context) error { | | | | | 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 | } } mgr.setState(box.StartStateStopped) } // Refresh internal box data. func (mgr *Manager) Refresh(ctx context.Context) error { mgr.mgrLogger.Debug("Refresh") if err := mgr.checkContinue(ctx); err != nil { return err } mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() for _, bx := range mgr.boxes { if rb, ok := bx.(box.Refresher); ok { rb.Refresh(ctx) } } return nil } // ReIndex data of the given zettel. func (mgr *Manager) ReIndex(ctx context.Context, zid id.Zid) error { mgr.mgrLogger.Debug("ReIndex") if err := mgr.checkContinue(ctx); err != nil { return err } mgr.infos <- box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: zid} return nil } // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { mgr.mgrLogger.Debug("ReadStats") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() subStats := make([]box.ManagedBoxStats, len(mgr.boxes)) for i, p := range mgr.boxes { p.ReadStats(&subStats[i]) } |
︙ | ︙ |
Changes to internal/box/manager/mapstore/mapstore.go.
︙ | ︙ | |||
579 580 581 582 583 584 585 | ms.mxStats.Unlock() } func (ms *mapStore) Dump(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() | | | | | | | | | | | | | | | | | | | 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 | ms.mxStats.Unlock() } func (ms *mapStore) Dump(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() _, _ = io.WriteString(w, "=== Dump\n") ms.dumpIndex(w) ms.dumpDead(w) dumpStringRefs(w, "Words", "", "", ms.words) dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) } func (ms *mapStore) dumpIndex(w io.Writer) { if len(ms.idx) == 0 { return } _, _ = io.WriteString(w, "==== Zettel Index\n") zids := make([]id.Zid, 0, len(ms.idx)) for id := range ms.idx { zids = append(zids, id) } slices.Sort(zids) for _, id := range zids { _, _ = fmt.Fprintln(w, "=====", id) zi := ms.idx[id] if !zi.dead.IsEmpty() { _, _ = fmt.Fprintln(w, "* Dead:", zi.dead) } dumpSet(w, "* Forward:", zi.forward) dumpSet(w, "* Backward:", zi.backward) otherRefs := make([]string, 0, len(zi.otherRefs)) for k := range zi.otherRefs { otherRefs = append(otherRefs, k) } slices.Sort(otherRefs) for _, k := range otherRefs { _, _ = fmt.Fprintln(w, "* Meta", k) dumpSet(w, "** Forward:", zi.otherRefs[k].forward) dumpSet(w, "** Backward:", zi.otherRefs[k].backward) } dumpStrings(w, "* Words", "", "", zi.words) dumpStrings(w, "* URLs", "[[", "]]", zi.urls) } } func (ms *mapStore) dumpDead(w io.Writer) { if len(ms.dead) == 0 { return } _, _ = fmt.Fprintf(w, "==== Dead References\n") zids := make([]id.Zid, 0, len(ms.dead)) for id := range ms.dead { zids = append(zids, id) } slices.Sort(zids) for _, id := range zids { _, _ = fmt.Fprintln(w, ";", id) _, _ = fmt.Fprintln(w, ":", ms.dead[id]) } } func dumpSet(w io.Writer, prefix string, s *idset.Set) { if !s.IsEmpty() { _, _ = io.WriteString(w, prefix) s.ForEach(func(zid id.Zid) { _, _ = io.WriteString(w, " ") _, _ = w.Write(zid.Bytes()) }) _, _ = fmt.Fprintln(w) } } func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { if len(slice) > 0 { sl := make([]string, len(slice)) copy(sl, slice) slices.Sort(sl) _, _ = fmt.Fprintln(w, title) for _, s := range sl { _, _ = fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) } } } func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { if len(srefs) == 0 { return } _, _ = fmt.Fprintln(w, "====", title) for _, s := range slices.Sorted(maps.Keys(srefs)) { _, _ = fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) _, _ = fmt.Fprintln(w, ":", srefs[s]) } } |
Changes to internal/box/membox/membox.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //----------------------------------------------------------------------------- // Package membox stores zettel volatile in main memory. package membox import ( "context" "net/url" "sync" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/kernel" | > | | < | | 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 | //----------------------------------------------------------------------------- // Package membox stores zettel volatile in main memory. package membox import ( "context" "log/slog" "net/url" "sync" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/box/manager" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &memBox{ logger: kernel.Main.GetLogger(kernel.BoxService).With("box", "mem", "boxnum", cdata.Number), u: u, cdata: *cdata, maxZettel: box.GetQueryInt(u, "max-zettel", 0, 127, 65535), maxBytes: box.GetQueryInt(u, "max-bytes", 0, 65535, (1024*1024*1024)-1), }, nil }) } type memBox struct { logger *slog.Logger u *url.URL cdata manager.ConnectData maxZettel int maxBytes int mx sync.RWMutex // Protects the following fields zettel map[id.Zid]zettel.Zettel curBytes int |
︙ | ︙ | |||
75 76 77 78 79 80 81 | } func (mb *memBox) Start(context.Context) error { mb.mx.Lock() mb.zettel = make(map[id.Zid]zettel.Zettel) mb.curBytes = 0 mb.mx.Unlock() | | | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | } func (mb *memBox) Start(context.Context) error { mb.mx.Lock() mb.zettel = make(map[id.Zid]zettel.Zettel) mb.curBytes = 0 mb.mx.Unlock() logging.LogTrace(mb.logger, "Start box", "max-zettel", mb.maxZettel, "max-bytes", mb.maxBytes) return nil } func (mb *memBox) Stop(context.Context) { mb.mx.Lock() mb.zettel = nil mb.mx.Unlock() |
︙ | ︙ | |||
114 115 116 117 118 119 120 | meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() mb.notifyChanged(zid, box.OnZettel) | | | | | | 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 | meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() mb.notifyChanged(zid, box.OnZettel) logging.LogTrace(mb.logger, "CreateZettel", "zid", zid) return zid, nil } func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { mb.mx.RLock() z, ok := mb.zettel[zid] mb.mx.RUnlock() if !ok { return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} } z.Meta = z.Meta.Clone() logging.LogTrace(mb.logger, "GetZettel") return z, nil } func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool { mb.mx.RLock() _, found := mb.zettel[zid] mb.mx.RUnlock() return found } func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() logging.LogTrace(mb.logger, "ApplyZid", "entries", len(mb.zettel)) for zid := range mb.zettel { if constraint(zid) { handle(zid) } } return nil } func (mb *memBox) ApplyMeta(ctx context.Context, handle box.MetaFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() logging.LogTrace(mb.logger, "ApplyMeta", "entries", len(mb.zettel)) for zid, zettel := range mb.zettel { if constraint(zid) { m := zettel.Meta.Clone() mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number) handle(m) } } |
︙ | ︙ | |||
199 200 201 202 203 204 205 | } zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() mb.notifyChanged(m.Zid, box.OnZettel) | | | 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | } zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() mb.notifyChanged(m.Zid, box.OnZettel) logging.LogTrace(mb.logger, "UpdateZettel") return nil } func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { mb.mx.RLock() _, ok := mb.zettel[zid] mb.mx.RUnlock() |
︙ | ︙ | |||
221 222 223 224 225 226 227 | mb.mx.Unlock() return box.ErrZettelNotFound{Zid: zid} } delete(mb.zettel, zid) mb.curBytes -= oldZettel.ByteSize() mb.mx.Unlock() mb.notifyChanged(zid, box.OnDelete) | | | | 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | mb.mx.Unlock() return box.ErrZettelNotFound{Zid: zid} } delete(mb.zettel, zid) mb.curBytes -= oldZettel.ByteSize() mb.mx.Unlock() mb.notifyChanged(zid, box.OnDelete) logging.LogTrace(mb.logger, "DeleteZettel") return nil } func (mb *memBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = false mb.mx.RLock() st.Zettel = len(mb.zettel) mb.mx.RUnlock() logging.LogTrace(mb.logger, "ReadStats", "zettel", st.Zettel) } |
Changes to internal/box/notify/directory.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "errors" | | | | 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 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "errors" "log/slog" "path/filepath" "regexp" "slices" "sync" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/query" ) type entrySet map[id.Zid]*DirEntry // DirServiceState signal the internal state of the service. |
︙ | ︙ | |||
52 53 54 55 56 57 58 | DsMissing // Directory is missing DsStopping // Service is shut down ) // DirService specifies a directory service for file based zettel. type DirService struct { box box.ManagedBox | | | | | 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 | DsMissing // Directory is missing DsStopping // Service is shut down ) // DirService specifies a directory service for file based zettel. type DirService struct { box box.ManagedBox logger *slog.Logger dirPath string notifier Notifier infos box.UpdateNotifier mx sync.RWMutex // protects status, entries state DirServiceState entries entrySet } // ErrNoDirectory signals missing directory data. var ErrNoDirectory = errors.New("unable to retrieve zettel directory information") // NewDirService creates a new directory service. func NewDirService(box box.ManagedBox, logger *slog.Logger, notifier Notifier, notify box.UpdateNotifier) *DirService { return &DirService{ box: box, logger: logger, notifier: notifier, infos: notify, state: DsCreated, } } // State the current service state. |
︙ | ︙ | |||
107 108 109 110 111 112 113 | ds.state = DsStopping ds.mx.Unlock() ds.notifier.Close() } func (ds *DirService) logMissingEntry(action string) error { err := ErrNoDirectory | | | 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | ds.state = DsStopping ds.mx.Unlock() ds.notifier.Close() } func (ds *DirService) logMissingEntry(action string) error { err := ErrNoDirectory ds.logger.Info("Unable to get directory information", "err", err, "action", action) return err } // NumDirEntries returns the number of entries in the directory. func (ds *DirService) NumDirEntries() int { ds.mx.RLock() defer ds.mx.RUnlock() |
︙ | ︙ | |||
217 218 219 220 221 222 223 | } } func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) { ds.mx.RLock() state := ds.state ds.mx.RUnlock() | < | < | | | | | 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 | } } func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) { ds.mx.RLock() state := ds.state ds.mx.RUnlock() logging.LogTrace(ds.logger, "notifyEvent", "state", state, "op", ev.Op, "name", ev.Name) if state == DsStopping { return nil, false } switch ev.Op { case Error: newEntries = nil if state != DsMissing { ds.logger.Error("Notifier confused", "state", state, logging.Err(ev.Err)) } case Make: newEntries = make(entrySet) case List: if ev.Name == "" { zids := getNewZids(newEntries) ds.mx.Lock() fromMissing := ds.state == DsMissing prevEntries := ds.entries ds.entries = newEntries ds.state = DsWorking ds.mx.Unlock() ds.onCreateDirectory(zids, prevEntries) if fromMissing { ds.logger.Info("Zettel directory found", "path", ds.dirPath) } return nil, true } if newEntries != nil { ds.onUpdateFileEvent(newEntries, ev.Name) } case Destroy: ds.onDestroyDirectory() ds.logger.Error("Zettel directory missing", "path", ds.dirPath) return nil, true case Update: ds.mx.Lock() zid := ds.onUpdateFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { ds.notifyChange(zid, box.OnZettel) } case Delete: ds.mx.Lock() zid := ds.onDeleteFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { ds.notifyChange(zid, box.OnDelete) } default: ds.logger.Error("Unknown zettel notification", "event", ev) } return newEntries, true } func getNewZids(entries entrySet) []id.Zid { zids := make([]id.Zid, 0, len(entries)) for zid := range entries { |
︙ | ︙ | |||
344 345 346 347 348 349 350 | zid := seekZid(name) if zid == id.Invalid { return id.Invalid } entry := fetchdirEntry(entries, zid) dupName1, dupName2 := ds.updateEntry(entry, name) if dupName1 != "" { | | | | 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | zid := seekZid(name) if zid == id.Invalid { return id.Invalid } entry := fetchdirEntry(entries, zid) dupName1, dupName2 := ds.updateEntry(entry, name) if dupName1 != "" { ds.logger.Info("Duplicate content (is ignored)", "name", dupName1) if dupName2 != "" { ds.logger.Info("Duplicate content (is ignored)", "name", dupName2) } return id.Invalid } return zid } func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid { |
︙ | ︙ | |||
412 413 414 415 416 417 418 | loop: for _, prevName := range uselessFiles { for _, newName := range entry.UselessFiles { if prevName == newName { continue loop } } | | | 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 | loop: for _, prevName := range uselessFiles { for _, newName := range entry.UselessFiles { if prevName == newName { continue loop } } ds.logger.Info("Previous duplicate file becomes useful", "name", prevName) } } func (ds *DirService) updateEntry(entry *DirEntry, name string) (string, string) { ext := onlyExt(name) if !extIsMetaAndContent(entry.ContentExt) { if ext == "" { |
︙ | ︙ | |||
573 574 575 576 577 578 579 | return newLen < oldLen } return newExt < oldExt } func (ds *DirService) notifyChange(zid id.Zid, reason box.UpdateReason) { if notify := ds.infos; notify != nil { | | | 571 572 573 574 575 576 577 578 579 580 581 | return newLen < oldLen } return newExt < oldExt } func (ds *DirService) notifyChange(zid id.Zid, reason box.UpdateReason) { if notify := ds.infos; notify != nil { logging.LogTrace(ds.logger, "notifychange", "zid", zid, "reason", reason) notify(ds.box, zid, reason) } } |
Changes to internal/box/notify/fsdir.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "os" "path/filepath" "strings" "github.com/fsnotify/fsnotify" | > > | | | | | < < < | > | < | | | > | | | 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 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "log/slog" "os" "path/filepath" "strings" "github.com/fsnotify/fsnotify" "zettelstore.de/z/internal/logging" ) type fsdirNotifier struct { logger *slog.Logger events chan Event done chan struct{} refresh chan struct{} base *fsnotify.Watcher path string fetcher EntryFetcher parent string } // NewFSDirNotifier creates a directory based notifier that receives notifications // from the file system. func NewFSDirNotifier(logger *slog.Logger, path string) (Notifier, error) { absPath, err := filepath.Abs(path) if err != nil { logger.Debug("Unable to create absolute path", "err", err, "path", path) return nil, err } watcher, err := fsnotify.NewWatcher() if err != nil { logger.Debug("Unable to create watcher", "err", err, "absPath", absPath) return nil, err } absParentDir := filepath.Dir(absPath) errParent := watcher.Add(absParentDir) err = watcher.Add(absPath) if errParent != nil { if err != nil { logger.Error("Unable to access Zettel directory and its parent directory", "parentDir", absParentDir, "errParent", errParent, "path", absPath, "err", err) _ = watcher.Close() return nil, err } logger.Info("Parent of Zettel directory cannot be supervised", "parentDir", absParentDir, "err", errParent) logger.Info("Zettelstore might not detect a deletion or movement of the Zettel directory", "path", absPath) } else if err != nil { // Not a problem, if container is not available. It might become available later. logger.Info("Zettel directory currently not available", "err", err, "path", absPath) } fsdn := &fsdirNotifier{ logger: logger, events: make(chan Event), refresh: make(chan struct{}), done: make(chan struct{}), base: watcher, path: absPath, fetcher: newDirPathFetcher(absPath), parent: absParentDir, |
︙ | ︙ | |||
86 87 88 89 90 91 92 | } func (fsdn *fsdirNotifier) Refresh() { fsdn.refresh <- struct{}{} } func (fsdn *fsdirNotifier) eventLoop() { | | | | | | | | | | | | | | | | | | > | | | | | | | | 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 | } func (fsdn *fsdirNotifier) Refresh() { fsdn.refresh <- struct{}{} } func (fsdn *fsdirNotifier) eventLoop() { defer func() { _ = fsdn.base.Close() }() defer close(fsdn.events) defer close(fsdn.refresh) if !listDirElements(fsdn.logger, fsdn.fetcher, fsdn.events, fsdn.done) { return } for fsdn.readAndProcessEvent() { } } func (fsdn *fsdirNotifier) readAndProcessEvent() bool { select { case <-fsdn.done: fsdn.traceDone(1) return false default: } select { case <-fsdn.done: fsdn.traceDone(2) return false case <-fsdn.refresh: logging.LogTrace(fsdn.logger, "refresh") listDirElements(fsdn.logger, fsdn.fetcher, fsdn.events, fsdn.done) case err, ok := <-fsdn.base.Errors: logging.LogTrace(fsdn.logger, "got errors", "err", err, "ok", ok) if !ok { return false } select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: fsdn.traceDone(3) return false } case ev, ok := <-fsdn.base.Events: logging.LogTrace(fsdn.logger, "file event", "name", ev.Name, "op", ev.Op, "ok", ok) if !ok { return false } if !fsdn.processEvent(&ev) { return false } } return true } func (fsdn *fsdirNotifier) traceDone(pos int64) { logging.LogTrace(fsdn.logger, "done with read and process events", "i", pos) } func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool { if strings.HasPrefix(ev.Name, fsdn.path) { if len(ev.Name) == len(fsdn.path) { return fsdn.processDirEvent(ev) } return fsdn.processFileEvent(ev) } logging.LogTrace(fsdn.logger, "event does not match", "path", fsdn.path, "name", ev.Name, "op", ev.Op) return true } func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool { if ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename) { fsdn.logger.Debug("Directory removed", "name", fsdn.path) _ = fsdn.base.Remove(fsdn.path) select { case fsdn.events <- Event{Op: Destroy}: case <-fsdn.done: logging.LogTrace(fsdn.logger, "done dir event processing", "i", 1) return false } return true } if ev.Has(fsnotify.Create) { err := fsdn.base.Add(fsdn.path) if err != nil { fsdn.logger.Error("Unable to add directory", "err", err, "name", fsdn.path) select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: logging.LogTrace(fsdn.logger, "done dir event processing", "i", 2) return false } } fsdn.logger.Debug("Directory added", "name", fsdn.path) return listDirElements(fsdn.logger, fsdn.fetcher, fsdn.events, fsdn.done) } logging.LogTrace(fsdn.logger, "Directory processed", "name", ev.Name, "op", ev.Op) return true } func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool { if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) { if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() { regular := err == nil && fi.Mode().IsRegular() logging.LogTrace(fsdn.logger, "error with file", "name", ev.Name, "op", ev.Op, logging.Err(err), "regular", regular) return true } logging.LogTrace(fsdn.logger, "File updated", "name", ev.Name, "op", ev.Op) return fsdn.sendEvent(Update, filepath.Base(ev.Name)) } if ev.Has(fsnotify.Rename) { fi, err := os.Lstat(ev.Name) if err != nil { logging.LogTrace(fsdn.logger, "File deleted", "name", ev.Name, "op", ev.Op) return fsdn.sendEvent(Delete, filepath.Base(ev.Name)) } if fi.Mode().IsRegular() { logging.LogTrace(fsdn.logger, "File updated", "name", ev.Name, "op", ev.Op) return fsdn.sendEvent(Update, filepath.Base(ev.Name)) } logging.LogTrace(fsdn.logger, "File not regular", "name", ev.Name) return true } if ev.Has(fsnotify.Remove) { logging.LogTrace(fsdn.logger, "File deleted", "name", ev.Name, "op", ev.Op) return fsdn.sendEvent(Delete, filepath.Base(ev.Name)) } logging.LogTrace(fsdn.logger, "File processed", "name", ev.Name, "op", ev.Op) return true } func (fsdn *fsdirNotifier) sendEvent(op EventOp, filename string) bool { select { case fsdn.events <- Event{Op: op, Name: filename}: case <-fsdn.done: logging.LogTrace(fsdn.logger, "done file event processing") return false } return true } func (fsdn *fsdirNotifier) Close() { close(fsdn.done) } |
Changes to internal/box/notify/helper.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "archive/zip" "os" | > | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "archive/zip" "log/slog" "os" "zettelstore.de/z/internal/logging" ) // EntryFetcher return a list of (file) names of an directory. type EntryFetcher interface { Fetch() ([]string, error) } |
︙ | ︙ | |||
53 54 55 56 57 58 59 | func newZipPathFetcher(zipPath string) EntryFetcher { return &zipPathFetcher{zipPath} } func (zpf *zipPathFetcher) Fetch() ([]string, error) { reader, err := zip.OpenReader(zpf.zipPath) if err != nil { return nil, err } | < > | | | | 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 | func newZipPathFetcher(zipPath string) EntryFetcher { return &zipPathFetcher{zipPath} } func (zpf *zipPathFetcher) Fetch() ([]string, error) { reader, err := zip.OpenReader(zpf.zipPath) if err != nil { return nil, err } result := make([]string, 0, len(reader.File)) for _, f := range reader.File { result = append(result, f.Name) } err = reader.Close() return result, err } // listDirElements write all files within the directory path as events. func listDirElements(logger *slog.Logger, fetcher EntryFetcher, events chan<- Event, done <-chan struct{}) bool { select { case events <- Event{Op: Make}: case <-done: return false } entries, err := fetcher.Fetch() if err != nil { select { case events <- Event{Op: Error, Err: err}: case <-done: return false } } for _, name := range entries { logging.LogTrace(logger, "File listed", "name", name) select { case events <- Event{Op: List, Name: name}: case <-done: return false } } |
︙ | ︙ |
Changes to internal/box/notify/simpledir.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "path/filepath" | > < < | | | | | | | | 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 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "log/slog" "path/filepath" ) type simpleDirNotifier struct { logger *slog.Logger events chan Event done chan struct{} refresh chan struct{} fetcher EntryFetcher } // NewSimpleDirNotifier creates a directory based notifier that will not receive // any notifications from the operating system. func NewSimpleDirNotifier(logger *slog.Logger, path string) (Notifier, error) { absPath, err := filepath.Abs(path) if err != nil { return nil, err } sdn := &simpleDirNotifier{ logger: logger, events: make(chan Event), done: make(chan struct{}), refresh: make(chan struct{}), fetcher: newDirPathFetcher(absPath), } go sdn.eventLoop() return sdn, nil } // NewSimpleZipNotifier creates a zip-file based notifier that will not receive // any notifications from the operating system. func NewSimpleZipNotifier(logger *slog.Logger, zipPath string) Notifier { sdn := &simpleDirNotifier{ logger: logger, events: make(chan Event), done: make(chan struct{}), refresh: make(chan struct{}), fetcher: newZipPathFetcher(zipPath), } go sdn.eventLoop() return sdn } func (sdn *simpleDirNotifier) Events() <-chan Event { return sdn.events } func (sdn *simpleDirNotifier) Refresh() { sdn.refresh <- struct{}{} } func (sdn *simpleDirNotifier) eventLoop() { defer close(sdn.events) defer close(sdn.refresh) if !listDirElements(sdn.logger, sdn.fetcher, sdn.events, sdn.done) { return } for { select { case <-sdn.done: return case <-sdn.refresh: listDirElements(sdn.logger, sdn.fetcher, sdn.events, sdn.done) } } } func (sdn *simpleDirNotifier) Close() { close(sdn.done) } |
Changes to internal/config/config.go.
︙ | ︙ | |||
25 26 27 28 29 30 31 | KeyFooterZettel = "footer-zettel" KeyHomeZettel = "home-zettel" KeyListsMenuZettel = "lists-menu-zettel" KeyShowBackLinks = "show-back-links" KeyShowFolgeLinks = "show-folge-links" KeyShowSequelLinks = "show-sequel-links" KeyShowSubordinateLinks = "show-subordinate-links" | < | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | KeyFooterZettel = "footer-zettel" KeyHomeZettel = "home-zettel" KeyListsMenuZettel = "lists-menu-zettel" KeyShowBackLinks = "show-back-links" KeyShowFolgeLinks = "show-folge-links" KeyShowSequelLinks = "show-sequel-links" KeyShowSubordinateLinks = "show-subordinate-links" // api.KeyLang ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig |
︙ | ︙ |
Changes to internal/encoder/htmlenc.go.
︙ | ︙ | |||
66 67 68 69 70 71 72 | head.AddN( shtml.SymHead, sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta), ) head.ExtendBang(hm) var sb strings.Builder if hasTitle { | | | 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | head.AddN( shtml.SymHead, sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta), ) head.ExtendBang(hm) var sb strings.Builder if hasTitle { _, _ = he.textEnc.WriteInlines(&sb, &isTitle) } else { sb.Write(zn.Meta.Zid.Bytes()) } head.Add(sx.MakeList(shtml.SymAttrTitle, sx.MakeString(sb.String()))) var body sx.ListBuilder body.Add(shtml.SymBody) |
︙ | ︙ |
Changes to internal/encoder/mdenc.go.
︙ | ︙ | |||
33 34 35 36 37 38 39 | // WriteZettel writes the encoded zettel to the writer. func (me *mdEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newMDVisitor(w, me.lang) v.acceptMeta(zn.InhMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { | | | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // WriteZettel writes the encoded zettel to the writer. func (me *mdEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newMDVisitor(w, me.lang) v.acceptMeta(zn.InhMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteLn() } ast.Walk(&v, &zn.BlocksAST) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as markdown. |
︙ | ︙ | |||
153 154 155 156 157 158 159 | return } v.writeSpaces(4) lcm1 := lc - 1 for i := 0; i < lc; i++ { b := vn.Content[i] if b != '\n' && b != '\r' { | | | | 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 | return } v.writeSpaces(4) lcm1 := lc - 1 for i := 0; i < lc; i++ { b := vn.Content[i] if b != '\n' && b != '\r' { _ = v.b.WriteByte(b) continue } j := i + 1 for ; j < lc; j++ { c := vn.Content[j] if c != '\n' && c != '\r' { break } } if j >= lcm1 { break } v.b.WriteLn() v.writeSpaces(4) i = j - 1 } } func (v *mdVisitor) visitRegion(rn *ast.RegionNode) { if rn.Kind != ast.RegionQuote { |
︙ | ︙ | |||
221 222 223 224 225 226 227 | func (v *mdVisitor) writeNestedList(ln *ast.NestedListNode, enum string) { v.listInfo = append(v.listInfo, len(enum)) regIndent := 4*len(v.listInfo) - 4 paraIndent := regIndent + len(enum) for i, item := range ln.Items { if i > 0 { | | | | | | | 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 | func (v *mdVisitor) writeNestedList(ln *ast.NestedListNode, enum string) { v.listInfo = append(v.listInfo, len(enum)) regIndent := 4*len(v.listInfo) - 4 paraIndent := regIndent + len(enum) for i, item := range ln.Items { if i > 0 { v.b.WriteLn() } v.writeSpaces(regIndent) v.b.WriteString(enum) for j, in := range item { if j > 0 { v.b.WriteLn() if _, ok := in.(*ast.ParaNode); ok { v.writeSpaces(paraIndent) } } ast.Walk(v, in) } } } func (v *mdVisitor) writeListQuote(ln *ast.NestedListNode) { v.listInfo = append(v.listInfo, 0) if len(v.listInfo) > 1 { return } prefix := v.listPrefix v.listPrefix = "> " for i, item := range ln.Items { if i > 0 { v.b.WriteLn() } v.b.WriteString(v.listPrefix) for j, in := range item { if j > 0 { v.b.WriteLn() if _, ok := in.(*ast.ParaNode); ok { v.b.WriteString(v.listPrefix) } } ast.Walk(v, in) } } v.listPrefix = prefix } func (v *mdVisitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteLn() } if l := len(v.listInfo); l > 0 { if v.listPrefix == "" { v.writeSpaces(4*l - 4 + v.listInfo[l-1]) } else { v.writeSpaces(4*l - 4) v.b.WriteString(v.listPrefix) |
︙ | ︙ | |||
292 293 294 295 296 297 298 | v.writeReference(ln.Ref, ln.Inlines) } func (v *mdVisitor) visitEmbedRef(en *ast.EmbedRefNode) { v.pushAttributes(en.Attrs) defer v.popAttributes() | | | | | | | | | 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 | v.writeReference(ln.Ref, ln.Inlines) } func (v *mdVisitor) visitEmbedRef(en *ast.EmbedRefNode) { v.pushAttributes(en.Attrs) defer v.popAttributes() _ = v.b.WriteByte('!') v.writeReference(en.Ref, en.Inlines) } func (v *mdVisitor) writeReference(ref *ast.Reference, is ast.InlineSlice) { if ref.State == ast.RefStateQuery { ast.Walk(v, &is) } else if len(is) > 0 { _ = v.b.WriteByte('[') ast.Walk(v, &is) v.b.WriteStrings("](", ref.String()) _ = v.b.WriteByte(')') } else if isAutoLinkable(ref) { _ = v.b.WriteByte('<') v.b.WriteString(ref.String()) _ = v.b.WriteByte('>') } else { s := ref.String() v.b.WriteStrings("[", s, "](", s, ")") } } func isAutoLinkable(ref *ast.Reference) bool { if ref.State != ast.RefStateExternal || ref.URL == nil { return false } return ref.URL.Scheme != "" } func (v *mdVisitor) visitFormat(fn *ast.FormatNode) { v.pushAttributes(fn.Attrs) defer v.popAttributes() switch fn.Kind { case ast.FormatEmph: _ = v.b.WriteByte('*') ast.Walk(v, &fn.Inlines) _ = v.b.WriteByte('*') case ast.FormatStrong: v.b.WriteString("__") ast.Walk(v, &fn.Inlines) v.b.WriteString("__") case ast.FormatQuote: v.writeQuote(fn) case ast.FormatMark: |
︙ | ︙ | |||
363 364 365 366 367 368 369 | } v.b.WriteString(rightQ) } func (v *mdVisitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralCode, ast.LiteralInput, ast.LiteralOutput: | | | | | | | 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | } v.b.WriteString(rightQ) } func (v *mdVisitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralCode, ast.LiteralInput, ast.LiteralOutput: _ = v.b.WriteByte('`') _, _ = v.b.Write(ln.Content) _ = v.b.WriteByte('`') case ast.LiteralComment: // ignore everything default: _, _ = v.b.Write(ln.Content) } } func (v *mdVisitor) writeSpaces(n int) { for range n { v.b.WriteSpace() } } |
Changes to internal/encoder/textenc.go.
︙ | ︙ | |||
27 28 29 30 31 32 33 | // TextEncoder encodes just the text and ignores any formatting. type TextEncoder struct{} // WriteZettel writes metadata and content. func (te *TextEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newTextVisitor(w) | | | | | 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 | // TextEncoder encodes just the text and ignores any formatting. type TextEncoder struct{} // WriteZettel writes metadata and content. func (te *TextEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newTextVisitor(w) _, _ = te.WriteMeta(&v.b, zn.InhMeta) v.visitBlockSlice(&zn.BlocksAST) length, err := v.b.Flush() return length, err } // WriteMeta encodes metadata as text. func (te *TextEncoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { buf := newEncWriter(w) for key, val := range m.Computed() { if meta.Type(key) == meta.TypeTagSet { writeTagSet(&buf, val.Elems()) } else { buf.WriteString(string(val)) } buf.WriteLn() } length, err := buf.Flush() return length, err } func writeTagSet(buf *encWriter, tags iter.Seq[meta.Value]) { first := true for tag := range tags { if !first { buf.WriteSpace() } first = false buf.WriteString(string(tag.CleanTag())) } } |
︙ | ︙ | |||
100 101 102 103 104 105 106 | return nil case *ast.VerbatimNode: v.visitVerbatim(n) return nil case *ast.RegionNode: v.visitBlockSlice(&n.Blocks) if len(n.Inlines) > 0 { | | | 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | return nil case *ast.VerbatimNode: v.visitVerbatim(n) return nil case *ast.RegionNode: v.visitBlockSlice(&n.Blocks) if len(n.Inlines) > 0 { v.b.WriteLn() ast.Walk(v, &n.Inlines) } return nil case *ast.NestedListNode: v.visitNestedList(n) return nil case *ast.DescriptionListNode: |
︙ | ︙ | |||
122 123 124 125 126 127 128 | case *ast.BLOBNode: return nil case *ast.TextNode: v.visitText(n.Text) return nil case *ast.BreakNode: if n.Hard { | | | | | | | < | | | 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 | case *ast.BLOBNode: return nil case *ast.TextNode: v.visitText(n.Text) return nil case *ast.BreakNode: if n.Hard { v.b.WriteLn() } else { v.b.WriteSpace() } return nil case *ast.LinkNode: if len(n.Inlines) > 0 { ast.Walk(v, &n.Inlines) } return nil case *ast.MarkNode: if len(n.Inlines) > 0 { ast.Walk(v, &n.Inlines) } return nil case *ast.FootnoteNode: if v.inlinePos > 0 { v.b.WriteSpace() } // No 'return nil' to write text case *ast.LiteralNode: if n.Kind != ast.LiteralComment { _, _ = v.b.Write(n.Content) } } return v } func (v *textVisitor) visitVerbatim(vn *ast.VerbatimNode) { if vn.Kind != ast.VerbatimComment { _, _ = v.b.Write(vn.Content) } } func (v *textVisitor) visitNestedList(ln *ast.NestedListNode) { for i, item := range ln.Items { v.writePosChar(i, '\n') for j, it := range item { v.writePosChar(j, '\n') ast.Walk(v, it) } } } func (v *textVisitor) visitDescriptionList(dl *ast.DescriptionListNode) { for i, descr := range dl.Descriptions { v.writePosChar(i, '\n') ast.Walk(v, &descr.Term) for _, b := range descr.Descriptions { v.b.WriteLn() for k, d := range b { v.writePosChar(k, '\n') ast.Walk(v, d) } } } } func (v *textVisitor) visitTable(tn *ast.TableNode) { if len(tn.Header) > 0 { v.writeRow(tn.Header) v.b.WriteLn() } for i, row := range tn.Rows { v.writePosChar(i, '\n') v.writeRow(row) } } |
︙ | ︙ | |||
219 220 221 222 223 224 225 | } func (v *textVisitor) visitText(s string) { spaceFound := false for _, ch := range s { if input.IsSpace(ch) { if !spaceFound { | | | | 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | } func (v *textVisitor) visitText(s string) { spaceFound := false for _, ch := range s { if input.IsSpace(ch) { if !spaceFound { v.b.WriteSpace() spaceFound = true } continue } spaceFound = false v.b.WriteString(string(ch)) } } func (v *textVisitor) writePosChar(pos int, ch byte) { if pos > 0 { _ = v.b.WriteByte(ch) } } |
Changes to internal/encoder/write.go.
︙ | ︙ | |||
22 23 24 25 26 27 28 | type encWriter struct { w io.Writer // The io.Writer to write to err error // Collect error length int // Collected length } // newEncWriter creates a new encWriter | | < < | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | type encWriter struct { w io.Writer // The io.Writer to write to err error // Collect error length int // Collected length } // newEncWriter creates a new encWriter func newEncWriter(w io.Writer) encWriter { return encWriter{w: w} } // Write writes the content of p. func (w *encWriter) Write(p []byte) (l int, err error) { if w.err != nil { return 0, w.err } l, w.err = w.w.Write(p) |
︙ | ︙ | |||
62 63 64 65 66 67 68 | var l int l, w.err = w.Write([]byte{b}) w.length += l return w.err } // WriteBytes writes the content of bs. | | < < > > > > > > > > > > > > > > | 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 | var l int l, w.err = w.Write([]byte{b}) w.length += l return w.err } // WriteBytes writes the content of bs. func (w *encWriter) WriteBytes(bs ...byte) { _, _ = w.Write(bs) } // WriteBase64 writes the content of p, encoded with base64. func (w *encWriter) WriteBase64(p []byte) { if w.err == nil { encoder := base64.NewEncoder(base64.StdEncoding, w.w) var l int l, w.err = encoder.Write(p) w.length += l err1 := encoder.Close() if w.err == nil { w.err = err1 } } } // WriteLn writes a new line character. func (w *encWriter) WriteLn() { if w.err == nil { w.err = w.WriteByte('\n') } } // WriteLn writes a space character. func (w *encWriter) WriteSpace() { if w.err == nil { w.err = w.WriteByte(' ') } } // Flush returns the collected length and error. func (w *encWriter) Flush() (int, error) { return w.length, w.err } |
Changes to internal/encoder/zmkenc.go.
︙ | ︙ | |||
33 34 35 36 37 38 39 | // WriteZettel writes the encoded zettel to the writer. func (*zmkEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newZmkVisitor(w) v.acceptMeta(zn.InhMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { | | | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | // WriteZettel writes the encoded zettel to the writer. func (*zmkEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newZmkVisitor(w) v.acceptMeta(zn.InhMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteLn() } ast.Walk(&v, &zn.BlocksAST) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as zmk. |
︙ | ︙ | |||
118 119 120 121 122 123 124 | case *ast.EmbedBLOBNode: v.visitEmbedBLOB(n) case *ast.CiteNode: v.visitCite(n) case *ast.FootnoteNode: v.b.WriteString("[^") ast.Walk(v, &n.Inlines) | | | | | 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 | case *ast.EmbedBLOBNode: v.visitEmbedBLOB(n) case *ast.CiteNode: v.visitCite(n) case *ast.FootnoteNode: v.b.WriteString("[^") ast.Walk(v, &n.Inlines) _ = v.b.WriteByte(']') v.visitAttributes(n.Attrs) case *ast.MarkNode: v.visitMark(n) case *ast.FormatNode: v.visitFormat(n) case *ast.LiteralNode: v.visitLiteral(n) default: return v } return nil } func (v *zmkVisitor) visitBlockSlice(bs *ast.BlockSlice) { var lastWasParagraph bool for i, bn := range *bs { if i > 0 { v.b.WriteLn() if lastWasParagraph && !v.inVerse { if _, ok := bn.(*ast.ParaNode); ok { v.b.WriteLn() } } } ast.Walk(v, bn) _, lastWasParagraph = bn.(*ast.ParaNode) } } |
︙ | ︙ | |||
170 171 172 173 174 175 176 | if vn.Kind == ast.VerbatimHTML { attrs = syntaxToHTML(attrs) } // TODO: scan cn.Lines to find embedded kind[0]s at beginning v.b.WriteString(kind) v.visitAttributes(attrs) | | | | | | | | 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 | if vn.Kind == ast.VerbatimHTML { attrs = syntaxToHTML(attrs) } // TODO: scan cn.Lines to find embedded kind[0]s at beginning v.b.WriteString(kind) v.visitAttributes(attrs) v.b.WriteLn() _, _ = v.b.Write(vn.Content) v.b.WriteLn() v.b.WriteString(kind) } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: ":::", ast.RegionQuote: "<<<", ast.RegionVerse: "\"\"\"", } func (v *zmkVisitor) visitRegion(rn *ast.RegionNode) { // Scan rn.Blocks for embedded regions to adjust length of regionCode kind, ok := mapRegionKind[rn.Kind] if !ok { panic(fmt.Sprintf("Unknown region kind %d", rn.Kind)) } v.b.WriteString(kind) v.visitAttributes(rn.Attrs) v.b.WriteLn() saveInVerse := v.inVerse v.inVerse = rn.Kind == ast.RegionVerse ast.Walk(v, &rn.Blocks) v.inVerse = saveInVerse v.b.WriteLn() v.b.WriteString(kind) if len(rn.Inlines) > 0 { v.b.WriteSpace() ast.Walk(v, &rn.Inlines) } } func (v *zmkVisitor) visitHeading(hn *ast.HeadingNode) { const headingSigns = "========= " v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-3:]) |
︙ | ︙ | |||
220 221 222 223 224 225 226 | ast.NestedListQuote: '>', } func (v *zmkVisitor) visitNestedList(ln *ast.NestedListNode) { v.prefix = append(v.prefix, mapNestedListKind[ln.Kind]) for i, item := range ln.Items { if i > 0 { | | | | | | | | 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 | ast.NestedListQuote: '>', } func (v *zmkVisitor) visitNestedList(ln *ast.NestedListNode) { v.prefix = append(v.prefix, mapNestedListKind[ln.Kind]) for i, item := range ln.Items { if i > 0 { v.b.WriteLn() } _, _ = v.b.Write(v.prefix) v.b.WriteSpace() for j, in := range item { if j > 0 { v.b.WriteLn() if _, ok := in.(*ast.ParaNode); ok { v.writePrefixSpaces() } } ast.Walk(v, in) } } v.prefix = v.prefix[:len(v.prefix)-1] } func (v *zmkVisitor) writePrefixSpaces() { if prefixLen := len(v.prefix); prefixLen > 0 { for i := 0; i <= prefixLen; i++ { v.b.WriteSpace() } } } func (v *zmkVisitor) visitDescriptionList(dn *ast.DescriptionListNode) { for i, descr := range dn.Descriptions { if i > 0 { v.b.WriteLn() } v.b.WriteString("; ") ast.Walk(v, &descr.Term) for _, b := range descr.Descriptions { v.b.WriteString("\n: ") for jj, dn := range b { |
︙ | ︙ | |||
275 276 277 278 279 280 281 | ast.AlignCenter: ":", ast.AlignRight: ">", } func (v *zmkVisitor) visitTable(tn *ast.TableNode) { if header := tn.Header; len(header) > 0 { v.writeTableHeader(header, tn.Align) | | | | 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | ast.AlignCenter: ":", ast.AlignRight: ">", } func (v *zmkVisitor) visitTable(tn *ast.TableNode) { if header := tn.Header; len(header) > 0 { v.writeTableHeader(header, tn.Align) v.b.WriteLn() } for i, row := range tn.Rows { if i > 0 { v.b.WriteLn() } v.writeTableRow(row, tn.Align) } } func (v *zmkVisitor) writeTableHeader(header ast.TableRow, align []ast.Alignment) { for pos, cell := range header { |
︙ | ︙ | |||
301 302 303 304 305 306 307 | v.b.WriteString(alignCode[colAlign]) } } } func (v *zmkVisitor) writeTableRow(row ast.TableRow, align []ast.Alignment) { for pos, cell := range row { | | | | | 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 | v.b.WriteString(alignCode[colAlign]) } } } func (v *zmkVisitor) writeTableRow(row ast.TableRow, align []ast.Alignment) { for pos, cell := range row { _ = v.b.WriteByte('|') if cell.Align != align[pos] { v.b.WriteString(alignCode[cell.Align]) } ast.Walk(v, &cell.Inlines) } } func (v *zmkVisitor) visitBLOB(bn *ast.BLOBNode) { if bn.Syntax == meta.ValueSyntaxSVG { v.b.WriteStrings("@@@", bn.Syntax, "\n") _, _ = v.b.Write(bn.Blob) v.b.WriteString("\n@@@\n") return } var sb strings.Builder _, _ = v.textEnc.WriteInlines(&sb, &bn.Description) v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.") } var escapeSeqs = set.New( "\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##", ) |
︙ | ︙ | |||
354 355 356 357 358 359 360 | v.b.WriteString(tn.Text[last:]) } func (v *zmkVisitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { | | | | | | | | | | | | | | | | | | | | 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 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 | v.b.WriteString(tn.Text[last:]) } func (v *zmkVisitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteLn() } v.writePrefixSpaces() } func (v *zmkVisitor) visitLink(ln *ast.LinkNode) { v.b.WriteString("[[") if len(ln.Inlines) > 0 { ast.Walk(v, &ln.Inlines) _ = v.b.WriteByte('|') } if ln.Ref.State == ast.RefStateBased { _ = v.b.WriteByte('/') } v.b.WriteStrings(ln.Ref.String(), "]]") } func (v *zmkVisitor) visitEmbedRef(en *ast.EmbedRefNode) { v.b.WriteString("{{") if len(en.Inlines) > 0 { ast.Walk(v, &en.Inlines) _ = v.b.WriteByte('|') } v.b.WriteStrings(en.Ref.String(), "}}") } func (v *zmkVisitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) { if en.Syntax == meta.ValueSyntaxSVG { v.b.WriteString("@@") _, _ = v.b.Write(en.Blob) v.b.WriteStrings("@@{=", en.Syntax, "}") return } v.b.WriteString("{{TODO: display inline BLOB}}") } func (v *zmkVisitor) visitCite(cn *ast.CiteNode) { v.b.WriteStrings("[@", cn.Key) if len(cn.Inlines) > 0 { v.b.WriteSpace() ast.Walk(v, &cn.Inlines) } _ = v.b.WriteByte(']') v.visitAttributes(cn.Attrs) } func (v *zmkVisitor) visitMark(mn *ast.MarkNode) { v.b.WriteStrings("[!", mn.Mark) if len(mn.Inlines) > 0 { _ = v.b.WriteByte('|') ast.Walk(v, &mn.Inlines) } _ = v.b.WriteByte(']') } var mapFormatKind = map[ast.FormatKind][]byte{ ast.FormatEmph: []byte("__"), ast.FormatStrong: []byte("**"), ast.FormatInsert: []byte(">>"), ast.FormatDelete: []byte("~~"), ast.FormatSuper: []byte("^^"), ast.FormatSub: []byte(",,"), ast.FormatQuote: []byte(`""`), ast.FormatMark: []byte("##"), ast.FormatSpan: []byte("::"), } func (v *zmkVisitor) visitFormat(fn *ast.FormatNode) { kind, ok := mapFormatKind[fn.Kind] if !ok { panic(fmt.Sprintf("Unknown format kind %d", fn.Kind)) } _, _ = v.b.Write(kind) ast.Walk(v, &fn.Inlines) _, _ = v.b.Write(kind) v.visitAttributes(fn.Attrs) } func (v *zmkVisitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralCode: v.writeLiteral('`', ln.Attrs, ln.Content) case ast.LiteralMath: v.b.WriteStrings("$$", string(ln.Content), "$$") v.visitAttributes(ln.Attrs) case ast.LiteralInput: v.writeLiteral('\'', ln.Attrs, ln.Content) case ast.LiteralOutput: v.writeLiteral('=', ln.Attrs, ln.Content) case ast.LiteralComment: v.b.WriteString("%%") v.visitAttributes(ln.Attrs) v.b.WriteSpace() _, _ = v.b.Write(ln.Content) default: panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind)) } } func (v *zmkVisitor) writeLiteral(code byte, a zsx.Attributes, content []byte) { v.b.WriteBytes(code, code) v.writeEscaped(string(content), code) v.b.WriteBytes(code, code) v.visitAttributes(a) } // visitAttributes write HTML attributes func (v *zmkVisitor) visitAttributes(a zsx.Attributes) { if a.IsEmpty() { return } _ = v.b.WriteByte('{') for i, k := range a.Keys() { if i > 0 { v.b.WriteSpace() } if k == "-" { _ = v.b.WriteByte('-') continue } v.b.WriteString(k) if vl := a[k]; len(vl) > 0 { v.b.WriteStrings("=\"", vl) _ = v.b.WriteByte('"') } } _ = v.b.WriteByte('}') } func (v *zmkVisitor) writeEscaped(s string, toEscape byte) { last := 0 for i := range len(s) { if b := s[i]; b == toEscape || b == '\\' { v.b.WriteString(s[last:i]) |
︙ | ︙ |
Changes to internal/evaluator/evaluator.go.
︙ | ︙ | |||
65 66 67 68 69 70 71 | if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimCode { if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.ValueSyntaxSxn { rd := sxreader.MakeReader(bytes.NewReader(vn.Content)) if objs, err := rd.ReadAll(); err == nil { result := make(ast.BlockSlice, len(objs)) for i, obj := range objs { var buf bytes.Buffer | | | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimCode { if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.ValueSyntaxSxn { rd := sxreader.MakeReader(bytes.NewReader(vn.Content)) if objs, err := rd.ReadAll(); err == nil { result := make(ast.BlockSlice, len(objs)) for i, obj := range objs { var buf bytes.Buffer _, _ = sxbuiltins.Print(&buf, obj) result[i] = &ast.VerbatimNode{ Kind: ast.VerbatimCode, Attrs: zsx.Attributes{"": classAttr}, Content: buf.Bytes(), } } return result |
︙ | ︙ |
Changes to internal/kernel/auth.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "errors" "sync" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth" | > < | > | 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 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "errors" "log/slog" "sync" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth" ) type authService struct { srvConfig mxService sync.RWMutex manager auth.Manager createManager CreateAuthManagerFunc } var errAlreadySetOwner = errors.New("changing an existing owner not allowed") var errAlreadyROMode = errors.New("system in readonly mode cannot change this mode") func (as *authService) Initialize(levelVar *slog.LevelVar, logger *slog.Logger) { as.logLevelVar = levelVar as.logger = logger as.descr = descriptionMap{ AuthOwner: { "Owner's zettel id", func(val string) (any, error) { if owner := as.cur[AuthOwner]; owner != nil && owner != id.Invalid { return nil, errAlreadySetOwner |
︙ | ︙ | |||
62 63 64 65 66 67 68 | } as.next = interfaceMap{ AuthOwner: id.Invalid, AuthReadonly: false, } } | | > > | | | | 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 | } as.next = interfaceMap{ AuthOwner: id.Invalid, AuthReadonly: false, } } func (as *authService) GetLogger() *slog.Logger { return as.logger } func (as *authService) GetLevel() slog.Level { return as.logLevelVar.Level() } func (as *authService) SetLevel(l slog.Level) { as.logLevelVar.Set(l) } func (as *authService) Start(*Kernel) error { as.mxService.Lock() defer as.mxService.Unlock() readonlyMode := as.GetNextConfig(AuthReadonly).(bool) owner := as.GetNextConfig(AuthOwner).(id.Zid) authMgr, err := as.createManager(readonlyMode, owner) if err != nil { as.logger.Error("Unable to create manager", "err", err) return err } as.logger.Info("Start Manager") as.manager = authMgr return nil } func (as *authService) IsStarted() bool { as.mxService.RLock() defer as.mxService.RUnlock() return as.manager != nil } func (as *authService) Stop(*Kernel) { as.logger.Info("Stop Manager") as.mxService.Lock() as.manager = nil as.mxService.Unlock() } func (*authService) GetStatistics() []KeyValue { return nil } |
Changes to internal/kernel/box.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 | package kernel import ( "context" "errors" "fmt" "io" "net/url" "strconv" "sync" "zettelstore.de/z/internal/box" | > < | > | 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 kernel import ( "context" "errors" "fmt" "io" "log/slog" "net/url" "strconv" "sync" "zettelstore.de/z/internal/box" ) type boxService struct { srvConfig mxService sync.RWMutex manager box.Manager createManager CreateBoxManagerFunc } var errInvalidDirType = errors.New("invalid directory type") func (ps *boxService) Initialize(levelVar *slog.LevelVar, logger *slog.Logger) { ps.logLevelVar = levelVar ps.logger = logger ps.descr = descriptionMap{ BoxDefaultDirType: { "Default directory box type", ps.noFrozen(func(val string) (any, error) { switch val { case BoxDirTypeNotify, BoxDirTypeSimple: |
︙ | ︙ | |||
65 66 67 68 69 70 71 | }, } ps.next = interfaceMap{ BoxDefaultDirType: BoxDirTypeNotify, } } | | > > | | | | | 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 | }, } ps.next = interfaceMap{ BoxDefaultDirType: BoxDirTypeNotify, } } func (ps *boxService) GetLogger() *slog.Logger { return ps.logger } func (ps *boxService) GetLevel() slog.Level { return ps.logLevelVar.Level() } func (ps *boxService) SetLevel(l slog.Level) { ps.logLevelVar.Set(l) } func (ps *boxService) Start(kern *Kernel) error { boxURIs := make([]*url.URL, 0, 4) for i := 1; ; i++ { u := ps.GetNextConfig(BoxURIs + strconv.Itoa(i)) if u == nil { break } boxURIs = append(boxURIs, u.(*url.URL)) } ps.mxService.Lock() defer ps.mxService.Unlock() mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg) if err != nil { ps.logger.Error("Unable to create manager", "err", err) return err } ps.logger.Info("Start Manager", "location", mgr.Location()) if err = mgr.Start(context.Background()); err != nil { ps.logger.Error("Unable to start manager", "err", err) return err } kern.cfg.setBox(mgr) ps.manager = mgr return nil } func (ps *boxService) IsStarted() bool { ps.mxService.RLock() defer ps.mxService.RUnlock() return ps.manager != nil } func (ps *boxService) Stop(*Kernel) { ps.logger.Info("Stop Manager") ps.mxService.RLock() mgr := ps.manager ps.mxService.RUnlock() mgr.Stop(context.Background()) ps.mxService.Lock() ps.manager = nil ps.mxService.Unlock() |
︙ | ︙ |
Changes to internal/kernel/cfg.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package kernel import ( "context" "errors" "fmt" "strconv" "strings" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" | > | | | | | 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 | package kernel import ( "context" "errors" "fmt" "log/slog" "strconv" "strings" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/logging" ) type configService struct { srvConfig mxService sync.RWMutex orig *meta.Meta manager box.Manager |
︙ | ︙ | |||
48 49 50 51 52 53 54 | keySiteName = "site-name" keyYAMLHeader = "yaml-header" keyZettelFileSyntax = "zettel-file-syntax" ) var errUnknownVisibility = errors.New("unknown visibility") | | > | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | keySiteName = "site-name" keyYAMLHeader = "yaml-header" keyZettelFileSyntax = "zettel-file-syntax" ) var errUnknownVisibility = errors.New("unknown visibility") func (cs *configService) Initialize(levelVar *slog.LevelVar, logger *slog.Logger) { cs.logLevelVar = levelVar cs.logger = logger cs.descr = descriptionMap{ keyDefaultCopyright: {"Default copyright", parseString, true}, keyDefaultLicense: {"Default license", parseString, true}, keyDefaultVisibility: { "Default zettel visibility", func(val string) (any, error) { |
︙ | ︙ | |||
83 84 85 86 87 88 89 | return config.ZettelmarkupHTML, nil } return config.NoHTML, nil }), true, }, meta.KeyLang: {"Language", parseString, true}, | | < | < > | > > | | | > | | > > > | | > | | > > > | | 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 | return config.ZettelmarkupHTML, nil } return config.NoHTML, nil }), true, }, meta.KeyLang: {"Language", parseString, true}, keyMaxTransclusions: {"Maximum transclusions", parseInt, true}, keySiteName: {"Site name", parseString, true}, keyYAMLHeader: {"YAML header", parseBool, true}, keyZettelFileSyntax: { "Zettel file syntax", func(val string) (any, error) { return strings.Fields(val), nil }, true, }, ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true}, config.KeyListsMenuZettel: {"Lists menu", parseZid, true}, config.KeyShowBackLinks: {"Show back links", parseString, true}, config.KeyShowFolgeLinks: {"Show folge links", parseString, true}, config.KeyShowSequelLinks: {"Show sequel links", parseString, true}, config.KeyShowSubordinateLinks: {"Show subordinate links", parseString, true}, } cs.next = interfaceMap{ keyDefaultCopyright: "", keyDefaultLicense: "", keyDefaultVisibility: meta.VisibilityLogin, keyExpertMode: false, config.KeyFooterZettel: id.Invalid, config.KeyHomeZettel: id.ZidDefaultHome, ConfigInsecureHTML: config.NoHTML, meta.KeyLang: meta.ValueLangEN, keyMaxTransclusions: 1024, keySiteName: "Zettelstore", keyYAMLHeader: false, keyZettelFileSyntax: nil, ConfigSimpleMode: false, config.KeyListsMenuZettel: id.ZidTOCListsMenu, config.KeyShowBackLinks: "", config.KeyShowFolgeLinks: "", config.KeyShowSequelLinks: "", config.KeyShowSubordinateLinks: "", } } func (cs *configService) GetLogger() *slog.Logger { return cs.logger } func (cs *configService) GetLevel() slog.Level { return cs.logLevelVar.Level() } func (cs *configService) SetLevel(l slog.Level) { cs.logLevelVar.Set(l) } func (cs *configService) Start(*Kernel) error { cs.logger.Info("Start Service") data := meta.New(id.ZidConfiguration) for _, kv := range cs.GetNextConfigList() { data.Set(kv.Key, meta.Value(kv.Value)) } cs.mxService.Lock() cs.orig = data cs.mxService.Unlock() return nil } func (cs *configService) IsStarted() bool { cs.mxService.RLock() defer cs.mxService.RUnlock() return cs.orig != nil } func (cs *configService) Stop(*Kernel) { cs.logger.Info("Stop Service") cs.mxService.Lock() cs.orig = nil cs.manager = nil cs.mxService.Unlock() } func (*configService) GetStatistics() []KeyValue { return nil } func (cs *configService) setBox(mgr box.Manager) { cs.mxService.Lock() cs.manager = mgr cs.mxService.Unlock() mgr.RegisterObserver(cs.observe) cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ZidConfiguration}) } func (cs *configService) doUpdate(p box.BaseBox) error { z, err := p.GetZettel(context.Background(), id.ZidConfiguration) logging.LogTrace(cs.logger, "got config meta", logging.Err(err)) if err != nil { return err } m := z.Meta cs.mxService.Lock() for key := range cs.orig.All() { var setErr error if val, ok := m.Get(key); ok { setErr = cs.SetConfig(key, string(val)) } else if defVal, defFound := cs.orig.Get(key); defFound { setErr = cs.SetConfig(key, string(defVal)) } if err == nil && setErr != nil && setErr != errAlreadyFrozen { err = fmt.Errorf("%w (key=%q)", setErr, key) } } cs.mxService.Unlock() cs.SwitchNextToCur() // Poor man's restart return err } func (cs *configService) observe(ci box.UpdateInfo) { if (ci.Reason != box.OnZettel && ci.Reason != box.OnDelete) || ci.Zid == id.ZidConfiguration { cs.logger.Debug("observe", "reason", ci.Reason, "zid", ci.Zid) go func() { cs.mxService.RLock() mgr := cs.manager cs.mxService.RUnlock() var err error if mgr != nil { err = cs.doUpdate(mgr) } else { err = cs.doUpdate(ci.Box) } if err != nil { cs.logger.Error("update config", "err", err) } }() } } // --- config.Config func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string { if m != nil { if val, found := m.Get(key); found { return string(val) } } if user := user.GetCurrentUser(ctx); user != nil { if val, found := user.Get(key); found { return string(val) } } result := cs.GetCurConfig(key) if result == nil { return "" |
︙ | ︙ | |||
277 278 279 280 281 282 283 | } // GetSiteName returns the current value of the "site-name" key. func (cs *configService) GetSiteName() string { return cs.GetCurConfig(keySiteName).(string) } // GetMaxTransclusions return the maximum number of indirect transclusions. func (cs *configService) GetMaxTransclusions() int { | | | 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 | } // GetSiteName returns the current value of the "site-name" key. func (cs *configService) GetSiteName() string { return cs.GetCurConfig(keySiteName).(string) } // GetMaxTransclusions return the maximum number of indirect transclusions. func (cs *configService) GetMaxTransclusions() int { return int(cs.GetCurConfig(keyMaxTransclusions).(int)) } // GetYAMLHeader returns the current value of the "yaml-header" key. func (cs *configService) GetYAMLHeader() bool { return cs.GetCurConfig(keyYAMLHeader).(bool) } // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. func (cs *configService) GetZettelFileSyntax() []meta.Value { |
︙ | ︙ |
Changes to internal/kernel/cmd.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 | //----------------------------------------------------------------------------- package kernel import ( "fmt" "io" "maps" "os" "runtime/metrics" "slices" "strconv" "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 | //----------------------------------------------------------------------------- package kernel import ( "fmt" "io" "log/slog" "maps" "os" "runtime/metrics" "slices" "strconv" "strings" zerostrings "t73f.de/r/zero/strings" "zettelstore.de/z/internal/logging" ) type cmdSession struct { w io.Writer kern *Kernel echo bool header bool |
︙ | ︙ | |||
58 59 60 61 62 63 64 | sess.println("Unknown command:", cmd, strings.Join(args, " ")) sess.println("-- Enter 'help' go get a list of valid commands.") return true } func (sess *cmdSession) println(args ...string) { if len(args) > 0 { | | | | | | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | sess.println("Unknown command:", cmd, strings.Join(args, " ")) sess.println("-- Enter 'help' go get a list of valid commands.") return true } func (sess *cmdSession) println(args ...string) { if len(args) > 0 { _, _ = io.WriteString(sess.w, args[0]) for _, arg := range args[1:] { _, _ = io.WriteString(sess.w, " ") _, _ = io.WriteString(sess.w, arg) } } _, _ = sess.w.Write(sess.eol) } func (sess *cmdSession) usage(cmd, val string) { sess.println("Usage:", cmd, val) } func (sess *cmdSession) printTable(table [][]string) { |
︙ | ︙ | |||
94 95 96 97 98 99 100 | func (sess *cmdSession) calcMaxLen(table [][]string) []int { maxLen := make([]int, 0) for _, row := range table { for colno, column := range row { if colno >= len(maxLen) { maxLen = append(maxLen, 0) } | | | | | | 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 | func (sess *cmdSession) calcMaxLen(table [][]string) []int { maxLen := make([]int, 0) for _, row := range table { for colno, column := range row { if colno >= len(maxLen) { maxLen = append(maxLen, 0) } colLen := zerostrings.Length(column) if colLen <= maxLen[colno] { continue } maxLen[colno] = min(colLen, sess.colwidth) } } return maxLen } func (sess *cmdSession) printRow(row []string, maxLen []int, prefix, delim string, pad rune) { for colno, column := range row { _, _ = io.WriteString(sess.w, prefix) prefix = delim _, _ = io.WriteString(sess.w, zerostrings.JustifyLeft(column, maxLen[colno], pad)) } _, _ = sess.w.Write(sess.eol) } func splitLine(line string) (string, []string) { s := strings.Fields(line) if len(s) == 0 { return "", nil } |
︙ | ︙ | |||
286 287 288 289 290 291 292 | srvD, found := getService(sess, args[0]) if !found { return true } key := args[1] newValue := strings.Join(args[2:], " ") if err := srvD.srv.SetConfig(key, newValue); err == nil { | | | 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 | srvD, found := getService(sess, args[0]) if !found { return true } key := args[1] newValue := strings.Join(args[2:], " ") if err := srvD.srv.SetConfig(key, newValue); err == nil { logging.LogMandatory(sess.kern.logger, "Update system configuration", "key", key, "value", newValue) } else { sess.println("Unable to set key", args[1], "to value", newValue, "because:", err.Error()) } return true } func cmdServices(sess *cmdSession, _ string, _ []string) bool { |
︙ | ︙ | |||
362 363 364 365 366 367 368 | return true } func cmdLogLevel(sess *cmdSession, _ string, args []string) bool { kern := sess.kern if len(args) == 0 { // Write log levels | | | | | | < < < < | | | | | | < | | | > | | | | < > | > | | | 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 | return true } func cmdLogLevel(sess *cmdSession, _ string, args []string) bool { kern := sess.kern if len(args) == 0 { // Write log levels level := kern.GetKernelLogLevel() table := [][]string{ {"Service", "Level", "Name"}, {"(kernel)", strconv.Itoa(int(level)), logging.LevelString(level)}, } for _, name := range sortedServiceNames(sess) { level = kern.srvNames[name].srv.GetLevel() table = append(table, []string{name, strconv.Itoa(int(level.Level())), logging.LevelString(level)}) } sess.printTable(table) return true } srvD, ok := getService(sess, args[0]) if !ok { return true } srv := srvD.srv if len(args) == 1 { lvl := srv.GetLevel() sess.println(strconv.Itoa(int(lvl)), lvl.String()) return true } levelString := args[1] var lvl slog.Level if val, err := strconv.ParseInt(levelString, 10, 8); err == nil { lvl = slog.Level(val) } else { lvl = logging.ParseLevel(levelString) } if lvl <= logging.LevelMissing || logging.LevelMandatory < lvl { sess.println("Invalid level: ", levelString) return true } logging.LogMandatory(kern.logger, "Update log level", "name", args[0], "level", logging.LevelString(lvl)) srv.SetLevel(lvl) return true } func lookupService(sess *cmdSession, cmd string, args []string) (Service, bool) { if len(args) == 0 { sess.usage(cmd, "SERVICE") return 0, false |
︙ | ︙ | |||
436 437 438 439 440 441 442 | } else { fileName = args[1] } kern := sess.kern if err := kern.doStartProfiling(profileName, fileName); err != nil { sess.println("Error:", err.Error()) } else { | | | | 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 | } else { fileName = args[1] } kern := sess.kern if err := kern.doStartProfiling(profileName, fileName); err != nil { sess.println("Error:", err.Error()) } else { logging.LogMandatory(sess.kern.logger, "Start profiling", "profile", profileName, "file", fileName) } return true } func cmdEndProfile(sess *cmdSession, _ string, _ []string) bool { kern := sess.kern err := kern.doStopProfiling() if err != nil { sess.println("Error:", err.Error()) } logging.LogMandatory(sess.kern.logger, "Stop profiling", logging.Err(err)) return true } func cmdMetrics(sess *cmdSession, _ string, _ []string) bool { var samples []metrics.Sample all := metrics.All() for _, d := range all { |
︙ | ︙ | |||
499 500 501 502 503 504 505 | func cmdDumpIndex(sess *cmdSession, _ string, _ []string) bool { sess.kern.dumpIndex(sess.w) return true } func cmdRefresh(sess *cmdSession, _ string, _ []string) bool { kern := sess.kern | | | > > | 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 | func cmdDumpIndex(sess *cmdSession, _ string, _ []string) bool { sess.kern.dumpIndex(sess.w) return true } func cmdRefresh(sess *cmdSession, _ string, _ []string) bool { kern := sess.kern logging.LogMandatory(sess.kern.logger, "Refresh") if err := kern.box.Refresh(); err != nil { logging.LogMandatory(sess.kern.logger, "refresh", "err", err) } return true } func cmdDumpRecover(sess *cmdSession, cmd string, args []string) bool { if len(args) == 0 { sess.usage(cmd, "RECOVER") sess.println("-- A valid value for RECOVER can be obtained via 'stat core'.") |
︙ | ︙ |
Changes to internal/kernel/config.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 | //----------------------------------------------------------------------------- package kernel import ( "errors" "fmt" "maps" "slices" "strconv" "strings" "sync" "t73f.de/r/zsc/domain/id" | > < > | | | | | | | | 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 | //----------------------------------------------------------------------------- package kernel import ( "errors" "fmt" "log/slog" "maps" "slices" "strconv" "strings" "sync" "t73f.de/r/zsc/domain/id" ) type parseFunc func(string) (any, error) type configDescription struct { text string parse parseFunc canList bool } type descriptionMap map[string]configDescription type interfaceMap map[string]any func (m interfaceMap) Clone() interfaceMap { return maps.Clone(m) } type srvConfig struct { logLevelVar *slog.LevelVar logger *slog.Logger mxConfig sync.RWMutex frozen bool descr descriptionMap cur interfaceMap next interfaceMap } func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() keys := slices.Sorted(maps.Keys(cfg.descr)) result := make([]serviceConfigDescription, 0, len(keys)) for _, k := range keys { text := cfg.descr[k].text if strings.HasSuffix(k, "-") { text = text + " (list)" } result = append(result, serviceConfigDescription{Key: k, Descr: text}) } return result } var errAlreadyFrozen = errors.New("value frozen") func (cfg *srvConfig) noFrozen(parse parseFunc) parseFunc { return func(val string) (any, error) { if cfg.frozen { return nil, errAlreadyFrozen } return parse(val) |
︙ | ︙ | |||
203 204 205 206 207 208 209 | func (cfg *srvConfig) SwitchNextToCur() { cfg.mxConfig.Lock() defer cfg.mxConfig.Unlock() cfg.cur = cfg.next.Clone() } | < < | | < < < < < | | < < < < < < < | 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 | func (cfg *srvConfig) SwitchNextToCur() { cfg.mxConfig.Lock() defer cfg.mxConfig.Unlock() cfg.cur = cfg.next.Clone() } var errNoBoolean = errors.New("no boolean value") func parseBool(val string) (any, error) { if val == "" { return false, errNoBoolean } switch val[0] { case '0', 'f', 'F', 'n', 'N': return false, nil } return true, nil } func parseString(val string) (any, error) { return val, nil } func parseInt(val string) (any, error) { return strconv.Atoi(val) } func parseInt64(val string) (any, error) { return strconv.ParseInt(val, 10, 64) } func parseZid(val string) (any, error) { return id.Parse(val) } func parseInvalidZid(val string) (any, error) { zid, _ := id.Parse(val) return zid, nil } |
Changes to internal/kernel/core.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "fmt" "maps" "net" "os" "runtime" "slices" "sync" "time" "t73f.de/r/zsc/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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "fmt" "log/slog" "maps" "net" "os" "runtime" "slices" "sync" "time" "t73f.de/r/zero/strings" "t73f.de/r/zsc/domain/id" ) type coreService struct { srvConfig started bool mxRecover sync.RWMutex mapRecover map[string]recoverInfo } type recoverInfo struct { count uint64 ts time.Time info any stack []byte } func (cs *coreService) Initialize(levelVar *slog.LevelVar, logger *slog.Logger) { cs.logLevelVar = levelVar cs.logger = logger cs.mapRecover = make(map[string]recoverInfo) cs.descr = descriptionMap{ CoreDebug: {"Debug mode", parseBool, false}, CoreGoArch: {"Go processor architecture", nil, false}, CoreGoOS: {"Go Operating System", nil, false}, CoreGoVersion: {"Go Version", nil, false}, |
︙ | ︙ | |||
89 90 91 92 93 94 95 | CoreVerbose: false, } if hn, err := os.Hostname(); err == nil { cs.next[CoreHostname] = hn } } | | > > | 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | CoreVerbose: false, } if hn, err := os.Hostname(); err == nil { cs.next[CoreHostname] = hn } } func (cs *coreService) GetLogger() *slog.Logger { return cs.logger } func (cs *coreService) GetLevel() slog.Level { return cs.logLevelVar.Level() } func (cs *coreService) SetLevel(l slog.Level) { cs.logLevelVar.Set(l) } func (cs *coreService) Start(*Kernel) error { cs.started = true return nil } func (cs *coreService) IsStarted() bool { return cs.started } func (cs *coreService) Stop(*Kernel) { |
︙ | ︙ | |||
139 140 141 142 143 144 145 | } return append( []string{ fmt.Sprintf("Count: %d", ri.count), fmt.Sprintf("Time: %v", ri.ts), fmt.Sprintf("Reason: %v", ri.info), }, | | | 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | } return append( []string{ fmt.Sprintf("Count: %d", ri.count), fmt.Sprintf("Time: %v", ri.ts), fmt.Sprintf("Reason: %v", ri.info), }, strings.SplitLines(string(ri.stack))..., ) } func (cs *coreService) updateRecoverInfo(name string, recoverInfo any, stack []byte) { cs.mxRecover.Lock() ri := cs.mapRecover[name] ri.count++ ri.ts = time.Now().Local() ri.info = recoverInfo ri.stack = stack cs.mapRecover[name] = ri cs.mxRecover.Unlock() } |
Changes to internal/kernel/kernel.go.
︙ | ︙ | |||
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 | // Package kernel provides the main kernel service. package kernel import ( "errors" "fmt" "io" "net" "net/url" "os" "os/signal" "runtime" "runtime/debug" "runtime/pprof" "strconv" "strings" "sync" "syscall" "time" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" | > | > > | < | | | | | | | > | | < | > > | | | | | | | | > > > | | | | 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 | // Package kernel provides the main kernel service. package kernel import ( "errors" "fmt" "io" "log/slog" "net" "net/url" "os" "os/signal" "runtime" "runtime/debug" "runtime/pprof" "strconv" "strings" "sync" "syscall" "time" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/web/server" ) // Main references the main kernel. var Main *Kernel // Kernel is the main internal kernel. type Kernel struct { logLevelVar slog.LevelVar logger *slog.Logger dlogWriter *kernelLogWriter wg sync.WaitGroup mx sync.RWMutex interrupt chan os.Signal profileName string fileName string profileFile *os.File profile *pprof.Profile self kernelService core coreService cfg configService auth authService box boxService web webService srvs map[Service]*serviceDescr srvNames map[string]serviceData depStart serviceDependency depStop serviceDependency // reverse of depStart } type serviceDescr struct { srv service name string logLevel slog.Level logLevelVar slog.LevelVar } type serviceData struct { srv service srvnum Service } type serviceDependency map[Service][]Service // create a new kernel. func init() { Main = createKernel() } // create a new func createKernel() *Kernel { lw := newKernelLogWriter(os.Stdout, 8192) kern := &Kernel{ dlogWriter: lw, interrupt: make(chan os.Signal, 5), } kern.logLevelVar.Set(defaultNormalLogLevel) kern.logger = slog.New(newKernelLogHandler(lw, &kern.logLevelVar)) kern.self.kernel = kern kern.srvs = map[Service]*serviceDescr{ KernelService: {srv: &kern.self, name: "kernel", logLevel: defaultNormalLogLevel}, CoreService: {srv: &kern.core, name: "core", logLevel: defaultNormalLogLevel}, ConfigService: {srv: &kern.cfg, name: "config", logLevel: defaultNormalLogLevel}, AuthService: {srv: &kern.auth, name: "auth", logLevel: defaultNormalLogLevel}, BoxService: {srv: &kern.box, name: "box", logLevel: defaultNormalLogLevel}, WebService: {srv: &kern.web, name: "web", logLevel: defaultNormalLogLevel}, } kern.srvNames = make(map[string]serviceData, len(kern.srvs)) for key, srvD := range kern.srvs { if _, ok := kern.srvNames[srvD.name]; ok { kern.logger.Error("Service data already set, ignored", "service", srvD.name) } kern.srvNames[srvD.name] = serviceData{srvD.srv, key} srvD.logLevelVar.Set(srvD.logLevel) srvLogger := slog.New(newKernelLogHandler(lw, &srvD.logLevelVar)).With( "system", strings.ToUpper(srvD.name)) kern.logger.Debug("Initialize", "service", srvD.name) srvD.srv.Initialize(&srvD.logLevelVar, srvLogger) } kern.depStart = serviceDependency{ KernelService: nil, CoreService: {KernelService}, ConfigService: {CoreService}, AuthService: {CoreService}, BoxService: {CoreService, ConfigService, AuthService}, |
︙ | ︙ | |||
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | ) // Constants for web service keys. const ( WebAssetDir = "asset-dir" WebBaseURL = "base-url" WebListenAddress = "listen" WebPersistentCookie = "persistent" WebProfiling = "profiling" WebMaxRequestSize = "max-request-size" WebSecureCookie = "secure" WebTokenLifetimeAPI = "api-lifetime" WebTokenLifetimeHTML = "html-lifetime" WebURLPrefix = "prefix" ) // KeyDescrValue is a triple of config data. type KeyDescrValue struct{ Key, Descr, Value string } // KeyValue is a pair of key and value. type KeyValue struct{ Key, Value string } | > > > | | > | 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 | ) // Constants for web service keys. const ( WebAssetDir = "asset-dir" WebBaseURL = "base-url" WebListenAddress = "listen" WebLoopbackIdent = "loopback-ident" WebLoopbackZid = "loopback-zid" WebPersistentCookie = "persistent" WebProfiling = "profiling" WebMaxRequestSize = "max-request-size" WebSecureCookie = "secure" WebSxMaxNesting = "sx-max-nesting" WebTokenLifetimeAPI = "api-lifetime" WebTokenLifetimeHTML = "html-lifetime" WebURLPrefix = "prefix" ) // KeyDescrValue is a triple of config data. type KeyDescrValue struct{ Key, Descr, Value string } // KeyValue is a pair of key and value. type KeyValue struct{ Key, Value string } // LogEntry stores values of one log line written by a logger. type LogEntry struct { Level slog.Level TS time.Time Prefix string Message string Details string } // CreateAuthManagerFunc is called to create a new auth manager. type CreateAuthManagerFunc func(readonly bool, owner id.Zid) (auth.Manager, error) // CreateBoxManagerFunc is called to create a new box manager. type CreateBoxManagerFunc func( |
︙ | ︙ | |||
244 245 246 247 248 249 250 | webServer server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config, ) error const ( | | | | | | | | | | | | | | | | | | | | | < | > > | > | | | | | | | | | 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 | webServer server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config, ) error const ( defaultNormalLogLevel = slog.LevelInfo defaultSimpleLogLevel = slog.LevelError ) // Setup sets the most basic data of a software: its name, its version, // and when the version was created. func (kern *Kernel) Setup(progname, version string, versionTime time.Time) { _ = kern.SetConfig(CoreService, CoreProgname, progname) _ = kern.SetConfig(CoreService, CoreVersion, version) _ = kern.SetConfig(CoreService, CoreVTime, versionTime.Local().Format(id.TimestampLayout)) } // Start the service. func (kern *Kernel) Start(headline, lineServer bool, configFilename string) { for _, srvD := range kern.srvs { srvD.srv.Freeze() } if kern.cfg.GetCurConfig(ConfigSimpleMode).(bool) { kern.logLevelVar.Set(defaultSimpleLogLevel) } kern.wg.Add(1) signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM) go func() { // Wait for interrupt. sig := <-kern.interrupt if strSig := sig.String(); strSig != "" { kern.logger.Info("Shut down Zettelstore", "signal", strSig) } kern.doShutdown() kern.wg.Done() }() _ = kern.StartService(KernelService) if headline { logger := kern.logger logging.LogMandatory(logger, fmt.Sprintf( "%v %v (%v@%v/%v)", kern.core.GetCurConfig(CoreProgname), kern.core.GetCurConfig(CoreVersion), kern.core.GetCurConfig(CoreGoVersion), kern.core.GetCurConfig(CoreGoOS), kern.core.GetCurConfig(CoreGoArch), )) logging.LogMandatory(logger, "Licensed under the latest version of the EUPL (European Union Public License)") if configFilename != "" { logging.LogMandatory(logger, "Configuration file found", "filename", configFilename) } else { logging.LogMandatory(logger, "No configuration file found / used") } if kern.core.GetCurConfig(CoreDebug).(bool) { logger.Info("----------------------------------------") logger.Info("DEBUG MODE, DO NO USE THIS IN PRODUCTION") logger.Info("----------------------------------------") } if kern.auth.GetCurConfig(AuthReadonly).(bool) { logger.Info("Read-only mode") } } if lineServer { port := kern.core.GetNextConfig(CorePort).(int) if port > 0 { listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) _ = startLineServer(kern, listenAddr) } } } func (kern *Kernel) doShutdown() { kern.stopService(KernelService) // Will stop all other services. } // WaitForShutdown blocks the call until Shutdown is called. func (kern *Kernel) WaitForShutdown() { kern.wg.Wait() _ = kern.doStopProfiling() } // --- Shutdown operation ---------------------------------------------------- // Shutdown the service. Waits for all concurrent activities to stop. func (kern *Kernel) Shutdown(silent bool) { logging.LogTrace(kern.logger, "Shutdown") kern.interrupt <- &shutdownSignal{silent: silent} } type shutdownSignal struct{ silent bool } func (s *shutdownSignal) String() string { if s.silent { return "" } return "shutdown" } func (*shutdownSignal) Signal() { /* Just a signal */ } // --- Log operation --------------------------------------------------------- // GetKernelLogger returns the kernel logger. func (kern *Kernel) GetKernelLogger() *slog.Logger { return kern.logger } // GetKernelLogLevel return the logging level of the kernel logger. func (kern *Kernel) GetKernelLogLevel() slog.Level { return kern.logLevelVar.Level() } // SetLogLevel sets the logging level for logger maintained by the kernel. // // Its syntax is: (SERVICE ":")? LEVEL (";" (SERVICE ":")? LEVEL)*. func (kern *Kernel) SetLogLevel(logLevel string) { defaultLevel, srvLevel := kern.parseLogLevel(logLevel) kern.mx.RLock() defer kern.mx.RUnlock() for srvN, srvD := range kern.srvs { if lvl, found := srvLevel[srvN]; found { srvD.srv.SetLevel(lvl) } else if defaultLevel != logging.LevelMissing { srvD.srv.SetLevel(defaultLevel) } } } func (kern *Kernel) parseLogLevel(logLevel string) (slog.Level, map[Service]slog.Level) { defaultLevel := logging.LevelMissing srvLevel := map[Service]slog.Level{} for spec := range strings.SplitSeq(logLevel, ";") { vals := cleanLogSpec(strings.Split(spec, ":")) switch len(vals) { case 0: case 1: if lvl := logging.ParseLevel(vals[0]); lvl != logging.LevelMissing { defaultLevel = lvl } default: serviceText, levelText := vals[0], vals[1] if srv, found := kern.srvNames[serviceText]; found { if lvl := logging.ParseLevel(levelText); lvl != logging.LevelMissing { srvLevel[srv.srvnum] = lvl } } } } return defaultLevel, srvLevel } |
︙ | ︙ | |||
399 400 401 402 403 404 405 | } } return vals } // RetrieveLogEntries returns all buffered log entries. func (kern *Kernel) RetrieveLogEntries() []LogEntry { | | | | | 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 | } } return vals } // RetrieveLogEntries returns all buffered log entries. func (kern *Kernel) RetrieveLogEntries() []LogEntry { return kern.dlogWriter.retrieveLogEntries() } // GetLastLogTime returns the time when the last logging with level > DEBUG happened. func (kern *Kernel) GetLastLogTime() time.Time { return kern.dlogWriter.getLastLogTime() } // LogRecover outputs some information about the previous panic. func (kern *Kernel) LogRecover(name string, recoverInfo any) { stack := debug.Stack() kern.logger.Error(name, "recovered_from", recoverInfo, "stack", stack) kern.core.updateRecoverInfo(name, recoverInfo, stack) } // --- Profiling --------------------------------------------------------- var errProfileInWork = errors.New("already profiling") var errProfileNotFound = errors.New("profile not found") |
︙ | ︙ | |||
437 438 439 440 441 442 443 | return errProfileInWork } if profileName == ProfileCPU { f, err := os.Create(fileName) if err != nil { return err } | | < | | < | 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 | return errProfileInWork } if profileName == ProfileCPU { f, err := os.Create(fileName) if err != nil { return err } if err = pprof.StartCPUProfile(f); err != nil { _ = f.Close() return err } kern.profileName = profileName kern.fileName = fileName kern.profileFile = f return nil } profile := pprof.Lookup(profileName) if profile == nil { return errProfileNotFound } f, err := os.Create(fileName) if err != nil { return err } kern.profileName = profileName kern.fileName = fileName kern.profile = profile kern.profileFile = f runtime.GC() // get up-to-date statistics return profile.WriteTo(f, 0) } // StopProfiling stops the current profiling and writes the result to // the file, which was named during StartProfiling(). // It will always be called before the software stops its operations. func (kern *Kernel) StopProfiling() error { kern.mx.Lock() |
︙ | ︙ | |||
522 523 524 525 526 527 528 | if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetStatistics() } return nil } // GetLogger returns a logger for the given service. | | | 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 | if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetStatistics() } return nil } // GetLogger returns a logger for the given service. func (kern *Kernel) GetLogger(srvnum Service) *slog.Logger { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetLogger() } return kern.GetKernelLogger() } |
︙ | ︙ | |||
608 609 610 611 612 613 614 | } // dumpIndex writes some data about the internal index into a writer. func (kern *Kernel) dumpIndex(w io.Writer) { kern.box.dumpIndex(w) } type service interface { // Initialize the data for the service. | | | > > > > > > | 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 | } // dumpIndex writes some data about the internal index into a writer. func (kern *Kernel) dumpIndex(w io.Writer) { kern.box.dumpIndex(w) } type service interface { // Initialize the data for the service. Initialize(*slog.LevelVar, *slog.Logger) // Get service logger. GetLogger() *slog.Logger // Get the log level. GetLevel() slog.Level // Set the service log level. SetLevel(slog.Level) // ConfigDescriptions returns a sorted list of configuration descriptions. ConfigDescriptions() []serviceConfigDescription // SetConfig stores a configuration value. SetConfig(key, value string) error |
︙ | ︙ | |||
669 670 671 672 673 674 675 | // --- The kernel as a service ------------------------------------------- type kernelService struct { kernel *Kernel } | | > | > > > > | | | | | | | 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 | // --- The kernel as a service ------------------------------------------- type kernelService struct { kernel *Kernel } func (*kernelService) Initialize(*slog.LevelVar, *slog.Logger) {} func (ks *kernelService) GetLogger() *slog.Logger { return ks.kernel.logger } func (ks *kernelService) GetLevel() slog.Level { return ks.kernel.logLevelVar.Level() } func (ks *kernelService) SetLevel(lvl slog.Level) { ks.kernel.logLevelVar.Set(lvl) } func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil } func (*kernelService) SetConfig(string, string) error { return errAlreadyFrozen } func (*kernelService) GetCurConfig(string) any { return nil } func (*kernelService) GetNextConfig(string) any { return nil } func (*kernelService) GetCurConfigList(bool) []KeyDescrValue { return nil } func (*kernelService) GetNextConfigList() []KeyDescrValue { return nil } func (*kernelService) GetStatistics() []KeyValue { return nil } func (*kernelService) Freeze() {} func (*kernelService) Start(*Kernel) error { return nil } func (*kernelService) SwitchNextToCur() {} func (*kernelService) IsStarted() bool { return true } func (*kernelService) Stop(*Kernel) {} |
Changes to internal/kernel/log.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel 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 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 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "bytes" "context" "io" "log/slog" "slices" "sync" "time" "zettelstore.de/z/internal/logging" ) type kernelLogHandler struct { klw *kernelLogWriter level slog.Leveler system string attrs string } func newKernelLogHandler(klw *kernelLogWriter, level slog.Leveler) *kernelLogHandler { return &kernelLogHandler{ klw: klw, level: level, system: "", attrs: "", } } func (klh *kernelLogHandler) Enabled(_ context.Context, level slog.Level) bool { return level >= klh.level.Level() } func (klh *kernelLogHandler) Handle(_ context.Context, rec slog.Record) error { var buf bytes.Buffer buf.WriteString(klh.attrs) rec.Attrs(func(attr slog.Attr) bool { if !attr.Equal(slog.Attr{}) { buf.WriteByte(' ') buf.WriteString(attr.Key) buf.WriteByte('=') buf.WriteString(attr.Value.Resolve().String()) } return true }) return klh.klw.writeMessage(rec.Level, rec.Time, klh.system, rec.Message, buf.Bytes()) } func (klh *kernelLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { h := newKernelLogHandler(klh.klw, klh.level) for _, attr := range attrs { if attr.Equal(slog.Attr{}) { continue } if attr.Key == "system" { system := attr.Value.String() if len(system) < 6 { system += " "[:6-len(system)] } h.system = system continue } h.attrs += attr.String() } return h } func (klh *kernelLogHandler) WithGroup(name string) slog.Handler { if name == "" { return klh } return klh // panic("kernelLogHandler.WithGroup(name) not implemented") } // kernelLogWriter adapts an io.Writer to a LogWriter type kernelLogWriter struct { w io.Writer mx sync.RWMutex // protects buf, serializes w.Write and retrieveLogEntries lastLog time.Time buf []byte writePos int data []logEntry full bool } // newKernelLogWriter creates a new LogWriter for kernel logging. func newKernelLogWriter(w io.Writer, capacity int) *kernelLogWriter { if capacity < 1 { capacity = 1 } return &kernelLogWriter{ w: w, lastLog: time.Now(), buf: make([]byte, 0, 500), data: make([]logEntry, capacity), } } func (klw *kernelLogWriter) writeMessage(level slog.Level, ts time.Time, prefix, msg string, details []byte) error { details = bytes.TrimSpace(details) klw.mx.Lock() if level > slog.LevelDebug { if !ts.IsZero() { klw.lastLog = ts } klw.data[klw.writePos] = logEntry{ level: level, ts: ts, prefix: prefix, msg: msg, details: slices.Clone(details), } klw.writePos++ if klw.writePos >= cap(klw.data) { klw.writePos = 0 klw.full = true } } klw.buf = klw.buf[:0] buf := klw.buf addTimestamp(&buf, ts) buf = append(buf, ' ') buf = append(buf, logging.LevelStringPad(level)...) buf = append(buf, ' ') if prefix != "" { buf = append(buf, prefix...) buf = append(buf, ' ') } buf = append(buf, msg...) buf = append(buf, ' ') buf = append(buf, details...) buf = append(buf, '\n') _, err := klw.w.Write(buf) klw.mx.Unlock() return err } func addTimestamp(buf *[]byte, ts time.Time) { year, month, day := ts.Date() |
︙ | ︙ | |||
106 107 108 109 110 111 112 | b[bp] = byte('0' + i - q*10) i = q } *buf = append(*buf, b[:wid]...) } type logEntry struct { | | | 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | b[bp] = byte('0' + i - q*10) i = q } *buf = append(*buf, b[:wid]...) } type logEntry struct { level slog.Level ts time.Time prefix string msg string details []byte } func (klw *kernelLogWriter) retrieveLogEntries() []LogEntry { |
︙ | ︙ | |||
150 151 152 153 154 155 156 | return klw.lastLog } func copyE2E(result *LogEntry, origin *logEntry) { result.Level = origin.level result.TS = origin.ts result.Prefix = origin.prefix | | > | 221 222 223 224 225 226 227 228 229 230 | return klw.lastLog } func copyE2E(result *LogEntry, origin *logEntry) { result.Level = origin.level result.TS = origin.ts result.Prefix = origin.prefix result.Message = origin.msg result.Details = string(origin.details) } |
Added internal/kernel/log_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 | //----------------------------------------------------------------------------- // Copyright (c) 2025-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2025-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "fmt" "log/slog" "strings" "testing" "testing/slogtest" ) func TestLogHand(t *testing.T) { t.SkipNow() // Handler does not implement Groups var sb strings.Builder logWriter := newKernelLogWriter(&sb, 1000) h := newKernelLogHandler(logWriter, slog.LevelInfo) results := func() []map[string]any { var ms []map[string]any for _, entry := range logWriter.retrieveLogEntries() { m := map[string]any{ slog.LevelKey: entry.Level, slog.MessageKey: entry.Message, } if ts := entry.TS; !ts.IsZero() { m[slog.TimeKey] = ts } details := entry.Details fmt.Printf("%q\n", details) for len(details) > 0 { pos := strings.Index(details, "=") if pos <= 0 { break } key := details[:pos] details = details[pos+1:] if details == "" || details[0] == '[' { break } pos = strings.Index(details, " ") var val string if pos <= 0 { val = details details = "" } else { val = details[:pos] details = details[pos+1:] } fmt.Printf("key %q, val %q\n", key, val) m[key] = val } ms = append(ms, m) } return ms } err := slogtest.TestHandler(h, results) if err != nil { t.Error(err) } t.Error(sb.String()) } |
Changes to internal/kernel/server.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- package kernel import ( "bufio" "net" ) func startLineServer(kern *Kernel, listenAddr string) error { ln, err := net.Listen("tcp", listenAddr) if err != nil { | > > | | | | | | | 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 | //----------------------------------------------------------------------------- package kernel import ( "bufio" "net" "zettelstore.de/z/internal/logging" ) func startLineServer(kern *Kernel, listenAddr string) error { ln, err := net.Listen("tcp", listenAddr) if err != nil { kern.logger.Error("Unable to start administration console", "err", err) return err } logging.LogMandatory(kern.logger, "Start administration console", "listen", listenAddr) go func() { lineServer(ln, kern) }() return nil } func lineServer(ln net.Listener, kern *Kernel) { // Something may panic. Ensure a running line service. defer func() { if ri := recover(); ri != nil { kern.LogRecover("Line", ri) go lineServer(ln, kern) } }() for { conn, err := ln.Accept() if err != nil { // handle error kern.logger.Error("Unable to accept connection", "err", err) break } go handleLineConnection(conn, kern) } _ = ln.Close() } func handleLineConnection(conn net.Conn, kern *Kernel) { // Something may panic. Ensure a running connection. defer func() { if ri := recover(); ri != nil { kern.LogRecover("LineConn", ri) go handleLineConnection(conn, kern) } }() logging.LogMandatory(kern.logger, "Start session on administration console", "from", conn.RemoteAddr().String()) cmds := cmdSession{} cmds.initialize(conn, kern) s := bufio.NewScanner(conn) for s.Scan() { line := s.Text() if !cmds.executeLine(line) { break } } _ = conn.Close() } |
Changes to internal/kernel/web.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "errors" "net" "net/netip" "net/url" "os" "path/filepath" "strconv" "strings" "sync" "time" | > > > | | > | 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 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package kernel import ( "errors" "log/slog" "net" "net/netip" "net/url" "os" "path/filepath" "strconv" "strings" "sync" "time" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/web/server" ) type webService struct { srvConfig mxService sync.RWMutex srvw server.Server setupServer SetupWebServerFunc } var errURLPrefixSyntax = errors.New("must not be empty and must start with '//'") func (ws *webService) Initialize(levelVar *slog.LevelVar, logger *slog.Logger) { ws.logLevelVar = levelVar ws.logger = logger ws.descr = descriptionMap{ WebAssetDir: { "Asset file directory", func(val string) (any, error) { val = filepath.Clean(val) finfo, err := os.Stat(val) |
︙ | ︙ | |||
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | ap, err := netip.ParseAddrPort(val) if err != nil { return "", err } return ap.String(), nil }, true}, WebMaxRequestSize: {"Max Request Size", parseInt64, true}, WebPersistentCookie: {"Persistent cookie", parseBool, true}, WebProfiling: {"Runtime profiling", parseBool, true}, WebSecureCookie: {"Secure cookie", parseBool, true}, WebTokenLifetimeAPI: { "Token lifetime API", makeDurationParser(10*time.Minute, 0, 1*time.Hour), true, }, WebTokenLifetimeHTML: { "Token lifetime HTML", | > > > | 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | ap, err := netip.ParseAddrPort(val) if err != nil { return "", err } return ap.String(), nil }, true}, WebLoopbackIdent: {"Loopback user ident", ws.noFrozen(parseString), true}, WebLoopbackZid: {"Loopback user zettel identifier", ws.noFrozen(parseInvalidZid), true}, WebMaxRequestSize: {"Max Request Size", parseInt64, true}, WebPersistentCookie: {"Persistent cookie", parseBool, true}, WebProfiling: {"Runtime profiling", parseBool, true}, WebSecureCookie: {"Secure cookie", parseBool, true}, WebSxMaxNesting: {"Max nesting of Sx calls", parseInt, true}, WebTokenLifetimeAPI: { "Token lifetime API", makeDurationParser(10*time.Minute, 0, 1*time.Hour), true, }, WebTokenLifetimeHTML: { "Token lifetime HTML", |
︙ | ︙ | |||
103 104 105 106 107 108 109 110 111 112 | true, }, } ws.next = interfaceMap{ WebAssetDir: "", WebBaseURL: "http://127.0.0.1:23123/", WebListenAddress: "127.0.0.1:23123", WebMaxRequestSize: int64(16 * 1024 * 1024), WebPersistentCookie: false, WebSecureCookie: true, | > > > | | 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | true, }, } ws.next = interfaceMap{ WebAssetDir: "", WebBaseURL: "http://127.0.0.1:23123/", WebListenAddress: "127.0.0.1:23123", WebLoopbackIdent: "", WebLoopbackZid: id.Invalid, WebMaxRequestSize: int64(16 * 1024 * 1024), WebPersistentCookie: false, WebProfiling: false, WebSecureCookie: true, WebSxMaxNesting: 32 * 1024, WebTokenLifetimeAPI: 1 * time.Hour, WebTokenLifetimeHTML: 10 * time.Minute, WebURLPrefix: "/", } } func makeDurationParser(defDur, minDur, maxDur time.Duration) parseFunc { |
︙ | ︙ | |||
131 132 133 134 135 136 137 | } return defDur, nil } } var errWrongBasePrefix = errors.New(WebURLPrefix + " does not match " + WebBaseURL) | | > > > > < | | > > | | | | | | | | | | | 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 | } return defDur, nil } } var errWrongBasePrefix = errors.New(WebURLPrefix + " does not match " + WebBaseURL) func (ws *webService) GetLogger() *slog.Logger { return ws.logger } func (ws *webService) GetLevel() slog.Level { return ws.logLevelVar.Level() } func (ws *webService) SetLevel(l slog.Level) { ws.logLevelVar.Set(l) } func (ws *webService) Start(kern *Kernel) error { baseURL := ws.GetNextConfig(WebBaseURL).(string) listenAddr := ws.GetNextConfig(WebListenAddress).(string) loopbackIdent := ws.GetNextConfig(WebLoopbackIdent).(string) loopbackZid := ws.GetNextConfig(WebLoopbackZid).(id.Zid) urlPrefix := ws.GetNextConfig(WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(WebSecureCookie).(bool) profile := ws.GetNextConfig(WebProfiling).(bool) maxRequestSize := max(ws.GetNextConfig(WebMaxRequestSize).(int64), 1024) if !strings.HasSuffix(baseURL, urlPrefix) { ws.logger.Error("url-prefix is not a suffix of base-url", "base-url", baseURL, "url-prefix", urlPrefix) return errWrongBasePrefix } if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() { ws.logger.Info("service may be reached from outside, but authentication is not enabled", "listen", listenAddr) } sd := server.ConfigData{ Log: ws.logger, ListenAddr: listenAddr, BaseURL: baseURL, URLPrefix: urlPrefix, MaxRequestSize: maxRequestSize, Auth: kern.auth.manager, LoopbackIdent: loopbackIdent, LoopbackZid: loopbackZid, PersistentCookie: persistentCookie, SecureCookie: secureCookie, Profiling: profile, } srvw := server.New(sd) err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg) if err != nil { ws.logger.Error("Unable to create", "err", err) return err } if err = srvw.Run(); err != nil { ws.logger.Error("Unable to start", "err", err) return err } ws.logger.Info("Start Service", "listen", listenAddr, "base-url", baseURL) ws.mxService.Lock() ws.srvw = srvw ws.mxService.Unlock() if kern.cfg.GetCurConfig(ConfigSimpleMode).(bool) { listenAddr := ws.GetNextConfig(WebListenAddress).(string) if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { logging.LogMandatory(ws.logger, strings.Repeat("--------------------", 3)) logging.LogMandatory(ws.logger, "Open your browser and enter the following URL:") logging.LogMandatory(ws.logger, " http://localhost"+listenAddr[idx:]) logging.LogMandatory(ws.logger, "") logging.LogMandatory(ws.logger, "If this does not work, try:") logging.LogMandatory(ws.logger, " http://127.0.0.1"+listenAddr[idx:]) } } return nil } func (ws *webService) IsStarted() bool { ws.mxService.RLock() defer ws.mxService.RUnlock() return ws.srvw != nil } func (ws *webService) Stop(*Kernel) { ws.logger.Info("Stop Service") ws.srvw.Stop() ws.mxService.Lock() ws.srvw = nil ws.mxService.Unlock() } func (*webService) GetStatistics() []KeyValue { return nil } |
Deleted internal/logger/logger.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/logger/logger_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted internal/logger/message.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added internal/logging/logging.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 | //----------------------------------------------------------------------------- // Copyright (c) 2025-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2025-present Detlef Stern //----------------------------------------------------------------------------- // Package logging provides some definitions to adapt package slog to Zettelstore needs package logging import ( "context" "log/slog" "strings" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth/user" ) // Some additional log levels. const ( LevelMissing slog.Level = -9999 LevelTrace slog.Level = -8 LevelMandatory slog.Level = 9999 ) // LevelString returns a string naming the level. func LevelString(level slog.Level) string { switch level { case LevelTrace: return "TRACE" case LevelMandatory: return ">>>>>" case slog.LevelDebug: return "DEBUG" case slog.LevelInfo: return "INFO" case slog.LevelError: return "ERROR" default: return level.String() } } // LevelStringPad returns a string naming the level. The string is a least 5 bytes long. func LevelStringPad(level slog.Level) string { s := LevelString(level) if len(s) < 5 { s = s + " "[0:5-len(s)] } return s } // LogTrace writes a trace log message. func LogTrace(logger *slog.Logger, msg string, args ...any) { logger.Log(context.Background(), LevelTrace, msg, args...) } // LogMandatory writes a mandatory log message. func LogMandatory(logger *slog.Logger, msg string, args ...any) { logger.Log(context.Background(), LevelMandatory, msg, args...) } // ParseLevel returns the recognized level. func ParseLevel(text string) slog.Level { switch strings.ToUpper(text) { case "TR", "TRA", "TRAC", "TRACE": return LevelTrace case "DE", "DEB", "DEBU", "DEBUG": return slog.LevelDebug case "IN", "INF", "INFO": return slog.LevelInfo case "WA", "WAR", "WARN": return slog.LevelWarn case "ER", "ERR", "ERRO", "ERROR": return slog.LevelError } return LevelMissing } // Err returns a log attribute, if an error occurred. func Err(err error) slog.Attr { if err == nil { return slog.Attr{} } return slog.Any("err", err) } // User returns a log attribute indicating the currently user. func User(ctx context.Context) slog.Attr { if um := user.GetCurrentUser(ctx); um != nil { if userID, found := um.Get(meta.KeyUserID); found { return slog.Any("user", userID) } return slog.String("user", um.Zid.String()) } return slog.Attr{} } |
Changes to internal/parser/cleaner.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | // cleaner provides functions to clean up the parsed AST. import ( "strconv" "strings" | > | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // cleaner provides functions to clean up the parsed AST. import ( "strconv" "strings" zerostrings "t73f.de/r/zero/strings" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/encoder" ) // Clean cleans the given block list. func Clean(bs *ast.BlockSlice, allowHTML bool) { cv := cleanVisitor{ allowHTML: allowHTML, hasMark: false, |
︙ | ︙ | |||
118 119 120 121 122 123 124 | } if hn.Slug == "" { var sb strings.Builder _, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines) if err != nil { return } | | | | 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 | } if hn.Slug == "" { var sb strings.Builder _, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines) if err != nil { return } hn.Slug = zerostrings.Slugify(sb.String()) } if hn.Slug != "" { hn.Fragment = cv.addIdentifier(hn.Slug, hn) } } func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) { if !cv.doMark { cv.hasMark = true return } if mn.Mark == "" { mn.Slug = "" mn.Fragment = cv.addIdentifier("*", mn) return } if mn.Slug == "" { mn.Slug = zerostrings.Slugify(mn.Mark) } mn.Fragment = cv.addIdentifier(mn.Slug, mn) } func (cv *cleanVisitor) addIdentifier(id string, node ast.Node) string { if cv.ids == nil { cv.ids = map[string]ast.Node{id: node} |
︙ | ︙ | |||
159 160 161 162 163 164 165 | return newID } } } cv.ids[id] = node return id } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 160 161 162 163 164 165 166 | return newID } } } cv.ids[id] = node return id } |
Changes to internal/query/context.go.
︙ | ︙ | |||
24 25 26 27 28 29 30 | "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) // ContextSpec contains all specification values for calculating a context. type ContextSpec struct { | | | | | | < < < < < < < < < < | | < < < < < < < | | | | | | < < | | > > > | | | > > | | > > | 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 | "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) // ContextSpec contains all specification values for calculating a context. type ContextSpec struct { directionSpec maxCost int maxCount int minCount int full bool } // ContextPort is the collection of box methods needed by this directive. type ContextPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *Query) ([]*meta.Meta, error) } // Print the spec on the given print environment. func (spec *ContextSpec) Print(pe *PrintEnv) { pe.printSpace() pe.writeString(api.ContextDirective) if spec.full { pe.printSpace() pe.writeString(api.FullDirective) } spec.directionSpec.print(pe) pe.printPosInt(api.CostDirective, spec.maxCost) pe.printPosInt(api.MaxDirective, spec.maxCount) pe.printPosInt(api.MinDirective, spec.minCount) } // Execute the specification. func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta { maxCost := float64(spec.maxCost) if maxCost <= 0 { maxCost = 17 } maxCount := spec.maxCount if maxCount <= 0 { maxCount = 200 } tasks := newContextQueue(startSeq, maxCost, maxCount, spec.minCount, port) result := make([]*meta.Meta, 0, max(spec.minCount, 16)) for { m, cost, level, dir := tasks.next() if m == nil { break } if level == 1 { cost = min(cost, 4.0) } result = append(result, m) for key, val := range m.ComputedRest() { tasks.addPair(ctx, key, val, cost, level, dir, spec) } if spec.full { newDir := 0 if spec.isDirected { newDir = 1 } tasks.addTags(ctx, m.GetFields(meta.KeyTags), cost, level, newDir) } } return result } type ztlCtxItem struct { cost float64 meta *meta.Meta level uint dir int8 // <0: backward, >0: forward, =0: not directed } type ztlCtxQueue []ztlCtxItem func (q ztlCtxQueue) Len() int { return len(q) } func (q ztlCtxQueue) Less(i, j int) bool { levelI, levelJ := q[i].level, q[j].level if levelI == 0 { |
︙ | ︙ | |||
161 162 163 164 165 166 167 | maxCount int minCount int tagMetas map[string][]*meta.Meta tagZids map[string]*idset.Set // just the zids of tagMetas metaZid map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags } | | | > | > > > | | > > > | | | > | | > > > > > > > > > > > | | | < < | | | | | | | | | 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 | maxCount int minCount int tagMetas map[string][]*meta.Meta tagZids map[string]*idset.Set // just the zids of tagMetas metaZid map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags } func newContextQueue(startSeq []*meta.Meta, maxCost float64, maxCount, minCount int, port ContextPort) *contextTask { result := &contextTask{ port: port, seen: idset.New(), maxCost: maxCost, maxCount: max(maxCount, minCount), minCount: minCount, tagMetas: make(map[string][]*meta.Meta), tagZids: make(map[string]*idset.Set), metaZid: make(map[id.Zid]*meta.Meta), } queue := make(ztlCtxQueue, 0, len(startSeq)) for _, m := range startSeq { queue = append(queue, ztlCtxItem{cost: 1, meta: m}) } heap.Init(&queue) result.queue = queue return result } func (ct *contextTask) addPair(ctx context.Context, key string, value meta.Value, curCost float64, level uint, dir int, spec *ContextSpec) { if key == meta.KeyBack { return } newDir := 0 newCost := curCost + contextCost(key) if key == meta.KeyBackward { if spec.isBackward { if spec.isDirected { newDir = -1 } ct.addIDSet(ctx, newCost, level, newDir, value) } return } if key == meta.KeyForward { if spec.isForward { if spec.isDirected { newDir = -1 } ct.addIDSet(ctx, newCost, level, newDir, value) } return } if meta.Inverse(key) != "" { // Backward reference if !spec.isBackward || dir > 0 { return } newDir = -1 } else { // Forward reference if !spec.isForward || dir < 0 { return } newDir = 1 } if !spec.isDirected { newDir = 0 } if t := meta.Type(key); t == meta.TypeID { ct.addID(ctx, newCost, level, newDir, value) } else if t == meta.TypeIDSet { ct.addIDSet(ctx, newCost, level, newDir, value) } } func contextCost(key string) float64 { switch key { case meta.KeyFolge, meta.KeyPrecursor: return 0.2 case meta.KeySequel, meta.KeyPrequel: return 1.0 } return 2 } func (ct *contextTask) addID(ctx context.Context, newCost float64, level uint, dir int, value meta.Value) { if zid, errParse := id.Parse(string(value)); errParse == nil { if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil { ct.addMeta(m, newCost, level, dir) } } } func (ct *contextTask) addMeta(m *meta.Meta, newCost float64, level uint, dir int) { if !ct.seen.Contains(m.Zid) { heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m, level: level + 1, dir: int8(dir)}) } } func (ct *contextTask) addIDSet(ctx context.Context, newCost float64, level uint, dir int, value meta.Value) { elems := value.AsSlice() refCost := referenceCost(newCost, len(elems)) for _, val := range elems { ct.addID(ctx, refCost, level, dir, meta.Value(val)) } } func referenceCost(baseCost float64, numReferences int) float64 { nRefs := float64(numReferences) return nRefs*math.Log2(nRefs+1) + baseCost - 1 } func (ct *contextTask) addTags(ctx context.Context, tagiter iter.Seq[string], baseCost float64, level uint, dir int) { tags := slices.Collect(tagiter) var zidSet *idset.Set for _, tag := range tags { zs := ct.updateTagData(ctx, tag) zidSet = zidSet.IUnion(zs) } zidSet.ForEach(func(zid id.Zid) { minCost := math.MaxFloat64 costFactor := 1.1 for _, tag := range tags { tagZids := ct.tagZids[tag] if tagZids.Contains(zid) { cost := tagCost(baseCost, tagZids.Length()) if cost < minCost { minCost = cost } costFactor /= 1.1 } } ct.addMeta(ct.metaZid[zid], minCost*costFactor, level, dir) }) } func (ct *contextTask) updateTagData(ctx context.Context, tag string) *idset.Set { if _, found := ct.tagMetas[tag]; found { return ct.tagZids[tag] } |
︙ | ︙ | |||
300 301 302 303 304 305 306 | } func tagCost(baseCost float64, numTags int) float64 { nTags := float64(numTags) return nTags*math.Log2(nTags+1) + baseCost - 1 } | | | | | 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 | } func tagCost(baseCost float64, numTags int) float64 { nTags := float64(numTags) return nTags*math.Log2(nTags+1) + baseCost - 1 } func (ct *contextTask) next() (*meta.Meta, float64, uint, int) { for len(ct.queue) > 0 { item := heap.Pop(&ct.queue).(ztlCtxItem) m := item.meta zid := m.Zid if ct.seen.Contains(zid) { continue } cost, level := item.cost, item.level if ct.hasEnough(cost, level) { break } ct.seen.Add(zid) return m, cost, item.level, int(item.dir) } return nil, -1, 0, 0 } func (ct *contextTask) hasEnough(cost float64, level uint) bool { if level <= 1 { // Always add direct descendants of the initial zettel return false } |
︙ | ︙ |
Added internal/query/direction.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 | //----------------------------------------------------------------------------- // Copyright (c) 2025-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2025-present Detlef Stern //----------------------------------------------------------------------------- package query import "t73f.de/r/zsc/api" type directionSpec struct { isDirected bool isForward bool isBackward bool } func (ds *directionSpec) cleanupAfterParse() { if !ds.isForward && !ds.isBackward { ds.isForward, ds.isBackward = true, true } else if ds.isForward && ds.isBackward { ds.isDirected = true } } func (ds directionSpec) print(pe *PrintEnv) { if ds.isDirected { pe.printSpace() pe.writeString(api.DirectedDirective) } else if ds.isForward { if !ds.isBackward { pe.printSpace() pe.writeString(api.ForwardDirective) } } else if ds.isBackward { pe.printSpace() pe.writeString(api.BackwardDirective) } else { panic("neither forward, backward, nor directed") } } |
Changes to internal/query/parser.go.
︙ | ︙ | |||
97 98 99 100 101 102 103 | inp.SkipSpace() if ps.mustStop() { q.zids = nil break } } | | | > | > > | > > | > | > > > > > > > > | | | 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 | inp.SkipSpace() if ps.mustStop() { q.zids = nil break } } hasContext, hasThread := false, false for { inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if !hasContext && ps.acceptSingleKw(api.ContextDirective) { q = ps.parseContext(q) hasContext = true continue } inp.SetPos(pos) if !hasThread && ps.acceptSingleKw(api.FolgeDirective) { q = ps.parseThread(q, true, false) hasThread = true continue } inp.SetPos(pos) if !hasThread && ps.acceptSingleKw(api.SequelDirective) { q = ps.parseThread(q, false, true) hasThread = true continue } inp.SetPos(pos) if !hasThread && ps.acceptSingleKw(api.ThreadDirective) { q = ps.parseThread(q, true, true) hasThread = true continue } inp.SetPos(pos) if q == nil || len(q.zids) == 0 { break } if ps.acceptSingleKw(api.IdentDirective) { q.directives = append(q.directives, &IdentSpec{}) |
︙ | ︙ | |||
208 209 210 211 212 213 214 | for { inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptSingleKw(api.FullDirective) { | | < < < < < < | > > | > > > | | < < < < | < < < < < | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | for { inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptSingleKw(api.FullDirective) { spec.full = true continue } inp.SetPos(pos) if ps.parseDirection(&spec.directionSpec) { continue } inp.SetPos(pos) if ps.acceptKwArgs(api.CostDirective) { if ps.parseCost(spec) { continue } } inp.SetPos(pos) if ps.acceptKwArgs(api.MaxDirective) { if num, ok := ps.scanPosInt(); ok { if spec.maxCount == 0 || spec.maxCount >= num { spec.maxCount = num } continue } } inp.SetPos(pos) if ps.acceptKwArgs(api.MinDirective) { if ps.parseMinCount(spec) { continue } } inp.SetPos(pos) break } spec.directionSpec.cleanupAfterParse() q = createIfNeeded(q) q.directives = append(q.directives, spec) return q } func (ps *parserState) parseCost(spec *ContextSpec) bool { num, ok := ps.scanPosInt() if !ok { return false } if spec.maxCost == 0 || spec.maxCost >= num { spec.maxCost = num } return true } func (ps *parserState) parseMinCount(spec *ContextSpec) bool { num, ok := ps.scanPosInt() if !ok { return false } if spec.minCount == 0 || spec.minCount <= num { spec.minCount = num } return true } func (ps *parserState) parseThread(q *Query, isFolge, isSequel bool) *Query { inp := ps.inp spec := &ThreadSpec{isFolge: isFolge, isSequel: isSequel} for { inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.parseDirection(&spec.directionSpec) { continue } inp.SetPos(pos) if ps.acceptKwArgs(api.MaxDirective) { if num, ok := ps.scanPosInt(); ok { if spec.maxCount == 0 || spec.maxCount >= num { spec.maxCount = num } continue } } inp.SetPos(pos) break } spec.directionSpec.cleanupAfterParse() q = createIfNeeded(q) q.directives = append(q.directives, spec) return q } func (ps *parserState) parseDirection(ds *directionSpec) bool { inp := ps.inp pos := inp.Pos if !ds.isBackward && !ds.isForward && ps.acceptSingleKw(api.DirectedDirective) { ds.isDirected = true return true } inp.SetPos(pos) if !ds.isDirected && !ds.isBackward && ps.acceptSingleKw(api.BackwardDirective) { ds.isBackward = true return true } inp.SetPos(pos) if !ds.isDirected && !ds.isForward && ps.acceptSingleKw(api.ForwardDirective) { ds.isForward = true return true } return false } func (ps *parserState) parseUnlinked(q *Query) *Query { inp := ps.inp spec := &UnlinkedSpec{} for { inp.SkipSpace() |
︙ | ︙ |
Changes to internal/query/parser_test.go.
︙ | ︙ | |||
50 51 52 53 54 55 56 57 58 59 60 61 62 63 | {"1 CONTEXT | N", "00000000000001 CONTEXT | N"}, {"1 1 CONTEXT", "00000000000001 CONTEXT"}, {"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"}, {"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"}, {"1 CONTEXT|N", "00000000000001 CONTEXT | N"}, {"CONTEXT 0", "CONTEXT 0"}, {"1 UNLINKED", "00000000000001 UNLINKED"}, {"UNLINKED", "UNLINKED"}, {"1 UNLINKED PHRASE", "00000000000001 UNLINKED PHRASE"}, {"1 UNLINKED PHRASE Zettel", "00000000000001 UNLINKED PHRASE Zettel"}, {"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"}, | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | {"1 CONTEXT | N", "00000000000001 CONTEXT | N"}, {"1 1 CONTEXT", "00000000000001 CONTEXT"}, {"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"}, {"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"}, {"1 CONTEXT|N", "00000000000001 CONTEXT | N"}, {"CONTEXT 0", "CONTEXT 0"}, {"FOLGE", "FOLGE"}, {"FOLGE a", "FOLGE a"}, {"0 FOLGE", "0 FOLGE"}, {"1 FOLGE", "00000000000001 FOLGE"}, {"1 FOLGE FOLGE", "00000000000001 FOLGE FOLGE"}, {"00000000000001 FOLGE", "00000000000001 FOLGE"}, {"100000000000001 FOLGE", "100000000000001 FOLGE"}, {"1 FOLGE DIRECTED", "00000000000001 FOLGE DIRECTED"}, {"1 FOLGE BACKWARD", "00000000000001 FOLGE BACKWARD"}, {"1 FOLGE FORWARD", "00000000000001 FOLGE FORWARD"}, {"1 FOLGE MAX 5", "00000000000001 FOLGE MAX 5"}, {"1 FOLGE MAX y", "00000000000001 FOLGE MAX y"}, {"1 FOLGE | N", "00000000000001 FOLGE | N"}, {"1 1 FOLGE", "00000000000001 FOLGE"}, {"1 2 FOLGE", "00000000000001 00000000000002 FOLGE"}, {"2 1 FOLGE", "00000000000002 00000000000001 FOLGE"}, {"1 FOLGE|N", "00000000000001 FOLGE | N"}, {"FOLGE 0", "FOLGE 0"}, {"SEQUEL", "SEQUEL"}, {"SEQUEL a", "SEQUEL a"}, {"0 SEQUEL", "0 SEQUEL"}, {"1 SEQUEL", "00000000000001 SEQUEL"}, {"1 SEQUEL SEQUEL", "00000000000001 SEQUEL SEQUEL"}, {"00000000000001 SEQUEL", "00000000000001 SEQUEL"}, {"100000000000001 SEQUEL", "100000000000001 SEQUEL"}, {"1 SEQUEL DIRECTED", "00000000000001 SEQUEL DIRECTED"}, {"1 SEQUEL BACKWARD", "00000000000001 SEQUEL BACKWARD"}, {"1 SEQUEL FORWARD", "00000000000001 SEQUEL FORWARD"}, {"1 SEQUEL MAX 5", "00000000000001 SEQUEL MAX 5"}, {"1 SEQUEL MAX y", "00000000000001 SEQUEL MAX y"}, {"1 SEQUEL | N", "00000000000001 SEQUEL | N"}, {"1 1 SEQUEL", "00000000000001 SEQUEL"}, {"1 2 SEQUEL", "00000000000001 00000000000002 SEQUEL"}, {"2 1 SEQUEL", "00000000000002 00000000000001 SEQUEL"}, {"1 SEQUEL|N", "00000000000001 SEQUEL | N"}, {"SEQUEL 0", "SEQUEL 0"}, {"THREAD", "THREAD"}, {"THREAD a", "THREAD a"}, {"0 THREAD", "0 THREAD"}, {"1 THREAD", "00000000000001 THREAD"}, {"1 THREAD THREAD", "00000000000001 THREAD THREAD"}, {"00000000000001 THREAD", "00000000000001 THREAD"}, {"100000000000001 THREAD", "100000000000001 THREAD"}, {"1 THREAD DIRECTED", "00000000000001 THREAD DIRECTED"}, {"1 THREAD BACKWARD", "00000000000001 THREAD BACKWARD"}, {"1 THREAD FORWARD", "00000000000001 THREAD FORWARD"}, {"1 THREAD MAX 5", "00000000000001 THREAD MAX 5"}, {"1 THREAD MAX y", "00000000000001 THREAD MAX y"}, {"1 THREAD | N", "00000000000001 THREAD | N"}, {"1 1 THREAD", "00000000000001 THREAD"}, {"1 2 THREAD", "00000000000001 00000000000002 THREAD"}, {"2 1 THREAD", "00000000000002 00000000000001 THREAD"}, {"1 THREAD|N", "00000000000001 THREAD | N"}, {"THREAD 0", "THREAD 0"}, {"1 UNLINKED", "00000000000001 UNLINKED"}, {"UNLINKED", "UNLINKED"}, {"1 UNLINKED PHRASE", "00000000000001 UNLINKED PHRASE"}, {"1 UNLINKED PHRASE Zettel", "00000000000001 UNLINKED PHRASE Zettel"}, {"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"}, |
︙ | ︙ |
Changes to internal/query/print.go.
︙ | ︙ | |||
92 93 94 95 96 97 98 | space bool } var bsSpace = []byte{' '} func (pe *PrintEnv) printSpace() { if pe.space { | | | | | | 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | space bool } var bsSpace = []byte{' '} func (pe *PrintEnv) printSpace() { if pe.space { _, _ = pe.w.Write(bsSpace) return } pe.space = true } func (pe *PrintEnv) write(ch byte) { _, _ = pe.w.Write([]byte{ch}) } func (pe *PrintEnv) writeString(s string) { _, _ = io.WriteString(pe.w, s) } func (pe *PrintEnv) writeStrings(sSeq ...string) { for _, s := range sSeq { _, _ = io.WriteString(pe.w, s) } } func (pe *PrintEnv) printZids(zids []id.Zid) { for i, zid := range zids { if i > 0 { pe.printSpace() |
︙ | ︙ |
Changes to internal/query/retrieve.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 | // This file contains helper functions to search within the index. import ( "fmt" "strings" "t73f.de/r/zsc/domain/id/idset" | > < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // This file contains helper functions to search within the index. import ( "fmt" "strings" zerostrings "t73f.de/r/zero/strings" "t73f.de/r/zsc/domain/id/idset" ) type searchOp struct { s string op compareOp } type searchFunc func(string) *idset.Set |
︙ | ︙ | |||
65 66 67 68 69 70 71 | scm[searchOp{s: s, op: op}] = sf } func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) { normCalls = make(searchCallMap, len(search)) negCalls = make(searchCallMap, len(search)) for _, val := range search { | | | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | scm[searchOp{s: s, op: op}] = sf } func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) { normCalls = make(searchCallMap, len(search)) negCalls = make(searchCallMap, len(search)) for _, val := range search { for _, word := range zerostrings.NormalizeWords(string(val.value)) { if cmpOp := val.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) } else { normCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) } } |
︙ | ︙ |
Changes to internal/query/select.go.
︙ | ︙ | |||
59 60 61 62 63 64 65 | count++ } } return count } func (ct *conjTerms) createSelectSpecs() (posSpecs, negSpecs []matchSpec) { | < < | < | | 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 | count++ } } return count } func (ct *conjTerms) createSelectSpecs() (posSpecs, negSpecs []matchSpec) { for key, values := range ct.mvals { if !meta.KeyIsValid(key) { continue } posMatch, negMatch := createPosNegMatchFunc(key, values, ct.addSearch) if posMatch != nil { posSpecs = append(posSpecs, matchSpec{key, posMatch}) } if negMatch != nil { negSpecs = append(negSpecs, matchSpec{key, negMatch}) } } return posSpecs, negSpecs } type addSearchFunc func(val expValue) func noAddSearch(expValue) { /* Just does nothing, for negated queries, or property keys */ } func createPosNegMatchFunc(key string, values []expValue, addSearch addSearchFunc) (posMatch, negMatch matchValueFunc) { var posValues, negValues []expValue for _, val := range values { if val.op.isNegated() { negValues = append(negValues, val) } else { posValues = append(posValues, val) } } |
︙ | ︙ |
Added internal/query/thread.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 | //----------------------------------------------------------------------------- // Copyright (c) 2025-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2025-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "container/heap" "context" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) // ThreadSpec contains all information for a thread directive. type ThreadSpec struct { directionSpec isFolge bool isSequel bool maxCount int } // Print the spec on the given print environment. func (spec *ThreadSpec) Print(pe *PrintEnv) { pe.printSpace() if spec.isFolge { if spec.isSequel { pe.writeString(api.ThreadDirective) } else { pe.writeString(api.FolgeDirective) } } else if spec.isSequel { pe.writeString(api.SequelDirective) } else { panic("neither folge nor sequel") } spec.directionSpec.print(pe) pe.printPosInt(api.MaxDirective, spec.maxCount) } // ThreadPort is the collection of box methods needed by this directive. type ThreadPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // Execute the specification. func (spec *ThreadSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ThreadPort) []*meta.Meta { tasks := newThreadQueue(startSeq, spec.maxCount, port) result := make([]*meta.Meta, 0, 16) for { m, level, dir := tasks.next() if m == nil { break } result = append(result, m) for key, val := range m.ComputedRest() { tasks.addPair(ctx, key, val, level, dir, spec) } } return result } type ztlThreadItem struct { meta *meta.Meta level uint dir int8 // <0: backward, >0: forward, =0: not directed } type ztlThreadQueue []ztlThreadItem func (q ztlThreadQueue) Len() int { return len(q) } func (q ztlThreadQueue) Less(i, j int) bool { if levelI, levelJ := q[i].level, q[j].level; levelI < levelJ { return true } else if levelI == levelJ { return q[i].meta.Zid < q[j].meta.Zid } return false } func (q ztlThreadQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] } func (q *ztlThreadQueue) Push(x any) { *q = append(*q, x.(ztlThreadItem)) } func (q *ztlThreadQueue) Pop() any { old := *q n := len(old) item := old[n-1] old[n-1].meta = nil // avoid memory leak *q = old[0 : n-1] return item } type threadTask struct { port ThreadPort seen *idset.Set queue ztlThreadQueue maxCount int } func newThreadQueue(startSeq []*meta.Meta, maxCount int, port ThreadPort) *threadTask { result := &threadTask{ port: port, seen: idset.New(), maxCount: maxCount, } queue := make(ztlThreadQueue, 0, len(startSeq)) for _, m := range startSeq { queue = append(queue, ztlThreadItem{meta: m}) } heap.Init(&queue) result.queue = queue return result } func (ct *threadTask) next() (*meta.Meta, uint, int) { for len(ct.queue) > 0 { item := heap.Pop(&ct.queue).(ztlThreadItem) m := item.meta zid := m.Zid if ct.seen.Contains(zid) { continue } level := item.level if ct.hasEnough(level) { break } ct.seen.Add(zid) return m, item.level, int(item.dir) } return nil, 0, 0 } func (ct *threadTask) hasEnough(level uint) bool { maxCount := ct.maxCount if level <= 1 || ct.maxCount <= 0 { // Always add direct descendants of the initial zettel return false } return maxCount <= ct.seen.Length() } func (ct *threadTask) addPair(ctx context.Context, key string, value meta.Value, level uint, dir int, spec *ThreadSpec) { isFolge, isSequel, isBackward, isForward := spec.isFolge, spec.isSequel, spec.isBackward, spec.isForward nextDir := int8(0) switch key { case meta.KeyPrecursor: if !isFolge || !isBackward || dir > 0 { return } nextDir = -1 case meta.KeyFolge: if !isFolge || !isForward || dir < 0 { return } nextDir = 1 case meta.KeyPrequel: if !isSequel || !isBackward || dir > 0 { return } nextDir = -1 case meta.KeySequel: if !isSequel || !isForward || dir < 0 { return } nextDir = 1 default: return } if !spec.isDirected { nextDir = 0 } elems := value.AsSlice() for _, val := range elems { if zid, errParse := id.Parse(val); errParse == nil { if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil { if !ct.seen.Contains(m.Zid) { heap.Push(&ct.queue, ztlThreadItem{meta: m, level: level + 1, dir: nextDir}) } } } } } |
Changes to internal/query/unlinked.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" | > < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "t73f.de/r/zero/strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" ) // UnlinkedSpec contains all specification values to calculate unlinked references. type UnlinkedSpec struct { words []string } |
︙ | ︙ | |||
42 43 44 45 46 47 48 | } result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title for _, m := range metaSeq { title, hasTitle := m.Get(meta.KeyTitle) if !hasTitle { continue } | | | 42 43 44 45 46 47 48 49 50 51 52 | } result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title for _, m := range metaSeq { title, hasTitle := m.Get(meta.KeyTitle) if !hasTitle { continue } result = append(result, strings.MakeWords(string(title))...) } return result } |
Changes to internal/usecase/authenticate.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "math/rand/v2" "net/http" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/cred" | > > < | | | | | | | | | | | | | | | | | | | | | | | 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 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "log/slog" "math/rand/v2" "net/http" "time" "t73f.de/r/webs/ip" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/cred" ) // Authenticate is the data for this use case. type Authenticate struct { log *slog.Logger token auth.TokenManager ucGetUser *GetUser } // NewAuthenticate creates a new use case. func NewAuthenticate(log *slog.Logger, token auth.TokenManager, ucGetUser *GetUser) Authenticate { return Authenticate{ log: log, token: token, ucGetUser: ucGetUser, } } // Run executes the use case. // // Parameter "r" is just included to produce better logging messages. It may be nil. Do not use it // for other purposes. func (uc *Authenticate) Run(ctx context.Context, r *http.Request, ident, credential string, d time.Duration, k auth.TokenKind) ([]byte, error) { identMeta, err := uc.ucGetUser.Run(ctx, ident) defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond) if identMeta == nil || err != nil { uc.log.Info("No user with given ident found", "ident", ident, "err", err, "remote", ip.GetRemoteAddr(r)) compensateCompare() return nil, err } if hashCred, ok := identMeta.Get(meta.KeyCredential); ok { ok, err = cred.CompareHashAndCredential(string(hashCred), identMeta.Zid, ident, credential) if err != nil { uc.log.Info("Error while comparing credentials", "ident", ident, "err", err, "remote", ip.GetRemoteAddr(r)) return nil, err } if ok { token, err2 := uc.token.GetToken(identMeta, d, k) if err2 != nil { uc.log.Info("Unable to produce authentication token", "ident", ident, "err", err2) return nil, err2 } uc.log.Info("Successful", "user", ident) return token, nil } uc.log.Info("Credentials don't match", "ident", ident, "remote", ip.GetRemoteAddr(r)) return nil, nil } uc.log.Info("No credential stored", "ident", ident) compensateCompare() return nil, nil } // compensateCompare if normal comapare is not possible, to avoid timing hints. func compensateCompare() { _, _ = cred.CompareHashAndCredential( "$2a$10$WHcSO3G9afJ3zlOYQR1suuf83bCXED2jmzjti/MH4YH4l2mivDuze", id.Invalid, "", "") } // addDelay after credential checking to allow some CPU time for other tasks. // durDelay is the normal delay, if time spend for checking is smaller than // the minimum delay minDelay. In addition some jitter (+/- 50 ms) is added. func addDelay(start time.Time, durDelay, minDelay time.Duration) { jitter := time.Duration(rand.IntN(100)-50) * time.Millisecond if elapsed := time.Since(start); elapsed+minDelay < durDelay { time.Sleep(durDelay - elapsed + jitter) } else { time.Sleep(minDelay + jitter) } } // IsAuthenticatedPort contains method for this usecase. type IsAuthenticatedPort interface { GetCurrentUser(context.Context) *meta.Meta } // IsAuthenticated cheks if the caller is already authenticated. type IsAuthenticated struct { logger *slog.Logger port IsAuthenticatedPort authz auth.AuthzManager } // NewIsAuthenticated creates a new use case object. func NewIsAuthenticated(logger *slog.Logger, port IsAuthenticatedPort, authz auth.AuthzManager) IsAuthenticated { return IsAuthenticated{ logger: logger, port: port, authz: authz, } } // IsAuthenticatedResult is an enumeration. type IsAuthenticatedResult uint8 // Values for IsAuthenticatedResult. const ( _ IsAuthenticatedResult = iota IsAuthenticatedDisabled IsAuthenticatedAndValid IsAuthenticatedAndInvalid ) // Run executes the use case. func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult { if !uc.authz.WithAuth() { uc.logger.Info("IsAuthenticated", "auth", "disabled") return IsAuthenticatedDisabled } if uc.port.GetCurrentUser(ctx) == nil { uc.logger.Info("IsAuthenticated is false") return IsAuthenticatedAndInvalid } uc.logger.Info("IsAuthenticated is true") return IsAuthenticatedAndValid } |
Changes to internal/usecase/create_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/config" | > | | | | < < < < < < < < < < < | 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 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "log/slog" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/zettel" ) // CreateZettelPort is the interface used by this use case. type CreateZettelPort interface { // CreateZettel creates a new zettel. CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) } // CreateZettel is the data for this use case. type CreateZettel struct { logger *slog.Logger rtConfig config.Config port CreateZettelPort } // NewCreateZettel creates a new use case. func NewCreateZettel(logger *slog.Logger, rtConfig config.Config, port CreateZettelPort) CreateZettel { return CreateZettel{ logger: logger, rtConfig: rtConfig, port: port, } } // PrepareCopy the zettel for further modification. func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel { origMeta := origZettel.Meta m := origMeta.Clone() if title, found := origMeta.Get(meta.KeyTitle); found { m.Set(meta.KeyTitle, prependTitle(title, "Copy", "Copy of ")) } setReadonly(m) content := origZettel.Content content.TrimSpace() return zettel.Zettel{Meta: m, Content: content} } // PrepareFolge the zettel for further modification. func (*CreateZettel) PrepareFolge(origZettel zettel.Zettel) zettel.Zettel { |
︙ | ︙ | |||
149 150 151 152 153 154 155 | m.Set(meta.KeyCreated, meta.Value(time.Now().Local().Format(id.TimestampLayout))) m.Delete(meta.KeyModified) m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() zid, err := uc.port.CreateZettel(ctx, zettel) | | | 139 140 141 142 143 144 145 146 147 148 | m.Set(meta.KeyCreated, meta.Value(time.Now().Local().Format(id.TimestampLayout))) m.Delete(meta.KeyModified) m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() zid, err := uc.port.CreateZettel(ctx, zettel) uc.logger.Info("Create zettel", "zid", zid, logging.User(ctx), logging.Err(err)) return zid, err } |
Changes to internal/usecase/delete_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/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 40 41 42 43 44 45 46 47 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "log/slog" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/logging" ) // DeleteZettelPort is the interface used by this use case. type DeleteZettelPort interface { // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } // DeleteZettel is the data for this use case. type DeleteZettel struct { logger *slog.Logger port DeleteZettelPort } // NewDeleteZettel creates a new use case. func NewDeleteZettel(logger *slog.Logger, port DeleteZettelPort) DeleteZettel { return DeleteZettel{logger: logger, port: port} } // Run executes the use case. func (uc *DeleteZettel) Run(ctx context.Context, zid id.Zid) error { err := uc.port.DeleteZettel(ctx, zid) uc.logger.Info("Delete zettel", "zid", zid, logging.User(ctx), logging.Err(err)) return err } |
Changes to internal/usecase/get_all_zettel.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/zettel" ) // GetAllZettelPort is the interface used by this use case. type GetAllZettelPort interface { GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) } | > | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/zettel" ) // GetAllZettelPort is the interface used by this use case. type GetAllZettelPort interface { GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) } |
︙ | ︙ |
Changes to internal/usecase/get_references.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package usecase import ( "iter" zeroiter "t73f.de/r/zero/iter" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/collect" ) // GetReferences is the usecase to retrieve references that occur in a zettel. type GetReferences struct{} | > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package usecase import ( "iter" zeroiter "t73f.de/r/zero/iter" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/collect" ) // GetReferences is the usecase to retrieve references that occur in a zettel. type GetReferences struct{} |
︙ | ︙ |
Changes to internal/usecase/get_zettel.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/zettel" ) // GetZettelPort is the interface used by this use case. type GetZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) | > | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/zettel" ) // GetZettelPort is the interface used by this use case. type GetZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) |
︙ | ︙ |
Changes to internal/usecase/query.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import ( "context" "errors" "fmt" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/collect" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" | > < | 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 | import ( "context" "errors" "fmt" "strings" zerostrings "t73f.de/r/zero/strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/collect" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/zettel" ) // QueryPort is the interface used by this use case. type QueryPort interface { GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) |
︙ | ︙ | |||
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 | for _, dir := range directives { if len(metaSeq) == 0 { return nil } switch ds := dir.(type) { case *query.ContextSpec: metaSeq = uc.processContextDirective(ctx, ds, metaSeq) case *query.IdentSpec: // Nothing to do. case *query.ItemsSpec: metaSeq = uc.processItemsDirective(ctx, ds, metaSeq) case *query.UnlinkedSpec: metaSeq = uc.processUnlinkedDirective(ctx, ds, metaSeq) default: panic(fmt.Sprintf("Unknown directive %T", ds)) } } if len(metaSeq) == 0 { return nil } return metaSeq } func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta { return spec.Execute(ctx, metaSeq, uc.port) } func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta { result := make([]*meta.Meta, 0, len(metaSeq)) for _, m := range metaSeq { zn, err := uc.ucEvaluate.Run(ctx, m.Zid, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax))) if err != nil { continue | > > > > > > > | 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 | for _, dir := range directives { if len(metaSeq) == 0 { return nil } switch ds := dir.(type) { case *query.ContextSpec: metaSeq = uc.processContextDirective(ctx, ds, metaSeq) case *query.ThreadSpec: metaSeq = uc.processThreadDirective(ctx, ds, metaSeq) case *query.IdentSpec: // Nothing to do. case *query.ItemsSpec: metaSeq = uc.processItemsDirective(ctx, ds, metaSeq) case *query.UnlinkedSpec: metaSeq = uc.processUnlinkedDirective(ctx, ds, metaSeq) default: panic(fmt.Sprintf("Unknown directive %T", ds)) } } if len(metaSeq) == 0 { return nil } return metaSeq } func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta { return spec.Execute(ctx, metaSeq, uc.port) } func (uc *Query) processThreadDirective(ctx context.Context, spec *query.ThreadSpec, metaSeq []*meta.Meta) []*meta.Meta { return spec.Execute(ctx, metaSeq, uc.port) } func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta { result := make([]*meta.Meta, 0, len(metaSeq)) for _, m := range metaSeq { zn, err := uc.ucEvaluate.Run(ctx, m.Zid, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax))) if err != nil { continue |
︙ | ︙ | |||
252 253 254 255 256 257 258 | func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string { var result []string var curList []string for _, in := range *is { switch n := in.(type) { case *ast.TextNode: | | | 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string { var result []string var curList []string for _, in := range *is { switch n := in.(type) { case *ast.TextNode: curList = append(curList, zerostrings.MakeWords(n.Text)...) default: if curList != nil { result = append(result, v.joinWords(curList)) curList = nil } } } if curList != nil { result = append(result, v.joinWords(curList)) } return result } |
Changes to internal/usecase/refresh.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" | > | | | | | | | 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 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "log/slog" "zettelstore.de/z/internal/logging" ) // RefreshPort is the interface used by this use case. type RefreshPort interface { Refresh(context.Context) error } // Refresh is the data for this use case. type Refresh struct { logger *slog.Logger port RefreshPort } // NewRefresh creates a new use case. func NewRefresh(logger *slog.Logger, port RefreshPort) Refresh { return Refresh{logger: logger, port: port} } // Run executes the use case. func (uc *Refresh) Run(ctx context.Context) error { err := uc.port.Refresh(ctx) uc.logger.Info("Refresh internal data", logging.User(ctx), logging.Err(err)) return err } |
Changes to internal/usecase/reindex.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 | // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/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 40 41 42 43 44 45 46 | // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "log/slog" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/logging" ) // ReIndexPort is the interface used by this use case. type ReIndexPort interface { ReIndex(context.Context, id.Zid) error } // ReIndex is the data for this use case. type ReIndex struct { logger *slog.Logger port ReIndexPort } // NewReIndex creates a new use case. func NewReIndex(logger *slog.Logger, port ReIndexPort) ReIndex { return ReIndex{logger: logger, port: port} } // Run executes the use case. func (uc *ReIndex) Run(ctx context.Context, zid id.Zid) error { err := uc.port.ReIndex(ctx, zid) uc.logger.Info("ReIndex zettel", "zid", zid, logging.User(ctx), logging.Err(err)) return err } |
Changes to internal/usecase/update_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" | > | | | | | | 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 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package usecase import ( "context" "log/slog" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/zettel" ) // UpdateZettelPort is the interface used by this use case. type UpdateZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) // UpdateZettel updates an existing zettel. UpdateZettel(ctx context.Context, zettel zettel.Zettel) error } // UpdateZettel is the data for this use case. type UpdateZettel struct { logger *slog.Logger port UpdateZettelPort } // NewUpdateZettel creates a new use case. func NewUpdateZettel(logger *slog.Logger, port UpdateZettelPort) UpdateZettel { return UpdateZettel{logger: logger, port: port} } // Run executes the use case. func (uc *UpdateZettel) Run(ctx context.Context, zettel zettel.Zettel, hasContent bool) error { m := zettel.Meta oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid) if err != nil { |
︙ | ︙ | |||
69 70 71 72 73 74 75 | } if !hasContent { zettel.Content = oldZettel.Content } zettel.Content.TrimSpace() err = uc.port.UpdateZettel(ctx, zettel) | | | 70 71 72 73 74 75 76 77 78 79 | } if !hasContent { zettel.Content = oldZettel.Content } zettel.Content.TrimSpace() err = uc.port.UpdateZettel(ctx, zettel) uc.logger.Info("Update zettel", "zid", m.Zid, logging.User(ctx), logging.Err(err)) return err } |
Changes to internal/web/adapter/api/api.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // Package api provides api handlers for web requests. package api import ( "bytes" "context" "net/http" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" | > | | | | | | | | | | | 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 | // Package api provides api handlers for web requests. package api import ( "bytes" "context" "log/slog" "net/http" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/server" ) // API holds all data and methods for delivering API call results. type API struct { logger *slog.Logger b server.Builder authz auth.AuthzManager token auth.TokenManager rtConfig config.Config policy auth.Policy tokenLifetime time.Duration } // New creates a new API object. func New(logger *slog.Logger, b server.Builder, authz auth.AuthzManager, token auth.TokenManager, rtConfig config.Config, pol auth.Policy) *API { a := &API{ logger: logger, b: b, authz: authz, token: token, rtConfig: rtConfig, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration), } return a } // NewURLBuilder creates a new URL builder object with the given key. func (a *API) NewURLBuilder(key byte) *api.URLBuilder { return a.b.NewURLBuilder(key) } func (a *API) getAuthData(ctx context.Context) *user.AuthData { return user.GetAuthData(ctx) } func (a *API) withAuth() bool { return a.authz.WithAuth() } func (a *API) getToken(ident *meta.Meta) ([]byte, error) { return a.token.GetToken(ident, a.tokenLifetime, auth.KindAPI) } func (a *API) reportUsecaseError(w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { a.logger.Error(text, "err", err) http.Error(w, http.StatusText(code), code) return } // TODO: must call PrepareHeader somehow http.Error(w, text, code) } func writeBuffer(w http.ResponseWriter, buf *bytes.Buffer, contentType string) error { return adapter.WriteData(w, buf.Bytes(), contentType) } func (a *API) getRights(ctx context.Context, m *meta.Meta) (result api.ZettelRights) { pol := a.policy user := user.GetCurrentUser(ctx) if pol.CanCreate(user, m) { result |= api.ZettelCanCreate } if pol.CanRead(user, m) { result |= api.ZettelCanRead } if pol.CanWrite(user, m, m) { |
︙ | ︙ |
Changes to internal/web/adapter/api/create_zettel.go.
︙ | ︙ | |||
69 70 71 72 73 74 75 | panic(encStr) } h := adapter.PrepareHeader(w, contentType) h.Set(api.HeaderLocation, location.String()) w.WriteHeader(http.StatusCreated) if _, err = w.Write(result); err != nil { | | | 69 70 71 72 73 74 75 76 77 78 79 | panic(encStr) } h := adapter.PrepareHeader(w, contentType) h.Set(api.HeaderLocation, location.String()) w.WriteHeader(http.StatusCreated) if _, err = w.Write(result); err != nil { a.logger.Error("Create zettel", "err", err, "zid", newZid) } }) } |
Changes to internal/web/adapter/api/get_data.go.
︙ | ︙ | |||
30 31 32 33 34 35 36 | sx.Int64(version.Major), sx.Int64(version.Minor), sx.Int64(version.Patch), sx.MakeString(version.Info), sx.MakeString(version.Hash), )) if err != nil { | | | 30 31 32 33 34 35 36 37 38 39 40 | sx.Int64(version.Major), sx.Int64(version.Minor), sx.Int64(version.Patch), sx.MakeString(version.Info), sx.MakeString(version.Hash), )) if err != nil { a.logger.Error("Write version info", "err", err) } }) } |
Changes to internal/web/adapter/api/get_references.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 25 26 27 28 29 30 31 | "iter" "net/http" "t73f.de/r/sx" zeroiter "t73f.de/r/zero/iter" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/content" ) // MakeGetReferencesHandler creates a new HTTP handler to return various lists // of zettel references. | > | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | "iter" "net/http" "t73f.de/r/sx" zeroiter "t73f.de/r/zero/iter" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/content" ) // MakeGetReferencesHandler creates a new HTTP handler to return various lists // of zettel references. |
︙ | ︙ | |||
61 62 63 64 65 66 67 | } enc, _ := getEncoding(r, q) if enc == api.EncoderData { var lb sx.ListBuilder lb.Collect(zeroiter.MapSeq(seq, func(s string) sx.Object { return sx.MakeString(s) })) if err = a.writeObject(w, zid, lb.List()); err != nil { | | | | 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 | } enc, _ := getEncoding(r, q) if enc == api.EncoderData { var lb sx.ListBuilder lb.Collect(zeroiter.MapSeq(seq, func(s string) sx.Object { return sx.MakeString(s) })) if err = a.writeObject(w, zid, lb.List()); err != nil { a.logger.Error("write sx data", "err", err, "zid", zid) } return } var buf bytes.Buffer for s := range seq { buf.WriteString(s) buf.WriteByte('\n') } if err = writeBuffer(w, &buf, content.PlainText); err != nil { a.logger.Error("write plain data", "err", err, "zid", zid) } }) } func getExternalURLs(zn *ast.ZettelNode, ucGetReferences usecase.GetReferences) iter.Seq[string] { return zeroiter.MapSeq( ucGetReferences.RunByExternal(zn), func(ref *ast.Reference) string { return ref.Value }, ) } |
Changes to internal/web/adapter/api/get_zettel.go.
︙ | ︙ | |||
99 100 101 102 103 104 105 | case partContent: contentType = content.MIMEFromSyntax(string(z.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax))) _, err = z.Content.Write(&buf) } if err != nil { | | | | 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | case partContent: contentType = content.MIMEFromSyntax(string(z.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax))) _, err = z.Content.Write(&buf) } if err != nil { a.logger.Error("Unable to store plain zettel/part in buffer", "err", err, "zid", zid) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if err = writeBuffer(w, &buf, contentType); err != nil { a.logger.Error("write plain data", "err", err, "zid", zid) } } func (a *API) writeSzData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) { z, err := getZettel.Run(ctx, zid) if err != nil { a.reportUsecaseError(w, err) |
︙ | ︙ | |||
132 133 134 135 136 137 138 | case partMeta: obj = sexp.EncodeMetaRights(api.MetaRights{ Meta: z.Meta.Map(), Rights: a.getRights(ctx, z.Meta), }) } if err = a.writeObject(w, zid, obj); err != nil { | | | 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | case partMeta: obj = sexp.EncodeMetaRights(api.MetaRights{ Meta: z.Meta.Map(), Rights: a.getRights(ctx, z.Meta), }) } if err = a.writeObject(w, zid, obj); err != nil { a.logger.Error("write sx data", "err", err, "zid", zid) } } func (a *API) writeEncodedZettelPart( ctx context.Context, w http.ResponseWriter, zn *ast.ZettelNode, enc api.EncodingEnum, encStr string, part partType, |
︙ | ︙ | |||
161 162 163 164 165 166 167 | _, err = encdr.WriteZettel(&buf, zn) case partMeta: _, err = encdr.WriteMeta(&buf, zn.InhMeta) case partContent: _, err = encdr.WriteBlocks(&buf, &zn.BlocksAST) } if err != nil { | | | | 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | _, err = encdr.WriteZettel(&buf, zn) case partMeta: _, err = encdr.WriteMeta(&buf, zn.InhMeta) case partContent: _, err = encdr.WriteBlocks(&buf, &zn.BlocksAST) } if err != nil { a.logger.Error("Unable to store data in buffer", "err", err, "zid", zn.Zid) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if buf.Len() == 0 { w.WriteHeader(http.StatusNoContent) return } if err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)); err != nil { a.logger.Error("Write encoded zettel", "err", err, "zid", zn.Zid) } } |
Changes to internal/web/adapter/api/login.go.
︙ | ︙ | |||
26 27 28 29 30 31 32 | ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !a.withAuth() { if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { | | | | | | | 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 | ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !a.withAuth() { if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { a.logger.Error("Login/free", "err", err) } return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { var err error token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindAPI) if err != nil { a.reportUsecaseError(w, err) return } } if len(token) == 0 { w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil { a.logger.Error("Login", "err", err) } }) } func retrieveIdentCred(r *http.Request) (string, string) { if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok { return ident, cred } if ident, cred, ok := r.BasicAuth(); ok { return ident, cred } return "", "" } // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. func (a *API) MakeRenewAuthHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if !a.withAuth() { if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { a.logger.Error("Refresh/free", "err", err) } return } authData := a.getAuthData(ctx) if authData == nil || len(authData.Token) == 0 || authData.User == nil { adapter.BadRequest(w, "Not authenticated") return } totalLifetime := authData.Expires.Sub(authData.Issued) currentLifetime := authData.Now.Sub(authData.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { if err := a.writeToken(w, string(authData.Token), totalLifetime-currentLifetime); err != nil { a.logger.Error("write old token", "err", err) } return } // Token is a little bit aged. Create a new one token, err := a.getToken(authData.User) if err != nil { a.reportUsecaseError(w, err) return } if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil { a.logger.Error("write renewed token", "err", err) } }) } func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error { return a.writeObject(w, id.Invalid, sx.MakeList( sx.MakeString("Bearer"), sx.MakeString(token), sx.Int64(int64(lifetime/time.Second)), )) } |
Changes to internal/web/adapter/api/query.go.
︙ | ︙ | |||
91 92 93 94 95 96 97 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } var buf bytes.Buffer err = queryAction(&buf, encoder, metaSeq, actions) if err != nil { | | | | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } var buf bytes.Buffer err = queryAction(&buf, encoder, metaSeq, actions) if err != nil { a.logger.Error("execute query action", "err", err, "query", sq) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } if err = writeBuffer(w, &buf, contentType); err != nil { a.logger.Error("write result buffer", "err", err) } }) } func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, actions []string) error { minVal, maxVal := -1, -1 if len(actions) > 0 { acts := make([]string, 0, len(actions)) |
︙ | ︙ | |||
296 297 298 299 300 301 302 | return true } func (a *API) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder, zid id.Zid) { w.Header().Set(api.HeaderContentType, content.PlainText) http.Redirect(w, r, ub.String(), http.StatusFound) if _, err := io.WriteString(w, zid.String()); err != nil { | | | 296 297 298 299 300 301 302 303 304 305 | return true } func (a *API) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder, zid id.Zid) { w.Header().Set(api.HeaderContentType, content.PlainText) http.Redirect(w, r, ub.String(), http.StatusFound) if _, err := io.WriteString(w, zid.String()); err != nil { a.logger.Error("redirect body", "err", err) } } |
Changes to internal/web/adapter/api/request.go.
︙ | ︙ | |||
20 21 22 23 24 25 26 27 28 29 30 31 32 33 | "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sexp" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/zettel" ) // getEncoding returns the data encoding selected by the caller. func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) { encoding := q.Get(api.QueryKeyEncoding) if encoding != "" { | > | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sexp" "t73f.de/r/zsx/input" "zettelstore.de/z/internal/zettel" ) // getEncoding returns the data encoding selected by the caller. func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) { encoding := q.Get(api.QueryKeyEncoding) if encoding != "" { |
︙ | ︙ | |||
81 82 83 84 85 86 87 | func getPart(q url.Values, defPart partType) partType { if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok { return part } return defPart } | < < < < < < < < < < < < < < < < < < < | | | 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 | func getPart(q url.Values, defPart partType) partType { if part, ok := partMap[q.Get(api.QueryKeyPart)]; ok { return part } return defPart } func buildZettelFromPlainData(r *http.Request, zid id.Zid) (zettel.Zettel, error) { defer func() { _ = r.Body.Close() }() b, err := io.ReadAll(r.Body) if err != nil { return zettel.Zettel{}, err } inp := input.NewInput(b) m := meta.NewFromInput(zid, inp) return zettel.Zettel{ Meta: m, Content: zettel.NewContent(inp.Src[inp.Pos:]), }, nil } func buildZettelFromData(r *http.Request, zid id.Zid) (zettel.Zettel, error) { defer func() { _ = r.Body.Close() }() rdr := sxreader.MakeReader(r.Body) obj, err := rdr.Read() if err != nil { return zettel.Zettel{}, err } zd, err := sexp.ParseZettel(obj) if err != nil { |
︙ | ︙ |
Changes to internal/web/adapter/api/response.go.
︙ | ︙ | |||
22 23 24 25 26 27 28 | "zettelstore.de/z/internal/web/content" ) func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error { var buf bytes.Buffer if _, err := sx.Print(&buf, obj); err != nil { | < < < < < | < | 22 23 24 25 26 27 28 29 30 31 32 33 34 | "zettelstore.de/z/internal/web/content" ) func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error { var buf bytes.Buffer if _, err := sx.Print(&buf, obj); err != nil { a.logger.Error("Unable to store object in buffer", "err", err, "zid", zid) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return nil } return writeBuffer(w, &buf, content.SXPF) } |
Changes to internal/web/adapter/request.go.
︙ | ︙ | |||
25 26 27 28 29 30 31 | "zettelstore.de/z/internal/query" ) // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { err := r.ParseForm() if err != nil { | | | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | "zettelstore.de/z/internal/query" ) // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { err := r.ParseForm() if err != nil { kernel.Main.GetLogger(kernel.WebService).Info("Unable to parse form", "err", err) return "", "", false } ident = strings.TrimSpace(r.PostFormValue("username")) cred = r.PostFormValue("password") if ident == "" { return "", "", false |
︙ | ︙ |
Changes to internal/web/adapter/webui/const.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | // WebUI related constants. const queryKeyAction = "_action" // Values for queryKeyAction const ( | | | | | < < | | | | < | 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 | // WebUI related constants. const queryKeyAction = "_action" // Values for queryKeyAction const ( valueActionCopy = "copy" valueActionFolge = "folge" valueActionNew = "new" valueActionSequel = "sequel" ) // Enumeration for queryKeyAction type createAction uint8 const ( actionCopy createAction = iota actionFolge actionNew actionSequel ) var createActionMap = map[string]createAction{ valueActionSequel: actionSequel, valueActionCopy: actionCopy, valueActionFolge: actionFolge, valueActionNew: actionNew, } func getCreateAction(s string) createAction { if action, found := createActionMap[s]; found { return action } return actionCopy |
︙ | ︙ |
Changes to internal/web/adapter/webui/create_zettel.go.
︙ | ︙ | |||
21 22 23 24 25 26 27 28 29 30 31 32 | "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/evaluator" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" | > < | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/evaluator" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/zettel" ) // MakeGetCreateZettelHandler creates a new HTTP handler to display the // HTML edit view for the various zettel creation methods. func (wui *WebUI) MakeGetCreateZettelHandler( getZettel usecase.GetZettel, |
︙ | ︙ | |||
66 67 68 69 70 71 72 | wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData) case actionNew: title := ast.NormalizedSpacedText(origZettel.Meta.GetTitle()) newTitle := ast.NormalizedSpacedText(q.Get(meta.KeyTitle)) wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData) case actionSequel: wui.renderZettelForm(ctx, w, createZettel.PrepareSequel(origZettel), "Sequel Zettel", "", roleData, syntaxData) | < < | 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData) case actionNew: title := ast.NormalizedSpacedText(origZettel.Meta.GetTitle()) newTitle := ast.NormalizedSpacedText(q.Get(meta.KeyTitle)) wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData) case actionSequel: wui.renderZettelForm(ctx, w, createZettel.PrepareSequel(origZettel), "Sequel Zettel", "", roleData, syntaxData) } }) } func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) { roleData := dataListFromArrangement(ucListRoles.Run(ctx)) syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx)) |
︙ | ︙ | |||
96 97 98 99 100 101 102 | w http.ResponseWriter, ztl zettel.Zettel, title string, formActionURL string, roleData []string, syntaxData []string, ) { | | | | 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 | w http.ResponseWriter, ztl zettel.Zettel, title string, formActionURL string, roleData []string, syntaxData []string, ) { user := user.GetCurrentUser(ctx) m := ztl.Meta var sb strings.Builder for key, val := range m.Rest() { sb.WriteString(key) sb.WriteString(": ") sb.WriteString(string(val)) sb.WriteByte('\n') } env, rb := wui.createRenderEnvironment(ctx, "form", wui.getUserLang(ctx), title, user) rb.bindString("heading", sx.MakeString(title)) rb.bindString("form-action-url", sx.MakeString(formActionURL)) rb.bindString("role-data", makeStringList(roleData)) rb.bindString("syntax-data", makeStringList(syntaxData)) rb.bindString("meta", sx.MakeString(sb.String())) if !ztl.Content.IsBinary() { rb.bindString("content", sx.MakeString(ztl.Content.AsString())) |
︙ | ︙ | |||
136 137 138 139 140 141 142 | reEdit, zettel, err := parseZettelForm(r, id.Invalid) if err == errMissingContent { wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing")) return } if err != nil { const msg = "Unable to read form data" | | | 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | reEdit, zettel, err := parseZettelForm(r, id.Invalid) if err == errMissingContent { wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing")) return } if err != nil { const msg = "Unable to read form data" wui.logger.Info(msg, "err", err) wui.reportError(ctx, w, adapter.NewErrBadRequest(msg)) return } newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) |
︙ | ︙ |
Changes to internal/web/adapter/webui/delete_zettel.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 | "slices" "t73f.de/r/sx" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" | | | | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | "slices" "t73f.de/r/sx" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/usecase" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func (wui *WebUI) MakeGetDeleteZettelHandler( getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel, |
︙ | ︙ | |||
45 46 47 48 49 50 51 | zs, err := getAllZettel.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } m := zs[0].Meta | | | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | zs, err := getAllZettel.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } m := zs[0].Meta user := user.GetCurrentUser(ctx) env, rb := wui.createRenderEnvironment( ctx, "delete", wui.getUserLang(ctx), "Delete Zettel "+m.Zid.String(), user) if len(zs) > 1 { rb.bindString("shadowed-box", sx.MakeString(string(zs[1].Meta.GetDefault(meta.KeyBoxNumber, "???")))) rb.bindString("incoming", nil) } else { rb.bindString("shadowed-box", nil) rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel))) |
︙ | ︙ |
Changes to internal/web/adapter/webui/edit_zettel.go.
︙ | ︙ | |||
63 64 65 66 67 68 69 | } reEdit, zettel, err := parseZettelForm(r, zid) hasContent := true if err != nil { if err != errMissingContent { const msg = "Unable to read zettel form" | | | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | } reEdit, zettel, err := parseZettelForm(r, zid) hasContent := true if err != nil { if err != errMissingContent { const msg = "Unable to read zettel form" wui.logger.Info(msg, "err", err) wui.reportError(ctx, w, adapter.NewErrBadRequest(msg)) return } hasContent = false } if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil { wui.reportError(ctx, w, err) |
︙ | ︙ |
Changes to internal/web/adapter/webui/favicon.go.
︙ | ︙ | |||
24 25 26 27 28 29 30 | // MakeFaviconHandler creates a HTTP handler to retrieve the favicon. func (wui *WebUI) MakeFaviconHandler(baseDir string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { filename := filepath.Join(baseDir, "favicon.ico") f, err := os.Open(filename) if err != nil { | | | | | | 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 | // MakeFaviconHandler creates a HTTP handler to retrieve the favicon. func (wui *WebUI) MakeFaviconHandler(baseDir string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { filename := filepath.Join(baseDir, "favicon.ico") f, err := os.Open(filename) if err != nil { wui.logger.Debug("Favicon not found", "err", err) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } defer func() { _ = f.Close() }() data, err := io.ReadAll(f) if err != nil { wui.logger.Error("Unable to read favicon data", "err", err) http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } if err = adapter.WriteData(w, data, ""); err != nil { wui.logger.Error("Write favicon", "err", err) } }) } |
Changes to internal/web/adapter/webui/forms.go.
︙ | ︙ | |||
103 104 105 106 107 108 109 | } return nil } func uploadedContent(r *http.Request, m *meta.Meta) ([]byte, *meta.Meta) { file, fh, err := r.FormFile("file") if file != nil { | | | 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | } return nil } func uploadedContent(r *http.Request, m *meta.Meta) ([]byte, *meta.Meta) { file, fh, err := r.FormFile("file") if file != nil { defer func() { _ = file.Close() }() if err == nil { data, err2 := io.ReadAll(file) if err2 != nil { return nil, m } if cts, found := fh.Header["Content-Type"]; found && len(cts) > 0 { ct := cts[0] |
︙ | ︙ |
Changes to internal/web/adapter/webui/get_info.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import ( "context" "net/http" "slices" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/evaluator" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/usecase" | > > < < | 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 | import ( "context" "net/http" "slices" "strings" "t73f.de/r/sx" zerostrings "t73f.de/r/zero/strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/encoder" "zettelstore.de/z/internal/evaluator" "zettelstore.de/z/internal/query" "zettelstore.de/z/internal/usecase" ) // MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetInfoHandler( ucParseZettel usecase.ParseZettel, ucGetReferences usecase.GetReferences, ucEvaluate *usecase.Evaluate, |
︙ | ︙ | |||
87 88 89 90 91 92 93 | if err != nil { wui.reportError(ctx, w, err) return } encTexts := encodingTexts() shadowLinks := getShadowLinks(ctx, zid, zn.InhMeta.GetDefault(meta.KeyBoxNumber, ""), ucGetAllZettel) | | | | 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | if err != nil { wui.reportError(ctx, w, err) return } encTexts := encodingTexts() shadowLinks := getShadowLinks(ctx, zid, zn.InhMeta.GetDefault(meta.KeyBoxNumber, ""), ucGetAllZettel) user := user.GetCurrentUser(ctx) env, rb := wui.createRenderEnvironment(ctx, "info", wui.getUserLang(ctx), title, user) rb.bindString("metadata", lbMetadata.List()) rb.bindString("local-links", locLinks) rb.bindString("query-links", queryLinks) rb.bindString("ext-links", extLinks) rb.bindString("unlinked-content", unlinkedContent) rb.bindString("phrase", sx.MakeString(phrase)) rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase)) |
︙ | ︙ | |||
134 135 136 137 138 139 140 | } func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query { var sb strings.Builder sb.Write(zid.Bytes()) sb.WriteByte(' ') sb.WriteString(api.UnlinkedDirective) | | | 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | } func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query { var sb strings.Builder sb.Write(zid.Bytes()) sb.WriteByte(' ') sb.WriteString(api.UnlinkedDirective) for _, word := range zerostrings.MakeWords(phrase) { sb.WriteByte(' ') sb.WriteString(api.PhraseDirective) sb.WriteByte(' ') sb.WriteString(word) } sb.WriteByte(' ') sb.WriteString(api.OrderDirective) |
︙ | ︙ |
Changes to internal/web/adapter/webui/get_zettel.go.
︙ | ︙ | |||
22 23 24 25 26 27 28 29 30 31 | "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/usecase" | > < | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/usecase" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetHTMLZettelHandler( evaluate *usecase.Evaluate, getZettel usecase.GetZettel, ) http.Handler { |
︙ | ︙ | |||
58 59 60 61 62 63 64 | metaObj := enc.MetaSxn(zn.InhMeta) content, endnotes, err := enc.BlocksSxn(&zn.BlocksAST) if err != nil { wui.reportError(ctx, w, err) return } | | | < < | | 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 | metaObj := enc.MetaSxn(zn.InhMeta) content, endnotes, err := enc.BlocksSxn(&zn.BlocksAST) if err != nil { wui.reportError(ctx, w, err) return } user := user.GetCurrentUser(ctx) getTextTitle := wui.makeGetTextTitle(ctx, getZettel) title := ast.NormalizedSpacedText(zn.InhMeta.GetTitle()) env, rb := wui.createRenderEnvironment(ctx, "zettel", zettelLang, title, user) rb.bindSymbol(symMetaHeader, metaObj) rb.bindString("heading", sx.MakeString(title)) if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" { rb.bindString( "role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery( meta.KeyRole+api.SearchOperatorHas+string(role)).String())) } if folgeRole, found := zn.InhMeta.Get(meta.KeyFolgeRole); found && folgeRole != "" { rb.bindString( "folge-role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery( meta.KeyRole+api.SearchOperatorHas+string(folgeRole)).String())) } rb.bindString("tag-refs", wui.transformTagSet(meta.KeyTags, zn.InhMeta.GetDefault(meta.KeyTags, "").AsSlice())) rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeyPrecursor, getTextTitle)) rb.bindString("prequel-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeyPrequel, getTextTitle)) rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeySuperior, getTextTitle)) rb.bindString("urls", metaURLAssoc(zn.InhMeta)) rb.bindString("content", content) rb.bindString("endnotes", endnotes) wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, meta.KeyFolge, config.KeyShowFolgeLinks, getTextTitle) wui.bindLinks(ctx, &rb, "sequel", zn.InhMeta, meta.KeySequel, config.KeyShowSequelLinks, getTextTitle) wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, meta.KeySubordinates, config.KeyShowSubordinateLinks, getTextTitle) wui.bindLinks(ctx, &rb, "back", zn.InhMeta, meta.KeyBack, config.KeyShowBackLinks, getTextTitle) if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" { for _, part := range []string{"meta", "actions", "heading"} { rb.rebindResolved("ROLE-"+string(role)+"-"+part, "ROLE-EXTRA-"+part) } } wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content) if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ZidZettelTemplate, env) } else { err = rb.err |
︙ | ︙ |
Changes to internal/web/adapter/webui/home.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 | import ( "context" "errors" "net/http" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/web/adapter" | > < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import ( "context" "errors" "net/http" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/zettel" ) type getRootPort interface { GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } |
︙ | ︙ | |||
48 49 50 51 52 53 54 | homeZid = id.ZidDefaultHome } _, err := s.GetZettel(ctx, homeZid) if err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | homeZid = id.ZidDefaultHome } _, err := s.GetZettel(ctx, homeZid) if err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && user.GetCurrentUser(ctx) == nil { wui.redirectFound(w, r, wui.NewURLBuilder('i')) return } wui.redirectFound(w, r, wui.NewURLBuilder('h')) }) } |
Changes to internal/web/adapter/webui/htmlgen.go.
︙ | ︙ | |||
271 272 273 274 275 276 277 | sh, err := g.th.Evaluate(sx, &env) if err != nil { return nil, nil, err } return sh, shtml.Endnotes(&env), nil } | < < < < < < < < | 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | sh, err := g.th.Evaluate(sx, &env) if err != nil { return nil, nil, err } return sh, shtml.Endnotes(&env), nil } func (g *htmlGenerator) nodeSxHTML(node ast.Node) *sx.Pair { sz := g.tx.GetSz(node) env := shtml.MakeEnvironment(g.lang) sh, err := g.th.Evaluate(sz, &env) if err != nil { return nil } |
︙ | ︙ |
Changes to internal/web/adapter/webui/htmlmeta.go.
︙ | ︙ | |||
110 111 112 113 114 115 116 | return lb.List() } return nil } func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair { var lb sx.ListBuilder | < > > | | 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | return lb.List() } return nil } func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair { var lb sx.ListBuilder for i, tag := range tags { if i == 0 { lb.Add(shtml.SymSPAN) } else if i > 0 { lb.Add(space) } lb.Add(wui.transformKeyValueText(key, meta.Value(tag), tag)) } if len(tags) > 1 { lb.AddN(space, wui.transformKeyValuesText(key, tags, "(all)")) } |
︙ | ︙ |
Changes to internal/web/adapter/webui/lists.go.
︙ | ︙ | |||
25 26 27 28 29 30 31 32 33 34 | "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/evaluator" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" | > < | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/evaluator" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML. func (wui *WebUI) MakeListHTMLMetaHandler( queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, |
︙ | ︙ | |||
79 80 81 82 83 84 85 | wui.reportError(ctx, w, err) return } numEntries = cnt } siteName := wui.rtConfig.GetSiteName() | | | | 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | wui.reportError(ctx, w, err) return } numEntries = cnt } siteName := wui.rtConfig.GetSiteName() user := user.GetCurrentUser(ctx) env, rb := wui.createRenderEnvironment(ctx, "list", userLang, siteName, user) if q == nil { rb.bindString("heading", sx.MakeString(siteName)) } else { var sb strings.Builder q.PrintHuman(&sb) rb.bindString("heading", sx.MakeString(sb.String())) } |
︙ | ︙ |
Changes to internal/web/adapter/webui/login.go.
︙ | ︙ | |||
36 37 38 39 40 41 42 | return } wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) }) } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { | | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | return } wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) }) } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { env, rb := wui.createRenderEnvironment(ctx, "login", wui.getUserLang(ctx), "Login", nil) rb.bindString("retry", sx.MakeBoolean(retry)) if rb.err == nil { rb.err = wui.renderSxnTemplate(ctx, w, id.ZidLoginTemplate, env) } if err := rb.err; err != nil { wui.reportError(ctx, w, err) } |
︙ | ︙ |
Changes to internal/web/adapter/webui/response.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | "net/http" "t73f.de/r/zsc/api" ) func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { us := ub.String() | | | 17 18 19 20 21 22 23 24 25 26 | "net/http" "t73f.de/r/zsc/api" ) func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { us := ub.String() wui.logger.Debug("redirect", "uri", us) http.Redirect(w, r, us, http.StatusFound) } |
Changes to internal/web/adapter/webui/sxn_code.go.
︙ | ︙ | |||
36 37 38 39 40 41 42 | return z.Meta, nil } dg := buildSxnCodeDigraph(ctx, id.ZidSxnStart, getMeta) if dg == nil { return nil, wui.rootBinding, nil } dg = dg.AddVertex(id.ZidSxnBase).AddEdge(id.ZidSxnStart, id.ZidSxnBase) | < | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | return z.Meta, nil } dg := buildSxnCodeDigraph(ctx, id.ZidSxnStart, getMeta) if dg == nil { return nil, wui.rootBinding, nil } dg = dg.AddVertex(id.ZidSxnBase).AddEdge(id.ZidSxnStart, id.ZidSxnBase) dg = dg.TransitiveClosure(id.ZidSxnStart) if zid, isDAG := dg.IsDAG(); !isDAG { return nil, nil, fmt.Errorf("zettel %v is part of a dependency cycle", zid) } bind := wui.rootBinding.MakeChildBinding("zettel", 128) for _, zid := range dg.SortReverse() { |
︙ | ︙ | |||
88 89 90 91 92 93 94 | } func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid, bind *sxeval.Binding) error { rdr, err := wui.makeZettelReader(ctx, zid) if err != nil { return err } | | | | | 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | } func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid, bind *sxeval.Binding) error { rdr, err := wui.makeZettelReader(ctx, zid) if err != nil { return err } env := sxeval.MakeEnvironment(bind) for { form, err2 := rdr.Read() if err2 != nil { if err2 == io.EOF { return nil } return err2 } wui.logger.Debug("Loaded sxn code", "zid", zid, "form", form) if _, err2 = env.Eval(form, nil); err2 != nil { return err2 } } } |
Changes to internal/web/adapter/webui/template.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | package webui import ( "bytes" "context" "fmt" "net/http" "net/url" "t73f.de/r/sx" "t73f.de/r/sx/sxbuiltins" "t73f.de/r/sx/sxeval" "t73f.de/r/sx/sxreader" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/collect" "zettelstore.de/z/internal/config" | > > > | | | | | > < < | | | | | | | | > | | | | | | | | | | | | | | | | | > | | | | | 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 | package webui import ( "bytes" "context" "fmt" "log/slog" "net/http" "net/url" "strings" "t73f.de/r/sx" "t73f.de/r/sx/sxbuiltins" "t73f.de/r/sx/sxeval" "t73f.de/r/sx/sxreader" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/internal/ast" "zettelstore.de/z/internal/auth/user" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/collect" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/logging" "zettelstore.de/z/internal/parser" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/zettel" ) func (wui *WebUI) createRootBinding() *sxeval.Binding { root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 32) _ = sxbuiltins.LoadPrelude(root) _ = sxeval.BindSpecials(root, specials...) _ = sxeval.BindBuiltins(root, builtins...) _ = sxeval.BindBuiltins(root, &sxeval.Builtin{ Name: "url-to-html", MinArity: 1, MaxArity: 1, TestPure: sxeval.AssertPure, Fn1: func(_ *sxeval.Environment, arg sx.Object, _ *sxeval.Frame) (sx.Object, error) { text, err := sxbuiltins.GetString(arg, 0) if err != nil { return nil, err } return wui.url2html(text), nil }, }, &sxeval.Builtin{ Name: "zid-content-path", MinArity: 1, MaxArity: 1, TestPure: sxeval.AssertPure, Fn1: func(_ *sxeval.Environment, arg sx.Object, _ *sxeval.Frame) (sx.Object, error) { s, err := sxbuiltins.GetString(arg, 0) if err != nil { return nil, err } zid, err := id.Parse(s.GetValue()) if err != nil { return nil, fmt.Errorf("parsing zettel identifier %q: %w", s.GetValue(), err) } ub := wui.NewURLBuilder('z').SetZid(zid) return sx.MakeString(ub.String()), nil }, }, &sxeval.Builtin{ Name: "query->url", MinArity: 1, MaxArity: 1, TestPure: sxeval.AssertPure, Fn1: func(_ *sxeval.Environment, arg sx.Object, _ *sxeval.Frame) (sx.Object, error) { qs, err := sxbuiltins.GetString(arg, 0) if err != nil { return nil, err } u := wui.NewURLBuilder('h').AppendQuery(qs.GetValue()) return sx.MakeString(u.String()), nil }, }) root.Freeze() return root } var ( specials = []*sxeval.Special{ &sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote &sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing &sxbuiltins.DefVarS, // defvar &sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda &sxbuiltins.SetXS, // set! &sxbuiltins.IfS, // if &sxbuiltins.BeginS, // begin &sxbuiltins.DefMacroS, // defmacro &sxbuiltins.LetS, &sxbuiltins.LetStarS, // let, let* &sxbuiltins.AndS, &sxbuiltins.OrS, // and, or } builtins = []*sxeval.Builtin{ &sxbuiltins.Equal, // = &sxbuiltins.NumGreater, // > &sxbuiltins.NullP, // null? &sxbuiltins.PairP, // pair? &sxbuiltins.Car, &sxbuiltins.Cdr, // car, cdr &sxbuiltins.Caar, &sxbuiltins.Cadr, &sxbuiltins.Cdar, &sxbuiltins.Cddr, &sxbuiltins.Caaar, &sxbuiltins.Caadr, &sxbuiltins.Cadar, &sxbuiltins.Caddr, &sxbuiltins.Cdaar, &sxbuiltins.Cdadr, &sxbuiltins.Cddar, &sxbuiltins.Cdddr, &sxbuiltins.List, // list &sxbuiltins.Append, // append &sxbuiltins.Assoc, // assoc &sxbuiltins.Map, // map &sxbuiltins.Apply, // apply &sxbuiltins.Concat, // concat &sxbuiltins.SymbolBoundP, // symbol-bound? &sxbuiltins.DefinedP, // defined? &sxbuiltins.CurrentFrame, // current-frame &sxbuiltins.ResolveSymbol, // resolve-symbol } ) func (wui *WebUI) url2html(text sx.String) sx.Object { if u, errURL := url.Parse(text.GetValue()); errURL == nil { if us := u.String(); us != "" { return sx.MakeList( shtml.SymA, sx.MakeList( sxhtml.SymAttr, sx.Cons(shtml.SymAttrHref, sx.MakeString(us)), sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank")), sx.Cons(shtml.SymAttrRel, sx.MakeString("external noreferrer")), ), text) } } return text } func (wui *WebUI) getParentBinding(ctx context.Context) (*sxeval.Binding, error) { wui.mxZettelBinding.Lock() defer wui.mxZettelBinding.Unlock() if parentBind := wui.zettelBinding; parentBind != nil { return parentBind, nil } dag, zettelBind, err := wui.loadAllSxnCodeZettel(ctx) if err != nil { wui.logger.Error("loading zettel sxn", "err", err) return nil, err } wui.dag = dag wui.zettelBinding = zettelBind return zettelBind, nil } // createRenderEnvironment creates a new environment and populates it with all // relevant data for the base template. func (wui *WebUI) createRenderEnvironment(ctx context.Context, name, lang, title string, user *meta.Meta) (*sxeval.Environment, renderBinder) { userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user) parentBind, err := wui.getParentBinding(ctx) bind := parentBind.MakeChildBinding(name, 128) rb := makeRenderBinder(bind, err) rb.bindString("lang", sx.MakeString(lang)) rb.bindString("css-base-url", sx.MakeString(wui.cssBaseURL)) rb.bindString("css-user-url", sx.MakeString(wui.cssUserURL)) rb.bindString("title", sx.MakeString(title)) rb.bindString("home-url", sx.MakeString(wui.homeURL)) rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth)) |
︙ | ︙ | |||
188 189 190 191 192 193 194 | rb.bindString("search-url", sx.MakeString(wui.searchURL)) rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery)) rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed)) rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer rb.bindString("debug-mode", sx.MakeBoolean(wui.debug)) rb.bindSymbol(symMetaHeader, sx.Nil()) rb.bindSymbol(symDetail, sx.Nil()) | > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | rb.bindString("search-url", sx.MakeString(wui.searchURL)) rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery)) rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed)) rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer rb.bindString("debug-mode", sx.MakeBoolean(wui.debug)) rb.bindSymbol(symMetaHeader, sx.Nil()) rb.bindSymbol(symDetail, sx.Nil()) nestH := sxeval.MakeNestingLimitHandler(wui.sxMaxNesting, sxeval.DefaultHandler{}) var handler sxeval.ComputeHandler = nestH if logger := wui.logger; logger.Handler().Enabled(context.Background(), logging.LevelTrace) { stepsH := sxeval.MakeStepsHandler(nestH) handler = &computeLogHandler{logger: logger, nest: nestH, next: stepsH} } env := sxeval.MakeEnvironment(bind).SetComputeHandler(handler) return env, rb } func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) { if user == nil { return false, "", "" } return true, wui.NewURLBuilder('h').SetZid(user.Zid).String(), string(user.GetDefault(meta.KeyUserID, "")) } type computeLogHandler struct { logger *slog.Logger nest *sxeval.NestingLimitHandler next *sxeval.StepsHandler } func (clh *computeLogHandler) Compute(env *sxeval.Environment, expr sxeval.Expr, frame *sxeval.Frame) (sx.Object, error) { fname := "nil" if frame != nil { fname = frame.Name() } curNesting, _ := clh.nest.Nesting() var sb strings.Builder _, _ = expr.Print(&sb) logging.LogTrace(clh.logger, "compute", slog.String("frame", fname), slog.Int("steps", clh.next.Steps), slog.Int("level", curNesting), slog.String("expr", sb.String())) obj, err := clh.next.Compute(env, expr, frame) if err == nil { logging.LogTrace(clh.logger, "result ", slog.String("frame", fname), slog.Int("steps", clh.next.Steps), slog.Int("level", curNesting), slog.Any("object", obj)) } return obj, err } func (clh *computeLogHandler) Reset() { clh.next.Reset() } type renderBinder struct { err error binding *sxeval.Binding } func makeRenderBinder(bind *sxeval.Binding, err error) renderBinder { return renderBinder{binding: bind, err: err} |
︙ | ︙ | |||
222 223 224 225 226 227 228 | } func (rb *renderBinder) bindKeyValue(key string, value meta.Value) { rb.bindString("meta-"+key, sx.MakeString(string(value))) if kt := meta.Type(key); kt.IsSet { rb.bindString("set-meta-"+key, makeStringList(value.AsSlice())) } } | | | > > | > > | < | | | | > > > > > | > > > > > | > > > > > > > > | 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 | } func (rb *renderBinder) bindKeyValue(key string, value meta.Value) { rb.bindString("meta-"+key, sx.MakeString(string(value))) if kt := meta.Type(key); kt.IsSet { rb.bindString("set-meta-"+key, makeStringList(value.AsSlice())) } } func (rb *renderBinder) rebindResolved(key, extraKey string) { if rb.err == nil { sym := sx.MakeSymbol(key) for curr := rb.binding; curr != nil; curr = curr.Parent() { if obj, found := curr.Lookup(sym); found { rb.bindString(extraKey, obj) return } } } } func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) { zid := m.Zid strZid := zid.String() newURLBuilder := wui.NewURLBuilder rb.bindString("zid", sx.MakeString(strZid)) rb.bindString("web-url", sx.MakeString(newURLBuilder('h').SetZid(zid).String())) if content != nil && wui.canWrite(ctx, user, m, *content) { rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(zid).String())) } rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(zid).String())) if wui.canCreate(ctx, user) { if content != nil && !content.IsBinary() { wui.bindCreateURL(rb, zid, "copy-url", valueActionCopy) } wui.bindCreateURL(rb, zid, "sequel-url", valueActionSequel) wui.bindCreateURL(rb, zid, "folge-url", valueActionFolge) } if wui.canDelete(ctx, user, m) { rb.bindString("delete-url", sx.MakeString(newURLBuilder('d').SetZid(zid).String())) } if val, found := m.Get(meta.KeyUselessFiles); found { rb.bindString("useless", sx.Cons(sx.MakeString(string(val)), nil)) } wui.bindQueryURL(rb, strZid, "context-url", api.ContextDirective) wui.bindQueryURL(rb, strZid, "context-full-url", api.ContextDirective+" "+api.FullDirective) canFolgeQuery := m.Has(meta.KeyPrecursor) || m.Has(meta.KeyFolge) canSequelQuery := m.Has(meta.KeyPrequel) || m.Has(meta.KeySequel) if canFolgeQuery || canSequelQuery { wui.bindQueryURL(rb, strZid, "thread-query-url", api.ThreadDirective) if canFolgeQuery { wui.bindQueryURL(rb, strZid, "folge-query-url", api.FolgeDirective) } if canSequelQuery { wui.bindQueryURL(rb, strZid, "sequel-query-url", api.SequelDirective) } } if wui.canRefresh(user) { rb.bindString("reindex-url", sx.MakeString(newURLBuilder('h').AppendQuery( strZid+" "+api.IdentDirective+api.ActionSeparator+api.ReIndexAction).String())) } // Ensure to have title, role, tags, and syntax included as "meta-*" rb.bindKeyValue(meta.KeyTitle, m.GetDefault(meta.KeyTitle, "")) rb.bindKeyValue(meta.KeyRole, m.GetDefault(meta.KeyRole, "")) rb.bindKeyValue(meta.KeyTags, m.GetDefault(meta.KeyTags, "")) rb.bindKeyValue(meta.KeySyntax, m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)) var metaPairs sx.ListBuilder for key, val := range m.Computed() { metaPairs.Add(sx.Cons(sx.MakeString(key), sx.MakeString(string(val)))) rb.bindKeyValue(key, val) } rb.bindString("metapairs", metaPairs.List()) } func (wui *WebUI) bindCreateURL(rb *renderBinder, zid id.Zid, symName, actionName string) { rb.bindString(symName, sx.MakeString(wui.NewURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, actionName).String())) } func (wui *WebUI) bindQueryURL(rb *renderBinder, strZid, symName, directive string) { rb.bindString(symName, sx.MakeString(wui.NewURLBuilder('h').AppendQuery(strZid+" "+directive+" "+api.DirectedDirective).String())) } func (wui *WebUI) buildListsMenuSxn(ctx context.Context, lang string) *sx.Pair { var zn *ast.ZettelNode if menuZid, err := id.Parse(wui.getConfig(ctx, nil, config.KeyListsMenuZettel)); err == nil { if zn, err = wui.evalZettel.Run(ctx, menuZid, ""); err != nil { zn = nil } |
︙ | ︙ | |||
351 352 353 354 355 356 357 | return content } } } return nil } | | | < | | | < | | | | | | > | > | < < | < | | | | | | | | | | 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 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 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 | return content } } } return nil } func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, env *sxeval.Environment) (sxeval.Expr, error) { if t := wui.getSxnCache(zid); t != nil { return t, nil } reader, err := wui.makeZettelReader(ctx, zid) if err != nil { return nil, err } objs, err := reader.ReadAll() if err != nil { wui.logger.Error("reading sxn template", "err", err, "zid", zid) return nil, err } if len(objs) != 1 { return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs)) } t, err := env.Parse(objs[0], nil) if err != nil { return nil, err } wui.setSxnCache(zid, t) return t, nil } func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) { ztl, err := wui.box.GetZettel(ctx, zid) if err != nil { return nil, err } reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes())) return reader, nil } func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, env *sxeval.Environment) (sx.Object, error) { templateExpr, err := wui.getSxnTemplate(ctx, zid, env) if err != nil { return nil, err } return env.Run(templateExpr, nil) } func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, env *sxeval.Environment) error { return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, env) } func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, env *sxeval.Environment) error { detailObj, err := wui.evalSxnTemplate(ctx, templateID, env) if err != nil { return err } if err = env.BindGlobal(symDetail, detailObj); err != nil { return err } pageObj, err := wui.evalSxnTemplate(ctx, id.ZidBaseTemplate, env) if err != nil { return err } wui.logger.Debug("render", "page", pageObj) gen := sxhtml.NewGenerator().SetNewline() var sb bytes.Buffer _, err = gen.WriteHTML(&sb, pageObj) if err != nil { return err } wui.prepareAndWriteHeader(w, code) if _, err = w.Write(sb.Bytes()); err != nil { wui.logger.Error("Unable to write HTML via template", "err", err) } return nil // No error reporting, since we do not know what happended during write to client. } func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { ctx = context.WithoutCancel(ctx) // Ignore any cancel / timeouts to write an error message. code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { wui.logger.Error(err.Error()) } else { wui.logger.Debug("reportError", "err", err) } user := user.GetCurrentUser(ctx) bind, rb := wui.createRenderEnvironment(ctx, "error", meta.ValueLangEN, "Error", user) rb.bindString("heading", sx.MakeString(http.StatusText(code))) rb.bindString("message", sx.MakeString(text)) if rb.err == nil { rb.err = wui.renderSxnTemplateStatus(ctx, w, code, id.ZidErrorTemplate, bind) } errSx := rb.err if errSx == nil { return } wui.logger.Error("while rendering error message", "err", errSx) // if errBind != nil, the HTTP header was not written wui.prepareAndWriteHeader(w, http.StatusInternalServerError) _, _ = fmt.Fprintf( w, `<!DOCTYPE html> <html> <head><title>Internal server error</title></head> <body> <h1>Internal server error</h1> <p>When generating error code %d with message:</p><pre>%v</pre><p>an error occurred:</p><pre>%v</pre> </body> </html>`, code, text, errSx) } func makeStringList(sl []string) *sx.Pair { var lb sx.ListBuilder for _, s := range sl { lb.Add(sx.MakeString(s)) } return lb.List() } |
Changes to internal/web/adapter/webui/webui.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "net/http" "sync" "time" "t73f.de/r/sx" "t73f.de/r/sx/sxeval" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zero/graph" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" | > > < | | 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 | //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "context" "log/slog" "math" "net/http" "sync" "time" "t73f.de/r/sx" "t73f.de/r/sx/sxeval" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zero/graph" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/box" "zettelstore.de/z/internal/config" "zettelstore.de/z/internal/kernel" "zettelstore.de/z/internal/usecase" "zettelstore.de/z/internal/web/adapter" "zettelstore.de/z/internal/web/server" "zettelstore.de/z/internal/zettel" ) // WebUI holds all data for delivering the web ui. type WebUI struct { logger *slog.Logger debug bool ab server.AuthBuilder authz auth.AuthzManager rtConfig config.Config token auth.TokenManager box webuiBox policy auth.Policy |
︙ | ︙ | |||
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 | refreshURL string withAuth bool loginURL string logoutURL string searchURL string createNewURL string rootBinding *sxeval.Binding mxZettelBinding sync.Mutex zettelBinding *sxeval.Binding dag graph.Digraph[id.Zid] genHTML *sxhtml.Generator } // webuiBox contains all box methods that are needed for WebUI operation. // // Note: these function must not do auth checking. type webuiBox interface { CanCreateZettel(context.Context) bool GetZettel(context.Context, id.Zid) (zettel.Zettel, error) GetMeta(context.Context, id.Zid) (*meta.Meta, error) CanUpdateZettel(context.Context, zettel.Zettel) bool CanDeleteZettel(context.Context, id.Zid) bool } // New creates a new WebUI struct. | > | | | 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 | refreshURL string withAuth bool loginURL string logoutURL string searchURL string createNewURL string sxMaxNesting int rootBinding *sxeval.Binding mxZettelBinding sync.Mutex zettelBinding *sxeval.Binding dag graph.Digraph[id.Zid] genHTML *sxhtml.Generator } // webuiBox contains all box methods that are needed for WebUI operation. // // Note: these function must not do auth checking. type webuiBox interface { CanCreateZettel(context.Context) bool GetZettel(context.Context, id.Zid) (zettel.Zettel, error) GetMeta(context.Context, id.Zid) (*meta.Meta, error) CanUpdateZettel(context.Context, zettel.Zettel) bool CanDeleteZettel(context.Context, id.Zid) bool } // New creates a new WebUI struct. func New(logger *slog.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI { loginoutBase := ab.NewURLBuilder('i') wui := &WebUI{ logger: logger, debug: kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool), ab: ab, rtConfig: rtConfig, authz: authz, token: token, box: mgr, policy: pol, |
︙ | ︙ | |||
110 111 112 113 114 115 116 117 118 119 | refreshURL: ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(), withAuth: authz.WithAuth(), loginURL: loginoutBase.String(), logoutURL: loginoutBase.AppendKVQuery("logout", "").String(), searchURL: ab.NewURLBuilder('h').String(), createNewURL: ab.NewURLBuilder('c').String(), zettelBinding: nil, genHTML: sxhtml.NewGenerator().SetNewline(), } | > | | 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | refreshURL: ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(), withAuth: authz.WithAuth(), loginURL: loginoutBase.String(), logoutURL: loginoutBase.AppendKVQuery("logout", "").String(), searchURL: ab.NewURLBuilder('h').String(), createNewURL: ab.NewURLBuilder('c').String(), sxMaxNesting: min(max(kernel.Main.GetConfig(kernel.WebService, kernel.WebSxMaxNesting).(int), 0), math.MaxInt), zettelBinding: nil, genHTML: sxhtml.NewGenerator().SetNewline(), } wui.rootBinding = wui.createRootBinding() wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui } func (wui *WebUI) getConfig(ctx context.Context, m *meta.Meta, key string) string { return wui.rtConfig.Get(ctx, m, key) |
︙ | ︙ |
Changes to internal/web/server/http.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package server import ( "context" "net" "net/http" "time" "t73f.de/r/zsc/api" | > > > > | > | | | > > > > > > > > > > > > > | | 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 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package server import ( "context" "log/slog" "net" "net/http" "time" "t73f.de/r/webs/middleware" "t73f.de/r/webs/middleware/logging" "t73f.de/r/webs/middleware/reqid" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/user" ) type webServer struct { log *slog.Logger baseURL string httpServer httpServer router httpRouter persistentCookie bool secureCookie bool } // ConfigData contains the data needed to configure a server. type ConfigData struct { Log *slog.Logger ListenAddr string BaseURL string URLPrefix string MaxRequestSize int64 Auth auth.TokenManager LoopbackIdent string LoopbackZid id.Zid PersistentCookie bool SecureCookie bool Profiling bool } // New creates a new web server. func New(sd ConfigData) Server { srv := webServer{ log: sd.Log, baseURL: sd.BaseURL, persistentCookie: sd.PersistentCookie, secureCookie: sd.SecureCookie, } rd := routerData{ log: sd.Log, urlPrefix: sd.URLPrefix, maxRequestSize: sd.MaxRequestSize, auth: sd.Auth, loopbackIdent: sd.LoopbackIdent, loopbackZid: sd.LoopbackZid, profiling: sd.Profiling, } srv.router.initializeRouter(rd) mwReqID := reqid.Config{WithContext: true} mwLogReq := logging.ReqConfig{ Logger: sd.Log, Level: slog.LevelDebug, Message: "ServeHTTP", WithRequestID: true, WithRemote: true} mwLogResp := logging.RespConfig{Logger: sd.Log, Level: slog.LevelDebug, Message: "/ServeHTTP", WithRequestID: true} mw := middleware.NewChain(mwReqID.Build(), mwLogReq.Build(), mwLogResp.Build()) srv.httpServer.initializeHTTPServer(sd.ListenAddr, middleware.Apply(mw, &srv.router)) return &srv } func (srv *webServer) Handle(pattern string, handler http.Handler) { srv.router.Handle(pattern, handler) } func (srv *webServer) AddListRoute(key byte, method Method, handler http.Handler) { |
︙ | ︙ | |||
101 102 103 104 105 106 107 | Secure: srv.secureCookie, HttpOnly: true, SameSite: http.SameSiteLaxMode, } if srv.persistentCookie && d > 0 { cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC() } | | | | < < < < < < < < < < < < < < < < | 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 | Secure: srv.secureCookie, HttpOnly: true, SameSite: http.SameSiteLaxMode, } if srv.persistentCookie && d > 0 { cookie.Expires = time.Now().Add(d).Add(30 * time.Second).UTC() } srv.log.Debug("SetToken", "token", token) if v := cookie.String(); v != "" { w.Header().Add("Set-Cookie", v) w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`) w.Header().Add("Vary", "Cookie") } } // ClearToken invalidates the session cookie by sending an empty one. func (srv *webServer) ClearToken(ctx context.Context, w http.ResponseWriter) context.Context { if authData := user.GetAuthData(ctx); authData == nil { // No authentication data stored in session, nothing to do. return ctx } if w != nil { srv.SetToken(w, nil, 0) } return user.UpdateContext(ctx, nil, nil) } func (srv *webServer) Run() error { return srv.httpServer.start() } func (srv *webServer) Stop() { srv.httpServer.stop() } // Server timeout values const shutdownTimeout = 5 * time.Second |
︙ | ︙ | |||
166 167 168 169 170 171 172 | // start the web server, but does not wait for its completion. func (srv *httpServer) start() error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err } | | | | 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | // start the web server, but does not wait for its completion. func (srv *httpServer) start() error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err } go func() { _ = srv.Serve(ln) }() return nil } // stop the web server. func (srv *httpServer) stop() { ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() _ = srv.Shutdown(ctx) } |
Changes to internal/web/server/router.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package server 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 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package server import ( "log/slog" "net/http" "net/http/pprof" "regexp" rtprf "runtime/pprof" "strings" "t73f.de/r/webs/ip" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/internal/auth" "zettelstore.de/z/internal/auth/user" ) type ( methodHandler [methodLAST]http.Handler routingTable [256]*methodHandler ) var mapMethod = map[string]Method{ http.MethodHead: MethodHead, http.MethodGet: MethodGet, http.MethodPost: MethodPost, http.MethodPut: MethodPut, http.MethodDelete: MethodDelete, } // httpRouter handles all routing for zettelstore. type httpRouter struct { log *slog.Logger urlPrefix string auth auth.TokenManager loopbackIdent string loopbackZid id.Zid minKey byte maxKey byte reURL *regexp.Regexp listTable routingTable zettelTable routingTable ur UserRetriever mux *http.ServeMux maxReqSize int64 } type routerData struct { log *slog.Logger urlPrefix string maxRequestSize int64 auth auth.TokenManager loopbackIdent string loopbackZid id.Zid profiling bool } // initializeRouter creates a new, empty router with the given root handler. func (rt *httpRouter) initializeRouter(rd routerData) { rt.log = rd.log rt.urlPrefix = rd.urlPrefix rt.auth = rd.auth rt.loopbackIdent = rd.loopbackIdent rt.loopbackZid = rd.loopbackZid rt.minKey = 255 rt.maxKey = 0 rt.reURL = regexp.MustCompile("^$") rt.mux = http.NewServeMux() rt.maxReqSize = rd.maxRequestSize if rd.profiling { |
︙ | ︙ | |||
127 128 129 130 131 132 133 | // Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics. func (rt *httpRouter) Handle(pattern string, handler http.Handler) { rt.mux.Handle(pattern, handler) } func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { | < < < < < < < < < < < < < < < < < < < < < < > > > > > > > > > > > > > | | | < | > > | > > | | 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 | // Handle registers the handler for the given pattern. If a handler already exists for pattern, Handle panics. func (rt *httpRouter) Handle(pattern string, handler http.Handler) { rt.mux.Handle(pattern, handler) } func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { if prefixLen := len(rt.urlPrefix); prefixLen > 1 { if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } r.URL.Path = r.URL.Path[prefixLen-1:] } r.Body = http.MaxBytesReader(w, r.Body, rt.maxReqSize) match := rt.reURL.FindStringSubmatch(r.URL.Path) if len(match) != 3 { rt.mux.ServeHTTP(w, rt.addUserContext(r)) return } key := match[1][0] var mh *methodHandler if match[2] == "" { mh = rt.listTable[key] } else { mh = rt.zettelTable[key] } method, ok := mapMethod[r.Method] if ok && mh != nil { if handler := mh[method]; handler != nil { r.URL.Path = "/" + match[2] handler.ServeHTTP(w, rt.addUserContext(r)) return } } http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { // No auth needed return r } ctx := r.Context() if rt.loopbackZid.IsValid() { if remoteAddr := ip.GetRemoteAddr(r); ip.IsLoopbackAddr(remoteAddr) { if u, err := rt.ur.GetUser(ctx, rt.loopbackZid, rt.loopbackIdent); err == nil { if u != nil { return r.WithContext(user.UpdateContext(ctx, u, nil)) } rt.log.Error("No match to loopback-zid", "loopback-ident", rt.loopbackIdent) } } } k := auth.KindAPI t := getHeaderToken(r) if len(t) == 0 { rt.log.Debug("no jwt token found") // IP already logged: ServeHTTP k = auth.KindwebUI t = getSessionToken(r) } if len(t) == 0 { rt.log.Debug("no auth token found in request") // IP already logged: ServeHTTP return r } tokenData, err := rt.auth.CheckToken(t, k) if err != nil { rt.log.Info("invalid auth token", "err", err, "remote", ip.GetRemoteAddr(r)) return r } u, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident) if err != nil { rt.log.Info("auth user not found", "zid", tokenData.Zid, "ident", tokenData.Ident, "err", err, "remote", ip.GetRemoteAddr(r)) return r } return r.WithContext(user.UpdateContext(ctx, u, &tokenData)) } func getSessionToken(r *http.Request) []byte { cookie, err := r.Cookie(sessionName) if err != nil { return nil } |
︙ | ︙ | |||
238 239 240 241 242 243 244 | 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):]) } | < < < < < < < < < < < < < < < | 241 242 243 244 245 246 247 | 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 internal/web/server/server.go.
︙ | ︙ | |||
62 63 64 65 66 67 68 | // SetToken sends the token to the client. SetToken(w http.ResponseWriter, token []byte, d time.Duration) // ClearToken invalidates the session cookie by sending an empty one. ClearToken(ctx context.Context, w http.ResponseWriter) context.Context } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | // SetToken sends the token to the client. SetToken(w http.ResponseWriter, token []byte, d time.Duration) // ClearToken invalidates the session cookie by sending an empty one. ClearToken(ctx context.Context, w http.ResponseWriter) context.Context } // AuthBuilder is a Builder that also allows to execute authentication functions. type AuthBuilder interface { Auth Builder } // Server is the main web server for accessing Zettelstore via HTTP. |
︙ | ︙ |
Deleted strfun/slugify.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted strfun/slugify_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted strfun/strfun.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted strfun/strfun_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to tests/client/client_test.go.
︙ | ︙ | |||
46 47 48 49 50 51 52 | } } } func TestListZettel(t *testing.T) { const ( | | | | | | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | } } } func TestListZettel(t *testing.T) { const ( ownerZettel = 58 configRoleZettel = 36 writerZettel = ownerZettel - 24 readerZettel = ownerZettel - 24 creatorZettel = 11 publicZettel = 6 ) testdata := []struct { user string exp int |
︙ | ︙ | |||
438 439 440 441 442 443 444 | search := "emoji" + api.ActionSeparator + api.RedirectAction ub := c.NewURLBuilder('z').AppendQuery(search) respRedirect, err := http.Get(ub.String()) if err != nil { t.Error(err) return } | | | | 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 | search := "emoji" + api.ActionSeparator + api.RedirectAction ub := c.NewURLBuilder('z').AppendQuery(search) respRedirect, err := http.Get(ub.String()) if err != nil { t.Error(err) return } defer func() { _ = respRedirect.Body.Close() }() bodyRedirect, err := io.ReadAll(respRedirect.Body) if err != nil { t.Error(err) return } ub.ClearQuery().SetZid(id.ZidEmoji) respEmoji, err := http.Get(ub.String()) if err != nil { t.Error(err) return } defer func() { _ = respEmoji.Body.Close() }() bodyEmoji, err := io.ReadAll(respEmoji.Body) if err != nil { t.Error(err) return } if !slices.Equal(bodyRedirect, bodyEmoji) { t.Error("Wrong redirect") |
︙ | ︙ |
Changes to tests/markdown_test.go.
︙ | ︙ | |||
78 79 80 81 82 83 84 | } func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { var sb strings.Builder testID := tc.Example*100 + 1 for _, enc := range encodings { t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(*testing.T) { | | | | | | | 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 | } func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { var sb strings.Builder testID := tc.Example*100 + 1 for _, enc := range encodings { t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(*testing.T) { _, _ = encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}).WriteBlocks(&sb, ast) sb.Reset() }) } } func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { zmkEncoder := encoder.Create(api.EncoderZmk, nil) var buf bytes.Buffer testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { buf.Reset() _, _ = zmkEncoder.WriteBlocks(&buf, ast) // gotFirst := buf.String() testID = tc.Example*100 + 2 secondAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML) buf.Reset() _, _ = zmkEncoder.WriteBlocks(&buf, &secondAst) gotSecond := buf.String() // if gotFirst != gotSecond { // st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond) // } testID = tc.Example*100 + 3 thirdAst := parser.Parse(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML) buf.Reset() _, _ = zmkEncoder.WriteBlocks(&buf, &thirdAst) gotThird := buf.String() if gotSecond != gotThird { st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird) } }) } func TestAdditionalMarkdown(t *testing.T) { testcases := []struct { md string exp string }{ {`abc<br>def`, "abc``<br>``{=\"html\"}def"}, } zmkEncoder := encoder.Create(api.EncoderZmk, nil) var sb strings.Builder for i, tc := range testcases { ast := createMDBlockSlice(tc.md, config.MarkdownHTML) sb.Reset() _, _ = zmkEncoder.WriteBlocks(&sb, &ast) got := sb.String() if got != tc.exp { t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got) } } } |
Changes to tests/naughtystrings_test.go.
︙ | ︙ | |||
35 36 37 38 39 40 41 | func getNaughtyStrings() (result []string, err error) { fpath := filepath.Join("..", "testdata", "naughty", "blns.txt") file, err := os.Open(fpath) if err != nil { return nil, err } | | | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | func getNaughtyStrings() (result []string, err error) { fpath := filepath.Join("..", "testdata", "naughty", "blns.txt") file, err := os.Open(fpath) if err != nil { return nil, err } defer func() { _ = file.Close() }() scanner := bufio.NewScanner(file) for scanner.Scan() { if text := scanner.Text(); text != "" && text[0] != '#' { result = append(result, text) } } return result, scanner.Err() |
︙ | ︙ |
Changes to tests/regression_test.go.
︙ | ︙ | |||
91 92 93 94 95 96 97 | } func resultFile(file string) (data string, err error) { f, err := os.Open(file) if err != nil { return "", err } | < > > > > | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | } func resultFile(file string) (data string, err error) { f, err := os.Open(file) if err != nil { return "", err } src, err := io.ReadAll(f) err2 := f.Close() if err == nil { err = err2 } return string(src), err } func checkFileContent(t *testing.T, filename, gotContent string) { t.Helper() wantContent, err := resultFile(filename) if err != nil { |
︙ | ︙ | |||
123 124 125 126 127 128 129 | } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() if enc := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}); enc != nil { var sf strings.Builder | | | 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() if enc := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}); enc != nil { var sf strings.Builder _, _ = enc.WriteMeta(&sf, zn.Meta) checkFileContent(t, resultName, sf.String()) return } panic(fmt.Sprintf("Unknown writer encoding %q", enc)) } func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) { |
︙ | ︙ |
Changes to tools/build/build.go.
︙ | ︙ | |||
23 24 25 26 27 28 29 30 31 32 | "io/fs" "os" "path/filepath" "slices" "strings" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" | > | | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | "io/fs" "os" "path/filepath" "slices" "strings" "time" zerostrings "t73f.de/r/zero/strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsx/input" "zettelstore.de/z/tools" ) func readVersionFile() (string, error) { content, err := os.ReadFile("VERSION") if err != nil { return "", err |
︙ | ︙ | |||
58 59 60 61 62 63 64 | const dirtySuffix = "-dirty" func readFossilDirty() (string, error) { s, err := tools.ExecuteCommand(nil, "fossil", "status", "--differ") if err != nil { return "", err } | | | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | const dirtySuffix = "-dirty" func readFossilDirty() (string, error) { s, err := tools.ExecuteCommand(nil, "fossil", "status", "--differ") if err != nil { return "", err } for _, line := range zerostrings.SplitLines(s) { for _, prefix := range dirtyPrefixes { if strings.HasPrefix(line, prefix) { return dirtySuffix, nil } } } return "", nil |
︙ | ︙ | |||
166 167 168 169 170 171 172 | return err } zipName := filepath.Join(path, "manual-"+base+".zip") zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } | | | | 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | return err } zipName := filepath.Join(path, "manual-"+base+".zip") zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } defer func() { _ = zipFile.Close() }() zipWriter := zip.NewWriter(zipFile) defer func() { _ = zipWriter.Close() }() for _, entry := range entries { if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil { return err } } return nil |
︙ | ︙ | |||
200 201 202 203 204 205 206 | if err != nil { return err } manualFile, err := os.Open(filepath.Join(path, name)) if err != nil { return err } | | | 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | if err != nil { return err } manualFile, err := os.Open(filepath.Join(path, name)) if err != nil { return err } defer func() { _ = manualFile.Close() }() if name != versionZid+".zettel" { _, err = io.Copy(w, manualFile) return err } data, err := io.ReadAll(manualFile) |
︙ | ︙ | |||
222 223 224 225 226 227 228 | var buf bytes.Buffer if _, err = fmt.Fprintf(&buf, "id: %s\n", versionZid); err != nil { return err } if _, err = m.WriteComputed(&buf); err != nil { return err } | < | | 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | var buf bytes.Buffer if _, err = fmt.Fprintf(&buf, "id: %s\n", versionZid); err != nil { return err } if _, err = m.WriteComputed(&buf); err != nil { return err } if _, err = fmt.Fprintf(&buf, "\n%s", getVersion()); err != nil { return err } _, err = io.Copy(w, &buf) return err } //--- release |
︙ | ︙ | |||
286 287 288 289 290 291 292 | } func createReleaseZip(zsName, zipName, fileName string) error { zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } | | | | < | < | < | | 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 | } func createReleaseZip(zsName, zipName, fileName string) error { zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } defer func() { _ = zipFile.Close() }() zw := zip.NewWriter(zipFile) defer func() { _ = zw.Close() }() if err = addFileToZip(zw, zsName, fileName); err != nil { return err } if err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt"); err != nil { return err } return addFileToZip(zw, "docs/readmezip.txt", "README.txt") } func addFileToZip(zipFile *zip.Writer, filepath, filename string) error { zsFile, err := os.Open(filepath) if err != nil { return err } defer func() { _ = zsFile.Close() }() stat, err := zsFile.Stat() if err != nil { return err } fh, err := zip.FileInfoHeader(stat) if err != nil { return err |
︙ | ︙ |
Changes to tools/testapi/testapi.go.
︙ | ︙ | |||
88 89 90 91 92 93 94 | func stopZettelstore(i *zsInfo) error { conn, err := net.Dial("tcp", i.adminAddress) if err != nil { fmt.Println("Unable to stop Zettelstore") return err } | | | > | > | | 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | func stopZettelstore(i *zsInfo) error { conn, err := net.Dial("tcp", i.adminAddress) if err != nil { fmt.Println("Unable to stop Zettelstore") return err } _, err = io.WriteString(conn, "shutdown\n") _ = conn.Close() if err == nil { err = i.cmd.Wait() } return err } func addressInUse(address string) bool { conn, err := net.Dial("tcp", address) if err != nil { return false } _ = conn.Close() return true } |
Changes to tools/tools.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 | "errors" "fmt" "io" "os" "os/exec" "strings" | | | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | "errors" "fmt" "io" "os" "os/exec" "strings" zerostrings "t73f.de/r/zero/strings" ) // Some constants to make Go work with fossil. var ( EnvDirectProxy = []string{"GOPROXY=direct"} EnvGoVCS = []string{"GOVCS=zettelstore.de:fossil,t73f.de:fossil"} ) |
︙ | ︙ | |||
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 | return err } if err := checkUnparam(forRelease); err != nil { return err } if err := checkRevive(); err != nil { return err } if forRelease { if err := checkGoVulncheck(); err != nil { return err } } return checkFossilExtra() } // CheckGoTest runs all internal unti tests. func CheckGoTest(pkg string, testParams ...string) error { var env []string env = append(env, EnvDirectProxy...) env = append(env, EnvGoVCS...) args := []string{"test", pkg} args = append(args, testParams...) out, err := ExecuteCommand(env, "go", args...) if err != nil { | > > > | | 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 | return err } if err := checkUnparam(forRelease); err != nil { return err } if err := checkRevive(); err != nil { return err } if err := checkErrCheck(); err != nil { return err } if forRelease { if err := checkGoVulncheck(); err != nil { return err } } return checkFossilExtra() } // CheckGoTest runs all internal unti tests. func CheckGoTest(pkg string, testParams ...string) error { var env []string env = append(env, EnvDirectProxy...) env = append(env, EnvGoVCS...) args := []string{"test", pkg} args = append(args, testParams...) out, err := ExecuteCommand(env, "go", args...) if err != nil { for _, line := range zerostrings.SplitLines(out) { if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") { continue } fmt.Fprintln(os.Stderr, line) } } return err |
︙ | ︙ | |||
164 165 166 167 168 169 170 171 172 173 174 175 176 177 | func checkRevive() error { out, err := ExecuteCommand(EnvGoVCS, "revive", "./...") if err != nil || out != "" { fmt.Fprintln(os.Stderr, "Some revive problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkUnparam(forRelease bool) error { path, err := findExecStrict("unparam", forRelease) if path == "" { | > > > > > > > > > > > | 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 | func checkRevive() error { out, err := ExecuteCommand(EnvGoVCS, "revive", "./...") if err != nil || out != "" { fmt.Fprintln(os.Stderr, "Some revive problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkErrCheck() error { out, err := ExecuteCommand(EnvGoVCS, "errcheck", "./...") if err != nil || out != "" { fmt.Fprintln(os.Stderr, "Some errcheck problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkUnparam(forRelease bool) error { path, err := findExecStrict("unparam", forRelease) if path == "" { |
︙ | ︙ | |||
224 225 226 227 228 229 230 | 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):") | | | 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | 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 zerostrings.SplitLines(out) { if i > 0 { fmt.Fprint(os.Stderr, ",") } fmt.Fprintf(os.Stderr, " %q", extra) } fmt.Fprintln(os.Stderr) } return nil } |
Changes to www/changes.wiki.
1 2 3 | <title>Change Log</title> <a id="0_22"></a> | > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | 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 | <title>Change Log</title> <a id="0_23"></a> <h2>Changes for Version 0.23.0 (pending)</h2> <a id="0_22"></a> <h2>Changes for Version 0.22.0 (2025-07-07)</h2> * Sx builtin <code>(bind-lookup ...)</code> is replaced with <code>(resolve-symbol ...)</code>. If you maintain your own Sx code to customize Zettelstore behaviour, you must update your code; otherwise it will break. If your code use <code>(ROLE-DEFAULT-action ...)</code> , infinite recursion might occur. This is now handled with the new startup configuration <code>sx-max-nesting</code>. In such cases, you should omit the call to <code>ROLE-DEFAULT-action</code>. For debugging purposes, use the <code>web:trace</code> logging level, which logs all Sx computations. (breaking: webui) * Sx templates are changed: base.sxn, info.sxn, zettel.sxn. If you are using a (self-) modified version, you should update your modifications. (breaking: webui) * Remove zettel for the Sx prelude. Fortunately, it was never documented, so it was likely unused. The prelude is now a constant string whithin Sx's code base. (breaking: webui) * Remove support for metadata keys <code>predecessor</code> and <code>successors</code>. A Zettelstore is not a version control system. Use <code>precursor</code> instead (if appropriate), use your own metadata key (which should end with <code>-zids</code>, or get rid of manually versioned zettel. In the WebUI, action “version” is removed. (breaking) * New query directives <code>FOLGE</code>, <code>SEQUEL</code>, and <code>THREAD</code> support folge zettel and sequel zettel (and both). See the manual for details. To make working with these directives a little bit more easier, appropriate query links are placed on main zettel web user interface and on info zettel page. (major) * If authentication is enabled, Zettelstore can now be accessed from the loopback device without logging in or obtaining an access token. (major: api, webui) * Context directive allows <code>DIRECTED</code>. (minor) * Metadata with keys ending with <code>-ref</code> or <code>-refs</code> are interpreted as zettel identifier. (minor) * The new startup configuration key <code>sx-max-nesting</code> allows setting a limit on the nesting depth of Sx computations. This is primarily useful to prevent unbounded recursion due to programming errors. Previously, such issues would crash the Zettelstore. (minor: webui) * At logging level <code>trace</code>, the web user interface now logs all Sx computations, mainly those used for rendering HTML templates. (minor: webui) * Move context link to zettel page. (minor: webui) * Make writing to zettel files more robust. When an error occurs, the state before writing the file is restored. (info) * Some smaller bug fixes and improvements, to the software and to the documentation. <a id="0_21"></a> <h2>Changes for Version 0.21.0 (2025-04-17)</h2> * Change zettel identifier for Zettelstore Log, Zettelstore Memory, and Zettelstore Sx Engine. See manual for updated identifier values. (breaking) * Sz encodings of links were simplified into one <code>LINK</code> symbol. Different link types are now specified by reference node. (breaking: api) * Sz encodings of lists, descriptions, tables, table cells, and block BLOBs have now an additional attributes entry, directly after the initial symbol. |
︙ | ︙ |
Changes to www/download.wiki.
︙ | ︙ | |||
14 15 16 17 18 19 20 | It’s useful for me and many others. It might be useful for you too. Check out the [https://zettelstore.de/manual/|manual] to get started and learn how to make the most of it. <h2>ZIP-ped Executables</h2> | | | | | | | | | | 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’s useful for me and many others. It might be useful for you too. Check out the [https://zettelstore.de/manual/|manual] to get started and learn how to make the most of it. <h2>ZIP-ped Executables</h2> Build: <code>v0.22.0</code> (2025-07-07). * [/uv/zettelstore-0.22.0-android-arm64.zip|Android] (arm64) * [/uv/zettelstore-0.22.0-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.22.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.22.0-darwin-arm64.zip|macOS] (arm64) * [/uv/zettelstore-0.22.0-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.22.0-windows-amd64.zip|Windows] (amd64) 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.22.0.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file box to read the zettel directly from the ZIP file. |
Changes to www/index.wiki.
1 2 3 4 5 6 7 | <title>Home</title> <b>Zettelstore</b> is a software that collects and relates your notes (“zettel”) to represent and enhance your knowledge. It helps with many tasks of personal knowledge management by explicitly supporting the [https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The method is based on creating many individual notes, each with one idea or information, | | | | | | | | | | | 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 | <title>Home</title> <b>Zettelstore</b> is a software that collects and relates your notes (“zettel”) to represent and enhance your knowledge. It helps with many tasks of personal knowledge management by explicitly supporting the [https://en.wikipedia.org/wiki/Zettelkasten|Zettelkasten method]. The method is based on creating many individual notes, each with one idea or information, that are related to each other. Since knowledge is typically built up gradually, one major focus is a long-term storage of these notes, hence the name “Zettelstore”. To get an initial impression, take a look at the [https://zettelstore.de/manual/|manual]. 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://t73f.de/r/zsc|Zettelstore Client] provides client software to access Zettelstore via its API more easily. * [https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. * [https://t73f.de/r/sx|Sx] provides an evaluator for symbolic expressions, which is used for HTML templates and more. [https://mastodon.social/tags/Zettelstore|Stay tuned] … <hr> <h3>Latest Release: 0.22.0 (2025-07-07)</h3> * [./download.wiki|Download] * [./changes.wiki#0_22|Change summary] * [/timeline?p=v0.22.0&bt=v0.21.0&y=ci|Check-ins for version 0.22], [/vdiff?to=v0.22.0&from=v0.21.0|content diff] * [/timeline?df=v0.22.0&y=ci|Check-ins derived from the 0.22 release], [/vdiff?from=v0.22.0&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://go.dev/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 | <title>Limitations and planned improvements</title> Here is a list of some shortcomings of Zettelstore. They are planned to be solved. | | | | | | | | | | > | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <title>Limitations and planned improvements</title> Here is a list of some shortcomings of Zettelstore. They are planned to be solved. * All zettel must be indexed to enable full query functionality; otherwise, some zettel may not be returned. * Quoted attribute values are not yet supported in Zettelmarkup (e.g. <code>{key="value with space"}</code>). * The horizontal tab character (<tt>U+0009</tt>) is not fully supported by the parser. * Citation keys are not yet supported. * Changes to the content syntax are not reflected in the file extension. * File names containing additional text beyond the zettel identifier are not always preserved. * Case sensitivity in file names is not consistently handled. Some file systems distinguish between upper and lower case (e.g., Linux, sometimes macOS), while others do not (e.g., default macOS, most Windows systems). Zettelstore cannot detect these differences. Avoid using files that differ only by case in your directory boxes or file boxes. * … |