2025-10-15 10:12:44 +03:00

208 lines
6.1 KiB
Go

package iovfs
import (
"fmt"
"io/fs"
"strings"
"time"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs/internal"
)
type RealpathFS interface {
fs.FS
Realpath(path string) (string, error)
}
type WritableFS interface {
fs.FS
WriteFile(path string, data []byte, perm fs.FileMode) error
MkdirAll(path string, perm fs.FileMode) error
// Removes `path` and all its contents. Will return the first error it encounters.
Remove(path string) error
Chtimes(path string, aTime time.Time, mTime time.Time) error
}
type FsWithSys interface {
vfs.FS
FSys() fs.FS
}
// From creates a new FS from an [fs.FS].
//
// For paths like `c:/foo/bar`, fsys will be used as though it's rooted at `/` and the path is `/c:/foo/bar`.
//
// If the provided [fs.FS] implements [RealpathFS], it will be used to implement the Realpath method.
// If the provided [fs.FS] implements [WritableFS], it will be used to implement the WriteFile method.
//
// From does not actually handle case-insensitivity; ensure the passed in [fs.FS]
// respects case-insensitive file names if needed. Consider using [vfstest.FromMap] for testing.
func From(fsys fs.FS, useCaseSensitiveFileNames bool) FsWithSys {
var realpath func(path string) (string, error)
if fsys, ok := fsys.(RealpathFS); ok {
realpath = func(path string) (string, error) {
rest, hadSlash := strings.CutPrefix(path, "/")
rp, err := fsys.Realpath(rest)
if err != nil {
return "", err
}
if hadSlash {
return "/" + rp, nil
}
return rp, nil
}
} else {
realpath = func(path string) (string, error) {
return path, nil
}
}
var writeFile func(path string, content string, writeByteOrderMark bool) error
var mkdirAll func(path string) error
var remove func(path string) error
var chtimes func(path string, aTime time.Time, mTime time.Time) error
if fsys, ok := fsys.(WritableFS); ok {
writeFile = func(path string, content string, writeByteOrderMark bool) error {
rest, _ := strings.CutPrefix(path, "/")
if writeByteOrderMark {
// Strada uses \uFEFF because NodeJS requires it, but substitutes it with the correct BOM based on the
// output encoding. \uFEFF is actually the BOM for big-endian UTF-16. For UTF-8 the actual BOM is
// \xEF\xBB\xBF.
content = stringutil.AddUTF8ByteOrderMark(content)
}
return fsys.WriteFile(rest, []byte(content), 0o666)
}
mkdirAll = func(path string) error {
rest, _ := strings.CutPrefix(path, "/")
return fsys.MkdirAll(rest, 0o777)
}
remove = func(path string) error {
rest, _ := strings.CutPrefix(path, "/")
return fsys.Remove(rest)
}
chtimes = func(path string, aTime time.Time, mTime time.Time) error {
rest, _ := strings.CutPrefix(path, "/")
return fsys.Chtimes(rest, aTime, mTime)
}
} else {
writeFile = func(string, string, bool) error {
panic("writeFile not supported")
}
mkdirAll = func(string) error {
panic("mkdirAll not supported")
}
remove = func(string) error {
panic("remove not supported")
}
chtimes = func(string, time.Time, time.Time) error {
panic("chtimes not supported")
}
}
return &ioFS{
common: internal.Common{
RootFor: func(root string) fs.FS {
if root == "/" {
return fsys
}
p := tspath.RemoveTrailingDirectorySeparator(root)
sub, err := fs.Sub(fsys, p)
if err != nil {
panic(fmt.Sprintf("vfs: failed to create sub file system for %q: %v", p, err))
}
return sub
},
},
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
realpath: realpath,
writeFile: writeFile,
mkdirAll: mkdirAll,
remove: remove,
chtimes: chtimes,
fsys: fsys,
}
}
type ioFS struct {
common internal.Common
useCaseSensitiveFileNames bool
realpath func(path string) (string, error)
writeFile func(path string, content string, writeByteOrderMark bool) error
mkdirAll func(path string) error
remove func(path string) error
chtimes func(path string, aTime time.Time, mTime time.Time) error
fsys fs.FS
}
var _ FsWithSys = (*ioFS)(nil)
func (vfs *ioFS) UseCaseSensitiveFileNames() bool {
return vfs.useCaseSensitiveFileNames
}
func (vfs *ioFS) DirectoryExists(path string) bool {
return vfs.common.DirectoryExists(path)
}
func (vfs *ioFS) FileExists(path string) bool {
return vfs.common.FileExists(path)
}
func (vfs *ioFS) GetAccessibleEntries(path string) vfs.Entries {
return vfs.common.GetAccessibleEntries(path)
}
func (vfs *ioFS) Stat(path string) vfs.FileInfo {
_ = internal.RootLength(path) // Assert path is rooted
return vfs.common.Stat(path)
}
func (vfs *ioFS) ReadFile(path string) (contents string, ok bool) {
return vfs.common.ReadFile(path)
}
func (vfs *ioFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
return vfs.common.WalkDir(root, walkFn)
}
func (vfs *ioFS) Remove(path string) error {
_ = internal.RootLength(path) // Assert path is rooted
return vfs.remove(path)
}
func (vfs *ioFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
_ = internal.RootLength(path) // Assert path is rooted
return vfs.chtimes(path, aTime, mTime)
}
func (vfs *ioFS) Realpath(path string) string {
root, rest := internal.SplitPath(path)
// splitPath normalizes the path into parts (e.g. "c:/foo/bar" -> "c:/", "foo/bar")
// Put them back together to call realpath.
realpath, err := vfs.realpath(root + rest)
if err != nil {
return path
}
return realpath
}
func (vfs *ioFS) WriteFile(path string, content string, writeByteOrderMark bool) error {
_ = internal.RootLength(path) // Assert path is rooted
if err := vfs.writeFile(path, content, writeByteOrderMark); err == nil {
return nil
}
if err := vfs.mkdirAll(tspath.GetDirectoryPath(tspath.NormalizePath(path))); err != nil {
return err
}
return vfs.writeFile(path, content, writeByteOrderMark)
}
func (vfs *ioFS) FSys() fs.FS {
return vfs.fsys
}