mirror of
https://github.com/minio/mc.git
synced 2025-11-12 01:02:26 +03:00
This is due to the fact that ReadAt() will not send io.EOF when reading past the offset, server instead returns Range error. Handle the error by only reading until the requested size.
1222 lines
32 KiB
Go
1222 lines
32 KiB
Go
/*
|
|
* Minio Client (C) 2015 Minio, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this fs except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"io/ioutil"
|
|
|
|
"github.com/minio/mc/pkg/hookreader"
|
|
"github.com/minio/mc/pkg/ioutils"
|
|
"github.com/minio/minio/pkg/probe"
|
|
"github.com/rjeczalik/notify"
|
|
)
|
|
|
|
// filesystem client
|
|
type fsClient struct {
|
|
PathURL *clientURL
|
|
}
|
|
|
|
const (
|
|
partSuffix = ".part.minio"
|
|
)
|
|
|
|
var ( // GOOS specific ignore list.
|
|
ignoreFiles = map[string][]string{
|
|
"darwin": {".DS_Store"},
|
|
// "default": []string{""},
|
|
}
|
|
)
|
|
|
|
// fsNew - instantiate a new fs
|
|
func fsNew(path string) (Client, *probe.Error) {
|
|
if strings.TrimSpace(path) == "" {
|
|
return nil, probe.NewError(EmptyPath{})
|
|
}
|
|
return &fsClient{
|
|
PathURL: newClientURL(normalizePath(path)),
|
|
}, nil
|
|
}
|
|
|
|
// isIgnoredFile returns true if 'filename' is on the exclude list.
|
|
func isIgnoredFile(filename string) bool {
|
|
matchFile := path.Base(filename)
|
|
|
|
// OS specific ignore list.
|
|
for _, ignoredFile := range ignoreFiles[runtime.GOOS] {
|
|
if ignoredFile == matchFile {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Default ignore list for all OSes.
|
|
for _, ignoredFile := range ignoreFiles["default"] {
|
|
if ignoredFile == matchFile {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// URL get url.
|
|
func (f *fsClient) GetURL() clientURL {
|
|
return *f.PathURL
|
|
}
|
|
|
|
// Watches for all fs events on an input path.
|
|
func (f *fsClient) Watch(params watchParams) (*watchObject, *probe.Error) {
|
|
eventChan := make(chan Event)
|
|
errorChan := make(chan *probe.Error)
|
|
doneChan := make(chan bool)
|
|
// Make the channel buffered to ensure no event is dropped. Notify will drop
|
|
// an event if the receiver is not able to keep up the sending pace.
|
|
neventChan := make(chan notify.EventInfo, 1)
|
|
|
|
var fsEvents []notify.Event
|
|
for _, event := range params.events {
|
|
switch event {
|
|
case "put":
|
|
fsEvents = append(fsEvents, EventTypePut...)
|
|
case "delete":
|
|
fsEvents = append(fsEvents, EventTypeDelete...)
|
|
default:
|
|
return nil, errInvalidArgument().Trace(event)
|
|
}
|
|
}
|
|
|
|
// Set up a watchpoint listening for events within a directory tree rooted
|
|
// at current working directory. Dispatch remove events to c.
|
|
recursivePath := f.PathURL.Path
|
|
if params.recursive {
|
|
recursivePath = f.PathURL.Path + "..."
|
|
}
|
|
if e := notify.Watch(recursivePath, neventChan, fsEvents...); e != nil {
|
|
return nil, probe.NewError(e)
|
|
}
|
|
|
|
// wait for doneChan to close the watcher, eventChan and errorChan
|
|
go func() {
|
|
<-doneChan
|
|
|
|
close(eventChan)
|
|
close(errorChan)
|
|
notify.Stop(neventChan)
|
|
}()
|
|
|
|
timeFormatFS := "2006-01-02T15:04:05.000Z"
|
|
|
|
// Get fsnotify notifications for events and errors, and sent them
|
|
// using eventChan and errorChan
|
|
go func() {
|
|
for {
|
|
select {
|
|
case event, ok := <-neventChan:
|
|
if !ok {
|
|
return
|
|
}
|
|
var i os.FileInfo
|
|
if IsPutEvent(event.Event()) {
|
|
// Look for any writes, send a response to indicate a full copy.
|
|
var e error
|
|
i, e = os.Stat(event.Path())
|
|
if e != nil {
|
|
if os.IsNotExist(e) {
|
|
continue
|
|
}
|
|
errorChan <- probe.NewError(e)
|
|
continue
|
|
}
|
|
if i.IsDir() {
|
|
// we want files
|
|
continue
|
|
}
|
|
eventChan <- Event{
|
|
Time: time.Now().Format(timeFormatFS),
|
|
Size: i.Size(),
|
|
Path: event.Path(),
|
|
Client: f,
|
|
Type: EventCreate,
|
|
}
|
|
} else if IsDeleteEvent(event.Event()) {
|
|
eventChan <- Event{
|
|
Time: time.Now().Format(timeFormatFS),
|
|
Path: event.Path(),
|
|
Client: f,
|
|
Type: EventRemove,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return &watchObject{
|
|
events: eventChan,
|
|
errors: errorChan,
|
|
done: doneChan,
|
|
}, nil
|
|
}
|
|
|
|
/// Object operations.
|
|
|
|
// Put - create a new file.
|
|
func (f *fsClient) Put(reader io.Reader, size int64, contentType string, progress io.Reader) (int64, *probe.Error) {
|
|
// ContentType is not handled on purpose.
|
|
// For filesystem this is a redundant information.
|
|
|
|
// Extract dir name.
|
|
objectDir, _ := filepath.Split(f.PathURL.Path)
|
|
objectPath := f.PathURL.Path
|
|
|
|
// Verify if destination already exists.
|
|
st, e := os.Stat(objectPath)
|
|
if e == nil {
|
|
// If the destination exists and is not a regular file.
|
|
if !st.Mode().IsRegular() {
|
|
return 0, probe.NewError(PathIsNotRegular{
|
|
Path: objectPath,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Proceed if file does not exist. return for all other errors.
|
|
if e != nil {
|
|
if !os.IsNotExist(e) {
|
|
return 0, probe.NewError(e)
|
|
}
|
|
}
|
|
|
|
// Write to a temporary file "object.part.mc" before commit.
|
|
objectPartPath := objectPath + partSuffix
|
|
if objectDir != "" {
|
|
// Create any missing top level directories.
|
|
if e = os.MkdirAll(objectDir, 0700); e != nil {
|
|
err := f.toClientError(e, f.PathURL.Path)
|
|
return 0, err.Trace(f.PathURL.Path)
|
|
}
|
|
}
|
|
|
|
// If exists, open in append mode. If not create it the part file.
|
|
partFile, e := os.OpenFile(objectPartPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
|
if e != nil {
|
|
err := f.toClientError(e, f.PathURL.Path)
|
|
return 0, err.Trace(f.PathURL.Path)
|
|
}
|
|
|
|
// Get stat to get the current size.
|
|
partSt, e := partFile.Stat()
|
|
if e != nil {
|
|
err := f.toClientError(e, objectPartPath)
|
|
return 0, err.Trace(objectPartPath)
|
|
}
|
|
|
|
var totalWritten int64
|
|
// Current file offset.
|
|
var currentOffset = partSt.Size()
|
|
|
|
// Use ReadAt() capability when reader implements it, but also avoid it in two cases:
|
|
// *) reader represents a standard input/output stream since they return illegal seek error when ReadAt() is invoked
|
|
// *) we know in advance that reader will provide zero length data
|
|
if readerAt, ok := reader.(io.ReaderAt); ok && !isStdIO(reader) && size > 0 {
|
|
// Notify the progress bar if any till current size.
|
|
if progress != nil {
|
|
if _, e = io.CopyN(ioutil.Discard, progress, currentOffset); e != nil {
|
|
return 0, probe.NewError(e)
|
|
}
|
|
}
|
|
|
|
// Allocate buffer of 10MiB once.
|
|
readAtBuffer := make([]byte, 10*1024*1024)
|
|
|
|
// Loop through all offsets on incoming io.ReaderAt and write
|
|
// to the destination.
|
|
for currentOffset < size {
|
|
readAtSize, re := readerAt.ReadAt(readAtBuffer, currentOffset)
|
|
if re != nil && re != io.EOF {
|
|
// For any errors other than io.EOF, we return error
|
|
// and breakout.
|
|
err := f.toClientError(re, objectPartPath)
|
|
return 0, err.Trace(objectPartPath)
|
|
}
|
|
writtenSize, we := partFile.Write(readAtBuffer[:readAtSize])
|
|
if we != nil {
|
|
err := f.toClientError(we, objectPartPath)
|
|
return 0, err.Trace(objectPartPath)
|
|
}
|
|
// read size and subsequent write differ, a possible
|
|
// corruption return here.
|
|
if readAtSize != writtenSize {
|
|
// Unexpected write (less data was written than expected).
|
|
return 0, probe.NewError(UnexpectedShortWrite{
|
|
InputSize: readAtSize,
|
|
WriteSize: writtenSize,
|
|
})
|
|
}
|
|
// Notify the progress bar if any for written size.
|
|
if progress != nil {
|
|
if _, e = io.CopyN(ioutil.Discard, progress, int64(writtenSize)); e != nil {
|
|
return totalWritten, probe.NewError(e)
|
|
}
|
|
}
|
|
currentOffset += int64(writtenSize)
|
|
// Once we see io.EOF we break out of the loop.
|
|
if re == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
// Save currently copied total into totalWritten.
|
|
totalWritten = currentOffset
|
|
} else {
|
|
reader = hookreader.NewHook(reader, progress)
|
|
// Discard bytes until currentOffset.
|
|
if _, e = io.CopyN(ioutil.Discard, reader, currentOffset); e != nil {
|
|
return 0, probe.NewError(e)
|
|
}
|
|
var n int64
|
|
n, e = io.Copy(partFile, reader)
|
|
if e != nil {
|
|
return 0, probe.NewError(e)
|
|
}
|
|
// Save currently copied total into totalWritten.
|
|
totalWritten = n + currentOffset
|
|
}
|
|
|
|
// Close the input reader as well, if possible.
|
|
closer, ok := reader.(io.Closer)
|
|
if ok {
|
|
if e = closer.Close(); e != nil {
|
|
return totalWritten, probe.NewError(e)
|
|
}
|
|
}
|
|
|
|
// Close the file before rename.
|
|
if e = partFile.Close(); e != nil {
|
|
return totalWritten, probe.NewError(e)
|
|
}
|
|
|
|
// Following verification is needed only for input size greater than '0'.
|
|
if size > 0 {
|
|
// Unexpected EOF reached (less data was written than expected).
|
|
if totalWritten < size {
|
|
return totalWritten, probe.NewError(UnexpectedEOF{
|
|
TotalSize: size,
|
|
TotalWritten: totalWritten,
|
|
})
|
|
}
|
|
// Unexpected ExcessRead (more data was written than expected).
|
|
if totalWritten > size {
|
|
return totalWritten, probe.NewError(UnexpectedExcessRead{
|
|
TotalSize: size,
|
|
TotalWritten: totalWritten,
|
|
})
|
|
}
|
|
}
|
|
// Safely completed put. Now commit by renaming to actual filename.
|
|
if e = os.Rename(objectPartPath, objectPath); e != nil {
|
|
err := f.toClientError(e, objectPath)
|
|
return totalWritten, err.Trace(objectPartPath, objectPath)
|
|
}
|
|
return totalWritten, nil
|
|
}
|
|
|
|
// ShareDownload - share download not implemented for filesystem.
|
|
func (f *fsClient) ShareDownload(expires time.Duration) (string, *probe.Error) {
|
|
return "", probe.NewError(APINotImplemented{
|
|
API: "ShareDownload",
|
|
APIType: "filesystem",
|
|
})
|
|
}
|
|
|
|
// ShareUpload - share upload not implemented for filesystem.
|
|
func (f *fsClient) ShareUpload(startsWith bool, expires time.Duration, contentType string) (string, map[string]string, *probe.Error) {
|
|
return "", nil, probe.NewError(APINotImplemented{
|
|
API: "ShareUpload",
|
|
APIType: "filesystem",
|
|
})
|
|
}
|
|
|
|
// readFile reads and returns the data inside the file located
|
|
// at the provided filepath.
|
|
func readFile(fpath string) (io.ReadCloser, error) {
|
|
// Golang strips trailing / if you clean(..) or
|
|
// EvalSymlinks(..). Adding '.' prevents it from doing so.
|
|
if strings.HasSuffix(fpath, "/") {
|
|
fpath = fpath + "."
|
|
}
|
|
fpath, e := filepath.EvalSymlinks(fpath)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
fileData, e := os.Open(fpath)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
return fileData, nil
|
|
}
|
|
|
|
// createFile creates an empty file at the provided filepath
|
|
// if one does not exist already.
|
|
func createFile(fpath string) (io.WriteCloser, error) {
|
|
st, e := os.Stat(fpath)
|
|
// If destination exists but is not regular.
|
|
if e == nil && !st.Mode().IsRegular() {
|
|
return nil, PathIsNotRegular{Path: fpath}
|
|
}
|
|
// If file exists already.
|
|
if e != nil && !os.IsNotExist(e) {
|
|
return nil, e
|
|
}
|
|
if e = os.MkdirAll(filepath.Dir(fpath), 0775); e != nil {
|
|
return nil, e
|
|
}
|
|
file, e := os.Create(fpath)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
// Copy - copy data from source to destination
|
|
func (f *fsClient) Copy(source string, size int64, progress io.Reader) *probe.Error {
|
|
// Don't use f.Get() f.Put() directly. Instead use readFile and createFile
|
|
destination := f.PathURL.Path
|
|
if destination == source { // Cannot copy file into itself
|
|
return errOverWriteNotAllowed(destination).Trace(destination)
|
|
}
|
|
rc, e := readFile(source)
|
|
if e != nil {
|
|
err := f.toClientError(e, destination)
|
|
return err.Trace(destination)
|
|
}
|
|
defer rc.Close()
|
|
wc, e := createFile(destination)
|
|
if e != nil {
|
|
err := f.toClientError(e, destination)
|
|
return err.Trace(destination)
|
|
}
|
|
defer wc.Close()
|
|
reader := hookreader.NewHook(rc, progress)
|
|
// Perform copy
|
|
n, _ := io.CopyN(wc, reader, size) // e == nil only if n != size
|
|
// Only check size related errors if size is positive
|
|
if size > 0 {
|
|
if n < size { // Unexpected early EOF
|
|
return probe.NewError(UnexpectedEOF{
|
|
TotalSize: size,
|
|
TotalWritten: n,
|
|
})
|
|
}
|
|
if n > size { // Unexpected ExcessRead
|
|
return probe.NewError(UnexpectedExcessRead{
|
|
TotalSize: size,
|
|
TotalWritten: n,
|
|
})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetPartial download a part object from bucket.
|
|
// sets err for any errors, reader is nil for errors.
|
|
func (f *fsClient) Get() (io.Reader, *probe.Error) {
|
|
tmppath := f.PathURL.Path
|
|
// Golang strips trailing / if you clean(..) or
|
|
// EvalSymlinks(..). Adding '.' prevents it from doing so.
|
|
if strings.HasSuffix(tmppath, string(f.PathURL.Separator)) {
|
|
tmppath = tmppath + "."
|
|
}
|
|
|
|
// Resolve symlinks.
|
|
_, e := filepath.EvalSymlinks(tmppath)
|
|
if e != nil {
|
|
err := f.toClientError(e, f.PathURL.Path)
|
|
return nil, err.Trace(f.PathURL.Path)
|
|
}
|
|
fileData, e := os.Open(f.PathURL.Path)
|
|
if e != nil {
|
|
err := f.toClientError(e, f.PathURL.Path)
|
|
return nil, err.Trace(f.PathURL.Path)
|
|
}
|
|
return fileData, nil
|
|
}
|
|
|
|
// Remove - remove entry read from clientContent channel.
|
|
func (f *fsClient) Remove(isIncomplete bool, contentCh <-chan *clientContent) <-chan *probe.Error {
|
|
errorCh := make(chan *probe.Error)
|
|
|
|
// Goroutine reads from contentCh and removes the entry in content.
|
|
go func() {
|
|
defer close(errorCh)
|
|
|
|
for content := range contentCh {
|
|
name := content.URL.Path
|
|
// Add partSuffix for incomplete uploads.
|
|
if isIncomplete {
|
|
name += partSuffix
|
|
}
|
|
if err := os.Remove(name); err != nil {
|
|
if os.IsPermission(err) {
|
|
// Ignore permission error.
|
|
errorCh <- probe.NewError(PathInsufficientPermission{Path: content.URL.Path})
|
|
} else {
|
|
errorCh <- probe.NewError(err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return errorCh
|
|
}
|
|
|
|
// List - list files and folders.
|
|
func (f *fsClient) List(isRecursive, isIncomplete bool, showDir DirOpt) <-chan *clientContent {
|
|
contentCh := make(chan *clientContent)
|
|
if isRecursive {
|
|
if showDir == DirNone {
|
|
go f.listRecursiveInRoutine(contentCh, isIncomplete)
|
|
} else {
|
|
go f.listDirOpt(contentCh, isIncomplete, showDir)
|
|
}
|
|
} else {
|
|
go f.listInRoutine(contentCh, isIncomplete)
|
|
}
|
|
|
|
return contentCh
|
|
}
|
|
|
|
// byDirName implements sort.Interface.
|
|
type byDirName []os.FileInfo
|
|
|
|
func (f byDirName) Len() int { return len(f) }
|
|
func (f byDirName) Less(i, j int) bool {
|
|
// For directory add an ending separator fortrue lexical
|
|
// order.
|
|
if f[i].Mode().IsDir() {
|
|
return f[i].Name()+string(filepath.Separator) < f[j].Name()
|
|
}
|
|
// For directory add an ending separator for true lexical
|
|
// order.
|
|
if f[j].Mode().IsDir() {
|
|
return f[i].Name() < f[j].Name()+string(filepath.Separator)
|
|
}
|
|
return f[i].Name() < f[j].Name()
|
|
}
|
|
func (f byDirName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
|
|
|
|
// readDir reads the directory named by dirname and returns
|
|
// a list of sorted directory entries.
|
|
func readDir(dirname string) ([]os.FileInfo, error) {
|
|
f, e := os.Open(dirname)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
list, e := f.Readdir(-1)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if e = f.Close(); e != nil {
|
|
return nil, e
|
|
}
|
|
sort.Sort(byDirName(list))
|
|
return list, nil
|
|
}
|
|
|
|
// listPrefixes - list all files for any given prefix.
|
|
func (f *fsClient) listPrefixes(prefix string, contentCh chan<- *clientContent, incomplete bool) {
|
|
dirName := filepath.Dir(prefix)
|
|
files, e := readDir(dirName)
|
|
if e != nil {
|
|
err := f.toClientError(e, dirName)
|
|
contentCh <- &clientContent{
|
|
Err: err.Trace(dirName),
|
|
}
|
|
return
|
|
}
|
|
pathURL := *f.PathURL
|
|
for _, fi := range files {
|
|
// Skip ignored files.
|
|
if isIgnoredFile(fi.Name()) {
|
|
continue
|
|
}
|
|
|
|
file := filepath.Join(dirName, fi.Name())
|
|
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
st, e := os.Stat(file)
|
|
if e != nil {
|
|
if os.IsPermission(e) {
|
|
// On windows there are folder symlinks
|
|
// which are called junction files which
|
|
// carry special meaning on windows
|
|
// - which cannot be accessed with regular operations
|
|
if runtime.GOOS == "windows" {
|
|
newPath := filepath.Join(prefix, fi.Name())
|
|
lfi, le := f.handleWindowsSymlinks(newPath)
|
|
if le != nil {
|
|
contentCh <- &clientContent{
|
|
Err: le.Trace(newPath),
|
|
}
|
|
continue
|
|
}
|
|
if incomplete {
|
|
if !strings.HasSuffix(lfi.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
} else {
|
|
if strings.HasSuffix(lfi.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
}
|
|
pathURL.Path = filepath.Join(pathURL.Path, lfi.Name())
|
|
contentCh <- &clientContent{
|
|
URL: pathURL,
|
|
Time: lfi.ModTime(),
|
|
Size: lfi.Size(),
|
|
Type: lfi.Mode(),
|
|
Err: probe.NewError(PathInsufficientPermission{
|
|
Path: pathURL.Path,
|
|
}),
|
|
}
|
|
continue
|
|
} else {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(PathInsufficientPermission{
|
|
Path: pathURL.Path,
|
|
}),
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
if os.IsNotExist(e) {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(BrokenSymlink{
|
|
Path: pathURL.Path,
|
|
}),
|
|
}
|
|
continue
|
|
}
|
|
if e != nil {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(e),
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
if strings.HasPrefix(file, prefix) {
|
|
if incomplete {
|
|
if !strings.HasSuffix(st.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
} else {
|
|
if strings.HasSuffix(st.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
}
|
|
contentCh <- &clientContent{
|
|
URL: *newClientURL(file),
|
|
Time: st.ModTime(),
|
|
Size: st.Size(),
|
|
Type: st.Mode(),
|
|
Err: nil,
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
if strings.HasPrefix(file, prefix) {
|
|
if incomplete {
|
|
if !strings.HasSuffix(fi.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
} else {
|
|
if strings.HasSuffix(fi.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
}
|
|
contentCh <- &clientContent{
|
|
URL: *newClientURL(file),
|
|
Time: fi.ModTime(),
|
|
Size: fi.Size(),
|
|
Type: fi.Mode(),
|
|
Err: nil,
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (f *fsClient) listInRoutine(contentCh chan<- *clientContent, incomplete bool) {
|
|
// close the channel when the function returns.
|
|
defer close(contentCh)
|
|
|
|
// save pathURL and file path for further usage.
|
|
pathURL := *f.PathURL
|
|
fpath := pathURL.Path
|
|
|
|
fst, err := f.fsStat(false)
|
|
if err != nil {
|
|
if _, ok := err.ToGoError().(PathNotFound); ok {
|
|
// If file does not exist treat it like a prefix and list all prefixes if any.
|
|
prefix := fpath
|
|
f.listPrefixes(prefix, contentCh, incomplete)
|
|
return
|
|
}
|
|
// For all other errors we return genuine error back to the caller.
|
|
contentCh <- &clientContent{Err: err.Trace(fpath)}
|
|
return
|
|
}
|
|
|
|
// Now if the file exists and doesn't end with a separator ('/') do not traverse it.
|
|
// If the directory doesn't end with a separator, do not traverse it.
|
|
if !strings.HasSuffix(fpath, string(pathURL.Separator)) && fst.Mode().IsDir() && fpath != "." {
|
|
f.listPrefixes(fpath, contentCh, incomplete)
|
|
return
|
|
}
|
|
|
|
// If we really see the directory.
|
|
switch fst.Mode().IsDir() {
|
|
case true:
|
|
files, e := readDir(fpath)
|
|
if err != nil {
|
|
contentCh <- &clientContent{Err: probe.NewError(e)}
|
|
return
|
|
}
|
|
for _, file := range files {
|
|
fi := file
|
|
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
fi, e = os.Stat(filepath.Join(fpath, fi.Name()))
|
|
if os.IsPermission(e) {
|
|
// On windows there are folder symlinks
|
|
// which are called junction files which
|
|
// carry special meaning on windows
|
|
// - which cannot be accessed with regular operations
|
|
if runtime.GOOS == "windows" {
|
|
newPath := filepath.Join(fpath, fi.Name())
|
|
lfi, le := f.handleWindowsSymlinks(newPath)
|
|
if le != nil {
|
|
contentCh <- &clientContent{
|
|
Err: le.Trace(newPath),
|
|
}
|
|
continue
|
|
}
|
|
if incomplete {
|
|
if !strings.HasSuffix(lfi.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
} else {
|
|
if strings.HasSuffix(lfi.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
}
|
|
pathURL = *f.PathURL
|
|
pathURL.Path = filepath.Join(pathURL.Path, lfi.Name())
|
|
contentCh <- &clientContent{
|
|
URL: pathURL,
|
|
Time: lfi.ModTime(),
|
|
Size: lfi.Size(),
|
|
Type: lfi.Mode(),
|
|
Err: probe.NewError(PathInsufficientPermission{Path: pathURL.Path}),
|
|
}
|
|
continue
|
|
} else {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(PathInsufficientPermission{Path: pathURL.Path}),
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
if os.IsNotExist(e) {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(BrokenSymlink{Path: file.Name()}),
|
|
}
|
|
continue
|
|
}
|
|
if e != nil {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(e),
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
if fi.Mode().IsRegular() || fi.Mode().IsDir() {
|
|
if incomplete {
|
|
if !strings.HasSuffix(fi.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
} else {
|
|
if strings.HasSuffix(fi.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
}
|
|
pathURL = *f.PathURL
|
|
pathURL.Path = filepath.Join(pathURL.Path, fi.Name())
|
|
|
|
// Skip ignored files.
|
|
if isIgnoredFile(fi.Name()) {
|
|
continue
|
|
}
|
|
|
|
contentCh <- &clientContent{
|
|
URL: pathURL,
|
|
Time: fi.ModTime(),
|
|
Size: fi.Size(),
|
|
Type: fi.Mode(),
|
|
Err: nil,
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
if incomplete {
|
|
if !strings.HasSuffix(fst.Name(), partSuffix) {
|
|
return
|
|
}
|
|
} else {
|
|
if strings.HasSuffix(fst.Name(), partSuffix) {
|
|
return
|
|
}
|
|
}
|
|
contentCh <- &clientContent{
|
|
URL: pathURL,
|
|
Time: fst.ModTime(),
|
|
Size: fst.Size(),
|
|
Type: fst.Mode(),
|
|
Err: nil,
|
|
}
|
|
}
|
|
}
|
|
|
|
// List files recursively using non-recursive mode.
|
|
func (f *fsClient) listDirOpt(contentCh chan *clientContent, isIncomplete bool, dirOpt DirOpt) {
|
|
defer close(contentCh)
|
|
|
|
// Trim trailing / or \.
|
|
currentPath := f.PathURL.Path
|
|
currentPath = strings.TrimSuffix(currentPath, "/")
|
|
if runtime.GOOS == "windows" {
|
|
currentPath = strings.TrimSuffix(currentPath, `\`)
|
|
}
|
|
|
|
// Closure function reads currentPath and sends to contentCh. If a directory is found, it lists the directory content recursively.
|
|
var listDir func(currentPath string) bool
|
|
listDir = func(currentPath string) (isStop bool) {
|
|
files, err := readDir(currentPath)
|
|
if err != nil {
|
|
if os.IsPermission(err) {
|
|
contentCh <- &clientContent{Err: probe.NewError(PathInsufficientPermission{Path: currentPath})}
|
|
return false
|
|
}
|
|
|
|
contentCh <- &clientContent{Err: probe.NewError(err)}
|
|
return true
|
|
}
|
|
|
|
for _, file := range files {
|
|
name := filepath.Join(currentPath, file.Name())
|
|
content := clientContent{
|
|
URL: *newClientURL(name),
|
|
Time: file.ModTime(),
|
|
Size: file.Size(),
|
|
Type: file.Mode(),
|
|
Err: nil,
|
|
}
|
|
if file.Mode().IsDir() {
|
|
if dirOpt == DirFirst && !isIncomplete {
|
|
contentCh <- &content
|
|
}
|
|
if listDir(filepath.Join(name)) {
|
|
return true
|
|
}
|
|
if dirOpt == DirLast && !isIncomplete {
|
|
contentCh <- &content
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if isIncomplete {
|
|
// Ignore if file name does not end with partSuffix.
|
|
if !strings.HasSuffix(file.Name(), partSuffix) {
|
|
continue
|
|
}
|
|
|
|
// Strip partSuffix
|
|
name = strings.SplitAfter(name, partSuffix)[0]
|
|
content.URL = *newClientURL(name)
|
|
}
|
|
|
|
contentCh <- &content
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// listDir() does not send currentPath to contentCh. We send it here depending on dirOpt.
|
|
|
|
if dirOpt == DirFirst && !isIncomplete {
|
|
contentCh <- &clientContent{URL: *newClientURL(currentPath), Type: os.ModeDir}
|
|
}
|
|
|
|
listDir(currentPath)
|
|
|
|
if dirOpt == DirLast && !isIncomplete {
|
|
contentCh <- &clientContent{URL: *newClientURL(currentPath), Type: os.ModeDir}
|
|
}
|
|
}
|
|
|
|
func (f *fsClient) listRecursiveInRoutine(contentCh chan *clientContent, incomplete bool) {
|
|
// close channels upon return.
|
|
defer close(contentCh)
|
|
var dirName string
|
|
var filePrefix string
|
|
pathURL := *f.PathURL
|
|
visitFS := func(fp string, fi os.FileInfo, e error) error {
|
|
// If file path ends with filepath.Separator and equals to root path, skip it.
|
|
if strings.HasSuffix(fp, string(pathURL.Separator)) {
|
|
if fp == dirName {
|
|
return nil
|
|
}
|
|
}
|
|
// We would never need to print system root path '/'.
|
|
if fp == "/" {
|
|
return nil
|
|
}
|
|
|
|
// Ignore files from ignore list.
|
|
if isIgnoredFile(fi.Name()) {
|
|
return nil
|
|
}
|
|
|
|
/// In following situations we need to handle listing properly.
|
|
// - When filepath is '/usr' and prefix is '/usr/bi'
|
|
// - When filepath is '/usr/bin/subdir' and prefix is '/usr/bi'
|
|
// - Do not check filePrefix if its '.'
|
|
if filePrefix != "." {
|
|
if !strings.HasPrefix(fp, filePrefix) &&
|
|
!strings.HasPrefix(filePrefix, fp) {
|
|
if e == nil {
|
|
if fi.IsDir() {
|
|
return ioutils.ErrSkipDir
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
// - Skip when fp is /usr and prefix is '/usr/bi'
|
|
// - Do not check filePrefix if its '.'
|
|
if filePrefix != "." {
|
|
if !strings.HasPrefix(fp, filePrefix) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
if e != nil {
|
|
// If operation is not permitted, we throw quickly back.
|
|
if strings.Contains(e.Error(), "operation not permitted") {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(e),
|
|
}
|
|
return nil
|
|
}
|
|
if os.IsPermission(e) {
|
|
if runtime.GOOS == "windows" {
|
|
lfi, le := f.handleWindowsSymlinks(fp)
|
|
if le != nil {
|
|
contentCh <- &clientContent{
|
|
Err: le.Trace(fp),
|
|
}
|
|
return nil
|
|
}
|
|
pathURL = *f.PathURL
|
|
pathURL.Path = filepath.Join(pathURL.Path, dirName)
|
|
if incomplete {
|
|
if !strings.HasSuffix(lfi.Name(), partSuffix) {
|
|
return nil
|
|
}
|
|
} else {
|
|
if strings.HasSuffix(lfi.Name(), partSuffix) {
|
|
return nil
|
|
}
|
|
}
|
|
contentCh <- &clientContent{
|
|
URL: pathURL,
|
|
Time: lfi.ModTime(),
|
|
Size: lfi.Size(),
|
|
Type: lfi.Mode(),
|
|
Err: probe.NewError(e),
|
|
}
|
|
} else {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(PathInsufficientPermission{Path: fp}),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return e
|
|
}
|
|
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
fi, e = os.Stat(fp)
|
|
if e != nil {
|
|
if os.IsPermission(e) {
|
|
if runtime.GOOS == "windows" {
|
|
lfi, le := f.handleWindowsSymlinks(fp)
|
|
if le != nil {
|
|
contentCh <- &clientContent{
|
|
Err: le.Trace(fp),
|
|
}
|
|
return nil
|
|
}
|
|
pathURL = *f.PathURL
|
|
pathURL.Path = filepath.Join(pathURL.Path, dirName)
|
|
if incomplete {
|
|
if !strings.HasSuffix(lfi.Name(), partSuffix) {
|
|
return nil
|
|
}
|
|
} else {
|
|
if !strings.HasSuffix(lfi.Name(), partSuffix) {
|
|
return nil
|
|
}
|
|
}
|
|
contentCh <- &clientContent{
|
|
URL: pathURL,
|
|
Time: lfi.ModTime(),
|
|
Size: lfi.Size(),
|
|
Type: lfi.Mode(),
|
|
Err: probe.NewError(e),
|
|
}
|
|
return nil
|
|
}
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(e),
|
|
}
|
|
return nil
|
|
}
|
|
// Ignore in-accessible broken symlinks.
|
|
if os.IsNotExist(e) {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(BrokenSymlink{Path: fp}),
|
|
}
|
|
return nil
|
|
}
|
|
// Ignore symlink loops.
|
|
if strings.Contains(e.Error(), "too many levels of symbolic links") {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(TooManyLevelsSymlink{Path: fp}),
|
|
}
|
|
return nil
|
|
}
|
|
return e
|
|
}
|
|
}
|
|
if fi.Mode().IsRegular() {
|
|
if incomplete {
|
|
if !strings.HasSuffix(fi.Name(), partSuffix) {
|
|
return nil
|
|
}
|
|
} else {
|
|
if strings.HasSuffix(fi.Name(), partSuffix) {
|
|
return nil
|
|
}
|
|
}
|
|
contentCh <- &clientContent{
|
|
URL: *newClientURL(fp),
|
|
Time: fi.ModTime(),
|
|
Size: fi.Size(),
|
|
Type: fi.Mode(),
|
|
Err: nil,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
// No prefix to be filtered by default.
|
|
filePrefix = ""
|
|
// if f.Path ends with filepath.Separator - assuming it to be a directory and moving on.
|
|
if strings.HasSuffix(pathURL.Path, string(pathURL.Separator)) {
|
|
dirName = pathURL.Path
|
|
} else {
|
|
// if not a directory, take base path to navigate through WalkFunc.
|
|
dirName = filepath.Dir(pathURL.Path)
|
|
if !strings.HasSuffix(dirName, string(pathURL.Separator)) {
|
|
// basepath truncates the filepath.Separator,
|
|
// add it deligently useful for trimming file path inside WalkFunc
|
|
dirName = dirName + string(pathURL.Separator)
|
|
}
|
|
// filePrefix is kept for filtering incoming contents through WalkFunc.
|
|
filePrefix = pathURL.Path
|
|
}
|
|
// walks invokes our custom function.
|
|
e := ioutils.FTW(dirName, visitFS)
|
|
if e != nil {
|
|
contentCh <- &clientContent{
|
|
Err: probe.NewError(e),
|
|
}
|
|
}
|
|
}
|
|
|
|
// MakeBucket - create a new bucket.
|
|
func (f *fsClient) MakeBucket(region string) *probe.Error {
|
|
e := os.MkdirAll(f.PathURL.Path, 0775)
|
|
if e != nil {
|
|
return probe.NewError(e)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAccessRules - unsupported API
|
|
func (f *fsClient) GetAccessRules() (map[string]string, *probe.Error) {
|
|
return map[string]string{}, probe.NewError(APINotImplemented{
|
|
API: "ListBucketPolicies",
|
|
APIType: "filesystem",
|
|
})
|
|
}
|
|
|
|
// GetAccess - get access policy permissions.
|
|
func (f *fsClient) GetAccess() (access string, err *probe.Error) {
|
|
// For windows this feature is not implemented.
|
|
if runtime.GOOS == "windows" {
|
|
return "", probe.NewError(APINotImplemented{API: "GetAccess", APIType: "filesystem"})
|
|
}
|
|
st, err := f.fsStat(false)
|
|
if err != nil {
|
|
return "", err.Trace(f.PathURL.String())
|
|
}
|
|
if !st.Mode().IsDir() {
|
|
return "", probe.NewError(APINotImplemented{API: "GetAccess", APIType: "filesystem"})
|
|
}
|
|
switch {
|
|
case st.Mode() == os.FileMode(0777):
|
|
return "readwrite", nil
|
|
case st.Mode() == os.FileMode(0555):
|
|
return "readonly", nil
|
|
case st.Mode() == os.FileMode(0333):
|
|
return "writeonly", nil
|
|
}
|
|
return "none", nil
|
|
}
|
|
|
|
// SetAccess - set access policy permissions.
|
|
func (f *fsClient) SetAccess(access string) *probe.Error {
|
|
// For windows this feature is not implemented.
|
|
if runtime.GOOS == "windows" {
|
|
return probe.NewError(APINotImplemented{API: "SetAccess", APIType: "filesystem"})
|
|
}
|
|
st, err := f.fsStat(false)
|
|
if err != nil {
|
|
return err.Trace(f.PathURL.String())
|
|
}
|
|
if !st.Mode().IsDir() {
|
|
return probe.NewError(APINotImplemented{API: "SetAccess", APIType: "filesystem"})
|
|
}
|
|
var mode os.FileMode
|
|
switch access {
|
|
case "readonly":
|
|
mode = os.FileMode(0555)
|
|
case "writeonly":
|
|
mode = os.FileMode(0333)
|
|
case "readwrite":
|
|
mode = os.FileMode(0777)
|
|
case "none":
|
|
mode = os.FileMode(0755)
|
|
}
|
|
e := os.Chmod(f.PathURL.Path, mode)
|
|
if e != nil {
|
|
return probe.NewError(e)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Stat - get metadata from path.
|
|
func (f *fsClient) Stat(isIncomplete bool) (content *clientContent, err *probe.Error) {
|
|
st, err := f.fsStat(isIncomplete)
|
|
if err != nil {
|
|
return nil, err.Trace(f.PathURL.String())
|
|
}
|
|
content = &clientContent{}
|
|
content.URL = *f.PathURL
|
|
content.Size = st.Size()
|
|
content.Time = st.ModTime()
|
|
content.Type = st.Mode()
|
|
return content, nil
|
|
}
|
|
|
|
// toClientError error constructs a typed client error for known filesystem errors.
|
|
func (f *fsClient) toClientError(e error, fpath string) *probe.Error {
|
|
if os.IsPermission(e) {
|
|
return probe.NewError(PathInsufficientPermission{Path: fpath})
|
|
}
|
|
if os.IsNotExist(e) {
|
|
return probe.NewError(PathNotFound{Path: fpath})
|
|
}
|
|
return probe.NewError(e)
|
|
}
|
|
|
|
// handle windows symlinks - eg: junction files.
|
|
func (f *fsClient) handleWindowsSymlinks(fpath string) (os.FileInfo, *probe.Error) {
|
|
// On windows there are directory symlinks which are called junction files.
|
|
// These files carry special meaning on windows they cannot be,
|
|
// accessed with regular operations.
|
|
file, e := os.Lstat(fpath)
|
|
if e != nil {
|
|
err := f.toClientError(e, fpath)
|
|
return nil, err.Trace(fpath)
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
// fsStat - wrapper function to get file stat.
|
|
func (f *fsClient) fsStat(isIncomplete bool) (os.FileInfo, *probe.Error) {
|
|
fpath := f.PathURL.Path
|
|
if isIncomplete {
|
|
fpath += partSuffix
|
|
}
|
|
// Golang strips trailing / if you clean(..) or
|
|
// EvalSymlinks(..). Adding '.' prevents it from doing so.
|
|
if strings.HasSuffix(fpath, string(f.PathURL.Separator)) {
|
|
fpath = fpath + "."
|
|
}
|
|
fpath, e := filepath.EvalSymlinks(fpath)
|
|
if e != nil {
|
|
if os.IsPermission(e) {
|
|
if runtime.GOOS == "windows" {
|
|
return f.handleWindowsSymlinks(f.PathURL.Path)
|
|
}
|
|
return nil, probe.NewError(PathInsufficientPermission{Path: f.PathURL.Path})
|
|
}
|
|
err := f.toClientError(e, f.PathURL.Path)
|
|
return nil, err.Trace(fpath)
|
|
}
|
|
st, e := os.Stat(fpath)
|
|
if e != nil {
|
|
if os.IsPermission(e) {
|
|
if runtime.GOOS == "windows" {
|
|
return f.handleWindowsSymlinks(fpath)
|
|
}
|
|
return nil, probe.NewError(PathInsufficientPermission{Path: f.PathURL.Path})
|
|
}
|
|
if os.IsNotExist(e) {
|
|
return nil, probe.NewError(PathNotFound{Path: f.PathURL.Path})
|
|
}
|
|
return nil, probe.NewError(e)
|
|
}
|
|
return st, nil
|
|
}
|