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 }