356 lines
7.9 KiB
Go
356 lines
7.9 KiB
Go
|
package ctxbilly
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
|
||
|
securejoin "github.com/cyphar/filepath-securejoin"
|
||
|
"github.com/iceber/iouring-go"
|
||
|
)
|
||
|
|
||
|
func NewURingFS() (*UringFS, error) {
|
||
|
ur, err := iouring.New(64, iouring.WithAsync())
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &UringFS{
|
||
|
ur: ur,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
var _ Filesystem = (*UringFS)(nil)
|
||
|
|
||
|
const (
|
||
|
defaultDirectoryMode = 0o755
|
||
|
defaultCreateMode = 0o666
|
||
|
)
|
||
|
|
||
|
// UringFS is a fs implementation based on the OS filesystem which is bound to
|
||
|
// a base dir.
|
||
|
// Prefer this fs implementation over ChrootOS.
|
||
|
//
|
||
|
// Behaviours of note:
|
||
|
// 1. Read and write operations can only be directed to files which descends
|
||
|
// from the base dir.
|
||
|
// 2. Symlinks don't have their targets modified, and therefore can point
|
||
|
// to locations outside the base dir or to non-existent paths.
|
||
|
// 3. Readlink and Lstat ensures that the link file is located within the base
|
||
|
// dir, evaluating any symlinks that file or base dir may contain.
|
||
|
type UringFS struct {
|
||
|
ur *iouring.IOURing
|
||
|
baseDir string
|
||
|
}
|
||
|
|
||
|
func newBoundOS(d string) *UringFS {
|
||
|
return &UringFS{baseDir: d}
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) Create(ctx context.Context, filename string) (File, error) {
|
||
|
return fs.OpenFile(ctx, filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode)
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) OpenFile(ctx context.Context, filename string, flag int, perm os.FileMode) (File, error) {
|
||
|
fn, err := fs.abs(filename)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
f, err := os.OpenFile(fn, flag, perm)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return newFile(fs.ur, f)
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) ReadDir(ctx context.Context, path string) ([]os.FileInfo, error) {
|
||
|
dir, err := fs.abs(path)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
entries, err := os.ReadDir(dir)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
infos := make([]os.FileInfo, 0, len(entries))
|
||
|
for _, v := range entries {
|
||
|
info, err := v.Info()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
infos = append(infos, info)
|
||
|
}
|
||
|
|
||
|
return infos, nil
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) Rename(ctx context.Context, from, to string) error {
|
||
|
f, err := fs.abs(from)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
t, err := fs.abs(to)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// MkdirAll for target name.
|
||
|
if err := fs.createDir(t); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return os.Rename(f, t)
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) MkdirAll(ctx context.Context, path string, perm os.FileMode) error {
|
||
|
dir, err := fs.abs(path)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return os.MkdirAll(dir, perm)
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) Stat(ctx context.Context, filename string) (os.FileInfo, error) {
|
||
|
filename, err := fs.abs(filename)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return os.Stat(filename)
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) Remove(ctx context.Context, filename string) error {
|
||
|
fn, err := fs.abs(filename)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return os.Remove(fn)
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) Join(elem ...string) string {
|
||
|
return filepath.Join(elem...)
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) RemoveAll(path string) error {
|
||
|
dir, err := fs.abs(path)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return os.RemoveAll(dir)
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) Symlink(ctx context.Context, target, link string) error {
|
||
|
ln, err := fs.abs(link)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// MkdirAll for containing dir.
|
||
|
if err := fs.createDir(ln); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return os.Symlink(target, ln)
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) Lstat(ctx context.Context, filename string) (os.FileInfo, error) {
|
||
|
filename = filepath.Clean(filename)
|
||
|
if !filepath.IsAbs(filename) {
|
||
|
filename = filepath.Join(fs.baseDir, filename)
|
||
|
}
|
||
|
if ok, err := fs.insideBaseDirEval(filename); !ok {
|
||
|
return nil, err
|
||
|
}
|
||
|
return os.Lstat(filename)
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) Readlink(ctx context.Context, link string) (string, error) {
|
||
|
if !filepath.IsAbs(link) {
|
||
|
link = filepath.Clean(filepath.Join(fs.baseDir, link))
|
||
|
}
|
||
|
if ok, err := fs.insideBaseDirEval(link); !ok {
|
||
|
return "", err
|
||
|
}
|
||
|
return os.Readlink(link)
|
||
|
}
|
||
|
|
||
|
// Chroot returns a new OS filesystem, with the base dir set to the
|
||
|
// result of joining the provided path with the underlying base dir.
|
||
|
// func (fs *UringFS) Chroot(path string) (Filesystem, error) {
|
||
|
// joined, err := securejoin.SecureJoin(fs.baseDir, path)
|
||
|
// if err != nil {
|
||
|
// return nil, err
|
||
|
// }
|
||
|
// return newBoundOS(joined), nil
|
||
|
// }
|
||
|
|
||
|
// Root returns the current base dir of the billy.Filesystem.
|
||
|
// This is required in order for this implementation to be a drop-in
|
||
|
// replacement for other upstream implementations (e.g. memory and osfs).
|
||
|
func (fs *UringFS) Root() string {
|
||
|
return fs.baseDir
|
||
|
}
|
||
|
|
||
|
func (fs *UringFS) createDir(fullpath string) error {
|
||
|
dir := filepath.Dir(fullpath)
|
||
|
if dir != "." {
|
||
|
if err := os.MkdirAll(dir, defaultDirectoryMode); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// abs transforms filename to an absolute path, taking into account the base dir.
|
||
|
// Relative paths won't be allowed to ascend the base dir, so `../file` will become
|
||
|
// `/working-dir/file`.
|
||
|
//
|
||
|
// Note that if filename is a symlink, the returned address will be the target of the
|
||
|
// symlink.
|
||
|
func (fs *UringFS) abs(filename string) (string, error) {
|
||
|
if filename == fs.baseDir {
|
||
|
filename = string(filepath.Separator)
|
||
|
}
|
||
|
|
||
|
path, err := securejoin.SecureJoin(fs.baseDir, filename)
|
||
|
if err != nil {
|
||
|
return "", nil
|
||
|
}
|
||
|
|
||
|
return path, nil
|
||
|
}
|
||
|
|
||
|
// insideBaseDirEval checks whether filename is contained within
|
||
|
// a dir that is within the fs.baseDir, by first evaluating any symlinks
|
||
|
// that either filename or fs.baseDir may contain.
|
||
|
func (fs *UringFS) insideBaseDirEval(filename string) (bool, error) {
|
||
|
dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
|
||
|
if dir == "" || os.IsNotExist(err) {
|
||
|
dir = filepath.Dir(filename)
|
||
|
}
|
||
|
wd, err := filepath.EvalSymlinks(fs.baseDir)
|
||
|
if wd == "" || os.IsNotExist(err) {
|
||
|
wd = fs.baseDir
|
||
|
}
|
||
|
if filename != wd && dir != wd && !strings.HasPrefix(dir, wd+string(filepath.Separator)) {
|
||
|
return false, fmt.Errorf("path outside base dir")
|
||
|
}
|
||
|
return true, nil
|
||
|
}
|
||
|
|
||
|
func newFile(fsur *iouring.IOURing, f *os.File) (*URingFile, error) {
|
||
|
ur, err := iouring.New(64, iouring.WithAttachWQ(fsur))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &URingFile{
|
||
|
ur: ur,
|
||
|
f: f,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
type URingFile struct {
|
||
|
ur *iouring.IOURing
|
||
|
f *os.File
|
||
|
}
|
||
|
|
||
|
// Close implements File.
|
||
|
func (o *URingFile) Close(ctx context.Context) error {
|
||
|
return errors.Join(o.ur.UnregisterFile(o.f), o.Close(ctx))
|
||
|
}
|
||
|
|
||
|
// Name implements File.
|
||
|
func (o *URingFile) Name() string {
|
||
|
return o.f.Name()
|
||
|
}
|
||
|
|
||
|
// Read implements File.
|
||
|
func (o *URingFile) Read(ctx context.Context, p []byte) (n int, err error) {
|
||
|
req, err := o.ur.Read(o.f, p, nil)
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
defer req.Cancel()
|
||
|
|
||
|
select {
|
||
|
case <-req.Done():
|
||
|
return req.GetRes()
|
||
|
case <-ctx.Done():
|
||
|
req.Cancel()
|
||
|
<-req.Done()
|
||
|
return req.GetRes()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ReadAt implements File.
|
||
|
func (o *URingFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
|
||
|
req, err := o.ur.Pread(o.f, p, uint64(off), nil)
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
defer req.Cancel()
|
||
|
|
||
|
select {
|
||
|
case <-req.Done():
|
||
|
return req.GetRes()
|
||
|
case <-ctx.Done():
|
||
|
req.Cancel()
|
||
|
<-req.Done()
|
||
|
return req.GetRes()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Write implements File.
|
||
|
func (o *URingFile) Write(ctx context.Context, p []byte) (n int, err error) {
|
||
|
req, err := o.ur.Write(o.f, p, nil)
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
defer req.Cancel()
|
||
|
|
||
|
select {
|
||
|
case <-req.Done():
|
||
|
return req.GetRes()
|
||
|
case <-ctx.Done():
|
||
|
req.Cancel()
|
||
|
<-req.Done()
|
||
|
return req.GetRes()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// WriteAt implements File.
|
||
|
func (o *URingFile) WriteAt(ctx context.Context, p []byte, off int64) (n int, err error) {
|
||
|
req, err := o.ur.Pwrite(o.f, p, uint64(off), nil)
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
defer req.Cancel()
|
||
|
|
||
|
select {
|
||
|
case <-req.Done():
|
||
|
return req.GetRes()
|
||
|
case <-ctx.Done():
|
||
|
req.Cancel()
|
||
|
<-req.Done()
|
||
|
return req.GetRes()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Seek implements File.
|
||
|
func (o *URingFile) Seek(offset int64, whence int) (int64, error) {
|
||
|
return o.f.Seek(offset, whence)
|
||
|
}
|
||
|
|
||
|
// Truncate implements File.
|
||
|
func (o *URingFile) Truncate(ctx context.Context, size int64) error {
|
||
|
return o.f.Truncate(size)
|
||
|
}
|
||
|
|
||
|
var _ File = (*URingFile)(nil)
|