From 75d69639e6e40fdb6f0b3f32b678e8e3217f61c6 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 20 Feb 2016 22:55:43 +0100 Subject: [PATCH 1/7] .gitignore: Add /vendor/pkg --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0310bdf7a..dfc8d1622 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /bin /restic /.vagrant +/vendor/pkg From c2348ba768fc10fd6aaba1fd294892a1361e0ade Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 20 Feb 2016 22:05:48 +0100 Subject: [PATCH 2/7] Add REST backend This is a port of the original work by @bchapuis in https://github.com/restic/restic/pull/253 --- doc/REST_backend.md | 56 +++++ src/restic/backend/rest/backend_test.go | 87 ++++++++ src/restic/backend/rest/config.go | 29 +++ src/restic/backend/rest/config_test.go | 41 ++++ src/restic/backend/rest/rest.go | 258 ++++++++++++++++++++++++ src/restic/backend/rest/rest_test.go | 54 +++++ src/restic/test/backend.go | 1 + 7 files changed, 526 insertions(+) create mode 100644 doc/REST_backend.md create mode 100644 src/restic/backend/rest/backend_test.go create mode 100644 src/restic/backend/rest/config.go create mode 100644 src/restic/backend/rest/config_test.go create mode 100644 src/restic/backend/rest/rest.go create mode 100644 src/restic/backend/rest/rest_test.go diff --git a/doc/REST_backend.md b/doc/REST_backend.md new file mode 100644 index 000000000..2f793c58a --- /dev/null +++ b/doc/REST_backend.md @@ -0,0 +1,56 @@ +REST Backend +============ + +Restic can interact with HTTP Backend that respects the following REST API. + +## HEAD /config + +Returns "200 OK" if the repository has a configuration, +an HTTP error otherwise. + +## GET /config + +Returns the content of the configuration file if the repository has a configuration, +an HTTP error otherwise. + +Response format: binary/octet-stream + +## POST /config + +Returns "200 OK" if the configuration of the request body has been saved, +an HTTP error otherwise. + +## GET /{type}/ + +Returns a JSON array containing the names of all the blobs stored for a given type. + +Response format: JSON + +## HEAD /{type}/{name} + +Returns "200 OK" if the blob with the given name and type is stored in the repository, +"404 not found" otherwise. If the blob exists, the HTTP header `Content-Length` +is set to the file size. + +## GET /{type}/{name} + +Returns the content of the blob with the given name and type if it is stored in the repository, +"404 not found" otherwise. + +If the request specifies a partial read with a Range header field, +then the status code of the response is 206 instead of 200 +and the response only contains the specified range. + +Response format: binary/octet-stream + +## POST /{type}/{name} + +Saves the content of the request body as a blob with the given name and type, +an HTTP error otherwise. + +Request format: binary/octet-stream + +## DELETE /{type}/{name} + +Returns "200 OK" if the blob with the given name and type has been deleted from the repository, +an HTTP error otherwise. diff --git a/src/restic/backend/rest/backend_test.go b/src/restic/backend/rest/backend_test.go new file mode 100644 index 000000000..4274bfcb1 --- /dev/null +++ b/src/restic/backend/rest/backend_test.go @@ -0,0 +1,87 @@ +// DO NOT EDIT, AUTOMATICALLY GENERATED +package rest_test + +import ( + "testing" + + "restic/backend/test" +) + +var SkipMessage string + +func TestRestBackendCreate(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestCreate(t) +} + +func TestRestBackendOpen(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestOpen(t) +} + +func TestRestBackendCreateWithConfig(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestCreateWithConfig(t) +} + +func TestRestBackendLocation(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestLocation(t) +} + +func TestRestBackendConfig(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestConfig(t) +} + +func TestRestBackendLoad(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestLoad(t) +} + +func TestRestBackendSave(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestSave(t) +} + +func TestRestBackendSaveFilenames(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestSaveFilenames(t) +} + +func TestRestBackendBackend(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestBackend(t) +} + +func TestRestBackendDelete(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestDelete(t) +} + +func TestRestBackendCleanup(t *testing.T) { + if SkipMessage != "" { + t.Skip(SkipMessage) + } + test.TestCleanup(t) +} diff --git a/src/restic/backend/rest/config.go b/src/restic/backend/rest/config.go new file mode 100644 index 000000000..c4459344c --- /dev/null +++ b/src/restic/backend/rest/config.go @@ -0,0 +1,29 @@ +package rest + +import ( + "errors" + "net/url" + "strings" +) + +// Config contains all configuration necessary to connect to a REST server. +type Config struct { + URL *url.URL +} + +// ParseConfig parses the string s and extracts the REST server URL. +func ParseConfig(s string) (interface{}, error) { + if !strings.HasPrefix(s, "rest:") { + return nil, errors.New("invalid REST backend specification") + } + + s = s[5:] + u, err := url.Parse(s) + + if err != nil { + return nil, err + } + + cfg := Config{URL: u} + return cfg, nil +} diff --git a/src/restic/backend/rest/config_test.go b/src/restic/backend/rest/config_test.go new file mode 100644 index 000000000..937204a57 --- /dev/null +++ b/src/restic/backend/rest/config_test.go @@ -0,0 +1,41 @@ +package rest + +import ( + "net/url" + "reflect" + "testing" +) + +func parseURL(s string) *url.URL { + u, err := url.Parse(s) + if err != nil { + panic(err) + } + + return u +} + +var configTests = []struct { + s string + cfg Config +}{ + {"rest:http://localhost:1234", Config{ + URL: parseURL("http://localhost:1234"), + }}, +} + +func TestParseConfig(t *testing.T) { + for i, test := range configTests { + cfg, err := ParseConfig(test.s) + if err != nil { + t.Errorf("test %d:%s failed: %v", i, test.s, err) + continue + } + + if !reflect.DeepEqual(cfg, test.cfg) { + t.Errorf("test %d:\ninput:\n %s\n wrong config, want:\n %v\ngot:\n %v", + i, test.s, test.cfg, cfg) + continue + } + } +} diff --git a/src/restic/backend/rest/rest.go b/src/restic/backend/rest/rest.go new file mode 100644 index 000000000..7cfa3c84d --- /dev/null +++ b/src/restic/backend/rest/rest.go @@ -0,0 +1,258 @@ +package rest + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "path" + + "restic/backend" +) + +const connLimit = 10 + +// restPath returns the path to the given resource. +func restPath(url *url.URL, h backend.Handle) string { + p := url.Path + if p == "" { + p = "/" + } + + var dir string + + switch h.Type { + case backend.Config: + dir = "" + case backend.Data: + dir = backend.Paths.Data + case backend.Snapshot: + dir = backend.Paths.Snapshots + case backend.Index: + dir = backend.Paths.Index + case backend.Lock: + dir = backend.Paths.Locks + case backend.Key: + dir = backend.Paths.Keys + default: + dir = string(h.Type) + } + + return path.Join(p, dir, h.Name) +} + +type restBackend struct { + url *url.URL + connChan chan struct{} + client *http.Client +} + +// Open opens the REST backend with the given config. +func Open(cfg Config) (backend.Backend, error) { + connChan := make(chan struct{}, connLimit) + for i := 0; i < connLimit; i++ { + connChan <- struct{}{} + } + tr := &http.Transport{} + client := http.Client{Transport: tr} + + return &restBackend{url: cfg.URL, connChan: connChan, client: &client}, nil +} + +// Location returns this backend's location (the server's URL). +func (b *restBackend) Location() string { + return b.url.String() +} + +// Load returns the data stored in the backend for h at the given offset +// and saves it in p. Load has the same semantics as io.ReaderAt. +func (b *restBackend) Load(h backend.Handle, p []byte, off int64) (n int, err error) { + if err := h.Valid(); err != nil { + return 0, err + } + + req, err := http.NewRequest("GET", restPath(b.url, h), nil) + if err != nil { + return 0, err + } + req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", off, off+int64(len(p)))) + client := *b.client + + <-b.connChan + resp, err := client.Do(req) + b.connChan <- struct{}{} + + if resp != nil { + defer func() { + e := resp.Body.Close() + + if err == nil { + err = e + } + }() + } + + if err != nil { + return 0, err + } + if resp.StatusCode != 206 { + return 0, errors.New("blob not found") + } + + return io.ReadFull(resp.Body, p) +} + +// Save stores data in the backend at the handle. +func (b *restBackend) Save(h backend.Handle, p []byte) (err error) { + if err := h.Valid(); err != nil { + return err + } + + client := *b.client + + <-b.connChan + resp, err := client.Post(restPath(b.url, h), "binary/octet-stream", bytes.NewReader(p)) + b.connChan <- struct{}{} + + if resp != nil { + defer func() { + e := resp.Body.Close() + + if err == nil { + err = e + } + }() + } + + if err != nil { + return err + } + + if resp.StatusCode != 200 { + return errors.New("blob not saved") + } + + return nil +} + +// Stat returns information about a blob. +func (b *restBackend) Stat(h backend.Handle) (backend.BlobInfo, error) { + if err := h.Valid(); err != nil { + return backend.BlobInfo{}, err + } + + client := *b.client + <-b.connChan + resp, err := client.Head(restPath(b.url, h)) + b.connChan <- struct{}{} + if err != nil { + return backend.BlobInfo{}, err + } + + if err = resp.Body.Close(); err != nil { + return backend.BlobInfo{}, err + } + + if resp.StatusCode != 200 { + return backend.BlobInfo{}, errors.New("blob not saved") + } + + if resp.ContentLength < 0 { + return backend.BlobInfo{}, errors.New("negative content length") + } + + bi := backend.BlobInfo{ + Size: resp.ContentLength, + } + + return bi, nil +} + +// Test returns true if a blob of the given type and name exists in the backend. +func (b *restBackend) Test(t backend.Type, name string) (bool, error) { + _, err := b.Stat(backend.Handle{Type: t, Name: name}) + if err != nil { + return false, nil + } + + return true, nil +} + +// Remove removes the blob with the given name and type. +func (b *restBackend) Remove(t backend.Type, name string) error { + h := backend.Handle{Type: t, Name: name} + if err := h.Valid(); err != nil { + return err + } + + req, err := http.NewRequest("DELETE", restPath(b.url, h), nil) + if err != nil { + return err + } + client := *b.client + + <-b.connChan + resp, err := client.Do(req) + b.connChan <- struct{}{} + + if err != nil { + return err + } + + if resp.StatusCode != 200 { + return errors.New("blob not removed") + } + + return resp.Body.Close() +} + +// List returns a channel that yields all names of blobs of type t. A +// goroutine is started for this. If the channel done is closed, sending +// stops. +func (b *restBackend) List(t backend.Type, done <-chan struct{}) <-chan string { + ch := make(chan string) + + client := *b.client + <-b.connChan + resp, err := client.Get(restPath(b.url, backend.Handle{Type: t})) + b.connChan <- struct{}{} + + if resp != nil { + defer resp.Body.Close() + } + + if err != nil { + close(ch) + return ch + } + + dec := json.NewDecoder(resp.Body) + var list []string + if err = dec.Decode(&list); err != nil { + close(ch) + return ch + } + + go func() { + defer close(ch) + for _, m := range list { + select { + case ch <- m: + case <-done: + return + } + } + }() + + return ch +} + +// Close closes all open files. +func (b *restBackend) Close() error { + // this does not need to do anything, all open files are closed within the + // same function. + return nil +} diff --git a/src/restic/backend/rest/rest_test.go b/src/restic/backend/rest/rest_test.go new file mode 100644 index 000000000..bbcfff9b0 --- /dev/null +++ b/src/restic/backend/rest/rest_test.go @@ -0,0 +1,54 @@ +package rest_test + +import ( + "errors" + "fmt" + "net/url" + "os" + + "restic/backend" + "restic/backend/rest" + "restic/backend/test" + . "restic/test" +) + +//go:generate go run ../test/generate_backend_tests.go + +func init() { + if TestRESTServer == "" { + SkipMessage = "REST test server not available" + return + } + + url, err := url.Parse(TestRESTServer) + if err != nil { + fmt.Fprintf(os.Stderr, "invalid url: %v\n", err) + return + } + + cfg := rest.Config{ + URL: url, + } + + test.CreateFn = func() (backend.Backend, error) { + be, err := rest.Open(cfg) + if err != nil { + return nil, err + } + + exists, err := be.Test(backend.Config, "") + if err != nil { + return nil, err + } + + if exists { + return nil, errors.New("config already exists") + } + + return be, nil + } + + test.OpenFn = func() (backend.Backend, error) { + return rest.Open(cfg) + } +} diff --git a/src/restic/test/backend.go b/src/restic/test/backend.go index ca9ee45b0..5516cecdf 100644 --- a/src/restic/test/backend.go +++ b/src/restic/test/backend.go @@ -23,6 +23,7 @@ var ( TestWalkerPath = getStringVar("RESTIC_TEST_PATH", ".") BenchArchiveDirectory = getStringVar("RESTIC_BENCH_DIR", ".") TestS3Server = getStringVar("RESTIC_TEST_S3_SERVER", "") + TestRESTServer = getStringVar("RESTIC_TEST_REST_SERVER", "") ) func getStringVar(name, defaultValue string) string { From ec34da2d6604ed662f0163402d843649b5a1c431 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Feb 2016 15:24:37 +0100 Subject: [PATCH 3/7] Add rest backend to location --- src/restic/location/location.go | 2 ++ src/restic/location/location_test.go | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/restic/location/location.go b/src/restic/location/location.go index 8e4a8f1e2..23e0af37b 100644 --- a/src/restic/location/location.go +++ b/src/restic/location/location.go @@ -5,6 +5,7 @@ import ( "strings" "restic/backend/local" + "restic/backend/rest" "restic/backend/s3" "restic/backend/sftp" ) @@ -27,6 +28,7 @@ var parsers = []parser{ {"local", local.ParseConfig}, {"sftp", sftp.ParseConfig}, {"s3", s3.ParseConfig}, + {"rest", rest.ParseConfig}, } // Parse extracts repository location information from the string s. If s diff --git a/src/restic/location/location_test.go b/src/restic/location/location_test.go index 8d9046348..bb4ac64c9 100644 --- a/src/restic/location/location_test.go +++ b/src/restic/location/location_test.go @@ -1,13 +1,24 @@ package location import ( + "net/url" "reflect" "testing" + "restic/backend/rest" "restic/backend/s3" "restic/backend/sftp" ) +func parseURL(s string) *url.URL { + u, err := url.Parse(s) + if err != nil { + panic(err) + } + + return u +} + var parseTests = []struct { s string u Location @@ -101,6 +112,11 @@ var parseTests = []struct { UseHTTP: true, }}, }, + {"rest:http://hostname.foo:1234/", Location{Scheme: "rest", + Config: rest.Config{ + URL: parseURL("http://hostname.foo:1234/"), + }}, + }, } func TestParse(t *testing.T) { From bd621197f8c1e88fb62bb7b2c2fd9e644753df50 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Feb 2016 15:24:46 +0100 Subject: [PATCH 4/7] Add rest backend to ui parser --- src/restic/cmd/restic/global.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/restic/cmd/restic/global.go b/src/restic/cmd/restic/global.go index b86138f29..49a2ace30 100644 --- a/src/restic/cmd/restic/global.go +++ b/src/restic/cmd/restic/global.go @@ -8,15 +8,17 @@ import ( "strings" "syscall" - "github.com/jessevdk/go-flags" - "golang.org/x/crypto/ssh/terminal" "restic/backend" "restic/backend/local" + "restic/backend/rest" "restic/backend/s3" "restic/backend/sftp" "restic/debug" "restic/location" "restic/repository" + + "github.com/jessevdk/go-flags" + "golang.org/x/crypto/ssh/terminal" ) var version = "compiled manually" @@ -247,6 +249,8 @@ func open(s string) (backend.Backend, error) { debug.Log("open", "opening s3 repository at %#v", cfg) return s3.Open(cfg) + case "rest": + return rest.Open(loc.Config.(rest.Config)) } debug.Log("open", "invalid repository location: %v", s) @@ -280,6 +284,8 @@ func create(s string) (backend.Backend, error) { debug.Log("open", "create s3 repository at %#v", loc.Config) return s3.Open(cfg) + case "rest": + return rest.Open(loc.Config.(rest.Config)) } debug.Log("open", "invalid repository scheme: %v", s) From f7a10a9b9c93d43d1b821d3860cf01cb54ea5409 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Feb 2016 16:02:13 +0100 Subject: [PATCH 5/7] backend tests: Test accessing config This commit adds real testing for accessing the config file with different names. --- src/restic/backend/test/tests.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/restic/backend/test/tests.go b/src/restic/backend/test/tests.go index 6313f39f6..60c35ffb0 100644 --- a/src/restic/backend/test/tests.go +++ b/src/restic/backend/test/tests.go @@ -164,7 +164,8 @@ func TestConfig(t testing.TB) { // try accessing the config with different names, should all return the // same config for _, name := range []string{"", "foo", "bar", "0000000000000000000000000000000000000000000000000000000000000000"} { - buf, err := backend.LoadAll(b, backend.Handle{Type: backend.Config}, nil) + h := backend.Handle{Type: backend.Config, Name: name} + buf, err := backend.LoadAll(b, h, nil) if err != nil { t.Fatalf("unable to read config with name %q: %v", name, err) } From 8ad98e8040fdcbb382ff7feafc0981ff541de182 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Feb 2016 16:35:25 +0100 Subject: [PATCH 6/7] rest backend: Fixes --- src/restic/backend/rest/rest.go | 26 +++++++----- src/restic/backend/rest/rest_path_test.go | 48 +++++++++++++++++++++++ 2 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 src/restic/backend/rest/rest_path_test.go diff --git a/src/restic/backend/rest/rest.go b/src/restic/backend/rest/rest.go index 7cfa3c84d..215b1c95f 100644 --- a/src/restic/backend/rest/rest.go +++ b/src/restic/backend/rest/rest.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "path" + "strings" "restic/backend" ) @@ -17,16 +18,14 @@ const connLimit = 10 // restPath returns the path to the given resource. func restPath(url *url.URL, h backend.Handle) string { - p := url.Path - if p == "" { - p = "/" - } + u := *url var dir string switch h.Type { case backend.Config: dir = "" + h.Name = "config" case backend.Data: dir = backend.Paths.Data case backend.Snapshot: @@ -41,7 +40,9 @@ func restPath(url *url.URL, h backend.Handle) string { dir = string(h.Type) } - return path.Join(p, dir, h.Name) + u.Path = path.Join(url.Path, dir, h.Name) + + return u.String() } type restBackend struct { @@ -98,8 +99,8 @@ func (b *restBackend) Load(h backend.Handle, p []byte, off int64) (n int, err er if err != nil { return 0, err } - if resp.StatusCode != 206 { - return 0, errors.New("blob not found") + if resp.StatusCode != 200 && resp.StatusCode != 206 { + return 0, fmt.Errorf("unexpected HTTP response code %v", resp.StatusCode) } return io.ReadFull(resp.Body, p) @@ -132,7 +133,7 @@ func (b *restBackend) Save(h backend.Handle, p []byte) (err error) { } if resp.StatusCode != 200 { - return errors.New("blob not saved") + return fmt.Errorf("unexpected HTTP response code %v", resp.StatusCode) } return nil @@ -157,7 +158,7 @@ func (b *restBackend) Stat(h backend.Handle) (backend.BlobInfo, error) { } if resp.StatusCode != 200 { - return backend.BlobInfo{}, errors.New("blob not saved") + return backend.BlobInfo{}, fmt.Errorf("unexpected HTTP response code %v", resp.StatusCode) } if resp.ContentLength < 0 { @@ -215,9 +216,14 @@ func (b *restBackend) Remove(t backend.Type, name string) error { func (b *restBackend) List(t backend.Type, done <-chan struct{}) <-chan string { ch := make(chan string) + url := restPath(b.url, backend.Handle{Type: t}) + if !strings.HasSuffix(url, "/") { + url += "/" + } + client := *b.client <-b.connChan - resp, err := client.Get(restPath(b.url, backend.Handle{Type: t})) + resp, err := client.Get(url) b.connChan <- struct{}{} if resp != nil { diff --git a/src/restic/backend/rest/rest_path_test.go b/src/restic/backend/rest/rest_path_test.go new file mode 100644 index 000000000..285240cac --- /dev/null +++ b/src/restic/backend/rest/rest_path_test.go @@ -0,0 +1,48 @@ +package rest + +import ( + "net/url" + "restic/backend" + "testing" +) + +var restPathTests = []struct { + Handle backend.Handle + URL *url.URL + Result string +}{ + { + URL: parseURL("https://hostname.foo"), + Handle: backend.Handle{ + Type: backend.Data, + Name: "foobar", + }, + Result: "https://hostname.foo/data/foobar", + }, + { + URL: parseURL("https://hostname.foo:1234/prefix/repo"), + Handle: backend.Handle{ + Type: backend.Lock, + Name: "foobar", + }, + Result: "https://hostname.foo:1234/prefix/repo/locks/foobar", + }, + { + URL: parseURL("https://hostname.foo:1234/prefix/repo"), + Handle: backend.Handle{ + Type: backend.Config, + Name: "foobar", + }, + Result: "https://hostname.foo:1234/prefix/repo/config", + }, +} + +func TestRESTPaths(t *testing.T) { + for i, test := range restPathTests { + result := restPath(test.URL, test.Handle) + if result != test.Result { + t.Errorf("test %d: resulting URL does not match, want:\n %#v\ngot: \n %#v", + i, test.Result, result) + } + } +} From 921c2f6069a0f1d53e0448bbbe664369cd0d739d Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Feb 2016 16:51:27 +0100 Subject: [PATCH 7/7] rest backend: Improve documentation --- doc/REST_backend.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/doc/REST_backend.md b/doc/REST_backend.md index 2f793c58a..78a3fd4eb 100644 --- a/doc/REST_backend.md +++ b/doc/REST_backend.md @@ -1,38 +1,41 @@ REST Backend ============ -Restic can interact with HTTP Backend that respects the following REST API. +Restic can interact with HTTP Backend that respects the following REST API. The +following values are valid for `{type}`: `data`, `keys`, `locks`, `snapshots`, +`index`, `config`. `{path}` is a path to the repository, so that multiple +different repositories can be accessed. The default path is `/`. -## HEAD /config +## HEAD {path}/config Returns "200 OK" if the repository has a configuration, an HTTP error otherwise. -## GET /config +## GET {path}/config Returns the content of the configuration file if the repository has a configuration, an HTTP error otherwise. Response format: binary/octet-stream -## POST /config +## POST {path}/config Returns "200 OK" if the configuration of the request body has been saved, an HTTP error otherwise. -## GET /{type}/ +## GET {path}/{type}/ Returns a JSON array containing the names of all the blobs stored for a given type. Response format: JSON -## HEAD /{type}/{name} +## HEAD {path}/{type}/{name} Returns "200 OK" if the blob with the given name and type is stored in the repository, "404 not found" otherwise. If the blob exists, the HTTP header `Content-Length` is set to the file size. -## GET /{type}/{name} +## GET {path}/{type}/{name} Returns the content of the blob with the given name and type if it is stored in the repository, "404 not found" otherwise. @@ -43,14 +46,14 @@ and the response only contains the specified range. Response format: binary/octet-stream -## POST /{type}/{name} +## POST {path}/{type}/{name} Saves the content of the request body as a blob with the given name and type, an HTTP error otherwise. Request format: binary/octet-stream -## DELETE /{type}/{name} +## DELETE {path}/{type}/{name} Returns "200 OK" if the blob with the given name and type has been deleted from the repository, an HTTP error otherwise.