github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/blob/fileblob/fileblob.go (about)

     1  // Copyright 2018 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package fileblob provides a blob implementation that uses the filesystem.
    16  // Use OpenBucket to construct a *blob.Bucket.
    17  //
    18  // By default fileblob stores blob metadata in 'sidecar files' under the original
    19  // filename but an additional ".attrs" suffix.
    20  // That behaviour can be changed via Options.Metadata;
    21  // writing of those metadata files can be suppressed by setting it to
    22  // 'MetadataDontWrite' or its equivalent "metadata=skip" in the URL for the opener.
    23  // In any case, absent any stored metadata many blob.Attributes fields
    24  // will be set to default values.
    25  //
    26  // # URLs
    27  //
    28  // For blob.OpenBucket, fileblob registers for the scheme "file".
    29  // To customize the URL opener, or for more details on the URL format,
    30  // see URLOpener.
    31  // See https://gocloud.dev/concepts/urls/ for background information.
    32  //
    33  // # Escaping
    34  //
    35  // Go CDK supports all UTF-8 strings; to make this work with services lacking
    36  // full UTF-8 support, strings must be escaped (during writes) and unescaped
    37  // (during reads). The following escapes are performed for fileblob:
    38  //   - Blob keys: ASCII characters 0-31 are escaped to "__0x<hex>__".
    39  //     If os.PathSeparator != "/", it is also escaped.
    40  //     Additionally, the "/" in "../", the trailing "/" in "//", and a trailing
    41  //     "/" is key names are escaped in the same way.
    42  //     On Windows, the characters "<>:"|?*" are also escaped.
    43  //
    44  // # As
    45  //
    46  // fileblob exposes the following types for As:
    47  //   - Bucket: os.FileInfo
    48  //   - Error: *os.PathError
    49  //   - ListObject: os.FileInfo
    50  //   - Reader: io.Reader
    51  //   - ReaderOptions.BeforeRead: *os.File
    52  //   - Attributes: os.FileInfo
    53  //   - CopyOptions.BeforeCopy: *os.File
    54  //   - WriterOptions.BeforeWrite: *os.File
    55  package fileblob // import "gocloud.dev/blob/fileblob"
    56  
    57  import (
    58  	"context"
    59  	"crypto/hmac"
    60  	"crypto/md5"
    61  	"crypto/sha256"
    62  	"encoding/base64"
    63  	"errors"
    64  	"fmt"
    65  	"hash"
    66  	"io"
    67  	"io/fs"
    68  	"io/ioutil"
    69  	"net/url"
    70  	"os"
    71  	"path/filepath"
    72  	"strconv"
    73  	"strings"
    74  	"time"
    75  
    76  	"gocloud.dev/blob"
    77  	"gocloud.dev/blob/driver"
    78  	"gocloud.dev/gcerrors"
    79  	"gocloud.dev/internal/escape"
    80  	"gocloud.dev/internal/gcerr"
    81  )
    82  
    83  const defaultPageSize = 1000
    84  
    85  func init() {
    86  	blob.DefaultURLMux().RegisterBucket(Scheme, &URLOpener{})
    87  }
    88  
    89  // Scheme is the URL scheme fileblob registers its URLOpener under on
    90  // blob.DefaultMux.
    91  const Scheme = "file"
    92  
    93  // URLOpener opens file bucket URLs like "file:///foo/bar/baz".
    94  //
    95  // The URL's host is ignored unless it is ".", which is used to signal a
    96  // relative path. For example, "file://./../.." uses "../.." as the path.
    97  //
    98  // If os.PathSeparator != "/", any leading "/" from the path is dropped
    99  // and remaining '/' characters are converted to os.PathSeparator.
   100  //
   101  // The following query parameters are supported:
   102  //
   103  //   - create_dir: (any non-empty value) the directory is created (using os.MkDirAll)
   104  //     if it does not already exist.
   105  //   - base_url: the base URL to use to construct signed URLs; see URLSignerHMAC
   106  //   - secret_key_path: path to read for the secret key used to construct signed URLs;
   107  //     see URLSignerHMAC
   108  //   - metadata: if set to "skip", won't write metadata such as blob.Attributes
   109  //     as per the package docstring
   110  //
   111  // If either of base_url / secret_key_path are provided, both must be.
   112  //
   113  //   - file:///a/directory
   114  //     -> Passes "/a/directory" to OpenBucket.
   115  //   - file://localhost/a/directory
   116  //     -> Also passes "/a/directory".
   117  //   - file://./../..
   118  //     -> The hostname is ".", signaling a relative path; passes "../..".
   119  //   - file:///c:/foo/bar on Windows.
   120  //     -> Passes "c:\foo\bar".
   121  //   - file://localhost/c:/foo/bar on Windows.
   122  //     -> Also passes "c:\foo\bar".
   123  //   - file:///a/directory?base_url=/show&secret_key_path=secret.key
   124  //     -> Passes "/a/directory" to OpenBucket, and sets Options.URLSigner
   125  //     to a URLSignerHMAC initialized with base URL "/show" and secret key
   126  //     bytes read from the file "secret.key".
   127  type URLOpener struct {
   128  	// Options specifies the default options to pass to OpenBucket.
   129  	Options Options
   130  }
   131  
   132  // OpenBucketURL opens a blob.Bucket based on u.
   133  func (o *URLOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) {
   134  	path := u.Path
   135  	// Hostname == "." means a relative path, so drop the leading "/".
   136  	// Also drop the leading "/" on Windows.
   137  	if u.Host == "." || os.PathSeparator != '/' {
   138  		path = strings.TrimPrefix(path, "/")
   139  	}
   140  	opts, err := o.forParams(ctx, u.Query())
   141  	if err != nil {
   142  		return nil, fmt.Errorf("open bucket %v: %v", u, err)
   143  	}
   144  	return OpenBucket(filepath.FromSlash(path), opts)
   145  }
   146  
   147  var recognizedParams = map[string]bool{
   148  	"create_dir":      true,
   149  	"base_url":        true,
   150  	"secret_key_path": true,
   151  	"metadata":        true,
   152  }
   153  
   154  type metadataOption string // Not exported as subject to change.
   155  
   156  // Settings for Options.Metadata.
   157  const (
   158  	// Metadata gets written to a separate file.
   159  	MetadataInSidecar metadataOption = ""
   160  	// Writes won't carry metadata, as per the package docstring.
   161  	MetadataDontWrite metadataOption = "skip"
   162  )
   163  
   164  func (o *URLOpener) forParams(ctx context.Context, q url.Values) (*Options, error) {
   165  	for k := range q {
   166  		if _, ok := recognizedParams[k]; !ok {
   167  			return nil, fmt.Errorf("invalid query parameter %q", k)
   168  		}
   169  	}
   170  	opts := new(Options)
   171  	*opts = o.Options
   172  
   173  	// Note: can't just use q.Get, because then we can't distinguish between
   174  	// "not set" (we should leave opts alone) vs "set to empty string" (which is
   175  	// one of the legal values, we should override opts).
   176  	metadataVal := q["metadata"]
   177  	if len(metadataVal) > 0 {
   178  		switch metadataOption(metadataVal[0]) {
   179  		case MetadataDontWrite:
   180  			opts.Metadata = MetadataDontWrite
   181  		case MetadataInSidecar:
   182  			opts.Metadata = MetadataInSidecar
   183  		default:
   184  			return nil, errors.New("fileblob.OpenBucket: unsupported value for query parameter 'metadata'")
   185  		}
   186  	}
   187  	if q.Get("create_dir") != "" {
   188  		opts.CreateDir = true
   189  	}
   190  	baseURL := q.Get("base_url")
   191  	keyPath := q.Get("secret_key_path")
   192  	if (baseURL == "") != (keyPath == "") {
   193  		return nil, errors.New("must supply both base_url and secret_key_path query parameters")
   194  	}
   195  	if baseURL != "" {
   196  		burl, err := url.Parse(baseURL)
   197  		if err != nil {
   198  			return nil, err
   199  		}
   200  		sk, err := ioutil.ReadFile(keyPath)
   201  		if err != nil {
   202  			return nil, err
   203  		}
   204  		opts.URLSigner = NewURLSignerHMAC(burl, sk)
   205  	}
   206  	return opts, nil
   207  }
   208  
   209  // Options sets options for constructing a *blob.Bucket backed by fileblob.
   210  type Options struct {
   211  	// URLSigner implements signing URLs (to allow access to a resource without
   212  	// further authorization) and verifying that a given URL is unexpired and
   213  	// contains a signature produced by the URLSigner.
   214  	// URLSigner is only required for utilizing the SignedURL API.
   215  	URLSigner URLSigner
   216  
   217  	// If true, create the directory backing the Bucket if it does not exist
   218  	// (using os.MkdirAll).
   219  	CreateDir bool
   220  
   221  	// Refers to the strategy for how to deal with metadata (such as blob.Attributes).
   222  	// For supported values please see the Metadata* constants.
   223  	// If left unchanged, 'MetadataInSidecar' will be used.
   224  	Metadata metadataOption
   225  }
   226  
   227  type bucket struct {
   228  	dir  string
   229  	opts *Options
   230  }
   231  
   232  // openBucket creates a driver.Bucket that reads and writes to dir.
   233  // dir must exist.
   234  func openBucket(dir string, opts *Options) (driver.Bucket, error) {
   235  	if opts == nil {
   236  		opts = &Options{}
   237  	}
   238  	absdir, err := filepath.Abs(dir)
   239  	if err != nil {
   240  		return nil, fmt.Errorf("failed to convert %s into an absolute path: %v", dir, err)
   241  	}
   242  	info, err := os.Stat(absdir)
   243  
   244  	// Optionally, create the directory if it does not already exist.
   245  	if err != nil && opts.CreateDir && os.IsNotExist(err) {
   246  		err = os.MkdirAll(absdir, os.FileMode(0777))
   247  		if err != nil {
   248  			return nil, fmt.Errorf("tried to create directory but failed: %v", err)
   249  		}
   250  		info, err = os.Stat(absdir)
   251  	}
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	if !info.IsDir() {
   256  		return nil, fmt.Errorf("%s is not a directory", absdir)
   257  	}
   258  	return &bucket{dir: absdir, opts: opts}, nil
   259  }
   260  
   261  // OpenBucket creates a *blob.Bucket backed by the filesystem and rooted at
   262  // dir, which must exist. See the package documentation for an example.
   263  func OpenBucket(dir string, opts *Options) (*blob.Bucket, error) {
   264  	drv, err := openBucket(dir, opts)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	return blob.NewBucket(drv), nil
   269  }
   270  
   271  func (b *bucket) Close() error {
   272  	return nil
   273  }
   274  
   275  // escapeKey does all required escaping for UTF-8 strings to work the filesystem.
   276  func escapeKey(s string) string {
   277  	s = escape.HexEscape(s, func(r []rune, i int) bool {
   278  		c := r[i]
   279  		switch {
   280  		case c < 32:
   281  			return true
   282  		// We're going to replace '/' with os.PathSeparator below. In order for this
   283  		// to be reversible, we need to escape raw os.PathSeparators.
   284  		case os.PathSeparator != '/' && c == os.PathSeparator:
   285  			return true
   286  		// For "../", escape the trailing slash.
   287  		case i > 1 && c == '/' && r[i-1] == '.' && r[i-2] == '.':
   288  			return true
   289  		// For "//", escape the trailing slash.
   290  		case i > 0 && c == '/' && r[i-1] == '/':
   291  			return true
   292  		// Escape the trailing slash in a key.
   293  		case c == '/' && i == len(r)-1:
   294  			return true
   295  		// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
   296  		case os.PathSeparator == '\\' && (c == '>' || c == '<' || c == ':' || c == '"' || c == '|' || c == '?' || c == '*'):
   297  			return true
   298  		}
   299  		return false
   300  	})
   301  	// Replace "/" with os.PathSeparator if needed, so that the local filesystem
   302  	// can use subdirectories.
   303  	if os.PathSeparator != '/' {
   304  		s = strings.Replace(s, "/", string(os.PathSeparator), -1)
   305  	}
   306  	return s
   307  }
   308  
   309  // unescapeKey reverses escapeKey.
   310  func unescapeKey(s string) string {
   311  	if os.PathSeparator != '/' {
   312  		s = strings.Replace(s, string(os.PathSeparator), "/", -1)
   313  	}
   314  	s = escape.HexUnescape(s)
   315  	return s
   316  }
   317  
   318  func (b *bucket) ErrorCode(err error) gcerrors.ErrorCode {
   319  	switch {
   320  	case os.IsNotExist(err):
   321  		return gcerrors.NotFound
   322  	default:
   323  		return gcerrors.Unknown
   324  	}
   325  }
   326  
   327  // path returns the full path for a key
   328  func (b *bucket) path(key string) (string, error) {
   329  	path := filepath.Join(b.dir, escapeKey(key))
   330  	if strings.HasSuffix(path, attrsExt) {
   331  		return "", errAttrsExt
   332  	}
   333  	return path, nil
   334  }
   335  
   336  // forKey returns the full path, os.FileInfo, and attributes for key.
   337  func (b *bucket) forKey(key string) (string, os.FileInfo, *xattrs, error) {
   338  	path, err := b.path(key)
   339  	if err != nil {
   340  		return "", nil, nil, err
   341  	}
   342  	info, err := os.Stat(path)
   343  	if err != nil {
   344  		return "", nil, nil, err
   345  	}
   346  	if info.IsDir() {
   347  		return "", nil, nil, os.ErrNotExist
   348  	}
   349  	xa, err := getAttrs(path)
   350  	if err != nil {
   351  		return "", nil, nil, err
   352  	}
   353  	return path, info, &xa, nil
   354  }
   355  
   356  // ListPaged implements driver.ListPaged.
   357  func (b *bucket) ListPaged(ctx context.Context, opts *driver.ListOptions) (*driver.ListPage, error) {
   358  
   359  	var pageToken string
   360  	if len(opts.PageToken) > 0 {
   361  		pageToken = string(opts.PageToken)
   362  	}
   363  	pageSize := opts.PageSize
   364  	if pageSize == 0 {
   365  		pageSize = defaultPageSize
   366  	}
   367  	// If opts.Delimiter != "", lastPrefix contains the last "directory" key we
   368  	// added. It is used to avoid adding it again; all files in this "directory"
   369  	// are collapsed to the single directory entry.
   370  	var lastPrefix string
   371  	var lastKeyAdded string
   372  
   373  	// If the Prefix contains a "/", we can set the root of the Walk
   374  	// to the path specified by the Prefix as any files below the path will not
   375  	// match the Prefix.
   376  	// Note that we use "/" explicitly and not os.PathSeparator, as the opts.Prefix
   377  	// is in the unescaped form.
   378  	root := b.dir
   379  	if i := strings.LastIndex(opts.Prefix, "/"); i > -1 {
   380  		root = filepath.Join(root, opts.Prefix[:i])
   381  	}
   382  
   383  	// Do a full recursive scan of the root directory.
   384  	var result driver.ListPage
   385  	err := filepath.WalkDir(root, func(path string, info fs.DirEntry, err error) error {
   386  		if err != nil {
   387  			// Couldn't read this file/directory for some reason; just skip it.
   388  			return nil
   389  		}
   390  		// Skip the self-generated attribute files.
   391  		if strings.HasSuffix(path, attrsExt) {
   392  			return nil
   393  		}
   394  		// os.Walk returns the root directory; skip it.
   395  		if path == b.dir {
   396  			return nil
   397  		}
   398  		// Strip the <b.dir> prefix from path.
   399  		prefixLen := len(b.dir)
   400  		// Include the separator for non-root.
   401  		if b.dir != "/" {
   402  			prefixLen++
   403  		}
   404  		path = path[prefixLen:]
   405  		// Unescape the path to get the key.
   406  		key := unescapeKey(path)
   407  		// Skip all directories. If opts.Delimiter is set, we'll create
   408  		// pseudo-directories later.
   409  		// Note that returning nil means that we'll still recurse into it;
   410  		// we're just not adding a result for the directory itself.
   411  		if info.IsDir() {
   412  			key += "/"
   413  			// Avoid recursing into subdirectories if the directory name already
   414  			// doesn't match the prefix; any files in it are guaranteed not to match.
   415  			if len(key) > len(opts.Prefix) && !strings.HasPrefix(key, opts.Prefix) {
   416  				return filepath.SkipDir
   417  			}
   418  			// Similarly, avoid recursing into subdirectories if we're making
   419  			// "directories" and all of the files in this subdirectory are guaranteed
   420  			// to collapse to a "directory" that we've already added.
   421  			if lastPrefix != "" && strings.HasPrefix(key, lastPrefix) {
   422  				return filepath.SkipDir
   423  			}
   424  			return nil
   425  		}
   426  		// Skip files/directories that don't match the Prefix.
   427  		if !strings.HasPrefix(key, opts.Prefix) {
   428  			return nil
   429  		}
   430  		var md5 []byte
   431  		if xa, err := getAttrs(path); err == nil {
   432  			// Note: we only have the MD5 hash for blobs that we wrote.
   433  			// For other blobs, md5 will remain nil.
   434  			md5 = xa.MD5
   435  		}
   436  		fi, err := info.Info()
   437  		if err != nil {
   438  			return err
   439  		}
   440  		asFunc := func(i interface{}) bool {
   441  			p, ok := i.(*os.FileInfo)
   442  			if !ok {
   443  				return false
   444  			}
   445  			*p = fi
   446  			return true
   447  		}
   448  		obj := &driver.ListObject{
   449  			Key:     key,
   450  			ModTime: fi.ModTime(),
   451  			Size:    fi.Size(),
   452  			MD5:     md5,
   453  			AsFunc:  asFunc,
   454  		}
   455  		// If using Delimiter, collapse "directories".
   456  		if opts.Delimiter != "" {
   457  			// Strip the prefix, which may contain Delimiter.
   458  			keyWithoutPrefix := key[len(opts.Prefix):]
   459  			// See if the key still contains Delimiter.
   460  			// If no, it's a file and we just include it.
   461  			// If yes, it's a file in a "sub-directory" and we want to collapse
   462  			// all files in that "sub-directory" into a single "directory" result.
   463  			if idx := strings.Index(keyWithoutPrefix, opts.Delimiter); idx != -1 {
   464  				prefix := opts.Prefix + keyWithoutPrefix[0:idx+len(opts.Delimiter)]
   465  				// We've already included this "directory"; don't add it.
   466  				if prefix == lastPrefix {
   467  					return nil
   468  				}
   469  				// Update the object to be a "directory".
   470  				obj = &driver.ListObject{
   471  					Key:    prefix,
   472  					IsDir:  true,
   473  					AsFunc: asFunc,
   474  				}
   475  				lastPrefix = prefix
   476  			}
   477  		}
   478  		// If there's a pageToken, skip anything before it.
   479  		if pageToken != "" && obj.Key <= pageToken {
   480  			return nil
   481  		}
   482  		// If we've already got a full page of results, set NextPageToken and stop.
   483  		// Unless the current object is a directory, in which case there may
   484  		// still be objects coming that are alphabetically before it (since
   485  		// we appended the delimiter). In that case, keep going; we'll trim the
   486  		// extra entries (if any) before returning.
   487  		if len(result.Objects) == pageSize && !obj.IsDir {
   488  			result.NextPageToken = []byte(result.Objects[pageSize-1].Key)
   489  			return io.EOF
   490  		}
   491  		result.Objects = append(result.Objects, obj)
   492  		// Normally, objects are added in the correct order (by Key).
   493  		// However, sometimes adding the file delimiter messes that up (e.g.,
   494  		// if the file delimiter is later in the alphabet than the last character
   495  		// of a key).
   496  		// Detect if this happens and swap if needed.
   497  		if len(result.Objects) > 1 && obj.Key < lastKeyAdded {
   498  			i := len(result.Objects) - 1
   499  			result.Objects[i-1], result.Objects[i] = result.Objects[i], result.Objects[i-1]
   500  			lastKeyAdded = result.Objects[i].Key
   501  		} else {
   502  			lastKeyAdded = obj.Key
   503  		}
   504  		return nil
   505  	})
   506  	if err != nil && err != io.EOF {
   507  		return nil, err
   508  	}
   509  	if len(result.Objects) > pageSize {
   510  		result.Objects = result.Objects[0:pageSize]
   511  		result.NextPageToken = []byte(result.Objects[pageSize-1].Key)
   512  	}
   513  	return &result, nil
   514  }
   515  
   516  // As implements driver.As.
   517  func (b *bucket) As(i interface{}) bool {
   518  	p, ok := i.(*os.FileInfo)
   519  	if !ok {
   520  		return false
   521  	}
   522  	fi, err := os.Stat(b.dir)
   523  	if err != nil {
   524  		return false
   525  	}
   526  	*p = fi
   527  	return true
   528  }
   529  
   530  // As implements driver.ErrorAs.
   531  func (b *bucket) ErrorAs(err error, i interface{}) bool {
   532  	if perr, ok := err.(*os.PathError); ok {
   533  		if p, ok := i.(**os.PathError); ok {
   534  			*p = perr
   535  			return true
   536  		}
   537  	}
   538  	return false
   539  }
   540  
   541  // Attributes implements driver.Attributes.
   542  func (b *bucket) Attributes(ctx context.Context, key string) (*driver.Attributes, error) {
   543  	_, info, xa, err := b.forKey(key)
   544  	if err != nil {
   545  		return nil, err
   546  	}
   547  	return &driver.Attributes{
   548  		CacheControl:       xa.CacheControl,
   549  		ContentDisposition: xa.ContentDisposition,
   550  		ContentEncoding:    xa.ContentEncoding,
   551  		ContentLanguage:    xa.ContentLanguage,
   552  		ContentType:        xa.ContentType,
   553  		Metadata:           xa.Metadata,
   554  		// CreateTime left as the zero time.
   555  		ModTime: info.ModTime(),
   556  		Size:    info.Size(),
   557  		MD5:     xa.MD5,
   558  		ETag:    fmt.Sprintf("\"%x-%x\"", info.ModTime().UnixNano(), info.Size()),
   559  		AsFunc: func(i interface{}) bool {
   560  			p, ok := i.(*os.FileInfo)
   561  			if !ok {
   562  				return false
   563  			}
   564  			*p = info
   565  			return true
   566  		},
   567  	}, nil
   568  }
   569  
   570  // NewRangeReader implements driver.NewRangeReader.
   571  func (b *bucket) NewRangeReader(ctx context.Context, key string, offset, length int64, opts *driver.ReaderOptions) (driver.Reader, error) {
   572  	path, info, xa, err := b.forKey(key)
   573  	if err != nil {
   574  		return nil, err
   575  	}
   576  	f, err := os.Open(path)
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  	if opts.BeforeRead != nil {
   581  		if err := opts.BeforeRead(func(i interface{}) bool {
   582  			p, ok := i.(**os.File)
   583  			if !ok {
   584  				return false
   585  			}
   586  			*p = f
   587  			return true
   588  		}); err != nil {
   589  			return nil, err
   590  		}
   591  	}
   592  	if offset > 0 {
   593  		if _, err := f.Seek(offset, io.SeekStart); err != nil {
   594  			return nil, err
   595  		}
   596  	}
   597  	r := io.Reader(f)
   598  	if length >= 0 {
   599  		r = io.LimitReader(r, length)
   600  	}
   601  	return &reader{
   602  		r: r,
   603  		c: f,
   604  		attrs: driver.ReaderAttributes{
   605  			ContentType: xa.ContentType,
   606  			ModTime:     info.ModTime(),
   607  			Size:        info.Size(),
   608  		},
   609  	}, nil
   610  }
   611  
   612  type reader struct {
   613  	r     io.Reader
   614  	c     io.Closer
   615  	attrs driver.ReaderAttributes
   616  }
   617  
   618  func (r *reader) Read(p []byte) (int, error) {
   619  	if r.r == nil {
   620  		return 0, io.EOF
   621  	}
   622  	return r.r.Read(p)
   623  }
   624  
   625  func (r *reader) Close() error {
   626  	if r.c == nil {
   627  		return nil
   628  	}
   629  	return r.c.Close()
   630  }
   631  
   632  func (r *reader) Attributes() *driver.ReaderAttributes {
   633  	return &r.attrs
   634  }
   635  
   636  func (r *reader) As(i interface{}) bool {
   637  	p, ok := i.(*io.Reader)
   638  	if !ok {
   639  		return false
   640  	}
   641  	*p = r.r
   642  	return true
   643  }
   644  
   645  func createTemp(path string) (*os.File, error) {
   646  	// Use a custom createTemp function rather than os.CreateTemp() as
   647  	// os.CreateTemp() sets the permissions of the tempfile to 0600, rather than
   648  	// 0666, making it inconsistent with the directories and attribute files.
   649  	try := 0
   650  	for {
   651  		// Append the current time with nanosecond precision and .tmp to the
   652  		// path. If the file already exists try again. Nanosecond changes enough
   653  		// between each iteration to make a conflict unlikely. Using the full
   654  		// time lowers the chance of a collision with a file using a similar
   655  		// pattern, but has undefined behavior after the year 2262.
   656  		name := path + "." + strconv.FormatInt(time.Now().UnixNano(), 16) + ".tmp"
   657  		f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
   658  		if os.IsExist(err) {
   659  			if try++; try < 10000 {
   660  				continue
   661  			}
   662  			return nil, &os.PathError{Op: "createtemp", Path: path + ".*.tmp", Err: os.ErrExist}
   663  		}
   664  		return f, err
   665  	}
   666  }
   667  
   668  // NewTypedWriter implements driver.NewTypedWriter.
   669  func (b *bucket) NewTypedWriter(ctx context.Context, key string, contentType string, opts *driver.WriterOptions) (driver.Writer, error) {
   670  	path, err := b.path(key)
   671  	if err != nil {
   672  		return nil, err
   673  	}
   674  	if err := os.MkdirAll(filepath.Dir(path), os.FileMode(0777)); err != nil {
   675  		return nil, err
   676  	}
   677  	f, err := createTemp(path)
   678  	if err != nil {
   679  		return nil, err
   680  	}
   681  	if opts.BeforeWrite != nil {
   682  		if err := opts.BeforeWrite(func(i interface{}) bool {
   683  			p, ok := i.(**os.File)
   684  			if !ok {
   685  				return false
   686  			}
   687  			*p = f
   688  			return true
   689  		}); err != nil {
   690  			return nil, err
   691  		}
   692  	}
   693  
   694  	if b.opts.Metadata == MetadataDontWrite {
   695  		w := &writer{
   696  			ctx:  ctx,
   697  			File: f,
   698  			path: path,
   699  		}
   700  		return w, nil
   701  	}
   702  
   703  	var metadata map[string]string
   704  	if len(opts.Metadata) > 0 {
   705  		metadata = opts.Metadata
   706  	}
   707  	attrs := xattrs{
   708  		CacheControl:       opts.CacheControl,
   709  		ContentDisposition: opts.ContentDisposition,
   710  		ContentEncoding:    opts.ContentEncoding,
   711  		ContentLanguage:    opts.ContentLanguage,
   712  		ContentType:        contentType,
   713  		Metadata:           metadata,
   714  	}
   715  	w := &writerWithSidecar{
   716  		ctx:        ctx,
   717  		f:          f,
   718  		path:       path,
   719  		attrs:      attrs,
   720  		contentMD5: opts.ContentMD5,
   721  		md5hash:    md5.New(),
   722  	}
   723  	return w, nil
   724  }
   725  
   726  // writerWithSidecar implements the strategy of storing metadata in a distinct file.
   727  type writerWithSidecar struct {
   728  	ctx        context.Context
   729  	f          *os.File
   730  	path       string
   731  	attrs      xattrs
   732  	contentMD5 []byte
   733  	// We compute the MD5 hash so that we can store it with the file attributes,
   734  	// not for verification.
   735  	md5hash hash.Hash
   736  }
   737  
   738  func (w *writerWithSidecar) Write(p []byte) (n int, err error) {
   739  	n, err = w.f.Write(p)
   740  	if err != nil {
   741  		// Don't hash the unwritten tail twice when writing is resumed.
   742  		w.md5hash.Write(p[:n])
   743  		return n, err
   744  	}
   745  	if _, err := w.md5hash.Write(p); err != nil {
   746  		return n, err
   747  	}
   748  	return n, nil
   749  }
   750  
   751  func (w *writerWithSidecar) Close() error {
   752  	err := w.f.Close()
   753  	if err != nil {
   754  		return err
   755  	}
   756  	// Always delete the temp file. On success, it will have been renamed so
   757  	// the Remove will fail.
   758  	defer func() {
   759  		_ = os.Remove(w.f.Name())
   760  	}()
   761  
   762  	// Check if the write was cancelled.
   763  	if err := w.ctx.Err(); err != nil {
   764  		return err
   765  	}
   766  
   767  	md5sum := w.md5hash.Sum(nil)
   768  	w.attrs.MD5 = md5sum
   769  
   770  	// Write the attributes file.
   771  	if err := setAttrs(w.path, w.attrs); err != nil {
   772  		return err
   773  	}
   774  	// Rename the temp file to path.
   775  	if err := os.Rename(w.f.Name(), w.path); err != nil {
   776  		_ = os.Remove(w.path + attrsExt)
   777  		return err
   778  	}
   779  	return nil
   780  }
   781  
   782  // writer is a file with a temporary name until closed.
   783  //
   784  // Embedding os.File allows the likes of io.Copy to use optimizations.,
   785  // which is why it is not folded into writerWithSidecar.
   786  type writer struct {
   787  	*os.File
   788  	ctx  context.Context
   789  	path string
   790  }
   791  
   792  func (w *writer) Close() error {
   793  	err := w.File.Close()
   794  	if err != nil {
   795  		return err
   796  	}
   797  	// Always delete the temp file. On success, it will have been renamed so
   798  	// the Remove will fail.
   799  	tempname := w.File.Name()
   800  	defer os.Remove(tempname)
   801  
   802  	// Check if the write was cancelled.
   803  	if err := w.ctx.Err(); err != nil {
   804  		return err
   805  	}
   806  
   807  	// Rename the temp file to path.
   808  	if err := os.Rename(tempname, w.path); err != nil {
   809  		return err
   810  	}
   811  	return nil
   812  }
   813  
   814  // Copy implements driver.Copy.
   815  func (b *bucket) Copy(ctx context.Context, dstKey, srcKey string, opts *driver.CopyOptions) error {
   816  	// Note: we could use NewRangeReader here, but since we need to copy all of
   817  	// the metadata (from xa), it's more efficient to do it directly.
   818  	srcPath, _, xa, err := b.forKey(srcKey)
   819  	if err != nil {
   820  		return err
   821  	}
   822  	f, err := os.Open(srcPath)
   823  	if err != nil {
   824  		return err
   825  	}
   826  	defer f.Close()
   827  
   828  	// We'll write the copy using Writer, to avoid re-implementing making of a
   829  	// temp file, cleaning up after partial failures, etc.
   830  	wopts := driver.WriterOptions{
   831  		CacheControl:       xa.CacheControl,
   832  		ContentDisposition: xa.ContentDisposition,
   833  		ContentEncoding:    xa.ContentEncoding,
   834  		ContentLanguage:    xa.ContentLanguage,
   835  		Metadata:           xa.Metadata,
   836  		BeforeWrite:        opts.BeforeCopy,
   837  	}
   838  	// Create a cancelable context so we can cancel the write if there are
   839  	// problems.
   840  	writeCtx, cancel := context.WithCancel(ctx)
   841  	defer cancel()
   842  	w, err := b.NewTypedWriter(writeCtx, dstKey, xa.ContentType, &wopts)
   843  	if err != nil {
   844  		return err
   845  	}
   846  	_, err = io.Copy(w, f)
   847  	if err != nil {
   848  		cancel() // cancel before Close cancels the write
   849  		w.Close()
   850  		return err
   851  	}
   852  	return w.Close()
   853  }
   854  
   855  // Delete implements driver.Delete.
   856  func (b *bucket) Delete(ctx context.Context, key string) error {
   857  	path, err := b.path(key)
   858  	if err != nil {
   859  		return err
   860  	}
   861  	err = os.Remove(path)
   862  	if err != nil {
   863  		return err
   864  	}
   865  	if err = os.Remove(path + attrsExt); err != nil && !os.IsNotExist(err) {
   866  		return err
   867  	}
   868  	return nil
   869  }
   870  
   871  // SignedURL implements driver.SignedURL
   872  func (b *bucket) SignedURL(ctx context.Context, key string, opts *driver.SignedURLOptions) (string, error) {
   873  	if b.opts.URLSigner == nil {
   874  		return "", gcerr.New(gcerr.Unimplemented, nil, 1, "fileblob.SignedURL: bucket does not have an Options.URLSigner")
   875  	}
   876  	if opts.BeforeSign != nil {
   877  		if err := opts.BeforeSign(func(interface{}) bool { return false }); err != nil {
   878  			return "", err
   879  		}
   880  	}
   881  	surl, err := b.opts.URLSigner.URLFromKey(ctx, key, opts)
   882  	if err != nil {
   883  		return "", err
   884  	}
   885  	return surl.String(), nil
   886  }
   887  
   888  // URLSigner defines an interface for creating and verifying a signed URL for
   889  // objects in a fileblob bucket. Signed URLs are typically used for granting
   890  // access to an otherwise-protected resource without requiring further
   891  // authentication, and callers should take care to restrict the creation of
   892  // signed URLs as is appropriate for their application.
   893  type URLSigner interface {
   894  	// URLFromKey defines how the bucket's object key will be turned
   895  	// into a signed URL. URLFromKey must be safe to call from multiple goroutines.
   896  	URLFromKey(ctx context.Context, key string, opts *driver.SignedURLOptions) (*url.URL, error)
   897  
   898  	// KeyFromURL must be able to validate a URL returned from URLFromKey.
   899  	// KeyFromURL must only return the object if if the URL is
   900  	// both unexpired and authentic. KeyFromURL must be safe to call from
   901  	// multiple goroutines. Implementations of KeyFromURL should not modify
   902  	// the URL argument.
   903  	KeyFromURL(ctx context.Context, surl *url.URL) (string, error)
   904  }
   905  
   906  // URLSignerHMAC signs URLs by adding the object key, expiration time, and a
   907  // hash-based message authentication code (HMAC) into the query parameters.
   908  // Values of URLSignerHMAC with the same secret key will accept URLs produced by
   909  // others as valid.
   910  type URLSignerHMAC struct {
   911  	baseURL   *url.URL
   912  	secretKey []byte
   913  }
   914  
   915  // NewURLSignerHMAC creates a URLSignerHMAC. If the secret key is empty,
   916  // then NewURLSignerHMAC panics.
   917  func NewURLSignerHMAC(baseURL *url.URL, secretKey []byte) *URLSignerHMAC {
   918  	if len(secretKey) == 0 {
   919  		panic("creating URLSignerHMAC: secretKey is required")
   920  	}
   921  	uc := new(url.URL)
   922  	*uc = *baseURL
   923  	return &URLSignerHMAC{
   924  		baseURL:   uc,
   925  		secretKey: secretKey,
   926  	}
   927  }
   928  
   929  // URLFromKey creates a signed URL by copying the baseURL and appending the
   930  // object key, expiry, and signature as a query params.
   931  func (h *URLSignerHMAC) URLFromKey(ctx context.Context, key string, opts *driver.SignedURLOptions) (*url.URL, error) {
   932  	sURL := new(url.URL)
   933  	*sURL = *h.baseURL
   934  
   935  	q := sURL.Query()
   936  	q.Set("obj", key)
   937  	q.Set("expiry", strconv.FormatInt(time.Now().Add(opts.Expiry).Unix(), 10))
   938  	q.Set("method", opts.Method)
   939  	if opts.ContentType != "" {
   940  		q.Set("contentType", opts.ContentType)
   941  	}
   942  	q.Set("signature", h.getMAC(q))
   943  	sURL.RawQuery = q.Encode()
   944  
   945  	return sURL, nil
   946  }
   947  
   948  func (h *URLSignerHMAC) getMAC(q url.Values) string {
   949  	signedVals := url.Values{}
   950  	signedVals.Set("obj", q.Get("obj"))
   951  	signedVals.Set("expiry", q.Get("expiry"))
   952  	signedVals.Set("method", q.Get("method"))
   953  	if contentType := q.Get("contentType"); contentType != "" {
   954  		signedVals.Set("contentType", contentType)
   955  	}
   956  	msg := signedVals.Encode()
   957  
   958  	hsh := hmac.New(sha256.New, h.secretKey)
   959  	hsh.Write([]byte(msg))
   960  	return base64.RawURLEncoding.EncodeToString(hsh.Sum(nil))
   961  }
   962  
   963  // KeyFromURL checks expiry and signature, and returns the object key
   964  // only if the signed URL is both authentic and unexpired.
   965  func (h *URLSignerHMAC) KeyFromURL(ctx context.Context, sURL *url.URL) (string, error) {
   966  	q := sURL.Query()
   967  
   968  	exp, err := strconv.ParseInt(q.Get("expiry"), 10, 64)
   969  	if err != nil || time.Now().Unix() > exp {
   970  		return "", errors.New("retrieving blob key from URL: key cannot be retrieved")
   971  	}
   972  
   973  	if !h.checkMAC(q) {
   974  		return "", errors.New("retrieving blob key from URL: key cannot be retrieved")
   975  	}
   976  	return q.Get("obj"), nil
   977  }
   978  
   979  func (h *URLSignerHMAC) checkMAC(q url.Values) bool {
   980  	mac := q.Get("signature")
   981  	expected := h.getMAC(q)
   982  	// This compares the Base-64 encoded MACs
   983  	return hmac.Equal([]byte(mac), []byte(expected))
   984  }