fs: Re-implement object layer to remember the fd (#3509)
This patch re-writes FS backend to support shared backend sharing locks for safe concurrent access across multiple servers.master
parent
a054c73e22
commit
1c699d8d3f
@ -1,85 +0,0 @@ |
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"bytes" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"strings" |
||||
) |
||||
|
||||
const policyJSON = "policy.json" |
||||
|
||||
func getBucketFromPolicyPath(oldPolicyPath string) string { |
||||
bucketPrefix, _ := filepath.Split(oldPolicyPath) |
||||
_, bucketName := filepath.Split(strings.TrimSuffix(bucketPrefix, slashSeparator)) |
||||
return bucketName |
||||
} |
||||
|
||||
func cleanupOldBucketPolicyConfigs() error { |
||||
// Get old bucket policy config directory.
|
||||
oldBucketsConfigDir, err := getOldBucketsConfigPath() |
||||
fatalIf(err, "Unable to fetch buckets config path to migrate bucket policy") |
||||
|
||||
// Recursively remove configDir/buckets/ - old bucket policy config location.
|
||||
// N B This is called only if all bucket policies were successfully migrated.
|
||||
return os.RemoveAll(oldBucketsConfigDir) |
||||
} |
||||
|
||||
func migrateBucketPolicyConfig(objAPI ObjectLayer) error { |
||||
// Get old bucket policy config directory.
|
||||
oldBucketsConfigDir, err := getOldBucketsConfigPath() |
||||
fatalIf(err, "Unable to fetch buckets config path to migrate bucket policy") |
||||
|
||||
// Check if config directory holding bucket policy exists before
|
||||
// migration.
|
||||
_, err = os.Stat(oldBucketsConfigDir) |
||||
if os.IsNotExist(err) { |
||||
return nil |
||||
} |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// WalkFunc that migrates access-policy.json to
|
||||
// .minio.sys/buckets/bucketName/policy.json on all disks.
|
||||
migrateBucketPolicy := func(policyPath string, fileInfo os.FileInfo, err error) error { |
||||
// policyFile - e.g /configDir/sample-bucket/access-policy.json
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
// Skip entries that aren't bucket policy files.
|
||||
if fileInfo.Name() != "access-policy.json" { |
||||
return nil |
||||
} |
||||
// Get bucketName from old policy file path.
|
||||
bucketName := getBucketFromPolicyPath(policyPath) |
||||
// Read bucket policy config from old location.
|
||||
policyBytes, err := ioutil.ReadFile(policyPath) |
||||
fatalIf(err, "Unable to read bucket policy to migrate bucket policy", policyPath) |
||||
newPolicyPath := retainSlash(bucketConfigPrefix) + retainSlash(bucketName) + policyJSON |
||||
var metadata map[string]string |
||||
sha256sum := "" |
||||
// Erasure code the policy config to all the disks.
|
||||
_, err = objAPI.PutObject(minioMetaBucket, newPolicyPath, int64(len(policyBytes)), bytes.NewReader(policyBytes), metadata, sha256sum) |
||||
fatalIf(err, "Unable to write bucket policy during migration.", newPolicyPath) |
||||
return nil |
||||
} |
||||
return filepath.Walk(oldBucketsConfigDir, migrateBucketPolicy) |
||||
} |
@ -0,0 +1,41 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2017 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. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/minio/cli" |
||||
) |
||||
|
||||
// Tests register command function.
|
||||
func TestRegisterCommand(t *testing.T) { |
||||
registerCommand(cli.Command{ |
||||
Name: "test1", |
||||
}) |
||||
ccount := len(commands) |
||||
if ccount != 1 { |
||||
t.Fatalf("Unexpected number of commands found %d", ccount) |
||||
} |
||||
registerCommand(cli.Command{ |
||||
Name: "test2", |
||||
}) |
||||
ccount = len(commands) |
||||
if ccount != 2 { |
||||
t.Fatalf("Unexpected number of commands found %d", ccount) |
||||
} |
||||
} |
@ -0,0 +1,41 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2017 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. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"testing" |
||||
|
||||
router "github.com/gorilla/mux" |
||||
) |
||||
|
||||
// Test cross domain xml handler.
|
||||
func TestCrossXMLHandler(t *testing.T) { |
||||
// Server initialization.
|
||||
mux := router.NewRouter().SkipClean(true) |
||||
handler := setCrossDomainPolicy(mux) |
||||
srv := httptest.NewServer(handler) |
||||
|
||||
resp, err := http.Get(srv.URL + crossDomainXMLEntity) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if resp.StatusCode != http.StatusOK { |
||||
t.Fatal("Unexpected http status received", resp.Status) |
||||
} |
||||
} |
@ -1,91 +0,0 @@ |
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import "io" |
||||
|
||||
// Reads from the requested local location uses a staging buffer. Restricts
|
||||
// reads upto requested range of length and offset. If successful staging
|
||||
// buffer is written to the incoming stream. Returns errors if any.
|
||||
func fsReadFile(disk StorageAPI, bucket, object string, writer io.Writer, totalLeft, startOffset int64, buf []byte) (err error) { |
||||
bufSize := int64(len(buf)) |
||||
// Start the read loop until requested range.
|
||||
for { |
||||
// Figure out the right size for the buffer.
|
||||
curLeft := bufSize |
||||
if totalLeft < bufSize { |
||||
curLeft = totalLeft |
||||
} |
||||
// Reads the file at offset.
|
||||
nr, er := disk.ReadFile(bucket, object, startOffset, buf[:curLeft]) |
||||
if nr > 0 { |
||||
// Write to response writer.
|
||||
nw, ew := writer.Write(buf[0:nr]) |
||||
if nw > 0 { |
||||
// Decrement whats left to write.
|
||||
totalLeft -= int64(nw) |
||||
|
||||
// Progress the offset
|
||||
startOffset += int64(nw) |
||||
} |
||||
if ew != nil { |
||||
err = traceError(ew) |
||||
break |
||||
} |
||||
if nr != int64(nw) { |
||||
err = traceError(io.ErrShortWrite) |
||||
break |
||||
} |
||||
} |
||||
if er == io.EOF || er == io.ErrUnexpectedEOF { |
||||
break |
||||
} |
||||
if er != nil { |
||||
err = traceError(er) |
||||
break |
||||
} |
||||
if totalLeft == 0 { |
||||
break |
||||
} |
||||
} |
||||
return err |
||||
} |
||||
|
||||
// Reads from input stream until end of file, takes an input buffer for staging reads.
|
||||
// The staging buffer is then written to the disk. Returns for any error that occurs
|
||||
// while reading the stream or writing to disk. Caller should cleanup partial files.
|
||||
// Upon errors total data written will be 0 and returns error, on success returns
|
||||
// total data written to disk.
|
||||
func fsCreateFile(disk StorageAPI, reader io.Reader, buf []byte, bucket, object string) (int64, error) { |
||||
bytesWritten := int64(0) |
||||
// Read the buffer till io.EOF and appends data to path at bucket/object.
|
||||
for { |
||||
n, rErr := reader.Read(buf) |
||||
if rErr != nil && rErr != io.EOF { |
||||
return 0, traceError(rErr) |
||||
} |
||||
bytesWritten += int64(n) |
||||
wErr := disk.AppendFile(bucket, object, buf[0:n]) |
||||
if wErr != nil { |
||||
return 0, traceError(wErr) |
||||
} |
||||
if rErr == io.EOF { |
||||
break |
||||
} |
||||
} |
||||
return bytesWritten, nil |
||||
} |
@ -0,0 +1,373 @@ |
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"io" |
||||
"os" |
||||
pathutil "path" |
||||
) |
||||
|
||||
// Removes only the file at given path does not remove
|
||||
// any parent directories, handles long paths for
|
||||
// windows automatically.
|
||||
func fsRemoveFile(filePath string) (err error) { |
||||
if filePath == "" { |
||||
return errInvalidArgument |
||||
} |
||||
|
||||
if err = checkPathLength(filePath); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = os.Remove(preparePath(filePath)); err != nil { |
||||
if os.IsNotExist(err) { |
||||
return errFileNotFound |
||||
} else if os.IsPermission(err) { |
||||
return errFileAccessDenied |
||||
} |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Removes all files and folders at a given path, handles
|
||||
// long paths for windows automatically.
|
||||
func fsRemoveAll(dirPath string) (err error) { |
||||
if dirPath == "" { |
||||
return errInvalidArgument |
||||
} |
||||
|
||||
if err = checkPathLength(dirPath); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = removeAll(dirPath); err != nil { |
||||
if os.IsPermission(err) { |
||||
return errVolumeAccessDenied |
||||
} |
||||
} |
||||
|
||||
return err |
||||
|
||||
} |
||||
|
||||
// Removes a directory only if its empty, handles long
|
||||
// paths for windows automatically.
|
||||
func fsRemoveDir(dirPath string) (err error) { |
||||
if dirPath == "" { |
||||
return errInvalidArgument |
||||
} |
||||
|
||||
if err = checkPathLength(dirPath); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = os.Remove(preparePath(dirPath)); err != nil { |
||||
if os.IsNotExist(err) { |
||||
return errVolumeNotFound |
||||
} else if isSysErrNotEmpty(err) { |
||||
return errVolumeNotEmpty |
||||
} |
||||
} |
||||
|
||||
return err |
||||
} |
||||
|
||||
// Creates a new directory, parent dir should exist
|
||||
// otherwise returns an error. If directory already
|
||||
// exists returns an error. Windows long paths
|
||||
// are handled automatically.
|
||||
func fsMkdir(dirPath string) (err error) { |
||||
if dirPath == "" { |
||||
return errInvalidArgument |
||||
} |
||||
|
||||
if err = checkPathLength(dirPath); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err = os.Mkdir(preparePath(dirPath), 0777); err != nil { |
||||
if os.IsExist(err) { |
||||
return errVolumeExists |
||||
} else if os.IsPermission(err) { |
||||
return errDiskAccessDenied |
||||
} else if isSysErrNotDir(err) { |
||||
// File path cannot be verified since
|
||||
// one of the parents is a file.
|
||||
return errDiskAccessDenied |
||||
} else if isSysErrPathNotFound(err) { |
||||
// Add specific case for windows.
|
||||
return errDiskAccessDenied |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Lookup if directory exists, returns directory
|
||||
// attributes upon success.
|
||||
func fsStatDir(statDir string) (os.FileInfo, error) { |
||||
if statDir == "" { |
||||
return nil, errInvalidArgument |
||||
} |
||||
if err := checkPathLength(statDir); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
fi, err := os.Stat(preparePath(statDir)) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
return nil, errVolumeNotFound |
||||
} else if os.IsPermission(err) { |
||||
return nil, errVolumeAccessDenied |
||||
} |
||||
return nil, err |
||||
} |
||||
|
||||
if !fi.IsDir() { |
||||
return nil, errVolumeAccessDenied |
||||
} |
||||
|
||||
return fi, nil |
||||
} |
||||
|
||||
// Lookup if file exists, returns file attributes upon success
|
||||
func fsStatFile(statFile string) (os.FileInfo, error) { |
||||
if statFile == "" { |
||||
return nil, errInvalidArgument |
||||
} |
||||
|
||||
if err := checkPathLength(statFile); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
fi, err := os.Stat(preparePath(statFile)) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
return nil, errFileNotFound |
||||
} else if os.IsPermission(err) { |
||||
return nil, errFileAccessDenied |
||||
} else if isSysErrNotDir(err) { |
||||
return nil, errFileAccessDenied |
||||
} else if isSysErrPathNotFound(err) { |
||||
return nil, errFileNotFound |
||||
} |
||||
return nil, err |
||||
} |
||||
if fi.IsDir() { |
||||
return nil, errFileNotFound |
||||
} |
||||
return fi, nil |
||||
} |
||||
|
||||
// Opens the file at given path, optionally from an offset. Upon success returns
|
||||
// a readable stream and the size of the readable stream.
|
||||
func fsOpenFile(readPath string, offset int64) (io.ReadCloser, int64, error) { |
||||
if readPath == "" || offset < 0 { |
||||
return nil, 0, errInvalidArgument |
||||
} |
||||
if err := checkPathLength(readPath); err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
fr, err := os.Open(preparePath(readPath)) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
return nil, 0, errFileNotFound |
||||
} else if os.IsPermission(err) { |
||||
return nil, 0, errFileAccessDenied |
||||
} else if isSysErrNotDir(err) { |
||||
// File path cannot be verified since one of the parents is a file.
|
||||
return nil, 0, errFileAccessDenied |
||||
} else if isSysErrPathNotFound(err) { |
||||
// Add specific case for windows.
|
||||
return nil, 0, errFileNotFound |
||||
} |
||||
return nil, 0, err |
||||
} |
||||
|
||||
// Stat to get the size of the file at path.
|
||||
st, err := fr.Stat() |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
// Verify if its not a regular file, since subsequent Seek is undefined.
|
||||
if !st.Mode().IsRegular() { |
||||
return nil, 0, errIsNotRegular |
||||
} |
||||
|
||||
// Seek to the requested offset.
|
||||
if offset > 0 { |
||||
_, err = fr.Seek(offset, os.SEEK_SET) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
} |
||||
|
||||
// Success.
|
||||
return fr, st.Size(), nil |
||||
} |
||||
|
||||
// Creates a file and copies data from incoming reader. Staging buffer is used by io.CopyBuffer.
|
||||
func fsCreateFile(tempObjPath string, reader io.Reader, buf []byte, fallocSize int64) (int64, error) { |
||||
if tempObjPath == "" || reader == nil || buf == nil { |
||||
return 0, errInvalidArgument |
||||
} |
||||
|
||||
if err := checkPathLength(tempObjPath); err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
if err := mkdirAll(pathutil.Dir(tempObjPath), 0777); err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
writer, err := os.OpenFile(preparePath(tempObjPath), os.O_CREATE|os.O_WRONLY, 0666) |
||||
if err != nil { |
||||
// File path cannot be verified since one of the parents is a file.
|
||||
if isSysErrNotDir(err) { |
||||
return 0, errFileAccessDenied |
||||
} |
||||
return 0, err |
||||
} |
||||
defer writer.Close() |
||||
|
||||
// Fallocate only if the size is final object is known.
|
||||
if fallocSize > 0 { |
||||
if err = fsFAllocate(int(writer.Fd()), 0, fallocSize); err != nil { |
||||
return 0, err |
||||
} |
||||
} |
||||
|
||||
bytesWritten, err := io.CopyBuffer(writer, reader, buf) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
return bytesWritten, nil |
||||
} |
||||
|
||||
// Removes uploadID at destination path.
|
||||
func fsRemoveUploadIDPath(basePath, uploadIDPath string) error { |
||||
if basePath == "" || uploadIDPath == "" { |
||||
return errInvalidArgument |
||||
} |
||||
|
||||
// List all the entries in uploadID.
|
||||
entries, err := readDir(uploadIDPath) |
||||
if err != nil && err != errFileNotFound { |
||||
return err |
||||
} |
||||
|
||||
// Delete all the entries obtained from previous readdir.
|
||||
for _, entryPath := range entries { |
||||
err = fsDeleteFile(basePath, pathJoin(uploadIDPath, entryPath)) |
||||
if err != nil && err != errFileNotFound { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// fsFAllocate is similar to Fallocate but provides a convenient
|
||||
// wrapper to handle various operating system specific errors.
|
||||
func fsFAllocate(fd int, offset int64, len int64) (err error) { |
||||
e := Fallocate(fd, offset, len) |
||||
// Ignore errors when Fallocate is not supported in the current system
|
||||
if e != nil && !isSysErrNoSys(e) && !isSysErrOpNotSupported(e) { |
||||
switch { |
||||
case isSysErrNoSpace(e): |
||||
err = errDiskFull |
||||
case isSysErrIO(e): |
||||
err = e |
||||
default: |
||||
// For errors: EBADF, EINTR, EINVAL, ENODEV, EPERM, ESPIPE and ETXTBSY
|
||||
// Appending was failed anyway, returns unexpected error
|
||||
err = errUnexpected |
||||
} |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Renames source path to destination path, creates all the
|
||||
// missing parents if they don't exist.
|
||||
func fsRenameFile(sourcePath, destPath string) error { |
||||
if err := mkdirAll(pathutil.Dir(destPath), 0777); err != nil { |
||||
return traceError(err) |
||||
} |
||||
if err := os.Rename(preparePath(sourcePath), preparePath(destPath)); err != nil { |
||||
return traceError(err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Delete a file and its parent if it is empty at the destination path.
|
||||
// this function additionally protects the basePath from being deleted.
|
||||
func fsDeleteFile(basePath, deletePath string) error { |
||||
if err := checkPathLength(basePath); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := checkPathLength(deletePath); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if basePath == deletePath { |
||||
return nil |
||||
} |
||||
|
||||
// Verify if the path exists.
|
||||
pathSt, err := os.Stat(preparePath(deletePath)) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
return errFileNotFound |
||||
} else if os.IsPermission(err) { |
||||
return errFileAccessDenied |
||||
} |
||||
return err |
||||
} |
||||
|
||||
if pathSt.IsDir() && !isDirEmpty(deletePath) { |
||||
// Verify if directory is empty.
|
||||
return nil |
||||
} |
||||
|
||||
// Attempt to remove path.
|
||||
if err = os.Remove(preparePath(deletePath)); err != nil { |
||||
if os.IsNotExist(err) { |
||||
return errFileNotFound |
||||
} else if os.IsPermission(err) { |
||||
return errFileAccessDenied |
||||
} else if isSysErrNotEmpty(err) { |
||||
return errVolumeNotEmpty |
||||
} |
||||
return err |
||||
} |
||||
|
||||
// Recursively go down the next path and delete again.
|
||||
if err := fsDeleteFile(basePath, pathutil.Dir(deletePath)); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,405 @@ |
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"bytes" |
||||
"runtime" |
||||
"testing" |
||||
) |
||||
|
||||
func TestFSStats(t *testing.T) { |
||||
// create posix test setup
|
||||
_, path, err := newPosixTestSetup() |
||||
if err != nil { |
||||
t.Fatalf("Unable to create posix test setup, %s", err) |
||||
} |
||||
defer removeAll(path) |
||||
|
||||
// Setup test environment.
|
||||
|
||||
if err = fsMkdir(""); err != errInvalidArgument { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
|
||||
if err = fsMkdir(pathJoin(path, "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001")); err != errFileNameTooLong { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
|
||||
if err = fsMkdir(pathJoin(path, "success-vol")); err != nil { |
||||
t.Fatalf("Unable to create volume, %s", err) |
||||
} |
||||
|
||||
var buf = make([]byte, 4096) |
||||
var reader = bytes.NewReader([]byte("Hello, world")) |
||||
if _, err = fsCreateFile(pathJoin(path, "success-vol", "success-file"), reader, buf, reader.Size()); err != nil { |
||||
t.Fatalf("Unable to create file, %s", err) |
||||
} |
||||
// Seek back.
|
||||
reader.Seek(0, 0) |
||||
|
||||
if err = fsMkdir(pathJoin(path, "success-vol", "success-file")); err != errVolumeExists { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
|
||||
if _, err = fsCreateFile(pathJoin(path, "success-vol", "path/to/success-file"), reader, buf, reader.Size()); err != nil { |
||||
t.Fatalf("Unable to create file, %s", err) |
||||
} |
||||
// Seek back.
|
||||
reader.Seek(0, 0) |
||||
|
||||
testCases := []struct { |
||||
srcFSPath string |
||||
srcVol string |
||||
srcPath string |
||||
expectedErr error |
||||
}{ |
||||
// Test case - 1.
|
||||
// Test case with valid inputs, expected to pass.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
srcPath: "success-file", |
||||
expectedErr: nil, |
||||
}, |
||||
// Test case - 2.
|
||||
// Test case with valid inputs, expected to pass.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
srcPath: "path/to/success-file", |
||||
expectedErr: nil, |
||||
}, |
||||
// Test case - 3.
|
||||
// Test case with non-existent file.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
srcPath: "nonexistent-file", |
||||
expectedErr: errFileNotFound, |
||||
}, |
||||
// Test case - 4.
|
||||
// Test case with non-existent file path.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
srcPath: "path/2/success-file", |
||||
expectedErr: errFileNotFound, |
||||
}, |
||||
// Test case - 5.
|
||||
// Test case with path being a directory.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
srcPath: "path", |
||||
expectedErr: errFileNotFound, |
||||
}, |
||||
// Test case - 6.
|
||||
// Test case with src path segment > 255.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
srcPath: "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", |
||||
expectedErr: errFileNameTooLong, |
||||
}, |
||||
// Test case - 7.
|
||||
// Test case validate only srcVol exists.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
expectedErr: nil, |
||||
}, |
||||
// Test case - 8.
|
||||
// Test case validate only srcVol doesn't exist.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol-non-existent", |
||||
expectedErr: errVolumeNotFound, |
||||
}, |
||||
// Test case - 9.
|
||||
// Test case validate invalid argument.
|
||||
{ |
||||
expectedErr: errInvalidArgument, |
||||
}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
if testCase.srcPath != "" { |
||||
if _, err := fsStatFile(pathJoin(testCase.srcFSPath, testCase.srcVol, testCase.srcPath)); err != testCase.expectedErr { |
||||
t.Fatalf("TestPosix case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err) |
||||
} |
||||
} else { |
||||
if _, err := fsStatDir(pathJoin(testCase.srcFSPath, testCase.srcVol)); err != testCase.expectedErr { |
||||
t.Fatalf("TestPosix case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestFSCreateAndOpen(t *testing.T) { |
||||
// Setup test environment.
|
||||
_, path, err := newPosixTestSetup() |
||||
if err != nil { |
||||
t.Fatalf("Unable to create posix test setup, %s", err) |
||||
} |
||||
defer removeAll(path) |
||||
|
||||
if err = fsMkdir(pathJoin(path, "success-vol")); err != nil { |
||||
t.Fatalf("Unable to create directory, %s", err) |
||||
} |
||||
|
||||
if _, err = fsCreateFile("", nil, nil, 0); err != errInvalidArgument { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
|
||||
if _, _, err = fsOpenFile("", -1); err != errInvalidArgument { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
|
||||
var buf = make([]byte, 4096) |
||||
var reader = bytes.NewReader([]byte("Hello, world")) |
||||
if _, err = fsCreateFile(pathJoin(path, "success-vol", "success-file"), reader, buf, reader.Size()); err != nil { |
||||
t.Fatalf("Unable to create file, %s", err) |
||||
} |
||||
// Seek back.
|
||||
reader.Seek(0, 0) |
||||
|
||||
testCases := []struct { |
||||
srcVol string |
||||
srcPath string |
||||
expectedErr error |
||||
}{ |
||||
// Test case - 1.
|
||||
// Test case with segment of the volume name > 255.
|
||||
{ |
||||
srcVol: "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", |
||||
srcPath: "success-file", |
||||
expectedErr: errFileNameTooLong, |
||||
}, |
||||
// Test case - 2.
|
||||
// Test case with src path segment > 255.
|
||||
{ |
||||
srcVol: "success-vol", |
||||
srcPath: "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", |
||||
expectedErr: errFileNameTooLong, |
||||
}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
_, err = fsCreateFile(pathJoin(path, testCase.srcVol, testCase.srcPath), reader, buf, reader.Size()) |
||||
if err != testCase.expectedErr { |
||||
t.Errorf("Test case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err) |
||||
} |
||||
_, _, err = fsOpenFile(pathJoin(path, testCase.srcVol, testCase.srcPath), 0) |
||||
if err != testCase.expectedErr { |
||||
t.Errorf("Test case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err) |
||||
} |
||||
} |
||||
|
||||
// Attempt to open a directory.
|
||||
if _, _, err = fsOpenFile(pathJoin(path), 0); err != errIsNotRegular { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
} |
||||
|
||||
func TestFSDeletes(t *testing.T) { |
||||
// create posix test setup
|
||||
_, path, err := newPosixTestSetup() |
||||
if err != nil { |
||||
t.Fatalf("Unable to create posix test setup, %s", err) |
||||
} |
||||
defer removeAll(path) |
||||
|
||||
// Setup test environment.
|
||||
if err = fsMkdir(pathJoin(path, "success-vol")); err != nil { |
||||
t.Fatalf("Unable to create directory, %s", err) |
||||
} |
||||
|
||||
var buf = make([]byte, 4096) |
||||
var reader = bytes.NewReader([]byte("Hello, world")) |
||||
if _, err = fsCreateFile(pathJoin(path, "success-vol", "success-file"), reader, buf, reader.Size()); err != nil { |
||||
t.Fatalf("Unable to create file, %s", err) |
||||
} |
||||
// Seek back.
|
||||
reader.Seek(0, 0) |
||||
|
||||
testCases := []struct { |
||||
srcVol string |
||||
srcPath string |
||||
expectedErr error |
||||
}{ |
||||
// Test case - 1.
|
||||
// valid case with existing volume and file to delete.
|
||||
{ |
||||
srcVol: "success-vol", |
||||
srcPath: "success-file", |
||||
expectedErr: nil, |
||||
}, |
||||
// Test case - 2.
|
||||
// The file was deleted in the last case, so DeleteFile should fail.
|
||||
{ |
||||
srcVol: "success-vol", |
||||
srcPath: "success-file", |
||||
expectedErr: errFileNotFound, |
||||
}, |
||||
// Test case - 3.
|
||||
// Test case with segment of the volume name > 255.
|
||||
{ |
||||
srcVol: "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", |
||||
srcPath: "success-file", |
||||
expectedErr: errFileNameTooLong, |
||||
}, |
||||
// Test case - 4.
|
||||
// Test case with src path segment > 255.
|
||||
{ |
||||
srcVol: "success-vol", |
||||
srcPath: "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", |
||||
expectedErr: errFileNameTooLong, |
||||
}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
if err = fsDeleteFile(path, pathJoin(path, testCase.srcVol, testCase.srcPath)); err != testCase.expectedErr { |
||||
t.Errorf("Test case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Tests fs removes.
|
||||
func TestFSRemoves(t *testing.T) { |
||||
// create posix test setup
|
||||
_, path, err := newPosixTestSetup() |
||||
if err != nil { |
||||
t.Fatalf("Unable to create posix test setup, %s", err) |
||||
} |
||||
defer removeAll(path) |
||||
|
||||
// Setup test environment.
|
||||
if err = fsMkdir(pathJoin(path, "success-vol")); err != nil { |
||||
t.Fatalf("Unable to create directory, %s", err) |
||||
} |
||||
|
||||
var buf = make([]byte, 4096) |
||||
var reader = bytes.NewReader([]byte("Hello, world")) |
||||
if _, err = fsCreateFile(pathJoin(path, "success-vol", "success-file"), reader, buf, reader.Size()); err != nil { |
||||
t.Fatalf("Unable to create file, %s", err) |
||||
} |
||||
// Seek back.
|
||||
reader.Seek(0, 0) |
||||
|
||||
if _, err = fsCreateFile(pathJoin(path, "success-vol", "success-file-new"), reader, buf, reader.Size()); err != nil { |
||||
t.Fatalf("Unable to create file, %s", err) |
||||
} |
||||
// Seek back.
|
||||
reader.Seek(0, 0) |
||||
|
||||
testCases := []struct { |
||||
srcFSPath string |
||||
srcVol string |
||||
srcPath string |
||||
expectedErr error |
||||
}{ |
||||
// Test case - 1.
|
||||
// valid case with existing volume and file to delete.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
srcPath: "success-file", |
||||
expectedErr: nil, |
||||
}, |
||||
// Test case - 2.
|
||||
// The file was deleted in the last case, so DeleteFile should fail.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
srcPath: "success-file", |
||||
expectedErr: errFileNotFound, |
||||
}, |
||||
// Test case - 3.
|
||||
// Test case with segment of the volume name > 255.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", |
||||
srcPath: "success-file", |
||||
expectedErr: errFileNameTooLong, |
||||
}, |
||||
// Test case - 4.
|
||||
// Test case with src path segment > 255.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
srcPath: "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", |
||||
expectedErr: errFileNameTooLong, |
||||
}, |
||||
// Test case - 5.
|
||||
// Test case with src path empty.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "success-vol", |
||||
expectedErr: errVolumeNotEmpty, |
||||
}, |
||||
// Test case - 6.
|
||||
// Test case with src path empty.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", |
||||
expectedErr: errFileNameTooLong, |
||||
}, |
||||
// Test case - 7.
|
||||
// Test case with src path empty.
|
||||
{ |
||||
srcFSPath: path, |
||||
srcVol: "non-existent", |
||||
expectedErr: errVolumeNotFound, |
||||
}, |
||||
// Test case - 8.
|
||||
// Test case with src and volume path empty.
|
||||
{ |
||||
expectedErr: errInvalidArgument, |
||||
}, |
||||
} |
||||
|
||||
for i, testCase := range testCases { |
||||
if testCase.srcPath != "" { |
||||
if err = fsRemoveFile(pathJoin(testCase.srcFSPath, testCase.srcVol, testCase.srcPath)); err != testCase.expectedErr { |
||||
t.Errorf("Test case %d: Expected: \"%s\", got: \"%s\"", i+1, testCase.expectedErr, err) |
||||
} |
||||
} else { |
||||
if err = fsRemoveDir(pathJoin(testCase.srcFSPath, testCase.srcVol, testCase.srcPath)); err != testCase.expectedErr { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
if err = fsRemoveAll(pathJoin(path, "success-vol")); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if err = fsRemoveAll(""); err != errInvalidArgument { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if err = fsRemoveAll("my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"); err != errFileNameTooLong { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if runtime.GOOS != "windows" { |
||||
if err = fsRemoveAll("/usr"); err != errVolumeAccessDenied { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,193 @@ |
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"os" |
||||
pathutil "path" |
||||
"sync" |
||||
|
||||
"github.com/minio/minio/pkg/lock" |
||||
) |
||||
|
||||
// fsIOPool represents a protected list to keep track of all
|
||||
// the concurrent readers at a given path.
|
||||
type fsIOPool struct { |
||||
sync.Mutex |
||||
readersMap map[string]*lock.RLockedFile |
||||
} |
||||
|
||||
// Open is a wrapper call to read locked file which
|
||||
// returns a ReadAtCloser.
|
||||
//
|
||||
// ReaderAt is provided so that the fd is non seekable, since
|
||||
// we are sharing fd's with concurrent threads, we don't want
|
||||
// all readers to change offsets on each other during such
|
||||
// concurrent operations. Using ReadAt allows us to read from
|
||||
// any offsets.
|
||||
//
|
||||
// Closer is implemented to track total readers and to close
|
||||
// only when there no more readers, the fd is purged if the lock
|
||||
// count has reached zero.
|
||||
func (fsi *fsIOPool) Open(path string) (*lock.RLockedFile, error) { |
||||
if err := checkPathLength(path); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
fsi.Lock() |
||||
rlkFile, ok := fsi.readersMap[path] |
||||
|
||||
// File reference exists on map, validate if its
|
||||
// really closed and we are safe to purge it.
|
||||
if ok && rlkFile != nil { |
||||
// If the file is closed and not removed from map is a bug.
|
||||
if rlkFile.IsClosed() { |
||||
// Log this as an error.
|
||||
errorIf(errUnexpected, "Unexpected entry found on the map %s", path) |
||||
|
||||
// Purge the cached lock path from map.
|
||||
delete(fsi.readersMap, path) |
||||
|
||||
// Indicate that we can populate the new fd.
|
||||
ok = false |
||||
} else { |
||||
// Increment the lock ref, since the file is not closed yet
|
||||
// and caller requested to read the file again.
|
||||
rlkFile.IncLockRef() |
||||
} |
||||
} |
||||
fsi.Unlock() |
||||
|
||||
// Locked path reference doesn't exist, freshly open the file in
|
||||
// read lock mode.
|
||||
if !ok { |
||||
var err error |
||||
// Open file for reading.
|
||||
rlkFile, err = lock.RLockedOpenFile(preparePath(path)) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
return nil, errFileNotFound |
||||
} else if os.IsPermission(err) { |
||||
return nil, errFileAccessDenied |
||||
} else if isSysErrIsDir(err) { |
||||
return nil, errIsNotRegular |
||||
} else if isSysErrNotDir(err) { |
||||
return nil, errFileAccessDenied |
||||
} else if isSysErrPathNotFound(err) { |
||||
return nil, errFileNotFound |
||||
} |
||||
return nil, err |
||||
} |
||||
|
||||
// Save new reader on the map.
|
||||
fsi.Lock() |
||||
fsi.readersMap[path] = rlkFile |
||||
fsi.Unlock() |
||||
} |
||||
|
||||
// Success.
|
||||
return rlkFile, nil |
||||
} |
||||
|
||||
// Write - Attempt to lock the file if it exists,
|
||||
// - if the file exists. Then we try to get a write lock this
|
||||
// will block if we can't get a lock perhaps another write
|
||||
// or read is in progress. Concurrent calls are protected
|
||||
// by the global namspace lock within the same process.
|
||||
func (fsi *fsIOPool) Write(path string) (wlk *lock.LockedFile, err error) { |
||||
if err = checkPathLength(path); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
wlk, err = lock.LockedOpenFile(preparePath(path), os.O_RDWR, 0666) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
return nil, errFileNotFound |
||||
} else if os.IsPermission(err) { |
||||
return nil, errFileAccessDenied |
||||
} else if isSysErrIsDir(err) { |
||||
return nil, errIsNotRegular |
||||
} |
||||
return nil, err |
||||
} |
||||
return wlk, nil |
||||
} |
||||
|
||||
// Create - creates a new write locked file instance.
|
||||
// - if the file doesn't exist. We create the file and hold lock.
|
||||
func (fsi *fsIOPool) Create(path string) (wlk *lock.LockedFile, err error) { |
||||
if err = checkPathLength(path); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// Creates parent if missing.
|
||||
if err = mkdirAll(pathutil.Dir(path), 0777); err != nil { |
||||
if os.IsPermission(err) { |
||||
return nil, errFileAccessDenied |
||||
} else if isSysErrNotDir(err) { |
||||
return nil, errFileAccessDenied |
||||
} |
||||
return nil, err |
||||
} |
||||
|
||||
// Attempt to create the file.
|
||||
wlk, err = lock.LockedOpenFile(preparePath(path), os.O_RDWR|os.O_CREATE, 0666) |
||||
if err != nil { |
||||
if os.IsPermission(err) { |
||||
return nil, errFileAccessDenied |
||||
} else if isSysErrIsDir(err) { |
||||
return nil, errIsNotRegular |
||||
} else if isSysErrPathNotFound(err) { |
||||
return nil, errFileAccessDenied |
||||
} |
||||
return nil, err |
||||
} |
||||
|
||||
// Success.
|
||||
return wlk, err |
||||
} |
||||
|
||||
// Close implements closing the path referenced by the reader in such
|
||||
// a way that it makes sure to remove entry from the map immediately
|
||||
// if no active readers are present.
|
||||
func (fsi *fsIOPool) Close(path string) error { |
||||
fsi.Lock() |
||||
defer fsi.Unlock() |
||||
|
||||
if err := checkPathLength(path); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Pop readers from path.
|
||||
rlkFile, ok := fsi.readersMap[path] |
||||
if !ok { |
||||
return nil |
||||
} |
||||
|
||||
// Close the reader.
|
||||
rlkFile.Close() |
||||
|
||||
// If the file is closed, remove it from the reader pool map.
|
||||
if rlkFile.IsClosed() { |
||||
|
||||
// Purge the cached lock path from map.
|
||||
delete(fsi.readersMap, path) |
||||
} |
||||
|
||||
// Success.
|
||||
return nil |
||||
} |
@ -0,0 +1,112 @@ |
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"runtime" |
||||
"testing" |
||||
|
||||
"github.com/minio/minio/pkg/lock" |
||||
) |
||||
|
||||
// Tests long path calls.
|
||||
func TestRWPoolLongPath(t *testing.T) { |
||||
rwPool := &fsIOPool{ |
||||
readersMap: make(map[string]*lock.RLockedFile), |
||||
} |
||||
|
||||
longPath := "my-obj-del-0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001" |
||||
if _, err := rwPool.Create(longPath); err != errFileNameTooLong { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if _, err := rwPool.Write(longPath); err != errFileNameTooLong { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if _, err := rwPool.Open(longPath); err != errFileNameTooLong { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
// Tests all RWPool methods.
|
||||
func TestRWPool(t *testing.T) { |
||||
// create posix test setup
|
||||
_, path, err := newPosixTestSetup() |
||||
if err != nil { |
||||
t.Fatalf("Unable to create posix test setup, %s", err) |
||||
} |
||||
defer removeAll(path) |
||||
|
||||
rwPool := &fsIOPool{ |
||||
readersMap: make(map[string]*lock.RLockedFile), |
||||
} |
||||
wlk, err := rwPool.Create(pathJoin(path, "success-vol", "file/path/1.txt")) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
wlk.Close() |
||||
|
||||
// Fails to create a parent directory if there is a file.
|
||||
_, err = rwPool.Create(pathJoin(path, "success-vol", "file/path/1.txt/test")) |
||||
if err != errFileAccessDenied { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
|
||||
// Fails to create a file if there is a directory.
|
||||
_, err = rwPool.Create(pathJoin(path, "success-vol", "file")) |
||||
if runtime.GOOS == "windows" { |
||||
if err != errFileAccessDenied { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
} else { |
||||
if err != errIsNotRegular { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
} |
||||
|
||||
rlk, err := rwPool.Open(pathJoin(path, "success-vol", "file/path/1.txt")) |
||||
if err != nil { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
rlk.Close() |
||||
|
||||
// Fails to read a directory.
|
||||
_, err = rwPool.Open(pathJoin(path, "success-vol", "file")) |
||||
if runtime.GOOS == "windows" { |
||||
if err != errFileAccessDenied { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
} else { |
||||
if err != errIsNotRegular { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
} |
||||
|
||||
// Fails to open a file which has a parent as file.
|
||||
_, err = rwPool.Open(pathJoin(path, "success-vol", "file/path/1.txt/test")) |
||||
if runtime.GOOS != "windows" { |
||||
if err != errFileAccessDenied { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
} else { |
||||
if err != errFileNotFound { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,68 @@ |
||||
/* |
||||
* Minio Cloud Storage, (C) 2017 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. |
||||
*/ |
||||
|
||||
package cmd |
||||
|
||||
import ( |
||||
"fmt" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/minio/dsync" |
||||
) |
||||
|
||||
// Tests lock rpc client.
|
||||
func TestLockRPCClient(t *testing.T) { |
||||
lkClient := newLockRPCClient(authConfig{ |
||||
accessKey: "abcd", |
||||
secretKey: "abcd123", |
||||
serverAddr: fmt.Sprintf("%X", time.Now().UTC().UnixNano()), |
||||
serviceEndpoint: pathJoin(lockRPCPath, "/test/1"), |
||||
secureConn: false, |
||||
serviceName: "Dsync", |
||||
}) |
||||
|
||||
// Attempt all calls.
|
||||
_, err := lkClient.RLock(dsync.LockArgs{}) |
||||
if err == nil { |
||||
t.Fatal("Expected for Rlock to fail") |
||||
} |
||||
|
||||
_, err = lkClient.Lock(dsync.LockArgs{}) |
||||
if err == nil { |
||||
t.Fatal("Expected for Lock to fail") |
||||
} |
||||
|
||||
_, err = lkClient.RUnlock(dsync.LockArgs{}) |
||||
if err == nil { |
||||
t.Fatal("Expected for RUnlock to fail") |
||||
} |
||||
|
||||
_, err = lkClient.Unlock(dsync.LockArgs{}) |
||||
if err == nil { |
||||
t.Fatal("Expected for Unlock to fail") |
||||
} |
||||
|
||||
_, err = lkClient.ForceUnlock(dsync.LockArgs{}) |
||||
if err == nil { |
||||
t.Fatal("Expected for ForceUnlock to fail") |
||||
} |
||||
|
||||
_, err = lkClient.Expired(dsync.LockArgs{}) |
||||
if err == nil { |
||||
t.Fatal("Expected for Expired to fail") |
||||
} |
||||
} |
@ -0,0 +1,137 @@ |
||||
Introduction [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) |
||||
------------ |
||||
|
||||
This feature allows Minio to serve a shared NAS drive across multiple Minio instances. There are no special configuration changes required to enable this feature. Access to files stored on NAS volume are locked and synchronized by default. |
||||
|
||||
Motivation |
||||
---------- |
||||
|
||||
Since Minio instances serve the purpose of a single tenant there is an increasing requirement where users want to run multiple Minio instances on a same backend which is managed by an existing NAS (NFS, GlusterFS, Other distributed filesystems) rather than a local disk. This feature is implemented also with minimal disruption in mind for the user and overall UI. |
||||
|
||||
Restrictions |
||||
------------ |
||||
|
||||
* A PutObject() is blocked and waits if another GetObject() is in progress. |
||||
* A CompleteMultipartUpload() is blocked and waits if another PutObject() or GetObject() is in progress. |
||||
* Cannot run FS mode as a remote disk RPC. |
||||
|
||||
## How To Run? |
||||
|
||||
Running Minio instances on shared backend is no different than running on a stand-alone disk. There are no special configuration changes required to enable this feature. Access to files stored on NAS volume are locked and synchronized by default. Following examples will clarify this further for each operating system of your choice: |
||||
|
||||
### Ubuntu 16.04 LTS |
||||
|
||||
Example 1: Start Minio instance on a shared backend mounted and available at `/mnt/nfs`. |
||||
|
||||
On linux server1 |
||||
```shell |
||||
minio server /mnt/nfs |
||||
``` |
||||
|
||||
On linux server2 |
||||
```shell |
||||
minio server /mnt/nfs |
||||
``` |
||||
|
||||
### Windows 2012 Server |
||||
|
||||
Example 1: Start Minio instance on a shared backend mounted and available at `\\remote-server\cifs`. |
||||
|
||||
On windows server1 |
||||
```cmd |
||||
minio.exe server \\remote-server\cifs\export |
||||
``` |
||||
|
||||
On windows server2 |
||||
```cmd |
||||
minio.exe server \\remote-server\cifs\export |
||||
``` |
||||
|
||||
Alternatively if `\\remote-server\cifs` is mounted as `D:\` drive. |
||||
|
||||
On windows server1 |
||||
```cmd |
||||
minio.exe server D:\export |
||||
``` |
||||
|
||||
On windows server2 |
||||
```cmd |
||||
minio.exe server D:\export |
||||
``` |
||||
|
||||
Architecture |
||||
------------------ |
||||
|
||||
## POSIX/Win32 Locks |
||||
|
||||
### Lock process |
||||
|
||||
With in the same Minio instance locking is handled by existing in-memory namespace locks (**sync.RWMutex** et. al). To synchronize locks between many Minio instances we leverage POSIX `fcntl()` locks on Unixes and on Windows `LockFileEx()` Win32 API. Requesting write lock block if there are any read locks held by neighboring Minio instance on the same path. So does the read lock if there are any active write locks in-progress. |
||||
|
||||
### Unlock process |
||||
|
||||
Unlocking happens on filesystems locks by just closing the file descriptor (fd) which was initially requested for lock operation. Closing the fd tells the kernel to relinquish all the locks held on the path by the current process. This gets trickier when there are many readers on the same path by the same process, it would mean that closing an fd relinquishes locks for all concurrent readers as well. To properly manage this situation a simple fd reference count is implemented, the same fd is shared between many readers. When readers start closing on the fd we start reducing the reference count, once reference count has reached zero we can be sure that there are no more readers active. So we proceed and close the underlying file descriptor which would relinquish the read lock held on the path. |
||||
|
||||
This doesn't apply for the writes because there is always one writer and many readers for any unique object. |
||||
|
||||
## Handling Concurrency. |
||||
|
||||
An example here shows how the contention is handled with GetObject(). |
||||
|
||||
GetObject() holds a read lock on `fs.json`. |
||||
```go |
||||
|
||||
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fsMetaJSONFile) |
||||
rlk, err := fs.rwPool.Open(fsMetaPath) |
||||
if err != nil { |
||||
return toObjectErr(traceError(err), bucket, object) |
||||
} |
||||
defer rlk.Close() |
||||
|
||||
... you can perform other operations here ... |
||||
|
||||
_, err = io.CopyBuffer(writer, reader, buf) |
||||
|
||||
... after successful copy operation unlocks the read lock ... |
||||
|
||||
``` |
||||
|
||||
A concurrent PutObject is requested on the same object, PutObject() attempts a write lock on `fs.json`. |
||||
|
||||
```go |
||||
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fsMetaJSONFile) |
||||
wlk, err := fs.rwPool.Create(fsMetaPath) |
||||
if err != nil { |
||||
return ObjectInfo{}, toObjectErr(err, bucket, object) |
||||
} |
||||
// This close will allow for locks to be synchronized on `fs.json`. |
||||
defer wlk.Close() |
||||
``` |
||||
|
||||
Now from the above snippet the following code one can notice that until the GetObject() returns writing to the client. Following portion of the code will block. |
||||
|
||||
```go |
||||
wlk, err := fs.rwPool.Create(fsMetaPath) |
||||
``` |
||||
|
||||
This restriction is needed so that corrupted data is not returned to the client in between I/O. The logic works vice-versa as well an on-going PutObject(), GetObject() would wait for the PutObject() to complete. |
||||
|
||||
### Caveats (concurrency) |
||||
|
||||
Consider for example 3 servers sharing the same backend |
||||
|
||||
On minio1 |
||||
|
||||
- DeleteObject(object1) --> lock acquired on `fs.json` while object1 is being deleted. |
||||
|
||||
On minio2 |
||||
|
||||
- PutObject(object1) --> lock waiting until DeleteObject finishes. |
||||
|
||||
On minio3 |
||||
|
||||
- PutObject(object1) --> (concurrent request during PutObject minio2 checking if `fs.json` exists) |
||||
|
||||
Once lock is acquired the minio2 validates if the file really exists to avoid obtaining lock on an fd which is already deleted. But this situation calls for a race with a third server which is also attempting to write the same file before the minio2 can validate if the file exists. It might be potentially possible `fs.json` is created so the lock acquired by minio2 might be invalid and can lead to a potential inconsistency. |
||||
|
||||
This is a known problem and cannot be solved by POSIX fcntl locks. These are considered to be the limits of shared filesystem. |
@ -0,0 +1,92 @@ |
||||
# Shared Backend Minio Quickstart Guide [![Slack](https://slack.minio.io/slack?type=svg)](https://slack.minio.io) [![Go Report Card](https://goreportcard.com/badge/minio/minio)](https://goreportcard.com/report/minio/minio) [![Docker Pulls](https://img.shields.io/docker/pulls/minio/minio.svg?maxAge=604800)](https://hub.docker.com/r/minio/minio/) [![codecov](https://codecov.io/gh/minio/minio/branch/master/graph/badge.svg)](https://codecov.io/gh/minio/minio) |
||||
|
||||
Minio now supports shared backend across multiple instances. This solves certain specific use cases. |
||||
|
||||
## Use Cases |
||||
|
||||
- Minio on NAS |
||||
- Minio on Distributed Filesystems |
||||
- Multi-user Shared Backend. |
||||
|
||||
## Why Minio On Shared Backend? |
||||
|
||||
This feature allows Minio to serve a shared NAS drive across multiple Minio instances. There are no special configuration changes required to enable this feature. Access to files stored on NAS volume are locked and synchronized by default. |
||||
|
||||
# Get started |
||||
|
||||
If you're aware of stand-alone Minio set up, the installation and running remains the same. |
||||
|
||||
## 1. Prerequisites |
||||
|
||||
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio). |
||||
|
||||
## 2. Run Minio On Shared Backend |
||||
|
||||
Below examples will clarify further for each operating system of your choice: |
||||
|
||||
### Ubuntu 16.04 LTS |
||||
|
||||
Run the following commands on all the object storage gateway servers where your NAS volume is accessible. By explicitly passing access and secret keys through the environment variable you make sure that all the gateway servers share the same key across. |
||||
|
||||
Example 1: Start Minio instance on a shared backend mounted and available at `/mnt/nfs`. |
||||
|
||||
On linux server1 |
||||
```sh |
||||
minio server /mnt/nfs |
||||
``` |
||||
|
||||
On linux server2 |
||||
```sh |
||||
minio server /mnt/nfs |
||||
``` |
||||
|
||||
### Windows 2012 Server |
||||
|
||||
Run the following commands on all the object storage gateway servers where your NAS volume is accessible. By explicitly passing access and secret keys through the environment variable you make sure that all the gateway servers share the same key across. |
||||
|
||||
Example 1: Start Minio instance on a shared backend mounted and available at `\\remote-server\smb`. |
||||
|
||||
On windows server1 |
||||
```cmd |
||||
set MINIO_ACCESS_KEY=my-username |
||||
set MINIO_SECRET_KEY=my-password |
||||
minio.exe server \\remote-server\smb\export |
||||
``` |
||||
|
||||
On windows server2 |
||||
```cmd |
||||
set MINIO_ACCESS_KEY=my-username |
||||
set MINIO_SECRET_KEY=my-password |
||||
minio.exe server \\remote-server\smb\export |
||||
``` |
||||
|
||||
Alternatively if `\\remote-server\smb` is mounted as `M:\` drive. |
||||
|
||||
On windows server1 |
||||
```cmd |
||||
set MINIO_ACCESS_KEY=my-username |
||||
set MINIO_SECRET_KEY=my-password |
||||
net use m: \\remote-server\smb\export /P:Yes |
||||
minio.exe server M:\export |
||||
``` |
||||
|
||||
On windows server2 |
||||
```cmd |
||||
set MINIO_ACCESS_KEY=my-username |
||||
set MINIO_SECRET_KEY=my-password |
||||
net use m: \\remote-server\smb\export /P:Yes |
||||
minio.exe server M:\export |
||||
``` |
||||
|
||||
## 3. Test your setup |
||||
|
||||
To test this setup, access the Minio server via browser or [`mc`](https://docs.minio.io/docs/minio-client-quickstart-guide). You’ll see the uploaded files are accessible from the node2 endpoint as well. |
||||
|
||||
## Explore Further |
||||
- [Use `mc` with Minio Server](https://docs.minio.io/docs/minio-client-quickstart-guide) |
||||
- [Use `aws-cli` with Minio Server](https://docs.minio.io/docs/aws-cli-with-minio) |
||||
- [Use `s3cmd` with Minio Server](https://docs.minio.io/docs/s3cmd-with-minio) |
||||
- [Use `minio-go` SDK with Minio Server](https://docs.minio.io/docs/golang-client-quickstart-guide) |
||||
- [The Minio documentation website](https://docs.minio.io) |
||||
|
||||
|
@ -0,0 +1,102 @@ |
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
// Package lock - implements filesystem locking wrappers around an
|
||||
// open file descriptor.
|
||||
package lock |
||||
|
||||
import ( |
||||
"os" |
||||
"sync" |
||||
) |
||||
|
||||
// RLockedFile represents a read locked file, implements a special
|
||||
// closer which only closes the associated *os.File when the ref count.
|
||||
// has reached zero, i.e when all the readers have given up their locks.
|
||||
type RLockedFile struct { |
||||
*LockedFile |
||||
mutex sync.Mutex |
||||
refs int // Holds read lock refs.
|
||||
} |
||||
|
||||
// IsClosed - Check if the rlocked file is already closed.
|
||||
func (r *RLockedFile) IsClosed() bool { |
||||
r.mutex.Lock() |
||||
defer r.mutex.Unlock() |
||||
return r.refs == 0 |
||||
} |
||||
|
||||
// IncLockRef - is used by called to indicate lock refs.
|
||||
func (r *RLockedFile) IncLockRef() { |
||||
r.mutex.Lock() |
||||
r.refs++ |
||||
r.mutex.Unlock() |
||||
} |
||||
|
||||
// Close - this closer implements a special closer
|
||||
// closes the underlying fd only when the refs
|
||||
// reach zero.
|
||||
func (r *RLockedFile) Close() (err error) { |
||||
r.mutex.Lock() |
||||
defer r.mutex.Unlock() |
||||
|
||||
if r.refs == 0 { |
||||
return os.ErrInvalid |
||||
} |
||||
|
||||
r.refs-- |
||||
if r.refs == 0 { |
||||
err = r.File.Close() |
||||
} |
||||
|
||||
return err |
||||
} |
||||
|
||||
// Provides a new initialized read locked struct from *os.File
|
||||
func newRLockedFile(lkFile *LockedFile) (*RLockedFile, error) { |
||||
if lkFile == nil { |
||||
return nil, os.ErrInvalid |
||||
} |
||||
|
||||
return &RLockedFile{ |
||||
LockedFile: lkFile, |
||||
refs: 1, |
||||
}, nil |
||||
} |
||||
|
||||
// RLockedOpenFile - returns a wrapped read locked file, if the file
|
||||
// doesn't exist at path returns an error.
|
||||
func RLockedOpenFile(path string) (*RLockedFile, error) { |
||||
lkFile, err := LockedOpenFile(path, os.O_RDONLY, 0666) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return newRLockedFile(lkFile) |
||||
|
||||
} |
||||
|
||||
// LockedFile represents a locked file, implements a helper
|
||||
// method Size(), represents the size of the underlying object.
|
||||
type LockedFile struct { |
||||
*os.File |
||||
size int64 |
||||
} |
||||
|
||||
// Size - size of the underlying locked file.
|
||||
func (l *LockedFile) Size() int64 { |
||||
return l.size |
||||
} |
@ -0,0 +1,75 @@ |
||||
// +build !windows,!plan9,!solaris
|
||||
|
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
package lock |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"syscall" |
||||
) |
||||
|
||||
// LockedOpenFile - initializes a new lock and protects
|
||||
// the file from concurrent access across mount points.
|
||||
// This implementation doesn't support all the open
|
||||
// flags and shouldn't be considered as replacement
|
||||
// for os.OpenFile().
|
||||
func LockedOpenFile(path string, flag int, perm os.FileMode) (*LockedFile, error) { |
||||
var lockType int |
||||
switch flag { |
||||
case syscall.O_RDONLY: |
||||
lockType = syscall.LOCK_SH |
||||
case syscall.O_WRONLY: |
||||
fallthrough |
||||
case syscall.O_RDWR: |
||||
fallthrough |
||||
case syscall.O_WRONLY | syscall.O_CREAT: |
||||
fallthrough |
||||
case syscall.O_RDWR | syscall.O_CREAT: |
||||
lockType = syscall.LOCK_EX |
||||
default: |
||||
return nil, fmt.Errorf("Unsupported flag (%d)", flag) |
||||
} |
||||
|
||||
f, err := os.OpenFile(path, flag|syscall.O_SYNC, perm) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if err = syscall.Flock(int(f.Fd()), lockType); err != nil { |
||||
f.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
st, err := os.Stat(path) |
||||
if err != nil { |
||||
f.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
if st.IsDir() { |
||||
f.Close() |
||||
return nil, &os.PathError{ |
||||
Op: "open", |
||||
Path: path, |
||||
Err: syscall.EISDIR, |
||||
} |
||||
} |
||||
|
||||
return &LockedFile{File: f, size: st.Size()}, nil |
||||
} |
@ -0,0 +1,192 @@ |
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
package lock |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"os" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
// Test lock fails.
|
||||
func TestLockFail(t *testing.T) { |
||||
f, err := ioutil.TempFile("", "lock") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
f.Close() |
||||
defer func() { |
||||
err = os.Remove(f.Name()) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
}() |
||||
|
||||
_, err = LockedOpenFile(f.Name(), os.O_APPEND, 0600) |
||||
if err == nil { |
||||
t.Fatal("Should fail here") |
||||
} |
||||
} |
||||
|
||||
// Tests lock directory fail.
|
||||
func TestLockDirFail(t *testing.T) { |
||||
d, err := ioutil.TempDir("", "lockDir") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
defer func() { |
||||
err = os.Remove(d) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
}() |
||||
|
||||
_, err = LockedOpenFile(d, os.O_APPEND, 0600) |
||||
if err == nil { |
||||
t.Fatal("Should fail here") |
||||
} |
||||
} |
||||
|
||||
// Tests rwlock methods.
|
||||
func TestRWLockedFile(t *testing.T) { |
||||
f, err := ioutil.TempFile("", "lock") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
f.Close() |
||||
defer func() { |
||||
err = os.Remove(f.Name()) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
}() |
||||
|
||||
rlk, err := RLockedOpenFile(f.Name()) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if rlk.Size() != 0 { |
||||
t.Fatal("File size should be zero", rlk.Size()) |
||||
} |
||||
isClosed := rlk.IsClosed() |
||||
if isClosed { |
||||
t.Fatal("File ref count shouldn't be zero") |
||||
} |
||||
|
||||
// Increase reference count to 2.
|
||||
rlk.IncLockRef() |
||||
|
||||
isClosed = rlk.IsClosed() |
||||
if isClosed { |
||||
t.Fatal("File ref count shouldn't be zero") |
||||
} |
||||
|
||||
// Decrease reference count by 1.
|
||||
if err = rlk.Close(); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
isClosed = rlk.IsClosed() |
||||
if isClosed { |
||||
t.Fatal("File ref count shouldn't be zero") |
||||
} |
||||
|
||||
// Decrease reference count by 1.
|
||||
if err = rlk.Close(); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// Now file should be closed.
|
||||
isClosed = rlk.IsClosed() |
||||
if !isClosed { |
||||
t.Fatal("File ref count should be zero") |
||||
} |
||||
|
||||
// Closing a file again should result in invalid argument.
|
||||
if err = rlk.Close(); err != os.ErrInvalid { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
_, err = newRLockedFile(nil) |
||||
if err != os.ErrInvalid { |
||||
t.Fatal("Unexpected error", err) |
||||
} |
||||
} |
||||
|
||||
// Tests lock and unlock semantics.
|
||||
func TestLockAndUnlock(t *testing.T) { |
||||
f, err := ioutil.TempFile("", "lock") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
f.Close() |
||||
defer func() { |
||||
err = os.Remove(f.Name()) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
}() |
||||
|
||||
// lock the file
|
||||
l, err := LockedOpenFile(f.Name(), os.O_WRONLY, 0600) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// unlock the file
|
||||
if err = l.Close(); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// try lock the unlocked file
|
||||
dupl, err := LockedOpenFile(f.Name(), os.O_WRONLY|os.O_CREATE, 0600) |
||||
if err != nil { |
||||
t.Errorf("err = %v, want %v", err, nil) |
||||
} |
||||
|
||||
// blocking on locked file
|
||||
locked := make(chan struct{}, 1) |
||||
go func() { |
||||
bl, blerr := LockedOpenFile(f.Name(), os.O_WRONLY, 0600) |
||||
if blerr != nil { |
||||
t.Fatal(blerr) |
||||
} |
||||
locked <- struct{}{} |
||||
if blerr = bl.Close(); blerr != nil { |
||||
t.Fatal(blerr) |
||||
} |
||||
}() |
||||
|
||||
select { |
||||
case <-locked: |
||||
t.Error("unexpected unblocking") |
||||
case <-time.After(100 * time.Millisecond): |
||||
} |
||||
|
||||
// unlock
|
||||
if err = dupl.Close(); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
// the previously blocked routine should be unblocked
|
||||
select { |
||||
case <-locked: |
||||
case <-time.After(1 * time.Second): |
||||
t.Error("unexpected blocking") |
||||
} |
||||
} |
@ -0,0 +1,172 @@ |
||||
// +build windows
|
||||
|
||||
/* |
||||
* 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. |
||||
*/ |
||||
|
||||
package lock |
||||
|
||||
import ( |
||||
"errors" |
||||
"os" |
||||
"syscall" |
||||
"unsafe" |
||||
) |
||||
|
||||
var ( |
||||
modkernel32 = syscall.NewLazyDLL("kernel32.dll") |
||||
procLockFileEx = modkernel32.NewProc("LockFileEx") |
||||
|
||||
errLocked = errors.New("The process cannot access the file because another process has locked a portion of the file.") |
||||
) |
||||
|
||||
const ( |
||||
// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
|
||||
errLockViolation syscall.Errno = 0x21 |
||||
) |
||||
|
||||
// LockedOpenFile - initializes a new lock and protects
|
||||
// the file from concurrent access.
|
||||
func LockedOpenFile(path string, flag int, perm os.FileMode) (*LockedFile, error) { |
||||
f, err := open(path, flag, perm) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if err = lockFile(syscall.Handle(f.Fd()), 0); err != nil { |
||||
f.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
st, err := os.Stat(path) |
||||
if err != nil { |
||||
f.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
if st.IsDir() { |
||||
f.Close() |
||||
return nil, &os.PathError{ |
||||
Op: "open", |
||||
Path: path, |
||||
Err: syscall.EISDIR, |
||||
} |
||||
} |
||||
|
||||
return &LockedFile{File: f, size: st.Size()}, nil |
||||
} |
||||
|
||||
func makeInheritSa() *syscall.SecurityAttributes { |
||||
var sa syscall.SecurityAttributes |
||||
sa.Length = uint32(unsafe.Sizeof(sa)) |
||||
sa.InheritHandle = 1 |
||||
return &sa |
||||
} |
||||
|
||||
// perm param is ignored, on windows file perms/NT acls
|
||||
// are not octet combinations. Providing access to NT
|
||||
// acls is out of scope here.
|
||||
func open(path string, flag int, perm os.FileMode) (*os.File, error) { |
||||
if path == "" { |
||||
return nil, syscall.ERROR_FILE_NOT_FOUND |
||||
} |
||||
|
||||
pathp, err := syscall.UTF16PtrFromString(path) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var access uint32 |
||||
switch flag { |
||||
case syscall.O_RDONLY: |
||||
access = syscall.GENERIC_READ |
||||
case syscall.O_WRONLY: |
||||
access = syscall.GENERIC_WRITE |
||||
case syscall.O_RDWR: |
||||
access = syscall.GENERIC_READ | syscall.GENERIC_WRITE |
||||
case syscall.O_RDWR | syscall.O_CREAT: |
||||
access = syscall.GENERIC_ALL |
||||
case syscall.O_WRONLY | syscall.O_CREAT: |
||||
access = syscall.GENERIC_ALL |
||||
} |
||||
|
||||
if flag&syscall.O_APPEND != 0 { |
||||
access &^= syscall.GENERIC_WRITE |
||||
access |= syscall.FILE_APPEND_DATA |
||||
} |
||||
|
||||
var sa *syscall.SecurityAttributes |
||||
if flag&syscall.O_CLOEXEC == 0 { |
||||
sa = makeInheritSa() |
||||
} |
||||
|
||||
var createflag uint32 |
||||
switch { |
||||
case flag&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL): |
||||
createflag = syscall.CREATE_NEW |
||||
case flag&(syscall.O_CREAT|syscall.O_TRUNC) == (syscall.O_CREAT | syscall.O_TRUNC): |
||||
createflag = syscall.CREATE_ALWAYS |
||||
case flag&syscall.O_CREAT == syscall.O_CREAT: |
||||
createflag = syscall.OPEN_ALWAYS |
||||
case flag&syscall.O_TRUNC == syscall.O_TRUNC: |
||||
createflag = syscall.TRUNCATE_EXISTING |
||||
default: |
||||
createflag = syscall.OPEN_EXISTING |
||||
} |
||||
|
||||
shareflag := uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE | syscall.FILE_SHARE_DELETE) |
||||
accessAttr := uint32(syscall.FILE_ATTRIBUTE_NORMAL | 0x80000000) |
||||
|
||||
fd, err := syscall.CreateFile(pathp, access, shareflag, sa, createflag, accessAttr, 0) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return os.NewFile(uintptr(fd), path), nil |
||||
} |
||||
|
||||
func lockFile(fd syscall.Handle, flags uint32) error { |
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
|
||||
var flag uint32 = 2 // Lockfile exlusive.
|
||||
flag |= flags |
||||
|
||||
if fd == syscall.InvalidHandle { |
||||
return nil |
||||
} |
||||
|
||||
err := lockFileEx(fd, flag, 1, 0, &syscall.Overlapped{}) |
||||
if err == nil { |
||||
return nil |
||||
} else if err.Error() == errLocked.Error() { |
||||
return errors.New("lock already acquired") |
||||
} else if err != errLockViolation { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func lockFileEx(h syscall.Handle, flags, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) { |
||||
var reserved = uint32(0) |
||||
r1, _, e1 := syscall.Syscall6(procLockFileEx.Addr(), 6, uintptr(h), uintptr(flags), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol))) |
||||
if r1 == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
Loading…
Reference in new issue