Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a97e1f50bf | ||
|
|
ba95b78adb | ||
|
|
72a1916cee | ||
|
|
31fc9dffc0 | ||
|
|
7efb9d2283 |
@ -1,5 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## v1.5.2 (2026-06-08)
|
||||
- **JS 对齐**: 将所有注册到 `jsmod` 的导出方法名统一为 PascalCase(如 `Read`, `Write`, `GetFileInfo`),消除 JS 环境下的调用摩擦。
|
||||
|
||||
## [1.3.3] - 2026-05-30
|
||||
- **新增**: 注册到 `jsmod`,将所有文件操作方法(Read/Write/Archive 等)注册为高危方法(unsafeList),确保安全沙箱隔离。
|
||||
|
||||
|
||||
83
file.go
83
file.go
@ -2,6 +2,7 @@ package file
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -9,6 +10,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/jsmod"
|
||||
)
|
||||
|
||||
type FileInfo struct {
|
||||
@ -119,6 +123,17 @@ func Write(filename string, content string) error {
|
||||
return WriteBytes(filename, []byte(content))
|
||||
}
|
||||
|
||||
func Append(filename string, content []byte) error {
|
||||
EnsureParentDir(filename)
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.Write(content)
|
||||
return err
|
||||
}
|
||||
|
||||
func Copy(from, to string) error {
|
||||
fromStat, err := os.Stat(from)
|
||||
if err != nil {
|
||||
@ -268,3 +283,71 @@ func RunCommand(command string, args ...string) ([]string, error) {
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// RealPath 返回客观的物理绝对路径,处理软连接和相对路径
|
||||
func RealPath(path string) string {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
|
||||
// 尝试解析软连接
|
||||
real, err := filepath.EvalSymlinks(abs)
|
||||
if err == nil {
|
||||
return real
|
||||
}
|
||||
|
||||
// 如果文件不存在,EvalSymlinks 会失败,此时返回绝对路径即可(后续写入操作会处理)
|
||||
return abs
|
||||
}
|
||||
|
||||
// VerifyPathForSafeMode 在安全模式下校验路径是否在 AllowedDirs 白名单内
|
||||
func VerifyPathForSafeMode(ctx context.Context, path string) (string, error) {
|
||||
// 如果不是安全模式,直接返回 RealPath
|
||||
if ctx == nil || !jsmod.IsSafeMode(ctx) {
|
||||
return RealPath(path), nil
|
||||
}
|
||||
|
||||
// 1. 获取白名单
|
||||
var allowedDirs []string
|
||||
cast.Convert(&allowedDirs, jsmod.Get(ctx, "AllowedDirs"))
|
||||
if len(allowedDirs) == 0 {
|
||||
return "", fmt.Errorf("file: access denied, AllowedDirs not found in context")
|
||||
}
|
||||
|
||||
// 2. 获取物理路径
|
||||
real := RealPath(path)
|
||||
|
||||
// 3. 校验逻辑:判断当前物理路径是否以任何一个白名单目录为前缀
|
||||
isAllowed := false
|
||||
for _, dir := range allowedDirs {
|
||||
if strings.HasPrefix(real, dir) {
|
||||
isAllowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 写操作保护:如果路径不存在,追溯其存在的父目录权限
|
||||
if !isAllowed {
|
||||
temp := real
|
||||
for temp != "" && temp != string(os.PathSeparator) {
|
||||
temp = filepath.Dir(temp)
|
||||
if _, err := os.Stat(temp); err == nil {
|
||||
// 找到了第一个存在的祖先目录,检查它是否受控
|
||||
for _, dir := range allowedDirs {
|
||||
if strings.HasPrefix(temp, dir) {
|
||||
isAllowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !isAllowed {
|
||||
return "", fmt.Errorf("file: permission denied for path %s", path)
|
||||
}
|
||||
|
||||
return real, nil
|
||||
}
|
||||
|
||||
14
go.mod
14
go.mod
@ -3,18 +3,18 @@ module apigo.cc/go/file
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
apigo.cc/go/cast v1.3.3
|
||||
apigo.cc/go/encoding v1.3.1
|
||||
apigo.cc/go/jsmod v1.0.0
|
||||
apigo.cc/go/safe v1.3.1
|
||||
apigo.cc/go/cast v1.5.0
|
||||
apigo.cc/go/encoding v1.5.0
|
||||
apigo.cc/go/jsmod v1.5.0
|
||||
apigo.cc/go/safe v1.5.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
apigo.cc/go/rand v1.3.1 // indirect
|
||||
apigo.cc/go/rand v1.5.0 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
||||
|
||||
26
go.sum
26
go.sum
@ -1,13 +1,13 @@
|
||||
apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
|
||||
apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||
apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y=
|
||||
apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY=
|
||||
apigo.cc/go/jsmod v1.0.0 h1:lVQMq0tCno4kbHlQ3j5wzsm+v24J+bznIoHxpton0pE=
|
||||
apigo.cc/go/jsmod v1.0.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
||||
apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8=
|
||||
apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||
apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q=
|
||||
apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE=
|
||||
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
|
||||
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
|
||||
apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I=
|
||||
apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA=
|
||||
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
|
||||
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
||||
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E=
|
||||
apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk=
|
||||
apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk=
|
||||
apigo.cc/go/safe v1.5.0/go.mod h1:OfQ5d6COePSGEuPvMeOk6KagX2sezw7nvKh7exj9SeM=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@ -20,10 +20,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
189
js_export.go
189
js_export.go
@ -1,38 +1,165 @@
|
||||
package file
|
||||
|
||||
import "apigo.cc/go/jsmod"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apigo.cc/go/jsmod"
|
||||
)
|
||||
|
||||
func init() {
|
||||
jsmod.Register("file", map[string]any{
|
||||
"exists": Exists,
|
||||
"read": Read,
|
||||
"readBytes": ReadBytes,
|
||||
"readLines": ReadLines,
|
||||
"readDir": ReadDir,
|
||||
"getFileInfo": GetFileInfo,
|
||||
"getAbsFilename": GetAbsFilename,
|
||||
"write": Write,
|
||||
"writeBytes": WriteBytes,
|
||||
"remove": Remove,
|
||||
"mkdir": Mkdir,
|
||||
"copy": Copy,
|
||||
"move": Move,
|
||||
"replace": Replace,
|
||||
"unmarshalFile": UnmarshalFile,
|
||||
"marshalFile": MarshalFile,
|
||||
"marshalFilePretty": MarshalFilePretty,
|
||||
"patchFile": PatchFile,
|
||||
"archive": Archive,
|
||||
"extract": Extract,
|
||||
"compress": Compress,
|
||||
"decompress": Decompress,
|
||||
"readFileFromMemory": ReadFileFromMemory,
|
||||
"readDirFromMemory": ReadDirFromMemory,
|
||||
// 读操作 (映射到私有包装器)
|
||||
"Exists": func(ctx context.Context, path string) bool {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return Exists(p)
|
||||
},
|
||||
"exists", "read", "readBytes", "readLines", "readDir", "getFileInfo", "getAbsFilename",
|
||||
"write", "writeBytes", "remove", "mkdir", "copy", "move", "replace",
|
||||
"unmarshalFile", "marshalFile", "marshalFilePretty", "patchFile",
|
||||
"archive", "extract", "compress", "decompress",
|
||||
"readFileFromMemory", "readDirFromMemory",
|
||||
)
|
||||
"Read": func(ctx context.Context, path string) (string, error) {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return Read(p)
|
||||
},
|
||||
"ReadBytes": func(ctx context.Context, path string) ([]byte, error) {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ReadBytes(p)
|
||||
},
|
||||
"ReadLines": func(ctx context.Context, path string) ([]string, error) {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ReadLines(p)
|
||||
},
|
||||
"ReadDir": func(ctx context.Context, path string) ([]FileInfo, error) {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ReadDir(p)
|
||||
},
|
||||
"GetFileInfo": func(ctx context.Context, path string) *FileInfo {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return GetFileInfo(p)
|
||||
},
|
||||
|
||||
// 写操作
|
||||
"Write": func(ctx context.Context, path string, content string) error {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Write(p, content)
|
||||
},
|
||||
"WriteBytes": func(ctx context.Context, path string, content []byte) error {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WriteBytes(p, content)
|
||||
},
|
||||
"Remove": func(ctx context.Context, path string) error {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Remove(p)
|
||||
},
|
||||
"Mkdir": func(ctx context.Context, path string) error {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Mkdir(p)
|
||||
},
|
||||
"Copy": func(ctx context.Context, from, to string) error {
|
||||
pFrom, err := VerifyPathForSafeMode(ctx, from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pTo, err := VerifyPathForSafeMode(ctx, to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Copy(pFrom, pTo)
|
||||
},
|
||||
"Move": func(ctx context.Context, from, to string) error {
|
||||
pFrom, err := VerifyPathForSafeMode(ctx, from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pTo, err := VerifyPathForSafeMode(ctx, to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Move(pFrom, pTo)
|
||||
},
|
||||
"Replace": func(ctx context.Context, path, old, new string) error {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Replace(p, old, new)
|
||||
},
|
||||
|
||||
// 序列化
|
||||
"UnmarshalFile": func(ctx context.Context, path string, to any) error {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return UnmarshalFile(p, to)
|
||||
},
|
||||
"MarshalFile": func(ctx context.Context, path string, data any) error {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalFile(p, data)
|
||||
},
|
||||
"MarshalFilePretty": func(ctx context.Context, path string, data any) error {
|
||||
p, err := VerifyPathForSafeMode(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return MarshalFilePretty(p, data)
|
||||
},
|
||||
|
||||
// 归档
|
||||
"Archive": func(ctx context.Context, src, dest string) error {
|
||||
pSrc, err := VerifyPathForSafeMode(ctx, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pDest, err := VerifyPathForSafeMode(ctx, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Archive(pSrc, pDest)
|
||||
},
|
||||
"Extract": func(ctx context.Context, src, dest string, strip bool) error {
|
||||
pSrc, err := VerifyPathForSafeMode(ctx, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pDest, err := VerifyPathForSafeMode(ctx, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Extract(pSrc, pDest, strip)
|
||||
},
|
||||
|
||||
// 压缩工具 (无路径,不校验)
|
||||
"Compress": Compress,
|
||||
"Decompress": Decompress,
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user