feat(filex): 自 go-hua/common 迁入文件目录工具并独立成包
- 运行目录定位、路径存在性、带进度回调下载、Zip 打包/解压(兼容 GBK 中文文件名) - 附单元测试、examples/filex 例程与包 README
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
@@ -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`(临时目录)场景回退到工作目录。
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user