client io architecture
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"galaxy/util"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -28,13 +29,13 @@ func NewFileStorage(path string) (*fs, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("path %s invalid: %s", path, err)
|
||||
}
|
||||
if ok, err := dirExists(absPath); err != nil {
|
||||
if ok, err := util.DirExists(absPath); err != nil {
|
||||
return nil, fmt.Errorf("check dir exist: %s", err)
|
||||
} else if !ok {
|
||||
return nil, errors.New("directory does not exist: " + absPath)
|
||||
}
|
||||
|
||||
if ok, err := writable(absPath); err != nil {
|
||||
if ok, err := util.Writable(absPath); err != nil {
|
||||
return nil, fmt.Errorf("check dir access: %s", err)
|
||||
} else if !ok {
|
||||
return nil, errors.New("directory should have read-write access: " + absPath)
|
||||
@@ -48,7 +49,7 @@ func NewFileStorage(path string) (*fs, error) {
|
||||
|
||||
func (f *fs) Lock() (func() error, error) {
|
||||
lockPath := f.lockFilePath()
|
||||
exists, err := fileExists(lockPath)
|
||||
exists, err := util.FileExists(lockPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check lock file exists: %s", err)
|
||||
}
|
||||
@@ -77,7 +78,7 @@ func (f *fs) Lock() (func() error, error) {
|
||||
}
|
||||
|
||||
func (f *fs) Exists(path string) (bool, error) {
|
||||
return fileExists(filepath.Join(f.root, path))
|
||||
return util.FileExists(filepath.Join(f.root, path))
|
||||
}
|
||||
|
||||
func (f *fs) WriteSafe(path string, v encoding.BinaryMarshaler) error {
|
||||
@@ -97,7 +98,7 @@ func (f *fs) WriteSafe(path string, v encoding.BinaryMarshaler) error {
|
||||
|
||||
targetDir := filepath.Dir(targetFilePath)
|
||||
if targetDir != f.root {
|
||||
ok, err := dirExists(targetDir)
|
||||
ok, err := util.DirExists(targetDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check target dir exists: %s", err)
|
||||
}
|
||||
@@ -110,12 +111,12 @@ func (f *fs) WriteSafe(path string, v encoding.BinaryMarshaler) error {
|
||||
}
|
||||
oldFilePath := targetFilePath + oldFileSuffix
|
||||
|
||||
targetExists, err := fileExists(targetFilePath)
|
||||
targetExists, err := util.FileExists(targetFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check target file exists: %s", err)
|
||||
}
|
||||
if targetExists {
|
||||
oldFileExists, err := fileExists(oldFilePath)
|
||||
oldFileExists, err := util.FileExists(oldFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check old file exists: %s", err)
|
||||
}
|
||||
@@ -125,7 +126,7 @@ func (f *fs) WriteSafe(path string, v encoding.BinaryMarshaler) error {
|
||||
}
|
||||
|
||||
newFilePath := targetFilePath + newFileSuffix
|
||||
newFileExists, err := fileExists(newFilePath)
|
||||
newFileExists, err := util.FileExists(newFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check new file exists: %s", err)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,45 @@
|
||||
package fs
|
||||
package fs_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"galaxy/server/internal/repo/fs"
|
||||
"galaxy/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
lockFile = ".lock"
|
||||
)
|
||||
|
||||
type sampleData struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (sd *sampleData) UnmarshalBinary(data []byte) error {
|
||||
sd.data = slices.Clone(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sd sampleData) MarshalBinary() (data []byte, err error) {
|
||||
return sd.data, nil
|
||||
}
|
||||
|
||||
func TestNewFileStorageSuccess(t *testing.T) {
|
||||
root, cleanup := util.CreateWorkDir(t)
|
||||
defer cleanup()
|
||||
_, err := NewFileStorage(root)
|
||||
_, err := fs.NewFileStorage(root)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
root, cleanup := util.CreateWorkDir(t)
|
||||
defer cleanup()
|
||||
fs, err := NewFileStorage(root)
|
||||
fs, err := fs.NewFileStorage(root)
|
||||
assert.NoError(t, err, "create file storage")
|
||||
unlock, err := fs.Lock()
|
||||
assert.NoError(t, err, "acquire lock")
|
||||
@@ -40,7 +59,7 @@ func TestExist(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fs, err := NewFileStorage(root)
|
||||
fs, err := fs.NewFileStorage(root)
|
||||
assert.NoError(t, err, "create file storage")
|
||||
|
||||
exist, err := fs.Exists(fileName)
|
||||
@@ -56,7 +75,7 @@ func TestWrite(t *testing.T) {
|
||||
root, cleanup := util.CreateWorkDir(t)
|
||||
defer cleanup()
|
||||
|
||||
fs, err := NewFileStorage(root)
|
||||
fs, err := fs.NewFileStorage(root)
|
||||
assert.NoError(t, err, "create file storage: %s", err)
|
||||
|
||||
unlock, err := fs.Lock()
|
||||
@@ -99,7 +118,7 @@ func TestRead(t *testing.T) {
|
||||
|
||||
sd := new(sampleData)
|
||||
|
||||
fs, err := NewFileStorage(root)
|
||||
fs, err := fs.NewFileStorage(root)
|
||||
assert.NoError(t, err, "create file storage: %s", err)
|
||||
|
||||
assert.EqualError(t, fs.Read("some.file", sd), "lock must be acquired before read")
|
||||
@@ -144,7 +163,7 @@ func TestRead(t *testing.T) {
|
||||
func TestWriteErrorWithoutLock(t *testing.T) {
|
||||
root, cleanup := util.CreateWorkDir(t)
|
||||
defer cleanup()
|
||||
fs, err := NewFileStorage(root)
|
||||
fs, err := fs.NewFileStorage(root)
|
||||
assert.NoError(t, err, "create file storage")
|
||||
sd := &sampleData{[]byte{0, 1, 2, 3}}
|
||||
err = fs.Write("some/path", sd)
|
||||
@@ -153,7 +172,7 @@ func TestWriteErrorWithoutLock(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewFileStorageErrorNotExists(t *testing.T) {
|
||||
_, err := NewFileStorage(filepath.Join(os.TempDir(), "non-existent-dir"))
|
||||
_, err := fs.NewFileStorage(filepath.Join(os.TempDir(), "non-existent-dir"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -165,7 +184,7 @@ func TestNewFileStorageErrorNotADirectory(t *testing.T) {
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = NewFileStorage(f.Name())
|
||||
_, err = fs.NewFileStorage(f.Name())
|
||||
assert.Error(t, err)
|
||||
if err := os.Remove(f.Name()); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -173,6 +192,6 @@ func TestNewFileStorageErrorNotADirectory(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewFileStorageErrorNoAccess(t *testing.T) {
|
||||
_, err := NewFileStorage(nonWritableDir)
|
||||
_, err := fs.NewFileStorage("/some/random/dir")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
const (
|
||||
nonWritableDir = "/usr/lib"
|
||||
)
|
||||
|
||||
type sampleData struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (sd *sampleData) UnmarshalBinary(data []byte) error {
|
||||
sd.data = slices.Clone(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sd sampleData) MarshalBinary() (data []byte, err error) {
|
||||
return sd.data, nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// for windows builds func [writable] should be refactored
|
||||
package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func dirExists(path string) (bool, error) {
|
||||
return pathExists(path, true)
|
||||
}
|
||||
|
||||
func fileExists(path string) (bool, error) {
|
||||
return pathExists(path, false)
|
||||
}
|
||||
|
||||
func pathExists(path string, isDir bool) (bool, error) {
|
||||
if fi, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
} else {
|
||||
if isDir != fi.IsDir() {
|
||||
return false, fmt.Errorf("wrong type: "+path+" mode=%s isDir=%t", fi.Mode(), isDir)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPathExists(t *testing.T) {
|
||||
root, cleanup := util.CreateWorkDir(t)
|
||||
defer cleanup()
|
||||
testDirExistsFunc(t, root, func(s string) (bool, error) { return pathExists(s, true) })
|
||||
testFileExistsFunc(t, root, func(s string) (bool, error) { return pathExists(s, false) })
|
||||
}
|
||||
|
||||
func TestDirExists(t *testing.T) {
|
||||
root, cleanup := util.CreateWorkDir(t)
|
||||
defer cleanup()
|
||||
testDirExistsFunc(t, root, dirExists)
|
||||
}
|
||||
|
||||
func TestFileExists(t *testing.T) {
|
||||
root, cleanup := util.CreateWorkDir(t)
|
||||
defer cleanup()
|
||||
testFileExistsFunc(t, root, fileExists)
|
||||
}
|
||||
|
||||
func TestWritable(t *testing.T) {
|
||||
root, cleanup := util.CreateWorkDir(t)
|
||||
defer cleanup()
|
||||
ok, err := writable(root)
|
||||
assert.NoError(t, err, "directory writable check")
|
||||
assert.True(t, ok, "directory should be writable")
|
||||
|
||||
ok, err = writable(nonWritableDir)
|
||||
assert.NoError(t, err, "system directory writable check")
|
||||
assert.False(t, ok, "system directory should not be writable")
|
||||
}
|
||||
|
||||
func testDirExistsFunc(t *testing.T, root string, dirCheck func(string) (bool, error)) {
|
||||
exists, err := dirCheck(root)
|
||||
assert.NoError(t, err, "directory existence check")
|
||||
assert.True(t, exists, "directory should exist")
|
||||
nonExistentDir := filepath.Join(root, uuid.New().String())
|
||||
exists, err = dirCheck(nonExistentDir)
|
||||
assert.NoError(t, err, "non-existent directory existence check")
|
||||
assert.False(t, exists, "non-existent directory should not exist")
|
||||
}
|
||||
|
||||
func testFileExistsFunc(t *testing.T, root string, fileCheck func(string) (bool, error)) {
|
||||
fpath := createTempFile(t, root)
|
||||
exists, err := fileCheck(fpath)
|
||||
assert.NoError(t, err, "file existence check")
|
||||
assert.True(t, exists, "file should exist")
|
||||
nonExistentFile := filepath.Join(root, uuid.New().String())
|
||||
exists, err = fileCheck(nonExistentFile)
|
||||
assert.NoError(t, err, "non-existent file existence check")
|
||||
assert.False(t, exists, "non-existent file should not exist")
|
||||
}
|
||||
|
||||
func createTempFile(t *testing.T, root string) string {
|
||||
t.Helper()
|
||||
|
||||
fd, err := os.CreateTemp(root, "a-file")
|
||||
if err != nil {
|
||||
assert.FailNow(t, "create temporary file", err)
|
||||
return ""
|
||||
}
|
||||
if err := fd.Close(); err != nil {
|
||||
assert.FailNow(t, "close temporary file", err)
|
||||
return ""
|
||||
}
|
||||
return fd.Name()
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//go:build unix || (js && wasm) || wasip1
|
||||
|
||||
package fs
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// writable reports whether path is writable on Windows.
|
||||
//
|
||||
// Semantics:
|
||||
// - for an existing regular file, it tries to open it for writing;
|
||||
// - for an existing directory, it tries to create and remove a temp file inside it;
|
||||
// - for other file types, it returns false with no error.
|
||||
func writable(filepath string) (bool, error) {
|
||||
return unix.Access(filepath, unix.W_OK) == nil, nil
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// writable reports whether path is writable on Windows.
|
||||
//
|
||||
// Semantics:
|
||||
// - for an existing regular file, it tries to open it for writing;
|
||||
// - for an existing directory, it tries to create and remove a temp file inside it;
|
||||
// - for other file types, it returns false with no error.
|
||||
//
|
||||
// This is intentionally an operational check, not a mode-bit check, because
|
||||
// on Windows effective writability is determined by ACLs and file attributes,
|
||||
// not by POSIX-like permission bits from os.FileMode.
|
||||
func writable(path string) (bool, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return writableDir(path)
|
||||
}
|
||||
|
||||
if !info.Mode().IsRegular() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return writableFile(path)
|
||||
}
|
||||
|
||||
// writableFile checks whether an existing regular file can be opened for writing.
|
||||
func writableFile(path string) (bool, error) {
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0)
|
||||
if err == nil {
|
||||
_ = f.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if isPermissionLikeError(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// writableDir checks whether a directory allows creating a child file.
|
||||
// That is usually the most useful definition of "directory is writable".
|
||||
func writableDir(path string) (bool, error) {
|
||||
pattern := filepath.Join(path, ".writable-check-*")
|
||||
f, err := os.CreateTemp(path, filepath.Base(pattern))
|
||||
if err == nil {
|
||||
name := f.Name()
|
||||
_ = f.Close()
|
||||
_ = os.Remove(name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if isPermissionLikeError(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// isPermissionLikeError normalizes the common Windows "access denied" style
|
||||
// failures to a simple false result instead of surfacing them as hard errors.
|
||||
func isPermissionLikeError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
return true
|
||||
}
|
||||
|
||||
var errno syscall.Errno
|
||||
if errors.As(err, &errno) {
|
||||
// ERROR_ACCESS_DENIED
|
||||
if errno == syscall.ERROR_ACCESS_DENIED {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) && errors.Is(pathErr.Err, os.ErrPermission) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
//go:build windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestWritable_NewFile verifies that a freshly created regular file is writable.
|
||||
func TestWritable_NewFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "file.txt")
|
||||
|
||||
err := os.WriteFile(path, []byte("x"), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
ok, err := writable(path)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
// TestWritable_NewDirectory verifies that a freshly created directory is writable
|
||||
// by checking that a temp file can be created inside it.
|
||||
func TestWritable_NewDirectory(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
ok, err := writable(dir)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
// TestWritable_MissingPath verifies that a missing path returns an error from Stat.
|
||||
func TestWritable_MissingPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "missing")
|
||||
|
||||
ok, err := writable(path)
|
||||
require.Error(t, err)
|
||||
require.False(t, ok)
|
||||
}
|
||||
Reference in New Issue
Block a user