Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 79 additions & 41 deletions internal/storage/storage_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@ import (
)

// NewFSStorage creates a new storage instance that stores files on the local filesystem.
func NewFSStorage(options *StorageOptions) (storage Storage, err error) {
func NewFSStorage(options *StorageOptions) (Storage, error) {
if options.Endpoint == "" {
return nil, errors.New("endpoint is required")
}
root, err := filepath.Abs(options.Endpoint)
if err != nil {
return nil, err
}
err = ensureDir(root)
if err != nil {
return
if err := ensureDir(root); err != nil {
return nil, err
}
return &fsStorage{root: root}, nil
}
Expand All @@ -30,8 +29,25 @@ type fsStorage struct {
root string
}

func (fs *fsStorage) Stat(key string) (stat Stat, err error) {
// safeJoinPath joins and validates that the resulting path is within fs.root
func (fs *fsStorage) safeJoinPath(key string) (string, error) {
filename := filepath.Join(fs.root, key)
absPath, err := filepath.Abs(filename)
if err != nil {
return "", err
}
// Ensure absPath is within fs.root
if !strings.HasPrefix(absPath, fs.root+string(os.PathSeparator)) && absPath != fs.root {
return "", errors.New("invalid file path")
}
return absPath, nil
}

func (fs *fsStorage) Stat(key string) (Stat, error) {
filename, err := fs.safeJoinPath(key)
if err != nil {
return nil, ErrNotFound
}
fi, err := os.Lstat(filename)
if err != nil {
if os.IsNotExist(err) || strings.HasSuffix(err.Error(), "not a directory") {
Expand All @@ -42,74 +58,99 @@ func (fs *fsStorage) Stat(key string) (stat Stat, err error) {
return fi, nil
}

func (fs *fsStorage) Get(key string) (content io.ReadCloser, stat Stat, err error) {
filename := filepath.Join(fs.root, key)
file, err := os.Open(filename)
if err != nil && (os.IsNotExist(err) || strings.HasSuffix(err.Error(), "not a directory")) {
err = ErrNotFound
func (fs *fsStorage) Get(key string) (io.ReadCloser, Stat, error) {
filename, err := fs.safeJoinPath(key)
if err != nil {
return nil, nil, ErrNotFound
}
if err == nil {
stat, err = file.Stat()
file, err := os.Open(filename)
if err != nil {
if os.IsNotExist(err) || strings.HasSuffix(err.Error(), "not a directory") {
return nil, nil, ErrNotFound
}
return nil, nil, err
}
stat, err := file.Stat()
if err != nil {
return
file.Close()
return nil, nil, err
}
content = file
return
return file, stat, nil
}

func (fs *fsStorage) List(prefix string) (keys []string, err error) {
func (fs *fsStorage) List(prefix string) ([]string, error) {
dir := strings.TrimSuffix(utils.NormalizePathname(prefix)[1:], "/")
return findFiles(filepath.Join(fs.root, dir), dir)
absDir, err := fs.safeJoinPath(dir)
if err != nil {
return nil, err
}
return findFiles(absDir, dir)
}

func (fs *fsStorage) Put(key string, content io.Reader) (err error) {
filename := filepath.Join(fs.root, key)
err = ensureDir(filepath.Dir(filename))
func (fs *fsStorage) Put(key string, content io.Reader) error {
filename, err := fs.safeJoinPath(key)
if err != nil {
return
return ErrNotFound
}

dir := filepath.Dir(filename)
if !strings.HasPrefix(dir, fs.root+string(os.PathSeparator)) && dir != fs.root {
return errors.New("invalid file path")
}
if err := ensureDir(dir); err != nil {
return err
}

file, err := os.Create(filename)
if err != nil {
return
return err
}
defer file.Close()

_, err = io.Copy(file, content)
file.Close()
if err != nil {
if _, err := io.Copy(file, content); err != nil {
os.Remove(filename) // clean up if error occurs
return err
}
return
return nil
}

func (fs *fsStorage) Delete(key string) (err error) {
return os.Remove(filepath.Join(fs.root, key))
func (fs *fsStorage) Delete(key string) error {
filename, err := fs.safeJoinPath(key)
if err != nil {
return ErrNotFound
}
return os.Remove(filename)
}

func (fs *fsStorage) DeleteAll(prefix string) (deletedKeys []string, err error) {
func (fs *fsStorage) DeleteAll(prefix string) ([]string, error) {
dir := strings.TrimSuffix(utils.NormalizePathname(prefix)[1:], "/")
if dir == "" {
return nil, errors.New("prefix is required")
}
keys, err := fs.List(prefix)

absDir, err := fs.safeJoinPath(dir)
if err != nil {
return
return nil, ErrNotFound
}
err = os.RemoveAll(filepath.Join(fs.root, dir))

keys, err := fs.List(prefix)
if err != nil {
return
return nil, err
}

if err := os.RemoveAll(absDir); err != nil {
return nil, err
}
return keys, nil
}

// ensureDir ensures the given directory exists.
func ensureDir(dir string) (err error) {
_, err = os.Lstat(dir)
func ensureDir(dir string) error {
_, err := os.Lstat(dir)
if err != nil && os.IsNotExist(err) {
err = os.MkdirAll(dir, 0755)
return os.MkdirAll(dir, 0755)
}
return
return nil
}

// findFiles returns a list of files in the given directory.
Expand All @@ -133,10 +174,7 @@ func findFiles(root string, parentDir string) ([]string, error) {
if err != nil {
return nil, err
}
newFiles := make([]string, len(files)+len(subFiles))
copy(newFiles, files)
copy(newFiles[len(files):], subFiles)
files = newFiles
files = append(files, subFiles...)
} else {
files = append(files, path)
}
Expand Down
26 changes: 25 additions & 1 deletion server/build_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strings"
Expand Down Expand Up @@ -1315,7 +1316,30 @@ func normalizeImportSpecifier(specifier string) string {

// validateJSFile validates javascript/typescript module from the given file.
func validateJSFile(filename string) (isESM bool, namedExports []string, err error) {
data, err := os.ReadFile(filename)
absFilename, err := filepath.Abs(filename)
if err != nil {
return
}

// Example rootDir for validation:
// NOTE: you may need to pass the package root in as argument or otherwise obtain it.
// This version assumes validateJSFile only accepts files under the current working directory.
rootDir, err := filepath.Abs(".")
if err != nil {
return
}

// enforce trailing separator for proper prefix match
rootDirWithSep := rootDir
if !strings.HasSuffix(rootDirWithSep, string(filepath.Separator)) {
rootDirWithSep += string(filepath.Separator)
}
if !strings.HasPrefix(absFilename, rootDirWithSep) {
err = errors.New("access outside of allowed package directory")
return
}

data, err := os.ReadFile(absFilename)
if err != nil {
return
}
Expand Down
Loading