From d710388a7304ac28807381328dd16897c91c773e Mon Sep 17 00:00:00 2001 From: John Guo Date: Sun, 15 Dec 2024 21:54:47 +0800 Subject: [PATCH] up --- net/ghttp/ghttp_server_handler.go | 21 +- os/gres/gres.go | 32 ++- os/gres/gres_file.go | 210 ------------------ os/gres/gres_fs.go | 25 --- os/gres/gres_fs_mixed.go | 79 ------- os/gres/gres_func.go | 184 ++------------- os/gres/gres_resource.go | 29 ++- os/gres/gres_z_unit_test.go | 19 +- os/gres/internal/defines/defines.go | 47 ++++ os/gres/internal/fs_mixed/fs_mixed.go | 109 +++++++++ .../fs_res/fs_res.go} | 121 +++++----- os/gres/internal/fs_res/fs_res_file.go | 126 +++++++++++ os/gres/internal/fs_res/fs_res_file_http.go | 86 +++++++ os/gres/internal/fs_res/fs_res_func.go | 164 ++++++++++++++ .../fs_res/fs_res_func_zip.go} | 9 +- .../fs_std/fs_std.go} | 53 ++--- os/gres/internal/fs_std/fs_std_file.go | 132 +++++++++++ os/gres/internal/fs_std/fs_std_file_http.go | 86 +++++++ os/gview/gview_parse.go | 4 +- 19 files changed, 936 insertions(+), 600 deletions(-) delete mode 100644 os/gres/gres_file.go delete mode 100644 os/gres/gres_fs.go delete mode 100644 os/gres/gres_fs_mixed.go create mode 100644 os/gres/internal/defines/defines.go create mode 100644 os/gres/internal/fs_mixed/fs_mixed.go rename os/gres/{gres_fs_res.go => internal/fs_res/fs_res.go} (56%) create mode 100644 os/gres/internal/fs_res/fs_res_file.go create mode 100644 os/gres/internal/fs_res/fs_res_file_http.go create mode 100644 os/gres/internal/fs_res/fs_res_func.go rename os/gres/{gres_func_zip.go => internal/fs_res/fs_res_func_zip.go} (95%) rename os/gres/{gres_fs_std.go => internal/fs_std/fs_std.go} (74%) create mode 100644 os/gres/internal/fs_std/fs_std_file.go create mode 100644 os/gres/internal/fs_std/fs_std_file_http.go diff --git a/net/ghttp/ghttp_server_handler.go b/net/ghttp/ghttp_server_handler.go index 34b5a5596..64956da62 100644 --- a/net/ghttp/ghttp_server_handler.go +++ b/net/ghttp/ghttp_server_handler.go @@ -287,22 +287,30 @@ func (s *Server) searchStaticFile(uri string) *staticFile { func (s *Server) serveFile(r *Request, f *staticFile, allowIndex ...bool) { // Use resource file from memory. if f.File != nil { + httpFile, err := f.File.HttpFile() + if err != nil { + intlog.Errorf(r.Context(), "serving file failed: %+v", err) + r.Response.WriteStatus(http.StatusInternalServerError) + return + } + defer httpFile.Close() if f.IsDir { if s.config.IndexFolder || (len(allowIndex) > 0 && allowIndex[0]) { - s.listDir(r, f.File) + s.listDir(r, httpFile) } else { r.Response.WriteStatus(http.StatusForbidden) } } else { info := f.File.FileInfo() - r.Response.ServeContent(info.Name(), info.ModTime(), f.File) + r.Response.ServeContent(info.Name(), info.ModTime(), httpFile) } return } // Use file from dist. file, err := os.Open(f.Path) if err != nil { - r.Response.WriteStatus(http.StatusForbidden) + intlog.Errorf(r.Context(), "open file failed: %+v", err) + r.Response.WriteStatus(http.StatusInternalServerError) return } defer func() { @@ -313,7 +321,12 @@ func (s *Server) serveFile(r *Request, f *staticFile, allowIndex ...bool) { // It ignores all custom buffer content and uses the file content. r.Response.ClearBuffer() - info, _ := file.Stat() + info, err := file.Stat() + if err != nil { + intlog.Errorf(r.Context(), "getting file info failed: %+v", err) + r.Response.WriteStatus(http.StatusInternalServerError) + return + } if info.IsDir() { if s.config.IndexFolder || (len(allowIndex) > 0 && allowIndex[0]) { s.listDir(r, file) diff --git a/os/gres/gres.go b/os/gres/gres.go index d9f2a5d52..52c3e7ee5 100644 --- a/os/gres/gres.go +++ b/os/gres/gres.go @@ -7,14 +7,44 @@ // Package gres provides resource management and packing/unpacking feature between files and bytes. package gres +import ( + "io/fs" + + "github.com/gogf/gf/v2/os/gres/internal/defines" + "github.com/gogf/gf/v2/os/gres/internal/fs_mixed" + "github.com/gogf/gf/v2/os/gres/internal/fs_res" + "github.com/gogf/gf/v2/os/gres/internal/fs_std" +) + +type ( + FS = defines.FS + File = defines.File + // Deprecated: used PackOption instead. + Option = defines.PackOption + PackOption = defines.PackOption + ExportOption = defines.ExportOption +) + var ( // Default resource file system. - defaultFS = NewResFS() + defaultFS = fs_res.NewFS() // Default resource object. defaultResource = Instance() ) +func NewResFS() *fs_res.FS { + return fs_res.NewFS() +} + +func NewStdFS(fs fs.FS) *fs_std.FS { + return fs_std.NewFS(fs) +} + +func NewMixedFS(resFS *fs_res.FS, stdFs fs.FS) *fs_mixed.FS { + return fs_mixed.NewFS(resFS, stdFs) +} + // Add unpacks and adds the `content` into the default resource object. // The unnecessary parameter `prefix` indicates the prefix // for each file storing into current resource object. diff --git a/os/gres/gres_file.go b/os/gres/gres_file.go deleted file mode 100644 index f3ec40b4f..000000000 --- a/os/gres/gres_file.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. -// -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, -// You can obtain one at https://github.com/gogf/gf. - -package gres - -import ( - "bytes" - "io" - "io/fs" - "net/http" - "os" - "sync" - "time" - - "github.com/gogf/gf/v2/encoding/gjson" - "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/os/gfile" - "github.com/gogf/gf/v2/text/gstr" -) - -// A File provides access to a single file. -// The File interface is the minimum implementation required of the file. -// Directory files should also implement [ReadDirFile]. -// A file may implement [io.ReaderAt] or [io.Seeker] as optimizations. -type File interface { - http.File - Name() string - Path() string - Content() []byte - FileInfo() os.FileInfo - Export(dst string, option ...ExportOption) error -} - -// File implements the interface fs.File. -type localFile struct { - name string // Name is the file name - path string // Path is the file path - content []byte // file content - file os.FileInfo // FileInfo is the underlying file info - fs FS // FS is the file system that contains this file - mu sync.Mutex // mu protects concurrent access to the file -} - -// Name returns the name of the file -func (f *localFile) Name() string { - return f.name -} - -// Path returns the path of the file -func (f *localFile) Path() string { - return f.path -} - -// FileInfo returns an os.FileInfo describing this file -func (f *localFile) FileInfo() os.FileInfo { - return f.file -} - -// Stat returns the FileInfo structure describing file. -func (f *localFile) Stat() (os.FileInfo, error) { - return f.file, nil -} - -// Content returns the file content -func (f *localFile) Content() []byte { - return f.content -} - -// Export exports and saves all its sub files to specified system path `dst` recursively. -func (f *localFile) Export(dst string, option ...ExportOption) error { - var ( - err error - name string - path string - exportOption ExportOption - exportFiles []File - ) - if f.FileInfo().IsDir() { - exportFiles = f.fs.ScanDir(f.path, "*", true) - } else { - exportFiles = append(exportFiles, f) - } - - if len(option) > 0 { - exportOption = option[0] - } - for _, exportFile := range exportFiles { - name = exportFile.Name() - if exportOption.RemovePrefix != "" { - name = gstr.TrimLeftStr(name, exportOption.RemovePrefix) - } - name = gstr.Trim(name, `\/`) - if name == "" { - continue - } - path = gfile.Join(dst, name) - if f.FileInfo().IsDir() { - err = gfile.Mkdir(path) - } else { - err = gfile.PutBytes(path, exportFile.Content()) - } - if err != nil { - return err - } - } - return nil -} - -// Close implements interface of http.File. -func (f *localFile) Close() error { - return nil -} - -// Readdir implements Readdir interface of http.File. -func (f *localFile) Readdir(count int) ([]os.FileInfo, error) { - files := f.fs.ScanDir(f.Name(), "*", false) - if len(files) > 0 { - if count <= 0 || count > len(files) { - count = len(files) - } - infos := make([]os.FileInfo, count) - for k, v := range files { - infos[k] = v.FileInfo() - } - return infos, nil - } - return nil, nil -} - -// Read implements the io.Reader interface. -func (f *localFile) Read(b []byte) (n int, err error) { - reader := bytes.NewReader(f.Content()) - if n, err = reader.Read(b); err != nil { - err = gerror.Wrapf(err, `read content failed`) - } - return -} - -// Seek implements the io.Seeker interface. -func (f *localFile) Seek(offset int64, whence int) (n int64, err error) { - reader := bytes.NewReader(f.Content()) - if n, err = reader.Seek(offset, whence); err != nil { - err = gerror.Wrapf(err, `seek failed for offset %d, whence %d`, offset, whence) - } - return -} - -func (f *localFile) getReader() (io.ReadSeeker, error) { - return bytes.NewReader(f.Content()), nil -} - -// MarshalJSON implements the interface MarshalJSON for json.Marshal. -func (f *localFile) MarshalJSON() ([]byte, error) { - info := f.FileInfo() - return gjson.Marshal(map[string]interface{}{ - "name": f.name, - "path": f.path, - "size": info.Size(), - "time": info.ModTime(), - "isDir": info.IsDir(), - "content": f.Content(), - }) -} - -// fileInfo is the internal implementation of os.FileInfo interface. -type fileInfo struct { - file *localFile -} - -// Name returns the base name of the file. -func (fi *fileInfo) Name() string { - return fi.file.Name() -} - -// Size returns the size in bytes of the file. -func (fi *fileInfo) Size() int64 { - return int64(len(fi.file.Content())) -} - -// Mode returns the file mode bits. -func (fi *fileInfo) Mode() fs.FileMode { - if fi.IsDir() { - return os.ModeDir | 0755 - } - return 0644 -} - -// ModTime returns the modification time. -func (fi *fileInfo) ModTime() time.Time { - if fi.file.file != nil { - return fi.file.file.ModTime() - } - return time.Now() -} - -// IsDir reports whether the file is a directory. -func (fi *fileInfo) IsDir() bool { - if fi.file.file != nil { - return fi.file.file.IsDir() - } - return false -} - -// Sys returns the underlying data source. -func (fi *fileInfo) Sys() interface{} { - return nil -} diff --git a/os/gres/gres_fs.go b/os/gres/gres_fs.go deleted file mode 100644 index 59944e263..000000000 --- a/os/gres/gres_fs.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. -// -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, -// You can obtain one at https://github.com/gogf/gf. - -package gres - -// FS is the interface that defines a virtual file system. -type FS interface { - // Get returns the file with given path. - Get(path string) File - - // IsEmpty checks and returns whether the resource is empty. - IsEmpty() bool - - // ScanDir returns the files under the given path, - // the parameter `path` should be a folder type. - ScanDir(path string, pattern string, recursive ...bool) []File -} - -// ExportOption contains options for Export. -type ExportOption struct { - RemovePrefix string // Remove the prefix from source file before export. -} diff --git a/os/gres/gres_fs_mixed.go b/os/gres/gres_fs_mixed.go deleted file mode 100644 index 10ded9ff1..000000000 --- a/os/gres/gres_fs_mixed.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. -// -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, -// You can obtain one at https://github.com/gogf/gf. - -package gres - -import ( - "io/fs" - "sort" -) - -// MixedFS implements the FS interface by combining StdFS and ResFS. -// It prioritizes using StdFS and falls back to ResFS when file not found. -type MixedFS struct { - stdFS *StdFS - resFS *ResFS -} - -var _ FS = (*MixedFS)(nil) - -// NewMixedFS creates and returns a new MixedFS. -func NewMixedFS(stdFs fs.FS, resFS *ResFS) *MixedFS { - return &MixedFS{ - resFS: resFS, - stdFS: NewStdFS(stdFs), - } -} - -// Get returns the file with given path. -func (fs *MixedFS) Get(path string) File { - if file := fs.resFS.Get(path); file != nil { - return file - } - return fs.stdFS.Get(path) -} - -// IsEmpty checks and returns whether the resource is empty. -func (fs *MixedFS) IsEmpty() bool { - return fs.resFS.IsEmpty() && fs.stdFS.IsEmpty() -} - -// ScanDir returns the files under the given path, -// the parameter `path` should be a folder type. -func (fs *MixedFS) ScanDir(path string, pattern string, recursive ...bool) []File { - var ( - filesMap = make(map[string]File) - files = make([]File, 0) - ) - - // Get files from ResFS - resFiles := fs.resFS.ScanDir(path, pattern, recursive...) - for _, file := range resFiles { - if _, exists := filesMap[file.Path()]; !exists { - filesMap[file.Path()] = file - } - } - - // Get files from StdFS - stdFiles := fs.stdFS.ScanDir(path, pattern, recursive...) - for _, file := range stdFiles { - filesMap[file.Path()] = file - } - - // Convert map to slice and sort by path - paths := make([]string, 0, len(filesMap)) - for filePath := range filesMap { - paths = append(paths, filePath) - } - sort.Strings(paths) - - // Build sorted result - for _, filePath := range paths { - files = append(files, filesMap[filePath]) - } - - return files -} diff --git a/os/gres/gres_func.go b/os/gres/gres_func.go index 4d8c2e525..833be66b0 100644 --- a/os/gres/gres_func.go +++ b/os/gres/gres_func.go @@ -7,39 +7,10 @@ package gres import ( - "archive/zip" - "bytes" - "encoding/hex" - "fmt" - "sync" - - "github.com/gogf/gf/v2/encoding/gbase64" - "github.com/gogf/gf/v2/encoding/gcompress" - "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gfile" - "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/os/gres/internal/fs_res" ) -const ( - packedGoSourceTemplate = ` -package %s - -import "github.com/gogf/gf/v2/os/gres" - -func init() { - if err := gres.Add("%s"); err != nil { - panic("add binary content to resource manager failed: " + err.Error()) - } -} -` -) - -// Option contains the extra options for Pack functions. -type Option struct { - Prefix string // The file path prefix for each file item in resource manager. - KeepPath bool // Keep the passed path when packing, usually for relative path. -} - // Pack packs the path specified by `srcPaths` into bytes. // The unnecessary parameter `keyPrefix` indicates the prefix for each file // packed into the result bytes. @@ -48,26 +19,13 @@ type Option struct { // // Deprecated: use PackWithOption instead. func Pack(srcPaths string, keyPrefix ...string) ([]byte, error) { - option := Option{} + option := PackOption{} if len(keyPrefix) > 0 && keyPrefix[0] != "" { option.Prefix = keyPrefix[0] } return PackWithOption(srcPaths, option) } -// PackWithOption packs the path specified by `srcPaths` into bytes. -// -// Note that parameter `srcPaths` supports multiple paths join with ','. -func PackWithOption(srcPaths string, option Option) ([]byte, error) { - var buffer = bytes.NewBuffer(nil) - err := zipPathWriter(srcPaths, buffer, option) - if err != nil { - return nil, err - } - // Gzip the data bytes to reduce the size. - return gcompress.Gzip(buffer.Bytes(), 9) -} - // PackToFile packs the path specified by `srcPaths` to target file `dstPath`. // The unnecessary parameter `keyPrefix` indicates the prefix for each file // packed into the result bytes. @@ -83,17 +41,6 @@ func PackToFile(srcPaths, dstPath string, keyPrefix ...string) error { return gfile.PutBytes(dstPath, data) } -// PackToFileWithOption packs the path specified by `srcPaths` to target file `dstPath`. -// -// Note that parameter `srcPaths` supports multiple paths join with ','. -func PackToFileWithOption(srcPaths, dstPath string, option Option) error { - data, err := PackWithOption(srcPaths, option) - if err != nil { - return err - } - return gfile.PutBytes(dstPath, data) -} - // PackToGoFile packs the path specified by `srcPaths` to target go file `goFilePath` // with given package name `pkgName`. // @@ -104,124 +51,41 @@ func PackToFileWithOption(srcPaths, dstPath string, option Option) error { // // Deprecated: use PackToGoFileWithOption instead. func PackToGoFile(srcPath, goFilePath, pkgName string, keyPrefix ...string) error { - data, err := Pack(srcPath, keyPrefix...) - if err != nil { - return err + option := PackOption{} + if len(keyPrefix) > 0 && keyPrefix[0] != "" { + option.Prefix = keyPrefix[0] } - return gfile.PutContents( - goFilePath, - fmt.Sprintf(gstr.TrimLeft(packedGoSourceTemplate), pkgName, gbase64.EncodeToString(data)), - ) + return PackToGoFileWithOption(srcPath, goFilePath, pkgName, option) +} + +// PackWithOption packs the path specified by `srcPaths` into bytes. +// +// Note that parameter `srcPaths` supports multiple paths join with ','. +func PackWithOption(srcPaths string, option PackOption) ([]byte, error) { + return fs_res.PackWithOption(srcPaths, option) +} + +// PackToFileWithOption packs the path specified by `srcPaths` to target file `dstPath`. +// +// Note that parameter `srcPaths` supports multiple paths join with ','. +func PackToFileWithOption(srcPaths, dstPath string, option PackOption) error { + return fs_res.PackToFileWithOption(srcPaths, dstPath, option) } // PackToGoFileWithOption packs the path specified by `srcPaths` to target go file `goFilePath` // with given package name `pkgName`. // // Note that parameter `srcPaths` supports multiple paths join with ','. -func PackToGoFileWithOption(srcPath, goFilePath, pkgName string, option Option) error { - data, err := PackWithOption(srcPath, option) - if err != nil { - return err - } - return gfile.PutContents( - goFilePath, - fmt.Sprintf(gstr.TrimLeft(packedGoSourceTemplate), pkgName, gbase64.EncodeToString(data)), - ) +func PackToGoFileWithOption(srcPath, goFilePath, pkgName string, option PackOption) error { + return fs_res.PackToGoFileWithOption(srcPath, goFilePath, pkgName, option) } // Unpack unpacks the content specified by `path` to []*File. func Unpack(path string) ([]File, error) { - realPath, err := gfile.Search(path) - if err != nil { - return nil, err - } - return UnpackContent(gfile.GetContents(realPath)) + return fs_res.Unpack(path) } // UnpackContent unpacks the content to []File. func UnpackContent(content string) ([]File, error) { - var ( - err error - data []byte - ) - if isHexStr(content) { - // It here keeps compatible with old version packing string using hex string. - // TODO remove this support in the future. - data, err = gcompress.UnGzip(hexStrToBytes(content)) - if err != nil { - return nil, err - } - } else if isBase64(content) { - // New version packing string using base64. - b, err := gbase64.DecodeString(content) - if err != nil { - return nil, err - } - data, err = gcompress.UnGzip(b) - if err != nil { - return nil, err - } - } else { - data, err = gcompress.UnGzip([]byte(content)) - if err != nil { - return nil, err - } - } - reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) - if err != nil { - err = gerror.Wrapf(err, `create zip reader failed`) - return nil, err - } - array := make([]File, len(reader.File)) - for i, file := range reader.File { - array[i] = &localFile{ - name: file.Name, - path: file.Name, - content: data, - file: file.FileInfo(), - fs: nil, - mu: sync.Mutex{}, - } - } - return array, nil -} - -// isBase64 checks and returns whether given content `s` is base64 string. -// It returns true if `s` is base64 string, or false if not. -func isBase64(s string) bool { - var r bool - for i := 0; i < len(s); i++ { - r = (s[i] >= '0' && s[i] <= '9') || - (s[i] >= 'a' && s[i] <= 'z') || - (s[i] >= 'A' && s[i] <= 'Z') || - (s[i] == '+' || s[i] == '-') || - (s[i] == '_' || s[i] == '/') || s[i] == '=' - if !r { - return false - } - } - return true -} - -// isHexStr checks and returns whether given content `s` is hex string. -// It returns true if `s` is hex string, or false if not. -func isHexStr(s string) bool { - var r bool - for i := 0; i < len(s); i++ { - r = (s[i] >= '0' && s[i] <= '9') || - (s[i] >= 'a' && s[i] <= 'f') || - (s[i] >= 'A' && s[i] <= 'F') - if !r { - return false - } - } - return true -} - -// hexStrToBytes converts hex string content to []byte. -func hexStrToBytes(s string) []byte { - src := []byte(s) - dst := make([]byte, hex.DecodedLen(len(src))) - _, _ = hex.Decode(dst, src) - return dst + return fs_res.UnpackContent(content) } diff --git a/os/gres/gres_resource.go b/os/gres/gres_resource.go index f490b96ca..f8f280fc8 100644 --- a/os/gres/gres_resource.go +++ b/os/gres/gres_resource.go @@ -7,10 +7,12 @@ package gres import ( - "context" + "fmt" + "os" "strings" - "github.com/gogf/gf/v2/internal/intlog" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gtime" ) // Resource implements the FS interface. @@ -76,7 +78,7 @@ func (r *Resource) GetContent(path string) []byte { // Contains checks whether the `path` exists in current resource object. func (r *Resource) Contains(path string) bool { - return r.Get(path) == nil + return r.Get(path) != nil } // IsEmpty checks and returns whether the resource manager is empty. @@ -116,12 +118,19 @@ func (r *Resource) Export(src, dst string, option ...ExportOption) error { // Dump prints the files of current resource object. func (r *Resource) Dump() { - var ctx = context.TODO() - if r.IsEmpty() { - intlog.Printf(ctx, "empty resource") - } else { - for _, v := range r.ScanDir("/", "*", true) { - intlog.Printf(ctx, "%s %d", v.Path(), v.FileInfo().Size()) - } + var ( + count int + info os.FileInfo + ) + for _, file := range r.fs.ListAll() { + count++ + info = file.FileInfo() + fmt.Printf( + "%v %8s %s\n", + gtime.New(info.ModTime()).ISO8601(), + gfile.FormatSize(info.Size()), + file.Name(), + ) } + fmt.Printf("TOTAL FILES: %d\n", count) } diff --git a/os/gres/gres_z_unit_test.go b/os/gres/gres_z_unit_test.go index 8b225d02e..929a565dd 100644 --- a/os/gres/gres_z_unit_test.go +++ b/os/gres/gres_z_unit_test.go @@ -25,7 +25,7 @@ func Test_PackFolderToGoFile(t *testing.T) { srcPath = gtest.DataPath("files") goFilePath = gfile.Temp(gtime.TimestampNanoStr(), "testdata.go") pkgName = "testdata" - err = gres.PackToGoFile(srcPath, goFilePath, pkgName) + err = gres.PackToGoFileWithOption(srcPath, goFilePath, pkgName, gres.PackOption{}) ) t.AssertNil(err) _ = gfile.Remove(goFilePath) @@ -33,7 +33,7 @@ func Test_PackFolderToGoFile(t *testing.T) { } func Test_PackMultiFilesToGoFile(t *testing.T) { - gres.Dump() + // gres.Dump() gtest.C(t, func(t *gtest.T) { var ( srcPath = gtest.DataPath("files") @@ -42,11 +42,11 @@ func Test_PackMultiFilesToGoFile(t *testing.T) { array, err = gfile.ScanDir(srcPath, "*", false) ) t.AssertNil(err) - err = gres.PackToGoFile(strings.Join(array, ","), goFilePath, pkgName) + err = gres.PackToGoFileWithOption(strings.Join(array, ","), goFilePath, pkgName, gres.PackOption{}) t.AssertNil(err) defer gfile.Remove(goFilePath) - t.AssertNil(gfile.CopyFile(goFilePath, gtest.DataPath("data/data.go"))) + //t.AssertNil(gfile.CopyFile(goFilePath, gtest.DataPath("data/data.go"))) }) } @@ -140,13 +140,12 @@ func Test_Unpack(t *testing.T) { } func Test_Basic(t *testing.T) { - // gres.Dump() + //gres.Dump() gtest.C(t, func(t *gtest.T) { t.Assert(gres.Get("none"), nil) t.Assert(gres.Contains("none"), false) t.Assert(gres.Contains("dir1"), true) }) - gtest.C(t, func(t *gtest.T) { path := "dir1/test1" file := gres.Get(path) @@ -158,8 +157,11 @@ func Test_Basic(t *testing.T) { t.Assert(info.IsDir(), false) t.Assert(info.Name(), "test1") + r, err := file.Open() + t.AssertNil(err) + b := make([]byte, 5) - n, err := file.Read(b) + n, err := r.Read(b) t.Assert(n, 5) t.AssertNil(err) t.Assert(string(b), "test1") @@ -210,9 +212,9 @@ func Test_ScanDir(t *testing.T) { gtest.C(t, func(t *gtest.T) { path := "dir1" files := gres.ScanDir(path, "*", false) - t.AssertNE(files, nil) t.Assert(len(files), 2) }) + gtest.C(t, func(t *gtest.T) { path := "dir1" files := gres.ScanDir(path, "*", true) @@ -272,6 +274,7 @@ func Test_Export(t *testing.T) { name := `template-res/index.html` t.Assert(gfile.GetContents(gfile.Join(dst, name)), gres.GetContent(name)) }) + gtest.C(t, func(t *gtest.T) { var ( src = `template-res` diff --git a/os/gres/internal/defines/defines.go b/os/gres/internal/defines/defines.go new file mode 100644 index 000000000..cf32f4668 --- /dev/null +++ b/os/gres/internal/defines/defines.go @@ -0,0 +1,47 @@ +package defines + +import ( + "io" + "net/http" + "os" +) + +// A File provides access to a single file. +// The File interface is the minimum implementation required of the file. +// Directory files should also implement [ReadDirFile]. +// A file may implement [io.ReaderAt] or [io.Seeker] as optimizations. +type File interface { + // Name returns the path of the file. + Name() string + Open() (io.ReadCloser, error) + Content() []byte + FileInfo() os.FileInfo + Export(dst string, option ...ExportOption) error + HttpFile() (http.File, error) +} + +// FS is the interface that defines a virtual file system. +type FS interface { + // Get returns the file with given path. + Get(path string) File + + // IsEmpty checks and returns whether the FS is empty. + IsEmpty() bool + + // ScanDir returns the files under the given path, + // the parameter `path` should be a folder type. + ScanDir(path string, pattern string, recursive ...bool) []File + + ListAll() []File +} + +// PackOption contains the extra options for Pack functions. +type PackOption struct { + Prefix string // The file path prefix for each file item in resource manager. + KeepPath bool // Keep the passed path when packing, usually for relative path. +} + +// ExportOption contains options for Export. +type ExportOption struct { + RemovePrefix string // Remove the prefix from source file before export. +} diff --git a/os/gres/internal/fs_mixed/fs_mixed.go b/os/gres/internal/fs_mixed/fs_mixed.go new file mode 100644 index 000000000..80a3e9653 --- /dev/null +++ b/os/gres/internal/fs_mixed/fs_mixed.go @@ -0,0 +1,109 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package fs_mixed + +import ( + "io/fs" + "sort" + + "github.com/gogf/gf/v2/os/gres/internal/defines" + "github.com/gogf/gf/v2/os/gres/internal/fs_res" + "github.com/gogf/gf/v2/os/gres/internal/fs_std" +) + +// FS implements the FS interface by combining fs_res.FS and StdFS. +// It prioritizes using fs_res.FS and falls back to StdFS when file not found. +type FS struct { + resFS *fs_res.FS + stdFS *fs_std.FS +} + +var _ defines.FS = (*FS)(nil) + +// NewFS creates and returns a new FS. +func NewFS(resFS *fs_res.FS, stdFs fs.FS) *FS { + return &FS{ + resFS: resFS, + stdFS: fs_std.NewFS(stdFs), + } +} + +// Get returns the file with given path. +func (fs *FS) Get(path string) defines.File { + if file := fs.resFS.Get(path); file != nil { + return file + } + return fs.stdFS.Get(path) +} + +// IsEmpty checks and returns whether the resource is empty. +func (fs *FS) IsEmpty() bool { + return fs.resFS.IsEmpty() && fs.stdFS.IsEmpty() +} + +// ScanDir returns the files under the given path, +// the parameter `path` should be a folder type. +func (fs *FS) ScanDir(path string, pattern string, recursive ...bool) []defines.File { + var ( + filesMap = make(map[string]defines.File) + files = make([]defines.File, 0) + ) + // Get files from StdFS + stdFiles := fs.stdFS.ScanDir(path, pattern, recursive...) + for _, file := range stdFiles { + filesMap[file.Name()] = file + } + + // Get files from fs_res.FS + resFiles := fs.resFS.ScanDir(path, pattern, recursive...) + for _, file := range resFiles { + if _, exists := filesMap[file.Name()]; !exists { + filesMap[file.Name()] = file + } + } + + // Convert map to slice and sort by path + paths := make([]string, 0, len(filesMap)) + for filePath := range filesMap { + paths = append(paths, filePath) + } + sort.Strings(paths) + + // Build sorted result + for _, filePath := range paths { + files = append(files, filesMap[filePath]) + } + + return files +} + +func (fs *FS) ListAll() []defines.File { + var ( + resAll = fs.resFS.ListAll() + stdAll = fs.resFS.ListAll() + filesMap = make(map[string]defines.File) + files = make([]defines.File, 0) + ) + for _, file := range stdAll { + filesMap[file.Name()] = file + } + for _, file := range resAll { + filesMap[file.Name()] = file + } + // Convert map to slice and sort by path + paths := make([]string, 0, len(filesMap)) + for filePath := range filesMap { + paths = append(paths, filePath) + } + sort.Strings(paths) + + // Build sorted result + for _, filePath := range paths { + files = append(files, filesMap[filePath]) + } + return files +} diff --git a/os/gres/gres_fs_res.go b/os/gres/internal/fs_res/fs_res.go similarity index 56% rename from os/gres/gres_fs_res.go rename to os/gres/internal/fs_res/fs_res.go index a793bcace..65f54c9f6 100644 --- a/os/gres/gres_fs_res.go +++ b/os/gres/internal/fs_res/fs_res.go @@ -4,34 +4,33 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package gres +package fs_res import ( "context" - "os" "path/filepath" "strings" "github.com/gogf/gf/v2/container/gtree" "github.com/gogf/gf/v2/internal/intlog" "github.com/gogf/gf/v2/os/gfile" - "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/os/gres/internal/defines" ) -// ResFS implements the FS interface using the default resource implementation. -type ResFS struct { +// FS implements the FS interface using the default resource implementation. +type FS struct { tree *gtree.BTree // The tree storing all resource files. } -var _ FS = (*ResFS)(nil) +var _ defines.FS = (*FS)(nil) const ( defaultTreeM = 100 ) -// NewResFS creates and returns a new ResFS. -func NewResFS() *ResFS { - return &ResFS{ +// NewFS creates and returns a new FS using resource manager. +func NewFS() *FS { + return &FS{ tree: gtree.NewBTree(defaultTreeM, func(v1, v2 interface{}) int { return strings.Compare(v1.(string), v2.(string)) }), @@ -39,7 +38,7 @@ func NewResFS() *ResFS { } // Get returns the file with given path. -func (fs *ResFS) Get(path string) File { +func (fs *FS) Get(path string) defines.File { if path == "" { return nil } @@ -52,24 +51,24 @@ func (fs *ResFS) Get(path string) File { } result := fs.tree.Get(path) if result != nil { - return result.(File) + return result.(defines.File) } return nil } // IsEmpty checks and returns whether the resource is empty. -func (fs *ResFS) IsEmpty() bool { +func (fs *FS) IsEmpty() bool { return fs.tree.IsEmpty() } // ScanDir returns the files under the given path, // the parameter `path` should be a folder type. -func (fs *ResFS) ScanDir(path string, pattern string, recursive ...bool) []File { +func (fs *FS) ScanDir(path string, pattern string, recursive ...bool) []defines.File { isRecursive := false if len(recursive) > 0 { isRecursive = recursive[0] } - return fs.doScanDir(path, pattern, isRecursive) + return fs.doScanDir(path, pattern, isRecursive, false) } // doScanDir is an internal method which scans directory @@ -79,7 +78,7 @@ func (fs *ResFS) ScanDir(path string, pattern string, recursive ...bool) []File // using the ',' symbol to separate multiple patterns. // // It scans directory recursively if given parameter `recursive` is true. -func (fs *ResFS) doScanDir(path string, pattern string, recursive bool) []File { +func (fs *FS) doScanDir(path string, pattern string, recursive bool, onlyFile bool) []defines.File { path = strings.ReplaceAll(path, "\\", "/") path = strings.ReplaceAll(path, "//", "/") if path != "/" { @@ -88,45 +87,47 @@ func (fs *ResFS) doScanDir(path string, pattern string, recursive bool) []File { } } var ( - files = make([]File, 0) + name = "" + files = make([]defines.File, 0) + length = len(path) patterns = strings.Split(pattern, ",") ) for i := 0; i < len(patterns); i++ { patterns[i] = strings.TrimSpace(patterns[i]) } - // Get root directory - rootFile := fs.Get(path) - if rootFile == nil || !rootFile.FileInfo().IsDir() { - return files - } - - // Walk through the tree to find matching files - fs.tree.IteratorAsc(func(key, value interface{}) bool { - var ( - file = value.(File) - filePath = key.(string) - ) - - // Skip if not under the target path - if !strings.HasPrefix(filePath, path) { + // Used for type checking for first entry. + first := true + fs.tree.IteratorFrom(path, true, func(key, value interface{}) bool { + if first { + if !value.(defines.File).FileInfo().IsDir() { + return false + } + first = false + } + if onlyFile && value.(defines.File).FileInfo().IsDir() { + return true + } + name = key.(string) + if len(name) <= length { + return true + } + if path != name[:length] { + return false + } + // To avoid of, eg: /i18n and /i18n-dir + if !first && name[length] != '/' { return true } - - // Skip if not recursive and file is in subdirectory if !recursive { - relPath := strings.TrimPrefix(filePath, path) - if strings.Contains(relPath, "/") { + if strings.IndexByte(name[length+1:], '/') != -1 { return true } } - - // Check if file matches any pattern - name := gfile.Basename(filePath) for _, p := range patterns { - if match, _ := filepath.Match(p, name); match { - files = append(files, file) - break + if match, err := filepath.Match(p, gfile.Basename(name)); err == nil && match { + files = append(files, value.(defines.File)) + return true } } return true @@ -134,8 +135,17 @@ func (fs *ResFS) doScanDir(path string, pattern string, recursive bool) []File { return files } -// Add adds the `content` into current ResFS with given `prefix`. -func (fs *ResFS) Add(content string, prefix ...string) error { +func (fs *FS) ListAll() []defines.File { + files := make([]defines.File, 0) + fs.tree.Iterator(func(key, value interface{}) bool { + files = append(files, value.(defines.File)) + return true + }) + return files +} + +// Add adds the `content` into current FS with given `prefix`. +func (fs *FS) Add(content string, prefix ...string) error { files, err := UnpackContent(content) if err != nil { intlog.Printf(context.TODO(), "Add resource files failed: %v", err) @@ -146,35 +156,18 @@ func (fs *ResFS) Add(content string, prefix ...string) error { namePrefix = prefix[0] } for i := 0; i < len(files); i++ { - files[i].(*localFile).fs = fs - fs.tree.Set(namePrefix+files[i].Path(), files[i]) + files[i].(*FileImp).fs = fs + fs.tree.Set(namePrefix+files[i].Name(), files[i]) } intlog.Printf(context.TODO(), "Add %d files to resource manager", fs.tree.Size()) return nil } -// Load loads, unpacks and adds the data from `path` into ResFS. -func (fs *ResFS) Load(path string, prefix ...string) error { +// Load loads, unpacks and adds the data from `path` into FS. +func (fs *FS) Load(path string, prefix ...string) error { realPath, err := gfile.Search(path) if err != nil { return err } return fs.Add(gfile.GetContents(realPath), prefix...) } - -// Dump prints the files of ResFS. -func (fs *ResFS) Dump() { - var info os.FileInfo - fs.tree.Iterator(func(key, value interface{}) bool { - info = value.(File).FileInfo() - intlog.Printf( - context.TODO(), - "%v %8s %s", - gtime.New(info.ModTime()).ISO8601(), - gfile.FormatSize(info.Size()), - key, - ) - return true - }) - intlog.Printf(context.TODO(), "TOTAL FILES: %d", fs.tree.Size()) -} diff --git a/os/gres/internal/fs_res/fs_res_file.go b/os/gres/internal/fs_res/fs_res_file.go new file mode 100644 index 000000000..57e01711b --- /dev/null +++ b/os/gres/internal/fs_res/fs_res_file.go @@ -0,0 +1,126 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package fs_res + +import ( + "archive/zip" + "context" + "io" + "net/http" + "os" + "time" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/internal/intlog" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gres/internal/defines" + "github.com/gogf/gf/v2/text/gstr" +) + +// FileImp implements the interface fs.File. +type FileImp struct { + file *zip.File // File is the underlying file object + fs defines.FS // FS is the file system that contains this file +} + +var _ defines.File = (*FileImp)(nil) + +func (f *FileImp) Name() string { + return f.file.Name +} + +// FileInfo returns an os.FileInfo describing this file +func (f *FileImp) FileInfo() os.FileInfo { + return f.file.FileInfo() +} + +// Stat returns the FileInfo structure describing file. +func (f *FileImp) Stat() (os.FileInfo, error) { + return f.FileInfo(), nil +} + +func (f *FileImp) Open() (io.ReadCloser, error) { + return f.file.Open() +} + +func (f *FileImp) HttpFile() (http.File, error) { + return NewHttpFile(f.fs, f.file) +} + +// Content returns the file content +func (f *FileImp) Content() []byte { + readCloser, err := f.file.Open() + if err != nil { + intlog.Error(context.Background(), err) + return nil + } + defer readCloser.Close() + content, err := io.ReadAll(readCloser) + if err != nil { + intlog.Error(context.Background(), err) + return nil + } + return content +} + +// Export exports and saves all its sub files to specified system path `dst` recursively. +func (f *FileImp) Export(dst string, option ...defines.ExportOption) error { + var ( + err error + name string + path string + exportOption defines.ExportOption + exportFiles []defines.File + ) + if f.FileInfo().IsDir() { + exportFiles = f.fs.ScanDir(f.Name(), "*", true) + } else { + exportFiles = append(exportFiles, f) + } + + if len(option) > 0 { + exportOption = option[0] + } + for _, exportFile := range exportFiles { + name = exportFile.Name() + if exportOption.RemovePrefix != "" { + name = gstr.TrimLeftStr(name, exportOption.RemovePrefix) + } + name = gstr.Trim(name, `\/`) + if name == "" { + continue + } + path = gfile.Join(dst, name) + if exportFile.FileInfo().IsDir() { + err = gfile.Mkdir(path) + } else { + err = gfile.PutBytes(path, exportFile.Content()) + } + if err != nil { + return err + } + } + return nil +} + +type jsonFileInfo struct { + Name string + Size int64 + Time time.Time + IsDir bool +} + +// MarshalJSON implements the interface MarshalJSON for json.Marshal. +func (f *FileImp) MarshalJSON() ([]byte, error) { + info := f.FileInfo() + return gjson.Marshal(jsonFileInfo{ + Name: f.Name(), + Size: info.Size(), + Time: info.ModTime(), + IsDir: info.IsDir(), + }) +} diff --git a/os/gres/internal/fs_res/fs_res_file_http.go b/os/gres/internal/fs_res/fs_res_file_http.go new file mode 100644 index 000000000..73a8335eb --- /dev/null +++ b/os/gres/internal/fs_res/fs_res_file_http.go @@ -0,0 +1,86 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package fs_res + +import ( + "archive/zip" + "bytes" + "io" + "net/http" + "os" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gres/internal/defines" +) + +// HttpFileImp implements the interface fs.File. +type HttpFileImp struct { + fs defines.FS // FS is the file system that contains this file + zipFile *zip.File // File is the underlying file object + readSeeker io.ReadSeeker // ReadCloser is the underlying file object +} + +var _ http.File = (*HttpFileImp)(nil) + +func NewHttpFile(fs defines.FS, zipFile *zip.File) (*HttpFileImp, error) { + readCloser, err := zipFile.Open() + if err != nil { + return nil, gerror.WrapCodef(gcode.CodeOperationFailed, err, `open zip file failed`) + } + content, err := io.ReadAll(readCloser) + if err != nil { + return nil, gerror.WrapCodef(gcode.CodeOperationFailed, err, `read zip file content failed`) + } + return &HttpFileImp{ + readSeeker: bytes.NewReader(content), + zipFile: zipFile, + fs: fs, + }, nil +} + +// Stat returns the FileInfo structure describing file. +func (f *HttpFileImp) Stat() (os.FileInfo, error) { + return f.zipFile.FileInfo(), nil +} + +// Close implements interface of http.File. +func (f *HttpFileImp) Close() error { + return nil +} + +// Readdir implements Readdir interface of http.File. +func (f *HttpFileImp) Readdir(count int) ([]os.FileInfo, error) { + files := f.fs.ScanDir(f.zipFile.Name, "*", false) + if len(files) > 0 { + if count <= 0 || count > len(files) { + count = len(files) + } + infos := make([]os.FileInfo, count) + for k, v := range files { + infos[k] = v.FileInfo() + } + return infos, nil + } + return nil, nil +} + +// Read implements the io.Reader interface. +func (f *HttpFileImp) Read(b []byte) (n int, err error) { + if n, err = f.readSeeker.Read(b); err != nil { + err = gerror.WrapCodef(gcode.CodeOperationFailed, err, `read content failed`) + } + return +} + +// Seek implements the io.Seeker interface. +func (f *HttpFileImp) Seek(offset int64, whence int) (n int64, err error) { + if n, err = f.readSeeker.Seek(offset, whence); err != nil { + err = gerror.Wrapf(err, `seek failed for offset %d, whence %d`, offset, whence) + } + return +} diff --git a/os/gres/internal/fs_res/fs_res_func.go b/os/gres/internal/fs_res/fs_res_func.go new file mode 100644 index 000000000..ac01dec74 --- /dev/null +++ b/os/gres/internal/fs_res/fs_res_func.go @@ -0,0 +1,164 @@ +package fs_res + +import ( + "archive/zip" + "bytes" + "encoding/hex" + "fmt" + + "github.com/gogf/gf/v2/encoding/gbase64" + "github.com/gogf/gf/v2/encoding/gcompress" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gres/internal/defines" + "github.com/gogf/gf/v2/text/gstr" +) + +const ( + packedGoSourceTemplate = ` +package %s + +import "github.com/gogf/gf/v2/os/gres" + +func init() { + if err := gres.Add("%s"); err != nil { + panic("add binary content to resource manager failed: " + err.Error()) + } +} +` +) + +// PackWithOption packs the path specified by `srcPaths` into bytes. +// +// Note that parameter `srcPaths` supports multiple paths join with ','. +func PackWithOption(srcPaths string, option defines.PackOption) ([]byte, error) { + var buffer = bytes.NewBuffer(nil) + err := zipPathWriter(srcPaths, buffer, option) + if err != nil { + return nil, err + } + // Gzip the data bytes to reduce the size. + return gcompress.Gzip(buffer.Bytes(), 9) +} + +// PackToFileWithOption packs the path specified by `srcPaths` to target file `dstPath`. +// +// Note that parameter `srcPaths` supports multiple paths join with ','. +func PackToFileWithOption(srcPaths, dstPath string, option defines.PackOption) error { + data, err := PackWithOption(srcPaths, option) + if err != nil { + return err + } + return gfile.PutBytes(dstPath, data) +} + +// PackToGoFileWithOption packs the path specified by `srcPaths` to target go file `goFilePath` +// with given package name `pkgName`. +// +// Note that parameter `srcPaths` supports multiple paths join with ','. +func PackToGoFileWithOption(srcPath, goFilePath, pkgName string, option defines.PackOption) error { + data, err := PackWithOption(srcPath, option) + if err != nil { + return err + } + return gfile.PutContents( + goFilePath, + fmt.Sprintf(gstr.TrimLeft(packedGoSourceTemplate), pkgName, gbase64.EncodeToString(data)), + ) +} + +// Unpack unpacks the content specified by `path` to []*File. +func Unpack(path string) ([]defines.File, error) { + realPath, err := gfile.Search(path) + if err != nil { + return nil, err + } + return UnpackContent(gfile.GetContents(realPath)) +} + +// UnpackContent unpacks the content to []File. +func UnpackContent(content string) ([]defines.File, error) { + var ( + err error + data []byte + ) + if isHexStr(content) { + // It here keeps compatible with old version packing string using hex string. + // TODO remove this support in the future. + data, err = gcompress.UnGzip(hexStrToBytes(content)) + if err != nil { + return nil, err + } + } else if isBase64(content) { + // New version packing string using base64. + b, err := gbase64.DecodeString(content) + if err != nil { + return nil, err + } + data, err = gcompress.UnGzip(b) + if err != nil { + return nil, err + } + } else { + data, err = gcompress.UnGzip([]byte(content)) + if err != nil { + return nil, err + } + } + reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + err = gerror.Wrapf(err, `create zip reader failed`) + return nil, err + } + var ( + fs = NewFS() + array = make([]defines.File, len(reader.File)) + ) + for i, file := range reader.File { + array[i] = &FileImp{ + file: file, + fs: fs, + } + } + return array, nil +} + +// isBase64 checks and returns whether given content `s` is base64 string. +// It returns true if `s` is base64 string, or false if not. +func isBase64(s string) bool { + var r bool + for i := 0; i < len(s); i++ { + r = (s[i] >= '0' && s[i] <= '9') || + (s[i] >= 'a' && s[i] <= 'z') || + (s[i] >= 'A' && s[i] <= 'Z') || + (s[i] == '+' || s[i] == '-') || + (s[i] == '_' || s[i] == '/') || s[i] == '=' + if !r { + return false + } + } + return true +} + +// isHexStr checks and returns whether given content `s` is hex string. +// It returns true if `s` is hex string, or false if not. +func isHexStr(s string) bool { + var r bool + for i := 0; i < len(s); i++ { + r = (s[i] >= '0' && s[i] <= '9') || + (s[i] >= 'a' && s[i] <= 'f') || + (s[i] >= 'A' && s[i] <= 'F') + if !r { + return false + } + } + return true +} + +// hexStrToBytes converts hex string content to []byte. +func hexStrToBytes(s string) []byte { + src := []byte(s) + dst := make([]byte, hex.DecodedLen(len(src))) + _, _ = hex.Decode(dst, src) + return dst +} diff --git a/os/gres/gres_func_zip.go b/os/gres/internal/fs_res/fs_res_func_zip.go similarity index 95% rename from os/gres/gres_func_zip.go rename to os/gres/internal/fs_res/fs_res_func_zip.go index bd2f1f65c..4483e4583 100644 --- a/os/gres/gres_func_zip.go +++ b/os/gres/internal/fs_res/fs_res_func_zip.go @@ -4,7 +4,7 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package gres +package fs_res import ( "archive/zip" @@ -16,6 +16,7 @@ import ( "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/fileinfo" "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gres/internal/defines" "github.com/gogf/gf/v2/text/gregex" ) @@ -24,7 +25,7 @@ import ( // // Note that the parameter `paths` can be either a directory or a file, which // supports multiple paths join with ','. -func zipPathWriter(paths string, writer io.Writer, option ...Option) error { +func zipPathWriter(paths string, writer io.Writer, option ...defines.PackOption) error { zipWriter := zip.NewWriter(writer) defer zipWriter.Close() for _, path := range strings.Split(paths, ",") { @@ -40,11 +41,11 @@ func zipPathWriter(paths string, writer io.Writer, option ...Option) error { // The parameter `exclude` specifies the exclusive file path that is not compressed to `zipWriter`, // commonly the destination zip file path. // The unnecessary parameter `prefix` indicates the path prefix for zip file. -func doZipPathWriter(srcPath string, zipWriter *zip.Writer, option ...Option) error { +func doZipPathWriter(srcPath string, zipWriter *zip.Writer, option ...defines.PackOption) error { var ( err error files []string - usedOption Option + usedOption defines.PackOption absolutePath string ) if len(option) > 0 { diff --git a/os/gres/gres_fs_std.go b/os/gres/internal/fs_std/fs_std.go similarity index 74% rename from os/gres/gres_fs_std.go rename to os/gres/internal/fs_std/fs_std.go index 5625c0437..f2625728d 100644 --- a/os/gres/gres_fs_std.go +++ b/os/gres/internal/fs_std/fs_std.go @@ -4,63 +4,46 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package gres +package fs_std import ( - "io" "io/fs" "os" "path/filepath" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gres/internal/defines" ) -// StdFS implements the FS interface using the standard library fs.FS. -type StdFS struct { +// FS implements the FS interface using the standard library fs.FS. +type FS struct { fs fs.FS } -var _ FS = (*StdFS)(nil) +var _ defines.FS = (*FS)(nil) -// NewStdFS creates and returns a new StdFS. -func NewStdFS(fs fs.FS) *StdFS { - return &StdFS{ +func NewFS(fs fs.FS) *FS { + return &FS{ fs: fs, } } // Get returns the file with given path. -func (fs *StdFS) Get(path string) File { +func (fs *FS) Get(path string) defines.File { f, err := fs.fs.Open(path) if err != nil { return nil } - defer f.Close() - - info, err := f.Stat() - if err != nil { - panic(err) - return nil - } - - // Read the content - content, err := io.ReadAll(f) - if err != nil { - return nil - } - - file := &localFile{ - name: info.Name(), - path: path, - file: info, - content: content, - fs: fs, + file := &FileImp{ + path: path, + file: f, + fs: fs, } return file } // IsEmpty checks and returns whether the resource is empty. -func (fs *StdFS) IsEmpty() bool { +func (fs *FS) IsEmpty() bool { if dir, ok := fs.fs.(interface { ReadDir(name string) ([]os.DirEntry, error) }); ok { @@ -75,9 +58,9 @@ func (fs *StdFS) IsEmpty() bool { // ScanDir returns the files under the given path, // the parameter `path` should be a folder type. -func (fs *StdFS) ScanDir(path string, pattern string, recursive ...bool) []File { +func (fs *FS) ScanDir(path string, pattern string, recursive ...bool) []defines.File { var ( - files = make([]File, 0) + files = make([]defines.File, 0) isRecursive = len(recursive) > 0 && recursive[0] ) err := fs.walkDir(path, func(path string, d os.DirEntry, err error) error { @@ -109,7 +92,7 @@ func (fs *StdFS) ScanDir(path string, pattern string, recursive ...bool) []File // walkDir walks the file tree rooted at path, calling fn for each file or // directory in the tree, including path. -func (fs *StdFS) walkDir(path string, fn func(path string, d os.DirEntry, err error) error) error { +func (fs *FS) walkDir(path string, fn func(path string, d os.DirEntry, err error) error) error { if dir, ok := fs.fs.(interface { ReadDir(name string) ([]os.DirEntry, error) }); ok { @@ -151,3 +134,7 @@ func (fs *StdFS) walkDir(path string, fn func(path string, d os.DirEntry, err er } return gerror.New("filesystem does not implement ReadDir") } + +func (fs *FS) ListAll() []defines.File { + return fs.ScanDir(".", "*", true) +} diff --git a/os/gres/internal/fs_std/fs_std_file.go b/os/gres/internal/fs_std/fs_std_file.go new file mode 100644 index 000000000..48293f81c --- /dev/null +++ b/os/gres/internal/fs_std/fs_std_file.go @@ -0,0 +1,132 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package fs_std + +import ( + "context" + "io" + "io/fs" + "net/http" + "os" + "time" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/internal/intlog" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gres/internal/defines" + "github.com/gogf/gf/v2/text/gstr" +) + +// FileImp implements the interface fs.File. +type FileImp struct { + path string + file fs.File // File is the underlying file object + fs defines.FS // FS is the file system that contains this file +} + +var _ defines.File = (*FileImp)(nil) + +func (f *FileImp) Name() string { + return f.path +} + +// FileInfo returns an os.FileInfo describing this file +func (f *FileImp) FileInfo() os.FileInfo { + info, err := f.Stat() + if err != nil { + intlog.Error(context.Background(), err) + return nil + } + return info +} + +// Stat returns the FileInfo structure describing file. +func (f *FileImp) Stat() (os.FileInfo, error) { + return f.file.Stat() +} + +func (f *FileImp) Open() (io.ReadCloser, error) { + return f.file, nil +} + +func (f *FileImp) HttpFile() (http.File, error) { + return NewHttpFile(f.fs, f.file) +} + +// Content returns the file content +func (f *FileImp) Content() []byte { + readCloser, err := f.Open() + if err != nil { + intlog.Error(context.Background(), err) + return nil + } + defer readCloser.Close() + content, err := io.ReadAll(readCloser) + if err != nil { + intlog.Error(context.Background(), err) + return nil + } + return content +} + +// Export exports and saves all its sub files to specified system path `dst` recursively. +func (f *FileImp) Export(dst string, option ...defines.ExportOption) error { + var ( + err error + name string + path string + exportOption defines.ExportOption + exportFiles []defines.File + ) + if f.FileInfo().IsDir() { + exportFiles = f.fs.ScanDir(f.Name(), "*", true) + } else { + exportFiles = append(exportFiles, f) + } + + if len(option) > 0 { + exportOption = option[0] + } + for _, exportFile := range exportFiles { + name = exportFile.Name() + if exportOption.RemovePrefix != "" { + name = gstr.TrimLeftStr(name, exportOption.RemovePrefix) + } + name = gstr.Trim(name, `\/`) + if name == "" { + continue + } + path = gfile.Join(dst, name) + if exportFile.FileInfo().IsDir() { + err = gfile.Mkdir(path) + } else { + err = gfile.PutBytes(path, exportFile.Content()) + } + if err != nil { + return err + } + } + return nil +} + +type jsonFileInfo struct { + Name string + Size int64 + Time time.Time + IsDir bool +} + +// MarshalJSON implements the interface MarshalJSON for json.Marshal. +func (f *FileImp) MarshalJSON() ([]byte, error) { + info := f.FileInfo() + return gjson.Marshal(jsonFileInfo{ + Name: f.Name(), + Size: info.Size(), + Time: info.ModTime(), + IsDir: info.IsDir(), + }) +} diff --git a/os/gres/internal/fs_std/fs_std_file_http.go b/os/gres/internal/fs_std/fs_std_file_http.go new file mode 100644 index 000000000..42a58edfe --- /dev/null +++ b/os/gres/internal/fs_std/fs_std_file_http.go @@ -0,0 +1,86 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package fs_std + +import ( + "bytes" + "io" + "io/fs" + "net/http" + "os" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gres/internal/defines" +) + +// HttpFileImp implements the interface fs.File. +type HttpFileImp struct { + fs defines.FS // FS is the file system that contains this file + fsFile fs.File // File is the underlying file object + readSeeker io.ReadSeeker // ReadCloser is the underlying file object +} + +var _ http.File = (*HttpFileImp)(nil) + +func NewHttpFile(fs defines.FS, fsFile fs.File) (*HttpFileImp, error) { + content, err := io.ReadAll(fsFile) + if err != nil { + return nil, gerror.WrapCodef(gcode.CodeOperationFailed, err, `read zip file content failed`) + } + return &HttpFileImp{ + readSeeker: bytes.NewReader(content), + fsFile: fsFile, + fs: fs, + }, nil +} + +// Stat returns the FileInfo structure describing file. +func (f *HttpFileImp) Stat() (os.FileInfo, error) { + return f.fsFile.Stat() +} + +// Close implements interface of http.File. +func (f *HttpFileImp) Close() error { + return nil +} + +// Readdir implements Readdir interface of http.File. +func (f *HttpFileImp) Readdir(count int) ([]os.FileInfo, error) { + info, err := f.fsFile.Stat() + if err != nil { + return nil, gerror.WrapCodef(gcode.CodeOperationFailed, err, `get file info failed`) + } + files := f.fs.ScanDir(info.Name(), "*", false) + if len(files) > 0 { + if count <= 0 || count > len(files) { + count = len(files) + } + infos := make([]os.FileInfo, count) + for k, v := range files { + infos[k] = v.FileInfo() + } + return infos, nil + } + return nil, nil +} + +// Read implements the io.Reader interface. +func (f *HttpFileImp) Read(b []byte) (n int, err error) { + if n, err = f.readSeeker.Read(b); err != nil { + err = gerror.WrapCodef(gcode.CodeOperationFailed, err, `read content failed`) + } + return +} + +// Seek implements the io.Seeker interface. +func (f *HttpFileImp) Seek(offset int64, whence int) (n int64, err error) { + if n, err = f.readSeeker.Seek(offset, whence); err != nil { + err = gerror.Wrapf(err, `seek failed for offset %d, whence %d`, offset, whence) + } + return +} diff --git a/os/gview/gview_parse.go b/os/gview/gview_parse.go index 243231872..47c202818 100644 --- a/os/gview/gview_parse.go +++ b/os/gview/gview_parse.go @@ -121,7 +121,7 @@ func (view *View) ParseOption(ctx context.Context, option Option) (result string path string folder string content string - resource *gres.File + resource gres.File ) // Searching the absolute file path for `file`. path, folder, resource, err = view.searchFile(ctx, option.File) @@ -371,7 +371,7 @@ func (view *View) formatTemplateObjectCreatingError(filePath, tplName string, er // searchFile returns the absolute path of the `file` and its template folder path. // The returned `folder` is the template folder path, not the folder of the template file `path`. -func (view *View) searchFile(ctx context.Context, file string) (path string, folder string, resource *gres.File, err error) { +func (view *View) searchFile(ctx context.Context, file string) (path string, folder string, resource gres.File, err error) { var tempPath string // Firstly, checking the resource manager. if !gres.IsEmpty() {