github.com/minio/mc@v0.0.0-20240507152021-646712d5e5fb/cmd/client-url.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "bytes" 22 "context" 23 "path/filepath" 24 "regexp" 25 "runtime" 26 "strings" 27 "time" 28 29 "github.com/minio/mc/pkg/probe" 30 "github.com/minio/pkg/v2/mimedb" 31 ) 32 33 // ClientURL url client url structure 34 type ClientURL struct { 35 Type ClientURLType 36 Scheme string 37 Host string 38 Path string 39 SchemeSeparator string 40 Separator rune 41 } 42 43 // ClientURLType - enum of different url types 44 type ClientURLType int 45 46 // url2StatOptions - convert url to stat options 47 type url2StatOptions struct { 48 urlStr, versionID string 49 fileAttr bool 50 encKeyDB map[string][]prefixSSEPair 51 timeRef time.Time 52 isZip bool 53 ignoreBucketExistsCheck bool 54 } 55 56 // enum types 57 const ( 58 objectStorage = iota // MinIO and S3 compatible cloud storage 59 fileSystem // POSIX compatible file systems 60 ) 61 62 // Maybe rawurl is of the form scheme:path. (Scheme must be [a-zA-Z][a-zA-Z0-9+-.]*) 63 // If so, return scheme, path; else return "", rawurl. 64 func getScheme(rawurl string) (scheme, path string) { 65 urlSplits := strings.Split(rawurl, "://") 66 if len(urlSplits) == 2 { 67 scheme, uri := urlSplits[0], "//"+urlSplits[1] 68 // ignore numbers in scheme 69 validScheme := regexp.MustCompile("^[a-zA-Z]+$") 70 if uri != "" { 71 if validScheme.MatchString(scheme) { 72 return scheme, uri 73 } 74 } 75 } 76 return "", rawurl 77 } 78 79 // Assuming s is of the form [s delimiter s]. 80 // If so, return s, [delimiter]s or return s, s if cutdelimiter == true 81 // If no delimiter found return s, "". 82 func splitSpecial(s, delimiter string, cutdelimiter bool) (string, string) { 83 i := strings.Index(s, delimiter) 84 if i < 0 { 85 // if delimiter not found return as is. 86 return s, "" 87 } 88 // if delimiter should be removed, remove it. 89 if cutdelimiter { 90 return s[0:i], s[i+len(delimiter):] 91 } 92 // return split strings with delimiter 93 return s[0:i], s[i:] 94 } 95 96 // getHost - extract host from authority string, we do not support ftp style username@ yet. 97 func getHost(authority string) (host string) { 98 i := strings.LastIndex(authority, "@") 99 if i >= 0 { 100 // TODO support, username@password style userinfo, useful for ftp support. 101 return 102 } 103 return authority 104 } 105 106 // newClientURL returns an abstracted URL for filesystems and object storage. 107 func newClientURL(urlStr string) *ClientURL { 108 scheme, rest := getScheme(urlStr) 109 if strings.HasPrefix(rest, "//") { 110 // if rest has '//' prefix, skip them 111 var authority string 112 authority, rest = splitSpecial(rest[2:], "/", false) 113 if rest == "" { 114 rest = "/" 115 } 116 host := getHost(authority) 117 if host != "" && (scheme == "http" || scheme == "https") { 118 return &ClientURL{ 119 Scheme: scheme, 120 Type: objectStorage, 121 Host: host, 122 Path: rest, 123 SchemeSeparator: "://", 124 Separator: '/', 125 } 126 } 127 } 128 return &ClientURL{ 129 Type: fileSystem, 130 Path: rest, 131 Separator: filepath.Separator, 132 } 133 } 134 135 // joinURLs join two input urls and returns a url 136 func joinURLs(url1, url2 *ClientURL) *ClientURL { 137 var url1Path, url2Path string 138 url1Path = filepath.ToSlash(url1.Path) 139 url2Path = filepath.ToSlash(url2.Path) 140 if strings.HasSuffix(url1Path, "/") { 141 url1.Path = url1Path + strings.TrimPrefix(url2Path, "/") 142 } else { 143 url1.Path = url1Path + "/" + strings.TrimPrefix(url2Path, "/") 144 } 145 return url1 146 } 147 148 // Clone the url into a new object. 149 func (u ClientURL) Clone() ClientURL { 150 return ClientURL{ 151 Type: u.Type, 152 Scheme: u.Scheme, 153 Host: u.Host, 154 Path: u.Path, 155 SchemeSeparator: u.SchemeSeparator, 156 Separator: u.Separator, 157 } 158 } 159 160 // String convert URL into its canonical form. 161 func (u ClientURL) String() string { 162 var buf bytes.Buffer 163 // if fileSystem no translation needed, return as is. 164 if u.Type == fileSystem { 165 return u.Path 166 } 167 // if objectStorage convert from any non standard paths to a supported URL path style. 168 if u.Type == objectStorage { 169 buf.WriteString(u.Scheme) 170 buf.WriteByte(':') 171 buf.WriteString("//") 172 if h := u.Host; h != "" { 173 buf.WriteString(h) 174 } 175 switch runtime.GOOS { 176 case "windows": 177 if u.Path != "" && u.Path[0] != '\\' && u.Host != "" && u.Path[0] != '/' { 178 buf.WriteByte('/') 179 } 180 buf.WriteString(strings.ReplaceAll(u.Path, "\\", "/")) 181 default: 182 if u.Path != "" && u.Path[0] != '/' && u.Host != "" { 183 buf.WriteByte('/') 184 } 185 buf.WriteString(u.Path) 186 } 187 } 188 return buf.String() 189 } 190 191 // urlJoinPath Join a path to existing URL. 192 func urlJoinPath(url1, url2 string) string { 193 u1 := newClientURL(url1) 194 u2 := newClientURL(url2) 195 return joinURLs(u1, u2).String() 196 } 197 198 // url2Stat returns stat info for URL - supports bucket, object and a prefixe with or without a trailing slash 199 func url2Stat(ctx context.Context, opts url2StatOptions) (client Client, content *ClientContent, err *probe.Error) { 200 client, err = newClient(opts.urlStr) 201 if err != nil { 202 return nil, nil, err.Trace(opts.urlStr) 203 } 204 alias, _ := url2Alias(opts.urlStr) 205 sse := getSSE(opts.urlStr, opts.encKeyDB[alias]) 206 207 content, err = client.Stat(ctx, StatOptions{preserve: opts.fileAttr, sse: sse, timeRef: opts.timeRef, versionID: opts.versionID, isZip: opts.isZip, ignoreBucketExists: opts.ignoreBucketExistsCheck}) 208 if err != nil { 209 return nil, nil, err.Trace(opts.urlStr) 210 } 211 return client, content, nil 212 } 213 214 // firstURL2Stat returns the stat info of the first object having the specified prefix 215 func firstURL2Stat(ctx context.Context, prefix string, timeRef time.Time, isZip bool) (client Client, content *ClientContent, err *probe.Error) { 216 client, err = newClient(prefix) 217 if err != nil { 218 return nil, nil, err.Trace(prefix) 219 } 220 content = <-client.List(ctx, ListOptions{Recursive: true, TimeRef: timeRef, Count: 1, ListZip: isZip}) 221 if content == nil { 222 return nil, nil, probe.NewError(ObjectMissing{timeRef: timeRef}).Trace(prefix) 223 } 224 if content.Err != nil { 225 return nil, nil, content.Err.Trace(prefix) 226 } 227 return client, content, nil 228 } 229 230 // url2Alias separates alias and path from the URL. Aliased URL is of 231 // the form alias/path/to/blah. 232 func url2Alias(aliasedURL string) (alias, path string) { 233 // Save aliased url. 234 urlStr := aliasedURL 235 236 // Convert '/' on windows to filepath.Separator. 237 urlStr = filepath.FromSlash(urlStr) 238 239 if runtime.GOOS == "windows" { 240 // Remove '/' prefix before alias if any to support '\\home' alias 241 // style under Windows 242 urlStr = strings.TrimPrefix(urlStr, string(filepath.Separator)) 243 } 244 245 // Remove everything after alias (i.e. after '/'). 246 urlParts := strings.SplitN(urlStr, string(filepath.Separator), 2) 247 if len(urlParts) == 2 { 248 // Convert windows style path separator to Unix style. 249 return urlParts[0], urlParts[1] 250 } 251 return urlParts[0], "" 252 } 253 254 // guessURLContentType - guess content-type of the URL. 255 // on failure just return 'application/octet-stream'. 256 func guessURLContentType(urlStr string) string { 257 url := newClientURL(urlStr) 258 contentType := mimedb.TypeByExtension(filepath.Ext(url.Path)) 259 return contentType 260 }