mirror of
https://github.com/restic/restic.git
synced 2025-03-16 00:00:05 +01:00
Simple REST backend
This commit is contained in:
parent
76b1f017c0
commit
4fbfca0c31
5 changed files with 235 additions and 2 deletions
|
@ -128,6 +128,7 @@ func testBackend(b backend.Backend, t *testing.T) {
|
|||
OK(t, err)
|
||||
|
||||
found, err := b.Test(tpe, id.String())
|
||||
|
||||
OK(t, err)
|
||||
Assert(t, found, fmt.Sprintf("id %q was not found before removal", id))
|
||||
|
||||
|
|
208
backend/rest/rest.go
Normal file
208
backend/rest/rest.go
Normal file
|
@ -0,0 +1,208 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/restic/restic/backend"
|
||||
)
|
||||
|
||||
const connLimit = 10
|
||||
|
||||
// Returns the url of the resource
|
||||
func restPath(url *url.URL, t backend.Type, name string) string {
|
||||
if t == backend.Config {
|
||||
return url.String() + "/" + string(t)
|
||||
}
|
||||
|
||||
return url.String() + "/" + string(t) + "/" + name
|
||||
}
|
||||
|
||||
type RestBlob struct {
|
||||
b *Rest
|
||||
buf *bytes.Buffer
|
||||
final bool
|
||||
}
|
||||
|
||||
func (rb *RestBlob) Write(p []byte) (int, error) {
|
||||
if rb.final {
|
||||
return 0, errors.New("blob already closed")
|
||||
}
|
||||
n, err := rb.buf.Write(p)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (rb *RestBlob) Read(p []byte) (int, error) {
|
||||
return rb.buf.Read(p)
|
||||
}
|
||||
|
||||
func (rb *RestBlob) Close() error {
|
||||
rb.final = true
|
||||
rb.buf.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Finalize moves the data blob to the final location for type and name.
|
||||
func (rb *RestBlob) Finalize(t backend.Type, name string) error {
|
||||
if rb.final {
|
||||
return errors.New("Already finalized")
|
||||
}
|
||||
|
||||
rb.final = true
|
||||
|
||||
// Check key does not already exist
|
||||
resp, err := http.Get(restPath(rb.b.url, t, name))
|
||||
if err == nil && resp.StatusCode == 200 {
|
||||
return errors.New("Key already exists")
|
||||
}
|
||||
|
||||
<-rb.b.connChan
|
||||
_, err2 := http.Post(restPath(rb.b.url, t, name), "binary/octet-stream", rb.buf)
|
||||
rb.b.connChan <- struct{}{}
|
||||
rb.buf.Reset()
|
||||
return err2
|
||||
}
|
||||
|
||||
// Size returns the number of bytes written to the backend so far.
|
||||
func (rb *RestBlob) Size() uint {
|
||||
return uint(rb.buf.Len())
|
||||
}
|
||||
|
||||
// A simple REST backend.
|
||||
type Rest struct {
|
||||
url *url.URL
|
||||
connChan chan struct{}
|
||||
}
|
||||
|
||||
// Open opens the http backend at the specified url.
|
||||
func Open(url *url.URL) (*Rest, error) {
|
||||
connChan := make(chan struct{}, connLimit)
|
||||
for i := 0; i < connLimit; i++ {
|
||||
connChan <- struct{}{}
|
||||
}
|
||||
return &Rest{url: url, connChan: connChan}, nil
|
||||
}
|
||||
|
||||
// Location returns a string that specifies the location of the repository, like a URL.
|
||||
func (b *Rest) Location() string {
|
||||
return b.url.Host
|
||||
}
|
||||
|
||||
// Create creates a new Blob. The data is available only after Finalize()
|
||||
// has been called on the returned Blob.
|
||||
func (b *Rest) Create() (backend.Blob, error) {
|
||||
blob := RestBlob{
|
||||
b: b,
|
||||
buf: &bytes.Buffer{},
|
||||
}
|
||||
return &blob, nil
|
||||
}
|
||||
|
||||
// Get returns an io.ReadCloser for the Blob with the given name of type t.
|
||||
func (b *Rest) Get(t backend.Type, name string) (io.ReadCloser, error) {
|
||||
resp, err := http.Get(restPath(b.url, t, name))
|
||||
if err == nil && resp.StatusCode != 200 {
|
||||
err = errors.New("Not found")
|
||||
}
|
||||
return resp.Body, err
|
||||
}
|
||||
|
||||
// GetReader returns an io.ReadCloser for the Blob with the given name of
|
||||
// type t at offset and length.
|
||||
func (b *Rest) GetReader(t backend.Type, name string, offset, length uint) (io.ReadCloser, error) {
|
||||
rc, err := b.Get(t, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n, errc := io.CopyN(ioutil.Discard, rc, int64(offset))
|
||||
if errc != nil {
|
||||
return nil, errc
|
||||
} else if n != int64(offset) {
|
||||
return nil, fmt.Errorf("less bytes read than expected, read: %d, expected: %d", n, offset)
|
||||
}
|
||||
|
||||
if length == 0 {
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
return backend.LimitReadCloser(rc, int64(length)), nil
|
||||
}
|
||||
|
||||
// Test a boolean value whether a Blob with the name and type exists.
|
||||
func (b *Rest) Test(t backend.Type, name string) (bool, error) {
|
||||
found := false
|
||||
|
||||
req, e1 := http.NewRequest("HEAD", restPath(b.url, t, name), nil)
|
||||
if e1 != nil {
|
||||
return found, e1
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if resp.StatusCode == 200 {
|
||||
found = true
|
||||
}
|
||||
return found, err
|
||||
}
|
||||
|
||||
// Remove removes a Blob with type t and name.
|
||||
func (b *Rest) Remove(t backend.Type, name string) error {
|
||||
req, err := http.NewRequest("DELETE", restPath(b.url, t, name), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err2 := http.DefaultClient.Do(req)
|
||||
return err2
|
||||
}
|
||||
|
||||
// Close the backend
|
||||
func (b *Rest) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns a channel that yields all names of blobs of type t in
|
||||
// lexicographic order. A goroutine is started for this. If the channel
|
||||
// done is closed, sending stops.
|
||||
func (b *Rest) List(t backend.Type, done <-chan struct{}) <-chan string {
|
||||
ch := make(chan string)
|
||||
|
||||
resp, e1 := http.Get(restPath(b.url, t, ""))
|
||||
if e1 != nil {
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
data, e2 := ioutil.ReadAll(resp.Body)
|
||||
if e2 != nil {
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
var list []string
|
||||
e3 := json.Unmarshal(data, &list)
|
||||
if e3 != nil {
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for _, m := range list {
|
||||
fmt.Println(m)
|
||||
select {
|
||||
case ch <- m:
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
18
backend/rest_test.go
Normal file
18
backend/rest_test.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package backend_test
|
||||
|
||||
import (
|
||||
"github.com/restic/restic/backend/rest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupRestBackend(t *testing.T) *rest.Rest {
|
||||
url, _ := url.Parse("http://localhost:8000")
|
||||
backend, _ := rest.Open(url)
|
||||
return backend
|
||||
}
|
||||
|
||||
func TestRestBackend(t *testing.T) {
|
||||
s := setupRestBackend(t)
|
||||
testBackend(s, t)
|
||||
}
|
|
@ -71,7 +71,7 @@ type s3Blob struct {
|
|||
|
||||
func (bb *s3Blob) Write(p []byte) (int, error) {
|
||||
if bb.final {
|
||||
return 0, errors.New("blob already closed")
|
||||
return 0, errors.New("Blob already closed")
|
||||
}
|
||||
|
||||
n, err := bb.buf.Write(p)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/jessevdk/go-flags"
|
||||
"github.com/restic/restic/backend"
|
||||
"github.com/restic/restic/backend/local"
|
||||
"github.com/restic/restic/backend/rest"
|
||||
"github.com/restic/restic/backend/s3"
|
||||
"github.com/restic/restic/backend/sftp"
|
||||
"github.com/restic/restic/repository"
|
||||
|
@ -129,10 +130,11 @@ func (o GlobalOptions) OpenRepository() (*repository.Repository, error) {
|
|||
// Open the backend specified by URI.
|
||||
// Valid formats are:
|
||||
// * /foo/bar -> local repository at /foo/bar
|
||||
// * c:\temp -> local repository at c:\temp - the path must exist
|
||||
// * s3://region/bucket -> amazon s3 bucket
|
||||
// * sftp://user@host/foo/bar -> remote sftp repository on host for user at path foo/bar
|
||||
// * sftp://host//tmp/backup -> remote sftp repository on host at path /tmp/backup
|
||||
// * c:\temp -> local repository at c:\temp - the path must exist
|
||||
// * http://host -> remote and public http repository
|
||||
func open(u string) (backend.Backend, error) {
|
||||
// check if the url is a directory that exists
|
||||
fi, err := os.Stat(u)
|
||||
|
@ -155,6 +157,8 @@ func open(u string) (backend.Backend, error) {
|
|||
|
||||
if url.Scheme == "s3" {
|
||||
return s3.Open(url.Host, url.Path[1:])
|
||||
} else if url.Scheme == "http" {
|
||||
return rest.Open(url)
|
||||
}
|
||||
|
||||
args := []string{url.Host}
|
||||
|
@ -190,6 +194,8 @@ func create(u string) (backend.Backend, error) {
|
|||
|
||||
if url.Scheme == "s3" {
|
||||
return s3.Open(url.Host, url.Path[1:])
|
||||
} else if url.Scheme == "http" {
|
||||
return rest.Open(url)
|
||||
}
|
||||
|
||||
args := []string{url.Host}
|
||||
|
|
Loading…
Add table
Reference in a new issue