feat(go/file): AI维护,重构API与测试覆盖,版本更新至v0.1.0
This commit is contained in:
parent
c6a5673a86
commit
3d6bf15e38
80
AI.md
Normal file
80
AI.md
Normal 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})
|
||||
```
|
||||
---
|
||||
97
README.md
97
README.md
@ -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
40
TEST.md
Normal 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
239
archive.go
Normal 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
36
archive_test.go
Normal 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
273
file.go
Normal 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
82
file_test.go
Normal 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")
|
||||
}
|
||||
240
memory.go
Normal file
240
memory.go
Normal 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
31
memory_test.go
Normal 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
101
object.go
Normal 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, ¤t)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
current = make(map[string]any)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
convert.To(patch, ¤t)
|
||||
return MarshalFilePretty(filename, current)
|
||||
}
|
||||
66
object_test.go
Normal file
66
object_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user