github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/client-fs.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  	"path"
    27  	"path/filepath"
    28  	"runtime"
    29  	"sort"
    30  	"strconv"
    31  	"strings"
    32  	"syscall"
    33  	"time"
    34  
    35  	"github.com/pkg/xattr"
    36  	"github.com/rjeczalik/notify"
    37  
    38  	xfilepath "github.com/minio/filepath"
    39  	"github.com/minio/mc/pkg/disk"
    40  	"github.com/minio/mc/pkg/hookreader"
    41  	"github.com/minio/mc/pkg/probe"
    42  	"github.com/minio/minio-go/v7"
    43  	"github.com/minio/minio-go/v7/pkg/encrypt"
    44  	"github.com/minio/minio-go/v7/pkg/lifecycle"
    45  	"github.com/minio/minio-go/v7/pkg/notification"
    46  	"github.com/minio/minio-go/v7/pkg/replication"
    47  	"github.com/minio/pkg/v2/console"
    48  )
    49  
    50  // filesystem client
    51  type fsClient struct {
    52  	PathURL *ClientURL
    53  }
    54  
    55  const (
    56  	partSuffix       = ".part.minio"
    57  	slashSeperator   = "/"
    58  	metadataKey      = "X-Amz-Meta-Mc-Attrs"
    59  	metadataKeyS3Cmd = "X-Amz-Meta-S3cmd-Attrs"
    60  )
    61  
    62  // GOOS specific ignore list.
    63  var ignoreFiles = map[string][]string{
    64  	"darwin":  {"*.DS_Store"},
    65  	"default": {"lost+found"},
    66  }
    67  
    68  // fsNew - instantiate a new fs
    69  func fsNew(path string) (Client, *probe.Error) {
    70  	if strings.TrimSpace(path) == "" {
    71  		return nil, probe.NewError(EmptyPath{})
    72  	}
    73  	absPath, e := filepath.Abs(path)
    74  	if e != nil {
    75  		return nil, probe.NewError(e)
    76  	}
    77  	// filepath.Abs removes the trailing slash in a path
    78  	// but we still need it because fsClient.List() does not
    79  	// traverse a directory without a trailing slash in the name
    80  	if path[len(path)-1] == filepath.Separator {
    81  		absPath += string(filepath.Separator)
    82  	}
    83  	return &fsClient{
    84  		PathURL: newClientURL(normalizePath(absPath)),
    85  	}, nil
    86  }
    87  
    88  //lint:ignore U1000 Used on some platforms.
    89  func isNotSupported(e error) bool {
    90  	if e == nil {
    91  		return false
    92  	}
    93  	errno := e.(*xattr.Error)
    94  	if errno == nil {
    95  		return false
    96  	}
    97  
    98  	// check if filesystem supports extended attributes
    99  	return errno.Err == syscall.ENOTSUP || errno.Err == syscall.EOPNOTSUPP
   100  }
   101  
   102  // isIgnoredFile returns true if 'filename' is on the exclude list.
   103  func isIgnoredFile(filename string) bool {
   104  	matchFile := filepath.Base(filename)
   105  
   106  	// OS specific ignore list.
   107  	for _, ignoredFile := range ignoreFiles[runtime.GOOS] {
   108  		matched, e := filepath.Match(ignoredFile, matchFile)
   109  		if e != nil {
   110  			panic(e)
   111  		}
   112  		if matched {
   113  			return true
   114  		}
   115  	}
   116  
   117  	// Default ignore list for all OSes.
   118  	for _, ignoredFile := range ignoreFiles["default"] {
   119  		matched, e := filepath.Match(ignoredFile, matchFile)
   120  		if e != nil {
   121  			panic(e)
   122  		}
   123  		if matched {
   124  			return true
   125  		}
   126  	}
   127  
   128  	return false
   129  }
   130  
   131  // URL get url.
   132  func (f *fsClient) GetURL() ClientURL {
   133  	return *f.PathURL
   134  }
   135  
   136  // Select replies a stream of query results.
   137  func (f *fsClient) Select(_ context.Context, _ string, _ encrypt.ServerSide, _ SelectObjectOpts) (io.ReadCloser, *probe.Error) {
   138  	return nil, probe.NewError(APINotImplemented{
   139  		API:     "Select",
   140  		APIType: "filesystem",
   141  	})
   142  }
   143  
   144  // Watches for all fs events on an input path.
   145  func (f *fsClient) Watch(_ context.Context, options WatchOptions) (*WatchObject, *probe.Error) {
   146  	eventChan := make(chan []EventInfo)
   147  	errorChan := make(chan *probe.Error)
   148  	doneChan := make(chan struct{})
   149  	// Make the channel buffered to ensure no event is dropped. Notify will drop
   150  	// an event if the receiver is not able to keep up the sending pace.
   151  	in, out := PipeChan(1000)
   152  
   153  	var fsEvents []notify.Event
   154  	for _, event := range options.Events {
   155  		switch event {
   156  		case "put":
   157  			fsEvents = append(fsEvents, EventTypePut...)
   158  		case "delete":
   159  			fsEvents = append(fsEvents, EventTypeDelete...)
   160  		case "get":
   161  			fsEvents = append(fsEvents, EventTypeGet...)
   162  		default:
   163  			// Event type not supported by FS client, such as
   164  			// bucket creation or deletion, ignore it.
   165  		}
   166  	}
   167  
   168  	// Set up a watchpoint listening for events within a directory tree rooted
   169  	// at current working directory. Dispatch remove events to c.
   170  	recursivePath := f.PathURL.Path
   171  	if options.Recursive {
   172  		recursivePath = f.PathURL.Path + "..."
   173  	}
   174  	if e := notify.Watch(recursivePath, in, fsEvents...); e != nil {
   175  		return nil, probe.NewError(e)
   176  	}
   177  
   178  	// wait for doneChan to close the watcher, eventChan and errorChan
   179  	go func() {
   180  		<-doneChan
   181  
   182  		close(eventChan)
   183  		close(errorChan)
   184  		notify.Stop(in)
   185  		// At this point, notify is guaranteed to not write
   186  		// in 'in' channel so we can close it.
   187  		close(in)
   188  	}()
   189  
   190  	timeFormatFS := "2006-01-02T15:04:05.000Z"
   191  
   192  	// Get fsnotify notifications for events and errors, and sent them
   193  	// using eventChan and errorChan
   194  	go func() {
   195  		for event := range out {
   196  			if isIgnoredFile(event.Path()) {
   197  				continue
   198  			}
   199  			var i os.FileInfo
   200  			if IsPutEvent(event.Event()) {
   201  				// Look for any writes, send a response to indicate a full copy.
   202  				var e error
   203  				i, e = os.Stat(event.Path())
   204  				if e != nil {
   205  					if os.IsNotExist(e) {
   206  						continue
   207  					}
   208  					errorChan <- probe.NewError(e)
   209  					continue
   210  				}
   211  				if i.IsDir() {
   212  					// we want files
   213  					continue
   214  				}
   215  				eventChan <- []EventInfo{{
   216  					Time: UTCNow().Format(timeFormatFS),
   217  					Size: i.Size(),
   218  					Path: event.Path(),
   219  					Type: notification.ObjectCreatedPut,
   220  				}}
   221  			} else if IsDeleteEvent(event.Event()) {
   222  				eventChan <- []EventInfo{{
   223  					Time: UTCNow().Format(timeFormatFS),
   224  					Path: event.Path(),
   225  					Type: notification.ObjectRemovedDelete,
   226  				}}
   227  			} else if IsGetEvent(event.Event()) {
   228  				eventChan <- []EventInfo{{
   229  					Time: UTCNow().Format(timeFormatFS),
   230  					Path: event.Path(),
   231  					Type: notification.ObjectAccessedGet,
   232  				}}
   233  			}
   234  		}
   235  	}()
   236  
   237  	return &WatchObject{
   238  		EventInfoChan: eventChan,
   239  		ErrorChan:     errorChan,
   240  		DoneChan:      doneChan,
   241  	}, nil
   242  }
   243  
   244  func preserveAttributes(fd *os.File, attr map[string]string) *probe.Error {
   245  	if val, ok := attr["mode"]; ok {
   246  		mode, e := strconv.ParseUint(val, 0, 32)
   247  		if e == nil {
   248  			// Attempt to change the file mode.
   249  			if e = fd.Chmod(os.FileMode(mode)); e != nil {
   250  				return probe.NewError(e)
   251  			}
   252  		}
   253  	}
   254  
   255  	var uid, gid int
   256  	var e error
   257  	if val, ok := attr["uid"]; ok {
   258  		uid, e = strconv.Atoi(val)
   259  		if e != nil {
   260  			uid = -1
   261  		}
   262  	}
   263  
   264  	if val, ok := attr["gid"]; ok {
   265  		gid, e = strconv.Atoi(val)
   266  		if e != nil {
   267  			gid = -1
   268  		}
   269  	}
   270  
   271  	// Attempt to change the owner.
   272  	if e = fd.Chown(uid, gid); e != nil {
   273  		return probe.NewError(e)
   274  	}
   275  
   276  	return nil
   277  }
   278  
   279  /// Object operations.
   280  
   281  func (f *fsClient) put(_ context.Context, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (int64, *probe.Error) {
   282  	// ContentType is not handled on purpose.
   283  	// For filesystem this is a redundant information.
   284  
   285  	// Extract dir name.
   286  	objectDir, objectName := filepath.Split(f.PathURL.Path)
   287  
   288  	if objectDir != "" {
   289  		// Create any missing top level directories.
   290  		if e := os.MkdirAll(objectDir, 0o777); e != nil {
   291  			err := f.toClientError(e, f.PathURL.Path)
   292  			return 0, err.Trace(f.PathURL.Path)
   293  		}
   294  
   295  		// Check if object name is empty, it must be an empty directory
   296  		if objectName == "" {
   297  			return 0, nil
   298  		}
   299  	}
   300  
   301  	objectPath := f.PathURL.Path
   302  
   303  	// Write to a temporary file "object.part.minio" before commit.
   304  	objectPartPath := objectPath + partSuffix
   305  
   306  	// We cannot resume this operation, then we
   307  	// should remove any partial download if any.
   308  	defer os.Remove(objectPartPath)
   309  
   310  	tmpFile, e := os.OpenFile(objectPartPath, os.O_CREATE|os.O_WRONLY, 0o666)
   311  	if e != nil {
   312  		err := f.toClientError(e, f.PathURL.Path)
   313  		return 0, err.Trace(f.PathURL.Path)
   314  	}
   315  
   316  	attr := make(map[string]string)
   317  	if _, ok := opts.metadata[metadataKey]; ok && opts.isPreserve {
   318  		attr, e = parseAttribute(opts.metadata)
   319  		if e != nil {
   320  			tmpFile.Close()
   321  			return 0, probe.NewError(e)
   322  		}
   323  		err := preserveAttributes(tmpFile, attr)
   324  		if err != nil {
   325  			console.Println(console.Colorize("Error", fmt.Sprintf("unable to preserve attributes, continuing to copy the content %s\n", err.ToGoError())))
   326  		}
   327  	}
   328  
   329  	totalWritten, e := io.Copy(tmpFile, hookreader.NewHook(reader, progress))
   330  	if e != nil {
   331  		tmpFile.Close()
   332  		return 0, probe.NewError(e)
   333  	}
   334  
   335  	// Close the input reader as well, if possible.
   336  	closer, ok := reader.(io.Closer)
   337  	if ok {
   338  		if e = closer.Close(); e != nil {
   339  			tmpFile.Close()
   340  			return totalWritten, probe.NewError(e)
   341  		}
   342  	}
   343  
   344  	// Close the file before renaming, we need to do this
   345  	// specifically for windows users - windows explicitly
   346  	// disallows renames on Open() fd's by default.
   347  	if e = tmpFile.Close(); e != nil {
   348  		return totalWritten, probe.NewError(e)
   349  	}
   350  
   351  	// Following verification is needed only for input size greater than '0'.
   352  	if size > 0 {
   353  		// Unexpected EOF reached (less data was written than expected).
   354  		if totalWritten < size {
   355  			return totalWritten, probe.NewError(UnexpectedEOF{
   356  				TotalSize:    size,
   357  				TotalWritten: totalWritten,
   358  			})
   359  		}
   360  		// Unexpected ExcessRead (more data was written than expected).
   361  		if totalWritten > size {
   362  			return totalWritten, probe.NewError(UnexpectedExcessRead{
   363  				TotalSize:    size,
   364  				TotalWritten: totalWritten,
   365  			})
   366  		}
   367  	}
   368  
   369  	// Safely completed put. Now commit by renaming to actual filename.
   370  	if e = os.Rename(objectPartPath, objectPath); e != nil {
   371  		err := f.toClientError(e, objectPath)
   372  		return totalWritten, err.Trace(objectPartPath, objectPath)
   373  	}
   374  
   375  	if len(attr) != 0 && opts.isPreserve {
   376  		atime, mtime, err := parseAtimeMtime(attr)
   377  		if err != nil {
   378  			return totalWritten, err.Trace()
   379  		}
   380  		if !atime.IsZero() && !mtime.IsZero() {
   381  			if e := os.Chtimes(objectPath, atime, mtime); e != nil {
   382  				return totalWritten, probe.NewError(e)
   383  			}
   384  		}
   385  	}
   386  
   387  	return totalWritten, nil
   388  }
   389  
   390  // Put - create a new file with metadata.
   391  func (f *fsClient) Put(ctx context.Context, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (int64, *probe.Error) {
   392  	return f.put(ctx, reader, size, progress, opts)
   393  }
   394  
   395  func (f *fsClient) putN(_ context.Context, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (int64, *probe.Error) {
   396  	// ContentType is not handled on purpose.
   397  	// For filesystem this is a redundant information.
   398  
   399  	// Extract dir name.
   400  	objectDir, objectName := filepath.Split(f.PathURL.Path)
   401  
   402  	if objectDir != "" {
   403  		// Create any missing top level directories.
   404  		if e := os.MkdirAll(objectDir, 0o777); e != nil {
   405  			err := f.toClientError(e, f.PathURL.Path)
   406  			return 0, err.Trace(f.PathURL.Path)
   407  		}
   408  
   409  		// Check if object name is empty, it must be an empty directory
   410  		if objectName == "" {
   411  			return 0, nil
   412  		}
   413  	}
   414  
   415  	objectPath := f.PathURL.Path
   416  
   417  	// Write to a temporary file "object.part.minio" before commit.
   418  	objectPartPath := objectPath + partSuffix
   419  
   420  	// We cannot resume this operation, then we
   421  	// should remove any partial download if any.
   422  	defer os.Remove(objectPartPath)
   423  
   424  	tmpFile, e := os.OpenFile(objectPartPath, os.O_CREATE|os.O_WRONLY, 0o666)
   425  	if e != nil {
   426  		err := f.toClientError(e, f.PathURL.Path)
   427  		return 0, err.Trace(f.PathURL.Path)
   428  	}
   429  
   430  	attr := make(map[string]string)
   431  	if _, ok := opts.metadata[metadataKey]; ok && opts.isPreserve {
   432  		attr, e = parseAttribute(opts.metadata)
   433  		if e != nil {
   434  			tmpFile.Close()
   435  			return 0, probe.NewError(e)
   436  		}
   437  		err := preserveAttributes(tmpFile, attr)
   438  		if err != nil {
   439  			console.Println(console.Colorize("Error", fmt.Sprintf("unable to preserve attributes, continuing to copy the content %s\n", err.ToGoError())))
   440  		}
   441  	}
   442  
   443  	totalWritten, e := io.CopyN(tmpFile, hookreader.NewHook(reader, progress), size)
   444  	if e != nil {
   445  		tmpFile.Close()
   446  		return 0, probe.NewError(e)
   447  	}
   448  
   449  	// Close the input reader as well, if possible.
   450  	closer, ok := reader.(io.Closer)
   451  	if ok {
   452  		if e = closer.Close(); e != nil {
   453  			tmpFile.Close()
   454  			return totalWritten, probe.NewError(e)
   455  		}
   456  	}
   457  
   458  	// Close the file before renaming, we need to do this
   459  	// specifically for windows users - windows explicitly
   460  	// disallows renames on Open() fd's by default.
   461  	if e = tmpFile.Close(); e != nil {
   462  		return totalWritten, probe.NewError(e)
   463  	}
   464  
   465  	// Following verification is needed only for input size greater than '0'.
   466  	if size > 0 {
   467  		// Unexpected ExcessRead (more data was written than expected).
   468  		if totalWritten > size {
   469  			return totalWritten, probe.NewError(UnexpectedExcessRead{
   470  				TotalSize:    size,
   471  				TotalWritten: totalWritten,
   472  			})
   473  		}
   474  	}
   475  
   476  	// Safely completed put. Now commit by renaming to actual filename.
   477  	if e = os.Rename(objectPartPath, objectPath); e != nil {
   478  		err := f.toClientError(e, objectPath)
   479  		return totalWritten, err.Trace(objectPartPath, objectPath)
   480  	}
   481  
   482  	if len(attr) != 0 && opts.isPreserve {
   483  		atime, mtime, err := parseAtimeMtime(attr)
   484  		if err != nil {
   485  			return totalWritten, err.Trace()
   486  		}
   487  		if !atime.IsZero() && !mtime.IsZero() {
   488  			if e := os.Chtimes(objectPath, atime, mtime); e != nil {
   489  				return totalWritten, probe.NewError(e)
   490  			}
   491  		}
   492  	}
   493  
   494  	return totalWritten, nil
   495  }
   496  
   497  // PutPart - create a new file with metadata, reading up to N bytes.
   498  func (f *fsClient) PutPart(ctx context.Context, reader io.Reader, size int64, progress io.Reader, opts PutOptions) (int64, *probe.Error) {
   499  	if size < 0 {
   500  		return f.put(ctx, reader, size, progress, opts)
   501  	}
   502  	return f.putN(ctx, reader, size, progress, opts)
   503  }
   504  
   505  // ShareDownload - share download not implemented for filesystem.
   506  func (f *fsClient) ShareDownload(_ context.Context, _ string, _ time.Duration) (string, *probe.Error) {
   507  	return "", probe.NewError(APINotImplemented{
   508  		API:     "ShareDownload",
   509  		APIType: "filesystem",
   510  	})
   511  }
   512  
   513  // ShareUpload - share upload not implemented for filesystem.
   514  func (f *fsClient) ShareUpload(_ context.Context, _ bool, _ time.Duration, _ string) (string, map[string]string, *probe.Error) {
   515  	return "", nil, probe.NewError(APINotImplemented{
   516  		API:     "ShareUpload",
   517  		APIType: "filesystem",
   518  	})
   519  }
   520  
   521  // Copy - copy data from source to destination
   522  func (f *fsClient) Copy(ctx context.Context, source string, opts CopyOptions, progress io.Reader) *probe.Error {
   523  	rc, e := os.Open(source)
   524  	if e != nil {
   525  		err := f.toClientError(e, source)
   526  		return err.Trace(source)
   527  	}
   528  	defer rc.Close()
   529  
   530  	putOpts := PutOptions{
   531  		metadata:   opts.metadata,
   532  		isPreserve: opts.isPreserve,
   533  	}
   534  
   535  	destination := f.PathURL.Path
   536  	if _, err := f.put(ctx, rc, opts.size, progress, putOpts); err != nil {
   537  		return err.Trace(destination, source)
   538  	}
   539  	return nil
   540  }
   541  
   542  // Get returns reader and any additional metadata.
   543  func (f *fsClient) Get(_ context.Context, opts GetOptions) (io.ReadCloser, *ClientContent, *probe.Error) {
   544  	fileData, e := os.Open(f.PathURL.Path)
   545  	if e != nil {
   546  		err := f.toClientError(e, f.PathURL.Path)
   547  		return nil, nil, err.Trace(f.PathURL.Path)
   548  	}
   549  	if opts.RangeStart != 0 {
   550  		_, e := fileData.Seek(opts.RangeStart, io.SeekStart)
   551  		if e != nil {
   552  			err := f.toClientError(e, f.PathURL.Path)
   553  			return nil, nil, err.Trace(f.PathURL.Path)
   554  		}
   555  	}
   556  
   557  	fi, e := fileData.Stat()
   558  	if e != nil {
   559  		return nil, nil, probe.NewError(e)
   560  	}
   561  
   562  	content := &ClientContent{}
   563  	content.URL = *f.PathURL
   564  	content.Size = fi.Size()
   565  	content.Time = fi.ModTime()
   566  	content.Type = fi.Mode()
   567  	content.Metadata = map[string]string{
   568  		"Content-Type": guessURLContentType(f.PathURL.Path),
   569  	}
   570  
   571  	path := f.PathURL.String()
   572  	// Populates meta data with file system attribute only in case of
   573  	// when preserve flag is passed.
   574  	if opts.Preserve {
   575  		fileAttr, err := disk.GetFileSystemAttrs(path)
   576  		if err != nil {
   577  			return nil, content, nil
   578  		}
   579  		metaData, pErr := getAllXattrs(path)
   580  		if pErr != nil {
   581  			return nil, content, nil
   582  		}
   583  		for k, v := range metaData {
   584  			content.Metadata[k] = v
   585  		}
   586  		content.Metadata[metadataKey] = fileAttr
   587  	}
   588  
   589  	return fileData, content, nil
   590  }
   591  
   592  // Check if the given error corresponds to ENOTEMPTY for unix
   593  // and ERROR_DIR_NOT_EMPTY for windows (directory not empty).
   594  func isSysErrNotEmpty(err error) bool {
   595  	if err == syscall.ENOTEMPTY {
   596  		return true
   597  	}
   598  	if pathErr, ok := err.(*os.PathError); ok {
   599  		if runtime.GOOS == "windows" {
   600  			if errno, _ok := pathErr.Err.(syscall.Errno); _ok && errno == 0x91 {
   601  				// ERROR_DIR_NOT_EMPTY
   602  				return true
   603  			}
   604  		}
   605  		if pathErr.Err == syscall.ENOTEMPTY {
   606  			return true
   607  		}
   608  	}
   609  	return false
   610  }
   611  
   612  // deleteFile deletes a file path if its empty. If it's successfully deleted,
   613  // it will recursively delete empty parent directories
   614  // until it finds one with files in it. Returns nil for a non-empty directory.
   615  func deleteFile(basePath, deletePath string) error {
   616  	// Attempt to remove path.
   617  	if e := os.Remove(deletePath); e != nil {
   618  		if isSysErrNotEmpty(e) {
   619  			return nil
   620  		}
   621  		if os.IsNotExist(e) {
   622  			return nil
   623  		}
   624  		return e
   625  	}
   626  
   627  	// Trailing slash is removed when found to ensure
   628  	// slashpath.Dir() to work as intended.
   629  	parentPath := strings.TrimSuffix(deletePath, slashSeperator)
   630  	parentPath = path.Dir(parentPath)
   631  
   632  	if !strings.HasPrefix(parentPath, basePath) {
   633  		// If parentPath jumps out of the original basePath,
   634  		// make sure to cancel such calls, we don't want
   635  		// to be deleting more than we should.
   636  		return nil
   637  	}
   638  
   639  	if parentPath != "." {
   640  		return deleteFile(basePath, parentPath)
   641  	}
   642  
   643  	return nil
   644  }
   645  
   646  // Remove - remove entry read from clientContent channel.
   647  func (f *fsClient) Remove(_ context.Context, isIncomplete, _, _, _ bool, contentCh <-chan *ClientContent) <-chan RemoveResult {
   648  	resultCh := make(chan RemoveResult)
   649  
   650  	// Goroutine reads from contentCh and removes the entry in content.
   651  	go func() {
   652  		defer close(resultCh)
   653  
   654  		for content := range contentCh {
   655  			if content.Err != nil {
   656  				resultCh <- RemoveResult{
   657  					Err: content.Err,
   658  				}
   659  				continue
   660  			}
   661  			name := content.URL.Path
   662  			// Add partSuffix for incomplete uploads.
   663  			if isIncomplete {
   664  				name += partSuffix
   665  			}
   666  			e := deleteFile(f.PathURL.Path, name)
   667  			if e == nil {
   668  				res := RemoveResult{}
   669  				res.ObjectName = content.URL.Path
   670  				resultCh <- res
   671  				continue
   672  			}
   673  			if os.IsNotExist(e) {
   674  				// ignore if path already removed.
   675  				continue
   676  			}
   677  			if os.IsPermission(e) {
   678  				// Ignore permission error.
   679  				resultCh <- RemoveResult{
   680  					Err: probe.NewError(PathInsufficientPermission{
   681  						Path: content.URL.Path,
   682  					}),
   683  				}
   684  			} else {
   685  				resultCh <- RemoveResult{
   686  					Err: probe.NewError(e),
   687  				}
   688  				return
   689  			}
   690  		}
   691  	}()
   692  
   693  	return resultCh
   694  }
   695  
   696  // ListBuckets returns the list of directories inside a base path
   697  func (f *fsClient) ListBuckets(_ context.Context) ([]*ClientContent, *probe.Error) {
   698  	// save pathURL and file path for further usage.
   699  	pathURL := *f.PathURL
   700  	path := pathURL.Path
   701  
   702  	st, e := os.Stat(path)
   703  	if e != nil {
   704  		if os.IsNotExist(e) {
   705  			return nil, probe.NewError(PathNotFound{Path: path})
   706  		}
   707  		return nil, probe.NewError(e)
   708  	}
   709  
   710  	if !st.Mode().IsDir() {
   711  		return nil, probe.NewError(PathNotADirectory{Path: path})
   712  	}
   713  
   714  	// List directories (buckets) inside path in a sorted way
   715  	files, e := readDir(path)
   716  	if e != nil {
   717  		return nil, probe.NewError(e)
   718  	}
   719  
   720  	bucketsList := make([]*ClientContent, 0, len(files))
   721  
   722  	for _, file := range files {
   723  		fi := file
   724  		if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
   725  			fp := filepath.Join(path, fi.Name())
   726  			fi, e = os.Stat(fp)
   727  			if e != nil {
   728  				// Ignore all errors on symlinks
   729  				continue
   730  			}
   731  		}
   732  
   733  		if isIgnoredFile(fi.Name()) {
   734  			continue
   735  		}
   736  
   737  		if !fi.Mode().IsDir() {
   738  			continue
   739  		}
   740  
   741  		pathURL = *f.PathURL
   742  		pathURL.Path = filepath.Join(pathURL.Path, fi.Name())
   743  
   744  		bucketsList = append(bucketsList, &ClientContent{
   745  			URL:  pathURL,
   746  			Time: fi.ModTime(),
   747  			Type: fi.Mode(),
   748  			Err:  nil,
   749  		})
   750  	}
   751  
   752  	return bucketsList, nil
   753  }
   754  
   755  // List - list files and folders.
   756  func (f *fsClient) List(_ context.Context, opts ListOptions) <-chan *ClientContent {
   757  	contentCh := make(chan *ClientContent, 1)
   758  	filteredCh := make(chan *ClientContent, 1)
   759  	if opts.ListZip {
   760  		contentCh <- &ClientContent{
   761  			Err: probe.NewError(errors.New("zip listing not supported for local files")),
   762  		}
   763  		close(filteredCh)
   764  		return filteredCh
   765  	}
   766  
   767  	if opts.Recursive {
   768  		if opts.ShowDir == DirNone {
   769  			go f.listRecursiveInRoutine(contentCh)
   770  		} else {
   771  			go f.listDirOpt(contentCh, opts.Incomplete, opts.WithMetadata, opts.ShowDir)
   772  		}
   773  	} else {
   774  		go f.listInRoutine(contentCh)
   775  	}
   776  
   777  	// This function filters entries from any  listing go routine
   778  	// created previously. If isIncomplete is activated, we will
   779  	// only show partly uploaded files,
   780  	go func() {
   781  		for c := range contentCh {
   782  			if opts.Incomplete {
   783  				if !strings.HasSuffix(c.URL.Path, partSuffix) {
   784  					continue
   785  				}
   786  				// Strip part suffix
   787  				c.URL.Path = strings.Split(c.URL.Path, partSuffix)[0]
   788  			} else {
   789  				if strings.HasSuffix(c.URL.Path, partSuffix) {
   790  					continue
   791  				}
   792  			}
   793  			// Send to filtered channel
   794  			filteredCh <- c
   795  		}
   796  		defer close(filteredCh)
   797  	}()
   798  
   799  	return filteredCh
   800  }
   801  
   802  // byDirName implements sort.Interface.
   803  type byDirName []os.FileInfo
   804  
   805  func (f byDirName) Len() int { return len(f) }
   806  func (f byDirName) Less(i, j int) bool {
   807  	// For directory add an ending separator fortrue lexical
   808  	// order.
   809  	if f[i].Mode().IsDir() {
   810  		return f[i].Name()+string(filepath.Separator) < f[j].Name()
   811  	}
   812  	// For directory add an ending separator for true lexical
   813  	// order.
   814  	if f[j].Mode().IsDir() {
   815  		return f[i].Name() < f[j].Name()+string(filepath.Separator)
   816  	}
   817  	return f[i].Name() < f[j].Name()
   818  }
   819  func (f byDirName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
   820  
   821  // readDir reads the directory named by dirname and returns
   822  // a list of sorted directory entries.
   823  func readDir(dirname string) ([]os.FileInfo, error) {
   824  	f, e := os.Open(dirname)
   825  	if e != nil {
   826  		return nil, e
   827  	}
   828  	list, e := f.Readdir(-1)
   829  	if e != nil {
   830  		return nil, e
   831  	}
   832  	defer f.Close()
   833  	sort.Sort(byDirName(list))
   834  	return list, nil
   835  }
   836  
   837  // listPrefixes - list all files for any given prefix.
   838  func (f *fsClient) listPrefixes(prefix string, contentCh chan<- *ClientContent) {
   839  	dirName := filepath.Dir(prefix)
   840  	files, e := readDir(dirName)
   841  	if e != nil {
   842  		err := f.toClientError(e, dirName)
   843  		contentCh <- &ClientContent{
   844  			Err: err.Trace(dirName),
   845  		}
   846  		return
   847  	}
   848  	for _, fi := range files {
   849  		// Skip ignored files.
   850  		if isIgnoredFile(fi.Name()) {
   851  			continue
   852  		}
   853  
   854  		file := filepath.Join(dirName, fi.Name())
   855  		if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
   856  			st, e := os.Stat(file)
   857  			if e != nil {
   858  				// Ignore any errors on symlink
   859  				continue
   860  			}
   861  			if strings.HasPrefix(file, prefix) {
   862  				contentCh <- &ClientContent{
   863  					URL:  *newClientURL(file),
   864  					Time: st.ModTime(),
   865  					Size: st.Size(),
   866  					Type: st.Mode(),
   867  					Err:  nil,
   868  				}
   869  				continue
   870  			}
   871  		}
   872  		if strings.HasPrefix(file, prefix) {
   873  			contentCh <- &ClientContent{
   874  				URL:  *newClientURL(file),
   875  				Time: fi.ModTime(),
   876  				Size: fi.Size(),
   877  				Type: fi.Mode(),
   878  				Err:  nil,
   879  			}
   880  		}
   881  	}
   882  }
   883  
   884  func (f *fsClient) listInRoutine(contentCh chan<- *ClientContent) {
   885  	// close the channel when the function returns.
   886  	defer close(contentCh)
   887  
   888  	// save pathURL and file path for further usage.
   889  	pathURL := *f.PathURL
   890  	fpath := pathURL.Path
   891  
   892  	fst, err := f.fsStat(false)
   893  	if err != nil {
   894  		if _, ok := err.ToGoError().(PathNotFound); ok {
   895  			// If file does not exist treat it like a prefix and list all prefixes if any.
   896  			prefix := fpath
   897  			f.listPrefixes(prefix, contentCh)
   898  			return
   899  		}
   900  		// For all other errors we return genuine error back to the caller.
   901  		contentCh <- &ClientContent{Err: err.Trace(fpath)}
   902  		return
   903  	}
   904  
   905  	// Now if the file exists and doesn't end with a separator ('/') do not traverse it.
   906  	// If the directory doesn't end with a separator, do not traverse it.
   907  	if !strings.HasSuffix(fpath, string(pathURL.Separator)) && fst.Mode().IsDir() && fpath != "." {
   908  		f.listPrefixes(fpath, contentCh)
   909  		return
   910  	}
   911  
   912  	// If we really see the directory.
   913  	switch fst.Mode().IsDir() {
   914  	case true:
   915  		files, e := readDir(fpath)
   916  		if e != nil {
   917  			contentCh <- &ClientContent{Err: probe.NewError(e)}
   918  			return
   919  		}
   920  		for _, file := range files {
   921  			fi := file
   922  			if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
   923  				fp := filepath.Join(fpath, fi.Name())
   924  				fi, e = os.Stat(fp)
   925  				if e != nil {
   926  					// Ignore all errors on symlinks
   927  					continue
   928  				}
   929  			}
   930  			if fi.Mode().IsRegular() || fi.Mode().IsDir() {
   931  				pathURL = *f.PathURL
   932  				pathURL.Path = filepath.Join(pathURL.Path, fi.Name())
   933  
   934  				// Skip ignored files.
   935  				if isIgnoredFile(fi.Name()) {
   936  					continue
   937  				}
   938  
   939  				contentCh <- &ClientContent{
   940  					URL:  pathURL,
   941  					Time: fi.ModTime(),
   942  					Size: fi.Size(),
   943  					Type: fi.Mode(),
   944  					Err:  nil,
   945  				}
   946  			}
   947  		}
   948  	default:
   949  		contentCh <- &ClientContent{
   950  			URL:  pathURL,
   951  			Time: fst.ModTime(),
   952  			Size: fst.Size(),
   953  			Type: fst.Mode(),
   954  			Err:  nil,
   955  		}
   956  	}
   957  }
   958  
   959  // List files recursively using non-recursive mode.
   960  func (f *fsClient) listDirOpt(contentCh chan *ClientContent, isIncomplete, _ bool, dirOpt DirOpt) {
   961  	defer close(contentCh)
   962  
   963  	// Trim trailing / or \.
   964  	currentPath := f.PathURL.Path
   965  	currentPath = strings.TrimSuffix(currentPath, "/")
   966  	if runtime.GOOS == "windows" {
   967  		currentPath = strings.TrimSuffix(currentPath, `\`)
   968  	}
   969  
   970  	// Closure function reads currentPath and sends to contentCh. If a directory is found, it lists the directory content recursively.
   971  	var listDir func(currentPath string) bool
   972  	listDir = func(currentPath string) (isStop bool) {
   973  		files, e := readDir(currentPath)
   974  		if e != nil {
   975  			if os.IsNotExist(e) {
   976  				contentCh <- &ClientContent{
   977  					Err: probe.NewError(PathNotFound{
   978  						Path: currentPath,
   979  					}),
   980  				}
   981  				return false
   982  			}
   983  			if os.IsPermission(e) {
   984  				contentCh <- &ClientContent{
   985  					Err: probe.NewError(PathInsufficientPermission{
   986  						Path: currentPath,
   987  					}),
   988  				}
   989  				return false
   990  			}
   991  
   992  			contentCh <- &ClientContent{Err: probe.NewError(e)}
   993  			return true
   994  		}
   995  
   996  		for _, file := range files {
   997  			name := filepath.Join(currentPath, file.Name())
   998  			content := ClientContent{
   999  				URL:  *newClientURL(name),
  1000  				Time: file.ModTime(),
  1001  				Size: file.Size(),
  1002  				Type: file.Mode(),
  1003  				Err:  nil,
  1004  			}
  1005  			if file.Mode().IsDir() {
  1006  				if dirOpt == DirFirst && !isIncomplete {
  1007  					contentCh <- &content
  1008  				}
  1009  				if listDir(filepath.Join(name)) {
  1010  					return true
  1011  				}
  1012  				if dirOpt == DirLast && !isIncomplete {
  1013  					contentCh <- &content
  1014  				}
  1015  
  1016  				continue
  1017  			}
  1018  
  1019  			contentCh <- &content
  1020  		}
  1021  
  1022  		return false
  1023  	}
  1024  
  1025  	// listDir() does not send currentPath to contentCh.  We send it here depending on dirOpt.
  1026  
  1027  	if dirOpt == DirFirst && !isIncomplete {
  1028  		contentCh <- &ClientContent{URL: *newClientURL(currentPath), Type: os.ModeDir}
  1029  	}
  1030  
  1031  	listDir(currentPath)
  1032  
  1033  	if dirOpt == DirLast && !isIncomplete {
  1034  		contentCh <- &ClientContent{URL: *newClientURL(currentPath), Type: os.ModeDir}
  1035  	}
  1036  }
  1037  
  1038  func (f *fsClient) listRecursiveInRoutine(contentCh chan *ClientContent) {
  1039  	// close channels upon return.
  1040  	defer close(contentCh)
  1041  	var dirName string
  1042  	var filePrefix string
  1043  	pathURL := *f.PathURL
  1044  	if runtime.GOOS == "windows" {
  1045  		pathURL.Path = filepath.FromSlash(pathURL.Path)
  1046  		pathURL.Separator = os.PathSeparator
  1047  	}
  1048  	visitFS := func(fp string, fi os.FileInfo, e error) error {
  1049  		// If file path ends with filepath.Separator and equals to root path, skip it.
  1050  		if strings.HasSuffix(fp, string(pathURL.Separator)) {
  1051  			if fp == dirName {
  1052  				return nil
  1053  			}
  1054  		}
  1055  		// We would never need to print system root path '/'.
  1056  		if fp == "/" {
  1057  			return nil
  1058  		}
  1059  
  1060  		// Ignore files from ignore list.
  1061  		if isIgnoredFile(fi.Name()) {
  1062  			return nil
  1063  		}
  1064  
  1065  		/// In following situations we need to handle listing properly.
  1066  		// - When filepath is '/usr' and prefix is '/usr/bi'
  1067  		// - When filepath is '/usr/bin/subdir' and prefix is '/usr/bi'
  1068  		// - Do not check filePrefix if its '.'
  1069  		if filePrefix != "." {
  1070  			if !strings.HasPrefix(fp, filePrefix) &&
  1071  				!strings.HasPrefix(filePrefix, fp) {
  1072  				if e == nil {
  1073  					if fi.IsDir() {
  1074  						return xfilepath.ErrSkipDir
  1075  					}
  1076  					return nil
  1077  				}
  1078  			}
  1079  			// - Skip when fp is /usr and prefix is '/usr/bi'
  1080  			// - Do not check filePrefix if its '.'
  1081  			if filePrefix != "." {
  1082  				if !strings.HasPrefix(fp, filePrefix) {
  1083  					return nil
  1084  				}
  1085  			}
  1086  		}
  1087  		if e != nil {
  1088  			// If operation is not permitted, we throw quickly back.
  1089  			if strings.Contains(e.Error(), "operation not permitted") {
  1090  				contentCh <- &ClientContent{
  1091  					Err: probe.NewError(e),
  1092  				}
  1093  				return nil
  1094  			}
  1095  			if os.IsPermission(e) {
  1096  				contentCh <- &ClientContent{
  1097  					Err: probe.NewError(PathInsufficientPermission{Path: fp}),
  1098  				}
  1099  				return nil
  1100  			}
  1101  			return e
  1102  		}
  1103  		if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
  1104  			fi, e = os.Stat(fp)
  1105  			if e != nil {
  1106  				// Ignore any errors for symlink
  1107  				return nil
  1108  			}
  1109  		}
  1110  		if fi.Mode().IsRegular() {
  1111  			contentCh <- &ClientContent{
  1112  				URL:  *newClientURL(fp),
  1113  				Time: fi.ModTime(),
  1114  				Size: fi.Size(),
  1115  				Type: fi.Mode(),
  1116  				Err:  nil,
  1117  			}
  1118  		}
  1119  		return nil
  1120  	}
  1121  	// No prefix to be filtered by default.
  1122  	filePrefix = ""
  1123  	// if f.Path ends with filepath.Separator - assuming it to be a directory and moving on.
  1124  	if strings.HasSuffix(pathURL.Path, string(pathURL.Separator)) {
  1125  		dirName = pathURL.Path
  1126  	} else {
  1127  		// if not a directory, take base path to navigate through WalkFunc.
  1128  		dirName = filepath.Dir(pathURL.Path)
  1129  		if !strings.HasSuffix(dirName, string(pathURL.Separator)) {
  1130  			// basepath truncates the filepath.Separator,
  1131  			// add it diligently useful for trimming file path inside WalkFunc
  1132  			dirName = dirName + string(pathURL.Separator)
  1133  		}
  1134  		// filePrefix is kept for filtering incoming contents through WalkFunc.
  1135  		filePrefix = pathURL.Path
  1136  	}
  1137  	// walks invokes our custom function.
  1138  	e := xfilepath.Walk(dirName, visitFS)
  1139  	if e != nil {
  1140  		contentCh <- &ClientContent{
  1141  			Err: probe.NewError(e),
  1142  		}
  1143  	}
  1144  }
  1145  
  1146  // MakeBucket - create a new bucket.
  1147  func (f *fsClient) MakeBucket(_ context.Context, _ string, _, _ bool) *probe.Error {
  1148  	// TODO: ignoreExisting has no effect currently. In the future, we want
  1149  	// to call os.Mkdir() when ignoredExisting is disabled and os.MkdirAll()
  1150  	// otherwise.
  1151  	// NOTE: withLock=true has no meaning here.
  1152  	e := os.MkdirAll(f.PathURL.Path, 0o777)
  1153  	if e != nil {
  1154  		return probe.NewError(e)
  1155  	}
  1156  	return nil
  1157  }
  1158  
  1159  // RemoveBucket - remove a bucket
  1160  func (f *fsClient) RemoveBucket(_ context.Context, forceRemove bool) *probe.Error {
  1161  	var e error
  1162  	if forceRemove {
  1163  		e = os.RemoveAll(f.PathURL.Path)
  1164  	} else {
  1165  		e = os.Remove(f.PathURL.Path)
  1166  	}
  1167  	return probe.NewError(e)
  1168  }
  1169  
  1170  // Set object lock configuration of bucket.
  1171  func (f *fsClient) SetObjectLockConfig(_ context.Context, _ minio.RetentionMode, _ uint64, _ minio.ValidityUnit) *probe.Error {
  1172  	return probe.NewError(APINotImplemented{
  1173  		API:     "SetObjectLockConfig",
  1174  		APIType: "filesystem",
  1175  	})
  1176  }
  1177  
  1178  // Get object lock configuration of bucket.
  1179  func (f *fsClient) GetObjectLockConfig(_ context.Context) (status string, mode minio.RetentionMode, validity uint64, unit minio.ValidityUnit, err *probe.Error) {
  1180  	return "", "", 0, "", probe.NewError(APINotImplemented{
  1181  		API:     "GetObjectLockConfig",
  1182  		APIType: "filesystem",
  1183  	})
  1184  }
  1185  
  1186  // GetAccessRules - unsupported API
  1187  func (f *fsClient) GetAccessRules(_ context.Context) (map[string]string, *probe.Error) {
  1188  	return map[string]string{}, probe.NewError(APINotImplemented{
  1189  		API:     "GetBucketPolicy",
  1190  		APIType: "filesystem",
  1191  	})
  1192  }
  1193  
  1194  // Set object retention for a given object.
  1195  func (f *fsClient) PutObjectRetention(_ context.Context, _ string, _ minio.RetentionMode, _ time.Time, _ bool) *probe.Error {
  1196  	return probe.NewError(APINotImplemented{
  1197  		API:     "PutObjectRetention",
  1198  		APIType: "filesystem",
  1199  	})
  1200  }
  1201  
  1202  func (f *fsClient) GetObjectRetention(_ context.Context, _ string) (minio.RetentionMode, time.Time, *probe.Error) {
  1203  	return "", time.Time{}, probe.NewError(APINotImplemented{
  1204  		API:     "GetObjectRetention",
  1205  		APIType: "filesystem",
  1206  	})
  1207  }
  1208  
  1209  // Set object legal hold for a given object.
  1210  func (f *fsClient) PutObjectLegalHold(_ context.Context, _ string, _ minio.LegalHoldStatus) *probe.Error {
  1211  	return probe.NewError(APINotImplemented{
  1212  		API:     "PutObjectLegalHold",
  1213  		APIType: "filesystem",
  1214  	})
  1215  }
  1216  
  1217  // Get object legal hold for a given object.
  1218  func (f *fsClient) GetObjectLegalHold(_ context.Context, _ string) (minio.LegalHoldStatus, *probe.Error) {
  1219  	return "", probe.NewError(APINotImplemented{
  1220  		API:     "GetObjectLegalHold",
  1221  		APIType: "filesystem",
  1222  	})
  1223  }
  1224  
  1225  // GetAccess - get access policy permissions.
  1226  func (f *fsClient) GetAccess(_ context.Context) (access, policyJSON string, err *probe.Error) {
  1227  	// For windows this feature is not implemented.
  1228  	if runtime.GOOS == "windows" {
  1229  		return "", "", probe.NewError(APINotImplemented{API: "GetAccess", APIType: "filesystem"})
  1230  	}
  1231  	st, err := f.fsStat(false)
  1232  	if err != nil {
  1233  		return "", "", err.Trace(f.PathURL.String())
  1234  	}
  1235  	if !st.Mode().IsDir() {
  1236  		return "", "", probe.NewError(APINotImplemented{API: "GetAccess", APIType: "filesystem"})
  1237  	}
  1238  	// Mask with os.ModePerm to get only inode permissions
  1239  	switch st.Mode() & os.ModePerm {
  1240  	case os.FileMode(0o777):
  1241  		return "readwrite", "", nil
  1242  	case os.FileMode(0o555):
  1243  		return "readonly", "", nil
  1244  	case os.FileMode(0o333):
  1245  		return "writeonly", "", nil
  1246  	}
  1247  	return "none", "", nil
  1248  }
  1249  
  1250  // SetAccess - set access policy permissions.
  1251  func (f *fsClient) SetAccess(_ context.Context, access string, isJSON bool) *probe.Error {
  1252  	// For windows this feature is not implemented.
  1253  	// JSON policy for fs is not yet implemented.
  1254  	if runtime.GOOS == "windows" || isJSON {
  1255  		return probe.NewError(APINotImplemented{API: "SetAccess", APIType: "filesystem"})
  1256  	}
  1257  	st, err := f.fsStat(false)
  1258  	if err != nil {
  1259  		return err.Trace(f.PathURL.String())
  1260  	}
  1261  	if !st.Mode().IsDir() {
  1262  		return probe.NewError(APINotImplemented{API: "SetAccess", APIType: "filesystem"})
  1263  	}
  1264  	var mode os.FileMode
  1265  	switch access {
  1266  	case "readonly":
  1267  		mode = os.FileMode(0o555)
  1268  	case "writeonly":
  1269  		mode = os.FileMode(0o333)
  1270  	case "readwrite":
  1271  		mode = os.FileMode(0o777)
  1272  	case "none":
  1273  		mode = os.FileMode(0o755)
  1274  	}
  1275  	e := os.Chmod(f.PathURL.Path, mode)
  1276  	if e != nil {
  1277  		return probe.NewError(e)
  1278  	}
  1279  	return nil
  1280  }
  1281  
  1282  // Stat - get metadata from path.
  1283  func (f *fsClient) Stat(_ context.Context, opts StatOptions) (content *ClientContent, err *probe.Error) {
  1284  	st, err := f.fsStat(opts.incomplete)
  1285  	if err != nil {
  1286  		return nil, err.Trace(f.PathURL.String())
  1287  	}
  1288  
  1289  	content = &ClientContent{}
  1290  	content.URL = *f.PathURL
  1291  	content.Size = st.Size()
  1292  	content.Time = st.ModTime()
  1293  	content.Type = st.Mode()
  1294  	content.Metadata = map[string]string{
  1295  		"Content-Type": guessURLContentType(f.PathURL.Path),
  1296  	}
  1297  
  1298  	path := f.PathURL.String()
  1299  	// Populates meta data with file system attribute only in case of
  1300  	// when preserve flag is passed.
  1301  	if opts.preserve {
  1302  		fileAttr, err := disk.GetFileSystemAttrs(path)
  1303  		if err != nil {
  1304  			return content, nil
  1305  		}
  1306  		metaData, pErr := getAllXattrs(path)
  1307  		if pErr != nil {
  1308  			return content, nil
  1309  		}
  1310  		for k, v := range metaData {
  1311  			content.Metadata[k] = v
  1312  		}
  1313  		content.Metadata[metadataKey] = fileAttr
  1314  	}
  1315  
  1316  	return content, nil
  1317  }
  1318  
  1319  // toClientError error constructs a typed client error for known filesystem errors.
  1320  func (f *fsClient) toClientError(e error, fpath string) *probe.Error {
  1321  	if os.IsPermission(e) {
  1322  		return probe.NewError(PathInsufficientPermission{Path: fpath})
  1323  	}
  1324  	if os.IsNotExist(e) {
  1325  		return probe.NewError(PathNotFound{Path: fpath})
  1326  	}
  1327  	if errors.Is(e, syscall.ELOOP) {
  1328  		return probe.NewError(TooManyLevelsSymlink{Path: fpath})
  1329  	}
  1330  	return probe.NewError(e)
  1331  }
  1332  
  1333  // fsStat - wrapper function to get file stat.
  1334  func (f *fsClient) fsStat(isIncomplete bool) (os.FileInfo, *probe.Error) {
  1335  	fpath := f.PathURL.Path
  1336  
  1337  	// Check if the path corresponds to a directory and returns
  1338  	// the successful result whether isIncomplete is specified or not.
  1339  	st, e := os.Stat(fpath)
  1340  	if e == nil && st.IsDir() {
  1341  		return st, nil
  1342  	}
  1343  
  1344  	if isIncomplete {
  1345  		fpath += partSuffix
  1346  	}
  1347  
  1348  	st, e = os.Stat(fpath)
  1349  	if e != nil {
  1350  		return nil, f.toClientError(e, fpath)
  1351  	}
  1352  	return st, nil
  1353  }
  1354  
  1355  func (f *fsClient) AddUserAgent(_, _ string) {
  1356  }
  1357  
  1358  // Get Object Tags
  1359  func (f *fsClient) GetTags(_ context.Context, _ string) (map[string]string, *probe.Error) {
  1360  	return nil, probe.NewError(APINotImplemented{
  1361  		API:     "GetObjectTagging",
  1362  		APIType: "filesystem",
  1363  	})
  1364  }
  1365  
  1366  // Set Object tags
  1367  func (f *fsClient) SetTags(_ context.Context, _, _ string) *probe.Error {
  1368  	return probe.NewError(APINotImplemented{
  1369  		API:     "SetObjectTagging",
  1370  		APIType: "filesystem",
  1371  	})
  1372  }
  1373  
  1374  // Delete object tags
  1375  func (f *fsClient) DeleteTags(_ context.Context, _ string) *probe.Error {
  1376  	return probe.NewError(APINotImplemented{
  1377  		API:     "DeleteObjectTagging",
  1378  		APIType: "filesystem",
  1379  	})
  1380  }
  1381  
  1382  // Get lifecycle configuration for a given bucket, not implemented.
  1383  func (f *fsClient) GetLifecycle(_ context.Context) (*lifecycle.Configuration, time.Time, *probe.Error) {
  1384  	return nil, time.Time{}, probe.NewError(APINotImplemented{
  1385  		API:     "GetLifecycle",
  1386  		APIType: "filesystem",
  1387  	})
  1388  }
  1389  
  1390  // Set lifecycle configuration for a given bucket, not implemented.
  1391  func (f *fsClient) SetLifecycle(_ context.Context, _ *lifecycle.Configuration) *probe.Error {
  1392  	return probe.NewError(APINotImplemented{
  1393  		API:     "SetLifecycle",
  1394  		APIType: "filesystem",
  1395  	})
  1396  }
  1397  
  1398  // Get version info for a bucket, not implemented.
  1399  func (f *fsClient) GetVersion(_ context.Context) (minio.BucketVersioningConfiguration, *probe.Error) {
  1400  	return minio.BucketVersioningConfiguration{}, probe.NewError(APINotImplemented{
  1401  		API:     "GetVersion",
  1402  		APIType: "filesystem",
  1403  	})
  1404  }
  1405  
  1406  // SetVersion - Set version configuration on a bucket, not implemented
  1407  func (f *fsClient) SetVersion(_ context.Context, _ string, _ []string, _ bool) *probe.Error {
  1408  	return probe.NewError(APINotImplemented{
  1409  		API:     "SetVersion",
  1410  		APIType: "filesystem",
  1411  	})
  1412  }
  1413  
  1414  // Get replication configuration for a given bucket, not implemented.
  1415  func (f *fsClient) GetReplication(_ context.Context) (replication.Config, *probe.Error) {
  1416  	return replication.Config{}, probe.NewError(APINotImplemented{
  1417  		API:     "GetReplication",
  1418  		APIType: "filesystem",
  1419  	})
  1420  }
  1421  
  1422  // Set replication configuration for a given bucket, not implemented.
  1423  func (f *fsClient) SetReplication(_ context.Context, _ *replication.Config, _ replication.Options) *probe.Error {
  1424  	return probe.NewError(APINotImplemented{
  1425  		API:     "SetReplication",
  1426  		APIType: "filesystem",
  1427  	})
  1428  }
  1429  
  1430  // Remove replication configuration for a given bucket. Not implemented
  1431  func (f *fsClient) RemoveReplication(_ context.Context) *probe.Error {
  1432  	return probe.NewError(APINotImplemented{
  1433  		API:     "RemoveReplication",
  1434  		APIType: "filesystem",
  1435  	})
  1436  }
  1437  
  1438  // GetReplicationMetrics - Get replication metrics for a given bucket, not implemented.
  1439  func (f *fsClient) GetReplicationMetrics(_ context.Context) (replication.MetricsV2, *probe.Error) {
  1440  	return replication.MetricsV2{}, probe.NewError(APINotImplemented{
  1441  		API:     "GetReplicationMetrics",
  1442  		APIType: "filesystem",
  1443  	})
  1444  }
  1445  
  1446  // ResetReplication - kicks off replication again on previously replicated objects if existing object
  1447  // replication is enabled in the replication config, not implemented
  1448  func (f *fsClient) ResetReplication(_ context.Context, _ time.Duration, _ string) (rinfo replication.ResyncTargetsInfo, err *probe.Error) {
  1449  	return rinfo, probe.NewError(APINotImplemented{
  1450  		API:     "ResetReplication",
  1451  		APIType: "filesystem",
  1452  	})
  1453  }
  1454  
  1455  // ReplicationResyncStatus - gets status of replication resync for this target arn
  1456  func (f *fsClient) ReplicationResyncStatus(_ context.Context, _ string) (rinfo replication.ResyncTargetsInfo, err *probe.Error) {
  1457  	return rinfo, probe.NewError(APINotImplemented{
  1458  		API:     "ReplicationResyncStatus",
  1459  		APIType: "filesystem",
  1460  	})
  1461  }
  1462  
  1463  // Get encryption info for a bucket, not implemented.
  1464  func (f *fsClient) GetEncryption(_ context.Context) (string, string, *probe.Error) {
  1465  	return "", "", probe.NewError(APINotImplemented{
  1466  		API:     "GetEncryption",
  1467  		APIType: "filesystem",
  1468  	})
  1469  }
  1470  
  1471  // SetEncryption - Set encryption configuration on a bucket, not implemented
  1472  func (f *fsClient) SetEncryption(_ context.Context, _, _ string) *probe.Error {
  1473  	return probe.NewError(APINotImplemented{
  1474  		API:     "SetEncryption",
  1475  		APIType: "filesystem",
  1476  	})
  1477  }
  1478  
  1479  // DeleteEncryption - removes encryption configuration on a bucket, not implemented
  1480  func (f *fsClient) DeleteEncryption(_ context.Context) *probe.Error {
  1481  	return probe.NewError(APINotImplemented{
  1482  		API:     "DeleteEncryption",
  1483  		APIType: "filesystem",
  1484  	})
  1485  }
  1486  
  1487  // Gets bucket infoOA
  1488  func (f *fsClient) GetBucketInfo(_ context.Context) (BucketInfo, *probe.Error) {
  1489  	return BucketInfo{}, probe.NewError(APINotImplemented{
  1490  		API:     "GetBucketInfo",
  1491  		APIType: "filesystem",
  1492  	})
  1493  }
  1494  
  1495  // Restore object - not implemented
  1496  func (f *fsClient) Restore(_ context.Context, _ string, _ int) *probe.Error {
  1497  	return probe.NewError(APINotImplemented{
  1498  		API:     "Restore",
  1499  		APIType: "filesystem",
  1500  	})
  1501  }
  1502  
  1503  // OD Get - not implemented
  1504  func (f *fsClient) GetPart(_ context.Context, _ int) (io.ReadCloser, *probe.Error) {
  1505  	return nil, probe.NewError(APINotImplemented{
  1506  		API:     "GetPart",
  1507  		APIType: "filesystem",
  1508  	})
  1509  }