minio/posix.go

628 lines
16 KiB
Go
Raw Normal View History

/*
* Minio Cloud Storage, (C) 2016 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file 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.
*/
fs: Break fs package to top-level and introduce ObjectAPI interface. ObjectAPI interface brings in changes needed for XL ObjectAPI layer. The new interface for any ObjectAPI layer is as below ``` // ObjectAPI interface. type ObjectAPI interface { // Bucket resource API. DeleteBucket(bucket string) *probe.Error ListBuckets() ([]BucketInfo, *probe.Error) MakeBucket(bucket string) *probe.Error GetBucketInfo(bucket string) (BucketInfo, *probe.Error) // Bucket query API. ListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsResult, *probe.Error) ListMultipartUploads(bucket string, resources BucketMultipartResourcesMetadata) (BucketMultipartResourcesMetadata, *probe.Error) // Object resource API. GetObject(bucket, object string, startOffset int64) (io.ReadCloser, *probe.Error) GetObjectInfo(bucket, object string) (ObjectInfo, *probe.Error) PutObject(bucket string, object string, size int64, data io.Reader, metadata map[string]string) (ObjectInfo, *probe.Error) DeleteObject(bucket, object string) *probe.Error // Object query API. NewMultipartUpload(bucket, object string) (string, *probe.Error) PutObjectPart(bucket, object, uploadID string, partID int, size int64, data io.Reader, md5Hex string) (string, *probe.Error) ListObjectParts(bucket, object string, resources ObjectResourcesMetadata) (ObjectResourcesMetadata, *probe.Error) CompleteMultipartUpload(bucket string, object string, uploadID string, parts []CompletePart) (ObjectInfo, *probe.Error) AbortMultipartUpload(bucket, object, uploadID string) *probe.Error } ```
2016-03-30 19:15:28 -04:00
package main
import (
"io"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"github.com/Sirupsen/logrus"
"github.com/minio/minio/pkg/disk"
"github.com/minio/minio/pkg/safe"
)
const (
fsMinSpacePercent = 5
)
// fsStorage - implements StorageAPI interface.
type fsStorage struct {
diskPath string
minFreeDisk int64
}
// isDirEmpty - returns whether given directory is empty or not.
func isDirEmpty(dirname string) (status bool, err error) {
f, err := os.Open(dirname)
if err == nil {
defer f.Close()
if _, err = f.Readdirnames(1); err == io.EOF {
status = true
err = nil
}
}
return status, err
}
// isDirExist - returns whether given directory exists or not.
func isDirExist(dirname string) (bool, error) {
fi, e := os.Stat(dirname)
if e != nil {
if os.IsNotExist(e) {
return false, nil
}
return false, e
2016-02-04 23:40:58 -05:00
}
return fi.IsDir(), nil
}
// Initialize a new storage disk.
func newPosix(diskPath string) (StorageAPI, error) {
if diskPath == "" {
log.Debug("Disk cannot be empty")
return nil, errInvalidArgument
}
st, err := os.Stat(diskPath)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": diskPath,
}).Debugf("Stat failed, with error %s.", err)
return nil, err
}
if !st.IsDir() {
log.WithFields(logrus.Fields{
"diskPath": diskPath,
}).Debugf("Disk %s.", syscall.ENOTDIR)
return nil, syscall.ENOTDIR
}
fs := fsStorage{
diskPath: diskPath,
minFreeDisk: fsMinSpacePercent, // Minimum 5% disk should be free.
}
log.WithFields(logrus.Fields{
"diskPath": diskPath,
"minFreeDisk": fsMinSpacePercent,
}).Debugf("Successfully configured FS storage API.")
return fs, nil
}
// checkDiskFree verifies if disk path has sufficient minium free disk
// space.
func checkDiskFree(diskPath string, minFreeDisk int64) (err error) {
di, err := disk.GetInfo(diskPath)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": diskPath,
}).Debugf("Failed to get disk info, %s", err)
return err
}
// Remove 5% from total space for cumulative disk
// space used for journalling, inodes etc.
availableDiskSpace := (float64(di.Free) / (float64(di.Total) - (0.05 * float64(di.Total)))) * 100
if int64(availableDiskSpace) <= minFreeDisk {
log.WithFields(logrus.Fields{
"availableDiskSpace": int64(availableDiskSpace),
"minFreeDiskSpace": minFreeDisk,
}).Debugf("Disk free space has reached its limit.")
return errDiskFull
}
// Success.
return nil
}
func removeDuplicateVols(volsInfo []VolInfo) []VolInfo {
// Use map to record duplicates as we find them.
result := []VolInfo{}
m := make(map[string]VolInfo)
for _, v := range volsInfo {
if _, found := m[v.Name]; !found {
m[v.Name] = v
}
}
result = make([]VolInfo, 0, len(m))
for _, v := range m {
result = append(result, v)
}
// Return the new slice.
return result
}
// gets all the unique directories from diskPath.
func getAllUniqueVols(dirPath string) ([]VolInfo, error) {
entries, err := readDir(dirPath)
if err != nil {
log.WithFields(logrus.Fields{
"dirPath": dirPath,
}).Debugf("readDir failed with error %s", err)
return nil, err
}
var volsInfo []VolInfo
for _, entry := range entries {
if !strings.HasSuffix(entry, slashSeparator) || !isValidVolname(filepath.Clean(entry)) {
// Skip if entry is neither a directory not a valid volume name.
continue
}
fi, err := os.Stat(pathJoin(dirPath, entry))
if err != nil {
log.WithFields(logrus.Fields{
"path": pathJoin(dirPath, entry),
}).Debugf("Stat failed with error %s", err)
return nil, err
}
volsInfo = append(volsInfo, VolInfo{
Name: fi.Name(),
// As os.Stat() doesn't carry other than ModTime(), use
// ModTime() as CreatedTime.
Created: fi.ModTime(),
})
}
return removeDuplicateVols(volsInfo), nil
}
// getVolumeDir - will convert incoming volume names to
// corresponding valid volume names on the backend in a platform
// compatible way for all operating systems. If volume is not found
// an error is generated.
func (s fsStorage) getVolumeDir(volume string) (string, error) {
if !isValidVolname(volume) {
return "", errInvalidArgument
}
volumeDir := pathJoin(s.diskPath, volume)
_, err := os.Stat(volumeDir)
if err == nil {
return volumeDir, nil
}
if os.IsNotExist(err) {
var volsInfo []VolInfo
volsInfo, err = getAllUniqueVols(s.diskPath)
if err != nil {
return volumeDir, errVolumeNotFound
}
for _, vol := range volsInfo {
// Verify if lowercase version of the volume is equal to
// the incoming volume, then use the proper name.
if strings.ToLower(vol.Name) == volume {
volumeDir = pathJoin(s.diskPath, vol.Name)
return volumeDir, nil
}
}
return volumeDir, errVolumeNotFound
} else if os.IsPermission(err) {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
}).Debugf("Stat failed with error %s", err)
return volumeDir, errVolumeAccessDenied
}
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
}).Debugf("Stat failed with error %s", err)
return volumeDir, err
}
// Make a volume entry.
func (s fsStorage) MakeVol(volume string) (err error) {
// Validate if disk is free.
if err = checkDiskFree(s.diskPath, s.minFreeDisk); err != nil {
return err
}
volumeDir, err := s.getVolumeDir(volume)
if err == nil {
// Volume already exists, return error.
return errVolumeExists
}
// If volume not found create it.
if err == errVolumeNotFound {
// Make a volume entry.
return os.Mkdir(volumeDir, 0700)
}
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("MakeVol failed with %s", err)
// For all other errors return here.
return err
}
// ListVols - list volumes.
func (s fsStorage) ListVols() (volsInfo []VolInfo, err error) {
// Get disk info to be populated for VolInfo.
var diskInfo disk.Info
diskInfo, err = disk.GetInfo(s.diskPath)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
}).Debugf("Failed to get disk info, %s", err)
return nil, err
}
volsInfo, err = getAllUniqueVols(s.diskPath)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
}).Debugf("getAllUniqueVols failed with %s", err)
return nil, err
}
for i, vol := range volsInfo {
// Volname on case sensitive fs backends can come in as
// capitalized, but object layer cannot consume it
// directly. Convert it as we see fit.
volName := strings.ToLower(vol.Name)
volInfo := VolInfo{
Name: volName,
Created: vol.Created,
Total: diskInfo.Total,
Free: diskInfo.Free,
FSType: diskInfo.FSType,
}
volsInfo[i] = volInfo
}
return volsInfo, nil
}
// StatVol - get volume info.
func (s fsStorage) StatVol(volume string) (volInfo VolInfo, err error) {
// Verify if volume is valid and it exists.
volumeDir, err := s.getVolumeDir(volume)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("getVolumeDir failed with %s", err)
return VolInfo{}, err
}
// Stat a volume entry.
var st os.FileInfo
st, err = os.Stat(volumeDir)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("Stat on the volume failed with %s", err)
if os.IsNotExist(err) {
return VolInfo{}, errVolumeNotFound
}
return VolInfo{}, err
}
// Get disk info, to be returned back along with volume info.
var diskInfo disk.Info
diskInfo, err = disk.GetInfo(s.diskPath)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("Failed to get disk info, %s", err)
return VolInfo{}, err
}
// As os.Stat() doesn't carry other than ModTime(), use ModTime()
// as CreatedTime.
createdTime := st.ModTime()
return VolInfo{
Name: volume,
Created: createdTime,
Free: diskInfo.Free,
Total: diskInfo.Total,
FSType: diskInfo.FSType,
}, nil
}
// DeleteVol - delete a volume.
func (s fsStorage) DeleteVol(volume string) error {
// Verify if volume is valid and it exists.
volumeDir, err := s.getVolumeDir(volume)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("getVolumeDir failed with %s", err)
return err
}
err = os.Remove(volumeDir)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("Volume remove failed with %s", err)
if os.IsNotExist(err) {
return errVolumeNotFound
} else if strings.Contains(err.Error(), "directory is not empty") {
2016-03-28 00:52:38 -04:00
// On windows the string is slightly different, handle it here.
return errVolumeNotEmpty
} else if strings.Contains(err.Error(), "directory not empty") {
// Hopefully for all other
// operating systems, this is
// assumed to be consistent.
return errVolumeNotEmpty
}
return err
}
2016-03-28 00:52:38 -04:00
return nil
}
// ListDir - return all the entries at the given directory path.
// If an entry is a directory it will be returned with a trailing "/".
func (s fsStorage) ListDir(volume, dirPath string) ([]string, error) {
// Verify if volume is valid and it exists.
volumeDir, err := s.getVolumeDir(volume)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("getVolumeDir failed with %s", err)
return nil, err
}
// Stat a volume entry.
_, err = os.Stat(volumeDir)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("Stat on the volume failed with %s", err)
if os.IsNotExist(err) {
return nil, errVolumeNotFound
}
return nil, err
}
return readDir(pathJoin(volumeDir, dirPath))
}
// ReadFile - read a file at a given offset.
func (s fsStorage) ReadFile(volume string, path string, offset int64) (readCloser io.ReadCloser, err error) {
volumeDir, err := s.getVolumeDir(volume)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("getVolumeDir failed with %s", err)
return nil, err
}
filePath := pathJoin(volumeDir, path)
file, err := os.Open(filePath)
if err != nil {
if os.IsNotExist(err) {
return nil, errFileNotFound
} else if os.IsPermission(err) {
return nil, errFileAccessDenied
}
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"filePath": filePath,
}).Debugf("Opening a file failed with %s", err)
return nil, err
}
st, err := file.Stat()
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"filePath": filePath,
}).Debugf("Stat failed with %s", err)
return nil, err
}
// Verify if its not a regular file, since subsequent Seek is undefined.
if !st.Mode().IsRegular() {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"filePath": filePath,
}).Debugf("Unexpected type %s", errIsNotRegular)
return nil, errFileNotFound
}
// Seek to requested offset.
_, err = file.Seek(offset, os.SEEK_SET)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"filePath": filePath,
"offset": offset,
}).Debugf("Seek failed with %s", err)
return nil, err
}
return file, nil
}
// CreateFile - create a file at path.
func (s fsStorage) CreateFile(volume, path string) (writeCloser io.WriteCloser, err error) {
volumeDir, err := s.getVolumeDir(volume)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("getVolumeDir failed with %s", err)
return nil, err
}
if err = checkDiskFree(s.diskPath, s.minFreeDisk); err != nil {
return nil, err
}
filePath := pathJoin(volumeDir, path)
// Verify if the file already exists and is not of regular type.
var st os.FileInfo
if st, err = os.Stat(filePath); err == nil {
if st.IsDir() {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"filePath": filePath,
}).Debugf("Unexpected type %s", errIsNotRegular)
return nil, errIsNotRegular
}
}
w, err := safe.CreateFileWithPrefix(filePath, "$tmpfile")
if err != nil {
// File path cannot be verified since one of the parents is a file.
if strings.Contains(err.Error(), "not a directory") {
return nil, errFileAccessDenied
}
return nil, err
}
return w, nil
}
// StatFile - get file info.
func (s fsStorage) StatFile(volume, path string) (file FileInfo, err error) {
volumeDir, err := s.getVolumeDir(volume)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("getVolumeDir failed with %s", err)
return FileInfo{}, err
}
filePath := pathJoin(volumeDir, path)
st, err := os.Stat(filePath)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"filePath": filePath,
}).Debugf("Stat failed with %s", err)
// File is really not found.
if os.IsNotExist(err) {
return FileInfo{}, errFileNotFound
}
// File path cannot be verified since one of the parents is a file.
if strings.Contains(err.Error(), "not a directory") {
return FileInfo{}, errFileNotFound
}
// Return all errors here.
return FileInfo{}, err
}
// If its a directory its not a regular file.
if st.Mode().IsDir() {
2016-04-25 02:12:54 -04:00
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"filePath": filePath,
}).Debugf("File is %s.", errIsNotRegular)
return FileInfo{}, errFileNotFound
}
return FileInfo{
Volume: volume,
Name: path,
ModTime: st.ModTime(),
Size: st.Size(),
Mode: st.Mode(),
}, nil
}
// deleteFile - delete file path if its empty.
func deleteFile(basePath, deletePath string) error {
if basePath == deletePath {
return nil
}
// Verify if the path exists.
pathSt, err := os.Stat(deletePath)
if err != nil {
log.WithFields(logrus.Fields{
"deletePath": deletePath,
}).Debugf("Stat failed with %s", err)
if os.IsNotExist(err) {
return errFileNotFound
} else if os.IsPermission(err) {
return errFileAccessDenied
}
return err
}
if pathSt.IsDir() {
// Verify if directory is empty.
empty, err := isDirEmpty(deletePath)
if err != nil {
log.WithFields(logrus.Fields{
"deletePath": deletePath,
}).Debugf("isDirEmpty failed with %s", err)
return err
}
if !empty {
return nil
}
}
// Attempt to remove path.
if err := os.Remove(deletePath); err != nil {
log.WithFields(logrus.Fields{
"deletePath": deletePath,
}).Debugf("Remove failed with %s", err)
return err
}
// Recursively go down the next path and delete again.
if err := deleteFile(basePath, path.Dir(deletePath)); err != nil {
log.WithFields(logrus.Fields{
"basePath": basePath,
"deleteDir": path.Dir(deletePath),
}).Debugf("deleteFile failed with %s", err)
return err
}
return nil
}
// DeleteFile - delete a file at path.
func (s fsStorage) DeleteFile(volume, path string) error {
volumeDir, err := s.getVolumeDir(volume)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": volume,
}).Debugf("getVolumeDir failed with %s", err)
return err
}
// Following code is needed so that we retain "/" suffix if any in
// path argument.
filePath := pathJoin(volumeDir, path)
// Delete file and delete parent directory as well if its empty.
return deleteFile(volumeDir, filePath)
}
// RenameFile - rename file.
func (s fsStorage) RenameFile(srcVolume, srcPath, dstVolume, dstPath string) error {
srcVolumeDir, err := s.getVolumeDir(srcVolume)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": srcVolume,
}).Errorf("getVolumeDir failed with %s", err)
return err
}
dstVolumeDir, err := s.getVolumeDir(dstVolume)
if err != nil {
log.WithFields(logrus.Fields{
"diskPath": s.diskPath,
"volume": dstVolume,
}).Errorf("getVolumeDir failed with %s", err)
return err
}
if err = os.MkdirAll(path.Join(dstVolumeDir, path.Dir(dstPath)), 0755); err != nil {
// File path cannot be verified since one of the parents is a file.
if strings.Contains(err.Error(), "not a directory") {
return errFileAccessDenied
}
log.Errorf("os.MkdirAll failed with %s", err)
return err
}
err = os.Rename(path.Join(srcVolumeDir, srcPath), path.Join(dstVolumeDir, dstPath))
if err != nil {
if os.IsNotExist(err) {
return errFileNotFound
}
log.Errorf("os.Rename failed with %s", err)
return err
}
return nil
}