github.com/mckael/restic@v0.8.3/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  const (
    34  	contentTypeV1 = "application/vnd.x.restic.rest.v1"
    35  	contentTypeV2 = "application/vnd.x.restic.rest.v2"
    36  )
    37  
    38  // Open opens the REST backend with the given config.
    39  func Open(cfg Config, rt http.RoundTripper) (*restBackend, error) {
    40  	client := &http.Client{Transport: rt}
    41  
    42  	sem, err := backend.NewSemaphore(cfg.Connections)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  
    47  	// use url without trailing slash for layout
    48  	url := cfg.URL.String()
    49  	if url[len(url)-1] == '/' {
    50  		url = url[:len(url)-1]
    51  	}
    52  
    53  	be := &restBackend{
    54  		url:    cfg.URL,
    55  		client: client,
    56  		Layout: &backend.RESTLayout{URL: url, Join: path.Join},
    57  		sem:    sem,
    58  	}
    59  
    60  	return be, nil
    61  }
    62  
    63  // Create creates a new REST on server configured in config.
    64  func Create(cfg Config, rt http.RoundTripper) (restic.Backend, error) {
    65  	be, err := Open(cfg, rt)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  
    70  	_, err = be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile})
    71  	if err == nil {
    72  		return nil, errors.Fatal("config file already exists")
    73  	}
    74  
    75  	url := *cfg.URL
    76  	values := url.Query()
    77  	values.Set("create", "true")
    78  	url.RawQuery = values.Encode()
    79  
    80  	resp, err := be.client.Post(url.String(), "binary/octet-stream", strings.NewReader(""))
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	if resp.StatusCode != http.StatusOK {
    86  		return nil, errors.Fatalf("server response unexpected: %v (%v)", resp.Status, resp.StatusCode)
    87  	}
    88  
    89  	_, err = io.Copy(ioutil.Discard, resp.Body)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	err = resp.Body.Close()
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	return be, nil
   100  }
   101  
   102  // Location returns this backend's location (the server's URL).
   103  func (b *restBackend) Location() string {
   104  	return b.url.String()
   105  }
   106  
   107  // Save stores data in the backend at the handle.
   108  func (b *restBackend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
   109  	if err := h.Valid(); err != nil {
   110  		return err
   111  	}
   112  
   113  	ctx, cancel := context.WithCancel(ctx)
   114  	defer cancel()
   115  
   116  	// make sure that client.Post() cannot close the reader by wrapping it
   117  	rd = ioutil.NopCloser(rd)
   118  
   119  	req, err := http.NewRequest(http.MethodPost, b.Filename(h), rd)
   120  	if err != nil {
   121  		return errors.Wrap(err, "NewRequest")
   122  	}
   123  	req.Header.Set("Content-Type", "application/octet-stream")
   124  	req.Header.Set("Accept", contentTypeV2)
   125  
   126  	b.sem.GetToken()
   127  	resp, err := ctxhttp.Do(ctx, b.client, req)
   128  	b.sem.ReleaseToken()
   129  
   130  	if resp != nil {
   131  		defer func() {
   132  			_, _ = io.Copy(ioutil.Discard, resp.Body)
   133  			e := resp.Body.Close()
   134  
   135  			if err == nil {
   136  				err = errors.Wrap(e, "Close")
   137  			}
   138  		}()
   139  	}
   140  
   141  	if err != nil {
   142  		return errors.Wrap(err, "client.Post")
   143  	}
   144  
   145  	if resp.StatusCode != 200 {
   146  		return errors.Errorf("server response unexpected: %v (%v)", resp.Status, resp.StatusCode)
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  // ErrIsNotExist is returned whenever the requested file does not exist on the
   153  // server.
   154  type ErrIsNotExist struct {
   155  	restic.Handle
   156  }
   157  
   158  func (e ErrIsNotExist) Error() string {
   159  	return fmt.Sprintf("%v does not exist", e.Handle)
   160  }
   161  
   162  // IsNotExist returns true if the error was caused by a non-existing file.
   163  func (b *restBackend) IsNotExist(err error) bool {
   164  	err = errors.Cause(err)
   165  	_, ok := err.(ErrIsNotExist)
   166  	return ok
   167  }
   168  
   169  // Load runs fn with a reader that yields the contents of the file at h at the
   170  // given offset.
   171  func (b *restBackend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
   172  	return backend.DefaultLoad(ctx, h, length, offset, b.openReader, fn)
   173  }
   174  
   175  func (b *restBackend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
   176  	debug.Log("Load %v, length %v, offset %v", h, length, offset)
   177  	if err := h.Valid(); err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	if offset < 0 {
   182  		return nil, errors.New("offset is negative")
   183  	}
   184  
   185  	if length < 0 {
   186  		return nil, errors.Errorf("invalid length %d", length)
   187  	}
   188  
   189  	req, err := http.NewRequest("GET", b.Filename(h), nil)
   190  	if err != nil {
   191  		return nil, errors.Wrap(err, "http.NewRequest")
   192  	}
   193  
   194  	byteRange := fmt.Sprintf("bytes=%d-", offset)
   195  	if length > 0 {
   196  		byteRange = fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length)-1)
   197  	}
   198  	req.Header.Set("Range", byteRange)
   199  	req.Header.Set("Accept", contentTypeV2)
   200  	debug.Log("Load(%v) send range %v", h, byteRange)
   201  
   202  	b.sem.GetToken()
   203  	resp, err := ctxhttp.Do(ctx, b.client, req)
   204  	b.sem.ReleaseToken()
   205  
   206  	if err != nil {
   207  		if resp != nil {
   208  			_, _ = io.Copy(ioutil.Discard, resp.Body)
   209  			_ = resp.Body.Close()
   210  		}
   211  		return nil, errors.Wrap(err, "client.Do")
   212  	}
   213  
   214  	if resp.StatusCode == http.StatusNotFound {
   215  		_ = resp.Body.Close()
   216  		return nil, ErrIsNotExist{h}
   217  	}
   218  
   219  	if resp.StatusCode != 200 && resp.StatusCode != 206 {
   220  		_ = resp.Body.Close()
   221  		return nil, errors.Errorf("unexpected HTTP response (%v): %v", resp.StatusCode, resp.Status)
   222  	}
   223  
   224  	return resp.Body, nil
   225  }
   226  
   227  // Stat returns information about a blob.
   228  func (b *restBackend) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
   229  	if err := h.Valid(); err != nil {
   230  		return restic.FileInfo{}, err
   231  	}
   232  
   233  	req, err := http.NewRequest(http.MethodHead, b.Filename(h), nil)
   234  	if err != nil {
   235  		return restic.FileInfo{}, errors.Wrap(err, "NewRequest")
   236  	}
   237  	req.Header.Set("Accept", contentTypeV2)
   238  
   239  	b.sem.GetToken()
   240  	resp, err := ctxhttp.Do(ctx, b.client, req)
   241  	b.sem.ReleaseToken()
   242  	if err != nil {
   243  		return restic.FileInfo{}, errors.Wrap(err, "client.Head")
   244  	}
   245  
   246  	_, _ = io.Copy(ioutil.Discard, resp.Body)
   247  	if err = resp.Body.Close(); err != nil {
   248  		return restic.FileInfo{}, errors.Wrap(err, "Close")
   249  	}
   250  
   251  	if resp.StatusCode == http.StatusNotFound {
   252  		_ = resp.Body.Close()
   253  		return restic.FileInfo{}, ErrIsNotExist{h}
   254  	}
   255  
   256  	if resp.StatusCode != 200 {
   257  		return restic.FileInfo{}, errors.Errorf("unexpected HTTP response (%v): %v", resp.StatusCode, resp.Status)
   258  	}
   259  
   260  	if resp.ContentLength < 0 {
   261  		return restic.FileInfo{}, errors.New("negative content length")
   262  	}
   263  
   264  	bi := restic.FileInfo{
   265  		Size: resp.ContentLength,
   266  		Name: h.Name,
   267  	}
   268  
   269  	return bi, nil
   270  }
   271  
   272  // Test returns true if a blob of the given type and name exists in the backend.
   273  func (b *restBackend) Test(ctx context.Context, h restic.Handle) (bool, error) {
   274  	_, err := b.Stat(ctx, h)
   275  	if err != nil {
   276  		return false, nil
   277  	}
   278  
   279  	return true, nil
   280  }
   281  
   282  // Remove removes the blob with the given name and type.
   283  func (b *restBackend) Remove(ctx context.Context, h restic.Handle) error {
   284  	if err := h.Valid(); err != nil {
   285  		return err
   286  	}
   287  
   288  	req, err := http.NewRequest("DELETE", b.Filename(h), nil)
   289  	if err != nil {
   290  		return errors.Wrap(err, "http.NewRequest")
   291  	}
   292  	req.Header.Set("Accept", contentTypeV2)
   293  
   294  	b.sem.GetToken()
   295  	resp, err := ctxhttp.Do(ctx, b.client, req)
   296  	b.sem.ReleaseToken()
   297  
   298  	if err != nil {
   299  		return errors.Wrap(err, "client.Do")
   300  	}
   301  
   302  	if resp.StatusCode == http.StatusNotFound {
   303  		_ = resp.Body.Close()
   304  		return ErrIsNotExist{h}
   305  	}
   306  
   307  	if resp.StatusCode != 200 {
   308  		return errors.Errorf("blob not removed, server response: %v (%v)", resp.Status, resp.StatusCode)
   309  	}
   310  
   311  	_, err = io.Copy(ioutil.Discard, resp.Body)
   312  	if err != nil {
   313  		return errors.Wrap(err, "Copy")
   314  	}
   315  
   316  	return errors.Wrap(resp.Body.Close(), "Close")
   317  }
   318  
   319  // List runs fn for each file in the backend which has the type t. When an
   320  // error occurs (or fn returns an error), List stops and returns it.
   321  func (b *restBackend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error {
   322  	url := b.Dirname(restic.Handle{Type: t})
   323  	if !strings.HasSuffix(url, "/") {
   324  		url += "/"
   325  	}
   326  
   327  	req, err := http.NewRequest(http.MethodGet, url, nil)
   328  	if err != nil {
   329  		return errors.Wrap(err, "NewRequest")
   330  	}
   331  	req.Header.Set("Accept", contentTypeV2)
   332  
   333  	b.sem.GetToken()
   334  	resp, err := ctxhttp.Do(ctx, b.client, req)
   335  	b.sem.ReleaseToken()
   336  
   337  	if err != nil {
   338  		return errors.Wrap(err, "Get")
   339  	}
   340  
   341  	if resp.Header.Get("Content-Type") == contentTypeV2 {
   342  		return b.listv2(ctx, t, resp, fn)
   343  	}
   344  
   345  	return b.listv1(ctx, t, resp, fn)
   346  }
   347  
   348  // listv1 uses the REST protocol v1, where a list HTTP request (e.g. `GET
   349  // /data/`) only returns the names of the files, so we need to issue an HTTP
   350  // HEAD request for each file.
   351  func (b *restBackend) listv1(ctx context.Context, t restic.FileType, resp *http.Response, fn func(restic.FileInfo) error) error {
   352  	debug.Log("parsing API v1 response")
   353  	dec := json.NewDecoder(resp.Body)
   354  	var list []string
   355  	if err := dec.Decode(&list); err != nil {
   356  		return errors.Wrap(err, "Decode")
   357  	}
   358  
   359  	for _, m := range list {
   360  		fi, err := b.Stat(ctx, restic.Handle{Name: m, Type: t})
   361  		if err != nil {
   362  			return err
   363  		}
   364  
   365  		if ctx.Err() != nil {
   366  			return ctx.Err()
   367  		}
   368  
   369  		fi.Name = m
   370  		err = fn(fi)
   371  		if err != nil {
   372  			return err
   373  		}
   374  
   375  		if ctx.Err() != nil {
   376  			return ctx.Err()
   377  		}
   378  	}
   379  
   380  	return ctx.Err()
   381  }
   382  
   383  // listv2 uses the REST protocol v2, where a list HTTP request (e.g. `GET
   384  // /data/`) returns the names and sizes of all files.
   385  func (b *restBackend) listv2(ctx context.Context, t restic.FileType, resp *http.Response, fn func(restic.FileInfo) error) error {
   386  	debug.Log("parsing API v2 response")
   387  	dec := json.NewDecoder(resp.Body)
   388  
   389  	var list []struct {
   390  		Name string `json:"name"`
   391  		Size int64  `json:"size"`
   392  	}
   393  	if err := dec.Decode(&list); err != nil {
   394  		return errors.Wrap(err, "Decode")
   395  	}
   396  
   397  	for _, item := range list {
   398  		if ctx.Err() != nil {
   399  			return ctx.Err()
   400  		}
   401  
   402  		fi := restic.FileInfo{
   403  			Name: item.Name,
   404  			Size: item.Size,
   405  		}
   406  
   407  		err := fn(fi)
   408  		if err != nil {
   409  			return err
   410  		}
   411  
   412  		if ctx.Err() != nil {
   413  			return ctx.Err()
   414  		}
   415  	}
   416  
   417  	return ctx.Err()
   418  }
   419  
   420  // Close closes all open files.
   421  func (b *restBackend) Close() error {
   422  	// this does not need to do anything, all open files are closed within the
   423  	// same function.
   424  	return nil
   425  }
   426  
   427  // Remove keys for a specified backend type.
   428  func (b *restBackend) removeKeys(ctx context.Context, t restic.FileType) error {
   429  	return b.List(ctx, t, func(fi restic.FileInfo) error {
   430  		return b.Remove(ctx, restic.Handle{Type: t, Name: fi.Name})
   431  	})
   432  }
   433  
   434  // Delete removes all data in the backend.
   435  func (b *restBackend) Delete(ctx context.Context) error {
   436  	alltypes := []restic.FileType{
   437  		restic.DataFile,
   438  		restic.KeyFile,
   439  		restic.LockFile,
   440  		restic.SnapshotFile,
   441  		restic.IndexFile}
   442  
   443  	for _, t := range alltypes {
   444  		err := b.removeKeys(ctx, t)
   445  		if err != nil {
   446  			return nil
   447  		}
   448  	}
   449  
   450  	err := b.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
   451  	if err != nil && b.IsNotExist(err) {
   452  		return nil
   453  	}
   454  	return err
   455  }