package utils import ( "archive/zip" "bufio" "errors" "io" "net/http" "os" "path/filepath" "strings" "sync" "github.com/spf13/cast" ) // FileStruct - File 结构体 type FileStruct struct { request *FileRequest response *FileResponse } // FileRequest - File 请求 type FileRequest struct { // 文件名 Name string // 文件路径(包含文件名) Path string // 目录路径(不包含文件名) Dir string // 文件后缀 Ext string // 限制行数 Limit int // 读取偏移量 Page int // 返回结果格式 Format string // 是否包含子目录 Sub bool // 域名 - 用于拼接文件路径 Domain string // 前缀 - 用于过滤前缀 Prefix string } // FileResponse - File 响应 type FileResponse struct { Error error Result any Text string Byte []byte Slice []any } // File - 文件系统 func File(request ...FileRequest) *FileStruct { if len(request) == 0 { request = append(request, FileRequest{}) } if Is.Empty(request[0].Limit) { request[0].Limit = 10 } if Is.Empty(request[0].Page) { request[0].Page = 1 } if Is.Empty(request[0].Format) { request[0].Format = "network" } if Is.Empty(request[0].Sub) { request[0].Sub = true } if Is.Empty(request[0].Ext) { request[0].Ext = "*" } return &FileStruct{ request: &request[0], response: &FileResponse{}, } } // Path 设置文件路径(包含文件名,如:/tmp/test.txt) func (this *FileStruct) Path(path any) *FileStruct { this.request.Path = cast.ToString(path) return this } // Dir 设置目录路径(不包含文件名,如:/tmp) func (this *FileStruct) Dir(dir any) *FileStruct { this.request.Dir = cast.ToString(dir) return this } // Name 设置文件名(不包含路径,如:test.txt) func (this *FileStruct) Name(name any) *FileStruct { this.request.Name = cast.ToString(name) return this } // Ext 设置文件后缀(如:.txt) func (this *FileStruct) Ext(ext any) *FileStruct { this.request.Ext = cast.ToString(ext) return this } // Domain 设置域名(用于拼接文件路径) func (this *FileStruct) Domain(domain any) *FileStruct { this.request.Domain = cast.ToString(domain) return this } // Prefix 设置前缀(用于过滤前缀) func (this *FileStruct) Prefix(prefix any) *FileStruct { this.request.Prefix = cast.ToString(prefix) return this } // Limit 设置限制行数 func (this *FileStruct) Limit(limit any) *FileStruct { this.request.Limit = cast.ToInt(limit) return this } // Page 设置读取偏移量 func (this *FileStruct) Page(page any) *FileStruct { this.request.Page = cast.ToInt(page) return this } // Save 保存文件 func (this *FileStruct) Save(reader io.Reader, path ...string) (result *FileResponse) { if len(path) != 0 { this.request.Path = path[0] } if Is.Empty(this.request.Path) { this.response.Error = errors.New("文件路径不能为空") return this.response } dir := filepath.Dir(this.request.Path) if _, err := os.Stat(dir); os.IsNotExist(err) { // 目录不存在,需要创建 if err := os.MkdirAll(dir, 0755); err != nil { this.response.Error = err return this.response } } // 创建文件 file, err := os.Create(this.request.Path) if err != nil { this.response.Error = err return this.response } // 关闭文件 defer func(file *os.File) { err := file.Close() if err != nil { this.response.Error = err return } }(file) // 将文件通过流的方式写入磁盘 _, err = io.Copy(file, reader) if err != nil { this.response.Error = err return this.response } return this.response } // Remove 删除文件或目录 func (this *FileStruct) Remove(path ...any) (result *FileResponse) { if len(path) != 0 { this.request.Path = cast.ToString(path[0]) } if Is.Empty(this.request.Path) { this.response.Error = errors.New("文件路径不能为空") return this.response } var err error fileInfo, err := os.Stat(this.request.Path) if err != nil { this.response.Error = err return this.response } if fileInfo.IsDir() { // 删除目录 err = os.RemoveAll(this.request.Path) } else { // 删除文件 err = os.Remove(this.request.Path) } if err != nil { this.response.Error = err return this.response } return this.response } // Download 下载文件 /** * @param path1 远程文件路径(下载地址) * @param path2 本地文件路径(保存路径,包含文件名) * @return *FileResponse * @example: * 1. item := utils.File().Download("https://inis.cn/name.zip", "public/test.zip") * 2. item := utils.File().Dir("public").Name("test.zip").Download("https://inis.cn/name.zip") * 3. item := utils.File(utils.FileRequest{ Path: "https://inis.cn/name.zip", Name: "test.zip", Dir: "public", }).Download() */ func (this *FileStruct) Download(path ...any) (result *FileResponse) { if len(path) != 0 { this.request.Path = cast.ToString(path[0]) } if Is.Empty(this.request.Path) { this.response.Error = errors.New("文件路径不能为空") return this.response } // 创建一个HTTP GET请求 req, err := http.NewRequest("GET", this.request.Path, nil) if err != nil { this.response.Error = err return this.response } // 发送HTTP请求并获取响应 resp, err := http.DefaultClient.Do(req) if err != nil { this.response.Error = err return this.response } defer resp.Body.Close() if Is.Empty(this.request.Name) { this.request.Name = filepath.Base(this.request.Path) } var savePath string if len(path) > 1 { savePath = cast.ToString(path[1]) } else { savePath = filepath.Join(this.request.Dir, this.request.Name) } // 如果目录不存在,需要创建 dir := filepath.Dir(savePath) if _, err := os.Stat(dir); os.IsNotExist(err) { if err := os.MkdirAll(dir, 0755); err != nil { this.response.Error = err return this.response } } // 创建本地文件并将HTTP响应的Body写入本地文件 saveFile, err := os.Create(savePath) if err != nil { this.response.Error = err return this.response } defer func(saveFile *os.File) { err := saveFile.Close() if err != nil { return } }(saveFile) _, err = io.Copy(saveFile, resp.Body) if err != nil { this.response.Error = err return this.response } return this.response } // Byte 获取文件字节 func (this *FileStruct) Byte(path ...any) (result *FileResponse) { if len(path) != 0 { this.request.Path = cast.ToString(path[0]) } if Is.Empty(this.request.Path) { this.response.Error = errors.New("文件路径不能为空") return this.response } // 读取文件 file, err := os.Open(this.request.Path) if err != nil { this.response.Error = err return this.response } defer func(file *os.File) { err := file.Close() if err != nil { return } }(file) // 获取文件信息 info, err := file.Stat() if err != nil { this.response.Error = err return this.response } size := info.Size() // 小于50MB,整体读取 if size < 50*1024*1024 { bytes := make([]byte, size) _, err = file.Read(bytes) if err != nil { this.response.Error = err return this.response } this.response.Byte = bytes this.response.Text = string(bytes) this.response.Result = bytes return this.response } // 大于等于50MB,分块读取 var bytes []byte buffer := make([]byte, 1024*1024) for { index, err := file.Read(buffer) if err != nil && err != io.EOF { this.response.Error = err break } bytes = append(bytes, buffer[:index]...) if err == io.EOF { break } } this.response.Byte = bytes this.response.Text = string(bytes) this.response.Result = bytes return this.response } // List 获取指定目录下的所有文件 func (this *FileStruct) List(path ...any) (result *FileResponse) { if len(path) != 0 { this.request.Path = cast.ToString(path[0]) } if Is.Empty(this.request.Dir) { this.response.Error = errors.New("目录路径不能为空") return this.response } var slice []string this.response.Error = filepath.Walk(this.request.Dir, func(path string, info os.FileInfo, err error) error { // 忽略当前目录 if info.IsDir() { return nil } // 忽略子目录 if !this.request.Sub && filepath.Dir(path) != path { return nil } // []string 转 []any var exts []any // this.request.Ext 逗号分隔的字符串 转 []string for _, val := range strings.Split(this.request.Ext, ",") { // 忽略空字符串 if Is.Empty(val) { continue } // 去除空格 exts = append(exts, strings.TrimSpace(val)) } // 忽略指定后缀 if !InArray("*", exts) && !InArray[any](filepath.Ext(path), exts) { return nil } slice = append(slice, path) return nil }) // 转码为网络路径 if this.request.Format == "network" { for key, val := range slice { slice[key] = filepath.ToSlash(val) if !Is.Empty(this.request.Domain) { slice[key] = this.request.Domain + slice[key][len(this.request.Prefix):] } } } for _, val := range slice { this.response.Slice = append(this.response.Slice, val) } this.response.Result = slice this.response.Text = strings.Join(slice, ",") this.response.Byte = []byte(this.response.Text) return this.response } // Exist 判断文件是否存在 func (this *FileStruct) Exist(path ...any) (ok bool) { if len(path) != 0 { this.request.Path = cast.ToString(path[0]) } if Is.Empty(this.request.Path) { return false } // 判断文件是否存在 if _, err := os.Stat(this.request.Path); os.IsNotExist(err) { return false } return true } // Line 按行读取文件 func (this *FileStruct) Line(path ...any) (result *FileResponse) { if len(path) != 0 { this.request.Path = cast.ToString(path[0]) } if Is.Empty(this.request.Path) { this.response.Error = errors.New("文件路径不能为空") return this.response } // 读取块 readBlock := func(file *os.File, start int, end int) ([]string, error) { lines := make([]string, 0) scanner := bufio.NewScanner(file) // 移动扫描器到指定的起始行 for i := 1; i < start && scanner.Scan(); i++ { } // 开始读取需要的行 for i := start; i <= end && scanner.Scan(); i++ { // 只把需要的行保存到切片中 if i >= start && i <= end { lines = append(lines, scanner.Text()) } } if err := scanner.Err(); err != nil { return nil, err } return lines, nil } file, err := os.Open(this.request.Path) if err != nil { this.response.Error = err return this.response } defer func(file *os.File) { err := file.Close() if err != nil { return } }(file) end := this.request.Page * this.request.Limit start := end - this.request.Limit + 1 lines := make([]string, 0) // 计算每个块的大小(以10MB为一个块) blockSize := 10 * 1024 * 1024 numBlocks := (end - start + 1) / blockSize if (end-start+1)%blockSize != 0 { numBlocks++ } // 并发读取每个块 var wg sync.WaitGroup wg.Add(numBlocks) for i := 0; i < numBlocks; i++ { go func(i int) { startLine := start + i*blockSize endLine := startLine + blockSize - 1 if endLine > end { endLine = end } blockLines, err := readBlock(file, startLine, endLine) if err != nil { this.response.Error = err return } else { lines = append(lines, blockLines...) } wg.Done() }(i) } wg.Wait() this.response.Result = lines this.response.Text = Json.Encode(this.response.Result) this.response.Byte = []byte(this.response.Text) for _, v := range lines { this.response.Slice = append(this.response.Slice, v) } return this.response } // DirInfo 获取目录信息 func (this *FileStruct) DirInfo(dir ...any) (result *FileResponse) { if len(dir) != 0 { this.request.Dir = cast.ToString(dir[0]) } if Is.Empty(this.request.Dir) { this.request.Dir = "./" } // 如果目录不是以 / 结尾,则补上 if this.request.Dir[len(this.request.Dir)-1:] != "/" { this.request.Dir += "/" } // 判断目录是否存在 if _, err := os.Stat(this.request.Dir); os.IsNotExist(err) { this.response.Error = err return this.response } // 获取目录信息 fileInfo, err := os.Stat(this.request.Dir) if err != nil { this.response.Error = err return this.response } var dirs []string var files []string // 只获取当前目录下的文件夹和文件 - 忽略子目录 fileList, err := os.ReadDir(this.request.Dir) if err != nil { this.response.Error = err return this.response } for _, file := range fileList { path := filepath.Join(this.request.Dir, file.Name()) // path 转网络路径 path = filepath.ToSlash(path) // 替换 this.request.Dir 为空字符串 path = strings.Replace(path, this.request.Dir, "", 1) if file.IsDir() { dirs = append(dirs, path) } else { files = append(files, path) } } // 获取目录信息 this.response.Result = map[string]any{ "info": fileInfo, "dirs": dirs, "files": files, } this.response.Text = Json.Encode(this.response.Result) this.response.Byte = []byte(this.response.Text) return this.response } // EnZip 压缩文件 /** * @return *FileResponse * @example: * 1. item := utils.File().Dir("public").Name("name.zip").EnZip() * 2. item := utils.File().Dir("public").Path("public/name.zip").EnZip() * 3. item := utils.File(utils.FileRequest{ Path: "public/name.zip", Dir: "public", }).EnZip() */ func (this *FileStruct) EnZip() (result *FileResponse) { if Is.Empty(this.request.Dir) { this.response.Error = errors.New("压缩目录不能为空") return this.response } if Is.Empty(this.request.Path) && !Is.Empty(this.request.Name) { // 判断 Dir 是否以 / 结尾 if this.request.Dir[len(this.request.Dir)-1:] != "/" { this.request.Dir += "/" } // 判断 Name 是否以 .zip 结尾 if this.request.Name[len(this.request.Name)-4:] != ".zip" { this.request.Name += ".zip" } this.request.Path = this.request.Dir + this.request.Name } if Is.Empty(this.request.Path) { this.response.Error = errors.New("压缩后的文件路径不能为空") return this.response } // 判断目录是否存在 if _, err := os.Stat(this.request.Dir); os.IsNotExist(err) { this.response.Error = err return this.response } var files []string err := filepath.Walk(this.request.Dir, func(path string, info os.FileInfo, err error) error { if !info.IsDir() { files = append(files, path) } this.response.Error = err return this.response.Error }) if err != nil { this.response.Error = err return this.response } zipFile, err := os.Create(this.request.Path) if err != nil { this.response.Error = err return this.response } defer func(zipFile *os.File) { err := zipFile.Close() if err != nil { this.response.Error = err return } }(zipFile) write := zip.NewWriter(zipFile) defer func(write *zip.Writer) { err := write.Close() if err != nil { this.response.Error = err return } }(write) for _, file := range files { item, err := os.Open(file) if err != nil { this.response.Error = err return this.response } defer func(item *os.File) { err := item.Close() if err != nil { this.response.Error = err return } }(item) info, err := item.Stat() if err != nil { this.response.Error = err return this.response } header, err := zip.FileInfoHeader(info) if err != nil { this.response.Error = err return this.response } header.Name = file header.Method = zip.Deflate writer, err := write.CreateHeader(header) if err != nil { this.response.Error = err return this.response } _, err = io.Copy(writer, item) if err != nil { this.response.Error = err return this.response } } this.response.Text = "1" this.response.Result = true this.response.Byte = []byte{1} return this.response } // UnZip 解压文件 /** * @return *FileResponse * @example: * 1. item := utils.File().Dir("public").Name("name.zip").UnZip() * 2. item := utils.File().Dir("public").Path("public/name.zip").UnZip() * 3. item := utils.File(utils.FileRequest{ Path: "public/name.zip", Dir: "public", }).UnZip() */ func (this *FileStruct) UnZip() (result *FileResponse) { if Is.Empty(this.request.Dir) { this.response.Error = errors.New("解压路径不能为空") return this.response } if Is.Empty(this.request.Path) && !Is.Empty(this.request.Name) { // 判断 Dir 是否以 / 结尾 if this.request.Dir[len(this.request.Dir)-1:] != "/" { this.request.Dir += "/" } // 判断 Name 是否以 .zip 结尾 if this.request.Name[len(this.request.Name)-4:] != ".zip" { this.request.Name += ".zip" } this.request.Path = this.request.Dir + this.request.Name } if Is.Empty(this.request.Path) { this.response.Error = errors.New("压缩包路径不能为空") return this.response } // 判断压缩包是否存在 if _, err := os.Stat(this.request.Path); os.IsNotExist(err) { this.response.Error = err return this.response } // 读取压缩包 read, err := zip.OpenReader(this.request.Path) if err != nil { this.response.Error = err return this.response } defer func(read *zip.ReadCloser) { err := read.Close() if err != nil { this.response.Error = err return } }(read) for _, file := range read.File { // 解压文件 err := this.extract(file, this.request.Dir) if err != nil { this.response.Error = err return this.response } } this.response.Text = "1" this.response.Result = true this.response.Byte = []byte{1} return this.response } // 提取文件 func (this *FileStruct) extract(file *zip.File, dir string) (err error) { read, err := file.Open() if err != nil { return err } defer func(read io.ReadCloser) { err := read.Close() if err != nil { } }(read) path := filepath.Join(dir, file.Name) if file.FileInfo().IsDir() { err := os.MkdirAll(path, file.Mode()) if err != nil { return err } } else { err := os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { return err } item, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) if err != nil { return err } defer func(item *os.File) { err := item.Close() if err != nil { return } }(item) _, err = io.Copy(item, read) if err != nil { return err } } return nil } // Rename 重命名文件 func (this *FileStruct) Rename(path ...any) (result *FileResponse) { if len(path) != 0 { this.request.Path = cast.ToString(path[0]) } if Is.Empty(this.request.Path) { this.response.Error = errors.New("文件路径不能为空") return this.response } if Is.Empty(this.request.Name) { this.response.Error = errors.New("文件名不能为空") return this.response } // 判断文件是否存在 if _, err := os.Stat(this.request.Path); os.IsNotExist(err) { this.response.Error = err return this.response } // 重命名文件 - 放在同一个目录下 err := os.Rename(this.request.Path, filepath.Dir(this.request.Path)+"/"+this.request.Name) if err != nil { this.response.Error = err return this.response } this.response.Text = "1" this.response.Result = true this.response.Byte = []byte{1} return this.response }