github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/internal/backend/rest/rest.go (about)

     1  package rest
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/url"
    11  	"path"
    12  	"strings"
    13  
    14  	"golang.org/x/net/context/ctxhttp"
    15  
    16  	"github.com/restic/restic/internal/debug"
    17  	"github.com/restic/restic/internal/errors"
    18  	"github.com/restic/restic/internal/restic"
    19  
    20  	"github.com/restic/restic/internal/backend"
    21  )
    22  
    23  // make sure the rest backend implements restic.Backend
    24  var _ restic.Backend = &restBackend{}
    25  
    26  type restBackend struct {
    27  	url    *url.URL
    28  	sem    *backend.Semaphore
    29  	client *http.Client
    30  	backend.Layout
    31  }
    32  
    33  // Open opens the REST backend with the given config.
    34  func Open(cfg Config, rt http.RoundTripper) (*restBackend, error) {
    35  	client := &http.Client{Transport: rt}
    36  
    37  	sem, err := backend.NewSemaphore(cfg.Connections)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  
    42  	// use url without trailing slash for layout
    43  	url := cfg.URL.String()
    44  	if url[len(url)-1] == '/' {
    45  		url = url[:len(url)-1]
    46  	}
    47  
    48  	be := &restBackend{
    49  		url:    cfg.URL,
    50  		client: client,
    51  		Layout: &backend.RESTLayout{URL: url, Join: path.Join},
    52  		sem:    sem,
    53  	}
    54  
    55  	return be, nil
    56  }
    57  
    58  // Create creates a new REST on server configured in config.
    59  func Create(cfg Config, rt http.RoundTripper) (restic.Backend, error) {
    60  	be, err := Open(cfg, rt)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	_, err = be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile})
    66  	if err == nil {
    67  		return nil, errors.Fatal("config file already exists")
    68  	}
    69  
    70  	url := *cfg.URL
    71  	values := url.Query()
    72  	values.Set("create", "true")
    73  	url.RawQuery = values.Encode()
    74  
    75  	resp, err := be.client.Post(url.String(), "binary/octet-stream", strings.NewReader(""))
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	if resp.StatusCode != http.StatusOK {
    81  		return nil, errors.Fatalf("server response unexpected: %v (%v)", resp.Status, resp.StatusCode)
    82  	}
    83  
    84  	_, err = io.Copy(ioutil.Discard, resp.Body)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	err = resp.Body.Close()
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	return be, nil
    95  }
    96  
    97  // Location returns this backend's location (the server's URL).
    98  func (b *restBackend) Location() string {
    99  	return b.url.String()
   100  }
   101  
   102  // Save stores data in the backend at the handle.
   103  func (b *restBackend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
   104  	if err := h.Valid(); err != nil {
   105  		return err
   106  	}
   107  
   108  	ctx, cancel := context.WithCancel(ctx)
   109  	defer cancel()
   110  
   111  	// make sure that client.Post() cannot close the reader by wrapping it
   112  	rd = ioutil.NopCloser(rd)
   113  
   114  	b.sem.GetToken()
   115  	resp, err := ctxhttp.Post(ctx, b.client, b.Filename(h), "binary/octet-stream", rd)
   116  	b.sem.ReleaseToken()
   117  
   118  	if resp != nil {
   119  		defer func() {
   120  			_, _ = io.Copy(ioutil.Discard, resp.Body)
   121  			e := resp.Body.Close()
   122  
   123  			if err == nil {
   124  				err = errors.Wrap(e, "Close")
   125  			}
   126  		}()
   127  	}
   128  
   129  	if err != nil {
   130  		return errors.Wrap(err, "client.Post")
   131  	}
   132  
   133  	if resp.StatusCode != 200 {
   134  		return errors.Errorf("server response unexpected: %v (%v)", resp.Status, resp.StatusCode)
   135  	}
   136  
   137  	return nil
   138  }
   139  
   140  // ErrIsNotExist is returned whenever the requested file does not exist on the
   141  // server.
   142  type ErrIsNotExist struct {
   143  	restic.Handle
   144  }
   145  
   146  func (e ErrIsNotExist) Error() string {
   147  	return fmt.Sprintf("%v does not exist", e.Handle)
   148  }
   149  
   150  // IsNotExist returns true if the error was caused by a non-existing file.
   151  func (b *restBackend) IsNotExist(err error) bool {
   152  	err = errors.Cause(err)
   153  	_, ok := err.(ErrIsNotExist)
   154  	return ok
   155  }
   156  
   157  // Load returns a reader that yields the contents of the file at h at the
   158  // given offset. If length is nonzero, only a portion of the file is
   159  // returned. rd must be closed after use.
   160  func (b *restBackend) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
   161  	debug.Log("Load %v, length %v, offset %v", h, length, offset)
   162  	if err := h.Valid(); err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	if offset < 0 {
   167  		return nil, errors.New("offset is negative")
   168  	}
   169  
   170  	if length < 0 {
   171  		return nil, errors.Errorf("invalid length %d", length)
   172  	}
   173  
   174  	req, err := http.NewRequest("GET", b.Filename(h), nil)
   175  	if err != nil {
   176  		return nil, errors.Wrap(err, "http.NewRequest")
   177  	}
   178  
   179  	byteRange := fmt.Sprintf("bytes=%d-", offset)
   180  	if length > 0 {
   181  		byteRange = fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length)-1)
   182  	}
   183  	req.Header.Add("Range", byteRange)
   184  	debug.Log("Load(%v) send range %v", h, byteRange)
   185  
   186  	b.sem.GetToken()
   187  	resp, err := ctxhttp.Do(ctx, b.client, req)
   188  	b.sem.ReleaseToken()
   189  
   190  	if err != nil {
   191  		if resp != nil {
   192  			_, _ = io.Copy(ioutil.Discard, resp.Body)
   193  			_ = resp.Body.Close()
   194  		}
   195  		return nil, errors.Wrap(err, "client.Do")
   196  	}
   197  
   198  	if resp.StatusCode == http.StatusNotFound {
   199  		_ = resp.Body.Close()
   200  		return nil, ErrIsNotExist{h}
   201  	}
   202  
   203  	if resp.StatusCode != 200 && resp.StatusCode != 206 {
   204  		_ = resp.Body.Close()
   205  		return nil, errors.Errorf("unexpected HTTP response (%v): %v", resp.StatusCode, resp.Status)
   206  	}
   207  
   208  	return resp.Body, nil
   209  }
   210  
   211  // Stat returns information about a blob.
   212  func (b *restBackend) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
   213  	if err := h.Valid(); err != nil {
   214  		return restic.FileInfo{}, err
   215  	}
   216  
   217  	b.sem.GetToken()
   218  	resp, err := ctxhttp.Head(ctx, b.client, b.Filename(h))
   219  	b.sem.ReleaseToken()
   220  	if err != nil {
   221  		return restic.FileInfo{}, errors.Wrap(err, "client.Head")
   222  	}
   223  
   224  	_, _ = io.Copy(ioutil.Discard, resp.Body)
   225  	if err = resp.Body.Close(); err != nil {
   226  		return restic.FileInfo{}, errors.Wrap(err, "Close")
   227  	}
   228  
   229  	if resp.StatusCode == http.StatusNotFound {
   230  		_ = resp.Body.Close()
   231  		return restic.FileInfo{}, ErrIsNotExist{h}
   232  	}
   233  
   234  	if resp.StatusCode != 200 {
   235  		return restic.FileInfo{}, errors.Errorf("unexpected HTTP response (%v): %v", resp.StatusCode, resp.Status)
   236  	}
   237  
   238  	if resp.ContentLength < 0 {
   239  		return restic.FileInfo{}, errors.New("negative content length")
   240  	}
   241  
   242  	bi := restic.FileInfo{
   243  		Size: resp.ContentLength,
   244  	}
   245  
   246  	return bi, nil
   247  }
   248  
   249  // Test returns true if a blob of the given type and name exists in the backend.
   250  func (b *restBackend) Test(ctx context.Context, h restic.Handle) (bool, error) {
   251  	_, err := b.Stat(ctx, h)
   252  	if err != nil {
   253  		return false, nil
   254  	}
   255  
   256  	return true, nil
   257  }
   258  
   259  // Remove removes the blob with the given name and type.
   260  func (b *restBackend) Remove(ctx context.Context, h restic.Handle) error {
   261  	if err := h.Valid(); err != nil {
   262  		return err
   263  	}
   264  
   265  	req, err := http.NewRequest("DELETE", b.Filename(h), nil)
   266  	if err != nil {
   267  		return errors.Wrap(err, "http.NewRequest")
   268  	}
   269  	b.sem.GetToken()
   270  	resp, err := ctxhttp.Do(ctx, b.client, req)
   271  	b.sem.ReleaseToken()
   272  
   273  	if err != nil {
   274  		return errors.Wrap(err, "client.Do")
   275  	}
   276  
   277  	if resp.StatusCode == http.StatusNotFound {
   278  		_ = resp.Body.Close()
   279  		return ErrIsNotExist{h}
   280  	}
   281  
   282  	if resp.StatusCode != 200 {
   283  		return errors.Errorf("blob not removed, server response: %v (%v)", resp.Status, resp.StatusCode)
   284  	}
   285  
   286  	_, err = io.Copy(ioutil.Discard, resp.Body)
   287  	if err != nil {
   288  		return errors.Wrap(err, "Copy")
   289  	}
   290  
   291  	return errors.Wrap(resp.Body.Close(), "Close")
   292  }
   293  
   294  // List returns a channel that yields all names of blobs of type t. A
   295  // goroutine is started for this. If the channel done is closed, sending
   296  // stops.
   297  func (b *restBackend) List(ctx context.Context, t restic.FileType) <-chan string {
   298  	ch := make(chan string)
   299  
   300  	url := b.Dirname(restic.Handle{Type: t})
   301  	if !strings.HasSuffix(url, "/") {
   302  		url += "/"
   303  	}
   304  
   305  	b.sem.GetToken()
   306  	resp, err := ctxhttp.Get(ctx, b.client, url)
   307  	b.sem.ReleaseToken()
   308  
   309  	if resp != nil {
   310  		defer func() {
   311  			_, _ = io.Copy(ioutil.Discard, resp.Body)
   312  			e := resp.Body.Close()
   313  
   314  			if err == nil {
   315  				err = errors.Wrap(e, "Close")
   316  			}
   317  		}()
   318  	}
   319  
   320  	if err != nil {
   321  		close(ch)
   322  		return ch
   323  	}
   324  
   325  	dec := json.NewDecoder(resp.Body)
   326  	var list []string
   327  	if err = dec.Decode(&list); err != nil {
   328  		close(ch)
   329  		return ch
   330  	}
   331  
   332  	go func() {
   333  		defer close(ch)
   334  		for _, m := range list {
   335  			select {
   336  			case ch <- m:
   337  			case <-ctx.Done():
   338  				return
   339  			}
   340  		}
   341  	}()
   342  
   343  	return ch
   344  }
   345  
   346  // Close closes all open files.
   347  func (b *restBackend) Close() error {
   348  	// this does not need to do anything, all open files are closed within the
   349  	// same function.
   350  	return nil
   351  }
   352  
   353  // Remove keys for a specified backend type.
   354  func (b *restBackend) removeKeys(ctx context.Context, t restic.FileType) error {
   355  	for key := range b.List(ctx, restic.DataFile) {
   356  		err := b.Remove(ctx, restic.Handle{Type: restic.DataFile, Name: key})
   357  		if err != nil {
   358  			return err
   359  		}
   360  	}
   361  
   362  	return nil
   363  }
   364  
   365  // Delete removes all data in the backend.
   366  func (b *restBackend) Delete(ctx context.Context) error {
   367  	alltypes := []restic.FileType{
   368  		restic.DataFile,
   369  		restic.KeyFile,
   370  		restic.LockFile,
   371  		restic.SnapshotFile,
   372  		restic.IndexFile}
   373  
   374  	for _, t := range alltypes {
   375  		err := b.removeKeys(ctx, t)
   376  		if err != nil {
   377  			return nil
   378  		}
   379  	}
   380  
   381  	err := b.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
   382  	if err != nil && b.IsNotExist(err) {
   383  		return nil
   384  	}
   385  	return err
   386  }