github.com/pingcap/br@v5.3.0-alpha.0.20220125034240-ec59c7b6ce30+incompatible/pkg/storage/parse.go (about)

     1  // Copyright 2020 PingCAP, Inc. Licensed under Apache-2.0.
     2  
     3  package storage
     4  
     5  import (
     6  	"net/url"
     7  	"path/filepath"
     8  	"reflect"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/pingcap/errors"
    13  	backuppb "github.com/pingcap/kvproto/pkg/backup"
    14  
    15  	berrors "github.com/pingcap/br/pkg/errors"
    16  )
    17  
    18  // BackendOptions further configures the storage backend not expressed by the
    19  // storage URL.
    20  type BackendOptions struct {
    21  	S3  S3BackendOptions  `json:"s3" toml:"s3"`
    22  	GCS GCSBackendOptions `json:"gcs" toml:"gcs"`
    23  }
    24  
    25  // ParseRawURL parse raw url to url object.
    26  func ParseRawURL(rawURL string) (*url.URL, error) {
    27  	// https://github.com/pingcap/br/issues/603
    28  	// In aws the secret key may contain '/+=' and '+' has a special meaning in URL.
    29  	// Replace "+" by "%2B" here to avoid this problem.
    30  	rawURL = strings.ReplaceAll(rawURL, "+", "%2B")
    31  	u, err := url.Parse(rawURL)
    32  	if err != nil {
    33  		return nil, errors.Trace(err)
    34  	}
    35  	return u, nil
    36  }
    37  
    38  // ParseBackend constructs a structured backend description from the
    39  // storage URL.
    40  func ParseBackend(rawURL string, options *BackendOptions) (*backuppb.StorageBackend, error) {
    41  	if len(rawURL) == 0 {
    42  		return nil, errors.Annotate(berrors.ErrStorageInvalidConfig, "empty store is not allowed")
    43  	}
    44  	u, err := ParseRawURL(rawURL)
    45  	if err != nil {
    46  		return nil, errors.Trace(err)
    47  	}
    48  	switch u.Scheme {
    49  	case "":
    50  		absPath, err := filepath.Abs(rawURL)
    51  		if err != nil {
    52  			return nil, errors.Annotatef(berrors.ErrStorageInvalidConfig, "covert data-source-dir '%s' to absolute path failed", rawURL)
    53  		}
    54  		local := &backuppb.Local{Path: absPath}
    55  		return &backuppb.StorageBackend{Backend: &backuppb.StorageBackend_Local{Local: local}}, nil
    56  
    57  	case "local", "file":
    58  		local := &backuppb.Local{Path: u.Path}
    59  		return &backuppb.StorageBackend{Backend: &backuppb.StorageBackend_Local{Local: local}}, nil
    60  
    61  	case "noop":
    62  		noop := &backuppb.Noop{}
    63  		return &backuppb.StorageBackend{Backend: &backuppb.StorageBackend_Noop{Noop: noop}}, nil
    64  
    65  	case "s3":
    66  		if u.Host == "" {
    67  			return nil, errors.Annotatef(berrors.ErrStorageInvalidConfig, "please specify the bucket for s3 in %s", rawURL)
    68  		}
    69  		prefix := strings.Trim(u.Path, "/")
    70  		s3 := &backuppb.S3{Bucket: u.Host, Prefix: prefix}
    71  		if options == nil {
    72  			options = &BackendOptions{S3: S3BackendOptions{ForcePathStyle: true}}
    73  		}
    74  		ExtractQueryParameters(u, &options.S3)
    75  		if err := options.S3.Apply(s3); err != nil {
    76  			return nil, errors.Trace(err)
    77  		}
    78  		return &backuppb.StorageBackend{Backend: &backuppb.StorageBackend_S3{S3: s3}}, nil
    79  
    80  	case "gs", "gcs":
    81  		if u.Host == "" {
    82  			return nil, errors.Annotatef(berrors.ErrStorageInvalidConfig, "please specify the bucket for gcs in %s", rawURL)
    83  		}
    84  		prefix := strings.Trim(u.Path, "/")
    85  		gcs := &backuppb.GCS{Bucket: u.Host, Prefix: prefix}
    86  		if options == nil {
    87  			options = &BackendOptions{}
    88  		}
    89  		ExtractQueryParameters(u, &options.GCS)
    90  		if err := options.GCS.apply(gcs); err != nil {
    91  			return nil, errors.Trace(err)
    92  		}
    93  		return &backuppb.StorageBackend{Backend: &backuppb.StorageBackend_Gcs{Gcs: gcs}}, nil
    94  
    95  	default:
    96  		return nil, errors.Annotatef(berrors.ErrStorageInvalidConfig, "storage %s not support yet", u.Scheme)
    97  	}
    98  }
    99  
   100  // ExtractQueryParameters moves the query parameters of the URL into the options
   101  // using reflection.
   102  //
   103  // The options must be a pointer to a struct which contains only string or bool
   104  // fields (more types will be supported in the future), and tagged for JSON
   105  // serialization.
   106  //
   107  // All of the URL's query parameters will be removed after calling this method.
   108  func ExtractQueryParameters(u *url.URL, options interface{}) {
   109  	type field struct {
   110  		index int
   111  		kind  reflect.Kind
   112  	}
   113  
   114  	// First, find all JSON fields in the options struct type.
   115  	o := reflect.Indirect(reflect.ValueOf(options))
   116  	ty := o.Type()
   117  	numFields := ty.NumField()
   118  	tagToField := make(map[string]field, numFields)
   119  	for i := 0; i < numFields; i++ {
   120  		f := ty.Field(i)
   121  		tag := f.Tag.Get("json")
   122  		tagToField[tag] = field{index: i, kind: f.Type.Kind()}
   123  	}
   124  
   125  	// Then, read content from the URL into the options.
   126  	for key, params := range u.Query() {
   127  		if len(params) == 0 {
   128  			continue
   129  		}
   130  		param := params[0]
   131  		normalizedKey := strings.ToLower(strings.ReplaceAll(key, "_", "-"))
   132  		if f, ok := tagToField[normalizedKey]; ok {
   133  			field := o.Field(f.index)
   134  			switch f.kind {
   135  			case reflect.Bool:
   136  				if v, e := strconv.ParseBool(param); e == nil {
   137  					field.SetBool(v)
   138  				}
   139  			case reflect.String:
   140  				field.SetString(param)
   141  			default:
   142  				panic("BackendOption introduced an unsupported kind, please handle it! " + f.kind.String())
   143  			}
   144  		}
   145  	}
   146  
   147  	// Clean up the URL finally.
   148  	u.RawQuery = ""
   149  }
   150  
   151  // FormatBackendURL obtains the raw URL which can be used the reconstruct the
   152  // backend. The returned URL does not contain options for further configurating
   153  // the backend. This is to avoid exposing secret tokens.
   154  func FormatBackendURL(backend *backuppb.StorageBackend) (u url.URL) {
   155  	switch b := backend.Backend.(type) {
   156  	case *backuppb.StorageBackend_Local:
   157  		u.Scheme = "local"
   158  		u.Path = b.Local.Path
   159  	case *backuppb.StorageBackend_Noop:
   160  		u.Scheme = "noop"
   161  		u.Path = "/"
   162  	case *backuppb.StorageBackend_S3:
   163  		u.Scheme = "s3"
   164  		u.Host = b.S3.Bucket
   165  		u.Path = b.S3.Prefix
   166  	case *backuppb.StorageBackend_Gcs:
   167  		u.Scheme = "gcs"
   168  		u.Host = b.Gcs.Bucket
   169  		u.Path = b.Gcs.Prefix
   170  	}
   171  	return
   172  }