├── .gitignore ├── LICENSE ├── README.md ├── examples ├── main.go ├── tree │ └── tree.go ├── views │ └── views.go ├── watch_path.go └── watch_single.go └── irmin ├── client.go ├── http.go ├── http_test.go ├── log.go ├── path.go ├── value.go └── views.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Magnus Skjegstad 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Go implementation of Irmin HTTP bindings 2 | 3 | This library is a Go implementation of the [Irmin](https://github.com/mirage/irmin.git) HTTP API. The HTTP API is partly documented [here](https://github.com/mirage/irmin/wiki/REST-API). Not all calls are available in Irmin version 0.10.0 or older. `Version()` can be used to check the Irmin version. 4 | 5 | #### Examples 6 | 7 | ##### Connecting to Irmin 8 | 9 | ```go 10 | uri, err := url.Parse("http://127.0.0.1:8080") 11 | if err != nil { 12 | panic(err) 13 | } 14 | conn := irmin.Create(uri, "example-app") 15 | ``` 16 | 17 | ##### Check Irmin version 18 | ```go 19 | v, err := conn.Version() 20 | if err != nil { 21 | panic(err) 22 | } 23 | fmt.Printf("Connected to Irmin version %s\n", v) 24 | ``` 25 | 26 | ##### Create or update a key 27 | ```go 28 | task := conn.NewTask("Update key") // Commit message 29 | key := irmin.ParsePath("/a/b") 30 | v := []byte("Hello world") 31 | hash, err := conn.Update(task, key, v) // Returns commit hash 32 | if err != nil { 33 | panic(err) 34 | } 35 | ``` 36 | 37 | ##### Read a value 38 | ```go 39 | key := irmin.ParsePath("/a/b") 40 | v, err := conn.ReadString(key) 41 | if err != nil { 42 | panic(err) 43 | } 44 | fmt.Printf("%s=%s\n", key.String(), v) 45 | ``` 46 | 47 | ##### Iterate through all keys 48 | ```go 49 | ch, err := conn.Iter() // Iterate through all keys 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | for key := range ch { 55 | v, err := conn.ReadString(key) 56 | if err != nil { 57 | panic(err) 58 | } 59 | fmt.Printf("%s=%s\n", key.String(), v) 60 | } 61 | ``` 62 | 63 | ##### Other examples 64 | 65 | - [Misc. common commands](examples/main.go) 66 | - [Iterate through all keys](examples/tree/tree.go) 67 | - [Creating and merging views/transactions](examples/views/views.go) 68 | - [Watch a key for changes](examples/watch_single.go) 69 | - [Watch a path recursively for changes](examples/watch_path.go) 70 | 71 | To run an example, clone `irmin-go` and run `go run examples/[example code]`. 72 | 73 | #### Installing Irmin 74 | Installation instructions for Irmin are available [here](https://github.com/mirage/irmin/blob/master/README.md). When installing with `opam`, the `--dev` parameter can be used to install the latest development version. 75 | 76 | To set up a test database with Irmin, this command will create a new database in `/tmp/irmin/test` (if it doesn't exist) and start listening for HTTP requests on port 8080: 77 | 78 | ``` 79 | irmin init -d -v --root /tmp/irmin/test -a http://:8080 80 | ``` 81 | 82 | #### Supported API calls 83 | 84 | - head 85 | - read 86 | - mem 87 | - list 88 | - iter 89 | - update 90 | - clone, clone-force 91 | - compare-and-set 92 | - remove, remove-rec 93 | - watch, watch-rec 94 | - tree/{list, mem, head, read, update, remove, remove-rec, iter, watch, watch-rec, clone, clone-force, compare-and-set} 95 | - view/{create, update, read, merge-path, update-path} 96 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/hex" 21 | "fmt" 22 | "net/url" 23 | 24 | "../irmin" 25 | ) 26 | 27 | // irmin init -d -v --root /tmp/irmin/test -a http://:8080 28 | 29 | func main() { 30 | uri, err := url.Parse("http://127.0.0.1:8080") 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | r := irmin.Create(uri, "api-tester") 36 | { // get version 37 | v, err := r.Version() 38 | if err != nil { 39 | panic(err) 40 | } 41 | fmt.Printf("version: %s\n", v) 42 | } 43 | { // list commands 44 | fmt.Printf("supported commands:\n") 45 | s, err := r.AvailableCommands() 46 | if err != nil { 47 | panic(err) 48 | } 49 | for i, v := range s { 50 | fmt.Printf("%d: %s\n", i, v) 51 | } 52 | } 53 | { // list 54 | paths, err := r.List(irmin.ParsePath("/a")) 55 | if err != nil { 56 | panic(err) 57 | } 58 | fmt.Printf("list /\n") 59 | for i, v := range paths { 60 | fmt.Printf("%d: %s\n", i, v.String()) 61 | } 62 | } 63 | { // iter 64 | var ch <-chan *irmin.Path 65 | if ch, err = r.Iter(); err != nil { 66 | panic(err) 67 | } 68 | 69 | for p := range ch { 70 | fmt.Printf("iter: %s\n", (*p).String()) 71 | } 72 | } 73 | { // iter on head 74 | head, err := r.Head() 75 | if err != nil { 76 | panic(err) 77 | } 78 | t := r.FromTree(hex.EncodeToString(head)) 79 | var ch <-chan *irmin.Path 80 | if ch, err = t.Iter(); err != nil { 81 | panic(err) 82 | } 83 | 84 | for p := range ch { 85 | fmt.Printf("iter from HEAD: %s\n", (*p).String()) 86 | } 87 | } 88 | { // iter + read 89 | var ch <-chan *irmin.Path 90 | if ch, err = r.Iter(); err != nil { 91 | panic(err) 92 | } 93 | 94 | for p := range ch { 95 | d, err := r.ReadString(*p) 96 | if err != nil { 97 | panic(err) 98 | } 99 | fmt.Printf("%s=%s\n", (*p).String(), d) 100 | } 101 | } 102 | { // update + read 103 | key := "g" 104 | fmt.Printf("update %s=hello world\n", key) 105 | data := []byte("Hello \"world") 106 | hash, err := r.Update(r.NewTask("update key"), irmin.ParsePath(key), data) 107 | if err != nil { 108 | panic(err) 109 | } 110 | fmt.Printf("update hash: %s\n", hash) 111 | fmt.Printf("read %s\n", key) 112 | d, err := r.ReadString(irmin.ParsePath(key)) 113 | if err != nil { 114 | panic(err) 115 | } 116 | fmt.Printf("%s=%s\n", key, d) 117 | } 118 | { // get head 119 | v, err := r.Head() 120 | if err != nil { 121 | panic(err) 122 | } 123 | fmt.Printf("head: %s\n", hex.EncodeToString(v)) 124 | } 125 | /* compare-and-set is not yet implemented in irmin, see https://github.com/mirage/irmin/issues/288 126 | { // compare-and-set 127 | key := "g" 128 | oldData := []byte("Hello world") 129 | newData := []byte("asdf") 130 | fmt.Printf("compare-and-set %s=%s to %s\n", key, oldData, newData) 131 | hash, err := r.CompareAndSet(r.NewTask("compare-and-set key"), irmin.ParsePath(key), &oldData, &newData) 132 | if err != nil { 133 | panic(err) 134 | } 135 | fmt.Printf("compare-and-set hash: %s\n", hash) 136 | fmt.Printf("read %s\n", key) 137 | d, err := r.ReadString(irmin.ParsePath(key)) 138 | if err != nil { 139 | panic(err) 140 | } 141 | fmt.Printf("%s=%s\n", key, d) 142 | } 143 | */ 144 | } 145 | -------------------------------------------------------------------------------- /examples/tree/tree.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "../../irmin" 21 | "fmt" 22 | "net/url" 23 | ) 24 | 25 | func main() { 26 | uri, _ := url.Parse("http://127.0.0.1:8080") 27 | r := irmin.Create(uri, "tree") 28 | 29 | ch, err := r.Iter() // Iterate through all keys 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | for path := range ch { 35 | d, err := r.ReadString(*path) // Read key 36 | if err != nil { 37 | panic(err) 38 | } 39 | fmt.Printf("%s=%s\n", (*path).String(), d) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/views/views.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "math/rand" 22 | "net/url" 23 | "time" 24 | 25 | "../../irmin" 26 | ) 27 | 28 | func listDb(r *irmin.Conn) { 29 | ch, err := r.Iter() // Iterate through all keys 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | for path := range ch { 35 | d, err := r.ReadString(*path) // Read key 36 | if err != nil { 37 | panic(err) 38 | } 39 | fmt.Printf("%s=%s\n", (*path).String(), d) 40 | } 41 | } 42 | 43 | func main() { 44 | rand.Seed(time.Now().UnixNano()) 45 | uri, _ := url.Parse("http://127.0.0.1:8080") 46 | r := irmin.Create(uri, "view-example") 47 | 48 | // Check for /view-test and remove if it exists 49 | b, err := r.Mem(irmin.ParsePath("/view-test/exists")) 50 | if err != nil { 51 | panic(err) 52 | } 53 | if b { 54 | fmt.Printf("view path exists, removing...\n") 55 | if err = r.RemoveRec(r.NewTask("removing existing /view-test"), irmin.ParsePath("/view-test")); err != nil { 56 | panic(err) 57 | } 58 | } 59 | 60 | // Create a key in /view-test 61 | s, err := r.Update(r.NewTask("update key /view-test/exists"), irmin.ParsePath("/view-test/exists"), irmin.NewValue(fmt.Sprintf("hello world %d", rand.Int31()))) 62 | if err != nil { 63 | panic(err) 64 | } 65 | fmt.Printf("update=%s\n", s) 66 | 67 | listDb(r) 68 | 69 | // Create view #1 from /view-test 70 | v1, err := r.CreateView(r.NewTask("create view 1"), irmin.ParsePath("/view-test/")) 71 | if err != nil { 72 | panic(err) 73 | } 74 | s, err = v1.Update(r.NewTask("add key"), irmin.ParsePath("from-view-1"), irmin.NewValue("hello world from view 1")) 75 | if err != nil { 76 | panic(err) 77 | } 78 | fmt.Printf("update view 1=%s\n", s) 79 | 80 | // Create view #2 from /view-test 81 | v2, err := r.CreateView(r.NewTask("create view 2"), irmin.ParsePath("/view-test/")) 82 | if err != nil { 83 | panic(err) 84 | } 85 | s, err = v2.Update(r.NewTask("add key"), irmin.ParsePath("from-view-2"), irmin.NewValue("hello world from view 2")) 86 | if err != nil { 87 | panic(err) 88 | } 89 | fmt.Printf("update view 2=%s\n", s) 90 | 91 | // Iterate through updated view 92 | ch, err := v2.Iter() 93 | if err != nil { 94 | panic(err) 95 | } 96 | for p := range ch { 97 | fmt.Printf("View path: %s\n", p.String()) 98 | } 99 | 100 | // Merge view 2 101 | 102 | fmt.Printf("merge view 2=%s\n", s) 103 | err = v2.MergePath(r.NewTask("merge view 2"), "master", irmin.ParsePath("/view-test/")) 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | // Merge view 1 109 | 110 | fmt.Printf("merge view 1=%s\n", s) 111 | err = v1.MergePath(r.NewTask("merge view 1"), "master", irmin.ParsePath("/view-test/")) 112 | if err != nil { 113 | panic(err) 114 | } 115 | 116 | listDb(r) 117 | } 118 | -------------------------------------------------------------------------------- /examples/watch_path.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/hex" 21 | "fmt" 22 | "net/url" 23 | 24 | "../irmin" 25 | ) 26 | 27 | // irmin init -d -v --root /tmp/irmin/test -a http://:8080 28 | 29 | func main() { 30 | uri, err := url.Parse("http://127.0.0.1:8080") 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | r := irmin.Create(uri, "api-tester") 36 | { // get version 37 | v, err := r.Version() 38 | if err != nil { 39 | panic(err) 40 | } 41 | fmt.Printf("version: %s\n", v) 42 | } 43 | 44 | key := irmin.ParsePath("/") 45 | fmt.Printf("Watching %s\n", key.String()) 46 | fmt.Printf("(run examples/views/views.go example to test)\n") 47 | ch, err := r.WatchPath(key, nil) 48 | if err != nil { 49 | panic(err) 50 | } 51 | for a := range ch { 52 | fmt.Printf("commit: %s\n", hex.EncodeToString(a.Commit)) 53 | for _, c := range a.Changes { 54 | fmt.Printf(" %s %s\n", c.Change, c.Key.String()) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/watch_single.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "encoding/hex" 21 | "fmt" 22 | "net/url" 23 | 24 | "../irmin" 25 | ) 26 | 27 | // irmin init -d -v --root /tmp/irmin/test -a http://:8080 28 | 29 | func main() { 30 | uri, err := url.Parse("http://127.0.0.1:8080") 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | r := irmin.Create(uri, "api-tester") 36 | { // get version 37 | v, err := r.Version() 38 | if err != nil { 39 | panic(err) 40 | } 41 | fmt.Printf("version: %s\n", v) 42 | } 43 | 44 | key := irmin.ParsePath("/view-test/exists") 45 | fmt.Printf("Watching %s\n", key.String()) 46 | fmt.Printf("(run examples/views/views.go example to test)\n") 47 | ch, err := r.Watch(key) 48 | if err != nil { 49 | panic(err) 50 | } 51 | for c := range ch { 52 | fmt.Printf("commit: %s value: %s\n", hex.EncodeToString(c.Commit), c.Value) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /irmin/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package irmin 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "io/ioutil" 24 | "net/http" 25 | "net/url" 26 | "sync" 27 | ) 28 | 29 | // Client contains basic state needed to connect to Irmin 30 | type Client struct { 31 | baseURI *url.URL // Irmin base URI 32 | log Log // Logger 33 | } 34 | 35 | // StreamReply contains one reply received from an Irmin stream 36 | type StreamReply struct { 37 | Error Value 38 | Result json.RawMessage 39 | } 40 | 41 | // NewClient creates a new client data structure 42 | func NewClient(uri *url.URL, log Log) *Client { 43 | return &Client{uri, log} 44 | } 45 | 46 | // Call connects to the specified URL and attempts to unmarshal the reply. The result is stored in v. 47 | func (c *Client) Call(uri *url.URL, post *postRequest, v interface{}) (err error) { 48 | c.log.Printf("calling: %s\n", uri.String()) 49 | var res *http.Response 50 | if post == nil { 51 | res, err = http.Get(uri.String()) 52 | } else { 53 | j, err := json.Marshal(post) 54 | if err != nil { 55 | panic(err) 56 | } 57 | c.log.Printf("post body: %s\n", j) 58 | res, err = http.Post(uri.String(), "application/json", bytes.NewBuffer(j)) 59 | } 60 | if err != nil { 61 | return 62 | } 63 | defer res.Body.Close() 64 | if res.StatusCode != 200 { 65 | return fmt.Errorf("Irmin HTTP server returned status %#v", res.Status) 66 | } 67 | body, err := ioutil.ReadAll(res.Body) 68 | if err != nil { 69 | return 70 | } 71 | c.log.Printf("returned: %s\n", body) 72 | 73 | return json.Unmarshal(body, v) 74 | } 75 | 76 | // CallStream connects to the given URL and returns a channel with responses until the stream is closed. The channel contains raw replies and must be unmarshaled by the caller. 77 | func (c *Client) CallStream(uri *url.URL, post *postRequest) (<-chan *StreamReply, error) { 78 | var streamToken struct { 79 | Stream Value 80 | } 81 | var version struct { 82 | Version Value 83 | } 84 | 85 | var res *http.Response 86 | var err error 87 | 88 | if post == nil { 89 | res, err = http.Get(uri.String()) 90 | } else { 91 | j, err := json.Marshal(post) 92 | if err != nil { 93 | panic(err) 94 | } 95 | res, err = http.Post(uri.String(), "application/json", bytes.NewBuffer(j)) 96 | } 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | wg := sync.WaitGroup{} 102 | wg.Add(1) 103 | defer wg.Done() 104 | go func() { 105 | wg.Wait() // close when all readers are done 106 | res.Body.Close() 107 | }() 108 | 109 | dec := json.NewDecoder(res.Body) 110 | var t interface{} 111 | if t, err = dec.Token(); err != nil { // read [ token 112 | return nil, err 113 | } 114 | switch t.(type) { 115 | case json.Delim: 116 | d := t.(json.Delim).String() 117 | if d != "[" { 118 | descr := fmt.Errorf("expected [, got %s", d) // If we are unable to unmarshal error msg, return this error 119 | // Invalid format. Try to unmarshal error value, in case it was returned outside the stream 120 | rest, err := ioutil.ReadAll(res.Body) 121 | if err != nil { 122 | return nil, err 123 | } 124 | buf, err := ioutil.ReadAll(dec.Buffered()) 125 | all := append([]byte(d), append(buf, rest...)...) 126 | var errormsg ErrorVersion 127 | err = json.Unmarshal(all, &errormsg) 128 | if err != nil { 129 | return nil, descr 130 | } 131 | if errormsg.Error != nil { 132 | return nil, fmt.Errorf("Server returned an error: %s", errormsg.Error.String()) 133 | } 134 | return nil, descr 135 | } 136 | default: 137 | err = fmt.Errorf("expected delimiter") 138 | return nil, err 139 | } 140 | 141 | err = dec.Decode(&streamToken) 142 | if err != nil || !bytes.Equal(streamToken.Stream, []byte("start")) { // look for stream start 143 | return nil, err 144 | } 145 | 146 | err = dec.Decode(&version) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | ch := make(chan *StreamReply, 100) 152 | wg.Add(1) 153 | go func() { 154 | defer func() { 155 | close(ch) 156 | wg.Done() 157 | }() 158 | 159 | for dec.More() { 160 | s := new(StreamReply) 161 | if err = dec.Decode(s); err != nil { 162 | return 163 | } 164 | if len(s.Result) == 0 { // If result is empty, look for stream end 165 | if err = dec.Decode(&streamToken); err != nil || bytes.Equal(streamToken.Stream, []byte("end")) { // look for stream end 166 | return 167 | } 168 | } 169 | ch <- s 170 | } 171 | }() 172 | return ch, nil 173 | } 174 | -------------------------------------------------------------------------------- /irmin/http.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package irmin 18 | 19 | import ( 20 | "encoding/hex" 21 | "encoding/json" 22 | "fmt" 23 | "net/url" 24 | "strings" 25 | "time" 26 | "unicode/utf8" 27 | ) 28 | 29 | // ErrorVersion contains the error version returned by Irmin 30 | type ErrorVersion struct { 31 | Error Value 32 | Version Value 33 | } 34 | 35 | type stringArrayReply struct { 36 | ErrorVersion 37 | Result []Value 38 | } 39 | 40 | type stringReply struct { 41 | ErrorVersion 42 | Result Value 43 | } 44 | 45 | type pathArrayReply struct { 46 | ErrorVersion 47 | Result []Path 48 | } 49 | 50 | type boolReply struct { 51 | ErrorVersion 52 | Result bool 53 | } 54 | 55 | // Task describes the commit message stored in Irmin 56 | type Task struct { 57 | Date string `json:"date"` 58 | UID string `json:"uid"` 59 | Owner Value `json:"owner"` 60 | Messages []Value `json:"messages"` 61 | } 62 | 63 | // CommitValuePair represents the value of a key at a specific commit 64 | type CommitValuePair struct { 65 | Commit []byte 66 | Value []byte 67 | } 68 | 69 | const ( 70 | // KeyDeleted is a change type returned by WatchPath when a key is deleted 71 | KeyDeleted = "-" 72 | // KeyCreated is change type returned by WatchPath when a key is created 73 | KeyCreated = "+" 74 | // KeyUpdated is change tpye returned by WatchPath when a key is updated 75 | KeyUpdated = "*" 76 | ) 77 | 78 | // WatchPathChange contains one change received from a watch, usually as a part of WatchPathCommit 79 | type WatchPathChange struct { 80 | Change string // *=Updated, +=Created, -=Deleted 81 | Key Path 82 | } 83 | 84 | // WatchPathCommit contains a commit and updated, deleted or created keys as returned by WatchPath 85 | type WatchPathCommit struct { 86 | Commit []byte 87 | Changes []WatchPathChange 88 | Error error // Only set if an error occurred and the watch needs to be restarted 89 | } 90 | 91 | type postRequest struct { 92 | Task Task `json:"task"` 93 | Data json.RawMessage `json:"params,omitempty"` 94 | } 95 | 96 | type commandsReply stringArrayReply 97 | type listReply pathArrayReply 98 | type memReply boolReply 99 | type readReply stringArrayReply 100 | type cloneReply stringReply 101 | type updateReply stringReply 102 | type removeReply stringReply 103 | type removeRecReply stringReply 104 | type headReply stringArrayReply 105 | 106 | // Conn is an Irmin REST API connection 107 | type Conn struct { 108 | Client 109 | tree string 110 | taskowner string 111 | } 112 | 113 | // Create an Irmin REST HTTP connection data structure 114 | func Create(uri *url.URL, taskowner string) *Conn { 115 | r := new(Conn) 116 | r.Client = *NewClient(uri, IgnoreLog{}) 117 | r.taskowner = taskowner 118 | return r 119 | } 120 | 121 | // SetLog sets the log implementation. Log messages are ignored by default. 122 | func (rest *Conn) SetLog(log Log) { 123 | rest.log = log 124 | } 125 | 126 | // FromTree returns new Conn with a new tree position. An empty tree value defaults to master branch. 127 | func (rest *Conn) FromTree(tree string) *Conn { 128 | t := *rest 129 | t.tree = tree 130 | return &t 131 | } 132 | 133 | // Tree reads the current tree position use for Tree sub-commands. Empty defaults to master. 134 | func (rest *Conn) Tree() string { 135 | return rest.tree 136 | } 137 | 138 | // TaskOwner returns name of task owner (commit author) 139 | func (rest *Conn) TaskOwner() string { 140 | return rest.taskowner 141 | } 142 | 143 | // SetTaskOwner sets the commit author in Irmin 144 | func (rest *Conn) SetTaskOwner(owner string) { 145 | rest.taskowner = owner 146 | } 147 | 148 | // NewTask creates a new task (commit message) that can be be submitted with a command 149 | func NewTask(taskowner string, message string) Task { 150 | var t Task 151 | t.Date = fmt.Sprintf("%d", time.Now().Unix()) 152 | t.UID = "0" 153 | t.Owner = NewValue(taskowner) 154 | t.Messages = []Value{NewValue(message)} 155 | return t 156 | } 157 | 158 | // NewTask creates a new task that can be be submitted with a command (commit message) 159 | func (rest *Conn) NewTask(message string) Task { 160 | return NewTask(rest.taskowner, message) 161 | } 162 | 163 | // MakeCallURL creates an invocation URL for an Irmin REST command with an optional sub command type 164 | func (rest *Conn) MakeCallURL(command string, path Path, supportsTree bool) (*url.URL, error) { 165 | var suffix *url.URL 166 | var err error 167 | 168 | u := path.URL() 169 | p := strings.Replace(u.String(), "+", "%20", -1) // Replace + with %20, see https://github.com/golang/go/issues/4013 170 | 171 | if supportsTree && rest.Tree() != "" { // Ignore the parameter if Tree is not set 172 | t := url.QueryEscape(rest.Tree()) 173 | if suffix, err = url.Parse(fmt.Sprintf("/tree/%s/%s%s", t, command, p)); err != nil { 174 | return nil, err 175 | } 176 | } else { 177 | if suffix, err = url.Parse(fmt.Sprintf("/%s%s", command, p)); err != nil { 178 | return nil, err 179 | } 180 | } 181 | 182 | return rest.baseURI.ResolveReference(suffix), nil 183 | } 184 | 185 | // AvailableCommands queries Irmin for a list of available commands 186 | func (rest *Conn) AvailableCommands() ([]string, error) { 187 | var data commandsReply 188 | 189 | uri, err := rest.MakeCallURL("", Path{}, true) 190 | if err != nil { 191 | return []string{}, err 192 | } 193 | 194 | if err = rest.Call(uri, nil, &data); err != nil { 195 | return []string{}, err 196 | } 197 | if data.Error.String() != "" { 198 | return []string{}, fmt.Errorf(data.Error.String()) 199 | } 200 | 201 | r := make([]string, len(data.Result)) 202 | for i, v := range data.Result { 203 | r[i] = v.String() 204 | } 205 | return r, nil 206 | } 207 | 208 | // Version returns the Irmin version 209 | func (rest *Conn) Version() (string, error) { 210 | var data commandsReply 211 | var err error 212 | uri, err := rest.MakeCallURL("", Path{}, true) 213 | if err != nil { 214 | return "", err 215 | } 216 | if err = rest.Call(uri, nil, &data); err != nil { 217 | return "", err 218 | } 219 | if data.Error.String() != "" { 220 | return "", fmt.Errorf(data.Error.String()) 221 | } 222 | 223 | return data.Version.String(), nil 224 | } 225 | 226 | // List returns a list of keys in a path 227 | func (rest *Conn) List(path Path) ([]Path, error) { 228 | var data listReply 229 | uri, err := rest.MakeCallURL("list", path, true) 230 | if err != nil { 231 | return []Path{}, err 232 | } 233 | if err = rest.Call(uri, nil, &data); err != nil { 234 | return []Path{}, err 235 | } 236 | if data.Error.String() != "" { 237 | return []Path{}, fmt.Errorf(data.Error.String()) 238 | } 239 | 240 | return data.Result, nil 241 | } 242 | 243 | // Mem returns true if a path exists 244 | func (rest *Conn) Mem(path Path) (bool, error) { 245 | var data memReply 246 | uri, err := rest.MakeCallURL("mem", path, true) 247 | if err != nil { 248 | return false, err 249 | } 250 | if err = rest.Call(uri, nil, &data); err != nil { 251 | return false, err 252 | } 253 | if data.Error.String() != "" { 254 | return false, fmt.Errorf(data.Error.String()) 255 | } 256 | return data.Result, nil 257 | } 258 | 259 | // Head returns the commit hash of HEAD. Returns nil if no current HEAD (db is empty) 260 | func (rest *Conn) Head() ([]byte, error) { 261 | var data headReply 262 | uri, err := rest.MakeCallURL("head", nil, true) 263 | if err != nil { 264 | return []byte{}, err 265 | } 266 | if err = rest.Call(uri, nil, &data); err != nil { 267 | return []byte{}, err 268 | } 269 | if data.Error.String() != "" { 270 | return []byte{}, fmt.Errorf("irmin error: %s", data.Error.String()) 271 | } 272 | if len(data.Result) > 1 { 273 | return []byte{}, fmt.Errorf("head returned more than one result") 274 | } 275 | if len(data.Result) == 1 { 276 | hash, err := hex.DecodeString(data.Result[0].String()) 277 | if err != nil { 278 | return []byte{}, fmt.Errorf("Unable to parse hash from Irmin: %s", data.Result[0]) 279 | } 280 | return hash, nil 281 | } 282 | if len(data.Result) == 0 { 283 | return nil, nil 284 | } 285 | return []byte{}, fmt.Errorf("Invalid data from Irmin.") 286 | } 287 | 288 | // Read key value as byte array 289 | func (rest *Conn) Read(path Path) ([]byte, error) { 290 | var data readReply 291 | uri, err := rest.MakeCallURL("read", path, true) 292 | if err != nil { 293 | return []byte{}, err 294 | } 295 | if err = rest.Call(uri, nil, &data); err != nil { 296 | return []byte{}, err 297 | } 298 | if data.Error.String() != "" { 299 | return []byte{}, fmt.Errorf(data.Error.String()) 300 | } 301 | if len(data.Result) > 1 { 302 | return []byte{}, fmt.Errorf("read %s returned more than one result", path.String()) 303 | } 304 | if len(data.Result) == 1 { 305 | return data.Result[0], nil 306 | } 307 | return []byte{}, fmt.Errorf("invalid key %s", path.String()) 308 | } 309 | 310 | // ReadString reads a value as string. The value must contain a valid UTF-8 encoded string. 311 | func (rest *Conn) ReadString(path Path) (string, error) { 312 | res, err := rest.Read(path) 313 | if err != nil { 314 | return "", err 315 | } 316 | if utf8.Valid(res) { 317 | return string(res), nil 318 | } 319 | return "", fmt.Errorf("path %s does not contain a valid utf8 string", path.String()) 320 | } 321 | 322 | // Update a key. Returns hash as string on success. 323 | func (rest *Conn) Update(t Task, path Path, contents []byte) (string, error) { 324 | var data updateReply 325 | var err error 326 | 327 | var body postRequest 328 | i := Value(contents) 329 | 330 | body.Data, err = i.MarshalJSON() 331 | if err != nil { 332 | return "", err 333 | } 334 | 335 | body.Task = t 336 | 337 | uri, err := rest.MakeCallURL("update", path, true) 338 | if err != nil { 339 | return "", err 340 | } 341 | if err = rest.Call(uri, &body, &data); err != nil { 342 | return data.Result.String(), err 343 | } 344 | if data.Error.String() != "" { 345 | return "", fmt.Errorf(data.Error.String()) 346 | } 347 | if data.Result.String() == "" { 348 | return "", fmt.Errorf("update seemed to succeed, but didn't return a hash", path.String(), data.Result.String()) 349 | } 350 | 351 | return data.Result.String(), nil 352 | } 353 | 354 | // Remove key 355 | func (rest *Conn) Remove(t Task, path Path) error { 356 | var data removeReply 357 | uri, err := rest.MakeCallURL("remove", path, true) 358 | if err != nil { 359 | return err 360 | } 361 | body := postRequest{t, nil} 362 | if err = rest.Call(uri, &body, &data); err != nil { 363 | return err 364 | } 365 | if data.Error.String() != "" { 366 | return fmt.Errorf(data.Error.String()) 367 | } 368 | if len(data.Result) > 1 { 369 | return fmt.Errorf("remove %s returned more than one result", path.String()) 370 | } 371 | 372 | return nil 373 | } 374 | 375 | // RemoveRec removes a key and its subtree recursively 376 | func (rest *Conn) RemoveRec(t Task, path Path) error { 377 | var data removeReply 378 | uri, err := rest.MakeCallURL("remove-rec", path, true) 379 | if err != nil { 380 | return err 381 | } 382 | body := postRequest{t, nil} 383 | if err = rest.Call(uri, &body, &data); err != nil { 384 | return err 385 | } 386 | if data.Error.String() != "" { 387 | return fmt.Errorf(data.Error.String()) 388 | return fmt.Errorf("remove-rec %s returned empty result", path.String()) 389 | } 390 | 391 | return nil 392 | } 393 | 394 | // Iter iterates through all keys in database. Returns results in a channel as they are received. 395 | func (rest *Conn) Iter() (<-chan *Path, error) { 396 | uri, err := rest.MakeCallURL("iter", Path{}, true) 397 | if err != nil { 398 | return nil, err 399 | } 400 | var ch <-chan *StreamReply 401 | if ch, err = rest.CallStream(uri, nil); err != nil || ch == nil { 402 | return nil, err 403 | } 404 | 405 | out := make(chan *Path, 1) 406 | 407 | go func() { 408 | defer close(out) 409 | for m := range ch { 410 | p := new(Path) 411 | if err := json.Unmarshal(m.Result, &p); err != nil { 412 | panic(err) // TODO This should be returned to caller 413 | } 414 | out <- p 415 | } 416 | }() 417 | 418 | return out, err 419 | } 420 | 421 | // Watch a specific key for create/delete/update. Returns commit/value pairs. This function is not recursive (see WatchPath) 422 | func (rest *Conn) Watch(path Path, firstCommit []byte) (<-chan *CommitValuePair, error) { // TODO not path 423 | type watchKeyReply [][]Value // An array of arrays of commit/value pairs 424 | 425 | var body *postRequest 426 | if firstCommit != nil { 427 | body = new(postRequest) 428 | body.Task = rest.NewTask("Watching db") 429 | s := hex.EncodeToString(firstCommit) 430 | body.Data = json.RawMessage(fmt.Sprintf("[\"%s\", \"%s\"]", s, "hei")) 431 | } 432 | 433 | uri, err := rest.MakeCallURL("watch", path, true) 434 | if err != nil { 435 | return nil, err 436 | } 437 | 438 | var ch <-chan *StreamReply 439 | if ch, err = rest.CallStream(uri, body); err != nil || ch == nil { 440 | return nil, err 441 | } 442 | 443 | out := make(chan *CommitValuePair, 1) 444 | 445 | go func() { 446 | defer close(out) 447 | for m := range ch { 448 | p := new([][]Value) 449 | if err := json.Unmarshal(m.Result, p); err != nil { 450 | panic(err) // TODO This should be returned to caller 451 | } 452 | for _, q := range *p { 453 | if len(q) != 2 { 454 | rest.log.Printf("length of response longer than 2 (%d), ignored", len(q)) 455 | continue 456 | } 457 | c := new(CommitValuePair) 458 | c.Commit, err = hex.DecodeString(q[0].String()) 459 | if err != nil { 460 | rest.log.Printf("Unable to decode commit hash from watch (ignored): %s", q[0].String()) 461 | continue 462 | } 463 | c.Value = q[1] 464 | out <- c 465 | } 466 | } 467 | }() 468 | 469 | return out, err 470 | } 471 | 472 | // WatchPath watches a path recursively. Returns keys that are updated, deleted or created. On error, the last item in the channel 473 | // will have .Error set - the channel is then closed. 474 | func (rest *Conn) WatchPath(path Path, firstCommit []byte) (<-chan *WatchPathCommit, error) { // TODO not path 475 | uri, err := rest.MakeCallURL("watch-rec", path, true) 476 | if err != nil { 477 | return nil, err 478 | } 479 | 480 | var body *postRequest 481 | if firstCommit != nil { 482 | body = new(postRequest) 483 | body.Task = rest.NewTask("Watching db") 484 | s := hex.EncodeToString(firstCommit) 485 | body.Data = json.RawMessage(fmt.Sprintf("[\"%s\"]", s, s)) 486 | } 487 | 488 | var ch <-chan *StreamReply 489 | if ch, err = rest.CallStream(uri, nil); err != nil || ch == nil { 490 | return nil, err 491 | } 492 | 493 | out := make(chan *WatchPathCommit, 1) 494 | 495 | type change struct { 496 | Change string `json:""` 497 | Key Path `json:""` 498 | } 499 | 500 | go func() { 501 | defer close(out) 502 | for m := range ch { 503 | c := new(WatchPathCommit) 504 | 505 | var q [2]json.RawMessage // array of raw messages 506 | if err := json.Unmarshal(m.Result, &q); err != nil { 507 | fmt.Printf("json(0): %s\n", m.Result) 508 | c.Error = err 509 | out <- c 510 | return 511 | } 512 | 513 | var s string // first entry in array is string (commit hash) 514 | if err := json.Unmarshal(q[0], &s); err != nil { 515 | fmt.Printf("json(1): %s\n", q[0]) 516 | c.Error = err 517 | out <- c 518 | return 519 | } 520 | commit, err := hex.DecodeString(s) 521 | if err != nil { 522 | rest.log.Printf("Unable to decode commit hash from watch-rec (ignored): %s", s) 523 | continue 524 | } 525 | 526 | var changes []json.RawMessage // second entry is array of string/path pairs 527 | if err := json.Unmarshal(q[1], &changes); err != nil { 528 | fmt.Printf("json(2): %s\n", q[1]) 529 | c.Error = err 530 | out <- c 531 | return 532 | } 533 | 534 | c.Commit = commit 535 | c.Changes = make([]WatchPathChange, len(changes)) 536 | 537 | for x, pair := range changes { 538 | var k []json.RawMessage // split pair in hash + path 539 | if err := json.Unmarshal(pair, &k); err != nil { 540 | fmt.Printf("json(3): %s\n", pair) 541 | c.Error = err 542 | out <- c 543 | return 544 | } 545 | if len(k) != 2 { 546 | c.Error = fmt.Errorf("Expected string/path pair array of len 2, actual len was %d", len(k)) 547 | out <- c 548 | return 549 | } 550 | 551 | var changetype string 552 | if err := json.Unmarshal(k[0], &changetype); err != nil { 553 | fmt.Printf("json(4): %s\n", k[0]) 554 | c.Error = err 555 | out <- c 556 | return 557 | } 558 | 559 | var key Path 560 | if err := json.Unmarshal(k[1], &key); err != nil { 561 | fmt.Printf("json(5): %s\n", k[1]) 562 | c.Error = err 563 | out <- c 564 | return 565 | } 566 | 567 | c.Changes[x].Change = changetype 568 | c.Changes[x].Key = key 569 | } 570 | 571 | out <- c 572 | } 573 | }() 574 | 575 | return out, err 576 | } 577 | 578 | // Clone the current tree and create a named tag. Force overwrites a previous clone with the same name. 579 | func (rest *Conn) Clone(t Task, name string, force bool) error { 580 | var data cloneReply 581 | 582 | path, err := ParseEncodedPath(url.QueryEscape(name)) // encode and wrap in IrminPath 583 | if err != nil { 584 | return err 585 | } 586 | command := "clone" 587 | if force { 588 | command = "clone-force" 589 | } 590 | 591 | uri, err := rest.MakeCallURL(command, path, true) 592 | if err != nil { 593 | return err 594 | } 595 | 596 | body := postRequest{t, nil} 597 | if err = rest.Call(uri, &body, &data); err != nil { 598 | return err 599 | } 600 | if data.Error.String() != "" { 601 | return fmt.Errorf(data.Error.String()) 602 | } 603 | if len(data.Result) > 1 { 604 | return fmt.Errorf("%s %s returned more than one result", command, name) 605 | } 606 | if (data.Result.String() != "ok") || (data.Result.String() == "" && force) { 607 | return fmt.Errorf(data.Result.String()) 608 | } 609 | 610 | return nil 611 | } 612 | 613 | // CompareAndSet sets a key if the current value is equal to the given value. 614 | func (rest *Conn) CompareAndSet(t Task, path Path, oldcontents *[]byte, contents *[]byte) (string, error) { 615 | var data updateReply 616 | 617 | uri, err := rest.MakeCallURL("compare-and-set", path, true) 618 | if err != nil { 619 | return "", err 620 | } 621 | 622 | var body postRequest 623 | 624 | post := [][]*Value{[]*Value{(*Value)(oldcontents)}, []*Value{(*Value)(contents)}} 625 | 626 | body.Data, err = json.Marshal(&post) 627 | if err != nil { 628 | return "", err 629 | } 630 | 631 | body.Task = t 632 | 633 | if err = rest.Call(uri, &body, &data); err != nil { 634 | return data.Result.String(), err 635 | } 636 | if data.Error.String() != "" { 637 | return "", fmt.Errorf(data.Error.String()) 638 | } 639 | if data.Result.String() == "" { 640 | return "", fmt.Errorf("compare-and-set seemed to succeed, but didn't return a hash", path.String(), data.Result.String()) 641 | } 642 | 643 | return data.Result.String(), nil 644 | } 645 | -------------------------------------------------------------------------------- /irmin/http_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | Copyright (c) 2015 Thomas Leonard 4 | 5 | Permission to use, copy, modify, and distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | package irmin 19 | 20 | import ( 21 | "bufio" 22 | "bytes" 23 | "encoding/hex" 24 | "errors" 25 | "fmt" 26 | "net/url" 27 | "os/exec" 28 | "strings" 29 | "testing" 30 | "time" 31 | ) 32 | 33 | func irminExec(t *testing.T, cmd string, args ...string) error { 34 | fullArgs := []string{cmd, "-s", "http", "--uri", "http://127.0.0.1:8085"} 35 | fullArgs = append(fullArgs, args...) 36 | t.Log("Running irmin %s", fullArgs) 37 | return exec.Command("irmin", fullArgs...).Run() 38 | } 39 | 40 | func spawnIrmin(t *testing.T) *exec.Cmd { 41 | t.Log("Starting Irmin") 42 | c := exec.Command("irmin", "init", "-d", "-v", "-s", "mem", "-a", "http://127.0.0.1:8085") 43 | //c := exec.Command("irmin", "init", "-d", "-v", "--root", "irmin_test", "-a", "http://:8085") 44 | stdout, err := c.StdoutPipe() 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | err = c.Start() 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | fromIrmin := bufio.NewReader(stdout) 53 | if fromIrmin == nil { 54 | t.Fatal(errors.New("NewReader returned nil!")) 55 | } 56 | for { 57 | line, err := fromIrmin.ReadString('\n') 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | // TODO: we really want to know when it is "started", not "starting" 62 | if strings.HasPrefix(line, "Server starting on port") { 63 | break 64 | } 65 | t.Log("Unexpected Irmin output: %#v", line) 66 | } 67 | return c 68 | } 69 | 70 | func stopIrmin(t *testing.T, c *exec.Cmd) { 71 | t.Log("Stopping irmin") 72 | err := c.Process.Kill() 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | err = c.Wait() 77 | if err != nil { 78 | t.Log(err) 79 | } 80 | } 81 | 82 | func TestIrminConnect(t *testing.T) { 83 | // Start Irmin 84 | irmin := spawnIrmin(t) 85 | // Stop Irmin 86 | defer stopIrmin(t, irmin) 87 | } 88 | 89 | func getConn(t *testing.T) *Conn { 90 | uri, err := url.Parse("http://127.0.0.1:8085") 91 | t.Log("Connecting to irmin @ ", uri) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | return Create(uri, "irmin-go-tester") 96 | } 97 | 98 | func TestHead(t *testing.T) { 99 | irmin := spawnIrmin(t) 100 | defer stopIrmin(t, irmin) 101 | 102 | t.Log("Testing Head on empty db") 103 | 104 | r := getConn(t) 105 | 106 | v, err := r.Head() 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | if v != nil { 111 | t.Fatal("head should be nil, but was %s\n", hex.EncodeToString(v)) 112 | } 113 | 114 | t.Log("Testing Head on non-empty db") 115 | 116 | key := "head-test" 117 | data := []byte("foo") 118 | hash, err := r.Update(r.NewTask("update key"), ParsePath(key), data) 119 | if err != nil { 120 | t.Fatal(err) 121 | } 122 | t.Logf("update hash is: %s\n", hash) 123 | 124 | v, err = r.Head() 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | h := hex.EncodeToString(v) 129 | t.Logf("head is: %s\n", h) 130 | 131 | if h != hash { 132 | t.Fatal("Hash returned by update did not match head (%s vs %s)", h, hash) 133 | } 134 | 135 | } 136 | 137 | func TestUpdate(t *testing.T) { 138 | irmin := spawnIrmin(t) 139 | defer stopIrmin(t, irmin) 140 | 141 | r := getConn(t) 142 | key := "update-test" 143 | t.Logf("update key '%s'", key) 144 | data := []byte("Hello \"world") 145 | hash, err := r.Update(r.NewTask("update key"), ParsePath(key), data) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | t.Logf("update hash: %s\n", hash) 150 | t.Logf("read key `%s`", key) 151 | d, err := r.ReadString(ParsePath(key)) 152 | if err != nil { 153 | t.Fatal(err) 154 | } 155 | if bytes.Compare(data, []byte(d)) != 0 { 156 | t.Fatal("update/read failed. Written value '%s' != '%s'", string(data), d) 157 | } 158 | } 159 | 160 | func TestWatch(t *testing.T) { 161 | irmin := spawnIrmin(t) 162 | defer stopIrmin(t, irmin) 163 | 164 | // Connect to Irmin 165 | r := getConn(t) 166 | 167 | // expect function waits for expected result with a timeout 168 | expect := func(ch <-chan *CommitValuePair, path Path, val []byte) { 169 | // Wait for Watch result or timeout 170 | timeout := time.After(1 * time.Second) 171 | select { 172 | case v := <-ch: 173 | if bytes.Compare(v.Value, val) != 0 { 174 | t.Fatalf("Watch result did not contain expected value (expected %s, got %s)", string(val), string(v.Value)) 175 | } else { 176 | d, err := r.Read(path) // read path to verify content 177 | if err != nil { 178 | t.Fatal(err) 179 | } 180 | if bytes.Compare(val, d) != 0 { 181 | t.Fatalf("Update and Watch succeeded, but Read returned old value (was '%s', should be '%s')", string(d), string(val)) 182 | } 183 | } 184 | case <-timeout: 185 | t.Fatal("Timed out while waiting for Watch result") 186 | } 187 | } 188 | 189 | // Test watching an existing key 190 | { 191 | path := ParsePath("/watch-test/1") 192 | data := []byte("foo") 193 | t.Logf("update key '%s'='%s'", path.String(), string(data)) 194 | hash, err := r.Update(r.NewTask("update key"), path, data) 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | t.Logf("update hash is %s", hash) 199 | 200 | t.Logf("Set Watch on existing key %s", path.String()) 201 | ch, err := r.Watch(path, nil) 202 | if err != nil { 203 | t.Fatal(err) 204 | } 205 | 206 | data = []byte("bar") 207 | t.Logf("update key '%s'='%s'", path.String(), string(data)) 208 | hash, err = r.Update(r.NewTask("update key"), path, data) 209 | if err != nil { 210 | t.Fatal(err) 211 | } 212 | t.Logf("update hash is %s", hash) 213 | 214 | expect(ch, path, data) 215 | } 216 | 217 | // Test watching a non-existing key 218 | { 219 | path := ParsePath("/watch-test/2") 220 | t.Logf("Set Watch on non-existing key %s", path.String()) 221 | ch, err := r.Watch(path, nil) 222 | if err != nil { 223 | t.Fatal(err) 224 | } 225 | 226 | data := []byte("barbar") 227 | t.Logf("update key '%s'='%s'", path.String(), string(data)) 228 | hash, err := r.Update(r.NewTask("update key"), path, data) 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | t.Logf("update hash is %s", hash) 233 | 234 | expect(ch, path, data) 235 | } 236 | 237 | // Test multiple updates 238 | { 239 | path := ParsePath("/watch-test/3") 240 | t.Logf("Set Watch for multiple updates on key %s", path.String()) 241 | ch, err := r.Watch(path, nil) 242 | if err != nil { 243 | t.Fatal(err) 244 | } 245 | 246 | data := []byte("foo") 247 | t.Logf("update key '%s'='%s'", path.String(), string(data)) 248 | hash, err := r.Update(r.NewTask("update key"), path, data) 249 | if err != nil { 250 | t.Fatal(err) 251 | } 252 | t.Logf("update hash is %s", hash) 253 | 254 | expect(ch, path, data) 255 | 256 | for i := 0; i < 4; i++ { 257 | data := []byte(fmt.Sprintf("bar %d", i)) 258 | t.Logf("update key '%s'='%s'", path.String(), string(data)) 259 | hash, err = r.Update(r.NewTask("update key"), path, data) 260 | if err != nil { 261 | t.Fatal(err) 262 | } 263 | t.Logf("update hash is %s", hash) 264 | expect(ch, path, data) 265 | time.Sleep(250 * time.Millisecond) 266 | } 267 | } 268 | 269 | // Test multiple updates, no delay 270 | { 271 | path := ParsePath("/watch-test/4") 272 | t.Logf("Set Watch for multiple updates on key %s (no delay)", path.String()) 273 | ch, err := r.Watch(path, nil) 274 | if err != nil { 275 | t.Fatal(err) 276 | } 277 | 278 | data := []byte("foo") 279 | t.Logf("update key '%s'='%s'", path.String(), string(data)) 280 | hash, err := r.Update(r.NewTask("update key"), path, data) 281 | if err != nil { 282 | t.Fatal(err) 283 | } 284 | t.Logf("update hash is %s", hash) 285 | 286 | expect(ch, path, data) 287 | 288 | t.Log("Trigger watch 1000 times") 289 | for i := 0; i < 1000; i++ { 290 | data := []byte(fmt.Sprintf("bar %d", i)) 291 | hash, err = r.Update(r.NewTask("update key"), path, data) 292 | if err != nil { 293 | t.Fatal(err) 294 | } 295 | expect(ch, path, data) 296 | } 297 | t.Log("done") 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /irmin/log.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package irmin 18 | 19 | // Log is a generic log interface that implements a Printf function 20 | type Log interface { 21 | Printf(format string, args ...interface{}) 22 | } 23 | 24 | // IgnoreLog is an implementation of Log that ignores all log messages 25 | type IgnoreLog struct{} 26 | 27 | // Printf implementation that ignores its input and returns 28 | func (i IgnoreLog) Printf(format string, args ...interface{}) { 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /irmin/path.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package irmin 18 | 19 | import ( 20 | "bytes" 21 | "net/url" 22 | "strings" 23 | ) 24 | 25 | // Path is a path in an Irmin tree 26 | type Path []Value 27 | 28 | // Delim returns the default path delimiter. Always '/' for now. 29 | func (path *Path) Delim() rune { 30 | return '/' 31 | } 32 | 33 | // ParseEncodedPath parses a path string separated by '/'. Each segment may be PCT encoded to escape '/' in the name. (see also url.QueryEscape) 34 | func ParseEncodedPath(p string) (Path, error) { 35 | // TODO use delim() here 36 | segs := strings.Split(strings.Trim(p, " /"), "/") 37 | is := make([]Value, len(segs)) 38 | for i := range segs { 39 | s, err := url.QueryUnescape(segs[i]) 40 | if err != nil { 41 | return Path{}, err 42 | } 43 | is[i] = []byte(s) 44 | } 45 | 46 | return is, nil 47 | } 48 | 49 | // ParsePath parses a path string separated by '/'. 50 | func ParsePath(p string) Path { 51 | // TODO use delim() here 52 | segs := strings.Split(strings.Trim(p, " /"), "/") 53 | is := make([]Value, len(segs)) 54 | for i := range segs { 55 | is[i] = []byte(segs[i]) 56 | } 57 | return is 58 | } 59 | 60 | // String representation of a Path 61 | func (path *Path) String() string { 62 | if len(*path) > 0 { 63 | var buf bytes.Buffer 64 | for _, v := range *path { 65 | buf.WriteRune(path.Delim()) 66 | buf.Write(v) 67 | } 68 | return buf.String() 69 | } 70 | return "" 71 | 72 | } 73 | 74 | // URL returns relative URL representation of a Path 75 | func (path *Path) URL() *url.URL { 76 | if len(*path) > 0 { 77 | var buf bytes.Buffer 78 | for _, v := range *path { 79 | buf.WriteRune(path.Delim()) 80 | buf.WriteString(url.QueryEscape(v.String())) 81 | } 82 | if u, err := url.Parse(buf.String()); err != nil { 83 | panic(err) // this should never happen 84 | } else { 85 | return u 86 | } 87 | } else { 88 | return new(url.URL) 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /irmin/value.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package irmin 18 | 19 | import ( 20 | "encoding/hex" 21 | "encoding/json" 22 | "fmt" 23 | "unicode/utf8" 24 | ) 25 | 26 | // Value contains a value read from Irmin 27 | type Value []byte 28 | 29 | // NewValue creates a new Value from a string 30 | func NewValue(s string) Value { 31 | return []byte(s) 32 | } 33 | 34 | // String returns the string representation of a value 35 | func (i *Value) String() string { 36 | return string(*i) 37 | } 38 | 39 | // MarshalJSON returns a JSON encoded value. If the value is valid UTF-8 it will be encoded as a string, otherwise it will be encoded as a list of hex values. 40 | func (i *Value) MarshalJSON() ([]byte, error) { 41 | if utf8.Valid(*i) { 42 | b, err := json.Marshal(string(*i)) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return []byte(fmt.Sprintf("%s", b)), nil /* output as string if valid utf8 */ 47 | } 48 | return []byte(fmt.Sprintf("{ \"hex\" : \"%x\" }", *i)), nil /* if not valid, output in hex format */ 49 | } 50 | 51 | // UnmarshalJSON unmarshals a JSON encoded value 52 | func (i *Value) UnmarshalJSON(b []byte) error { 53 | type IrminHex struct { /* only used internally */ 54 | Hex string 55 | } 56 | var h IrminHex 57 | var s string 58 | var err error 59 | if err = json.Unmarshal(b, &s); err == nil { /* data as string */ 60 | if utf8.ValidString(s) { 61 | *i = []byte(s) 62 | } else { 63 | err = fmt.Errorf("string not valid utf8: %s", s) 64 | } 65 | } else { 66 | if err = json.Unmarshal(b, &h); err == nil { /* try to parse as hex */ 67 | *i, err = hex.DecodeString(h.Hex) 68 | } 69 | } 70 | return err 71 | } 72 | -------------------------------------------------------------------------------- /irmin/views.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 Magnus Skjegstad 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package irmin 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "net/url" 23 | "strings" 24 | "unicode/utf8" 25 | ) 26 | 27 | // View describes a transaction/view in Irmin 28 | type View struct { 29 | srv *Conn 30 | head string 31 | node string 32 | path Path 33 | } 34 | 35 | type createViewReply stringReply 36 | type viewReadReply stringReply 37 | type viewMergeReply stringReply 38 | type viewUpdateReply updateReply 39 | 40 | // CreateView creates a new view (transaction) in Irmin relative to the given path 41 | func (rest *Conn) CreateView(t Task, path Path) (*View, error) { 42 | 43 | var data createViewReply 44 | 45 | var body postRequest 46 | body.Data = nil 47 | body.Task = t 48 | 49 | // TODO Rename command to /create when https://github.com/mirage/irmin/issues/294 is fixed 50 | uri, err := rest.MakeCallURL("view/create/create", path, true) 51 | if err != nil { 52 | return nil, err 53 | } 54 | err = rest.Call(uri, &body, &data) 55 | if err != nil { 56 | return nil, err 57 | } 58 | if data.Error.String() != "" { 59 | return nil, fmt.Errorf(data.Error.String()) 60 | } 61 | if data.Result.String() == "" { 62 | return nil, fmt.Errorf("empty result") 63 | } 64 | // TODO Simplify parsing if https://github.com/mirage/irmin/issues/295 is fixed 65 | r := strings.Split(data.Result.String(), "-") // Just basic error checking here, hashes not checked for errors 66 | if len(r) != 2 { 67 | return nil, fmt.Errorf("invalid result: %s", data.Result.String()) 68 | } 69 | 70 | v := new(View) 71 | v.srv = rest 72 | v.head = r[0] 73 | v.node = r[1] 74 | v.path = path 75 | return v, nil 76 | } 77 | 78 | // Path returns the original path the view was created from 79 | func (view *View) Path() Path { 80 | return view.path 81 | } 82 | 83 | // Tree returns the position the view was created from. An empty tree value will default to the master branch. 84 | func (view *View) Tree() string { 85 | return view.srv.Tree() 86 | } 87 | 88 | // Read a value from a view 89 | func (view *View) Read(path Path) ([]byte, error) { 90 | var data viewReadReply 91 | var err error 92 | cmd := fmt.Sprintf("view/%s/read", url.QueryEscape(view.node)) 93 | uri, err := view.srv.MakeCallURL(cmd, path, false) 94 | if err != nil { 95 | return nil, err 96 | } 97 | if err = view.srv.Call(uri, nil, &data); err != nil { 98 | return []byte{}, err 99 | } 100 | if data.Error.String() != "" { 101 | return []byte{}, fmt.Errorf(data.Error.String()) 102 | } 103 | return data.Result, nil 104 | } 105 | 106 | // ReadString reads a value and converts it into a string. If the value is not valid utf8 an error is returned. 107 | func (view *View) ReadString(path Path) (string, error) { 108 | // TODO This code duplicates functionality from rest.ReadString 109 | res, err := view.Read(path) 110 | if err != nil { 111 | return "", err 112 | } 113 | if utf8.Valid(res) { 114 | return string(res), nil 115 | } 116 | return "", fmt.Errorf("path %s does not contain a valid utf8 string", path.String()) 117 | } 118 | 119 | // Update a key. Returns hash as string on success. 120 | func (view *View) Update(t Task, path Path, contents []byte) (string, error) { 121 | var data viewUpdateReply 122 | var err error 123 | 124 | var body postRequest 125 | i := Value(contents) 126 | 127 | body.Data, err = i.MarshalJSON() 128 | if err != nil { 129 | return "", err 130 | } 131 | 132 | body.Task = t 133 | 134 | cmd := fmt.Sprintf("view/%s/update", url.QueryEscape(view.node)) 135 | uri, err := view.srv.MakeCallURL(cmd, path, false) 136 | if err != nil { 137 | return "", err 138 | } 139 | if err = view.srv.Call(uri, &body, &data); err != nil { 140 | return data.Result.String(), err 141 | } 142 | if data.Error.String() != "" { 143 | return "", fmt.Errorf(data.Error.String()) 144 | } 145 | if data.Result.String() == "" { 146 | return "", fmt.Errorf("update seemed to succeed, but didn't return a hash", path.String(), data.Result.String()) 147 | } 148 | 149 | view.node = data.Result.String() // Store new node position 150 | 151 | return view.node, nil 152 | } 153 | 154 | // MergePath will attempt to merge view into the specified branch and path. An empty tree value defaults to master. 155 | func (view *View) MergePath(t Task, tree string, path Path) error { 156 | var data viewMergeReply 157 | var err error 158 | 159 | var body postRequest 160 | i := Value([]byte(view.head)) // body contains head 161 | 162 | body.Data, err = i.MarshalJSON() 163 | if err != nil { 164 | return err 165 | } 166 | 167 | body.Task = t 168 | 169 | cmd := fmt.Sprintf("tree/%s/view/%s/merge-path", url.QueryEscape(tree), url.QueryEscape(view.node)) 170 | uri, err := view.srv.MakeCallURL(cmd, path, false) 171 | if err != nil { 172 | return err 173 | } 174 | if err = view.srv.Call(uri, &body, &data); err != nil { 175 | return err 176 | } 177 | if data.Error.String() != "" { 178 | return fmt.Errorf(data.Error.String()) 179 | } 180 | // TODO Assumes succses if no error, should probably check result 181 | 182 | return nil 183 | } 184 | 185 | // UpdatePath writes the view into the specified tree and path. Overwrites existing values. 186 | func (view *View) UpdatePath(t Task, tree string, path Path) error { 187 | var data viewUpdateReply 188 | var err error 189 | 190 | body := postRequest{t, nil} 191 | 192 | cmd := fmt.Sprintf("tree/%s/view/%s/update-path", url.QueryEscape(tree), url.QueryEscape(view.node)) 193 | uri, err := view.srv.MakeCallURL(cmd, path, false) 194 | if err != nil { 195 | return err 196 | } 197 | if err = view.srv.Call(uri, &body, &data); err != nil { 198 | return err 199 | } 200 | if data.Error.String() != "" { 201 | return fmt.Errorf(data.Error.String()) 202 | } 203 | if data.Result.String() == "" { 204 | return fmt.Errorf("update-path seemed to succeed, but didn't return a hash", path.String(), data.Result.String()) 205 | } 206 | 207 | return nil 208 | } 209 | 210 | // Iter iterates through all keys in a view. Returns results in a channel as they are received. 211 | func (view *View) Iter() (<-chan *Path, error) { 212 | var ch <-chan *StreamReply 213 | var err error 214 | cmd := fmt.Sprintf("view/%s/iter", url.QueryEscape(view.node)) 215 | uri, err := view.srv.MakeCallURL(cmd, Path{}, false) 216 | if err != nil { 217 | return nil, err 218 | } 219 | if ch, err = view.srv.CallStream(uri, nil); err != nil || ch == nil { 220 | return nil, err 221 | } 222 | 223 | out := make(chan *Path, 1) 224 | 225 | go func() { 226 | defer close(out) 227 | for m := range ch { 228 | p := new(Path) 229 | if err := json.Unmarshal(m.Result, &p); err != nil { 230 | panic(err) // TODO This should be returned to caller 231 | } 232 | out <- p 233 | } 234 | }() 235 | 236 | return out, err 237 | } 238 | 239 | // NewTask creates a new task that can be be submitted with a command. This is used as the commit message by Irmin. 240 | func (view *View) NewTask(message string) Task { 241 | return NewTask(view.srv.taskowner, message) 242 | } 243 | --------------------------------------------------------------------------------