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