Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From version-0.0.12 To version-0.0.11
2021-04-17
| ||
12:34 | Increase version to 0.0.13-dev to begin next development cycle ... (check-in: 5f0c8f2d4c user: stern tags: trunk) | |
2021-04-16
| ||
16:16 | Version 0.0.12 ... (check-in: 86f8bc8a70 user: stern tags: trunk, release, version-0.0.12) | |
16:08 | Show default dir place type in startup values zettel ... (check-in: 8269a7cbc4 user: stern tags: trunk) | |
2021-04-05
| ||
15:59 | Increase version to 0.0.12-dev to begin next development cycle ... (check-in: 737632737f user: stern tags: trunk) | |
12:18 | Version 0.0.11 ... (check-in: 6db9ad537f user: stern tags: trunk, release, version-0.0.11) | |
2021-04-03
| ||
17:31 | Include license file and readme into zip-file for released software ... (check-in: ca6e7ae6d7 user: stern tags: trunk) | |
Changes to VERSION.
|
| | | 1 | 0.0.11 |
Deleted cmd/fd_limit.go.
|
| < < < < < < < < < < < < < < < |
Deleted cmd/fd_limit_raise.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to cmd/main.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | package cmd import ( "context" "flag" "fmt" | < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package cmd import ( "context" "flag" "fmt" "os" "strings" "zettelstore.de/z/config/runtime" "zettelstore.de/z/config/startup" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" |
︙ | ︙ | |||
124 125 126 127 128 129 130 | return cfg } func setupOperations(cfg *meta.Meta, withPlaces, simple bool) error { var mgr place.Manager var idx index.Indexer if withPlaces { | < < < < < < < | | < | > > > > | 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 | return cfg } func setupOperations(cfg *meta.Meta, withPlaces, simple bool) error { var mgr place.Manager var idx index.Indexer if withPlaces { idx = indexer.New() filter := index.NewMetaFilter(idx) p, err := manager.New(getPlaces(cfg), cfg.GetBool(startup.KeyReadOnlyMode), filter) if err != nil { return err } mgr = p } err := startup.SetupStartup(cfg, mgr, idx, simple) if err != nil { fmt.Fprintln(os.Stderr, "Unable to connect to specified places") return err } if withPlaces { if err := mgr.Start(context.Background()); err != nil { fmt.Fprintln(os.Stderr, "Unable to start zettel place") return err } runtime.SetupConfiguration(mgr) progplace.Setup(cfg, mgr, idx) |
︙ | ︙ |
Changes to config/startup/startup.go.
︙ | ︙ | |||
20 21 22 23 24 25 26 | "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" ) var config struct { | | | | | | < | | | | | | | < < < | | < | | | | | | | | | | < < < < < < | | > < < < < < < < < < < < < < < < < < < > | 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 | "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/index" "zettelstore.de/z/place" ) var config struct { simple bool // was started without run command verbose bool readonlyMode bool urlPrefix string listenAddress string owner id.Zid withAuth bool secret []byte insecCookie bool persistCookie bool htmlLifetime time.Duration apiLifetime time.Duration manager place.Manager indexer index.Indexer } // Predefined keys for startup zettel const ( KeyInsecureCookie = "insecure-cookie" KeyListenAddress = "listen-addr" KeyOwner = "owner" KeyPersistentCookie = "persistent-cookie" KeyPlaceOneURI = "place-1-uri" KeyReadOnlyMode = "read-only-mode" KeyTokenLifetimeHTML = "token-lifetime-html" KeyTokenLifetimeAPI = "token-lifetime-api" KeyURLPrefix = "url-prefix" KeyVerbose = "verbose" ) // SetupStartup initializes the startup data. func SetupStartup(cfg *meta.Meta, manager place.Manager, idx index.Indexer, simple bool) error { if config.urlPrefix != "" { panic("startup.config already set") } config.simple = simple config.verbose = cfg.GetBool(KeyVerbose) config.readonlyMode = cfg.GetBool(KeyReadOnlyMode) config.urlPrefix = cfg.GetDefault(KeyURLPrefix, "/") if prefix, ok := cfg.Get(KeyURLPrefix); ok && len(prefix) > 0 && prefix[0] == '/' && prefix[len(prefix)-1] == '/' { config.urlPrefix = prefix } else { config.urlPrefix = "/" } if val, ok := cfg.Get(KeyListenAddress); ok { config.listenAddress = val // TODO: check for valid string } else { config.listenAddress = "127.0.0.1:23123" } config.owner = id.Invalid if owner, ok := cfg.Get(KeyOwner); ok { if zid, err := id.Parse(owner); err == nil { config.owner = zid config.withAuth = true } } if config.withAuth { config.insecCookie = cfg.GetBool(KeyInsecureCookie) config.persistCookie = cfg.GetBool(KeyPersistentCookie) config.secret = calcSecret(cfg) config.htmlLifetime = getDuration( cfg, KeyTokenLifetimeHTML, 1*time.Hour, 1*time.Minute, 30*24*time.Hour) config.apiLifetime = getDuration( cfg, KeyTokenLifetimeAPI, 10*time.Minute, 0, 1*time.Hour) } config.simple = simple && !config.withAuth config.manager = manager config.indexer = idx return nil } func calcSecret(cfg *meta.Meta) []byte { h := fnv.New128() if secret, ok := cfg.Get("secret"); ok { io.WriteString(h, secret) } |
︙ | ︙ | |||
167 168 169 170 171 172 173 | // the service. func URLPrefix() string { return config.urlPrefix } // ListenAddress returns the string that specifies the the network card and the ip port // where the server listens for requests func ListenAddress() string { return config.listenAddress } | < < < | 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | // the service. func URLPrefix() string { return config.urlPrefix } // ListenAddress returns the string that specifies the the network card and the ip port // where the server listens for requests func ListenAddress() string { return config.listenAddress } // WithAuth returns true if user authentication is enabled. func WithAuth() bool { return config.withAuth } // SecureCookie returns whether the web app should set cookies to secure mode. func SecureCookie() bool { return config.withAuth && !config.insecCookie } // PersistentCookie returns whether the web app should set persistent cookies |
︙ | ︙ |
Changes to docs/manual/00001004010000.zettel.
︙ | ︙ | |||
11 12 13 14 15 16 17 | Therefore only the owner of the computer on which Zettelstore runs can change this information. The file for start-up configuration must be created via a text editor in advance. The syntax of the configuration file is the same as for any zettel metadata. The following keys are supported: | < < < < < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | Therefore only the owner of the computer on which Zettelstore runs can change this information. The file for start-up configuration must be created via a text editor in advance. The syntax of the configuration file is the same as for any zettel metadata. The following keys are supported: ; [!insecure-cookie]''insecure-cookie'' : Must be set to ''true'', if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP). Otherwise web browser are free to ignore the authentication cookie. Default: ''false'' ; [!listen-addr]''listen-addr'' : Configures the network address, where is zettel web service is listening for requests. |
︙ | ︙ |
Changes to docs/manual/00001004011400.zettel.
1 2 | id: 00001004011400 title: Configure file directory places | < > < | < < < < < < < < < < < < < < < < < | | < < > | > < < < < < | 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 | id: 00001004011400 title: Configure file directory places tags: #configuration #manual #zettelstore syntax: zmk role: manual Under certain circumstances, it is preferable to further configure a file directory place. This is done by appending query parameters after the base place URI ''dir:\//DIR''. The following parameters are supported: |= Parameter:|Description|Default value:| |rescan|Time (in seconds) after which the directory should be scanned fully|600 |worker|Number of worker that can access the directory in parallel|17 |readonly|Allow only operations that do not change a zettel or create a new zettel|n/a === Rescan On most platforms, Zettelstore automatically detects changes to zettel files that originates from other software[^This includes most Linux distributions, macOS, and Windows]. It is done on a ""best-effort"" basis. Under certain circumstances it is possible that Zettelstore does not detect a change done by another software. To cope with this unlikely, but still possible event, Zettelstore re-scans periodically the file diectory. The time interval is configured by the ''rescan'' parameter, e.g. ``` place-1-uri: dir:///home/zettel?rescan=300 ``` This makes Zettelstore to re-scan the directory ''/home/zettel/'' every 300 seconds, i.e. 5 minutes. For a Zettelstore with many zettel, re-scanning the directory may take a while, especially if it is stored on a remote computer (e.g. via CIFS/SMB or NFS). In this case, you should adjust the parameter value. Please note that a directory re-scan invalidates all internal data of a Zettelstore. It might trigger a re-build of the backlink database (and other internal databases). Therefore a large value if preferred. === Worker Internally, Zettelstore parallels concurrent requests for a zettel or its metadata. The number of parallel activities is configured by the ''worker'' parameter. Its default value 17 is a good compromise when you think about the high variability of possible Zettelstore environments. A computer contains a limited number of internal processing units (CPU). Its number ranges from 1 to (currently) 128, e.g. in bigger server environments. Zettelstore typically runs on a system with 1 to 8 CPUs. Access to zettel file is ultimately managed by the underlying operating system. Depending on the hardware, only a limited number of parallel accesses are desireable. Since Zettelstore is a single-user software, the value 17 is quite reasonable, even for higher use. On smaller hardware[^In comparison to a normal desktop or laptop computer], such as the [[Raspberry Zero|https://www.raspberrypi.org/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. === Readonly Sometimes you may want to provide zettel from a file directory place, but you want to disallow any changes. If you provide the query parameter ''readonly'' (with or without a corresponding value), the place will disallow any changes. ``` place-1-uri: dir:///home/zettel?readonly ``` If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory places will be in read-only mode too, even if not explicitly configured. |
Changes to go.mod.
1 2 3 4 5 6 7 | module zettelstore.de/z go 1.16 require ( github.com/fsnotify/fsnotify v1.4.9 github.com/pascaldekloe/jwt v1.10.0 | | | 1 2 3 4 5 6 7 8 9 10 11 12 | module zettelstore.de/z go 1.16 require ( github.com/fsnotify/fsnotify v1.4.9 github.com/pascaldekloe/jwt v1.10.0 github.com/yuin/goldmark v1.3.3 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 golang.org/x/text v0.3.6 ) |
Changes to go.sum.
1 2 3 4 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs= github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A= | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/pascaldekloe/jwt v1.10.0 h1:ktcIUV4TPvh404R5dIBEnPCsSwj0sqi3/0+XafE5gJs= github.com/pascaldekloe/jwt v1.10.0/go.mod h1:TKhllgThT7TOP5rGr2zMLKEDZRAgJfBbtKyVeRsNB9A= github.com/yuin/goldmark v1.3.3 h1:37BdQwPx8VOSic8eDSWee6QL9mRpZRm9VJp/QugNrW0= github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= |
︙ | ︙ |
Changes to place/dirplace/directory/directory.go.
1 2 3 4 5 6 7 8 9 10 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- | | > > > | > | | > | > > > > > > | > > | > > > | > | > | > | > > > | > > > > | < > > | > > > | | > > > > > | > > > | > | < < < | | > > > > | > > > > > | | | > | > > > > | < | > > > > > | > > > > > > | > > > > > < > | > > | | 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package directory manages the directory part of a dirstore. package directory import ( "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/place/change" ) // Service specifies a directory scan service. type Service struct { dirPath string rescanTime time.Duration done chan struct{} cmds chan dirCmd infos chan<- change.Info } // NewService creates a new directory service. func NewService(directoryPath string, rescanTime time.Duration, chci chan<- change.Info) *Service { srv := &Service{ dirPath: directoryPath, rescanTime: rescanTime, cmds: make(chan dirCmd), infos: chci, } return srv } // Start makes the directory service operational. func (srv *Service) Start() { tick := make(chan struct{}) rawEvents := make(chan *fileEvent) events := make(chan *fileEvent) ready := make(chan int) go srv.directoryService(events, ready) go collectEvents(events, rawEvents) go watchDirectory(srv.dirPath, rawEvents, tick) if srv.done != nil { panic("src.done already set") } srv.done = make(chan struct{}) go ping(tick, srv.rescanTime, srv.done) <-ready } // Stop stops the directory service. func (srv *Service) Stop() { close(srv.done) srv.done = nil } func (srv *Service) notifyChange(reason change.Reason, zid id.Zid) { if chci := srv.infos; chci != nil { chci <- change.Info{Reason: reason, Zid: zid} } } // NumEntries returns the number of managed zettel. func (srv *Service) NumEntries() int { resChan := make(chan resNumEntries) srv.cmds <- &cmdNumEntries{resChan} return <-resChan } // GetEntries returns an unsorted list of all current directory entries. func (srv *Service) GetEntries() []Entry { resChan := make(chan resGetEntries) srv.cmds <- &cmdGetEntries{resChan} return <-resChan } // GetEntry returns the entry with the specified zettel id. If there is no such // zettel id, an empty entry is returned. func (srv *Service) GetEntry(zid id.Zid) Entry { resChan := make(chan resGetEntry) srv.cmds <- &cmdGetEntry{zid, resChan} return <-resChan } // GetNew returns an entry with a new zettel id. func (srv *Service) GetNew() Entry { resChan := make(chan resNewEntry) srv.cmds <- &cmdNewEntry{resChan} return <-resChan } // UpdateEntry notifies the directory of an updated entry. func (srv *Service) UpdateEntry(entry *Entry) { resChan := make(chan struct{}) srv.cmds <- &cmdUpdateEntry{entry, resChan} <-resChan } // RenameEntry notifies the directory of an renamed entry. func (srv *Service) RenameEntry(curEntry, newEntry *Entry) error { resChan := make(chan resRenameEntry) srv.cmds <- &cmdRenameEntry{curEntry, newEntry, resChan} return <-resChan } // DeleteEntry removes a zettel id from the directory of entries. func (srv *Service) DeleteEntry(zid id.Zid) { resChan := make(chan struct{}) srv.cmds <- &cmdDeleteEntry{zid, resChan} <-resChan } |
Added place/dirplace/directory/entry.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package directory manages the directory part of a dirstore. package directory import "zettelstore.de/z/domain/id" // MetaSpec defines all possibilities where meta data can be stored. type MetaSpec int // Constants for MetaSpec const ( MetaSpecUnknown MetaSpec = iota MetaSpecNone // no meta information MetaSpecFile // meta information is in meta file MetaSpecHeader // meta information is in header ) // Entry stores everything for a directory entry. type Entry struct { Zid id.Zid MetaSpec MetaSpec // location of meta information MetaPath string // file path of meta information ContentPath string // file path of zettel content ContentExt string // (normalized) file extension of zettel content Duplicates bool // multiple content files } // IsValid checks whether the entry is valid. func (e *Entry) IsValid() bool { return e.Zid.IsValid() } |
Added place/dirplace/directory/service.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package directory manages the directory part of a directory place. package directory import ( "log" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/place" "zettelstore.de/z/place/change" ) // ping sends every tick a signal to reload the directory list func ping(tick chan<- struct{}, rescanTime time.Duration, done <-chan struct{}) { ticker := time.NewTicker(rescanTime) defer close(tick) for { select { case _, ok := <-ticker.C: if !ok { return } tick <- struct{}{} case _, ok := <-done: if !ok { ticker.Stop() return } } } } func newEntry(ev *fileEvent) *Entry { de := new(Entry) de.Zid = ev.zid updateEntry(de, ev) return de } func updateEntry(de *Entry, ev *fileEvent) { if ev.ext == "meta" { de.MetaSpec = MetaSpecFile de.MetaPath = ev.path return } if de.ContentExt != "" && de.ContentExt != ev.ext { de.Duplicates = true return } if de.MetaSpec != MetaSpecFile { if ev.ext == "zettel" { de.MetaSpec = MetaSpecHeader } else { de.MetaSpec = MetaSpecNone } } de.ContentPath = ev.path de.ContentExt = ev.ext } type dirMap map[id.Zid]*Entry func dirMapUpdate(dm dirMap, ev *fileEvent) { de := dm[ev.zid] if de == nil { dm[ev.zid] = newEntry(ev) return } updateEntry(de, ev) } func deleteFromMap(dm dirMap, ev *fileEvent) { if ev.ext == "meta" { if entry, ok := dm[ev.zid]; ok { if entry.MetaSpec == MetaSpecFile { entry.MetaSpec = MetaSpecNone return } } } delete(dm, ev.zid) } // directoryService is the main service. func (srv *Service) directoryService(events <-chan *fileEvent, ready chan<- int) { curMap := make(dirMap) var newMap dirMap for { select { case ev, ok := <-events: if !ok { return } switch ev.status { case fileStatusReloadStart: newMap = make(dirMap) case fileStatusReloadEnd: curMap = newMap newMap = nil if ready != nil { ready <- len(curMap) close(ready) ready = nil } srv.notifyChange(change.OnReload, id.Invalid) case fileStatusError: log.Println("DIRPLACE", "ERROR", ev.err) case fileStatusUpdate: srv.processFileUpdateEvent(ev, curMap, newMap) case fileStatusDelete: srv.processFileDeleteEvent(ev, curMap, newMap) } case cmd, ok := <-srv.cmds: if ok { cmd.run(curMap) } } } } func (srv *Service) processFileUpdateEvent(ev *fileEvent, curMap, newMap dirMap) { if newMap != nil { dirMapUpdate(newMap, ev) } else { dirMapUpdate(curMap, ev) srv.notifyChange(change.OnUpdate, ev.zid) } } func (srv *Service) processFileDeleteEvent(ev *fileEvent, curMap, newMap dirMap) { if newMap != nil { deleteFromMap(newMap, ev) } else { deleteFromMap(curMap, ev) srv.notifyChange(change.OnDelete, ev.zid) } } type dirCmd interface { run(m dirMap) } type cmdNumEntries struct { result chan<- resNumEntries } type resNumEntries = int func (cmd *cmdNumEntries) run(m dirMap) { cmd.result <- len(m) } type cmdGetEntries struct { result chan<- resGetEntries } type resGetEntries []Entry func (cmd *cmdGetEntries) run(m dirMap) { res := make([]Entry, 0, len(m)) for _, de := range m { res = append(res, *de) } cmd.result <- res } type cmdGetEntry struct { zid id.Zid result chan<- resGetEntry } type resGetEntry = Entry func (cmd *cmdGetEntry) run(m dirMap) { entry := m[cmd.zid] if entry == nil { cmd.result <- Entry{Zid: id.Invalid} } else { cmd.result <- *entry } } type cmdNewEntry struct { result chan<- resNewEntry } type resNewEntry = Entry func (cmd *cmdNewEntry) run(m dirMap) { zid := id.New(false) if _, ok := m[zid]; !ok { entry := &Entry{Zid: zid, MetaSpec: MetaSpecUnknown} m[zid] = entry cmd.result <- *entry return } for { zid = id.New(true) if _, ok := m[zid]; !ok { entry := &Entry{Zid: zid, MetaSpec: MetaSpecUnknown} m[zid] = entry cmd.result <- *entry return } // TODO: do not wait here, but in a non-blocking goroutine. time.Sleep(100 * time.Millisecond) } } type cmdUpdateEntry struct { entry *Entry result chan<- struct{} } func (cmd *cmdUpdateEntry) run(m dirMap) { entry := *cmd.entry m[entry.Zid] = &entry cmd.result <- struct{}{} } type cmdRenameEntry struct { curEntry *Entry newEntry *Entry result chan<- resRenameEntry } type resRenameEntry = error func (cmd *cmdRenameEntry) run(m dirMap) { newEntry := *cmd.newEntry newZid := newEntry.Zid if _, found := m[newZid]; found { cmd.result <- &place.ErrInvalidID{Zid: newZid} return } delete(m, cmd.curEntry.Zid) m[newZid] = &newEntry cmd.result <- nil } type cmdDeleteEntry struct { zid id.Zid result chan<- struct{} } func (cmd *cmdDeleteEntry) run(m dirMap) { delete(m, cmd.zid) cmd.result <- struct{}{} } |
Added place/dirplace/directory/watch.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package directory manages the directory part of a directory place. package directory import ( "os" "path/filepath" "regexp" "time" "github.com/fsnotify/fsnotify" "zettelstore.de/z/domain/id" ) var validFileName = regexp.MustCompile(`^(\d{14}).*(\.(.+))$`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) } type fileStatus int const ( fileStatusNone fileStatus = iota fileStatusReloadStart fileStatusReloadEnd fileStatusError fileStatusUpdate fileStatusDelete ) type fileEvent struct { status fileStatus path string // Full file path zid id.Zid ext string // File extension err error // Error if Status == fileStatusError } type sendResult int const ( sendDone sendResult = iota sendReload sendExit ) func watchDirectory(directory string, events chan<- *fileEvent, tick <-chan struct{}) { defer close(events) var watcher *fsnotify.Watcher defer func() { if watcher != nil { watcher.Close() } }() sendEvent := func(ev *fileEvent) sendResult { select { case events <- ev: case _, ok := <-tick: if ok { return sendReload } return sendExit } return sendDone } sendError := func(err error) sendResult { return sendEvent(&fileEvent{status: fileStatusError, err: err}) } sendFileEvent := func(status fileStatus, path string, match []string) sendResult { zid, err := id.Parse(match[1]) if err != nil { return sendDone } event := &fileEvent{ status: status, path: path, zid: zid, ext: match[3], } return sendEvent(event) } reloadStartEvent := &fileEvent{status: fileStatusReloadStart} reloadEndEvent := &fileEvent{status: fileStatusReloadEnd} reloadFiles := func() bool { entries, err := os.ReadDir(directory) if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } return true } if res := sendEvent(reloadStartEvent); res != sendDone { return res == sendReload } if watcher != nil { watcher.Close() } watcher, err = fsnotify.NewWatcher() if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } } for _, entry := range entries { if entry.IsDir() { continue } if info, err1 := entry.Info(); err1 != nil || !info.Mode().IsRegular() { continue } name := entry.Name() match := matchValidFileName(name) if len(match) > 0 { path := filepath.Join(directory, name) if res := sendFileEvent(fileStatusUpdate, path, match); res != sendDone { return res == sendReload } } } if watcher != nil { err = watcher.Add(directory) if err != nil { if res := sendError(err); res != sendDone { return res == sendReload } } } if res := sendEvent(reloadEndEvent); res != sendDone { return res == sendReload } return true } handleEvents := func() bool { const createOps = fsnotify.Create | fsnotify.Write const deleteOps = fsnotify.Remove | fsnotify.Rename for { select { case wevent, ok := <-watcher.Events: if !ok { return false } path := filepath.Clean(wevent.Name) match := matchValidFileName(filepath.Base(path)) if len(match) == 0 { continue } if wevent.Op&createOps != 0 { if fi, err := os.Lstat(path); err != nil || !fi.Mode().IsRegular() { continue } if res := sendFileEvent( fileStatusUpdate, path, match); res != sendDone { return res == sendReload } } if wevent.Op&deleteOps != 0 { if res := sendFileEvent( fileStatusDelete, path, match); res != sendDone { return res == sendReload } } case err, ok := <-watcher.Errors: if !ok { return false } if res := sendError(err); res != sendDone { return res == sendReload } case _, ok := <-tick: return ok } } } for { if !reloadFiles() { return } if watcher == nil { if _, ok := <-tick; !ok { return } } else { if !handleEvents() { return } } } } func sendCollectedEvents(out chan<- *fileEvent, events []*fileEvent) { for _, ev := range events { if ev.status != fileStatusNone { out <- ev } } } func addEvent(events []*fileEvent, ev *fileEvent) []*fileEvent { switch ev.status { case fileStatusNone: return events case fileStatusReloadStart: events = events[0:0] case fileStatusUpdate, fileStatusDelete: if len(events) > 0 && mergeEvents(events, ev) { return events } } return append(events, ev) } func mergeEvents(events []*fileEvent, ev *fileEvent) bool { for i := len(events) - 1; i >= 0; i-- { oev := events[i] switch oev.status { case fileStatusReloadStart, fileStatusReloadEnd: return false case fileStatusUpdate, fileStatusDelete: if ev.path == oev.path { if ev.status == oev.status { return true } oev.status = fileStatusNone return false } } } return false } func collectEvents(out chan<- *fileEvent, in <-chan *fileEvent) { defer close(out) var sendTime time.Time sendTimeSet := false ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() events := make([]*fileEvent, 0, 32) buffer := false for { select { case ev, ok := <-in: if !ok { sendCollectedEvents(out, events) return } if ev.status == fileStatusReloadStart { buffer = false events = events[0:0] } if buffer { if !sendTimeSet { sendTime = time.Now().Add(1500 * time.Millisecond) sendTimeSet = true } events = addEvent(events, ev) if len(events) > 1024 { sendCollectedEvents(out, events) events = events[0:0] sendTimeSet = false } continue } out <- ev if ev.status == fileStatusReloadEnd { buffer = true } case now := <-ticker.C: if sendTimeSet && now.After(sendTime) { sendCollectedEvents(out, events) events = events[0:0] sendTimeSet = false } } } } |
Added place/dirplace/directory/watch_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 | //----------------------------------------------------------------------------- // Copyright (c) 2020 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package directory manages the directory part of a directory place. package directory import ( "testing" ) func sameStringSlices(sl1, sl2 []string) bool { if len(sl1) != len(sl2) { return false } for i := 0; i < len(sl1); i++ { if sl1[i] != sl2[i] { return false } } return true } func TestMatchValidFileName(t *testing.T) { testcases := []struct { name string exp []string }{ {"", []string{}}, {".txt", []string{}}, {"12345678901234.txt", []string{"12345678901234", ".txt", "txt"}}, {"12345678901234abc.txt", []string{"12345678901234", ".txt", "txt"}}, {"12345678901234.abc.txt", []string{"12345678901234", ".txt", "txt"}}, } for i, tc := range testcases { got := matchValidFileName(tc.name) if len(got) == 0 { if len(tc.exp) > 0 { t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) } } else { if got[0] != tc.name { t.Errorf("TC=%d, name=%q, got=%v", i, tc.name, got) } if !sameStringSlices(got[1:], tc.exp) { t.Errorf("TC=%d, name=%q, exp=%v, got=%v", i, tc.name, tc.exp, got) } } } } |
Changes to place/dirplace/dirplace.go.
︙ | ︙ | |||
22 23 24 25 26 27 28 | "time" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" | < < | | | | > | < | < < < < < < < < < | 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 | "time" "zettelstore.de/z/config/runtime" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/dirplace/directory" "zettelstore.de/z/place/fileplace" "zettelstore.de/z/place/manager" "zettelstore.de/z/search" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (place.ManagedPlace, error) { path := getDirPath(u) if _, err := os.Stat(path); os.IsNotExist(err) { return nil, err } dp := dirPlace{ u: u, readonly: getQueryBool(u, "readonly"), cdata: *cdata, dir: path, dirRescan: time.Duration( getQueryInt(u, "rescan", 60, 3600, 30*24*60*60)) * time.Second, fSrvs: uint32(getQueryInt(u, "worker", 1, 17, 1499)), } return &dp, nil }) } func getDirPath(u *url.URL) string { if u.Opaque != "" { return filepath.Clean(u.Opaque) } return filepath.Clean(u.Path) } |
︙ | ︙ | |||
90 91 92 93 94 95 96 | return max } return iVal } // dirPlace uses a directory to store zettel as files. type dirPlace struct { | | | | | | < | < | | | | | < < < | < < < < < < < < < | < < < < < < < < > > > > > > > > > > < < < < > | | | < | | | | | | | < < < | < < < | | 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | return max } return iVal } // dirPlace uses a directory to store zettel as files. type dirPlace struct { u *url.URL readonly bool cdata manager.ConnectData dir string dirRescan time.Duration dirSrv *directory.Service fSrvs uint32 fCmds []chan fileCmd mxCmds sync.RWMutex } func (dp *dirPlace) Location() string { return dp.u.String() } func (dp *dirPlace) Start(ctx context.Context) error { dp.mxCmds.Lock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) for i := uint32(0); i < dp.fSrvs; i++ { cc := make(chan fileCmd) go fileService(i, cc) dp.fCmds = append(dp.fCmds, cc) } dp.dirSrv = directory.NewService(dp.dir, dp.dirRescan, dp.cdata.Notify) dp.mxCmds.Unlock() dp.dirSrv.Start() return nil } func (dp *dirPlace) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function var sum uint32 = 2166136261 ^ uint32(zid) sum *= 16777619 sum ^= uint32(zid >> 32) sum *= 16777619 dp.mxCmds.RLock() defer dp.mxCmds.RUnlock() return dp.fCmds[sum%dp.fSrvs] } func (dp *dirPlace) Stop(ctx context.Context) error { dirSrv := dp.dirSrv dp.dirSrv = nil dirSrv.Stop() for _, c := range dp.fCmds { close(c) } return nil } func (dp *dirPlace) CanCreateZettel(ctx context.Context) bool { return !dp.readonly } func (dp *dirPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { if dp.readonly { return id.Invalid, place.ErrReadOnly } meta := zettel.Meta entry := dp.dirSrv.GetNew() meta.Zid = entry.Zid dp.updateEntryFromMeta(&entry, meta) err := setZettel(dp, &entry, zettel) if err == nil { dp.dirSrv.UpdateEntry(&entry) } return meta.Zid, err } func (dp *dirPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { entry := dp.dirSrv.GetEntry(zid) if !entry.IsValid() { return domain.Zettel{}, place.ErrNotFound } m, c, err := getMetaContent(dp, &entry, zid) if err != nil { return domain.Zettel{}, err } dp.cleanupMeta(ctx, m) zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)} return zettel, nil } func (dp *dirPlace) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { entry := dp.dirSrv.GetEntry(zid) if !entry.IsValid() { return nil, place.ErrNotFound } m, err := getMeta(dp, &entry, zid) if err != nil { return nil, err } dp.cleanupMeta(ctx, m) return m, nil } func (dp *dirPlace) FetchZids(ctx context.Context) (id.Set, error) { entries := dp.dirSrv.GetEntries() result := id.NewSetCap(len(entries)) for _, entry := range entries { result[entry.Zid] = true } return result, nil } func (dp *dirPlace) SelectMeta(ctx context.Context, match search.MetaMatchFunc) (res []*meta.Meta, err error) { entries := dp.dirSrv.GetEntries() res = make([]*meta.Meta, 0, len(entries)) // The following loop could be parallelized if needed for performance. for _, entry := range entries { m, err1 := getMeta(dp, &entry, entry.Zid) err = err1 if err != nil { continue } dp.cleanupMeta(ctx, m) dp.cdata.Filter.Enrich(ctx, m) |
︙ | ︙ | |||
256 257 258 259 260 261 262 | return place.ErrReadOnly } meta := zettel.Meta if !meta.Zid.IsValid() { return &place.ErrInvalidID{Zid: meta.Zid} } | | < < < | | | | < < < < | | 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 | return place.ErrReadOnly } meta := zettel.Meta if !meta.Zid.IsValid() { return &place.ErrInvalidID{Zid: meta.Zid} } entry := dp.dirSrv.GetEntry(meta.Zid) if !entry.IsValid() { // Existing zettel, but new in this place. entry.Zid = meta.Zid dp.updateEntryFromMeta(&entry, meta) } else if entry.MetaSpec == directory.MetaSpecNone { defaultMeta := fileplace.CalcDefaultMeta(entry.Zid, entry.ContentExt) if !meta.Equal(defaultMeta, true) { dp.updateEntryFromMeta(&entry, meta) dp.dirSrv.UpdateEntry(&entry) } } return setZettel(dp, &entry, zettel) } func (dp *dirPlace) updateEntryFromMeta(entry *directory.Entry, meta *meta.Meta) { entry.MetaSpec, entry.ContentExt = calcSpecExt(meta) basePath := filepath.Join(dp.dir, entry.Zid.String()) if entry.MetaSpec == directory.MetaSpecFile { entry.MetaPath = basePath + ".meta" |
︙ | ︙ | |||
316 317 318 319 320 321 322 | func (dp *dirPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if dp.readonly { return place.ErrReadOnly } if curZid == newZid { return nil } | | | | | | | | | < < < < < | | | | | < < < | | 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 | func (dp *dirPlace) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if dp.readonly { return place.ErrReadOnly } if curZid == newZid { return nil } curEntry := dp.dirSrv.GetEntry(curZid) if !curEntry.IsValid() { return place.ErrNotFound } // Check whether zettel with new ID already exists in this place if _, err := dp.GetMeta(ctx, newZid); err == nil { return &place.ErrInvalidID{Zid: newZid} } oldMeta, oldContent, err := getMetaContent(dp, &curEntry, curZid) if err != nil { return err } newEntry := directory.Entry{ Zid: newZid, MetaSpec: curEntry.MetaSpec, MetaPath: renamePath(curEntry.MetaPath, curZid, newZid), ContentPath: renamePath(curEntry.ContentPath, curZid, newZid), ContentExt: curEntry.ContentExt, } if err := dp.dirSrv.RenameEntry(&curEntry, &newEntry); err != nil { return err } oldMeta.Zid = newZid newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)} if err := setZettel(dp, &newEntry, newZettel); err != nil { // "Rollback" rename. No error checking... dp.dirSrv.RenameEntry(&newEntry, &curEntry) return err } return deleteZettel(dp, &curEntry, curZid) } func (dp *dirPlace) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { if dp.readonly { return false } entry := dp.dirSrv.GetEntry(zid) return entry.IsValid() } func (dp *dirPlace) DeleteZettel(ctx context.Context, zid id.Zid) error { if dp.readonly { return place.ErrReadOnly } entry := dp.dirSrv.GetEntry(zid) if !entry.IsValid() { return nil } dp.dirSrv.DeleteEntry(zid) err := deleteZettel(dp, &entry, zid) return err } func (dp *dirPlace) ReadStats(st *place.Stats) { st.ReadOnly = dp.readonly st.Zettel = dp.dirSrv.NumEntries() } func (dp *dirPlace) cleanupMeta(ctx context.Context, m *meta.Meta) { if role, ok := m.Get(meta.KeyRole); !ok || role == "" { m.Set(meta.KeyRole, runtime.GetDefaultRole()) } if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { |
︙ | ︙ |
Deleted place/dirplace/makedir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/notifydir/notifydir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/notifydir/service.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/notifydir/watch.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/dirplace/notifydir/watch_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to place/dirplace/service.go.
︙ | ︙ | |||
143 144 145 146 147 148 149 | err = cmd.runMetaSpecFile() case directory.MetaSpecHeader: err = cmd.runMetaSpecHeader() case directory.MetaSpecNone: // TODO: if meta has some additional infos: write meta to new .meta; // update entry in dir err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString()) | | | 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | err = cmd.runMetaSpecFile() case directory.MetaSpecHeader: err = cmd.runMetaSpecHeader() case directory.MetaSpecNone: // TODO: if meta has some additional infos: write meta to new .meta; // update entry in dir err = writeFileContent(cmd.entry.ContentPath, cmd.zettel.Content.AsString()) case directory.MetaSpecUnknown: panic("TODO: ???") } cmd.rc <- err } func (cmd *fileSetZettel) runMetaSpecFile() error { f, err := openFileWrite(cmd.entry.MetaPath) |
︙ | ︙ | |||
215 216 217 218 219 220 221 | if err == nil { err = err1 } case directory.MetaSpecHeader: err = os.Remove(cmd.entry.ContentPath) case directory.MetaSpecNone: err = os.Remove(cmd.entry.ContentPath) | | | 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | if err == nil { err = err1 } case directory.MetaSpecHeader: err = os.Remove(cmd.entry.ContentPath) case directory.MetaSpecNone: err = os.Remove(cmd.entry.ContentPath) case directory.MetaSpecUnknown: panic("TODO: ???") } cmd.rc <- err } // Utility functions ---------------------------------------- |
︙ | ︙ |
Deleted place/dirplace/simpledir/simpledir.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted place/helper.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to place/memplace/memplace.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Package memplace stores zettel volatile in main memory. package memplace import ( "context" "net/url" "sync" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/change" "zettelstore.de/z/place/manager" | > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package memplace stores zettel volatile in main memory. package memplace import ( "context" "net/url" "sync" "time" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/place" "zettelstore.de/z/place/change" "zettelstore.de/z/place/manager" |
︙ | ︙ | |||
64 65 66 67 68 69 70 | return nil } func (mp *memPlace) CanCreateZettel(ctx context.Context) bool { return true } func (mp *memPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { mp.mx.Lock() | < < < < < < < < | | | > > > > > > | > > > > > > > > | 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 | return nil } func (mp *memPlace) CanCreateZettel(ctx context.Context) bool { return true } func (mp *memPlace) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { mp.mx.Lock() meta := zettel.Meta.Clone() meta.Zid = mp.calcNewZid() zettel.Meta = meta mp.zettel[meta.Zid] = zettel mp.mx.Unlock() mp.notifyChanged(change.OnUpdate, meta.Zid) return meta.Zid, nil } func (mp *memPlace) calcNewZid() id.Zid { zid := id.New(false) if _, ok := mp.zettel[zid]; !ok { return zid } for { zid = id.New(true) if _, ok := mp.zettel[zid]; !ok { return zid } time.Sleep(100 * time.Millisecond) } } func (mp *memPlace) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { mp.mx.RLock() zettel, ok := mp.zettel[zid] mp.mx.RUnlock() if !ok { |
︙ | ︙ |
Changes to place/place.go.
︙ | ︙ | |||
167 168 169 170 171 172 173 | // ErrReadOnly is returned if there is an attepmt to write to a read-only place. var ErrReadOnly = errors.New("read-only place") // ErrNotFound is returned if a zettel was not found in the place. var ErrNotFound = errors.New("zettel not found") | < < < < | 167 168 169 170 171 172 173 174 175 176 177 | // ErrReadOnly is returned if there is an attepmt to write to a read-only place. var ErrReadOnly = errors.New("read-only place") // ErrNotFound is returned if a zettel was not found in the place. var ErrNotFound = errors.New("zettel not found") // ErrInvalidID is returned if the zettel id is not appropriate for the place operation. type ErrInvalidID struct{ Zid id.Zid } func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() } |
Changes to place/progplace/config.go.
︙ | ︙ | |||
77 78 79 80 81 82 83 | fmt.Fprintf(&sb, "|Listen address| %v\n", startup.ListenAddress()) fmt.Fprintf(&sb, "|Authentication enabled|%v\n", startup.WithAuth()) fmt.Fprintf(&sb, "|Secure cookie|%v\n", startup.SecureCookie()) fmt.Fprintf(&sb, "|Persistent Cookie|%v\n", startup.PersistentCookie()) html, api := startup.TokenLifetime() fmt.Fprintf(&sb, "|API Token lifetime|%v\n", api) fmt.Fprintf(&sb, "|HTML Token lifetime|%v\n", html) | < | 77 78 79 80 81 82 83 84 85 | fmt.Fprintf(&sb, "|Listen address| %v\n", startup.ListenAddress()) fmt.Fprintf(&sb, "|Authentication enabled|%v\n", startup.WithAuth()) fmt.Fprintf(&sb, "|Secure cookie|%v\n", startup.SecureCookie()) fmt.Fprintf(&sb, "|Persistent Cookie|%v\n", startup.PersistentCookie()) html, api := startup.TokenLifetime() fmt.Fprintf(&sb, "|API Token lifetime|%v\n", api) fmt.Fprintf(&sb, "|HTML Token lifetime|%v\n", html) return sb.String() } |
Changes to tests/regression_test.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package tests provides some higher-level tests. package tests import ( "context" "fmt" "io" | < < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // Package tests provides some higher-level tests. package tests import ( "context" "fmt" "io" "os" "path/filepath" "strings" "testing" "zettelstore.de/z/ast" "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/place" "zettelstore.de/z/place/manager" |
︙ | ︙ | |||
49 50 51 52 53 54 55 | panic(err) } cdata := manager.ConnectData{Filter: &noFilter{}, Notify: nil} for _, entry := range entries { if entry.IsDir() { place, err := manager.Connect( | | | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | panic(err) } cdata := manager.ConnectData{Filter: &noFilter{}, Notify: nil} for _, entry := range entries { if entry.IsDir() { place, err := manager.Connect( "dir://"+filepath.Join(root, entry.Name()), false, &cdata, ) if err != nil { panic(err) } places = append(places, place) |
︙ | ︙ | |||
129 130 131 132 133 134 135 | if gotFirst != gotSecond { t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond) } } func getPlaceName(p place.ManagedPlace, root string) string { | | < < < < | 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | if gotFirst != gotSecond { t.Errorf("\n1st: %q\n2nd: %q", gotFirst, gotSecond) } } func getPlaceName(p place.ManagedPlace, root string) string { return p.Location()[len("dir://")+len(root):] } func match(*meta.Meta) bool { return true } func checkContentPlace(t *testing.T, p place.ManagedPlace, wd, placeName string) { ss := p.(place.StartStopper) if err := ss.Start(context.Background()); err != nil { |
︙ | ︙ |
Changes to web/adapter/response.go.
︙ | ︙ | |||
55 56 57 58 59 60 61 | } if err1, ok := err.(*ErrBadRequest); ok { return http.StatusBadRequest, err1.Text } if err == place.ErrStopped { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v.", err) } | < < < | 55 56 57 58 59 60 61 62 63 | } if err1, ok := err.(*ErrBadRequest); ok { return http.StatusBadRequest, err1.Text } if err == place.ErrStopped { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v.", err) } return http.StatusInternalServerError, err.Error() } |
Changes to www/changes.wiki.
1 2 | <title>Change Log</title> | < < < | < < < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 | <title>Change Log</title> <a name="0_0_12"></a> <h2>Changes for Version 0.0.12 (pending)</h2> <a name="0_0_11"></a> <h2>Changes for Version 0.0.11 (2021-04-05)</h2> * New place schema "file" allows to read zettel from a ZIP file. A zettel collection can now be packaged and distributed easier. (major: server) * Non-restricted search is a full-text search. The search string will |
︙ | ︙ | |||
51 52 53 54 55 56 57 | or the whole string. If a search value begins with '#', only zettel with the exact tag will be returned. Otherwise a zettel will be returned if the search string just matches the prefix of only one of its tags. (minor: api, webui) * Many smaller bug fixes and inprovements, to the software and to the documentation. A note for users of macOS: in the current release and with macOS's default values, | | > > | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | or the whole string. If a search value begins with '#', only zettel with the exact tag will be returned. Otherwise a zettel will be returned if the search string just matches the prefix of only one of its tags. (minor: api, webui) * Many smaller bug fixes and inprovements, to the software and to the documentation. A note for users of macOS: in the current release and with macOS's default values, a zettel directory place must not contain more than approx. 250 files. There are four options to mitigate this limitation temporarily: # You [https://zettelstore.de/manual/h/00001004010000|re-configure] your Zettelstore to use more than one directory place. # You update the per-process limit of open files on macOS. # You setup a virtualization environment to run Zettelstore on Linux or Windows. # You wait for version 0.0.12 which addresses this issue. <a name="0_0_10"></a> <h2>Changes for Version 0.0.10 (2021-02-26)</h2> * Menu item “Home” now redirects to a home zettel. |
︙ | ︙ |
Changes to www/download.wiki.
1 2 3 4 5 6 7 8 9 10 11 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> Build: <code>v0.0.11</code> (2021-04-05). * [/uv/zettelstore-0.0.11-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.0.11-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.0.11-windows-amd64.zip|Windows] (amd64) * [/uv/zettelstore-0.0.11-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.0.11-darwin-arm64.zip|macOS] (arm64, aka Apple silicon) Unzip the appropriate file, install and execute Zettelstore according to the manual. <h2>Zettel for the manual</h2> As a starter, you can download the zettel for the manual [/uv/manual-0.0.11.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file place to read the zettel directly from the ZIP file. |
Changes to www/index.wiki.
︙ | ︙ | |||
13 14 15 16 17 18 19 | It is a live example of the zettelstore software, running in read-only mode. The software, including the manual, is licensed under the [/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)]. [https://twitter.com/zettelstore|Stay tuned]… <hr> | | | | | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | It is a live example of the zettelstore software, running in read-only mode. The software, including the manual, is licensed under the [/file?name=LICENSE.txt&ci=trunk|European Union Public License 1.2 (or later)]. [https://twitter.com/zettelstore|Stay tuned]… <hr> <h3>Latest Release: 0.0.11 (2021-04-05)</h3> * [./download.wiki|Download] * [./changes.wiki#0_0_11|Change Summary] * [/timeline?p=version-0.0.11&bt=version-0.0.10&y=ci|Check-ins for version 0.0.11], [/vdiff?to=version-0.0.11&from=version-0.0.10|content diff] * [/timeline?df=version-0.0.11&y=ci|Check-ins derived from the 0.0.11 release], [/vdiff?from=version-0.0.11&to=trunk|content diff] * [./plan.wiki|Limitations and planned Improvements] * [/timeline?t=release|Timeline of all past releases] <hr> <h2>Build instructions</h2> Just install [https://fossil-scm.org|Fossil], [https://golang.org/dl/|Go] and some Go-based tools. Please read the |
︙ | ︙ |