github.com/Files-com/files-sdk-go/v2@v2.1.2/file/remotefs.go (about)

     1  package file
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	goFs "io/fs"
    11  	"math"
    12  	"math/rand"
    13  	"net/http"
    14  	"path/filepath"
    15  	"strconv"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  
    20  	files_sdk "github.com/Files-com/files-sdk-go/v2"
    21  	"github.com/Files-com/files-sdk-go/v2/folder"
    22  	"github.com/Files-com/files-sdk-go/v2/lib"
    23  	"github.com/samber/lo"
    24  )
    25  
    26  type SizeTrust int
    27  
    28  const (
    29  	NullSizeTrust SizeTrust = iota
    30  	UntrustedSizeValue
    31  	TrustedSizeValue
    32  )
    33  
    34  type FS struct {
    35  	files_sdk.Config
    36  	context.Context
    37  	Root       string
    38  	cache      *sync.Map
    39  	cacheDir   *sync.Map
    40  	useCache   bool
    41  	cacheMutex *lib.KeyedMutex
    42  }
    43  
    44  func (f *FS) Init(config files_sdk.Config, cache bool) *FS {
    45  	f.Config = config
    46  	f.ClearCache()
    47  	f.useCache = cache
    48  	return f
    49  }
    50  
    51  func (f *FS) WithContext(ctx context.Context) interface{} {
    52  	return &FS{Context: ctx, Config: f.Config, cache: f.cache, useCache: f.useCache, cacheDir: f.cacheDir, cacheMutex: f.cacheMutex}
    53  }
    54  
    55  func (f *FS) ClearCache() {
    56  	f.cache = &sync.Map{}
    57  	f.cacheDir = &sync.Map{}
    58  	m := lib.NewKeyedMutex()
    59  	f.cacheMutex = &m
    60  }
    61  
    62  type File struct {
    63  	*files_sdk.File
    64  	*FS
    65  	io.ReadCloser
    66  	downloadRequestId string
    67  	MaxConnections    int
    68  	stat              bool
    69  	fileMutex         *sync.Mutex
    70  	SizeTrust
    71  	serverBytesSent int64
    72  }
    73  
    74  type ReadDirFile struct {
    75  	*File
    76  	count int
    77  }
    78  
    79  func (f *File) safeFile() files_sdk.File {
    80  	f.fileMutex.Lock()
    81  	defer f.fileMutex.Unlock()
    82  	return *f.File
    83  }
    84  
    85  func (f *File) Init() *File {
    86  	f.fileMutex = &sync.Mutex{}
    87  	f.SizeTrust = NullSizeTrust
    88  	return f
    89  }
    90  
    91  func (f *File) Name() string {
    92  	return f.safeFile().DisplayName
    93  }
    94  
    95  func (f *File) IsDir() bool {
    96  	return f.safeFile().Type == "directory"
    97  }
    98  
    99  func (f *File) Type() goFs.FileMode {
   100  	return goFs.ModePerm
   101  }
   102  
   103  func (f *File) Info() (goFs.FileInfo, error) {
   104  	return f.Stat()
   105  }
   106  
   107  type Info struct {
   108  	files_sdk.File
   109  	sizeTrust SizeTrust
   110  }
   111  
   112  func (i Info) Name() string {
   113  	return i.File.DisplayName
   114  }
   115  
   116  func (i Info) Size() int64 {
   117  	return i.File.Size
   118  }
   119  
   120  type UntrustedSize interface {
   121  	UntrustedSize() bool
   122  	SizeTrust() SizeTrust
   123  	goFs.FileInfo
   124  }
   125  
   126  func (i Info) UntrustedSize() bool {
   127  	return i.sizeTrust == UntrustedSizeValue || i.sizeTrust == NullSizeTrust
   128  }
   129  
   130  func (i Info) SizeTrust() SizeTrust {
   131  	return i.sizeTrust
   132  }
   133  
   134  type PossibleSize interface {
   135  	PossibleSize() int64
   136  }
   137  
   138  func (i Info) PossibleSize() int64 {
   139  	return i.File.Size
   140  }
   141  
   142  func (i Info) Mode() goFs.FileMode {
   143  	return goFs.ModePerm
   144  }
   145  
   146  func (i Info) ModTime() time.Time {
   147  	return *i.File.Mtime
   148  }
   149  
   150  func (i Info) IsDir() bool {
   151  	return i.File.Type == "directory"
   152  }
   153  
   154  func (i Info) Sys() interface{} {
   155  	return i.File
   156  }
   157  
   158  func (i Info) RemoteMount() bool {
   159  	if i.Crc32 != "" { // Detect if is Files.com native file.
   160  		return false
   161  	}
   162  
   163  	return true
   164  }
   165  
   166  func (f *File) Stat() (goFs.FileInfo, error) {
   167  	f.fileMutex.Lock()
   168  	defer f.fileMutex.Unlock()
   169  	return Info{File: *f.File, sizeTrust: f.SizeTrust}, nil
   170  }
   171  
   172  func (f *File) Read(b []byte) (n int, err error) {
   173  	f.fileMutex.Lock()
   174  	defer f.fileMutex.Unlock()
   175  
   176  	if f.ReadCloser == nil {
   177  		err = f.readCloserInit()
   178  		if downloadRequestExpired(err) {
   179  			f.Config.LogPath(f.File.Path, map[string]interface{}{"message": "downloadRequestExpired", "error": err})
   180  			f.File.DownloadUri = "" // force a new query
   181  			*f.File, err = (&Client{Config: f.Config}).DownloadUri(files_sdk.FileDownloadParams{File: *f.File}, files_sdk.WithContext(f.Context))
   182  			if err == nil {
   183  				err = f.readCloserInit()
   184  			}
   185  		}
   186  
   187  		if err != nil {
   188  			status, statusErr := (&Client{Config: f.Config}).DownloadRequestStatus(f.File.DownloadUri, f.downloadRequestId, files_sdk.WithContext(f.Context))
   189  			if statusErr != nil {
   190  				return n, err
   191  			}
   192  			if !status.IsNil() {
   193  				return n, status
   194  			}
   195  
   196  			return
   197  		}
   198  	}
   199  
   200  	return f.ReadCloser.Read(b)
   201  }
   202  
   203  func parseSize(response *http.Response) (size int64, sizeTrust SizeTrust) {
   204  	var err error
   205  
   206  	if response.StatusCode == http.StatusPartialContent {
   207  		if contentRange := response.Header.Get("Content-Range"); contentRange != "" {
   208  			rangeParts := strings.SplitN(contentRange, "/", 2)
   209  			if len(rangeParts) == 2 {
   210  				size, err = strconv.ParseInt(rangeParts[1], 10, 64)
   211  				if err == nil {
   212  					sizeTrust = TrustedSizeValue
   213  					return
   214  				}
   215  			}
   216  		}
   217  	} else if response.ContentLength > -1 {
   218  		sizeTrust = TrustedSizeValue
   219  		size = response.ContentLength
   220  
   221  		return
   222  	}
   223  
   224  	// For some remote mounts file size information cannot be trusted and will not be returned.
   225  	// In order to ensure the total file was received after a download `Client{}.DownloadRequestStatus` should be called.
   226  	sizeTrust = UntrustedSizeValue
   227  
   228  	return
   229  }
   230  
   231  func parseMaxConnections(response *http.Response) int {
   232  	maxConnections, _ := strconv.Atoi(response.Header.Get("X-Files-Max-Connections"))
   233  	return maxConnections
   234  }
   235  
   236  func (f *File) readCloserInit() (err error) {
   237  	*f.File, err = (&Client{Config: f.Config}).Download(
   238  		files_sdk.FileDownloadParams{File: *f.File},
   239  		files_sdk.WithContext(f.Context),
   240  		files_sdk.ResponseOption(func(response *http.Response) error {
   241  			f.MaxConnections = parseMaxConnections(response)
   242  			f.downloadRequestId = response.Header.Get("X-Files-Download-Request-Id")
   243  			f.Size, f.SizeTrust = parseSize(response)
   244  			if err := lib.ResponseErrors(response, files_sdk.APIError(), lib.NotStatus(http.StatusOK)); err != nil {
   245  				return &goFs.PathError{Path: f.File.Path, Err: err, Op: "read"}
   246  			}
   247  
   248  			f.ReadCloser = &ReadWrapper{ReadCloser: response.Body}
   249  			return nil
   250  		}),
   251  	)
   252  	return err
   253  }
   254  
   255  type ReaderRange interface {
   256  	ReaderRange(off int64, end int64) (io.ReadCloser, error)
   257  	goFs.File
   258  }
   259  
   260  type ReadAtLeastWrapper struct {
   261  	io.ReadCloser
   262  	io.Reader
   263  }
   264  
   265  func (r ReadAtLeastWrapper) Close() error {
   266  	return r.ReadCloser.Close()
   267  }
   268  
   269  func (f ReadAtLeastWrapper) Read(b []byte) (n int, err error) {
   270  	return f.Reader.Read(b)
   271  }
   272  
   273  func (f *File) ReaderRange(off int64, end int64) (r io.ReadCloser, err error) {
   274  	if err = f.downloadURI(); err != nil {
   275  		return
   276  	}
   277  	f.fileMutex.Lock()
   278  	rangerReaderCloser := ReaderCloserDownloadStatus{file: f, expectedSize: (end + 1) - off, rangeRequest: true, ReadWrapper: &ReadWrapper{}}
   279  
   280  	headers := &http.Header{}
   281  	headers.Set("Range", fmt.Sprintf("bytes=%v-%v", off, end))
   282  	_, err = (&Client{Config: f.Config}).Download(
   283  		files_sdk.FileDownloadParams{File: *f.File},
   284  		files_sdk.WithContext(f.Context),
   285  		files_sdk.RequestHeadersOption(headers),
   286  		files_sdk.ResponseOption(func(response *http.Response) error {
   287  			f.downloadRequestId = response.Header.Get("X-Files-Download-Request-Id")
   288  			rangerReaderCloser.file.downloadRequestId = response.Header.Get("X-Files-Download-Request-Id")
   289  			f.MaxConnections = parseMaxConnections(response)
   290  			f.Size, f.SizeTrust = parseSize(response)
   291  			if err := lib.ResponseErrors(response, lib.IsStatus(http.StatusForbidden), files_sdk.APIError(), lib.NotStatus(http.StatusPartialContent)); err != nil {
   292  				return &goFs.PathError{Path: f.File.Path, Err: err, Op: "ReaderRange"}
   293  			}
   294  			rangerReaderCloser.ReadCloser = &ReadWrapper{ReadCloser: response.Body}
   295  			return nil
   296  		}),
   297  	)
   298  	f.fileMutex.Unlock()
   299  	if downloadRequestExpired(err) {
   300  		f.Config.LogPath(f.File.Path, map[string]interface{}{"message": "downloadRequestExpired", "error": err})
   301  		f.File.DownloadUri = "" // force a new query
   302  		err = f.downloadURI()
   303  		if err != nil {
   304  			return r, err
   305  		}
   306  
   307  		return f.ReaderRange(off, end)
   308  	}
   309  	return rangerReaderCloser, err
   310  }
   311  
   312  type ReadWrapper struct {
   313  	io.ReadCloser
   314  	read int
   315  }
   316  
   317  func (r *ReadWrapper) Read(p []byte) (n int, err error) {
   318  	n, err = r.ReadCloser.Read(p)
   319  	r.read += n
   320  	return
   321  }
   322  
   323  type ReaderCloserDownloadStatus struct {
   324  	*ReadWrapper
   325  	file         *File
   326  	expectedSize int64
   327  	rangeRequest bool
   328  	UntrustedSizeRangeRequestSize
   329  }
   330  
   331  type UntrustedSizeRangeRequestSize struct {
   332  	ExpectedSize int64
   333  	SentSize     int64
   334  	ReceivedSize int64
   335  	Status       string
   336  }
   337  
   338  func (u UntrustedSizeRangeRequestSize) VerifyReceived() error {
   339  	if u.Status == "started" { // Race condition where server does not record download status. Trust what we asked for and got is correct.
   340  		if u.ReceivedSize != u.ExpectedSize {
   341  			errors.Join(UntrustedSizeRangeRequestSizeExpectedReceived, fmt.Errorf("expected %v bytes %v received", u.ExpectedSize, u.ReceivedSize))
   342  		}
   343  	} else if u.ReceivedSize != u.SentSize {
   344  		return errors.Join(UntrustedSizeRangeRequestSizeSentReceived, fmt.Errorf("expected %v bytes sent %v received", u.SentSize, u.ReceivedSize))
   345  	}
   346  	return nil
   347  }
   348  
   349  func (u UntrustedSizeRangeRequestSize) Log() map[string]interface{} {
   350  	return map[string]interface{}{
   351  		"expected_size":  u.ExpectedSize,
   352  		"sent_size":      u.SentSize,
   353  		"received_size":  u.ReceivedSize,
   354  		"VerifyReceived": u.VerifyReceived(),
   355  		"Mismatch":       u.Mismatch(),
   356  		"Status":         u.Status,
   357  	}
   358  }
   359  
   360  var UntrustedSizeRangeRequestSizeExpectedReceived = fmt.Errorf("received size did not match server expected size")
   361  var UntrustedSizeRangeRequestSizeSentReceived = fmt.Errorf("received size did not match server send size")
   362  
   363  func (u UntrustedSizeRangeRequestSize) Mismatch() error {
   364  	if u.Status == "started" {
   365  		return nil
   366  	}
   367  	if u.ExpectedSize > u.SentSize {
   368  		return UntrustedSizeRangeRequestSizeSentLessThanExpected
   369  	}
   370  	if u.ExpectedSize < u.SentSize {
   371  		return UntrustedSizeRangeRequestSizeSentMoreThanExpected
   372  	}
   373  	return nil
   374  }
   375  
   376  var UntrustedSizeRangeRequestSizeSentMoreThanExpected = fmt.Errorf("server send more than expected")
   377  
   378  var UntrustedSizeRangeRequestSizeSentLessThanExpected = fmt.Errorf("server send less than expected")
   379  
   380  func (r ReaderCloserDownloadStatus) Close() error {
   381  	if r.ReadCloser == nil {
   382  		return nil
   383  	}
   384  	err := r.ReadCloser.Close()
   385  	defer func() { r.ReadCloser = nil }()
   386  	if err != nil {
   387  		return err
   388  	}
   389  
   390  	if r.file.downloadRequestId == "" {
   391  		return nil
   392  	}
   393  
   394  	info, err := r.file.Info()
   395  	if err != nil {
   396  		return err
   397  	}
   398  
   399  	if untrustedInfo, ok := info.(UntrustedSize); ok && (untrustedInfo.UntrustedSize() || untrustedInfo.SizeTrust() == NullSizeTrust) {
   400  		r.file.fileMutex.Lock()
   401  
   402  		status, err := (&Client{Config: r.file.Config}).DownloadRequestStatus(r.file.DownloadUri, r.file.downloadRequestId, files_sdk.WithContext(r.file.Context))
   403  		r.file.fileMutex.Unlock()
   404  		if err != nil {
   405  			return err
   406  		}
   407  		if !status.IsNil() && (status.Data.Status == "failed" || status.Data.Status == "error") {
   408  			return status
   409  		}
   410  		r.UntrustedSizeRangeRequestSize = UntrustedSizeRangeRequestSize{
   411  			r.expectedSize,
   412  			status.Data.BytesTransferred,
   413  			int64(r.ReadWrapper.read),
   414  			status.Data.Status,
   415  		}
   416  
   417  		if err := r.UntrustedSizeRangeRequestSize.VerifyReceived(); err != nil {
   418  			r.file.Config.LogPath(info.Name(), r.UntrustedSizeRangeRequestSize.Log())
   419  			return err
   420  		}
   421  
   422  		// The true size can only be known after the server determines that the full file has been sent without any errors.
   423  		if r.rangeRequest {
   424  			if err := r.UntrustedSizeRangeRequestSize.Mismatch(); err != nil {
   425  				r.file.Config.LogPath(info.Name(), r.UntrustedSizeRangeRequestSize.Log())
   426  				return err
   427  			}
   428  
   429  			if r.file.SizeTrust == UntrustedSizeValue {
   430  				r.file.serverBytesSent += status.Data.BytesTransferred
   431  			}
   432  		} else {
   433  			r.file.SizeTrust = TrustedSizeValue
   434  			r.file.Size = status.Data.BytesTransferred
   435  		}
   436  
   437  		if dataBytes, err := json.Marshal(status.Data); err == nil {
   438  			dataMap := make(map[string]interface{})
   439  			if err = json.Unmarshal(dataBytes, &dataMap); err == nil {
   440  				r.file.Config.LogPath(info.Name(), lo.Assign(dataMap, map[string]interface{}{"message": "download request server status"}))
   441  			}
   442  		}
   443  	}
   444  	return nil
   445  }
   446  
   447  func (f *File) ReadAt(p []byte, off int64) (n int, err error) {
   448  	err = f.downloadURI()
   449  	if err != nil {
   450  		return
   451  	}
   452  	headers := &http.Header{}
   453  	headers.Set("Range", fmt.Sprintf("bytes=%v-%v", off, int64(len(p))+off-1))
   454  	_, err = (&Client{Config: f.Config}).Download(
   455  		files_sdk.FileDownloadParams{
   456  			File: *f.File,
   457  		},
   458  		files_sdk.WithContext(f.Context),
   459  		files_sdk.RequestHeadersOption(headers),
   460  		files_sdk.ResponseOption(func(response *http.Response) error {
   461  			if err := lib.ResponseErrors(response, lib.IsStatus(http.StatusForbidden), lib.NotStatus(http.StatusPartialContent), files_sdk.APIError()); err != nil {
   462  				return &goFs.PathError{Path: f.File.Path, Err: err, Op: "ReadAt"}
   463  			}
   464  			n, err = io.ReadFull(response.Body, p)
   465  			if err != nil && err != io.EOF {
   466  				return err
   467  			}
   468  			if int64(len(p)) >= response.ContentLength && int64(n) != response.ContentLength {
   469  				return &goFs.PathError{Path: f.File.Path, Err: fmt.Errorf("content-length did not match body"), Op: "ReadAt"}
   470  			}
   471  			return nil
   472  		}),
   473  	)
   474  
   475  	if downloadRequestExpired(err) {
   476  		f.Config.LogPath(f.File.Path, map[string]interface{}{"message": "downloadRequestExpired", "error": err})
   477  		f.File.DownloadUri = "" // force a new query
   478  		err = f.downloadURI()
   479  		if err != nil {
   480  			return n, err
   481  		}
   482  
   483  		return f.ReadAt(p, off)
   484  	}
   485  
   486  	return n, err
   487  }
   488  
   489  func downloadRequestExpired(err error) bool {
   490  	if err == nil {
   491  		return false
   492  	}
   493  	responseErr, ok := errors.Unwrap(err).(lib.ResponseError)
   494  	return ok && responseErr.StatusCode == http.StatusForbidden
   495  }
   496  
   497  func (f *File) downloadURI() (err error) {
   498  	f.fileMutex.Lock()
   499  	*f.File, err = (&Client{Config: f.Config}).DownloadUri(files_sdk.FileDownloadParams{File: *f.File}, files_sdk.WithContext(f.Context))
   500  	f.fileMutex.Unlock()
   501  	return
   502  }
   503  
   504  func (f *File) Close() error {
   505  	f.fileMutex.Lock()
   506  	f.fileMutex.Unlock()
   507  	defer func() { f.ReadCloser = nil }()
   508  	switch f.ReadCloser.(type) {
   509  	case *ReadWrapper:
   510  		return ReaderCloserDownloadStatus{ReadWrapper: f.ReadCloser.(*ReadWrapper), file: f}.Close()
   511  	default:
   512  		return ReaderCloserDownloadStatus{ReadWrapper: &ReadWrapper{ReadCloser: f.ReadCloser}, file: f}.Close()
   513  	}
   514  }
   515  
   516  func (f *File) WithContext(ctx context.Context) goFs.File {
   517  	newF := *f
   518  	fs := *newF.FS
   519  	newF.FS = fs.WithContext(ctx).(*FS)
   520  	return &newF
   521  }
   522  
   523  func (f *FS) Open(name string) (goFs.File, error) {
   524  	if name == "." {
   525  		name = ""
   526  	}
   527  	result, ok := f.cache.Load(lib.NormalizeForComparison(name))
   528  	if ok {
   529  		file := result.(*File)
   530  		if file.IsDir() {
   531  			return &ReadDirFile{File: file}, nil
   532  		}
   533  		return file, nil
   534  	}
   535  	path := lib.UrlJoinNoEscape(f.Root, name)
   536  	var err error
   537  	var fileInfo files_sdk.File
   538  	if path == "" { // skip call on root path
   539  		fileInfo = files_sdk.File{Type: "directory"}
   540  	} else {
   541  		fileInfo, err = (&Client{Config: f.Config}).Find(files_sdk.FileFindParams{Path: path}, files_sdk.WithContext(f.Context))
   542  		if err != nil {
   543  			return &File{}, &goFs.PathError{Path: path, Err: err, Op: "open"}
   544  		}
   545  	}
   546  
   547  	file := (&File{File: &fileInfo, FS: f}).Init()
   548  	if f.useCache {
   549  		f.cache.Store(lib.NormalizeForComparison(path), file)
   550  	}
   551  	if fileInfo.Type == "directory" {
   552  		return &ReadDirFile{File: file}, nil
   553  	} else {
   554  		return file, nil
   555  	}
   556  }
   557  
   558  type DirEntryError struct {
   559  	DirEntries []goFs.DirEntry
   560  	error
   561  }
   562  
   563  func (f *FS) ReadDir(name string) ([]goFs.DirEntry, error) {
   564  	if name == "." {
   565  		name = ""
   566  	}
   567  	cacheName := lib.NormalizeForComparison(name)
   568  	if f.useCache {
   569  		f.cacheMutex.Lock(cacheName)
   570  		defer f.cacheMutex.Unlock(cacheName)
   571  
   572  		dirs, ok := f.cacheDir.Load(cacheName)
   573  		if ok {
   574  			dirEntryError := dirs.(DirEntryError)
   575  			return dirEntryError.DirEntries, dirEntryError.error
   576  		}
   577  	}
   578  
   579  	dirs, err := ReadDirFile{File: (&File{File: &files_sdk.File{Path: name}, FS: f}).Init()}.ReadDir(0)
   580  	if f.useCache && errors.Is(err, files_sdk.ResponseError{}) {
   581  		f.cacheDir.Store(cacheName, DirEntryError{dirs, err})
   582  	}
   583  	return dirs, err
   584  }
   585  
   586  func (f ReadDirFile) ReadDir(n int) ([]goFs.DirEntry, error) {
   587  	var files []goFs.DirEntry
   588  	if f.Context != nil && f.Context.Err() != nil {
   589  		return files, &goFs.PathError{Path: f.Path, Err: f.Context.Err(), Op: "readdir"}
   590  	}
   591  	folderClient := folder.Client{Config: f.Config}
   592  	it, err := folderClient.ListFor(files_sdk.FolderListForParams{Path: f.Path}, files_sdk.WithContext(f.Context))
   593  	if err != nil {
   594  		return files, &goFs.PathError{Path: f.Path, Err: err, Op: "readdir"}
   595  	}
   596  	if f.count > 0 {
   597  		return files, io.EOF
   598  	}
   599  	for it.Next() && (n <= 0 || n > 0 && n >= f.count) {
   600  		fi := it.File()
   601  		if err != nil {
   602  			return files, &goFs.PathError{Path: f.Path, Err: err, Op: "readdir"}
   603  		}
   604  		parts := strings.Split(fi.Path, "/")
   605  		dir := strings.Join(parts[0:len(parts)-1], "/")
   606  		if lib.NormalizeForComparison(dir) == lib.NormalizeForComparison(f.Path) {
   607  			// There is a bug in the API that it could return a nested file not in the current directory.
   608  			file := (&File{File: &fi, FS: f.FS}).Init()
   609  			if f.useCache {
   610  				f.cache.Store(lib.NormalizeForComparison(fi.Path), file)
   611  			}
   612  			files = append(files, file)
   613  		}
   614  
   615  		f.count += 1
   616  	}
   617  
   618  	if it.Err() != nil {
   619  		return files, &goFs.PathError{Path: f.Path, Err: it.Err(), Op: "readdir"}
   620  	}
   621  	return files, nil
   622  }
   623  
   624  func (f *FS) MkdirAll(dir string, _ goFs.FileMode) error {
   625  	var parentPath string
   626  	for _, dirPath := range strings.Split(dir, "/") {
   627  		if dirPath == "" {
   628  			break
   629  		}
   630  		folderClient := folder.Client{Config: f.Config}
   631  		_, err := folderClient.Create(files_sdk.FolderCreateParams{Path: lib.UrlJoinNoEscape(parentPath, dirPath)}, files_sdk.WithContext(f.Context))
   632  		rErr, ok := err.(files_sdk.ResponseError)
   633  		if err != nil && ok && rErr.Type != "processing-failure/destination-exists" {
   634  			return err
   635  		}
   636  
   637  		parentPath = lib.UrlJoinNoEscape(parentPath, dirPath)
   638  	}
   639  	return nil
   640  }
   641  
   642  func (f *FS) PathSeparator() string {
   643  	return "/"
   644  }
   645  
   646  var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
   647  
   648  func randSeq(n int) string {
   649  	b := make([]rune, n)
   650  	for i := range b {
   651  		b[i] = letters[rand.Intn(len(letters))]
   652  	}
   653  	return string(b)
   654  }
   655  
   656  func (f *FS) MkdirTemp(dir, pattern string) (string, error) {
   657  	if dir == "" {
   658  		dir = filepath.Join(f.TempDir(), randSeq(10))
   659  	}
   660  	path := f.PathJoin(dir, pattern)
   661  	return path, f.MkdirAll(path, 0750)
   662  }
   663  
   664  type WritableFile struct {
   665  	*Client
   666  	*FS
   667  	path string
   668  	*bytes.Buffer
   669  }
   670  
   671  func (w WritableFile) init() WritableFile {
   672  	w.Buffer = bytes.NewBuffer([]byte{})
   673  	return w
   674  }
   675  
   676  func (w WritableFile) Write(p []byte) (int, error) {
   677  	return w.Buffer.Write(p)
   678  }
   679  
   680  func (w WritableFile) Close() (err error) {
   681  	return w.Client.Upload(
   682  		UploadWithContext(w.Context),
   683  		UploadWithReader(bytes.NewReader(w.Buffer.Bytes())),
   684  		UploadWithDestinationPath(w.path),
   685  		UploadWithSize(int64(w.Buffer.Len())),
   686  	)
   687  }
   688  
   689  // Create Not for performant use cases.
   690  func (f *FS) Create(path string) (io.WriteCloser, error) {
   691  	return WritableFile{FS: f, Client: &Client{Config: f.Config}, path: path}.init(), nil
   692  }
   693  
   694  func (f *FS) RemoveAll(path string) error {
   695  	return (&Client{Config: f.Config}).Delete(files_sdk.FileDeleteParams{Path: path, Recursive: lib.Bool(true)}, files_sdk.WithContext(f.Context))
   696  }
   697  
   698  func (f *FS) Remove(path string) error {
   699  	return (&Client{Config: f.Config}).Delete(files_sdk.FileDeleteParams{Path: path}, files_sdk.WithContext(f.Context))
   700  }
   701  
   702  func (f *FS) PathJoin(s ...string) string {
   703  	return lib.UrlJoinNoEscape(s...)
   704  }
   705  
   706  func (f *FS) RelPath(parent, child string) (string, error) {
   707  	path := strings.Replace(child, parent, "", 1)
   708  	if path == "" {
   709  		return ".", nil
   710  	}
   711  	path = strings.TrimSuffix(path, f.PathSeparator())
   712  	path = strings.TrimPrefix(path, f.PathSeparator())
   713  	return path, nil
   714  }
   715  
   716  func (f *FS) SplitPath(path string) (string, string) {
   717  	if path == "" {
   718  		return "", ""
   719  	}
   720  
   721  	parts := strings.Split(path, f.PathSeparator())
   722  
   723  	return f.PathJoin(parts[:int(math.Min(float64(len(parts)-2), float64(len(parts))))]...), parts[len(parts)-1]
   724  }
   725  
   726  func (f *FS) TempDir() string {
   727  	return "tmp"
   728  }