github.com/artpar/rclone@v1.67.3/backend/netstorage/netstorage.go (about)

     1  // Package netstorage provides an interface to Akamai NetStorage API
     2  package netstorage
     3  
     4  import (
     5  	"context"
     6  	"crypto/hmac"
     7  	"crypto/sha256"
     8  	"encoding/base64"
     9  	"encoding/hex"
    10  	"encoding/xml"
    11  	"errors"
    12  	"fmt"
    13  	gohash "hash"
    14  	"io"
    15  	"math/rand"
    16  	"net/http"
    17  	"net/url"
    18  	"path"
    19  	"strconv"
    20  	"strings"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/artpar/rclone/fs"
    25  	"github.com/artpar/rclone/fs/config/configmap"
    26  	"github.com/artpar/rclone/fs/config/configstruct"
    27  	"github.com/artpar/rclone/fs/config/obscure"
    28  	"github.com/artpar/rclone/fs/fserrors"
    29  	"github.com/artpar/rclone/fs/fshttp"
    30  	"github.com/artpar/rclone/fs/hash"
    31  	"github.com/artpar/rclone/fs/walk"
    32  	"github.com/artpar/rclone/lib/pacer"
    33  	"github.com/artpar/rclone/lib/rest"
    34  )
    35  
    36  // Constants
    37  const (
    38  	minSleep      = 10 * time.Millisecond
    39  	maxSleep      = 2 * time.Second
    40  	decayConstant = 2 // bigger for slower decay, exponential
    41  )
    42  
    43  func init() {
    44  	fsi := &fs.RegInfo{
    45  		Name:        "netstorage",
    46  		Description: "Akamai NetStorage",
    47  		NewFs:       NewFs,
    48  		CommandHelp: commandHelp,
    49  		Options: []fs.Option{{
    50  			Name: "protocol",
    51  			Help: `Select between HTTP or HTTPS protocol.
    52  
    53  Most users should choose HTTPS, which is the default.
    54  HTTP is provided primarily for debugging purposes.`,
    55  			Examples: []fs.OptionExample{{
    56  				Value: "http",
    57  				Help:  "HTTP protocol",
    58  			}, {
    59  				Value: "https",
    60  				Help:  "HTTPS protocol",
    61  			}},
    62  			Default:  "https",
    63  			Advanced: true,
    64  		}, {
    65  			Name: "host",
    66  			Help: `Domain+path of NetStorage host to connect to.
    67  
    68  Format should be ` + "`<domain>/<internal folders>`",
    69  			Required:  true,
    70  			Sensitive: true,
    71  		}, {
    72  			Name:      "account",
    73  			Help:      "Set the NetStorage account name",
    74  			Required:  true,
    75  			Sensitive: true,
    76  		}, {
    77  			Name: "secret",
    78  			Help: `Set the NetStorage account secret/G2O key for authentication.
    79  
    80  Please choose the 'y' option to set your own password then enter your secret.`,
    81  			IsPassword: true,
    82  			Required:   true,
    83  		}},
    84  	}
    85  	fs.Register(fsi)
    86  }
    87  
    88  var commandHelp = []fs.CommandHelp{{
    89  	Name:  "du",
    90  	Short: "Return disk usage information for a specified directory",
    91  	Long: `The usage information returned, includes the targeted directory as well as all
    92  files stored in any sub-directories that may exist.`,
    93  }, {
    94  	Name:  "symlink",
    95  	Short: "You can create a symbolic link in ObjectStore with the symlink action.",
    96  	Long: `The desired path location (including applicable sub-directories) ending in
    97  the object that will be the target of the symlink (for example, /links/mylink).
    98  Include the file extension for the object, if applicable.
    99  ` + "`rclone backend symlink <src> <path>`",
   100  },
   101  }
   102  
   103  // Options defines the configuration for this backend
   104  type Options struct {
   105  	Endpoint string `config:"host"`
   106  	Account  string `config:"account"`
   107  	Secret   string `config:"secret"`
   108  	Protocol string `config:"protocol"`
   109  }
   110  
   111  // Fs stores the interface to the remote HTTP files
   112  type Fs struct {
   113  	name             string
   114  	root             string
   115  	features         *fs.Features      // optional features
   116  	opt              Options           // options for this backend
   117  	endpointURL      string            // endpoint as a string
   118  	srv              *rest.Client      // the connection to the Netstorage server
   119  	pacer            *fs.Pacer         // to pace the API calls
   120  	filetype         string            // dir, file or symlink
   121  	dirscreated      map[string]bool   // if implicit dir has been created already
   122  	dirscreatedMutex sync.Mutex        // mutex to protect dirscreated
   123  	statcache        map[string][]File // cache successful stat requests
   124  	statcacheMutex   sync.RWMutex      // RWMutex to protect statcache
   125  }
   126  
   127  // Object is a remote object that has been stat'd (so it exists, but is not necessarily open for reading)
   128  type Object struct {
   129  	fs       *Fs
   130  	filetype string // dir, file or symlink
   131  	remote   string // remote path
   132  	size     int64  // size of the object in bytes
   133  	modTime  int64  // modification time of the object
   134  	md5sum   string // md5sum of the object
   135  	fullURL  string // full path URL
   136  	target   string // symlink target when filetype is symlink
   137  }
   138  
   139  //------------------------------------------------------------------------------
   140  
   141  // Stat is an object which holds the information of the stat element of the response xml
   142  type Stat struct {
   143  	XMLName   xml.Name `xml:"stat"`
   144  	Files     []File   `xml:"file"`
   145  	Directory string   `xml:"directory,attr"`
   146  }
   147  
   148  // File is an object which holds the information of the file element of the response xml
   149  type File struct {
   150  	XMLName    xml.Name `xml:"file"`
   151  	Type       string   `xml:"type,attr"`
   152  	Name       string   `xml:"name,attr"`
   153  	NameBase64 string   `xml:"name_base64,attr"`
   154  	Size       int64    `xml:"size,attr"`
   155  	Md5        string   `xml:"md5,attr"`
   156  	Mtime      int64    `xml:"mtime,attr"`
   157  	Bytes      int64    `xml:"bytes,attr"`
   158  	Files      int64    `xml:"files,attr"`
   159  	Target     string   `xml:"target,attr"`
   160  }
   161  
   162  // List is an object which holds the information of the list element of the response xml
   163  type List struct {
   164  	XMLName xml.Name   `xml:"list"`
   165  	Files   []File     `xml:"file"`
   166  	Resume  ListResume `xml:"resume"`
   167  }
   168  
   169  // ListResume represents the resume xml element of the list
   170  type ListResume struct {
   171  	XMLName xml.Name `xml:"resume"`
   172  	Start   string   `xml:"start,attr"`
   173  }
   174  
   175  // Du represents the du xml element of the response
   176  type Du struct {
   177  	XMLName   xml.Name `xml:"du"`
   178  	Directory string   `xml:"directory,attr"`
   179  	Duinfo    DuInfo   `xml:"du-info"`
   180  }
   181  
   182  // DuInfo represents the du-info xml element of the response
   183  type DuInfo struct {
   184  	XMLName xml.Name `xml:"du-info"`
   185  	Files   int64    `xml:"files,attr"`
   186  	Bytes   int64    `xml:"bytes,attr"`
   187  }
   188  
   189  // GetName returns a normalized name of the Stat item
   190  func (s Stat) GetName() xml.Name {
   191  	return s.XMLName
   192  }
   193  
   194  // GetName returns a normalized name of the List item
   195  func (l List) GetName() xml.Name {
   196  	return l.XMLName
   197  }
   198  
   199  // GetName returns a normalized name of the Du item
   200  func (d Du) GetName() xml.Name {
   201  	return d.XMLName
   202  }
   203  
   204  //------------------------------------------------------------------------------
   205  
   206  // NewFs creates a new Fs object from the name and root. It connects to
   207  // the host specified in the config file.
   208  //
   209  // If root refers to an existing object, then it should return an Fs which
   210  // points to the parent of that object and ErrorIsFile.
   211  func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
   212  	// Parse config into Options struct
   213  	opt := new(Options)
   214  	err := configstruct.Set(m, opt)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	// The base URL (endPoint is protocol + :// + domain/internal folder
   220  	opt.Endpoint = opt.Protocol + "://" + opt.Endpoint
   221  	fs.Debugf(nil, "NetStorage NewFS endpoint %q", opt.Endpoint)
   222  	if !strings.HasSuffix(opt.Endpoint, "/") {
   223  		opt.Endpoint += "/"
   224  	}
   225  
   226  	// Decrypt credentials, even though it is hard to eyedrop the hex string, it adds an extra piece of mind
   227  	opt.Secret = obscure.MustReveal(opt.Secret)
   228  
   229  	// Parse the endpoint and stick the root onto it
   230  	base, err := url.Parse(opt.Endpoint)
   231  	if err != nil {
   232  		return nil, fmt.Errorf("couldn't parse URL %q: %w", opt.Endpoint, err)
   233  	}
   234  	u, err := rest.URLJoin(base, rest.URLPathEscape(root))
   235  	if err != nil {
   236  		return nil, fmt.Errorf("couldn't join URL %q and %q: %w", base.String(), root, err)
   237  	}
   238  	client := fshttp.NewClient(ctx)
   239  
   240  	f := &Fs{
   241  		name:        name,
   242  		root:        root,
   243  		opt:         *opt,
   244  		endpointURL: u.String(),
   245  		pacer:       fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
   246  		dirscreated: make(map[string]bool),
   247  		statcache:   make(map[string][]File),
   248  	}
   249  	f.srv = rest.NewClient(client)
   250  	f.srv.SetSigner(f.getAuth)
   251  
   252  	f.features = (&fs.Features{
   253  		CanHaveEmptyDirectories: true,
   254  	}).Fill(ctx, f)
   255  
   256  	err = f.initFs(ctx, "")
   257  	switch err {
   258  	case nil:
   259  		// Object is the directory
   260  		return f, nil
   261  	case fs.ErrorObjectNotFound:
   262  		return f, nil
   263  	case fs.ErrorIsFile:
   264  		// Correct root if definitely pointing to a file
   265  		f.root = path.Dir(f.root)
   266  		if f.root == "." || f.root == "/" {
   267  			f.root = ""
   268  		}
   269  		// Fs points to the parent directory
   270  		return f, err
   271  	default:
   272  		return nil, err
   273  	}
   274  }
   275  
   276  // Command the backend to run a named commands: du and symlink
   277  func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[string]string) (out interface{}, err error) {
   278  	switch name {
   279  	case "du":
   280  		// No arg parsing needed, the path is passed in the fs
   281  		return f.netStorageDuRequest(ctx)
   282  	case "symlink":
   283  		dst := ""
   284  		if len(arg) > 0 {
   285  			dst = arg[0]
   286  		} else {
   287  			return nil, errors.New("NetStorage symlink command: need argument for target")
   288  		}
   289  		// Strip off the leading slash added by NewFs on object not found
   290  		URL := strings.TrimSuffix(f.url(""), "/")
   291  		return f.netStorageSymlinkRequest(ctx, URL, dst, nil)
   292  	default:
   293  		return nil, fs.ErrorCommandNotFound
   294  	}
   295  }
   296  
   297  // Name returns the configured name of the file system
   298  func (f *Fs) Name() string {
   299  	return f.name
   300  }
   301  
   302  // Root returns the root for the filesystem
   303  func (f *Fs) Root() string {
   304  	return f.root
   305  }
   306  
   307  // String returns the URL for the filesystem
   308  func (f *Fs) String() string {
   309  	return f.endpointURL
   310  }
   311  
   312  // Features returns the optional features of this Fs
   313  func (f *Fs) Features() *fs.Features {
   314  	return f.features
   315  }
   316  
   317  // Precision return the precision of this Fs
   318  func (f *Fs) Precision() time.Duration {
   319  	return time.Second
   320  }
   321  
   322  // NewObject creates a new remote http file object
   323  // NewObject finds the Object at remote
   324  // If it can't be found returns fs.ErrorObjectNotFound
   325  // If it isn't a file, then it returns fs.ErrorIsDir
   326  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
   327  	URL := f.url(remote)
   328  	files, err := f.netStorageStatRequest(ctx, URL, false)
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  	if files == nil {
   333  		fs.Errorf(nil, "Stat for %q has empty files", URL)
   334  		return nil, fs.ErrorObjectNotFound
   335  	}
   336  
   337  	file := files[0]
   338  	switch file.Type {
   339  	case
   340  		"file",
   341  		"symlink":
   342  		return f.newObjectWithInfo(remote, &file)
   343  	case "dir":
   344  		return nil, fs.ErrorIsDir
   345  	default:
   346  		return nil, fmt.Errorf("object of an unsupported type %s for %q: %w", file.Type, URL, err)
   347  	}
   348  }
   349  
   350  // initFs initializes Fs based on the stat reply
   351  func (f *Fs) initFs(ctx context.Context, dir string) error {
   352  	// Path must end with the slash, so the join later will work correctly
   353  	defer func() {
   354  		if !strings.HasSuffix(f.endpointURL, "/") {
   355  			f.endpointURL += "/"
   356  		}
   357  	}()
   358  	URL := f.url(dir)
   359  	files, err := f.netStorageStatRequest(ctx, URL, true)
   360  	if err == fs.ErrorObjectNotFound || files == nil {
   361  		return fs.ErrorObjectNotFound
   362  	}
   363  	if err != nil {
   364  		return err
   365  	}
   366  
   367  	f.filetype = files[0].Type
   368  	switch f.filetype {
   369  	case "dir":
   370  		// This directory is known to exist, adding to explicit directories
   371  		f.setDirscreated(URL)
   372  		return nil
   373  	case
   374  		"file",
   375  		"symlink":
   376  		// Fs should point to the parent of that object and return ErrorIsFile
   377  		lastindex := strings.LastIndex(f.endpointURL, "/")
   378  		if lastindex != -1 {
   379  			f.endpointURL = f.endpointURL[0 : lastindex+1]
   380  		} else {
   381  			fs.Errorf(nil, "Remote URL %q unexpectedly does not include the slash", f.endpointURL)
   382  		}
   383  		return fs.ErrorIsFile
   384  	default:
   385  		err = fmt.Errorf("unsupported object type %s for %q: %w", f.filetype, URL, err)
   386  		f.filetype = ""
   387  		return err
   388  	}
   389  }
   390  
   391  // url joins the remote onto the endpoint URL
   392  func (f *Fs) url(remote string) string {
   393  	if remote == "" {
   394  		return f.endpointURL
   395  	}
   396  
   397  	pathescapeURL := rest.URLPathEscape(remote)
   398  	// Strip off initial "./" from the path, which can be added by path escape function following the RFC 3986 4.2
   399  	// (a segment must be preceded by a dot-segment (e.g., "./this:that") to make a relative-path reference).
   400  	pathescapeURL = strings.TrimPrefix(pathescapeURL, "./")
   401  	// Cannot use rest.URLJoin() here because NetStorage is an object storage and allows to have a "."
   402  	// directory name, which will be eliminated by the join function.
   403  	return f.endpointURL + pathescapeURL
   404  }
   405  
   406  // getFileName returns the file name if present, otherwise decoded name_base64
   407  // if present, otherwise an empty string
   408  func (f *Fs) getFileName(file *File) string {
   409  	if file.Name != "" {
   410  		return file.Name
   411  	}
   412  	if file.NameBase64 != "" {
   413  		decoded, err := base64.StdEncoding.DecodeString(file.NameBase64)
   414  		if err == nil {
   415  			return string(decoded)
   416  		}
   417  		fs.Errorf(nil, "Failed to base64 decode object %s: %v", file.NameBase64, err)
   418  	}
   419  	return ""
   420  }
   421  
   422  // List the objects and directories in dir into entries.  The
   423  // entries can be returned in any order but should be for a
   424  // complete directory.
   425  //
   426  // dir should be "" to list the root, and should not have
   427  // trailing slashes.
   428  //
   429  // This should return ErrDirNotFound if the directory isn't
   430  // found.
   431  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   432  	if f.filetype == "" {
   433  		// This happens in two scenarios.
   434  		// 1. NewFs is done on a nonexistent object, then later rclone attempts to List/ListR this NewFs.
   435  		// 2. List/ListR is called from the context of test_all and not the regular rclone binary.
   436  		err := f.initFs(ctx, dir)
   437  		if err != nil {
   438  			if err == fs.ErrorObjectNotFound {
   439  				return nil, fs.ErrorDirNotFound
   440  			}
   441  			return nil, err
   442  		}
   443  	}
   444  
   445  	URL := f.url(dir)
   446  	files, err := f.netStorageDirRequest(ctx, URL)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  	if dir != "" && !strings.HasSuffix(dir, "/") {
   451  		dir += "/"
   452  	}
   453  	for _, item := range files {
   454  		name := dir + f.getFileName(&item)
   455  		switch item.Type {
   456  		case "dir":
   457  			when := time.Unix(item.Mtime, 0)
   458  			entry := fs.NewDir(name, when).SetSize(item.Bytes).SetItems(item.Files)
   459  			entries = append(entries, entry)
   460  		case "file":
   461  			if entry, _ := f.newObjectWithInfo(name, &item); entry != nil {
   462  				entries = append(entries, entry)
   463  			}
   464  		case "symlink":
   465  			var entry fs.Object
   466  			// Add .rclonelink suffix to allow local backend code to convert to a symlink.
   467  			// In case both .rclonelink file AND symlink file exists, the first will be used.
   468  			if entry, _ = f.newObjectWithInfo(name+".rclonelink", &item); entry != nil {
   469  				fs.Infof(nil, "Converting a symlink to the rclonelink %s target %s", entry.Remote(), item.Target)
   470  				entries = append(entries, entry)
   471  			}
   472  		default:
   473  			fs.Logf(nil, "Ignoring unsupported object type %s for %q path", item.Type, name)
   474  		}
   475  	}
   476  	return entries, nil
   477  }
   478  
   479  // ListR lists the objects and directories of the Fs starting
   480  // from dir recursively into out.
   481  //
   482  // dir should be "" to start from the root, and should not
   483  // have trailing slashes.
   484  //
   485  // This should return ErrDirNotFound if the directory isn't
   486  // found.
   487  //
   488  // It should call callback for each tranche of entries read.
   489  // These need not be returned in any particular order.  If
   490  // callback returns an error then the listing will stop
   491  // immediately.
   492  //
   493  // Don't implement this unless you have a more efficient way
   494  // of listing recursively that doing a directory traversal.
   495  func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (err error) {
   496  	if f.filetype == "" {
   497  		// This happens in two scenarios.
   498  		// 1. NewFs is done on a nonexistent object, then later rclone attempts to List/ListR this NewFs.
   499  		// 2. List/ListR is called from the context of test_all and not the regular rclone binary.
   500  		err := f.initFs(ctx, dir)
   501  		if err != nil {
   502  			if err == fs.ErrorObjectNotFound {
   503  				return fs.ErrorDirNotFound
   504  			}
   505  			return err
   506  		}
   507  	}
   508  
   509  	if !strings.HasSuffix(dir, "/") && dir != "" {
   510  		dir += "/"
   511  	}
   512  	URL := f.url(dir)
   513  	u, err := url.Parse(URL)
   514  	if err != nil {
   515  		fs.Errorf(nil, "Unable to parse URL %q: %v", URL, err)
   516  		return fs.ErrorDirNotFound
   517  	}
   518  
   519  	list := walk.NewListRHelper(callback)
   520  	for resumeStart := u.Path; resumeStart != ""; {
   521  		var files []File
   522  		files, resumeStart, err = f.netStorageListRequest(ctx, URL, u.Path)
   523  		if err != nil {
   524  			if err == fs.ErrorObjectNotFound {
   525  				return fs.ErrorDirNotFound
   526  			}
   527  			return err
   528  		}
   529  		for _, item := range files {
   530  			name := f.getFileName(&item)
   531  			// List output includes full paths starting from [CP Code]/
   532  			path := strings.TrimPrefix("/"+name, u.Path)
   533  			if path == "" {
   534  				// Skip the starting directory itself
   535  				continue
   536  			}
   537  			switch item.Type {
   538  			case "dir":
   539  				when := time.Unix(item.Mtime, 0)
   540  				entry := fs.NewDir(dir+strings.TrimSuffix(path, "/"), when)
   541  				if err := list.Add(entry); err != nil {
   542  					return err
   543  				}
   544  			case "file":
   545  				if entry, _ := f.newObjectWithInfo(dir+path, &item); entry != nil {
   546  					if err := list.Add(entry); err != nil {
   547  						return err
   548  					}
   549  				}
   550  			case "symlink":
   551  				// Add .rclonelink suffix to allow local backend code to convert to a symlink.
   552  				// In case both .rclonelink file AND symlink file exists, the first will be used.
   553  				if entry, _ := f.newObjectWithInfo(dir+path+".rclonelink", &item); entry != nil {
   554  					fs.Infof(nil, "Converting a symlink to the rclonelink %s for target %s", entry.Remote(), item.Target)
   555  					if err := list.Add(entry); err != nil {
   556  						return err
   557  					}
   558  				}
   559  			default:
   560  				fs.Logf(nil, "Ignoring unsupported object type %s for %s path", item.Type, name)
   561  			}
   562  		}
   563  		if resumeStart != "" {
   564  			// Perform subsequent list action call, construct the
   565  			// URL where the previous request finished
   566  			u, err := url.Parse(f.endpointURL)
   567  			if err != nil {
   568  				fs.Errorf(nil, "Unable to parse URL %q: %v", f.endpointURL, err)
   569  				return fs.ErrorDirNotFound
   570  			}
   571  			resumeURL, err := rest.URLJoin(u, rest.URLPathEscape(resumeStart))
   572  			if err != nil {
   573  				fs.Errorf(nil, "Unable to join URL %q for resumeStart %s: %v", f.endpointURL, resumeStart, err)
   574  				return fs.ErrorDirNotFound
   575  			}
   576  			URL = resumeURL.String()
   577  		}
   578  
   579  	}
   580  	return list.Flush()
   581  }
   582  
   583  // Put in to the remote path with the modTime given of the given size
   584  //
   585  // May create the object even if it returns an error - if so
   586  // will return the object and the error, otherwise will return
   587  // nil and the error
   588  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   589  	err := f.implicitCheck(ctx, src.Remote(), true)
   590  	if err != nil {
   591  		return nil, err
   592  	}
   593  	// Barebones object will get filled in Update
   594  	o := &Object{
   595  		fs:      f,
   596  		remote:  src.Remote(),
   597  		fullURL: f.url(src.Remote()),
   598  	}
   599  	// We pass through the Update's error
   600  	err = o.Update(ctx, in, src, options...)
   601  	if err != nil {
   602  		return nil, err
   603  	}
   604  	return o, nil
   605  }
   606  
   607  // implicitCheck prevents implicit dir creation by doing mkdir from base up to current dir,
   608  // does NOT check if these dirs created conflict with existing dirs/files so can result in dupe
   609  func (f *Fs) implicitCheck(ctx context.Context, remote string, isfile bool) error {
   610  	// Find base (URL including the CPCODE path) and root (what follows after that)
   611  	URL := f.url(remote)
   612  	u, err := url.Parse(URL)
   613  	if err != nil {
   614  		fs.Errorf(nil, "Unable to parse URL %q while implicit checking directory: %v", URL, err)
   615  		return err
   616  	}
   617  	startPos := 0
   618  	if strings.HasPrefix(u.Path, "/") {
   619  		startPos = 1
   620  	}
   621  	pos := strings.Index(u.Path[startPos:], "/")
   622  	if pos == -1 {
   623  		fs.Errorf(nil, "URL %q unexpectedly does not include the slash in the CPCODE path", URL)
   624  		return nil
   625  	}
   626  	root := rest.URLPathEscape(u.Path[startPos+pos+1:])
   627  	u.Path = u.Path[:startPos+pos]
   628  	base := u.String()
   629  	if !strings.HasSuffix(base, "/") {
   630  		base += "/"
   631  	}
   632  
   633  	if isfile {
   634  		// Get the base name of root
   635  		lastindex := strings.LastIndex(root, "/")
   636  		if lastindex == -1 {
   637  			// We are at the level of CPCODE path
   638  			return nil
   639  		}
   640  		root = root[0 : lastindex+1]
   641  	}
   642  
   643  	// We make sure root always has "/" at the end
   644  	if !strings.HasSuffix(root, "/") {
   645  		root += "/"
   646  	}
   647  
   648  	for root != "" {
   649  		frontindex := strings.Index(root, "/")
   650  		if frontindex == -1 {
   651  			return nil
   652  		}
   653  		frontdir := root[0 : frontindex+1]
   654  		root = root[frontindex+1:]
   655  		base += frontdir
   656  		if !f.testAndSetDirscreated(base) {
   657  			fs.Infof(nil, "Implicitly create directory %s", base)
   658  			err := f.netStorageMkdirRequest(ctx, base)
   659  			if err != nil {
   660  				fs.Errorf("Mkdir request in implicit check failed for base %s: %v", base, err)
   661  				return err
   662  			}
   663  		}
   664  	}
   665  	return nil
   666  }
   667  
   668  // Purge all files in the directory specified.
   669  // NetStorage quick-delete is disabled by default AND not instantaneous.
   670  // Returns fs.ErrorCantPurge when quick-delete fails.
   671  func (f *Fs) Purge(ctx context.Context, dir string) error {
   672  	URL := f.url(dir)
   673  	const actionHeader = "version=1&action=quick-delete&quick-delete=imreallyreallysure"
   674  	if _, err := f.callBackend(ctx, URL, "POST", actionHeader, true, nil, nil); err != nil {
   675  		fs.Logf(nil, "Purge using quick-delete failed, fallback on recursive delete: %v", err)
   676  		return fs.ErrorCantPurge
   677  	}
   678  	fs.Logf(nil, "Purge using quick-delete has been queued, you may not see immediate changes")
   679  	return nil
   680  }
   681  
   682  // PutStream uploads to the remote path with the modTime given of indeterminate size
   683  func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   684  	// Pass through error from Put
   685  	return f.Put(ctx, in, src, options...)
   686  }
   687  
   688  // Fs is the filesystem this remote http file object is located within
   689  func (o *Object) Fs() fs.Info {
   690  	return o.fs
   691  }
   692  
   693  // String returns the URL to the remote HTTP file
   694  func (o *Object) String() string {
   695  	if o == nil {
   696  		return "<nil>"
   697  	}
   698  	return o.remote
   699  }
   700  
   701  // Remote the name of the remote HTTP file, relative to the fs root
   702  func (o *Object) Remote() string {
   703  	return o.remote
   704  }
   705  
   706  // Hash returns the Md5sum of an object returning a lowercase hex string
   707  func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
   708  	if t != hash.MD5 {
   709  		return "", hash.ErrUnsupported
   710  	}
   711  	return o.md5sum, nil
   712  }
   713  
   714  // Size returns the size in bytes of the remote http file
   715  func (o *Object) Size() int64 {
   716  	return o.size
   717  }
   718  
   719  // Md5Sum returns the md5 of the object
   720  func (o *Object) Md5Sum() string {
   721  	return o.md5sum
   722  }
   723  
   724  // ModTime returns the modification time of the object
   725  //
   726  // It attempts to read the objects mtime and if that isn't present the
   727  // LastModified returned in the http headers
   728  func (o *Object) ModTime(ctx context.Context) time.Time {
   729  	return time.Unix(o.modTime, 0)
   730  }
   731  
   732  // SetModTime sets the modification and access time to the specified time
   733  func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
   734  	URL := o.fullURL
   735  	when := strconv.FormatInt(modTime.Unix(), 10)
   736  	actionHeader := "version=1&action=mtime&mtime=" + when
   737  	if _, err := o.fs.callBackend(ctx, URL, "POST", actionHeader, true, nil, nil); err != nil {
   738  		fs.Debugf(nil, "NetStorage action mtime failed for %q: %v", URL, err)
   739  		return err
   740  	}
   741  	o.fs.deleteStatCache(URL)
   742  	o.modTime = modTime.Unix()
   743  	return nil
   744  }
   745  
   746  // Storable returns whether this object is storable
   747  func (o *Object) Storable() bool {
   748  	return true
   749  }
   750  
   751  // Open an object for read
   752  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
   753  	return o.netStorageDownloadRequest(ctx, options)
   754  }
   755  
   756  // Hashes returns the supported hash sets.
   757  func (f *Fs) Hashes() hash.Set {
   758  	return hash.Set(hash.MD5)
   759  }
   760  
   761  // Mkdir makes the root directory of the Fs object
   762  // Shouldn't return an error if it already exists
   763  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
   764  	// ImplicitCheck will mkdir from base up to dir, if not already in dirscreated
   765  	return f.implicitCheck(ctx, dir, false)
   766  }
   767  
   768  // Remove an object
   769  func (o *Object) Remove(ctx context.Context) error {
   770  	return o.netStorageDeleteRequest(ctx)
   771  }
   772  
   773  // Rmdir removes the root directory of the Fs object
   774  // Return an error if it doesn't exist or isn't empty
   775  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   776  	return f.netStorageRmdirRequest(ctx, dir)
   777  }
   778  
   779  // Update netstorage with the object
   780  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
   781  	o.size = src.Size()
   782  	o.modTime = src.ModTime(ctx).Unix()
   783  	// Don't do md5 check because that's done by server
   784  	o.md5sum = ""
   785  	err := o.netStorageUploadRequest(ctx, in, src)
   786  	// We return an object updated with source stats,
   787  	// we don't refetch the obj after upload
   788  	if err != nil {
   789  		return err
   790  	}
   791  	return nil
   792  }
   793  
   794  // newObjectWithInfo creates an fs.Object for any netstorage.File or symlink.
   795  // If it can't be found it returns the error fs.ErrorObjectNotFound.
   796  // It returns fs.ErrorIsDir error for directory objects, but still fills the
   797  // fs.Object structure (for directory operations).
   798  func (f *Fs) newObjectWithInfo(remote string, info *File) (fs.Object, error) {
   799  	if info == nil {
   800  		return nil, fs.ErrorObjectNotFound
   801  	}
   802  	URL := f.url(remote)
   803  	size := info.Size
   804  	if info.Type == "symlink" {
   805  		// File size for symlinks is absent but for .rclonelink to work
   806  		// the size should be the length of the target name
   807  		size = int64(len(info.Target))
   808  	}
   809  	o := &Object{
   810  		fs:       f,
   811  		filetype: info.Type,
   812  		remote:   remote,
   813  		size:     size,
   814  		modTime:  info.Mtime,
   815  		md5sum:   info.Md5,
   816  		fullURL:  URL,
   817  		target:   info.Target,
   818  	}
   819  	if info.Type == "dir" {
   820  		return o, fs.ErrorIsDir
   821  	}
   822  	return o, nil
   823  }
   824  
   825  // getAuth is the signing hook to get the NetStorage auth
   826  func (f *Fs) getAuth(req *http.Request) error {
   827  	// Set Authorization header
   828  	dataHeader := generateDataHeader(f)
   829  	path := req.URL.RequestURI()
   830  	//lint:ignore SA1008 false positive when running staticcheck, the header name is according to docs even if not canonical
   831  	//nolint:staticcheck // Don't include staticcheck when running golangci-lint to avoid SA1008
   832  	actionHeader := req.Header["X-Akamai-ACS-Action"][0]
   833  	fs.Debugf(nil, "NetStorage API %s call %s for path %q", req.Method, actionHeader, path)
   834  	req.Header.Set("X-Akamai-ACS-Auth-Data", dataHeader)
   835  	req.Header.Set("X-Akamai-ACS-Auth-Sign", generateSignHeader(f, dataHeader, path, actionHeader))
   836  	return nil
   837  }
   838  
   839  // retryErrorCodes is a slice of error codes that we will retry
   840  var retryErrorCodes = []int{
   841  	423, // Locked
   842  	429, // Too Many Requests
   843  	500, // Internal Server Error
   844  	502, // Bad Gateway
   845  	503, // Service Unavailable
   846  	504, // Gateway Timeout
   847  	509, // Bandwidth Limit Exceeded
   848  }
   849  
   850  // shouldRetry returns a boolean as to whether this resp and err
   851  // deserve to be retried.  It returns the err as a convenience
   852  func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
   853  	if fserrors.ContextError(ctx, &err) {
   854  		return false, err
   855  	}
   856  	return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
   857  }
   858  
   859  // callBackend calls NetStorage API using either rest.Call or rest.CallXML function,
   860  // depending on whether the response is required
   861  func (f *Fs) callBackend(ctx context.Context, URL, method, actionHeader string, noResponse bool, response interface{}, options []fs.OpenOption) (io.ReadCloser, error) {
   862  	opts := rest.Opts{
   863  		Method:     method,
   864  		RootURL:    URL,
   865  		NoResponse: noResponse,
   866  		ExtraHeaders: map[string]string{
   867  			"*X-Akamai-ACS-Action": actionHeader,
   868  		},
   869  	}
   870  	if options != nil {
   871  		opts.Options = options
   872  	}
   873  
   874  	var resp *http.Response
   875  	err := f.pacer.Call(func() (bool, error) {
   876  		var err error
   877  		if response != nil {
   878  			resp, err = f.srv.CallXML(ctx, &opts, nil, response)
   879  		} else {
   880  			resp, err = f.srv.Call(ctx, &opts)
   881  		}
   882  		return shouldRetry(ctx, resp, err)
   883  	})
   884  	if err != nil {
   885  		if resp != nil && resp.StatusCode == http.StatusNotFound {
   886  			// 404 HTTP code translates into Object not found
   887  			return nil, fs.ErrorObjectNotFound
   888  		}
   889  		return nil, fmt.Errorf("failed to call NetStorage API: %w", err)
   890  	}
   891  	if noResponse {
   892  		return nil, nil
   893  	}
   894  	return resp.Body, nil
   895  }
   896  
   897  // netStorageStatRequest performs a NetStorage stat request
   898  func (f *Fs) netStorageStatRequest(ctx context.Context, URL string, directory bool) ([]File, error) {
   899  	if strings.HasSuffix(URL, ".rclonelink") {
   900  		fs.Infof(nil, "Converting rclonelink to a symlink on the stat request %q", URL)
   901  		URL = strings.TrimSuffix(URL, ".rclonelink")
   902  	}
   903  	URL = strings.TrimSuffix(URL, "/")
   904  	files := f.getStatCache(URL)
   905  	if files == nil {
   906  		const actionHeader = "version=1&action=stat&implicit=yes&format=xml&encoding=utf-8&slash=both"
   907  		statResp := &Stat{}
   908  		if _, err := f.callBackend(ctx, URL, "GET", actionHeader, false, statResp, nil); err != nil {
   909  			fs.Debugf(nil, "NetStorage action stat failed for %q: %v", URL, err)
   910  			return nil, err
   911  		}
   912  		files = statResp.Files
   913  		f.setStatCache(URL, files)
   914  	}
   915  	// Multiple objects can be returned with the "slash=both" option,
   916  	// when file/symlink/directory has the same name
   917  	for i := range files {
   918  		if files[i].Type == "symlink" {
   919  			// Add .rclonelink suffix to allow local backend code to convert to a symlink.
   920  			files[i].Name += ".rclonelink"
   921  			fs.Infof(nil, "Converting a symlink to the rclonelink on the stat request %s", files[i].Name)
   922  		}
   923  		entrywanted := (directory && files[i].Type == "dir") ||
   924  			(!directory && files[i].Type != "dir")
   925  		if entrywanted {
   926  			filestamp := files[0]
   927  			files[0] = files[i]
   928  			files[i] = filestamp
   929  		}
   930  	}
   931  	return files, nil
   932  }
   933  
   934  // netStorageDirRequest performs a NetStorage dir request
   935  func (f *Fs) netStorageDirRequest(ctx context.Context, URL string) ([]File, error) {
   936  	const actionHeader = "version=1&action=dir&format=xml&encoding=utf-8"
   937  	statResp := &Stat{}
   938  	if _, err := f.callBackend(ctx, URL, "GET", actionHeader, false, statResp, nil); err != nil {
   939  		if err == fs.ErrorObjectNotFound {
   940  			return nil, fs.ErrorDirNotFound
   941  		}
   942  		fs.Debugf(nil, "NetStorage action dir failed for %q: %v", URL, err)
   943  		return nil, err
   944  	}
   945  	return statResp.Files, nil
   946  }
   947  
   948  // netStorageListRequest performs a NetStorage list request
   949  // Second returning parameter is resumeStart string, if not empty the function should be restarted with the adjusted URL to continue the listing.
   950  func (f *Fs) netStorageListRequest(ctx context.Context, URL, endPath string) ([]File, string, error) {
   951  	actionHeader := "version=1&action=list&mtime_all=yes&format=xml&encoding=utf-8"
   952  	if !pathIsOneLevelDeep(endPath) {
   953  		// Add end= to limit the depth to endPath
   954  		escapeEndPath := url.QueryEscape(strings.TrimSuffix(endPath, "/"))
   955  		// The "0" character exists in place of the trailing slash to
   956  		// accommodate ObjectStore directory logic
   957  		end := "&end=" + strings.TrimSuffix(escapeEndPath, "/") + "0"
   958  		actionHeader += end
   959  	}
   960  	listResp := &List{}
   961  	if _, err := f.callBackend(ctx, URL, "GET", actionHeader, false, listResp, nil); err != nil {
   962  		if err == fs.ErrorObjectNotFound {
   963  			// List action is known to return 404 for a valid [CP Code] path with no objects inside.
   964  			// Call stat to find out whether it is an empty directory or path does not exist.
   965  			fs.Debugf(nil, "NetStorage action list returned 404, call stat for %q", URL)
   966  			files, err := f.netStorageStatRequest(ctx, URL, true)
   967  			if err == nil && len(files) > 0 && files[0].Type == "dir" {
   968  				return []File{}, "", nil
   969  			}
   970  		}
   971  		fs.Debugf(nil, "NetStorage action list failed for %q: %v", URL, err)
   972  		return nil, "", err
   973  	}
   974  	return listResp.Files, listResp.Resume.Start, nil
   975  }
   976  
   977  // netStorageUploadRequest performs a NetStorage upload request
   978  func (o *Object) netStorageUploadRequest(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
   979  	URL := o.fullURL
   980  	if URL == "" {
   981  		URL = o.fs.url(src.Remote())
   982  	}
   983  	if strings.HasSuffix(URL, ".rclonelink") {
   984  		bits, err := io.ReadAll(in)
   985  		if err != nil {
   986  			return err
   987  		}
   988  		targ := string(bits)
   989  		symlinkloc := strings.TrimSuffix(URL, ".rclonelink")
   990  		fs.Infof(nil, "Converting rclonelink to a symlink on upload %s target %s", symlinkloc, targ)
   991  		_, err = o.fs.netStorageSymlinkRequest(ctx, symlinkloc, targ, &o.modTime)
   992  		return err
   993  	}
   994  
   995  	u, err := url.Parse(URL)
   996  	if err != nil {
   997  		return fmt.Errorf("unable to parse URL %q while uploading: %w", URL, err)
   998  	}
   999  	path := u.RequestURI()
  1000  
  1001  	const actionHeader = "version=1&action=upload&sha256=atend&mtime=atend"
  1002  	trailers := &http.Header{}
  1003  	hr := newHashReader(in, sha256.New())
  1004  	reader := customReader(
  1005  		func(p []byte) (n int, err error) {
  1006  			if n, err = hr.Read(p); err != nil && err == io.EOF {
  1007  				// Send the "chunked trailer" after upload of the object
  1008  				digest := hex.EncodeToString(hr.Sum(nil))
  1009  				actionHeader := "version=1&action=upload&sha256=" + digest +
  1010  					"&mtime=" + strconv.FormatInt(src.ModTime(ctx).Unix(), 10)
  1011  				trailers.Add("X-Akamai-ACS-Action", actionHeader)
  1012  				dataHeader := generateDataHeader(o.fs)
  1013  				trailers.Add("X-Akamai-ACS-Auth-Data", dataHeader)
  1014  				signHeader := generateSignHeader(o.fs, dataHeader, path, actionHeader)
  1015  				trailers.Add("X-Akamai-ACS-Auth-Sign", signHeader)
  1016  			}
  1017  			return
  1018  		},
  1019  	)
  1020  
  1021  	var resp *http.Response
  1022  	opts := rest.Opts{
  1023  		Method:     "PUT",
  1024  		RootURL:    URL,
  1025  		NoResponse: true,
  1026  		Options:    options,
  1027  		Body:       reader,
  1028  		Trailer:    trailers,
  1029  		ExtraHeaders: map[string]string{
  1030  			"*X-Akamai-ACS-Action": actionHeader,
  1031  		},
  1032  	}
  1033  	err = o.fs.pacer.CallNoRetry(func() (bool, error) {
  1034  		resp, err = o.fs.srv.Call(ctx, &opts)
  1035  		return shouldRetry(ctx, resp, err)
  1036  	})
  1037  	if err != nil {
  1038  		if resp != nil && resp.StatusCode == http.StatusNotFound {
  1039  			// 404 HTTP code translates into Object not found
  1040  			return fs.ErrorObjectNotFound
  1041  		}
  1042  		fs.Debugf(nil, "NetStorage action upload failed for %q: %v", URL, err)
  1043  		// Remove failed upload
  1044  		_ = o.Remove(ctx)
  1045  		return fmt.Errorf("failed to call NetStorage API upload: %w", err)
  1046  	}
  1047  
  1048  	// Invalidate stat cache
  1049  	o.fs.deleteStatCache(URL)
  1050  	if o.size == -1 {
  1051  		files, err := o.fs.netStorageStatRequest(ctx, URL, false)
  1052  		if err != nil {
  1053  			return nil
  1054  		}
  1055  		if files != nil {
  1056  			o.size = files[0].Size
  1057  		}
  1058  	}
  1059  	return nil
  1060  }
  1061  
  1062  // netStorageDownloadRequest performs a NetStorage download request
  1063  func (o *Object) netStorageDownloadRequest(ctx context.Context, options []fs.OpenOption) (in io.ReadCloser, err error) {
  1064  	URL := o.fullURL
  1065  	// If requested file ends with .rclonelink and target has value
  1066  	// then serve the content of target (the symlink target)
  1067  	if strings.HasSuffix(URL, ".rclonelink") && o.target != "" {
  1068  		fs.Infof(nil, "Converting a symlink to the rclonelink file on download %q", URL)
  1069  		reader := strings.NewReader(o.target)
  1070  		readcloser := io.NopCloser(reader)
  1071  		return readcloser, nil
  1072  	}
  1073  
  1074  	const actionHeader = "version=1&action=download"
  1075  	fs.FixRangeOption(options, o.size)
  1076  	body, err := o.fs.callBackend(ctx, URL, "GET", actionHeader, false, nil, options)
  1077  	if err != nil {
  1078  		fs.Debugf(nil, "NetStorage action download failed for %q: %v", URL, err)
  1079  		return nil, err
  1080  	}
  1081  	return body, nil
  1082  }
  1083  
  1084  // netStorageDuRequest performs a NetStorage du request
  1085  func (f *Fs) netStorageDuRequest(ctx context.Context) (interface{}, error) {
  1086  	URL := f.url("")
  1087  	const actionHeader = "version=1&action=du&format=xml&encoding=utf-8"
  1088  	duResp := &Du{}
  1089  	if _, err := f.callBackend(ctx, URL, "GET", actionHeader, false, duResp, nil); err != nil {
  1090  		if err == fs.ErrorObjectNotFound {
  1091  			return nil, errors.New("NetStorage du command: target is not a directory or does not exist")
  1092  		}
  1093  		fs.Debugf(nil, "NetStorage action du failed for %q: %v", URL, err)
  1094  		return nil, err
  1095  	}
  1096  	//passing the output format expected from return of Command to be displayed by rclone code
  1097  	out := map[string]int64{
  1098  		"Number of files": duResp.Duinfo.Files,
  1099  		"Total bytes":     duResp.Duinfo.Bytes,
  1100  	}
  1101  	return out, nil
  1102  }
  1103  
  1104  // netStorageDuRequest performs a NetStorage symlink request
  1105  func (f *Fs) netStorageSymlinkRequest(ctx context.Context, URL string, dst string, modTime *int64) (interface{}, error) {
  1106  	target := url.QueryEscape(strings.TrimSuffix(dst, "/"))
  1107  	actionHeader := "version=1&action=symlink&target=" + target
  1108  	if modTime != nil {
  1109  		when := strconv.FormatInt(*modTime, 10)
  1110  		actionHeader += "&mtime=" + when
  1111  	}
  1112  	if _, err := f.callBackend(ctx, URL, "POST", actionHeader, true, nil, nil); err != nil {
  1113  		fs.Debugf(nil, "NetStorage action symlink failed for %q: %v", URL, err)
  1114  		return nil, fmt.Errorf("symlink creation failed: %w", err)
  1115  	}
  1116  	f.deleteStatCache(URL)
  1117  	out := map[string]string{
  1118  		"Symlink successfully created": dst,
  1119  	}
  1120  	return out, nil
  1121  }
  1122  
  1123  // netStorageMkdirRequest performs a NetStorage mkdir request
  1124  func (f *Fs) netStorageMkdirRequest(ctx context.Context, URL string) error {
  1125  	const actionHeader = "version=1&action=mkdir"
  1126  	if _, err := f.callBackend(ctx, URL, "POST", actionHeader, true, nil, nil); err != nil {
  1127  		fs.Debugf(nil, "NetStorage action mkdir failed for %q: %v", URL, err)
  1128  		return err
  1129  	}
  1130  	f.deleteStatCache(URL)
  1131  	return nil
  1132  }
  1133  
  1134  // netStorageDeleteRequest performs a NetStorage delete request
  1135  func (o *Object) netStorageDeleteRequest(ctx context.Context) error {
  1136  	URL := o.fullURL
  1137  	// We shouldn't be creating .rclonelink files on remote
  1138  	// but delete corresponding symlink if it exists
  1139  	if strings.HasSuffix(URL, ".rclonelink") {
  1140  		fs.Infof(nil, "Converting rclonelink to a symlink on delete %q", URL)
  1141  		URL = strings.TrimSuffix(URL, ".rclonelink")
  1142  	}
  1143  
  1144  	const actionHeader = "version=1&action=delete"
  1145  	if _, err := o.fs.callBackend(ctx, URL, "POST", actionHeader, true, nil, nil); err != nil {
  1146  		fs.Debugf(nil, "NetStorage action delete failed for %q: %v", URL, err)
  1147  		return err
  1148  	}
  1149  	o.fs.deleteStatCache(URL)
  1150  	return nil
  1151  }
  1152  
  1153  // netStorageRmdirRequest performs a NetStorage rmdir request
  1154  func (f *Fs) netStorageRmdirRequest(ctx context.Context, dir string) error {
  1155  	URL := f.url(dir)
  1156  	const actionHeader = "version=1&action=rmdir"
  1157  	if _, err := f.callBackend(ctx, URL, "POST", actionHeader, true, nil, nil); err != nil {
  1158  		if err == fs.ErrorObjectNotFound {
  1159  			return fs.ErrorDirNotFound
  1160  		}
  1161  		fs.Debugf(nil, "NetStorage action rmdir failed for %q: %v", URL, err)
  1162  		return err
  1163  	}
  1164  	f.deleteStatCache(URL)
  1165  	f.deleteDirscreated(URL)
  1166  	return nil
  1167  }
  1168  
  1169  // deleteDirscreated deletes URL from dirscreated map thread-safely
  1170  func (f *Fs) deleteDirscreated(URL string) {
  1171  	URL = strings.TrimSuffix(URL, "/")
  1172  	f.dirscreatedMutex.Lock()
  1173  	delete(f.dirscreated, URL)
  1174  	f.dirscreatedMutex.Unlock()
  1175  }
  1176  
  1177  // setDirscreated sets to true URL in dirscreated map thread-safely
  1178  func (f *Fs) setDirscreated(URL string) {
  1179  	URL = strings.TrimSuffix(URL, "/")
  1180  	f.dirscreatedMutex.Lock()
  1181  	f.dirscreated[URL] = true
  1182  	f.dirscreatedMutex.Unlock()
  1183  }
  1184  
  1185  // testAndSetDirscreated atomic test-and-set to true URL in dirscreated map,
  1186  // returns the previous value
  1187  func (f *Fs) testAndSetDirscreated(URL string) bool {
  1188  	URL = strings.TrimSuffix(URL, "/")
  1189  	f.dirscreatedMutex.Lock()
  1190  	oldValue := f.dirscreated[URL]
  1191  	f.dirscreated[URL] = true
  1192  	f.dirscreatedMutex.Unlock()
  1193  	return oldValue
  1194  }
  1195  
  1196  // deleteStatCache deletes URL from stat cache thread-safely
  1197  func (f *Fs) deleteStatCache(URL string) {
  1198  	URL = strings.TrimSuffix(URL, "/")
  1199  	f.statcacheMutex.Lock()
  1200  	delete(f.statcache, URL)
  1201  	f.statcacheMutex.Unlock()
  1202  }
  1203  
  1204  // getStatCache gets value from statcache map thread-safely
  1205  func (f *Fs) getStatCache(URL string) (files []File) {
  1206  	URL = strings.TrimSuffix(URL, "/")
  1207  	f.statcacheMutex.RLock()
  1208  	files = f.statcache[URL]
  1209  	f.statcacheMutex.RUnlock()
  1210  	if files != nil {
  1211  		fs.Debugf(nil, "NetStorage stat cache hit for %q", URL)
  1212  	}
  1213  	return
  1214  }
  1215  
  1216  // setStatCache sets value to statcache map thread-safely
  1217  func (f *Fs) setStatCache(URL string, files []File) {
  1218  	URL = strings.TrimSuffix(URL, "/")
  1219  	f.statcacheMutex.Lock()
  1220  	f.statcache[URL] = files
  1221  	f.statcacheMutex.Unlock()
  1222  }
  1223  
  1224  type hashReader struct {
  1225  	io.Reader
  1226  	gohash.Hash
  1227  }
  1228  
  1229  func newHashReader(r io.Reader, h gohash.Hash) hashReader {
  1230  	return hashReader{io.TeeReader(r, h), h}
  1231  }
  1232  
  1233  type customReader func([]byte) (int, error)
  1234  
  1235  func (c customReader) Read(p []byte) (n int, err error) {
  1236  	return c(p)
  1237  }
  1238  
  1239  // generateRequestID generates the unique requestID
  1240  func generateRequestID() int64 {
  1241  	s1 := rand.NewSource(time.Now().UnixNano())
  1242  	r1 := rand.New(s1)
  1243  	return r1.Int63()
  1244  }
  1245  
  1246  // computeHmac256 calculates the hash for the sign header
  1247  func computeHmac256(message string, secret string) string {
  1248  	key := []byte(secret)
  1249  	h := hmac.New(sha256.New, key)
  1250  	h.Write([]byte(message))
  1251  	return base64.StdEncoding.EncodeToString(h.Sum(nil))
  1252  }
  1253  
  1254  // getEpochTimeInSeconds returns current epoch time in seconds
  1255  func getEpochTimeInSeconds() int64 {
  1256  	now := time.Now()
  1257  	secs := now.Unix()
  1258  	return secs
  1259  }
  1260  
  1261  // generateDataHeader generates data header needed for making the request
  1262  func generateDataHeader(f *Fs) string {
  1263  	return "5, 0.0.0.0, 0.0.0.0, " + strconv.FormatInt(getEpochTimeInSeconds(), 10) + ", " + strconv.FormatInt(generateRequestID(), 10) + "," + f.opt.Account
  1264  }
  1265  
  1266  // generateSignHeader generates sign header needed for making the request
  1267  func generateSignHeader(f *Fs, dataHeader string, path string, actionHeader string) string {
  1268  	var message = dataHeader + path + "\nx-akamai-acs-action:" + actionHeader + "\n"
  1269  	return computeHmac256(message, f.opt.Secret)
  1270  }
  1271  
  1272  // pathIsOneLevelDeep returns true if a given path does not go deeper than one level
  1273  func pathIsOneLevelDeep(path string) bool {
  1274  	return !strings.Contains(strings.TrimSuffix(strings.TrimPrefix(path, "/"), "/"), "/")
  1275  }
  1276  
  1277  // Check the interfaces are satisfied
  1278  var (
  1279  	_ fs.Fs          = &Fs{}
  1280  	_ fs.Purger      = &Fs{}
  1281  	_ fs.PutStreamer = &Fs{}
  1282  	_ fs.ListRer     = &Fs{}
  1283  	_ fs.Object      = &Object{}
  1284  )