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 }