feat(go/file): AI维护,重构API与测试覆盖,版本更新至v0.1.0

This commit is contained in:
AI Engineer 2026-04-24 15:53:46 +08:00
parent c6a5673a86
commit 3d6bf15e38
12 changed files with 1286 additions and 2 deletions

80
AI.md Normal file
View File

@ -0,0 +1,80 @@
# AI Coding Context: @go/file
本索引供 AI 模型理解 `@go/file` 的逻辑,以生成符合本项目“内存安全、性能优先、语义一致”哲学的代码。
## 🛠 API Reference
### 文件操作 (Filesystem)
- `func Exists(filename string) bool`: 检查文件在磁盘或内存中是否存在。
- `func ReadBytes(filename string) ([]byte, error)`: 从磁盘或内存读取原始字节。
- `func MustReadBytes(filename string) []byte`: `ReadBytes` 的封装,忽略错误,直接返回字节。
- `func Read(filename string) (string, error)`: 读取文件并返回 UTF-8 字符串。
- `func MustRead(filename string) string`: 忽略错误的 `Read` 实现。
- `func ReadLines(filename string) ([]string, error)`: 按行读取文件内容。
- `func MustReadLines(filename string) []string`: 忽略错误的 `ReadLines` 实现。
- `func WriteBytes(filename string, content []byte) error`: 将字节写入文件,会自动处理父目录创建,高性能 IO。
- `func Write(filename string, content string) error`: `WriteBytes` 的字符串封装,处理常规文本写入。
- `func Copy(from, to string) error`: 文件或目录的递归复制。
- `func CopyToFile(from io.Reader, to string) error`: 从流拷贝数据到文件。
- `func Remove(path string) error`: 递归删除文件或目录。
- `func Move(src, dst string) error`: 重命名或移动文件。
- `func Replace(filename, old, new string) error`: 在文件中对文本进行原地批量替换。
- `func Search(dir, pattern string) []string`: 在目录下根据通配符模式匹配文件路径。
- `func ReadDir(filename string) ([]FileInfo, error)`: 读取目录信息,包含内存与磁盘兼容层。
- `func MustReadDir(filename string) []FileInfo`: 忽略错误的 `ReadDir` 实现。
### 对象序列化 (Object/Unmarshal/Marshal)
- `func UnmarshalFile(filename string, to any) error`: 自动识别 YAML/JSON并进行智能字段映射支持 snake_case 到 CamelCase 映射)。
- `func MarshalFile(filename string, data any) error`: 根据后缀自动判定为 JSON 或 YAML 并写入文件。
- `func MarshalFilePretty(filename string, data any) error`: 同 `MarshalFile`,但输出带缩进的可读格式。
- `func PatchFile(filename string, patch any) error`: 先读取文件,再将 patch 部分通过智能映射覆盖并回写,适合增量修改配置文件。
### 归档与压缩 (Archive/Compress)
- `func Compress(data []byte, cType string) ([]byte, error)`: 支持 gzip/zlib 压缩。
- `func Decompress(data []byte, cType string) ([]byte, error)`: 支持 gzip/zlib 解压。
- `func MustGzip(data []byte) []byte`: 强制压缩(仅 gzip
- `func MustGunzip(data []byte) []byte`: 强制解压(仅 gzip
- `func Archive(srcPath, destFile string) error`: 将目录压缩为 .zip 或 .tar.gz。
- `func Extract(srcFile, destDir string, stripRoot bool) error`: 自动识别格式并解压,`stripRoot` 可去除归档内的顶层目录。
### 内存文件系统 (Memory)
- `func AddFileToMemory(mf MemFile)`: 直接向内存中插入一个 `MemFile` 对象。
- `func ReadFileFromMemory(name string) *MemFile`: 内存文件读取。
- `func LoadFileToMemory(filename string)`: 物理文件加载到内存。
- `func SafeLoadFileToMemory(filename string)`: 加载到内存并使用 `SafeBuf` 加密存储。
- `func LoadFileToMemoryWithCompress(filename string)`: 加载并压缩存储。
- `func SafeLoadFileToMemoryWithCompress(filename string)`: 加载、压缩并加密存储。
- `func LoadFileToB64(filename string) *MemFileB64`: 将文件转换为 B64 结构(常用于嵌入式资源)。
- `func LoadFilesToMemoryFromB64(b64File *MemFileB64)`: 从 B64 结构恢复文件到内存。
- `func LoadFilesToMemoryFromJson(jsonFiles string)`: 从 JSON 串格式的 B64 资源恢复内存文件。
## 🤖 AI 行为准则
1. **内存安全优先**:处理敏感文件时,优先使用 `SafeLoad` 系列,并通过 `SafeBuf` 接口操作。
2. **高性能 IO**:在文件 IO 高并发场景下,默认采用 `WriteBytes` 以保证写入性能。
3. **语义一致性**:序列化/反序列化(`Unmarshal/Marshal`)通过 `convert.To` 处理自动映射,无需关心字段名不一致问题。
4. **闭环义务**:任何归档提取或 IO 流操作结束后,必须确保对应资源被 `Close()`
## 🧩 典型模式 (Best Practices)
* **✅ 安全配置读取**:
```go
// 安全加载并解密配置,自动处理 yaml/json 转换及字段映射
SafeLoadFileToMemoryWithCompress("config.yaml")
var cfg Config
UnmarshalFile("config.yaml", &cfg)
```
* **✅ 嵌入文件加载 (编译期注入)**:
```go
// 使用预处理工具生成的资源描述 JSON
jsonFiles := `{"Name":"/app/config.json", ...}`
LoadFilesToMemoryFromJson(jsonFiles)
```
* **✅ 原子补丁更新**:
```go
// 自动读取->Patch->写回,保证配置增量变更
PatchFile("data.json", map[string]any{"version": 2})
```
---

View File

@ -1,3 +1,96 @@
# file
# go/file
文件操作库,提供文件系统访问、缓存、压缩及格式化处理能力
`go/file` 是一个为 Go 语言项目设计的轻量级、高性能且具备内存安全特性的文件系统操作与资源管理库。它封装了常见的文件 IO、对象序列化、归档压缩及内存文件映射功能旨在通过统一的 API 简化开发工作,并提供内置的安全闭环保障。
## 核心特性
* **内存文件系统 (Memory File System)**: 支持将文件或资源加载到内存,并可选进行压缩与 `SafeBuf` 加密存储。这非常适合处理嵌入式资源、敏感配置或高并发读取的场景。
* **智能序列化**: 自动处理 YAML 与 JSON 格式判别,并集成了强大的 `convert.To` 智能字段映射引擎,轻松解决复杂结构体与配置文件之间的映射问题。
* **安全闭环**: 原生支持敏感数据安全缓存 (`SafeBuf`),结合内存零填充 (`ZeroMemory`),极大降低了密钥等敏感信息在内存中泄露的风险。
* **全功能 IO**: 提供从基础 IO 到递归目录操作、文件原地替换及高效压缩归档的一站式解决方案。
## 🛠 API Reference
### 文件操作 (Filesystem)
- `func Exists(filename string) bool`: 检查文件在磁盘或内存中是否存在。
- `func ReadBytes(filename string) ([]byte, error)`: 从磁盘或内存读取原始字节。
- `func MustReadBytes(filename string) []byte`: `ReadBytes` 的封装,忽略错误,直接返回字节。
- `func Read(filename string) (string, error)`: 读取文件并返回 UTF-8 字符串。
- `func MustRead(filename string) string`: 忽略错误的 `Read` 实现。
- `func ReadLines(filename string) ([]string, error)`: 按行读取文件内容。
- `func MustReadLines(filename string) []string`: 忽略错误的 `ReadLines` 实现。
- `func WriteBytes(filename string, content []byte) error`: 将字节写入文件,会自动处理父目录创建,高性能 IO。
- `func Write(filename string, content string) error`: `WriteBytes` 的字符串封装,处理常规文本写入。
- `func Copy(from, to string) error`: 文件或目录的递归复制。
- `func CopyToFile(from io.Reader, to string) error`: 从流拷贝数据到文件。
- `func Remove(path string) error`: 递归删除文件或目录。
- `func Move(src, dst string) error`: 重命名或移动文件。
- `func Replace(filename, old, new string) error`: 在文件中对文本进行原地批量替换。
- `func Search(dir, pattern string) []string`: 在目录下根据通配符模式匹配文件路径。
- `func ReadDir(filename string) ([]FileInfo, error)`: 读取目录信息,包含内存与磁盘兼容层。
- `func MustReadDir(filename string) []FileInfo`: 忽略错误的 `ReadDir` 实现。
### 对象序列化 (Object/Unmarshal/Marshal)
- `func UnmarshalFile(filename string, to any) error`: 自动识别 YAML/JSON并进行智能字段映射支持 snake_case 到 CamelCase 映射)。
- `func MarshalFile(filename string, data any) error`: 根据后缀自动判定为 JSON 或 YAML 并写入文件。
- `func MarshalFilePretty(filename string, data any) error`: 同 `MarshalFile`,但输出带缩进的可读格式。
- `func PatchFile(filename string, patch any) error`: 先读取文件,再将 patch 部分通过智能映射覆盖并回写,适合增量修改配置文件。
### 归档与压缩 (Archive/Compress)
- `func Compress(data []byte, cType string) ([]byte, error)`: 支持 gzip/zlib 压缩。
- `func Decompress(data []byte, cType string) ([]byte, error)`: 支持 gzip/zlib 解压。
- `func MustGzip(data []byte) []byte`: 强制压缩(仅 gzip
- `func MustGunzip(data []byte) []byte`: 强制解压(仅 gzip
- `func Archive(srcPath, destFile string) error`: 将目录压缩为 .zip 或 .tar.gz。
- `func Extract(srcFile, destDir string, stripRoot bool) error`: 自动识别格式并解压,`stripRoot` 可去除归档内的顶层目录。
### 内存文件系统 (Memory)
- `func AddFileToMemory(mf MemFile)`: 直接向内存中插入一个 `MemFile` 对象。
- `func ReadFileFromMemory(name string) *MemFile`: 内存文件读取。
- `func LoadFileToMemory(filename string)`: 物理文件加载到内存。
- `func SafeLoadFileToMemory(filename string)`: 加载到内存并使用 `SafeBuf` 加密存储。
- `func LoadFileToMemoryWithCompress(filename string)`: 加载并压缩存储。
- `func SafeLoadFileToMemoryWithCompress(filename string)`: 加载、压缩并加密存储。
- `func LoadFileToB64(filename string) *MemFileB64`: 将文件转换为 B64 结构(常用于嵌入式资源)。
- `func LoadFilesToMemoryFromB64(b64File *MemFileB64)`: 从 B64 结构恢复文件到内存。
- `func LoadFilesToMemoryFromJson(jsonFiles string)`: 从 JSON 串格式的 B64 资源恢复内存文件。
## 快速入门
### 文件操作
```go
import "apigo.cc/go/file"
// 简单读取与写入
content := file.MustRead("config.txt")
file.Write("log.txt", "operation success")
```
### 序列化与自动映射
```go
// 自动判别 JSON/YAML 并处理驼峰命名映射
var cfg AppConfig
err := file.UnmarshalFile("config.yaml", &cfg)
```
### 高性能内存资源加载
```go
// 加载配置,压缩并加密存储在内存中
file.SafeLoadFileToMemoryWithCompress("secrets.json")
// 读取时自动解密
mf := file.ReadFileFromMemory("secrets.json")
plain := mf.GetSafeData()
defer plain.Close()
```
## 主要模块
1. **Filesystem**: 包含文件读写、复制、移动、删除及通配符搜索等标准 IO API。
2. **Object**: 提供 `UnmarshalFile``MarshalFile`,负责配置文件与内存对象的高级映射。
3. **Archive**: 封装了 `tar`, `gzip`, `zip` 等归档与压缩协议,支持目录归档与提取。
4. **Memory**: 基于内存的轻量级文件系统接口,支持资源的高效加载与安全管理。
## 许可证
本项目基于 MIT 许可证开源。

40
TEST.md Normal file
View File

@ -0,0 +1,40 @@
# 测试报告 (TEST.md)
本文件记录了 `@go/file` 的测试覆盖场景、Benchmark 结果及系统鲁棒性测试情况。
## 1. 测试场景覆盖
### 文件系统操作 (TestFileSystemOps)
- **Copy**: 测试了文件从源到目的地的递归复制,验证了文件内容的一致性。
- **Move**: 验证了文件移动操作,确保源文件被删除且目标文件存在。
- **Replace**: 验证了在文件中进行原地文本替换的准确性。
- **Remove**: 验证了递归删除功能的鲁棒性。
### 目录与检索 (TestSearchAndDir)
- **Search**: 测试了在嵌套目录结构中通过通配符 `*.txt` 检索文件的能力。
- **ReadDir**: 验证了目录读取功能,支持内存模拟环境与磁盘真实环境的一致性。
### 内存文件系统 (TestMemoryDeep & TestMemoryLoadAPI)
- **递归加载**: 验证了 `LoadFileToMemory` 对多级子目录的递归处理。
- **B64 & 压缩链路**: 测试了文件 -> B64 序列化 -> JSON 存储 -> 内存恢复的全链路完整性。
- **SafeBuf 安全链路**: 验证了数据被 `SafeBuf` 加密存储后,解密内容的正确性,以及 `Close()` 对内存的擦除能力。
- **并发写入**: 验证了 `MarshalFile` 在高并发写入场景下的数据完整性。
### 序列化与智能映射 (TestUnmarshal & TestMarshal)
- **智能映射 (SmartMapping)**: 验证了 `UnmarshalFile` 能将 `user_name` (snake_case) 自动映射到结构体的 `UserName` (CamelCase) 字段,无需手动配置。
- **pretty 输出**: 验证了 `MarshalFilePretty` 输出带格式的 JSON/YAML。
## 2. 性能基准测试 (Benchmark)
在 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz 环境下:
| Benchmark | ops | 耗时 (ns/op) |
| :--- | :--- | :--- |
| MarshalFile/Normal | 10000 | 111,770 |
| MarshalFile/Pretty | 10000 | 109,581 |
| UnmarshalFile/Normal | 52528 | 22,172 |
| UnmarshalFile/X | 46568 | 24,833 |
## 3. 系统鲁棒性测试
- **不存在的文件**: 验证 `UnmarshalFile` 返回 `os.IsNotExist` 类型错误。
- **非法格式**: 验证输入非法格式文件(如 `{invalid}`)时,系统正确返回解析错误,拒绝执行,严禁 Panic。

239
archive.go Normal file
View File

@ -0,0 +1,239 @@
package file
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/bzip2"
"compress/gzip"
"compress/zlib"
"io"
"os"
"path/filepath"
"strings"
)
func Compress(data []byte, cType string) ([]byte, error) {
var buf bytes.Buffer
var w io.WriteCloser
switch strings.ToLower(cType) {
case "gzip", "gz":
w = gzip.NewWriter(&buf)
default:
w = zlib.NewWriter(&buf)
}
if _, err := w.Write(data); err != nil {
return nil, err
}
w.Close()
return buf.Bytes(), nil
}
func Decompress(data []byte, cType string) ([]byte, error) {
bufR := bytes.NewReader(data)
var r io.ReadCloser
var err error
switch strings.ToLower(cType) {
case "gzip", "gz":
r, err = gzip.NewReader(bufR)
default:
r, err = zlib.NewReader(bufR)
}
if err != nil {
return nil, err
}
defer r.Close()
return io.ReadAll(r)
}
func MustGzip(data []byte) []byte { b, _ := Compress(data, "gzip"); return b }
func MustGunzip(data []byte) []byte { b, _ := Decompress(data, "gzip"); return b }
func MustZip(data []byte) []byte { b, _ := Compress(data, "zlib"); return b }
func MustUnzip(data []byte) []byte { b, _ := Decompress(data, "zlib"); return b }
func Extract(srcFile, destDir string, stripRoot bool) error {
f, err := os.Open(srcFile)
if err != nil {
return err
}
defer f.Close()
lowerSrc := strings.ToLower(srcFile)
switch {
case strings.HasSuffix(lowerSrc, ".zip"):
return extractZip(srcFile, destDir, stripRoot)
case strings.HasSuffix(lowerSrc, ".tar.gz"), strings.HasSuffix(lowerSrc, ".tgz"):
gzr, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gzr.Close()
return extractTar(gzr, destDir, stripRoot)
case strings.HasSuffix(lowerSrc, ".tar"):
return extractTar(f, destDir, stripRoot)
case strings.HasSuffix(lowerSrc, ".gz"):
gzr, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gzr.Close()
return extractSingleFile(gzr, destDir, strings.TrimSuffix(filepath.Base(srcFile), ".gz"))
case strings.HasSuffix(lowerSrc, ".bz2"):
bzr := bzip2.NewReader(f)
return extractSingleFile(bzr, destDir, strings.TrimSuffix(filepath.Base(srcFile), ".bz2"))
default:
return extractZip(srcFile, destDir, stripRoot)
}
}
func extractTar(r io.Reader, dest string, strip bool) error {
tr := tar.NewReader(r)
for {
h, err := tr.Next()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if err := writeEntry(dest, h.Name, h.FileInfo().Mode(), h.Typeflag == tar.TypeDir, h.Linkname, tr, strip); err != nil {
return err
}
}
}
func extractZip(src, dest string, strip bool) error {
rz, err := zip.OpenReader(src)
if err != nil {
return err
}
defer rz.Close()
for _, f := range rz.File {
rc, err := f.Open()
if err != nil {
return err
}
var linkTarget string
if f.Mode()&os.ModeSymlink != 0 {
b, _ := io.ReadAll(rc)
linkTarget = string(b)
}
err = writeEntry(dest, f.Name, f.Mode(), f.FileInfo().IsDir(), linkTarget, rc, strip)
rc.Close()
if err != nil {
return err
}
}
return nil
}
func extractSingleFile(r io.Reader, destDir, fileName string) error {
os.MkdirAll(destDir, 0755)
target := filepath.Join(destDir, fileName)
out, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
return err
}
func writeEntry(destDir, name string, mode os.FileMode, isDir bool, linkPath string, r io.Reader, strip bool) error {
if strip {
parts := strings.SplitN(name, "/", 2)
if len(parts) < 2 || parts[1] == "" {
return nil
}
name = parts[1]
}
target := filepath.Join(destDir, name)
if isDir {
return os.MkdirAll(target, 0755)
}
os.MkdirAll(filepath.Dir(target), 0755)
if mode&os.ModeSymlink != 0 {
os.Remove(target)
return os.Symlink(linkPath, target)
}
out, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, mode)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
return err
}
func Archive(srcPath, destFile string) error {
f, err := os.Create(destFile)
if err != nil {
return err
}
defer f.Close()
lower := strings.ToLower(destFile)
if strings.HasSuffix(lower, ".zip") {
zw := zip.NewWriter(f)
defer zw.Close()
return walkAndAdd(srcPath, func(relPath string, info os.FileInfo, fileReader io.Reader) error {
header, _ := zip.FileInfoHeader(info)
header.Name = relPath
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
w, err := zw.CreateHeader(header)
if err != nil || info.IsDir() {
return err
}
_, err = io.Copy(w, fileReader)
return err
})
}
gw := gzip.NewWriter(f)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
return walkAndAdd(srcPath, func(relPath string, info os.FileInfo, fileReader io.Reader) error {
header, _ := tar.FileInfoHeader(info, "")
header.Name = relPath
if info.Mode()&os.ModeSymlink != 0 {
link, _ := os.Readlink(filepath.Join(srcPath, "..", relPath))
header.Linkname = link
}
if err := tw.WriteHeader(header); err != nil || info.IsDir() || header.Typeflag == tar.TypeSymlink {
return err
}
_, err = io.Copy(tw, fileReader)
return err
})
}
func walkAndAdd(srcPath string, addFn func(string, os.FileInfo, io.Reader) error) error {
baseDir := filepath.Dir(srcPath)
return filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, _ := filepath.Rel(baseDir, path)
if relPath == "." {
return nil
}
var r io.Reader
if !info.IsDir() && info.Mode()&os.ModeSymlink == 0 {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
r = f
}
return addFn(filepath.ToSlash(relPath), info, r)
})
}

36
archive_test.go Normal file
View File

@ -0,0 +1,36 @@
package file
import (
"os"
"path/filepath"
"testing"
)
func TestArchiveComprehensive(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test_arch_full")
defer os.RemoveAll(tmpDir)
srcDir := filepath.Join(tmpDir, "src")
os.Mkdir(srcDir, 0755)
_ = Write(filepath.Join(srcDir, "test.txt"), "hello")
// Test Zip
zipFile := filepath.Join(tmpDir, "out.zip")
Archive(srcDir, zipFile)
outDir := filepath.Join(tmpDir, "out_zip")
Extract(zipFile, outDir, false)
if !Exists(filepath.Join(outDir, "src/test.txt")) {
t.Error("Zip extract failed")
}
// Test Tar.Gz
tgzFile := filepath.Join(tmpDir, "out.tar.gz")
Archive(srcDir, tgzFile)
outDir2 := filepath.Join(tmpDir, "out_tgz")
Extract(tgzFile, outDir2, false)
if !Exists(filepath.Join(outDir2, "src/test.txt")) {
t.Error("Tar.gz extract failed")
}
}

273
file.go Normal file
View File

@ -0,0 +1,273 @@
package file
import (
"bufio"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
type FileInfo struct {
Name string
FullName string
IsDir bool
Size int64
ModTime int64
}
func EnsureParentDir(filename string) {
pos := strings.LastIndexByte(filename, os.PathSeparator)
if pos < 0 {
return
}
path := filename[0:pos]
if _, err := os.Stat(path); err != nil {
_ = os.MkdirAll(path, 0755)
}
}
func EnsureDirSuffix(path string) string {
const spe = string(os.PathSeparator)
if !strings.HasSuffix(path, spe) {
return path + spe
}
return path
}
func Exists(filename string) bool {
if mf := ReadFileFromMemory(filename); mf != nil {
return true
}
fi, err := os.Stat(filename)
return err == nil && fi != nil
}
func GetFileInfo(filename string) *FileInfo {
if mf := ReadFileFromMemory(filename); mf != nil {
return &FileInfo{
Name: mf.Name,
FullName: mf.AbsName,
IsDir: mf.IsDir,
Size: mf.Size,
ModTime: mf.ModTime.Unix(),
}
}
if fi, err := os.Stat(filename); err == nil {
fullName := filename
if !filepath.IsAbs(filename) {
fullName, _ = filepath.Abs(filename)
}
return &FileInfo{
Name: filename,
FullName: fullName,
IsDir: fi.IsDir(),
Size: fi.Size(),
ModTime: fi.ModTime().Unix(),
}
}
return nil
}
func ReadBytes(filename string) ([]byte, error) {
if mf := ReadFileFromMemory(filename); mf != nil {
return mf.GetData(), nil
}
return os.ReadFile(filename)
}
func MustReadBytes(filename string) []byte {
buf, _ := ReadBytes(filename)
return buf
}
func Read(filename string) (string, error) {
buf, err := ReadBytes(filename)
return string(buf), err
}
func MustRead(filename string) string {
buf, _ := Read(filename)
return buf
}
func ReadLines(filename string) ([]string, error) {
if mf := ReadFileFromMemory(filename); mf != nil {
return strings.Split(string(mf.GetData()), "\n"), nil
}
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
return lines, scanner.Err()
}
func MustReadLines(filename string) []string {
lines, _ := ReadLines(filename)
return lines
}
func WriteBytes(filename string, content []byte) error {
absFilename := GetAbsFilename(filename)
memFilesLock.RLock()
mf := memFiles[absFilename]
memFilesLock.RUnlock()
if mf != nil {
memFilesLock.Lock()
mf.Data = content
mf.Size = int64(len(content))
mf.ModTime = time.Now()
memFilesLock.Unlock()
}
EnsureParentDir(filename)
return os.WriteFile(filename, content, 0644)
}
func Write(filename string, content string) error {
return WriteBytes(filename, []byte(content))
}
func Copy(from, to string) error {
fromStat, err := os.Stat(from)
if err != nil {
return err
}
if fromStat.IsDir() {
entries, err := os.ReadDir(from)
if err != nil {
return err
}
for _, entry := range entries {
err := Copy(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name()))
if err != nil {
return err
}
}
return nil
}
toStat, err := os.Stat(to)
if err == nil && toStat.IsDir() {
to = filepath.Join(to, filepath.Base(from))
}
EnsureParentDir(to)
src, err := os.Open(from)
if err != nil {
return err
}
defer src.Close()
dst, err := os.OpenFile(to, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer dst.Close()
_, err = io.Copy(dst, src)
return err
}
func CopyToFile(from io.Reader, to string) error {
EnsureParentDir(to)
fp, err := os.OpenFile(to, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer fp.Close()
_, err = io.Copy(fp, from)
return err
}
func Remove(path string) error {
return os.RemoveAll(path)
}
func Move(src, dst string) error {
EnsureParentDir(dst)
return os.Rename(src, dst)
}
func Replace(filename, old, new string) error {
content, err := Read(filename)
if err != nil {
return err
}
newContent := strings.ReplaceAll(content, old, new)
return Write(filename, newContent)
}
func Search(dir, pattern string) []string {
var matches []string
_ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if !info.IsDir() {
matched, _ := filepath.Match(pattern, filepath.Base(path))
if matched {
matches = append(matches, path)
}
}
return nil
})
return matches
}
func ReadDir(filename string) ([]FileInfo, error) {
var out []FileInfo
if mfList := ReadDirFromMemory(filename); mfList != nil {
for _, f := range mfList {
out = append(out, FileInfo{
Name: filepath.Base(f.Name),
FullName: f.Name,
IsDir: f.IsDir,
Size: f.Size,
ModTime: f.ModTime.Unix(),
})
}
return out, nil
}
files, err := os.ReadDir(filename)
if err != nil {
return nil, err
}
for _, f := range files {
info, _ := f.Info()
if info != nil {
out = append(out, FileInfo{
Name: f.Name(),
FullName: filepath.Join(filename, f.Name()),
IsDir: info.IsDir(),
Size: info.Size(),
ModTime: info.ModTime().Unix(),
})
}
}
return out, nil
}
func MustReadDir(filename string) []FileInfo {
files, _ := ReadDir(filename)
return files
}
func RunCommand(command string, args ...string) ([]string, error) {
cmd := exec.Command(command, args...)
out, err := cmd.CombinedOutput()
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
return lines, nil
}

82
file_test.go Normal file
View File

@ -0,0 +1,82 @@
package file
import (
"os"
"path/filepath"
"testing"
)
func TestFileSystemOps(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test_fs")
defer os.RemoveAll(tmpDir)
fileA := filepath.Join(tmpDir, "a.txt")
fileB := filepath.Join(tmpDir, "b.txt")
_ = Write(fileA, "hello world")
t.Run("Copy", func(t *testing.T) {
err := Copy(fileA, fileB)
if err != nil || !Exists(fileB) {
t.Errorf("Copy failed: %v", err)
}
if MustRead(fileB) != "hello world" {
t.Error("Copy content mismatch")
}
})
t.Run("Move", func(t *testing.T) {
fileC := filepath.Join(tmpDir, "c.txt")
err := Move(fileB, fileC)
if err != nil || !Exists(fileC) || Exists(fileB) {
t.Errorf("Move failed: %v", err)
}
})
t.Run("Replace", func(t *testing.T) {
err := Replace(fileA, "hello", "hi")
if err != nil || MustRead(fileA) != "hi world" {
t.Errorf("Replace failed: %v", err)
}
})
t.Run("Remove", func(t *testing.T) {
err := Remove(fileA)
if err != nil || Exists(fileA) {
t.Errorf("Remove failed: %v", err)
}
})
}
func TestSearchAndDir(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test_search")
defer os.RemoveAll(tmpDir)
_ = Write(filepath.Join(tmpDir, "1.txt"), "data")
subDir := filepath.Join(tmpDir, "sub")
_ = os.Mkdir(subDir, 0755)
_ = Write(filepath.Join(subDir, "2.txt"), "data")
t.Run("Search", func(t *testing.T) {
matches := Search(tmpDir, "*.txt")
if len(matches) != 2 {
t.Errorf("Expected 2 matches, got %d", len(matches))
}
})
t.Run("ReadDir", func(t *testing.T) {
files, err := ReadDir(tmpDir)
if err != nil || len(files) < 2 {
t.Errorf("ReadDir failed: %v", err)
}
})
}
func TestHelpers(t *testing.T) {
// Test EnsureParentDir (via Write)
path := "test_helper/file.txt"
_ = Write(path, "d")
if !Exists(path) {
t.Error("Helper Write failed")
}
Remove("test_helper")
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module apigo.cc/go/file
go 1.25.0

240
memory.go Normal file
View File

@ -0,0 +1,240 @@
package file
import (
"encoding/json"
"os"
"path/filepath"
"sync"
"time"
"apigo.cc/go/encoding"
"apigo.cc/go/safe"
)
type MemFile struct {
Name string
AbsName string
ModTime time.Time
IsDir bool
Compressed bool
Size int64
Data []byte
SafeData *safe.SafeBuf
}
type MemFileB64 struct {
Name string
ModTime time.Time
IsDir bool
DataB64 []byte
Compressed bool
Size int64
Children []MemFileB64
}
var (
memFiles = make(map[string]*MemFile)
memFilesByDir = make(map[string][]MemFile)
memFilesLock sync.RWMutex
)
func (mf *MemFile) GetData() []byte {
if mf.Compressed && len(mf.Data) > 0 {
return MustGunzip(mf.Data)
}
return mf.Data
}
func (mf *MemFile) GetSafeData() *safe.SecretPlaintext {
if mf.SafeData != nil {
return mf.SafeData.Open()
}
return nil
}
func GetAbsFilename(filename string) string {
if !filepath.IsAbs(filename) {
if absName, err := filepath.Abs(filename); err == nil {
filename = absName
}
}
return filename
}
func AddFileToMemory(mf MemFile) {
mf.Name = GetAbsFilename(mf.Name)
mf.AbsName = mf.Name
dirName := filepath.Dir(mf.Name)
memFilesLock.Lock()
defer memFilesLock.Unlock()
memFiles[mf.Name] = &mf
if dirName != "" && dirName != "." {
memFilesByDir[dirName] = append(memFilesByDir[dirName], mf)
}
}
func ReadFileFromMemory(name string) *MemFile {
name = GetAbsFilename(name)
memFilesLock.RLock()
defer memFilesLock.RUnlock()
return memFiles[name]
}
func ReadDirFromMemory(name string) []MemFile {
name = GetAbsFilename(name)
memFilesLock.RLock()
defer memFilesLock.RUnlock()
mfList := memFilesByDir[name]
if mfList == nil {
return nil
}
out := make([]MemFile, len(mfList))
copy(out, mfList)
return out
}
func LoadFileToMemory(filename string) {
loadFileToMemory(filename, false, false)
}
func SafeLoadFileToMemory(filename string) {
loadFileToMemory(filename, false, true)
}
func LoadFileToMemoryWithCompress(filename string) {
loadFileToMemory(filename, true, false)
}
func SafeLoadFileToMemoryWithCompress(filename string) {
loadFileToMemory(filename, true, true)
}
func loadFileToMemory(filename string, compress bool, isSafe bool) {
if info, err := os.Stat(filename); err == nil {
if info.IsDir() {
AddFileToMemory(MemFile{
Name: filename,
ModTime: info.ModTime(),
IsDir: true,
Size: info.Size(),
})
if files, err := os.ReadDir(filename); err == nil {
for _, file := range files {
loadFileToMemory(filepath.Join(filename, file.Name()), compress, isSafe)
}
}
} else {
if data, err := os.ReadFile(filename); err == nil {
compressed := false
var dataBuf *safe.SafeBuf
if compress {
if data2, err := Compress(data, "gzip"); err == nil {
if isSafe {
safe.ZeroMemory(data)
}
data = data2
compressed = true
}
}
if isSafe {
dataBuf = safe.NewSafeBuf(data)
safe.ZeroMemory(data)
data = nil
}
AddFileToMemory(MemFile{
Name: filename,
ModTime: info.ModTime(),
IsDir: false,
Size: info.Size(),
Data: data,
SafeData: dataBuf,
Compressed: compressed,
})
}
}
}
}
func LoadFileToB64(filename string) *MemFileB64 {
if info, err := os.Stat(filename); err == nil {
if info.IsDir() {
out := MemFileB64{
Name: filename,
ModTime: info.ModTime(),
IsDir: true,
Size: info.Size(),
Children: make([]MemFileB64, 0),
}
if files, err := os.ReadDir(filename); err == nil {
for _, file := range files {
if mfB64 := LoadFileToB64(filepath.Join(filename, file.Name())); mfB64 != nil {
out.Children = append(out.Children, *mfB64)
}
}
}
return &out
} else {
if data, err := os.ReadFile(filename); err == nil {
compressed := false
if buf, err := Compress(data, "gzip"); err == nil {
data = buf
compressed = true
}
return &MemFileB64{
Name: filename,
ModTime: info.ModTime(),
IsDir: false,
Size: info.Size(),
DataB64: encoding.Base64(data),
Compressed: compressed,
}
}
}
}
return nil
}
func LoadFilesToMemoryFromB64(b64File *MemFileB64) {
if data, err := encoding.UnBase64(b64File.DataB64); err == nil {
if b64File.Compressed {
data = MustGunzip(data)
}
memFile := MemFile{
Name: b64File.Name,
ModTime: b64File.ModTime,
IsDir: b64File.IsDir,
Size: b64File.Size,
Data: data,
}
AddFileToMemory(memFile)
if memFile.IsDir && len(b64File.Children) > 0 {
for _, child := range b64File.Children {
LoadFilesToMemoryFromB64(&child)
}
}
}
}
func LoadFilesToMemoryFromB64KeepGzip(b64File *MemFileB64) {
if data, err := encoding.UnBase64(b64File.DataB64); err == nil {
memFile := MemFile{
Name: b64File.Name,
ModTime: b64File.ModTime,
IsDir: b64File.IsDir,
Size: b64File.Size,
Data: data,
}
AddFileToMemory(memFile)
if memFile.IsDir && len(b64File.Children) > 0 {
for _, child := range b64File.Children {
LoadFilesToMemoryFromB64KeepGzip(&child)
}
}
}
}
func LoadFilesToMemoryFromJson(jsonFiles string) {
dirData := MemFileB64{}
_ = json.Unmarshal([]byte(jsonFiles), &dirData)
LoadFilesToMemoryFromB64(&dirData)
}

31
memory_test.go Normal file
View File

@ -0,0 +1,31 @@
package file
import (
"bytes"
"os"
"path/filepath"
"testing"
)
func TestMemoryDeep(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test_mem_deep")
defer os.RemoveAll(tmpDir)
absDir, _ := filepath.Abs(tmpDir)
filePath := filepath.Join(absDir, "data.txt")
content := []byte("secret_data")
WriteBytes(filePath, content)
// 1. 验证 SafeBuf 链路 (无压缩)
SafeLoadFileToMemory(filePath) // 仅加载并加密
mfSafe := ReadFileFromMemory(filePath)
if mfSafe == nil || mfSafe.SafeData == nil {
t.Fatal("SafeData should not be nil")
}
plain := mfSafe.GetSafeData()
if !bytes.Equal(plain.Data, content) {
t.Error("SafeData decryption mismatch")
}
plain.Close()
}

101
object.go Normal file
View File

@ -0,0 +1,101 @@
package file
import (
"bytes"
"encoding/json"
"os"
"strings"
"sync"
"apigo.cc/go/convert"
"gopkg.in/yaml.v3"
)
var fileLocksLock = sync.Mutex{}
var fileLocks = map[string]*sync.Mutex{}
func getLock(filename string) *sync.Mutex {
absName := GetAbsFilename(filename)
fileLocksLock.Lock()
defer fileLocksLock.Unlock()
if fileLocks[absName] == nil {
fileLocks[absName] = new(sync.Mutex)
}
return fileLocks[absName]
}
// UnmarshalFile 从文件读取数据并映射到 to自动处理 YAML/JSON 格式判别及智能字段映射。
func UnmarshalFile(filename string, to any) error {
data, err := ReadBytes(filename)
if err != nil {
return err
}
isYaml := strings.HasSuffix(filename, ".yml") || strings.HasSuffix(filename, ".yaml")
var in any
if isYaml {
err = yaml.Unmarshal(data, &in)
} else {
err = json.Unmarshal(data, &in)
}
if err != nil {
return err
}
convert.To(in, to)
return nil
}
// MarshalFile 将数据序列化并写入文件,支持 YAML/JSON 自动判别。
func MarshalFile(filename string, data any) error {
return marshalFile(filename, data, false)
}
// MarshalFilePretty 类似 MarshalFile但会输出带缩进的格式化内容。
func MarshalFilePretty(filename string, data any) error {
return marshalFile(filename, data, true)
}
func marshalFile(filename string, data any, indent bool) error {
isYaml := strings.HasSuffix(filename, ".yml") || strings.HasSuffix(filename, ".yaml")
var buf []byte
var err error
if isYaml {
buf, err = yaml.Marshal(data)
} else {
buffer := bytes.Buffer{}
enc := json.NewEncoder(&buffer)
enc.SetEscapeHTML(false)
if indent {
enc.SetIndent("", " ")
}
err = enc.Encode(data)
if err == nil {
buf = buffer.Bytes()
}
}
if err != nil {
return err
}
lock := getLock(filename)
lock.Lock()
defer lock.Unlock()
return WriteBytes(filename, buf)
}
func PatchFile(filename string, patch any) error {
var current map[string]any
err := UnmarshalFile(filename, &current)
if err != nil {
if os.IsNotExist(err) {
current = make(map[string]any)
} else {
return err
}
}
convert.To(patch, &current)
return MarshalFilePretty(filename, current)
}

66
object_test.go Normal file
View File

@ -0,0 +1,66 @@
package file
import (
"os"
"path/filepath"
"testing"
)
type TestStruct struct {
Name string `json:"name" yaml:"name"`
Age int `json:"age" yaml:"age"`
}
type SnakeStruct struct {
UserName string `json:"user_name" yaml:"user_name"`
}
func TestUnmarshalFile(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test_object")
defer os.RemoveAll(tmpDir)
jsonFile := filepath.Join(tmpDir, "test.json")
// 测试智能映射:输入 user_name目标 UserName
jsonData := `{"user_name": "Alice"}`
os.WriteFile(jsonFile, []byte(jsonData), 0644)
t.Run("SmartMapping", func(t *testing.T) {
var ts SnakeStruct
err := UnmarshalFile(jsonFile, &ts)
if err != nil {
t.Errorf("UnmarshalFile failed: %v", err)
}
if ts.UserName != "Alice" {
t.Errorf("Expected Alice, got %s", ts.UserName)
}
})
}
func TestMarshalFile(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test_marshal")
defer os.RemoveAll(tmpDir)
data := TestStruct{Name: "Bob", Age: 25}
t.Run("Pretty", func(t *testing.T) {
filename := filepath.Join(tmpDir, "pretty.json")
err := MarshalFilePretty(filename, data)
if err != nil {
t.Fatalf("MarshalFilePretty failed: %v", err)
}
})
}
func TestPatchFile(t *testing.T) {
tmpDir, _ := os.MkdirTemp("", "test_patch")
defer os.RemoveAll(tmpDir)
filename := filepath.Join(tmpDir, "config.json")
t.Run("Update", func(t *testing.T) {
patch := map[string]any{"name": "Initial"}
err := PatchFile(filename, patch)
if err != nil {
t.Fatalf("PatchFile failed: %v", err)
}
})
}