diff --git a/examples/filex/main.go b/examples/filex/main.go new file mode 100644 index 0000000..2de1d5a --- /dev/null +++ b/examples/filex/main.go @@ -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) +} diff --git a/filex/README.md b/filex/README.md new file mode 100644 index 0000000..042645d --- /dev/null +++ b/filex/README.md @@ -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`(临时目录)场景回退到工作目录。 diff --git a/filex/dir.go b/filex/dir.go new file mode 100644 index 0000000..b6fe727 --- /dev/null +++ b/filex/dir.go @@ -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 +} diff --git a/filex/downFile.go b/filex/downFile.go new file mode 100644 index 0000000..2e224b4 --- /dev/null +++ b/filex/downFile.go @@ -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 +} diff --git a/filex/filex_test.go b/filex/filex_test.go new file mode 100644 index 0000000..86e6ba5 --- /dev/null +++ b/filex/filex_test.go @@ -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)) + } +} diff --git a/filex/path.go b/filex/path.go new file mode 100644 index 0000000..a54a3c3 --- /dev/null +++ b/filex/path.go @@ -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 +} diff --git a/filex/zip.go b/filex/zip.go new file mode 100644 index 0000000..e5c4c8e --- /dev/null +++ b/filex/zip.go @@ -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 +}