diff --git a/os/gres/gres.go b/os/gres/gres.go index 7bb9e815d..70d16d754 100644 --- a/os/gres/gres.go +++ b/os/gres/gres.go @@ -7,11 +7,6 @@ // Package gres provides resource management and packing/unpacking feature between files and bytes. package gres -const ( - // Separator for directories. - Separator = "/" -) - var ( // Default resource object. defaultResource = Instance() @@ -56,7 +51,7 @@ func Contains(path string) bool { // IsEmpty checks and returns whether the resource manager is empty. func IsEmpty() bool { - return defaultResource.tree.IsEmpty() + return defaultResource.IsEmpty() } // ScanDir returns the files under the given path, the parameter `path` should be a folder type. diff --git a/os/gres/gres_file.go b/os/gres/gres_file.go index 8e42ed1d3..bcb4e0e3d 100644 --- a/os/gres/gres_file.go +++ b/os/gres/gres_file.go @@ -7,62 +7,192 @@ package gres import ( - "archive/zip" "bytes" "io" + "io/fs" "os" + "time" - "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gstr" ) +// File implements the interface fs.File. type File struct { - file *zip.File - reader *bytes.Reader - resource *Resource + name string // Name is the file name + path string // Path is the file path + file os.FileInfo // FileInfo is the underlying file info + reader io.Reader // Reader is the file content reader + resource []byte // Resource is the file content in binary format + fs FS // FS is the file system that contains this file } -// Name returns the name of the file. +// Name returns the name of the file func (f *File) Name() string { - return f.file.Name + return f.name } -// Open returns a ReadCloser that provides access to the File's contents. -// Multiple files may be read concurrently. -func (f *File) Open() (io.ReadCloser, error) { - return f.file.Open() +// Path returns the path of the file +func (f *File) Path() string { + return f.path } -// Content returns the content of the file. -func (f *File) Content() []byte { - reader, err := f.Open() - if err != nil { - return nil +// Open opens the file for reading +func (f *File) Open() error { + if f.reader == nil && len(f.resource) > 0 { + f.reader = bytes.NewReader(f.resource) } - defer reader.Close() - buffer := bytes.NewBuffer(nil) - if _, err = io.Copy(buffer, reader); err != nil { - return nil - } - return buffer.Bytes() + return nil } -// FileInfo returns an os.FileInfo for the FileHeader. +// Close closes the file +func (f *File) Close() error { + if closer, ok := f.reader.(io.Closer); ok { + return closer.Close() + } + return nil +} + +// Read reads up to len(p) bytes into p +func (f *File) Read(p []byte) (n int, err error) { + if f.reader == nil { + if err := f.Open(); err != nil { + return 0, err + } + } + return f.reader.Read(p) +} + +// Seek implements the io.Seeker interface +func (f *File) Seek(offset int64, whence int) (int64, error) { + if seeker, ok := f.reader.(io.Seeker); ok { + return seeker.Seek(offset, whence) + } + return 0, fs.ErrInvalid +} + +// FileInfo returns an os.FileInfo describing this file func (f *File) FileInfo() os.FileInfo { - return f.file.FileInfo() + return f.file +} + +// Stat returns the FileInfo structure describing file +func (f *File) Stat() (os.FileInfo, error) { + return f.file, nil +} + +// Content returns the file content +func (f *File) Content() []byte { + if len(f.resource) > 0 { + return f.resource + } + buffer := new(bytes.Buffer) + if err := f.Open(); err != nil { + return nil + } + defer f.Close() + if _, err := io.Copy(buffer, f); err != nil { + return nil + } + f.resource = buffer.Bytes() + return f.resource } // Export exports and saves all its sub files to specified system path `dst` recursively. func (f *File) Export(dst string, option ...ExportOption) error { - return f.resource.Export(f.Name(), dst, option...) + 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 exportFile.FileInfo().IsDir() { + err = gfile.Mkdir(path) + } else { + err = gfile.PutBytes(path, exportFile.Content()) + } + if err != nil { + return err + } + } + return nil } // MarshalJSON implements the interface MarshalJSON for json.Marshal. -func (f File) MarshalJSON() ([]byte, error) { +func (f *File) MarshalJSON() ([]byte, error) { info := f.FileInfo() - return json.Marshal(map[string]interface{}{ - "name": f.Name(), - "size": info.Size(), - "time": info.ModTime(), - "file": !info.IsDir(), + 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 *File +} + +// 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 new file mode 100644 index 000000000..2f4769f20 --- /dev/null +++ b/os/gres/gres_fs.go @@ -0,0 +1,25 @@ +// 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 new file mode 100644 index 000000000..f517bfd6e --- /dev/null +++ b/os/gres/gres_fs_mixed.go @@ -0,0 +1,87 @@ +// 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" +) + +// 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 +} + +// 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 + defaultFiles := fs.resFS.ScanDir(path, pattern, recursive...) + for _, file := range defaultFiles { + if _, exists := filesMap[file.Path()]; !exists { + filesMap[file.Path()] = file + } + } + + // Get files from StdFS + stdFiles := fs.stdFS.ScanDir(path, pattern, recursive...) + if len(stdFiles) > 0 { + + } + for _, file := range stdFiles { + filesMap[file.Path()] = file + } + + // Convert map to slice + for _, file := range filesMap { + files = append(files, file) + } + + return files +} + +// ScanDirFile returns all sub-files with absolute paths of given `path`, +// It scans directory recursively if given parameter `recursive` is true. +func (fs *MixedFS) ScanDirFile(path string, pattern string, recursive ...bool) []*File { + return fs.ScanDir(path, pattern, recursive...) +} + +// Export exports and saves specified path `src` and all its sub files +// to specified system path `dst` recursively. +func (fs *MixedFS) Export(src, dst string, option ...ExportOption) error { + if file := fs.Get(src); file != nil { + return file.Export(dst, option...) + } + return nil +} diff --git a/os/gres/gres_fs_res.go b/os/gres/gres_fs_res.go new file mode 100644 index 000000000..4963396cb --- /dev/null +++ b/os/gres/gres_fs_res.go @@ -0,0 +1,178 @@ +// 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 ( + "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" +) + +// ResFS implements the FS interface using the default resource implementation. +type ResFS struct { + tree *gtree.BTree // The tree storing all resource files. +} + +const ( + defaultTreeM = 100 +) + +// NewResFS creates and returns a new ResFS. +func NewResFS() *ResFS { + return &ResFS{ + tree: gtree.NewBTree(defaultTreeM, func(v1, v2 interface{}) int { + return strings.Compare(v1.(string), v2.(string)) + }), + } +} + +// Get returns the file with given path. +func (fs *ResFS) Get(path string) *File { + if path == "" { + return nil + } + path = strings.ReplaceAll(path, "\\", "/") + path = strings.ReplaceAll(path, "//", "/") + if path != "/" { + for path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + } + result := fs.tree.Get(path) + if result != nil { + return result.(*File) + } + return nil +} + +// IsEmpty checks and returns whether the resource is empty. +func (fs *ResFS) 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 { + isRecursive := false + if len(recursive) > 0 { + isRecursive = recursive[0] + } + return fs.doScanDir(path, pattern, isRecursive) +} + +// doScanDir is an internal method which scans directory +// and returns the absolute path list of files that are not sorted. +// +// The pattern parameter `pattern` supports multiple file name patterns, +// 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 { + path = strings.ReplaceAll(path, "\\", "/") + path = strings.ReplaceAll(path, "//", "/") + if path != "/" { + for path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + } + var ( + files = make([]*File, 0) + 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) { + return true + } + + // Skip if not recursive and file is in subdirectory + if !recursive { + relPath := strings.TrimPrefix(filePath, path) + if strings.Contains(relPath, "/") { + 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 + } + } + return true + }) + return files +} + +// Add adds the `content` into current ResFS with given `prefix`. +func (fs *ResFS) Add(content string, prefix ...string) error { + files, err := UnpackContent(content) + if err != nil { + intlog.Printf(context.TODO(), "Add resource files failed: %v", err) + return err + } + namePrefix := "" + if len(prefix) > 0 { + namePrefix = prefix[0] + } + for i := 0; i < len(files); i++ { + files[i].fs = fs + fs.tree.Set(namePrefix+files[i].Path(), 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 { + 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/gres_fs_std.go b/os/gres/gres_fs_std.go new file mode 100644 index 000000000..feadae2c4 --- /dev/null +++ b/os/gres/gres_fs_std.go @@ -0,0 +1,150 @@ +// 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" + "io/fs" + "os" + "path/filepath" + + "github.com/gogf/gf/v2/errors/gerror" +) + +// StdFS implements the FS interface using the standard library fs.FS. +type StdFS struct { + fs fs.FS +} + +// NewStdFS creates and returns a new StdFS. +func NewStdFS(fs fs.FS) *StdFS { + return &StdFS{ + fs: fs, + } +} + +// Get returns the file with given path. +func (fs *StdFS) Get(path string) *File { + f, err := fs.fs.Open(path) + if err != nil { + return nil + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return nil + } + + // Read the content + content, err := io.ReadAll(f) + if err != nil { + return nil + } + + file := &File{ + name: info.Name(), + path: path, + file: info, + resource: content, + fs: fs, + } + return file +} + +// IsEmpty checks and returns whether the resource is empty. +func (fs *StdFS) IsEmpty() bool { + if dir, ok := fs.fs.(interface { + ReadDir(name string) ([]os.DirEntry, error) + }); ok { + entries, err := dir.ReadDir(".") + if err != nil { + return true + } + return len(entries) == 0 + } + return true +} + +// 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 { + var ( + files = make([]*File, 0) + isRecursive = len(recursive) > 0 && recursive[0] + ) + err := fs.walkDir(path, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + matched, err := filepath.Match(pattern, filepath.Base(path)) + if err != nil { + return err + } + if matched { + if file := fs.Get(path); file != nil { + files = append(files, file) + } + } + } + if !isRecursive && d.IsDir() && path != "." { + return filepath.SkipDir + } + return nil + }) + + if err != nil { + return nil + } + return files +} + +// 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 { + if dir, ok := fs.fs.(interface { + ReadDir(name string) ([]os.DirEntry, error) + }); ok { + entries, err := dir.ReadDir(path) + if err != nil { + err = fn(path, nil, err) + if err != nil { + return err + } + return nil + } + + for _, entry := range entries { + var ( + fileName = entry.Name() + filePath = filepath.Join(path, fileName) + ) + err = fn(filePath, entry, nil) + if err != nil { + if gerror.Is(err, filepath.SkipDir) { + if entry.IsDir() { + continue + } + return nil + } + return err + } + if entry.IsDir() { + err = fs.walkDir(filePath, fn) + if err != nil { + if gerror.Is(err, filepath.SkipDir) { + continue + } + return err + } + } + } + return nil + } + return gerror.New("filesystem does not implement ReadDir") +} diff --git a/os/gres/gres_func.go b/os/gres/gres_func.go index f26c2fb5b..a96992bbd 100644 --- a/os/gres/gres_func.go +++ b/os/gres/gres_func.go @@ -173,7 +173,14 @@ func UnpackContent(content string) ([]*File, error) { } array := make([]*File, len(reader.File)) for i, file := range reader.File { - array[i] = &File{file: file} + array[i] = &File{ + name: file.Name, + path: file.Name, + file: file.FileInfo(), + reader: nil, + resource: nil, + fs: nil, + } } return array, nil } diff --git a/os/gres/gres_resource.go b/os/gres/gres_resource.go index 085dfc35e..ad6086cea 100644 --- a/os/gres/gres_resource.go +++ b/os/gres/gres_resource.go @@ -8,90 +8,35 @@ package gres import ( "context" - "fmt" - "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/text/gstr" ) +// Resource implements the FS interface. type Resource struct { - tree *gtree.BTree + fs FS } -const ( - defaultTreeM = 100 -) - // New creates and returns a new resource object. func New() *Resource { + return NewWithFS(NewResFS()) +} + +// NewWithFS sets the underlying file system implementation. +func NewWithFS(fs FS) *Resource { return &Resource{ - tree: gtree.NewBTree(defaultTreeM, func(v1, v2 interface{}) int { - return strings.Compare(v1.(string), v2.(string)) - }), + fs: fs, } } -// Add unpacks and adds the `content` into current resource object. -// The unnecessary parameter `prefix` indicates the prefix -// for each file storing into current resource object. -func (r *Resource) Add(content string, prefix ...string) error { - files, err := UnpackContent(content) - if err != nil { - intlog.Printf(context.TODO(), "Add resource files failed: %v", err) - return err - } - namePrefix := "" - if len(prefix) > 0 { - namePrefix = prefix[0] - } - for i := 0; i < len(files); i++ { - files[i].resource = r - r.tree.Set(namePrefix+files[i].file.Name, files[i]) - } - intlog.Printf(context.TODO(), "Add %d files to resource manager", r.tree.Size()) - return nil -} - -// Load loads, unpacks and adds the data from `path` into current resource object. -// The unnecessary parameter `prefix` indicates the prefix -// for each file storing into current resource object. -func (r *Resource) Load(path string, prefix ...string) error { - realPath, err := gfile.Search(path) - if err != nil { - return err - } - return r.Add(gfile.GetContents(realPath), prefix...) -} - // Get returns the file with given path. func (r *Resource) Get(path string) *File { - if path == "" { - return nil - } - path = strings.ReplaceAll(path, "\\", "/") - path = strings.ReplaceAll(path, "//", "/") - if path != "/" { - for path[len(path)-1] == '/' { - path = path[:len(path)-1] - } - } - result := r.tree.Get(path) - if result != nil { - return result.(*File) - } - return nil + return r.fs.Get(path) } // GetWithIndex searches file with `path`, if the file is directory // it then does index files searching under this directory. -// -// GetWithIndex is usually used for http static file service. func (r *Resource) GetWithIndex(path string, indexFiles []string) *File { // Necessary for double char '/' replacement in prefix. path = strings.ReplaceAll(path, "\\", "/") @@ -101,11 +46,11 @@ func (r *Resource) GetWithIndex(path string, indexFiles []string) *File { path = path[:len(path)-1] } } - if file := r.Get(path); file != nil { + if file := r.fs.Get(path); file != nil { if len(indexFiles) > 0 && file.FileInfo().IsDir() { var f *File for _, name := range indexFiles { - if f = r.Get(path + "/" + name); f != nil { + if f = r.fs.Get(path + "/" + name); f != nil { return f } } @@ -126,163 +71,52 @@ 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. func (r *Resource) IsEmpty() bool { - return r.tree.IsEmpty() + return r.fs.IsEmpty() } // ScanDir returns the files under the given path, the parameter `path` should be a folder type. -// -// The pattern parameter `pattern` supports multiple file name patterns, -// using the ',' symbol to separate multiple patterns. -// -// It scans directory recursively if given parameter `recursive` is true. -// -// Note that the returned files does not contain given parameter `path`. func (r *Resource) ScanDir(path string, pattern string, recursive ...bool) []*File { - isRecursive := false - if len(recursive) > 0 { - isRecursive = recursive[0] - } - return r.doScanDir(path, pattern, isRecursive, false) + return r.fs.ScanDir(path, pattern, recursive...) } // ScanDirFile returns all sub-files with absolute paths of given `path`, // It scans directory recursively if given parameter `recursive` is true. -// -// Note that it returns only files, exclusive of directories. func (r *Resource) ScanDirFile(path string, pattern string, recursive ...bool) []*File { - isRecursive := false - if len(recursive) > 0 { - isRecursive = recursive[0] - } - return r.doScanDir(path, pattern, isRecursive, true) -} - -// doScanDir is an internal method which scans directory -// and returns the absolute path list of files that are not sorted. -// -// The pattern parameter `pattern` supports multiple file name patterns, -// using the ',' symbol to separate multiple patterns. -// -// It scans directory recursively if given parameter `recursive` is true. -func (r *Resource) doScanDir(path string, pattern string, recursive bool, onlyFile bool) []*File { - path = strings.ReplaceAll(path, "\\", "/") - path = strings.ReplaceAll(path, "//", "/") - if path != "/" { - for path[len(path)-1] == '/' { - path = path[:len(path)-1] - } - } var ( - name = "" - files = make([]*File, 0) - length = len(path) - patterns = strings.Split(pattern, ",") + result = make([]*File, 0) + files = r.fs.ScanDir(path, pattern, recursive...) ) - for i := 0; i < len(patterns); i++ { - patterns[i] = strings.TrimSpace(patterns[i]) - } - // Used for type checking for first entry. - first := true - r.tree.IteratorFrom(path, true, func(key, value interface{}) bool { - if first { - if !value.(*File).FileInfo().IsDir() { - return false - } - first = false - } - if onlyFile && value.(*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 - } - if !recursive { - if strings.IndexByte(name[length+1:], '/') != -1 { - return true - } - } - for _, p := range patterns { - if match, err := filepath.Match(p, gfile.Basename(name)); err == nil && match { - files = append(files, value.(*File)) - return true - } - } - return true - }) - return files -} - -// ExportOption is the option for function Export. -type ExportOption struct { - RemovePrefix string // Remove the prefix of file name from resource. -} - -// Export exports and saves specified path `srcPath` and all its sub files to specified system path `dstPath` recursively. -func (r *Resource) Export(src, dst string, option ...ExportOption) error { - var ( - err error - name string - path string - exportOption ExportOption - files []*File - ) - - if r.Get(src).FileInfo().IsDir() { - files = r.doScanDir(src, "*", true, false) - } else { - files = append(files, r.Get(src)) - } - - if len(option) > 0 { - exportOption = option[0] - } for _, file := range files { - name = file.Name() - if exportOption.RemovePrefix != "" { - name = gstr.TrimLeftStr(name, exportOption.RemovePrefix) - } - name = gstr.Trim(name, `\/`) - if name == "" { + if file.FileInfo().IsDir() { continue } - path = gfile.Join(dst, name) - if file.FileInfo().IsDir() { - err = gfile.Mkdir(path) - } else { - err = gfile.PutBytes(path, file.Content()) - } - if err != nil { - return err - } + result = append(result, file) + } + return result +} + +// Export exports and saves specified path `src` and all its sub files +// to specified system path `dst` recursively. +func (r *Resource) Export(src, dst string, option ...ExportOption) error { + if file := r.Get(src); file != nil { + return file.Export(dst, option...) } return nil } // Dump prints the files of current resource object. func (r *Resource) Dump() { - var info os.FileInfo - r.tree.Iterator(func(key, value interface{}) bool { - info = value.(*File).FileInfo() - fmt.Printf( - "%v %8s %s\n", - gtime.New(info.ModTime()).ISO8601(), - gfile.FormatSize(info.Size()), - key, - ) - return true - }) - fmt.Printf("TOTAL FILES: %d\n", r.tree.Size()) + 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()) + } + } }