Permalink
Browse files

Initial -secondary implementation, begin 0.1.0

Note: The UI for this is currently terrible, usually requiring a manual
reload of the page to see what's actually happening (state is hard). So
far this has only been tested on Linux using Syncthing between two
computers.

This is a rough implementation that fixes #64

Testing and a UI is still needed.
  • Loading branch information...
jmcfarlane committed Sep 8, 2017
1 parent b403295 commit 2d71d704f5f52792da2943194594d08f9475b990
Showing with 279 additions and 22 deletions.
  1. +13 −0 CHANGELOG.md
  2. +25 −8 app/model.go
  3. +2 −2 bindata_assetfs.go
  4. +79 −8 bolt.go
  5. +8 −2 index.go
  6. +37 −1 main.go
  7. +1 −1 main_test.go
  8. +108 −0 secondary.go
  9. +6 −0 static/js/views/row.js
@@ -1,3 +1,16 @@
## v0.1.0 / ...
* Initial (experimental) support for distributed writes.
* The idea is you can share your notes directory via a tool like
Syncthing, and then run a single primary and as many
`-secondary` instances as you like. All computers have write
access, and all changes get replicated through the primary back
to the secondary nodes. Current replication mechanisms (maybe)
known to work:
1. [Syncthing](https://syncthing.net/)
* Testing has only been performed on Linux
* UI is very much incomplete
## v0.0.10 / 2017-05-15
* Add flag `bolt.timeout` for use with opening BoltDB.
@@ -27,11 +27,28 @@ type Note struct {
Tags string `json:"tags"`
UID string `json:"uid"`
Updated string `json:"updated"`
// Fields largely to support the `-secondary` feature
AheadOfPrimary bool `json:"ahead_of_primary"`
Deleted bool `json:"deleted"`
Time time.Time `json:"time"`
// Private fields
SecondaryPath string `json:"-"`
}
// Notes is a collection of Note objects
type Notes []Note
// Map of notes key'd by uid
func Map(notes Notes) map[string]Note {
var m = make(map[string]Note)
for _, note := range notes {
m[note.UID] = note
}
return m
}
// TimeSorter sorts notes lines by last updated
type TimeSorter Notes
@@ -55,6 +72,11 @@ func (note Note) ToJSON() (string, error) {
// FromBytes converts encoding.Gob bytes into a Note
func (note *Note) FromBytes(b []byte) error {
// Zero byte file represents a logical delete
if len(b) == 0 {
note.Deleted = true
return nil
}
buf := bytes.NewBuffer(b)
dec := gob.NewDecoder(buf)
return dec.Decode(&note)
@@ -74,15 +96,10 @@ func (note Note) ToBytes() ([]byte, error) {
return buf.Bytes(), nil
}
// The current timestamp in time.RFC3339 format
func now() string {
t := time.Now()
return t.Format(time.RFC3339)
}
// Prepare a note for being persisted to storage
// Persistable prepares a note for being persisted to storage
func Persistable(note Note) (Note, error) {
note.Updated = now()
note.Time = time.Now()
note.Updated = note.Time.Format(time.RFC3339)
// Generate a uuid if necessary
if note.UID == "" {
note.UID = uuid.NewV4().String()
@@ -519,7 +519,7 @@ func staticJsViewsPasswordJs() (*asset, error) {
return a, nil
}
var _staticJsViewsRowJs = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xa4\x56\x4d\x8f\xdb\x36\x13\xbe\xfb\x57\x4c\x80\x20\x94\x16\x7a\xe5\xf7\xec\xc0\x68\xd1\x4d\x2e\x45\x9b\x14\xc9\xa2\x39\x04\x81\x41\x8b\x23\x8b\x0d\x4d\x6a\xc9\x91\xb5\x6e\xd6\xff\xbd\x20\x45\x7d\xd9\xde\x5d\x14\x5d\x60\x61\x90\xf3\xc9\x99\x67\x9e\xd1\xf2\xe6\x66\x01\x37\xf0\x73\x29\x15\x9a\x03\xda\x83\xc4\x16\xde\xa1\x2b\xac\xdc\xa2\x03\x0e\xc4\xb7\x0a\xc1\x9a\x16\x82\xa8\x34\x16\x38\x68\x43\xb8\x80\x9b\xe5\x42\x60\x29\x35\x26\x5f\x17\x00\xcc\xcb\xdd\xd2\x8b\x58\xe6\xcf\x84\x0f\xf4\x8a\x70\x5f\x2b\x4e\xe8\x96\xd6\xb4\x79\x45\x7b\xd5\x09\xb7\xbc\xf8\xbe\x35\x3a\xaa\x36\x5a\xa0\x75\x85\xb1\xc8\x16\xdf\xb2\x45\xd9\xe8\x82\xa4\xd1\x89\x77\xf6\xa7\xc4\x36\x0b\x11\xdd\x9d\xcf\xe5\x93\x69\xef\xa2\xd3\x14\x7e\x2c\x00\x2c\x52\x63\x35\xfc\x12\x3d\xe6\xde\x20\xc7\x07\x42\x2d\x12\x2f\x07\xc0\x03\x6a\x72\x2b\xe8\x4e\x00\xac\x50\xb2\xf8\xce\x56\xc0\x8c\xfe\x64\xda\xdb\x70\x0a\xb2\x53\xb6\x08\xbf\x85\xe2\xce\x7d\xe0\x7b\x5c\x01\xf3\xa1\xff\x67\x4d\x1b\x52\x05\x20\xbe\x8b\x02\xb2\x2c\xaa\x4b\x2d\x49\x72\x25\xff\xc6\x15\x0c\xc9\x9b\xda\xff\xb8\x74\x08\x7b\xe0\x16\xf6\x46\x70\x05\x6b\xa0\x4a\xba\x3c\x6a\xe4\x35\x77\xae\x35\x56\xfc\xee\x85\x6f\xa3\x76\xd0\xd8\x48\x2d\xf0\x01\xd6\xf0\xe3\x34\xbf\x27\xbe\x85\x35\xe8\x46\xcd\xf5\x83\x7a\x92\xce\xee\xf6\x46\xa0\xca\x8d\x4e\x58\x51\x71\xbd\xc3\x55\x23\x05\xcb\x3a\x99\x90\xae\x56\xfc\x78\x6b\x34\xa1\xa6\xee\xf2\x05\x6b\x87\x0a\x0b\xc2\xc1\x05\x99\xdd\x4e\xe1\xe7\x78\xfb\x82\x8b\x2e\xce\xaa\x44\x2a\x2a\xfc\x0f\x59\xf4\x96\xe1\xbd\xcf\x1b\x08\x74\x64\xcd\xb1\xb7\x30\xfa\x5d\x77\xf1\x92\x55\x61\x8f\xa1\x3d\x2b\xb4\xd6\x58\x96\x75\xad\xcb\x2d\x7a\xac\xbe\xf7\x77\xf1\x6a\xee\xa2\xef\xa9\x43\x6e\x8b\x2a\x74\x34\x38\xec\xce\x7d\x16\xdd\xe9\xe9\x14\x1c\x92\x37\x19\x2a\x5d\x72\xe5\xf0\x9a\x62\x18\xca\x0e\x4e\x6f\x67\x08\x0e\x95\x99\xa0\x71\x84\xe1\x04\x58\x39\xf1\x9d\x83\x35\x24\x13\x8f\x3b\x1f\xda\xdf\xb3\x14\x1e\x1f\x81\xb1\x34\x77\xb5\x92\x94\x30\x60\xe9\x3c\x46\x57\x8b\x49\x90\xc2\x28\x9f\xb2\x34\xfa\x2c\xdc\x6b\x54\x61\xf8\x93\x4d\xde\x33\x42\x72\x75\xa4\xb3\xc1\x0e\xc2\xcc\xaf\xa6\x8f\x25\xf3\xeb\xe7\x8f\x1f\x92\x34\x6a\x9c\xd2\xa1\x22\x91\x03\x2e\xcb\x10\xf8\xcd\xff\xdd\xc0\x97\x0a\x35\xf0\x40\x65\xd2\x41\xa0\x00\x14\x50\x70\xa5\x80\xc3\x1e\xa9\x32\x02\x8c\xf7\x81\x10\xc2\x01\x19\x08\x40\xf5\x57\xbd\x93\x88\x60\x90\xe4\x50\x95\x90\xd4\xd6\xec\x6b\x92\x7a\x17\x98\xb1\x1f\x64\x90\x25\x68\x2c\xd0\x39\x6e\x8f\x69\xde\x1b\xbf\xeb\x70\xee\xb5\x7d\x14\xff\x3e\x68\xa5\x52\x50\xf1\xba\x46\x0d\x46\x17\x18\x24\x43\x14\xd7\x9b\xba\xa6\xf0\xee\xca\x46\xa9\x23\xc4\xf1\xe9\xfd\x2e\xc3\xef\x48\x65\x57\xbb\x2e\xcb\xd8\x65\x4f\x1d\xe9\xa4\xca\xc3\x65\xee\x2a\xd3\x8e\xdc\xd1\x17\xb5\x3f\x9f\x16\xff\x8a\xc6\xb2\xc1\xcd\x50\x94\x75\x1c\xa1\x1d\xd2\x1f\xf1\x2e\x49\xf3\x03\x57\x63\xd0\x21\xcb\x09\x16\x51\x87\x51\x44\xc1\x52\x78\xf3\x06\x5e\xf5\xfe\xa6\x8f\x88\xfd\xef\xfc\x87\x77\x6c\xf2\xad\xd4\x22\x89\x33\xdf\x97\x26\x4e\x5c\x3a\x3e\xea\x62\xa2\x42\x71\x23\x15\x25\x43\xac\x27\x51\x15\x7b\x1a\xda\x26\x90\xb8\x54\x0e\x4c\x09\x1c\x6a\x6e\x49\x16\x8d\xe2\x36\xae\xcb\x49\xaf\xe6\x84\xf7\xcc\x94\x46\x9a\xc7\x16\xfa\x55\x98\x8c\x8f\xde\x58\xd3\x76\xf3\x31\x16\x3b\xbc\x61\x3a\x34\xa3\x88\xf8\x76\x08\x38\xeb\xdb\x28\x98\x29\xbb\x4b\x35\x37\x4c\x5e\xe4\xc1\xe4\x3a\xf9\xcd\x90\x90\x57\x52\xe0\x99\x62\xc0\xdb\x55\x76\xbe\xe3\xdb\x5b\x65\x1c\x9e\x71\xe3\x64\xc0\x83\xf1\xbc\x1f\xf3\x1d\x74\xb5\x9e\x1e\xb5\x24\x7a\xc8\xbe\x4e\x18\x89\x55\x29\xad\x23\x36\x66\x26\xbe\x9e\x63\x6f\xa0\xe0\x14\x7e\x02\xc6\x85\xb8\xf5\x1f\x06\x0c\x56\xc0\x2c\xee\xcd\x01\xbb\xf3\xb7\xa9\xe6\x3c\xb5\x61\xdf\x3c\xdd\x65\x4f\x8e\x1e\x3a\x45\x95\xf8\xba\x7a\xb7\x63\xb9\x04\x2a\x24\x7c\xea\xe1\x63\xbd\xae\xba\xbf\x6e\x7c\xb9\x69\x22\xdf\xb0\x2c\x7c\x51\x4c\x39\xd8\x49\xd5\x01\xc6\x36\x38\x34\x7f\x9e\x83\x6f\xef\xf3\x8f\x9b\x02\xa0\xb7\xf2\x43\xfa\xbc\xd5\x94\x8e\x06\xab\xb0\x37\x27\x76\xf7\xf3\x0e\xbb\x66\xfb\x17\x16\xd4\xb7\x79\xda\xc9\x4e\xc2\xd2\x9c\xcc\x6f\xa6\x45\x7b\xcb\x1d\x26\xe9\x64\x72\xb8\xa7\xfa\x35\x6c\xf2\x72\x20\x8e\xc9\x96\xcc\xc6\x98\xc4\x77\x57\xb8\xa7\xdb\xeb\xc4\x2d\xb9\x2f\x92\x2a\xaf\x95\xc1\x7d\x7e\xdf\xa0\x3d\x8e\x7c\x33\xc0\xfa\x9c\xf0\x26\x96\x31\xd5\xd1\xda\xef\xe1\x2e\xbd\xc7\x47\xd8\xe4\xd2\xbd\xdf\xd7\x74\x4c\x7a\xf1\x05\x9b\xcf\x89\xfc\x04\xa8\x1c\x4e\x74\x96\x4b\xf8\x8c\x85\xd1\x82\xdb\x63\xac\xa8\xdf\x88\x7e\xb7\x80\xff\x68\xef\xbe\x1d\x06\x75\x9f\xe2\x26\xf7\x10\xe1\x52\xbb\xe4\x3e\x6f\xa4\x70\xd9\x45\x81\xfd\x57\x65\x3a\xcb\xe5\x6a\x36\x57\xf2\x09\x19\x7d\x30\xe0\x4c\x53\x87\x35\x7a\x34\xcd\xb9\x93\x39\x87\x8c\xc4\x7d\x9a\xc3\x63\x28\xe2\x04\x22\x15\x3f\x3a\xe2\x9e\xfb\x35\xa2\x50\x38\xa6\x18\x3b\xd7\x2b\x74\x5f\x93\x1f\xcb\xa4\xd7\x5b\xaf\xe1\xff\x11\x7f\xde\xbf\x07\xbe\xff\xff\x27\x00\x00\xff\xff\xc5\xd8\x86\x35\x34\x0d\x00\x00")
var _staticJsViewsRowJs = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xa4\x56\x5f\x6f\xdb\x36\x10\x7f\xf7\xa7\xb8\x02\x45\x25\x05\xaa\xbc\x67\x17\xc6\x86\xa5\x7d\x19\xb6\x76\x68\x8b\xf5\xa1\x28\x0c\x5a\x3c\x59\x5c\x69\x52\x21\x4f\x51\xbd\x26\xdf\x7d\x38\x8a\xfa\x17\x3b\x29\x86\x05\x08\x0c\xf1\xfe\xdf\xfd\xf8\x3b\xae\xaf\xae\x56\x70\x05\xbf\x54\x4a\xa3\xbd\x45\x77\xab\xb0\x83\xd7\xe8\x4b\xa7\xf6\xe8\x41\x00\x89\xbd\x46\x70\xb6\x83\x20\xaa\xac\x03\x01\xc6\x12\xae\xe0\x6a\xbd\x92\x58\x29\x83\xe9\xe7\x15\x40\xc2\x72\xbf\x66\x51\x92\xf3\x37\xe1\x37\x7a\x46\x78\x6c\xb4\x20\xf4\x6b\x67\xbb\xa2\xa6\xa3\xee\x85\x7b\x51\x7e\xdd\x5b\x13\x55\x5b\x23\xd1\xf9\xd2\x3a\x4c\x56\x5f\xf2\x55\xd5\x9a\x92\x94\x35\x29\x3b\xfb\x4b\x61\x97\x87\x88\xfe\x23\xe7\xf2\xde\x76\x1f\xa3\xd3\x0c\xbe\xaf\x00\x1c\x52\xeb\x0c\xfc\x1a\x3d\x16\x6c\x50\xe0\x37\x42\x23\x53\x96\x03\xe0\x2d\x1a\xf2\x1b\xe8\xbf\x00\x92\x52\xab\xf2\x6b\xb2\x81\xc4\x9a\xf7\xb6\xbb\x0e\x5f\x41\x76\x9f\xaf\xc2\x6f\xa9\x85\xf7\x6f\xc5\x11\x37\x90\x70\xe8\x97\xce\x76\x21\x55\x00\x12\x87\x28\x20\x97\x44\x75\x65\x14\x29\xa1\xd5\x3f\xb8\x81\x31\x79\xdb\xf0\x8f\xcf\xc6\xb0\xb7\xc2\xc1\xd1\x4a\xa1\x61\x0b\x54\x2b\x5f\x44\x8d\xa2\x11\xde\x77\xd6\xc9\x3f\x58\xf8\x2a\x6a\x07\x8d\x9d\x32\x12\xbf\xc1\x16\xbe\xdf\x2f\xcf\x49\xec\x61\x0b\xa6\xd5\x4b\xfd\xa0\x9e\x66\x8b\xb3\xa3\x95\xa8\x0b\x6b\xd2\xa4\xac\x85\x39\xe0\xa6\x55\x32\xc9\x7b\x99\x54\xbe\xd1\xe2\x74\x6d\x0d\xa1\xa1\xfe\xf0\x07\xd6\x1e\x35\x96\x84\xa3\x0b\xb2\x87\x83\xc6\x0f\xf1\xf4\x07\x2e\xfa\x38\x9b\x0a\xa9\xac\xf1\x7f\x64\x31\x58\x86\x7a\x9f\x36\x90\xe8\xc9\xd9\xd3\x60\x61\xcd\xeb\xfe\xe0\x47\x56\xa5\x3b\x85\xf1\x6c\xd0\x39\xeb\x92\xbc\x1f\x5d\xe1\x90\xb1\xfa\x86\xcf\xe2\xd1\xd2\xc5\x30\x53\x8f\xc2\x95\x75\x98\x68\x70\xd8\x7f\x0f\x59\xf4\x5f\x8f\xa7\xe0\x91\xd8\x64\xec\x74\x25\xb4\xc7\x4b\x8a\xe1\x52\xf6\x70\x7a\xb5\x40\x70\xe8\xcc\x0c\x8d\x13\x0c\x67\xc0\x2a\x48\x1c\x3c\x6c\x21\x9d\x79\x3c\x70\x68\x3e\x4f\x32\xb8\xbb\x83\x24\xc9\x0a\xdf\x68\x45\x69\x02\x49\xb6\x8c\xd1\xf7\x62\x16\xa4\xb4\x9a\x53\x56\xd6\x3c\x08\xf7\x1c\x75\xb8\xfc\xe9\xae\x18\x18\x21\xbd\x78\xa5\xf3\xd1\x0e\xc2\x9d\xdf\xcc\x8b\x25\xfb\xdb\x87\x77\x6f\xd3\x2c\x6a\xdc\x67\x63\x47\x54\x75\x5e\x83\xa8\x51\xc8\x9d\xad\x76\x8d\x53\x47\xe1\x4e\x7d\x3d\x67\x6a\x12\x35\x72\x93\xb3\x2c\x9b\xc5\x1e\xb3\x2e\xbd\x67\xdc\xea\x00\x81\xc4\xb1\xe2\x10\xf4\xfe\x89\xe0\x93\xd7\x47\x9d\x32\x3f\xbe\x94\x58\x5a\x27\xb8\x63\xec\x5e\x2b\x83\x2f\xa9\x76\xb6\x3d\xd4\xe7\x71\x22\xd1\x9d\xcf\x3a\x90\x38\xff\x5d\xc1\xa7\x1a\x0d\x88\xc0\xd7\xca\x43\xe0\x39\x94\x50\x0a\xad\x41\xc0\x11\xa9\xb6\x12\x2c\xfb\x40\x08\xd9\x02\x59\x08\xb7\x91\x8f\x06\x27\xf1\x9a\x82\x22\x8f\xba\x82\xb4\x71\xf6\xd8\x90\x32\x87\x40\xff\x03\x5b\x71\xdd\x06\x4b\xf4\x5e\xb8\x53\x56\x0c\xc6\xaf\xfb\xcb\xcc\xda\x1c\x85\x87\x08\x9d\xd2\x1a\x6a\xd1\x34\x68\xc0\x9a\x12\x83\x64\x8c\xe2\x07\x53\xdf\x96\xec\xae\x6a\xb5\x3e\x41\xe4\x88\xc1\xef\x3a\xfc\x4e\x7c\x7d\x11\xda\xe3\x24\x98\x1f\xcf\x3a\xcf\x87\x85\xaf\x6d\x37\x11\xe4\xd0\xd4\xa9\xd5\xff\x89\xab\xf3\xd1\xcd\xd8\x94\x6d\xe4\x89\x03\xd2\x9f\xf1\x2c\xcd\x8a\x5b\xa1\xd3\x27\xc1\x8a\x26\xf0\x0d\x23\x06\x5e\xbc\x80\x67\x83\xbf\x79\x11\x71\xfe\xbd\xff\x50\xc7\xae\xd8\x2b\x23\xd3\x48\x6c\x43\x6b\x22\xad\x9c\xe1\x67\x16\x33\x34\x37\xf2\x6d\x3a\xc6\x7a\x14\x55\x71\xa6\x61\x6c\x12\x49\x28\xed\xc1\x56\x20\xa0\x11\x8e\x54\xd9\x6a\xe1\xe2\x9b\x60\x36\xab\x25\xab\x3f\x41\x45\x71\x97\x61\x07\xc3\xbe\x4f\xa7\xa2\x77\xce\x76\x3d\x09\x4c\xcd\x0e\x35\xcc\x99\x61\x12\x91\xd8\x8f\x01\x17\x73\x9b\x04\x0b\x65\x7f\xae\xe6\x47\x7a\x89\x64\x9f\x5e\x66\xf8\x05\x12\x8a\x5a\x49\x7c\xa0\x18\xf0\x76\x71\x05\x7d\x14\xfb\x6b\x6d\x3d\x3e\x58\x00\xb3\x0b\x1e\x8c\x97\xf3\x58\x2e\xda\x8b\xfd\x64\xd4\x92\x1c\x20\xfb\x3c\x4d\x48\x6e\x2a\xe5\x3c\x4d\x64\x42\xf2\xf3\x43\xec\x8d\x7b\x26\x83\x9f\x21\x11\x52\x5e\xf3\xeb\x27\x81\x0d\xf3\xdd\xd1\xde\x62\xff\xfd\x65\xae\xb9\x4c\x6d\x5c\xaa\x8f\x4f\x99\x69\x8f\xa1\x53\xd6\x29\xf7\x95\xdd\x4e\xed\xea\xd9\xf2\xb1\xc2\xa7\x7e\x5d\x74\x7f\xd9\xf8\x7c\x9d\x46\xbe\x49\xf2\xf0\x6c\x9a\x2f\x1a\xaf\x74\x0f\x18\xd7\xe2\x38\xfc\x65\x0e\x3c\xde\xa7\x8b\x9b\x03\x60\xb0\xe2\x4b\xfa\xb4\xd5\x9c\x8e\x46\xab\xf0\x38\x98\xd9\xdd\x2c\x27\xec\xdb\xfd\xdf\x58\xd2\x30\xe6\xf9\x24\x7b\x49\x92\x15\x64\x7f\xb7\x1d\xba\x6b\xe1\x31\xcd\x66\x37\x47\x30\xd5\x6f\x61\x57\x54\x23\x71\xcc\x9e\x02\xf9\x14\x93\xc4\xe1\x02\xf7\xf4\x8f\x17\x12\x8e\xfc\x27\x45\x35\x6b\xe5\x70\x53\xdc\xb4\xe8\x4e\x13\xdf\x8c\xb0\x7e\x48\x78\x33\xcb\x98\xea\x64\xcd\xcb\xb9\x4f\xef\xee\x0e\x76\x85\xf2\x6f\x8e\x0d\x9d\xd2\x41\x7c\xc6\xe6\x4b\x22\xbf\x07\xd4\x1e\x67\x3a\xeb\x35\x7c\xc0\xd2\x1a\x29\xdc\x29\x76\x94\x37\x22\xef\x16\xe0\xcd\xdb\x3f\x90\x46\x75\x4e\x71\x57\x30\x44\x84\x32\x3e\xbd\x29\x5a\x25\x7d\x7e\xd6\x60\x7e\x3a\x2f\x1f\x0a\x17\xb3\xb9\x90\x4f\xc8\xe8\xad\x05\x6f\xdb\x26\xac\xd1\x93\x6d\x1f\x3a\x59\x72\xc8\x44\xdc\xf7\x4b\x78\x8c\x4d\x9c\x41\xa4\x16\x27\x4f\x82\xb9\xdf\x20\x4a\x8d\x53\x8a\x71\x72\x83\x42\xff\x64\x7e\x57\xa5\x83\xde\x76\x0b\x3f\x45\xfc\xb1\x7f\x06\x3e\xff\xff\x1b\x00\x00\xff\xff\xb2\x17\x98\x48\x19\x0e\x00\x00")
func staticJsViewsRowJsBytes() ([]byte, error) {
return bindataRead(
@@ -534,7 +534,7 @@ func staticJsViewsRowJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "static/js/views/row.js", size: 3380, mode: os.FileMode(436), modTime: time.Unix(1257894000, 0)}
info := bindataFileInfo{name: "static/js/views/row.js", size: 3609, mode: os.FileMode(436), modTime: time.Unix(1257894000, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
87 bolt.go
@@ -7,6 +7,7 @@ import (
"github.com/boltdb/bolt"
"github.com/jmcfarlane/notable/app"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
@@ -17,29 +18,49 @@ type BoltDB struct {
Path string
Type string
NotesBucket []byte
// Secondary nodes do not have direct write access to the
// database. They write change files which are consumed by the
// primary process.
Secondary *Secondary
}
func openBoltDB(path string) (*BoltDB, error) {
func openBoltDB(path string, secondary bool) (*BoltDB, error) {
db := &BoltDB{
NotesBucket: []byte("notes"),
Path: path,
Type: "BoltDB",
}
if secondary {
db.Secondary = &Secondary{
Path: db.Path,
}
}
_, fileExisted := createParentDirs(path)
engine, err := bolt.Open(path, 0600, &bolt.Options{Timeout: *boltTimeout})
engine, err := bolt.Open(path, 0600, &bolt.Options{
ReadOnly: secondary,
Timeout: *boltTimeout,
})
if err != nil {
return db, err
}
db.Engine = engine
if !fileExisted {
if !secondary && !fileExisted {
db.createSchema()
db.migrate()
}
return db, nil
}
func isNil(s *Secondary) bool {
if s == nil {
return true
}
return false
}
func (db *BoltDB) String() string {
return fmt.Sprintf("type=%s path=%s", db.Type, db.Path)
return fmt.Sprintf("type=%s path=%s secondary=%v", db.Type, db.Path, !isNil(db.Secondary))
}
func (db *BoltDB) close() {
@@ -65,6 +86,9 @@ func (db *BoltDB) createSchema() {
}
func (db *BoltDB) deleteByUID(uid string) error {
if db.Secondary != nil {
return db.Secondary.deleteByUID(uid)
}
if err := unIndex(uid); err != nil {
return err
}
@@ -76,6 +100,16 @@ func (db *BoltDB) deleteByUID(uid string) error {
}
func (db *BoltDB) getNoteByUID(uid string, password string) (app.Note, error) {
if db.Secondary != nil {
notes := db.Secondary.list()
// Sort in reverse order, so the FIRST note wins.
sort.Sort(sort.Reverse(app.TimeSorter(notes)))
for _, note := range notes {
if note.UID == uid {
return decryptNote(note, password)
}
}
}
var note app.Note
err := db.Engine.View(func(tx *bolt.Tx) error {
b := tx.Bucket(db.NotesBucket)
@@ -115,10 +149,26 @@ func (db *BoltDB) migrate() {
func (db *BoltDB) list() app.Notes {
var notes app.Notes
var updates map[string]app.Note
if db.Secondary != nil {
updatedNotes := db.Secondary.list()
// Sort in ascending order, so the LAST note wins.
sort.Sort(app.TimeSorter(updatedNotes))
updates = app.Map(updatedNotes)
}
db.Engine.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(db.NotesBucket)
c := bucket.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
// Take any local updates over what the db has (ignoring deletes)
if note, ok := updates[string(k)]; ok {
if !note.Deleted {
note.Content = ""
notes = append(notes, note)
continue
}
}
// From the primary database
var note app.Note
err := note.FromBytes(v)
if err != nil {
@@ -129,6 +179,24 @@ func (db *BoltDB) list() app.Notes {
}
return nil
})
// Add any secondary notes not yet seen (at all) by the primary
primary := app.Map(notes)
for k, note := range updates {
if _, ok := primary[k]; !ok {
note.Content = ""
notes = append(notes, note)
continue
}
}
// Represent any note deletions not yet consumed by the primary
for i, note := range notes {
if secondaryNote, ok := updates[note.UID]; ok {
notes[i].Deleted = secondaryNote.Deleted
}
}
sort.Sort(sort.Reverse(app.TimeSorter(notes)))
return notes
}
@@ -138,12 +206,15 @@ func (db *BoltDB) update(note app.Note) (app.Note, error) {
if err != nil {
return note, err
}
if db.Secondary != nil {
return db.Secondary.update(note)
}
b, err := note.ToBytes()
if err != nil {
return note, errors.Wrap(err, "Aborted prior to persist attempt")
}
err = db.Engine.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(db.NotesBucket)
b, err := note.ToBytes()
if err != nil {
return err
}
bucket.Put([]byte(note.UID), b)
return nil
})
@@ -21,9 +21,15 @@ func unIndex(uid string) error {
func getIndex(path string) (bleve.Index, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
mapping := bleve.NewIndexMapping()
return bleve.New(path, mapping)
idx, err := bleve.New(path, mapping)
if err != nil {
return idx, err
}
idx.Close()
}
return bleve.Open(path)
return bleve.OpenUsing(path, map[string]interface{}{
"read_only": *secondary,
})
}
func indexNote(note app.Note) error {
38 main.go
@@ -15,6 +15,7 @@ import (
"time"
"github.com/blevesearch/bleve"
"github.com/jmcfarlane/notable/app"
"github.com/julienschmidt/httprouter"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
@@ -53,6 +54,7 @@ var (
daemon = flag.Bool("daemon", true, "Run as a daemon")
doReIndex = flag.Bool("reindex", false, "Re-index all notes on startup")
restart = flag.Bool("restart", false, "Restart if already running")
secondary = flag.Bool("secondary", false, "Run program as secondary, not primary")
useBolt = flag.Bool("use.bolt", true, "Use the new BoltDB backend")
version = flag.Bool("version", false, "Print program version information")
boltTimeout = flag.Duration("bolt.timeout", time.Duration(time.Second*2), "Boltdb open timeout")
@@ -130,6 +132,29 @@ func getRouter() *httprouter.Router {
return router
}
func persistSecondaryUpdate(note app.Note) error {
if note.Deleted {
return db.deleteByUID(note.UID)
}
note.AheadOfPrimary = false
_, err := db.update(note)
return err
}
func consumeUpdatesFromSecondaries(db Backend, secondaries Secondary) {
for _, note := range secondaries.list() {
if err := persistSecondaryUpdate(note); err != nil {
log.Errorf("Unable to recover note=%v err=%v", note, err)
continue
}
if err := os.Remove(note.SecondaryPath); err != nil {
log.Errorf("Unable to delete secondary note=%v err=%v", note, err)
continue
}
log.Infof("Successfully recovered uid=%s", note.UID)
}
}
func main() {
flag.Parse()
if *version {
@@ -154,7 +179,7 @@ func main() {
*dbPath = filepath.Join(homeDirPath(), ".notable/notes.db")
}
if *useBolt || runtime.GOOS == "darwin" {
db, err = openBoltDB(*dbPath)
db, err = openBoltDB(*dbPath, *secondary)
} else {
db, err = openSqlite3(*dbPath)
}
@@ -171,6 +196,17 @@ func main() {
log.Panic("Re-indexing failed:", err)
}
}
if *secondary {
go reloadAsNeeded()
} else {
secondaries := Secondary{Path: *dbPath}
consumeUpdatesFromSecondaries(db, secondaries)
go func() {
for _ = range time.NewTicker(time.Second * 2).C {
consumeUpdatesFromSecondaries(db, secondaries)
}
}()
}
defer db.close()
log.Infof("Using backend %s", db)
if *daemon {
@@ -59,7 +59,7 @@ func setup(t *testing.T) Mock {
db, err = openSqlite3(file.Name())
default:
backend = "boltdb"
db, err = openBoltDB(file.Name())
db, err = openBoltDB(file.Name(), false)
}
idx, err = getIndex(file.Name() + ".idx")
fmt.Println("TESTING BACKEND:", backend)
Oops, something went wrong.

0 comments on commit 2d71d70

Please sign in to comment.