feat(filex): 自 go-hua/common 迁入文件目录工具并独立成包

- 运行目录定位、路径存在性、带进度回调下载、Zip 打包/解压(兼容 GBK 中文文件名)
- 附单元测试、examples/filex 例程与包 README
This commit is contained in:
2026-09-20 12:20:50 +08:00
parent ca9dedaf82
commit c7e8eb14f8
7 changed files with 463 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
// filex 包示例:文件与目录工具(运行目录、路径存在性、Zip 打包解压)
package main
import (
"fmt"
"os"
"path/filepath"
"git.zeroonesoft.cn/golib/zogo/filex"
)
func main() {
// 路径与目录
exists, _ := filex.PathExists(".")
fmt.Println("当前目录存在:", exists)
fmt.Println("工作目录:", filex.GetWorkDir())
fmt.Println("运行目录:", filex.GetRunDir())
// Zip 打包与解压(兼容 GBK 中文文件名)
zipDemo()
}
func zipDemo() {
dir, err := os.MkdirTemp("", "zogo-filex-*")
if err != nil {
fmt.Println("创建临时目录失败:", err)
return
}
defer os.RemoveAll(dir)
src := filepath.Join(dir, "aaa")
if err := os.MkdirAll(src, 0755); err != nil {
fmt.Println(err)
return
}
if err := os.WriteFile(filepath.Join(src, "hello.txt"), []byte("hello zip"), 0644); err != nil {
fmt.Println(err)
return
}
zipPath := filepath.Join(dir, "aaa.zip")
if err := filex.Zip(src, zipPath); err != nil {
fmt.Println("Zip 失败:", err)
return
}
dest := filepath.Join(dir, "out")
if err := filex.Unzip(zipPath, dest); err != nil {
fmt.Println("Unzip 失败:", err)
return
}
data, err := os.ReadFile(filepath.Join(dest, "aaa", "hello.txt"))
fmt.Println("解压读回内容:", string(data), "错误:", err)
}
+38
View File
@@ -0,0 +1,38 @@
# filex
文件与目录工具:工作目录/运行目录定位、路径存在性判断、带进度回调的文件下载、
Zip 打包与解压(兼容 GBK 编码的中文文件名)。
> 迁移自 go-hua/common(原 dir.go / path.go / downFile.go / zip.go)。
## 用法
```go
import "git.zeroonesoft.cn/golib/zogo/filex"
// 目录定位
filex.GetWorkDir() // os.Getwd()
filex.GetRunDir() // 可执行文件所在目录(兼容 go run,尾带分隔符)
filex.GetCurrentAbPathByCaller()
// 路径存在性
exists, err := filex.PathExists("/path/to/x")
// 下载文件(fb 进度回调:总长度、已下载长度)
err := filex.DownloadFile("https://example.com/a.zip", "D:/tmp/a.zip",
func(length, downLen int64) { ... })
// Zip 打包(目录或单文件)/ 解压(Flags==0 按 GB18030 解码文件名,兼容中文 zip)
err := filex.Zip("D:/data/src", "D:/tmp/src.zip")
err := filex.Unzip("D:/tmp/src.zip", "D:/data/out")
```
完整可运行例程:[examples/filex/main.go](../examples/filex/main.go)
## 注意
- `DownloadFile` 跳过 TLS 证书校验(`InsecureSkipVerify: true`),下载超时 300s,
只支持 GET;不要用于对安全性敏感的场景。
- `Zip` 目录项名以 `\` 结尾(历史行为,兼容易语言侧解压);`Unzip` 已兼容 UTF-8
与 GBK 两种文件名编码。
- `GetRunDir` 在 `go run`(临时目录)场景回退到工作目录。
+77
View File
@@ -0,0 +1,77 @@
// Package filex 文件与目录工具:工作目录/运行目录定位、路径存在性判断、
// 带进度回调的文件下载、Zip 打包与解压(兼容 GBK 中文文件名)。
package filex
import (
"os"
"path"
"path/filepath"
"runtime"
"strings"
)
// 获取工作目录
func GetWorkDir() string {
str, _ := os.Getwd()
return str
}
// trimmedPath 规范化路径并去掉末尾分隔符
func trimmedPath(file string) string {
idx := strings.LastIndexByte(file, '/')
if idx == -1 {
return file
}
// Find the penultimate separator.
idx = strings.LastIndexByte(file[:idx], '/')
if idx == -1 {
return file
}
return file[:idx+1]
}
// 最终方案-全兼容
func GetRunDir() string {
dir := getCurrentAbPathByExecutable()
if strings.Contains(dir, getTmpDir()) {
dir = GetWorkDir()
}
// 结尾不是分隔符时补上(原写法用 || 恒为 true,行为等价但触发 vet 告警)
if dir[len(dir)-1:] != "/" && dir[len(dir)-1:] != "\\" {
dir += string(filepath.Separator)
}
return dir
}
// 获取系统临时目录,兼容go run
func getTmpDir() string {
dir := os.Getenv("TEMP")
if dir == "" {
dir = os.Getenv("TMP")
}
res, _ := filepath.EvalSymlinks(dir)
// 取父目录
dir = filepath.Dir(res)
return dir
}
// 获取当前执行文件绝对路径 (go build)
func getCurrentAbPathByExecutable() string {
exePath, err := os.Executable()
if err != nil {
return ""
}
res, _ := filepath.EvalSymlinks(filepath.Dir(exePath))
return res
}
// 获取当前执行文件绝对路径(go run)
func GetCurrentAbPathByCaller() string {
var abPath string
_, filename, _, ok := runtime.Caller(0)
if ok {
abPath = path.Dir(filename)
}
abPath = trimmedPath(filename)
return abPath
}
+110
View File
@@ -0,0 +1,110 @@
package filex
import (
"compress/gzip"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
)
// DownloadFile 下载文件到指定路径
// fb 为进度回调,参数为总长度与已下载长度
func DownloadFile(url string, downPath string, fb func(length, downLen int64)) error {
var (
fileSize int64
buf = make([]byte, 32*1024)
written int64
)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
//req.Header.Add("Authorization", "BNbmgAAGI155F6MJ3N2Tk9ruL_6XQpx-uxkkg:tGCY3xCsgybHd5IjcDMi9yZXBvcy93aF9mbG93RGF0YVNvdXJjZTEiLCJleHBpcmVzIjoxNTM2NzU4NjQ3LCJjb250ZW5VudFR5cGUiOiIiLCJoZWFkZXJzIjoiIiwibWV0aG9kIjoiR0VUIn0=")
req.Header.Add("Accept-Encoding", "gzip")
//req.Header.Set("Content-Encoding", "gzip")
//req.Header.Set("User-Agent", "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)")
req.Header.Set("Accept", " */*")
// req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
client.Timeout = time.Second * 300
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return errors.New(resp.Status)
}
//读取服务器返回的文件大小
fileSize = resp.ContentLength
//fmt.Println(resp.Header)
if fileSize == -1 {
fmt.Println("取文件长度失败", url)
}
folderPath := filepath.Dir(downPath)
err = os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
return err
}
//创建文件
file, err := os.Create(downPath)
if err != nil {
return err
}
defer file.Close()
if resp.Body == nil {
return errors.New("body is null")
}
defer resp.Body.Close()
//下面是 io.copyBuffer() 的简化版本
body := resp.Body
if resp.Header.Get("Content-Encoding") == "gzip" {
body, err = gzip.NewReader(resp.Body)
if err != nil {
fmt.Println("http resp unzip is failed,err: ", err)
}
}
for {
//读取bytes
nr, er := body.Read(buf)
if nr > 0 {
//写入bytes
nw, ew := file.Write(buf[0:nr])
//数据长度大于0
if nw > 0 {
written += int64(nw)
}
//写入出错
if ew != nil {
err = ew
break
}
//读取是数据长度不等于写入的数据长度
if nr != nw {
err = io.ErrShortWrite
break
}
}
if er != nil {
if er != io.EOF {
err = er
}
break
}
//没有错误了快使用 callback
fb(fileSize, written)
}
return err
}
+49
View File
@@ -0,0 +1,49 @@
package filex
import (
"os"
"path/filepath"
"testing"
)
func TestPathExistsAndWorkDir(t *testing.T) {
exists, err := PathExists(".")
if err != nil || !exists {
t.Errorf("PathExists(.) = %v, %v, want true", exists, err)
}
if exists, _ := PathExists(filepath.Join(t.TempDir(), "not-exist")); exists {
t.Error("不存在的路径应返回 false")
}
if GetWorkDir() == "" {
t.Error("GetWorkDir 不应为空")
}
}
func TestZipAndUnzip(t *testing.T) {
dir := t.TempDir()
src := filepath.Join(dir, "src", "inner")
if err := os.MkdirAll(src, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "hello.txt"), []byte("zip content"), 0644); err != nil {
t.Fatal(err)
}
zipPath := filepath.Join(dir, "src.zip")
if err := Zip(filepath.Join(dir, "src"), zipPath); err != nil {
t.Fatalf("Zip: %v", err)
}
dest := filepath.Join(dir, "out")
if err := Unzip(zipPath, dest); err != nil {
t.Fatalf("Unzip: %v", err)
}
data, err := os.ReadFile(filepath.Join(dest, "src", "inner", "hello.txt"))
if err != nil {
t.Fatalf("读取解压文件: %v", err)
}
if string(data) != "zip content" {
t.Errorf("解压内容 = %q", string(data))
}
}
+16
View File
@@ -0,0 +1,16 @@
package filex
import "os"
// PathExists 判断一个文件或文件夹是否存在
// 输入文件路径,根据返回的bool值来判断文件或文件夹是否存在
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
+120
View File
@@ -0,0 +1,120 @@
package filex
import (
"archive/zip"
"bytes"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"io"
"os"
"path/filepath"
"strings"
)
// Zip 打包成zip文件 可以是目录或单个文件
func Zip(srcFile string, destZip string) error {
// 预防:旧文件无法覆盖
os.RemoveAll(destZip)
//创建zip文件
zipFile, err := os.Create(destZip)
if err != nil {
return err
}
defer zipFile.Close()
//打开zip文件
archive := zip.NewWriter(zipFile)
defer archive.Close()
//遍历目录
filepath.Walk(srcFile, func(path string, info os.FileInfo, err error) error {
//println(path)
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = strings.TrimPrefix(path, filepath.Dir(srcFile)+string(os.PathSeparator))
println(header.Name)
// header.Name = path
if info.IsDir() {
header.Name += "\\"
} else {
header.Method = zip.Deflate
}
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
if err != nil {
return err
}
}
return err
})
return err
}
// Unzip 解压 zip 文件到指定目录(兼容 GBK 编码的中文文件名)
func Unzip(zipFile string, destDir string) error {
zipReader, err := zip.OpenReader(zipFile)
if err != nil {
return err
}
defer zipReader.Close()
for _, f := range zipReader.File {
var decodeName string
if f.Flags == 0 {
//如果标致位是0 则是默认的本地编码 默认为gbk
i := bytes.NewReader([]byte(f.Name))
decoder := transform.NewReader(i, simplifiedchinese.GB18030.NewDecoder())
content, _ := io.ReadAll(decoder)
decodeName = string(content)
} else {
//如果标志为是 1 << 11也就是 2048 则是utf-8编码
decodeName = f.Name
}
fpath := filepath.Join(destDir, decodeName)
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
} else {
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err
}
inFile, err := f.Open()
if err != nil {
return err
}
defer inFile.Close()
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer outFile.Close()
_, err = io.Copy(outFile, inFile)
if err != nil {
return err
}
}
}
return nil
}