github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/storage/driver/gcs/gcs.go (about)

     1  // Package gcs provides a storagedriver.StorageDriver implementation to
     2  // store blobs in Google cloud storage.
     3  //
     4  // This package leverages the google.golang.org/cloud/storage client library
     5  //for interfacing with gcs.
     6  //
     7  // Because gcs is a key, value store the Stat call does not support last modification
     8  // time for directories (directories are an abstraction for key, value stores)
     9  //
    10  // Keep in mind that gcs guarantees only eventual consistency, so do not assume
    11  // that a successful write will mean immediate access to the data written (although
    12  // in most regions a new object put has guaranteed read after write). The only true
    13  // guarantee is that once you call Stat and receive a certain file size, that much of
    14  // the file is already accessible.
    15  //
    16  // +build include_gcs
    17  
    18  package gcs
    19  
    20  import (
    21  	"bytes"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"math/rand"
    26  	"net/http"
    27  	"net/url"
    28  	"sort"
    29  	"strings"
    30  	"time"
    31  
    32  	"golang.org/x/net/context"
    33  	"golang.org/x/oauth2"
    34  	"golang.org/x/oauth2/google"
    35  	"golang.org/x/oauth2/jwt"
    36  	"google.golang.org/api/googleapi"
    37  	storageapi "google.golang.org/api/storage/v1"
    38  	"google.golang.org/cloud"
    39  	"google.golang.org/cloud/storage"
    40  
    41  	ctx "github.com/docker/distribution/context"
    42  	storagedriver "github.com/docker/distribution/registry/storage/driver"
    43  	"github.com/docker/distribution/registry/storage/driver/base"
    44  	"github.com/docker/distribution/registry/storage/driver/factory"
    45  )
    46  
    47  const driverName = "gcs"
    48  const dummyProjectID = "<unknown>"
    49  
    50  // driverParameters is a struct that encapsulates all of the driver parameters after all values have been set
    51  type driverParameters struct {
    52  	bucket        string
    53  	config        *jwt.Config
    54  	email         string
    55  	privateKey    []byte
    56  	client        *http.Client
    57  	rootDirectory string
    58  }
    59  
    60  func init() {
    61  	factory.Register(driverName, &gcsDriverFactory{})
    62  }
    63  
    64  // gcsDriverFactory implements the factory.StorageDriverFactory interface
    65  type gcsDriverFactory struct{}
    66  
    67  // Create StorageDriver from parameters
    68  func (factory *gcsDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) {
    69  	return FromParameters(parameters)
    70  }
    71  
    72  // driver is a storagedriver.StorageDriver implementation backed by GCS
    73  // Objects are stored at absolute keys in the provided bucket.
    74  type driver struct {
    75  	client        *http.Client
    76  	bucket        string
    77  	email         string
    78  	privateKey    []byte
    79  	rootDirectory string
    80  }
    81  
    82  // FromParameters constructs a new Driver with a given parameters map
    83  // Required parameters:
    84  // - bucket
    85  func FromParameters(parameters map[string]interface{}) (storagedriver.StorageDriver, error) {
    86  	bucket, ok := parameters["bucket"]
    87  	if !ok || fmt.Sprint(bucket) == "" {
    88  		return nil, fmt.Errorf("No bucket parameter provided")
    89  	}
    90  
    91  	rootDirectory, ok := parameters["rootdirectory"]
    92  	if !ok {
    93  		rootDirectory = ""
    94  	}
    95  
    96  	var ts oauth2.TokenSource
    97  	jwtConf := new(jwt.Config)
    98  	if keyfile, ok := parameters["keyfile"]; ok {
    99  		jsonKey, err := ioutil.ReadFile(fmt.Sprint(keyfile))
   100  		if err != nil {
   101  			return nil, err
   102  		}
   103  		jwtConf, err = google.JWTConfigFromJSON(jsonKey, storage.ScopeFullControl)
   104  		if err != nil {
   105  			return nil, err
   106  		}
   107  		ts = jwtConf.TokenSource(context.Background())
   108  	} else {
   109  		var err error
   110  		ts, err = google.DefaultTokenSource(context.Background(), storage.ScopeFullControl)
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  
   115  	}
   116  
   117  	params := driverParameters{
   118  		bucket:        fmt.Sprint(bucket),
   119  		rootDirectory: fmt.Sprint(rootDirectory),
   120  		email:         jwtConf.Email,
   121  		privateKey:    jwtConf.PrivateKey,
   122  		client:        oauth2.NewClient(context.Background(), ts),
   123  	}
   124  
   125  	return New(params)
   126  }
   127  
   128  // New constructs a new driver
   129  func New(params driverParameters) (storagedriver.StorageDriver, error) {
   130  	rootDirectory := strings.Trim(params.rootDirectory, "/")
   131  	if rootDirectory != "" {
   132  		rootDirectory += "/"
   133  	}
   134  	d := &driver{
   135  		bucket:        params.bucket,
   136  		rootDirectory: rootDirectory,
   137  		email:         params.email,
   138  		privateKey:    params.privateKey,
   139  		client:        params.client,
   140  	}
   141  
   142  	return &base.Base{
   143  		StorageDriver: d,
   144  	}, nil
   145  }
   146  
   147  // Implement the storagedriver.StorageDriver interface
   148  
   149  func (d *driver) Name() string {
   150  	return driverName
   151  }
   152  
   153  // GetContent retrieves the content stored at "path" as a []byte.
   154  // This should primarily be used for small objects.
   155  func (d *driver) GetContent(context ctx.Context, path string) ([]byte, error) {
   156  	rc, err := d.ReadStream(context, path, 0)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	defer rc.Close()
   161  
   162  	p, err := ioutil.ReadAll(rc)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	return p, nil
   167  }
   168  
   169  // PutContent stores the []byte content at a location designated by "path".
   170  // This should primarily be used for small objects.
   171  func (d *driver) PutContent(context ctx.Context, path string, contents []byte) error {
   172  	wc := storage.NewWriter(d.context(context), d.bucket, d.pathToKey(path))
   173  	wc.ContentType = "application/octet-stream"
   174  	defer wc.Close()
   175  	_, err := wc.Write(contents)
   176  	return err
   177  }
   178  
   179  // ReadStream retrieves an io.ReadCloser for the content stored at "path"
   180  // with a given byte offset.
   181  // May be used to resume reading a stream by providing a nonzero offset.
   182  func (d *driver) ReadStream(context ctx.Context, path string, offset int64) (io.ReadCloser, error) {
   183  	name := d.pathToKey(path)
   184  
   185  	// copied from google.golang.org/cloud/storage#NewReader :
   186  	// to set the additional "Range" header
   187  	u := &url.URL{
   188  		Scheme: "https",
   189  		Host:   "storage.googleapis.com",
   190  		Path:   fmt.Sprintf("/%s/%s", d.bucket, name),
   191  	}
   192  	req, err := http.NewRequest("GET", u.String(), nil)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	if offset > 0 {
   197  		req.Header.Set("Range", fmt.Sprintf("bytes=%v-", offset))
   198  	}
   199  	res, err := d.client.Do(req)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	if res.StatusCode == http.StatusNotFound {
   204  		res.Body.Close()
   205  		return nil, storagedriver.PathNotFoundError{Path: path}
   206  	}
   207  	if res.StatusCode == http.StatusRequestedRangeNotSatisfiable {
   208  		res.Body.Close()
   209  		obj, err := storageStatObject(d.context(context), d.bucket, name)
   210  		if err != nil {
   211  			return nil, err
   212  		}
   213  		if offset == int64(obj.Size) {
   214  			return ioutil.NopCloser(bytes.NewReader([]byte{})), nil
   215  		}
   216  		return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset}
   217  	}
   218  	if res.StatusCode < 200 || res.StatusCode > 299 {
   219  		res.Body.Close()
   220  		return nil, fmt.Errorf("storage: can't read object %v/%v, status code: %v", d.bucket, name, res.Status)
   221  	}
   222  	return res.Body, nil
   223  }
   224  
   225  // WriteStream stores the contents of the provided io.ReadCloser at a
   226  // location designated by the given path.
   227  // May be used to resume writing a stream by providing a nonzero offset.
   228  // The offset must be no larger than the CurrentSize for this path.
   229  func (d *driver) WriteStream(context ctx.Context, path string, offset int64, reader io.Reader) (totalRead int64, err error) {
   230  	if offset < 0 {
   231  		return 0, storagedriver.InvalidOffsetError{Path: path, Offset: offset}
   232  	}
   233  
   234  	if offset == 0 {
   235  		return d.writeCompletely(context, path, 0, reader)
   236  	}
   237  
   238  	service, err := storageapi.New(d.client)
   239  	if err != nil {
   240  		return 0, err
   241  	}
   242  	objService := storageapi.NewObjectsService(service)
   243  	var obj *storageapi.Object
   244  	err = retry(5, func() error {
   245  		o, err := objService.Get(d.bucket, d.pathToKey(path)).Do()
   246  		obj = o
   247  		return err
   248  	})
   249  	//	obj, err := retry(5, objService.Get(d.bucket, d.pathToKey(path)).Do)
   250  	if err != nil {
   251  		return 0, err
   252  	}
   253  
   254  	// cannot append more chunks, so redo from scratch
   255  	if obj.ComponentCount >= 1023 {
   256  		return d.writeCompletely(context, path, offset, reader)
   257  	}
   258  
   259  	// skip from reader
   260  	objSize := int64(obj.Size)
   261  	nn, err := skip(reader, objSize-offset)
   262  	if err != nil {
   263  		return nn, err
   264  	}
   265  
   266  	// Size <= offset
   267  	partName := fmt.Sprintf("%v#part-%d#", d.pathToKey(path), obj.ComponentCount)
   268  	gcsContext := d.context(context)
   269  	wc := storage.NewWriter(gcsContext, d.bucket, partName)
   270  	wc.ContentType = "application/octet-stream"
   271  
   272  	if objSize < offset {
   273  		err = writeZeros(wc, offset-objSize)
   274  		if err != nil {
   275  			wc.CloseWithError(err)
   276  			return nn, err
   277  		}
   278  	}
   279  	n, err := io.Copy(wc, reader)
   280  	if err != nil {
   281  		wc.CloseWithError(err)
   282  		return nn, err
   283  	}
   284  	err = wc.Close()
   285  	if err != nil {
   286  		return nn, err
   287  	}
   288  	// wc was closed succesfully, so the temporary part exists, schedule it for deletion at the end
   289  	// of the function
   290  	defer storageDeleteObject(gcsContext, d.bucket, partName)
   291  
   292  	req := &storageapi.ComposeRequest{
   293  		Destination: &storageapi.Object{Bucket: obj.Bucket, Name: obj.Name, ContentType: obj.ContentType},
   294  		SourceObjects: []*storageapi.ComposeRequestSourceObjects{
   295  			{
   296  				Name:       obj.Name,
   297  				Generation: obj.Generation,
   298  			}, {
   299  				Name:       partName,
   300  				Generation: wc.Object().Generation,
   301  			}},
   302  	}
   303  
   304  	err = retry(5, func() error { _, err := objService.Compose(d.bucket, obj.Name, req).Do(); return err })
   305  	if err == nil {
   306  		nn = nn + n
   307  	}
   308  
   309  	return nn, err
   310  }
   311  
   312  type request func() error
   313  
   314  func retry(maxTries int, req request) error {
   315  	backoff := time.Second
   316  	var err error
   317  	for i := 0; i < maxTries; i++ {
   318  		err = req()
   319  		if err == nil {
   320  			return nil
   321  		}
   322  
   323  		status, ok := err.(*googleapi.Error)
   324  		if !ok || (status.Code != 429 && status.Code < http.StatusInternalServerError) {
   325  			return err
   326  		}
   327  
   328  		time.Sleep(backoff - time.Second + (time.Duration(rand.Int31n(1000)) * time.Millisecond))
   329  		if i <= 4 {
   330  			backoff = backoff * 2
   331  		}
   332  	}
   333  	return err
   334  }
   335  
   336  func (d *driver) writeCompletely(context ctx.Context, path string, offset int64, reader io.Reader) (totalRead int64, err error) {
   337  	wc := storage.NewWriter(d.context(context), d.bucket, d.pathToKey(path))
   338  	wc.ContentType = "application/octet-stream"
   339  	defer wc.Close()
   340  
   341  	// Copy the first offset bytes of the existing contents
   342  	// (padded with zeros if needed) into the writer
   343  	if offset > 0 {
   344  		existing, err := d.ReadStream(context, path, 0)
   345  		if err != nil {
   346  			return 0, err
   347  		}
   348  		defer existing.Close()
   349  		n, err := io.CopyN(wc, existing, offset)
   350  		if err == io.EOF {
   351  			err = writeZeros(wc, offset-n)
   352  		}
   353  		if err != nil {
   354  			return 0, err
   355  		}
   356  	}
   357  	return io.Copy(wc, reader)
   358  }
   359  
   360  func skip(reader io.Reader, count int64) (int64, error) {
   361  	if count <= 0 {
   362  		return 0, nil
   363  	}
   364  	return io.CopyN(ioutil.Discard, reader, count)
   365  }
   366  
   367  func writeZeros(wc io.Writer, count int64) error {
   368  	buf := make([]byte, 32*1024)
   369  	for count > 0 {
   370  		size := cap(buf)
   371  		if int64(size) > count {
   372  			size = int(count)
   373  		}
   374  		n, err := wc.Write(buf[0:size])
   375  		if err != nil {
   376  			return err
   377  		}
   378  		count = count - int64(n)
   379  	}
   380  	return nil
   381  }
   382  
   383  // Stat retrieves the FileInfo for the given path, including the current
   384  // size in bytes and the creation time.
   385  func (d *driver) Stat(context ctx.Context, path string) (storagedriver.FileInfo, error) {
   386  	var fi storagedriver.FileInfoFields
   387  	//try to get as file
   388  	gcsContext := d.context(context)
   389  	obj, err := storageStatObject(gcsContext, d.bucket, d.pathToKey(path))
   390  	if err == nil {
   391  		fi = storagedriver.FileInfoFields{
   392  			Path:    path,
   393  			Size:    obj.Size,
   394  			ModTime: obj.Updated,
   395  			IsDir:   false,
   396  		}
   397  		return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil
   398  	}
   399  	//try to get as folder
   400  	dirpath := d.pathToDirKey(path)
   401  
   402  	var query *storage.Query
   403  	query = &storage.Query{}
   404  	query.Prefix = dirpath
   405  	query.MaxResults = 1
   406  
   407  	objects, err := storageListObjects(gcsContext, d.bucket, query)
   408  	if err != nil {
   409  		return nil, err
   410  	}
   411  	if len(objects.Results) < 1 {
   412  		return nil, storagedriver.PathNotFoundError{Path: path}
   413  	}
   414  	fi = storagedriver.FileInfoFields{
   415  		Path:  path,
   416  		IsDir: true,
   417  	}
   418  	obj = objects.Results[0]
   419  	if obj.Name == dirpath {
   420  		fi.Size = obj.Size
   421  		fi.ModTime = obj.Updated
   422  	}
   423  	return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil
   424  }
   425  
   426  // List returns a list of the objects that are direct descendants of the
   427  //given path.
   428  func (d *driver) List(context ctx.Context, path string) ([]string, error) {
   429  	var query *storage.Query
   430  	query = &storage.Query{}
   431  	query.Delimiter = "/"
   432  	query.Prefix = d.pathToDirKey(path)
   433  	list := make([]string, 0, 64)
   434  	for {
   435  		objects, err := storageListObjects(d.context(context), d.bucket, query)
   436  		if err != nil {
   437  			return nil, err
   438  		}
   439  		for _, object := range objects.Results {
   440  			// GCS does not guarantee strong consistency between
   441  			// DELETE and LIST operationsCheck that the object is not deleted,
   442  			// so filter out any objects with a non-zero time-deleted
   443  			if object.Deleted.IsZero() {
   444  				name := object.Name
   445  				// Ignore objects with names that end with '#' (these are uploaded parts)
   446  				if name[len(name)-1] != '#' {
   447  					name = d.keyToPath(name)
   448  					list = append(list, name)
   449  				}
   450  			}
   451  		}
   452  		for _, subpath := range objects.Prefixes {
   453  			subpath = d.keyToPath(subpath)
   454  			list = append(list, subpath)
   455  		}
   456  		query = objects.Next
   457  		if query == nil {
   458  			break
   459  		}
   460  	}
   461  	if path != "/" && len(list) == 0 {
   462  		// Treat empty response as missing directory, since we don't actually
   463  		// have directories in Google Cloud Storage.
   464  		return nil, storagedriver.PathNotFoundError{Path: path}
   465  	}
   466  	return list, nil
   467  }
   468  
   469  // Move moves an object stored at sourcePath to destPath, removing the
   470  // original object.
   471  func (d *driver) Move(context ctx.Context, sourcePath string, destPath string) error {
   472  	prefix := d.pathToDirKey(sourcePath)
   473  	gcsContext := d.context(context)
   474  	keys, err := d.listAll(gcsContext, prefix)
   475  	if err != nil {
   476  		return err
   477  	}
   478  	if len(keys) > 0 {
   479  		destPrefix := d.pathToDirKey(destPath)
   480  		copies := make([]string, 0, len(keys))
   481  		sort.Strings(keys)
   482  		var err error
   483  		for _, key := range keys {
   484  			dest := destPrefix + key[len(prefix):]
   485  			_, err = storageCopyObject(gcsContext, d.bucket, key, d.bucket, dest, nil)
   486  			if err == nil {
   487  				copies = append(copies, dest)
   488  			} else {
   489  				break
   490  			}
   491  		}
   492  		// if an error occurred, attempt to cleanup the copies made
   493  		if err != nil {
   494  			for i := len(copies) - 1; i >= 0; i-- {
   495  				_ = storageDeleteObject(gcsContext, d.bucket, copies[i])
   496  			}
   497  			return err
   498  		}
   499  		// delete originals
   500  		for i := len(keys) - 1; i >= 0; i-- {
   501  			err2 := storageDeleteObject(gcsContext, d.bucket, keys[i])
   502  			if err2 != nil {
   503  				err = err2
   504  			}
   505  		}
   506  		return err
   507  	}
   508  	_, err = storageCopyObject(gcsContext, d.bucket, d.pathToKey(sourcePath), d.bucket, d.pathToKey(destPath), nil)
   509  	if err != nil {
   510  		if status := err.(*googleapi.Error); status != nil {
   511  			if status.Code == http.StatusNotFound {
   512  				return storagedriver.PathNotFoundError{Path: sourcePath}
   513  			}
   514  		}
   515  		return err
   516  	}
   517  	return storageDeleteObject(gcsContext, d.bucket, d.pathToKey(sourcePath))
   518  }
   519  
   520  // listAll recursively lists all names of objects stored at "prefix" and its subpaths.
   521  func (d *driver) listAll(context context.Context, prefix string) ([]string, error) {
   522  	list := make([]string, 0, 64)
   523  	query := &storage.Query{}
   524  	query.Prefix = prefix
   525  	query.Versions = false
   526  	for {
   527  		objects, err := storageListObjects(d.context(context), d.bucket, query)
   528  		if err != nil {
   529  			return nil, err
   530  		}
   531  		for _, obj := range objects.Results {
   532  			// GCS does not guarantee strong consistency between
   533  			// DELETE and LIST operationsCheck that the object is not deleted,
   534  			// so filter out any objects with a non-zero time-deleted
   535  			if obj.Deleted.IsZero() {
   536  				list = append(list, obj.Name)
   537  			}
   538  		}
   539  		query = objects.Next
   540  		if query == nil {
   541  			break
   542  		}
   543  	}
   544  	return list, nil
   545  }
   546  
   547  // Delete recursively deletes all objects stored at "path" and its subpaths.
   548  func (d *driver) Delete(context ctx.Context, path string) error {
   549  	prefix := d.pathToDirKey(path)
   550  	gcsContext := d.context(context)
   551  	keys, err := d.listAll(gcsContext, prefix)
   552  	if err != nil {
   553  		return err
   554  	}
   555  	if len(keys) > 0 {
   556  		sort.Sort(sort.Reverse(sort.StringSlice(keys)))
   557  		for _, key := range keys {
   558  			err := storageDeleteObject(gcsContext, d.bucket, key)
   559  			// GCS only guarantees eventual consistency, so listAll might return
   560  			// paths that no longer exist. If this happens, just ignore any not
   561  			// found error
   562  			if status, ok := err.(*googleapi.Error); ok {
   563  				if status.Code == http.StatusNotFound {
   564  					err = nil
   565  				}
   566  			}
   567  			if err != nil {
   568  				return err
   569  			}
   570  		}
   571  		return nil
   572  	}
   573  	err = storageDeleteObject(gcsContext, d.bucket, d.pathToKey(path))
   574  	if err != nil {
   575  		if status := err.(*googleapi.Error); status != nil {
   576  			if status.Code == http.StatusNotFound {
   577  				return storagedriver.PathNotFoundError{Path: path}
   578  			}
   579  		}
   580  	}
   581  	return err
   582  }
   583  
   584  func storageDeleteObject(context context.Context, bucket string, name string) error {
   585  	return retry(5, func() error {
   586  		return storage.DeleteObject(context, bucket, name)
   587  	})
   588  }
   589  
   590  func storageStatObject(context context.Context, bucket string, name string) (*storage.Object, error) {
   591  	var obj *storage.Object
   592  	err := retry(5, func() error {
   593  		var err error
   594  		obj, err = storage.StatObject(context, bucket, name)
   595  		return err
   596  	})
   597  	return obj, err
   598  }
   599  
   600  func storageListObjects(context context.Context, bucket string, q *storage.Query) (*storage.Objects, error) {
   601  	var objs *storage.Objects
   602  	err := retry(5, func() error {
   603  		var err error
   604  		objs, err = storage.ListObjects(context, bucket, q)
   605  		return err
   606  	})
   607  	return objs, err
   608  }
   609  
   610  func storageCopyObject(context context.Context, srcBucket, srcName string, destBucket, destName string, attrs *storage.ObjectAttrs) (*storage.Object, error) {
   611  	var obj *storage.Object
   612  	err := retry(5, func() error {
   613  		var err error
   614  		obj, err = storage.CopyObject(context, srcBucket, srcName, destBucket, destName, attrs)
   615  		return err
   616  	})
   617  	return obj, err
   618  }
   619  
   620  // URLFor returns a URL which may be used to retrieve the content stored at
   621  // the given path, possibly using the given options.
   622  // Returns ErrUnsupportedMethod if this driver has no privateKey
   623  func (d *driver) URLFor(context ctx.Context, path string, options map[string]interface{}) (string, error) {
   624  	if d.privateKey == nil {
   625  		return "", storagedriver.ErrUnsupportedMethod{}
   626  	}
   627  
   628  	name := d.pathToKey(path)
   629  	methodString := "GET"
   630  	method, ok := options["method"]
   631  	if ok {
   632  		methodString, ok = method.(string)
   633  		if !ok || (methodString != "GET" && methodString != "HEAD") {
   634  			return "", storagedriver.ErrUnsupportedMethod{}
   635  		}
   636  	}
   637  
   638  	expiresTime := time.Now().Add(20 * time.Minute)
   639  	expires, ok := options["expiry"]
   640  	if ok {
   641  		et, ok := expires.(time.Time)
   642  		if ok {
   643  			expiresTime = et
   644  		}
   645  	}
   646  
   647  	opts := &storage.SignedURLOptions{
   648  		GoogleAccessID: d.email,
   649  		PrivateKey:     d.privateKey,
   650  		Method:         methodString,
   651  		Expires:        expiresTime,
   652  	}
   653  	return storage.SignedURL(d.bucket, name, opts)
   654  }
   655  
   656  func (d *driver) context(context ctx.Context) context.Context {
   657  	return cloud.WithContext(context, dummyProjectID, d.client)
   658  }
   659  
   660  func (d *driver) pathToKey(path string) string {
   661  	return strings.TrimRight(d.rootDirectory+strings.TrimLeft(path, "/"), "/")
   662  }
   663  
   664  func (d *driver) pathToDirKey(path string) string {
   665  	return d.pathToKey(path) + "/"
   666  }
   667  
   668  func (d *driver) keyToPath(key string) string {
   669  	return "/" + strings.Trim(strings.TrimPrefix(key, d.rootDirectory), "/")
   670  }