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  }