github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/http/http.go (about)

     1  // Package http provides a filesystem interface using golang.org/net/http
     2  //
     3  // It treats HTML pages served from the endpoint as directory
     4  // listings, and includes any links found as files.
     5  package http
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"mime"
    13  	"net/http"
    14  	"net/url"
    15  	"path"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  
    20  	"github.com/rclone/rclone/fs"
    21  	"github.com/rclone/rclone/fs/config/configmap"
    22  	"github.com/rclone/rclone/fs/config/configstruct"
    23  	"github.com/rclone/rclone/fs/fshttp"
    24  	"github.com/rclone/rclone/fs/hash"
    25  	"github.com/rclone/rclone/lib/rest"
    26  	"golang.org/x/net/html"
    27  )
    28  
    29  var (
    30  	errorReadOnly = errors.New("http remotes are read only")
    31  	timeUnset     = time.Unix(0, 0)
    32  )
    33  
    34  func init() {
    35  	fsi := &fs.RegInfo{
    36  		Name:        "http",
    37  		Description: "HTTP",
    38  		NewFs:       NewFs,
    39  		CommandHelp: commandHelp,
    40  		Options: []fs.Option{{
    41  			Name:     "url",
    42  			Help:     "URL of HTTP host to connect to.\n\nE.g. \"https://example.com\", or \"https://user:pass@example.com\" to use a username and password.",
    43  			Required: true,
    44  		}, {
    45  			Name: "headers",
    46  			Help: `Set HTTP headers for all transactions.
    47  
    48  Use this to set additional HTTP headers for all transactions.
    49  
    50  The input format is comma separated list of key,value pairs.  Standard
    51  [CSV encoding](https://godoc.org/encoding/csv) may be used.
    52  
    53  For example, to set a Cookie use 'Cookie,name=value', or '"Cookie","name=value"'.
    54  
    55  You can set multiple headers, e.g. '"Cookie","name=value","Authorization","xxx"'.`,
    56  			Default:  fs.CommaSepList{},
    57  			Advanced: true,
    58  		}, {
    59  			Name: "no_slash",
    60  			Help: `Set this if the site doesn't end directories with /.
    61  
    62  Use this if your target website does not use / on the end of
    63  directories.
    64  
    65  A / on the end of a path is how rclone normally tells the difference
    66  between files and directories.  If this flag is set, then rclone will
    67  treat all files with Content-Type: text/html as directories and read
    68  URLs from them rather than downloading them.
    69  
    70  Note that this may cause rclone to confuse genuine HTML files with
    71  directories.`,
    72  			Default:  false,
    73  			Advanced: true,
    74  		}, {
    75  			Name: "no_head",
    76  			Help: `Don't use HEAD requests.
    77  
    78  HEAD requests are mainly used to find file sizes in dir listing.
    79  If your site is being very slow to load then you can try this option.
    80  Normally rclone does a HEAD request for each potential file in a
    81  directory listing to:
    82  
    83  - find its size
    84  - check it really exists
    85  - check to see if it is a directory
    86  
    87  If you set this option, rclone will not do the HEAD request. This will mean
    88  that directory listings are much quicker, but rclone won't have the times or
    89  sizes of any files, and some files that don't exist may be in the listing.`,
    90  			Default:  false,
    91  			Advanced: true,
    92  		}, {
    93  			Name:    "no_escape",
    94  			Help:    "Do not escape URL metacharacters in path names.",
    95  			Default: false,
    96  		}},
    97  	}
    98  	fs.Register(fsi)
    99  }
   100  
   101  // Options defines the configuration for this backend
   102  type Options struct {
   103  	Endpoint string          `config:"url"`
   104  	NoSlash  bool            `config:"no_slash"`
   105  	NoHead   bool            `config:"no_head"`
   106  	Headers  fs.CommaSepList `config:"headers"`
   107  	NoEscape bool            `config:"no_escape"`
   108  }
   109  
   110  // Fs stores the interface to the remote HTTP files
   111  type Fs struct {
   112  	name        string
   113  	root        string
   114  	features    *fs.Features   // optional features
   115  	opt         Options        // options for this backend
   116  	ci          *fs.ConfigInfo // global config
   117  	endpoint    *url.URL
   118  	endpointURL string // endpoint as a string
   119  	httpClient  *http.Client
   120  }
   121  
   122  // Object is a remote object that has been stat'd (so it exists, but is not necessarily open for reading)
   123  type Object struct {
   124  	fs          *Fs
   125  	remote      string
   126  	size        int64
   127  	modTime     time.Time
   128  	contentType string
   129  }
   130  
   131  // statusError returns an error if the res contained an error
   132  func statusError(res *http.Response, err error) error {
   133  	if err != nil {
   134  		return err
   135  	}
   136  	if res.StatusCode < 200 || res.StatusCode > 299 {
   137  		_ = res.Body.Close()
   138  		return fmt.Errorf("HTTP Error: %s", res.Status)
   139  	}
   140  	return nil
   141  }
   142  
   143  // getFsEndpoint decides if url is to be considered a file or directory,
   144  // and returns a proper endpoint url to use for the fs.
   145  func getFsEndpoint(ctx context.Context, client *http.Client, url string, opt *Options) (string, bool) {
   146  	// If url ends with '/' it is already a proper url always assumed to be a directory.
   147  	if url[len(url)-1] == '/' {
   148  		return url, false
   149  	}
   150  
   151  	// If url does not end with '/' we send a HEAD request to decide
   152  	// if it is directory or file, and if directory appends the missing
   153  	// '/', or if file returns the directory url to parent instead.
   154  	createFileResult := func() (string, bool) {
   155  		fs.Debugf(nil, "If path is a directory you must add a trailing '/'")
   156  		parent, _ := path.Split(url)
   157  		return parent, true
   158  	}
   159  	createDirResult := func() (string, bool) {
   160  		fs.Debugf(nil, "To avoid the initial HEAD request add a trailing '/' to the path")
   161  		return url + "/", false
   162  	}
   163  
   164  	// If HEAD requests are not allowed we just have to assume it is a file.
   165  	if opt.NoHead {
   166  		fs.Debugf(nil, "Assuming path is a file as --http-no-head is set")
   167  		return createFileResult()
   168  	}
   169  
   170  	// Use a client which doesn't follow redirects so the server
   171  	// doesn't redirect http://host/dir to http://host/dir/
   172  	noRedir := *client
   173  	noRedir.CheckRedirect = func(req *http.Request, via []*http.Request) error {
   174  		return http.ErrUseLastResponse
   175  	}
   176  	req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
   177  	if err != nil {
   178  		fs.Debugf(nil, "Assuming path is a file as HEAD request could not be created: %v", err)
   179  		return createFileResult()
   180  	}
   181  	addHeaders(req, opt)
   182  	res, err := noRedir.Do(req)
   183  
   184  	if err != nil {
   185  		fs.Debugf(nil, "Assuming path is a file as HEAD request could not be sent: %v", err)
   186  		return createFileResult()
   187  	}
   188  	if res.StatusCode == http.StatusNotFound {
   189  		fs.Debugf(nil, "Assuming path is a directory as HEAD response is it does not exist as a file (%s)", res.Status)
   190  		return createDirResult()
   191  	}
   192  	if res.StatusCode == http.StatusMovedPermanently ||
   193  		res.StatusCode == http.StatusFound ||
   194  		res.StatusCode == http.StatusSeeOther ||
   195  		res.StatusCode == http.StatusTemporaryRedirect ||
   196  		res.StatusCode == http.StatusPermanentRedirect {
   197  		redir := res.Header.Get("Location")
   198  		if redir != "" {
   199  			if redir[len(redir)-1] == '/' {
   200  				fs.Debugf(nil, "Assuming path is a directory as HEAD response is redirect (%s) to a path that ends with '/': %s", res.Status, redir)
   201  				return createDirResult()
   202  			}
   203  			fs.Debugf(nil, "Assuming path is a file as HEAD response is redirect (%s) to a path that does not end with '/': %s", res.Status, redir)
   204  			return createFileResult()
   205  		}
   206  		fs.Debugf(nil, "Assuming path is a file as HEAD response is redirect (%s) but no location header", res.Status)
   207  		return createFileResult()
   208  	}
   209  	if res.StatusCode < 200 || res.StatusCode > 299 {
   210  		// Example is 403 (http.StatusForbidden) for servers not allowing HEAD requests.
   211  		fs.Debugf(nil, "Assuming path is a file as HEAD response is an error (%s)", res.Status)
   212  		return createFileResult()
   213  	}
   214  
   215  	fs.Debugf(nil, "Assuming path is a file as HEAD response is success (%s)", res.Status)
   216  	return createFileResult()
   217  }
   218  
   219  // Make the http connection with opt
   220  func (f *Fs) httpConnection(ctx context.Context, opt *Options) (isFile bool, err error) {
   221  	if len(opt.Headers)%2 != 0 {
   222  		return false, errors.New("odd number of headers supplied")
   223  	}
   224  
   225  	if !strings.HasSuffix(opt.Endpoint, "/") {
   226  		opt.Endpoint += "/"
   227  	}
   228  
   229  	// Parse the endpoint and stick the root onto it
   230  	base, err := url.Parse(opt.Endpoint)
   231  	if err != nil {
   232  		return false, err
   233  	}
   234  	u, err := rest.URLJoin(base, rest.URLPathEscape(f.root))
   235  	if err != nil {
   236  		return false, err
   237  	}
   238  
   239  	client := fshttp.NewClient(ctx)
   240  
   241  	endpoint, isFile := getFsEndpoint(ctx, client, u.String(), opt)
   242  	fs.Debugf(nil, "Root: %s", endpoint)
   243  	u, err = url.Parse(endpoint)
   244  	if err != nil {
   245  		return false, err
   246  	}
   247  
   248  	// Update f with the new parameters
   249  	f.httpClient = client
   250  	f.endpoint = u
   251  	f.endpointURL = u.String()
   252  	return isFile, nil
   253  }
   254  
   255  // NewFs creates a new Fs object from the name and root. It connects to
   256  // the host specified in the config file.
   257  func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
   258  	// Parse config into Options struct
   259  	opt := new(Options)
   260  	err := configstruct.Set(m, opt)
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  
   265  	ci := fs.GetConfig(ctx)
   266  	f := &Fs{
   267  		name: name,
   268  		root: root,
   269  		opt:  *opt,
   270  		ci:   ci,
   271  	}
   272  	f.features = (&fs.Features{
   273  		CanHaveEmptyDirectories: true,
   274  	}).Fill(ctx, f)
   275  
   276  	// Make the http connection
   277  	isFile, err := f.httpConnection(ctx, opt)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  
   282  	if isFile {
   283  		// return an error with an fs which points to the parent
   284  		return f, fs.ErrorIsFile
   285  	}
   286  
   287  	if !strings.HasSuffix(f.endpointURL, "/") {
   288  		return nil, errors.New("internal error: url doesn't end with /")
   289  	}
   290  
   291  	return f, nil
   292  }
   293  
   294  // Name returns the configured name of the file system
   295  func (f *Fs) Name() string {
   296  	return f.name
   297  }
   298  
   299  // Root returns the root for the filesystem
   300  func (f *Fs) Root() string {
   301  	return f.root
   302  }
   303  
   304  // String returns the URL for the filesystem
   305  func (f *Fs) String() string {
   306  	return f.endpointURL
   307  }
   308  
   309  // Features returns the optional features of this Fs
   310  func (f *Fs) Features() *fs.Features {
   311  	return f.features
   312  }
   313  
   314  // Precision is the remote http file system's modtime precision, which we have no way of knowing. We estimate at 1s
   315  func (f *Fs) Precision() time.Duration {
   316  	return time.Second
   317  }
   318  
   319  // NewObject creates a new remote http file object
   320  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
   321  	o := &Object{
   322  		fs:     f,
   323  		remote: remote,
   324  	}
   325  	err := o.head(ctx)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  	return o, nil
   330  }
   331  
   332  // Join's the remote onto the base URL
   333  func (f *Fs) url(remote string) string {
   334  	if f.opt.NoEscape {
   335  		// Directly concatenate without escaping, no_escape behavior
   336  		return f.endpointURL + remote
   337  	}
   338  	// Default behavior
   339  	return f.endpointURL + rest.URLPathEscape(remote)
   340  }
   341  
   342  // Errors returned by parseName
   343  var (
   344  	errURLJoinFailed     = errors.New("URLJoin failed")
   345  	errFoundQuestionMark = errors.New("found ? in URL")
   346  	errHostMismatch      = errors.New("host mismatch")
   347  	errSchemeMismatch    = errors.New("scheme mismatch")
   348  	errNotUnderRoot      = errors.New("not under root")
   349  	errNameIsEmpty       = errors.New("name is empty")
   350  	errNameContainsSlash = errors.New("name contains /")
   351  )
   352  
   353  // parseName turns a name as found in the page into a remote path or returns an error
   354  func parseName(base *url.URL, name string) (string, error) {
   355  	// make URL absolute
   356  	u, err := rest.URLJoin(base, name)
   357  	if err != nil {
   358  		return "", errURLJoinFailed
   359  	}
   360  	// check it doesn't have URL parameters
   361  	uStr := u.String()
   362  	if strings.Contains(uStr, "?") {
   363  		return "", errFoundQuestionMark
   364  	}
   365  	// check that this is going back to the same host and scheme
   366  	if base.Host != u.Host {
   367  		return "", errHostMismatch
   368  	}
   369  	if base.Scheme != u.Scheme {
   370  		return "", errSchemeMismatch
   371  	}
   372  	// check has path prefix
   373  	if !strings.HasPrefix(u.Path, base.Path) {
   374  		return "", errNotUnderRoot
   375  	}
   376  	// calculate the name relative to the base
   377  	name = u.Path[len(base.Path):]
   378  	// mustn't be empty
   379  	if name == "" {
   380  		return "", errNameIsEmpty
   381  	}
   382  	// mustn't contain a / - we are looking for a single level directory
   383  	slash := strings.Index(name, "/")
   384  	if slash >= 0 && slash != len(name)-1 {
   385  		return "", errNameContainsSlash
   386  	}
   387  	return name, nil
   388  }
   389  
   390  // Parse turns HTML for a directory into names
   391  // base should be the base URL to resolve any relative names from
   392  func parse(base *url.URL, in io.Reader) (names []string, err error) {
   393  	doc, err := html.Parse(in)
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  	var (
   398  		walk func(*html.Node)
   399  		seen = make(map[string]struct{})
   400  	)
   401  	walk = func(n *html.Node) {
   402  		if n.Type == html.ElementNode && n.Data == "a" {
   403  			for _, a := range n.Attr {
   404  				if a.Key == "href" {
   405  					name, err := parseName(base, a.Val)
   406  					if err == nil {
   407  						if _, found := seen[name]; !found {
   408  							names = append(names, name)
   409  							seen[name] = struct{}{}
   410  						}
   411  					}
   412  					break
   413  				}
   414  			}
   415  		}
   416  		for c := n.FirstChild; c != nil; c = c.NextSibling {
   417  			walk(c)
   418  		}
   419  	}
   420  	walk(doc)
   421  	return names, nil
   422  }
   423  
   424  // Adds the configured headers to the request if any
   425  func addHeaders(req *http.Request, opt *Options) {
   426  	for i := 0; i < len(opt.Headers); i += 2 {
   427  		key := opt.Headers[i]
   428  		value := opt.Headers[i+1]
   429  		req.Header.Add(key, value)
   430  	}
   431  }
   432  
   433  // Adds the configured headers to the request if any
   434  func (f *Fs) addHeaders(req *http.Request) {
   435  	addHeaders(req, &f.opt)
   436  }
   437  
   438  // Read the directory passed in
   439  func (f *Fs) readDir(ctx context.Context, dir string) (names []string, err error) {
   440  	URL := f.url(dir)
   441  	u, err := url.Parse(URL)
   442  	if err != nil {
   443  		return nil, fmt.Errorf("failed to readDir: %w", err)
   444  	}
   445  	if !strings.HasSuffix(URL, "/") {
   446  		return nil, fmt.Errorf("internal error: readDir URL %q didn't end in /", URL)
   447  	}
   448  	// Do the request
   449  	req, err := http.NewRequestWithContext(ctx, "GET", URL, nil)
   450  	if err != nil {
   451  		return nil, fmt.Errorf("readDir failed: %w", err)
   452  	}
   453  	f.addHeaders(req)
   454  	res, err := f.httpClient.Do(req)
   455  	if err == nil {
   456  		defer fs.CheckClose(res.Body, &err)
   457  		if res.StatusCode == http.StatusNotFound {
   458  			return nil, fs.ErrorDirNotFound
   459  		}
   460  	}
   461  	err = statusError(res, err)
   462  	if err != nil {
   463  		return nil, fmt.Errorf("failed to readDir: %w", err)
   464  	}
   465  
   466  	contentType := strings.SplitN(res.Header.Get("Content-Type"), ";", 2)[0]
   467  	switch contentType {
   468  	case "text/html":
   469  		names, err = parse(u, res.Body)
   470  		if err != nil {
   471  			return nil, fmt.Errorf("readDir: %w", err)
   472  		}
   473  	default:
   474  		return nil, fmt.Errorf("can't parse content type %q", contentType)
   475  	}
   476  	return names, nil
   477  }
   478  
   479  // List the objects and directories in dir into entries.  The
   480  // entries can be returned in any order but should be for a
   481  // complete directory.
   482  //
   483  // dir should be "" to list the root, and should not have
   484  // trailing slashes.
   485  //
   486  // This should return ErrDirNotFound if the directory isn't
   487  // found.
   488  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   489  	if !strings.HasSuffix(dir, "/") && dir != "" {
   490  		dir += "/"
   491  	}
   492  	names, err := f.readDir(ctx, dir)
   493  	if err != nil {
   494  		return nil, fmt.Errorf("error listing %q: %w", dir, err)
   495  	}
   496  	var (
   497  		entriesMu sync.Mutex // to protect entries
   498  		wg        sync.WaitGroup
   499  		checkers  = f.ci.Checkers
   500  		in        = make(chan string, checkers)
   501  	)
   502  	add := func(entry fs.DirEntry) {
   503  		entriesMu.Lock()
   504  		entries = append(entries, entry)
   505  		entriesMu.Unlock()
   506  	}
   507  	for i := 0; i < checkers; i++ {
   508  		wg.Add(1)
   509  		go func() {
   510  			defer wg.Done()
   511  			for remote := range in {
   512  				file := &Object{
   513  					fs:     f,
   514  					remote: remote,
   515  				}
   516  				switch err := file.head(ctx); err {
   517  				case nil:
   518  					add(file)
   519  				case fs.ErrorNotAFile:
   520  					// ...found a directory not a file
   521  					add(fs.NewDir(remote, time.Time{}))
   522  				default:
   523  					fs.Debugf(remote, "skipping because of error: %v", err)
   524  				}
   525  			}
   526  		}()
   527  	}
   528  	for _, name := range names {
   529  		isDir := name[len(name)-1] == '/'
   530  		name = strings.TrimRight(name, "/")
   531  		remote := path.Join(dir, name)
   532  		if isDir {
   533  			add(fs.NewDir(remote, time.Time{}))
   534  		} else {
   535  			in <- remote
   536  		}
   537  	}
   538  	close(in)
   539  	wg.Wait()
   540  	return entries, nil
   541  }
   542  
   543  // Put in to the remote path with the modTime given of the given size
   544  //
   545  // May create the object even if it returns an error - if so
   546  // will return the object and the error, otherwise will return
   547  // nil and the error
   548  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   549  	return nil, errorReadOnly
   550  }
   551  
   552  // PutStream uploads to the remote path with the modTime given of indeterminate size
   553  func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   554  	return nil, errorReadOnly
   555  }
   556  
   557  // Fs is the filesystem this remote http file object is located within
   558  func (o *Object) Fs() fs.Info {
   559  	return o.fs
   560  }
   561  
   562  // String returns the URL to the remote HTTP file
   563  func (o *Object) String() string {
   564  	if o == nil {
   565  		return "<nil>"
   566  	}
   567  	return o.remote
   568  }
   569  
   570  // Remote the name of the remote HTTP file, relative to the fs root
   571  func (o *Object) Remote() string {
   572  	return o.remote
   573  }
   574  
   575  // Hash returns "" since HTTP (in Go or OpenSSH) doesn't support remote calculation of hashes
   576  func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) {
   577  	return "", hash.ErrUnsupported
   578  }
   579  
   580  // Size returns the size in bytes of the remote http file
   581  func (o *Object) Size() int64 {
   582  	return o.size
   583  }
   584  
   585  // ModTime returns the modification time of the remote http file
   586  func (o *Object) ModTime(ctx context.Context) time.Time {
   587  	return o.modTime
   588  }
   589  
   590  // url returns the native url of the object
   591  func (o *Object) url() string {
   592  	return o.fs.url(o.remote)
   593  }
   594  
   595  // head sends a HEAD request to update info fields in the Object
   596  func (o *Object) head(ctx context.Context) error {
   597  	if o.fs.opt.NoHead {
   598  		o.size = -1
   599  		o.modTime = timeUnset
   600  		o.contentType = fs.MimeType(ctx, o)
   601  		return nil
   602  	}
   603  	url := o.url()
   604  	req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
   605  	if err != nil {
   606  		return fmt.Errorf("stat failed: %w", err)
   607  	}
   608  	o.fs.addHeaders(req)
   609  	res, err := o.fs.httpClient.Do(req)
   610  	if err == nil && res.StatusCode == http.StatusNotFound {
   611  		return fs.ErrorObjectNotFound
   612  	}
   613  	err = statusError(res, err)
   614  	if err != nil {
   615  		return fmt.Errorf("failed to stat: %w", err)
   616  	}
   617  	return o.decodeMetadata(ctx, res)
   618  }
   619  
   620  // decodeMetadata updates info fields in the Object according to HTTP response headers
   621  func (o *Object) decodeMetadata(ctx context.Context, res *http.Response) error {
   622  	t, err := http.ParseTime(res.Header.Get("Last-Modified"))
   623  	if err != nil {
   624  		t = timeUnset
   625  	}
   626  	o.modTime = t
   627  	o.contentType = res.Header.Get("Content-Type")
   628  	o.size = rest.ParseSizeFromHeaders(res.Header)
   629  
   630  	// If NoSlash is set then check ContentType to see if it is a directory
   631  	if o.fs.opt.NoSlash {
   632  		mediaType, _, err := mime.ParseMediaType(o.contentType)
   633  		if err != nil {
   634  			return fmt.Errorf("failed to parse Content-Type: %q: %w", o.contentType, err)
   635  		}
   636  		if mediaType == "text/html" {
   637  			return fs.ErrorNotAFile
   638  		}
   639  	}
   640  	return nil
   641  }
   642  
   643  // SetModTime sets the modification and access time to the specified time
   644  //
   645  // it also updates the info field
   646  func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
   647  	return errorReadOnly
   648  }
   649  
   650  // Storable returns whether the remote http file is a regular file (not a directory, symbolic link, block device, character device, named pipe, etc.)
   651  func (o *Object) Storable() bool {
   652  	return true
   653  }
   654  
   655  // Open a remote http file object for reading. Seek is supported
   656  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
   657  	url := o.url()
   658  	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
   659  	if err != nil {
   660  		return nil, fmt.Errorf("Open failed: %w", err)
   661  	}
   662  
   663  	// Add optional headers
   664  	for k, v := range fs.OpenOptionHeaders(options) {
   665  		req.Header.Add(k, v)
   666  	}
   667  	o.fs.addHeaders(req)
   668  
   669  	// Do the request
   670  	res, err := o.fs.httpClient.Do(req)
   671  	err = statusError(res, err)
   672  	if err != nil {
   673  		return nil, fmt.Errorf("Open failed: %w", err)
   674  	}
   675  	if err = o.decodeMetadata(ctx, res); err != nil {
   676  		return nil, fmt.Errorf("decodeMetadata failed: %w", err)
   677  	}
   678  	return res.Body, nil
   679  }
   680  
   681  // Hashes returns hash.HashNone to indicate remote hashing is unavailable
   682  func (f *Fs) Hashes() hash.Set {
   683  	return hash.Set(hash.None)
   684  }
   685  
   686  // Mkdir makes the root directory of the Fs object
   687  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
   688  	return errorReadOnly
   689  }
   690  
   691  // Remove a remote http file object
   692  func (o *Object) Remove(ctx context.Context) error {
   693  	return errorReadOnly
   694  }
   695  
   696  // Rmdir removes the root directory of the Fs object
   697  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   698  	return errorReadOnly
   699  }
   700  
   701  // Update in to the object with the modTime given of the given size
   702  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
   703  	return errorReadOnly
   704  }
   705  
   706  // MimeType of an Object if known, "" otherwise
   707  func (o *Object) MimeType(ctx context.Context) string {
   708  	return o.contentType
   709  }
   710  
   711  var commandHelp = []fs.CommandHelp{{
   712  	Name:  "set",
   713  	Short: "Set command for updating the config parameters.",
   714  	Long: `This set command can be used to update the config parameters
   715  for a running http backend.
   716  
   717  Usage Examples:
   718  
   719      rclone backend set remote: [-o opt_name=opt_value] [-o opt_name2=opt_value2]
   720      rclone rc backend/command command=set fs=remote: [-o opt_name=opt_value] [-o opt_name2=opt_value2]
   721      rclone rc backend/command command=set fs=remote: -o url=https://example.com
   722  
   723  The option keys are named as they are in the config file.
   724  
   725  This rebuilds the connection to the http backend when it is called with
   726  the new parameters. Only new parameters need be passed as the values
   727  will default to those currently in use.
   728  
   729  It doesn't return anything.
   730  `,
   731  }}
   732  
   733  // Command the backend to run a named command
   734  //
   735  // The command run is name
   736  // args may be used to read arguments from
   737  // opts may be used to read optional arguments from
   738  //
   739  // The result should be capable of being JSON encoded
   740  // If it is a string or a []string it will be shown to the user
   741  // otherwise it will be JSON encoded and shown to the user like that
   742  func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[string]string) (out interface{}, err error) {
   743  	switch name {
   744  	case "set":
   745  		newOpt := f.opt
   746  		err := configstruct.Set(configmap.Simple(opt), &newOpt)
   747  		if err != nil {
   748  			return nil, fmt.Errorf("reading config: %w", err)
   749  		}
   750  		_, err = f.httpConnection(ctx, &newOpt)
   751  		if err != nil {
   752  			return nil, fmt.Errorf("updating session: %w", err)
   753  		}
   754  		f.opt = newOpt
   755  		keys := []string{}
   756  		for k := range opt {
   757  			keys = append(keys, k)
   758  		}
   759  		fs.Logf(f, "Updated config values: %s", strings.Join(keys, ", "))
   760  		return nil, nil
   761  	default:
   762  		return nil, fs.ErrorCommandNotFound
   763  	}
   764  }
   765  
   766  // Check the interfaces are satisfied
   767  var (
   768  	_ fs.Fs          = &Fs{}
   769  	_ fs.PutStreamer = &Fs{}
   770  	_ fs.Object      = &Object{}
   771  	_ fs.MimeTyper   = &Object{}
   772  	_ fs.Commander   = &Fs{}
   773  )