diff --git a/os/gres/gres.go b/os/gres/gres.go index 70d16d754..d9f2a5d52 100644 --- a/os/gres/gres.go +++ b/os/gres/gres.go @@ -8,6 +8,9 @@ package gres var ( + // Default resource file system. + defaultFS = NewResFS() + // Default resource object. defaultResource = Instance() ) @@ -16,18 +19,18 @@ var ( // The unnecessary parameter `prefix` indicates the prefix // for each file storing into current resource object. func Add(content string, prefix ...string) error { - return defaultResource.Add(content, prefix...) + return defaultFS.Add(content, prefix...) } // Load loads, unpacks and adds the data from `path` into the default resource object. // The unnecessary parameter `prefix` indicates the prefix // for each file storing into current resource object. func Load(path string, prefix ...string) error { - return defaultResource.Load(path, prefix...) + return defaultFS.Load(path, prefix...) } // Get returns the file with given path. -func Get(path string) *File { +func Get(path string) File { return defaultResource.Get(path) } @@ -35,7 +38,7 @@ func Get(path string) *File { // it then does index files searching under this directory. // // GetWithIndex is usually used for http static file service. -func GetWithIndex(path string, indexFiles []string) *File { +func GetWithIndex(path string, indexFiles []string) File { return defaultResource.GetWithIndex(path, indexFiles) } @@ -60,7 +63,7 @@ func IsEmpty() bool { // using the ',' symbol to separate multiple patterns. // // It scans directory recursively if given parameter `recursive` is true. -func ScanDir(path string, pattern string, recursive ...bool) []*File { +func ScanDir(path string, pattern string, recursive ...bool) []File { return defaultResource.ScanDir(path, pattern, recursive...) } @@ -68,7 +71,7 @@ func ScanDir(path string, pattern string, recursive ...bool) []*File { // It scans directory recursively if given parameter `recursive` is true. // // Note that it returns only files, exclusive of directories. -func ScanDirFile(path string, pattern string, recursive ...bool) []*File { +func ScanDirFile(path string, pattern string, recursive ...bool) []File { return defaultResource.ScanDirFile(path, pattern, recursive...) } diff --git a/os/gres/gres_file.go b/os/gres/gres_file.go index bcb4e0e3d..e57f36626 100644 --- a/os/gres/gres_file.go +++ b/os/gres/gres_file.go @@ -11,104 +11,71 @@ import ( "io" "io/fs" "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 { + Name() string + Path() string + Content() []byte + FileInfo() os.FileInfo + Export(dst string, option ...ExportOption) error + + // For http.File implementation. + + Readdir(count int) ([]os.FileInfo, error) + io.ReadSeekCloser +} + // File implements the interface fs.File. -type File struct { - 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 +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 *File) Name() string { +func (f *localFile) Name() string { return f.name } // Path returns the path of the file -func (f *File) Path() string { +func (f *localFile) Path() string { return f.path } -// 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) - } - return nil -} - -// 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 { +func (f *localFile) FileInfo() os.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 +func (f *localFile) Content() []byte { + return f.content } // Export exports and saves all its sub files to specified system path `dst` recursively. -func (f *File) Export(dst string, option ...ExportOption) error { +func (f *localFile) Export(dst string, option ...ExportOption) error { var ( err error name string path string exportOption ExportOption - exportFiles []*File + exportFiles []File ) - if f.FileInfo().IsDir() { exportFiles = f.fs.ScanDir(f.path, "*", true) } else { @@ -128,7 +95,7 @@ func (f *File) Export(dst string, option ...ExportOption) error { continue } path = gfile.Join(dst, name) - if exportFile.FileInfo().IsDir() { + if f.FileInfo().IsDir() { err = gfile.Mkdir(path) } else { err = gfile.PutBytes(path, exportFile.Content()) @@ -140,8 +107,51 @@ func (f *File) Export(dst string, option ...ExportOption) error { 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 *File) MarshalJSON() ([]byte, error) { +func (f *localFile) MarshalJSON() ([]byte, error) { info := f.FileInfo() return gjson.Marshal(map[string]interface{}{ "name": f.name, @@ -155,7 +165,7 @@ func (f *File) MarshalJSON() ([]byte, error) { // fileInfo is the internal implementation of os.FileInfo interface. type fileInfo struct { - file *File + file *localFile } // Name returns the base name of the file. diff --git a/os/gres/gres_fs.go b/os/gres/gres_fs.go index 2f4769f20..59944e263 100644 --- a/os/gres/gres_fs.go +++ b/os/gres/gres_fs.go @@ -9,14 +9,14 @@ 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 + 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 + ScanDir(path string, pattern string, recursive ...bool) []File } // ExportOption contains options for Export. diff --git a/os/gres/gres_fs_mixed.go b/os/gres/gres_fs_mixed.go index f517bfd6e..10ded9ff1 100644 --- a/os/gres/gres_fs_mixed.go +++ b/os/gres/gres_fs_mixed.go @@ -8,6 +8,7 @@ package gres import ( "io/fs" + "sort" ) // MixedFS implements the FS interface by combining StdFS and ResFS. @@ -17,6 +18,8 @@ type MixedFS struct { resFS *ResFS } +var _ FS = (*MixedFS)(nil) + // NewMixedFS creates and returns a new MixedFS. func NewMixedFS(stdFs fs.FS, resFS *ResFS) *MixedFS { return &MixedFS{ @@ -26,7 +29,7 @@ func NewMixedFS(stdFs fs.FS, resFS *ResFS) *MixedFS { } // Get returns the file with given path. -func (fs *MixedFS) Get(path string) *File { +func (fs *MixedFS) Get(path string) File { if file := fs.resFS.Get(path); file != nil { return file } @@ -40,15 +43,15 @@ func (fs *MixedFS) IsEmpty() bool { // 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 { +func (fs *MixedFS) ScanDir(path string, pattern string, recursive ...bool) []File { var ( - filesMap = make(map[string]*File) - files = make([]*File, 0) + filesMap = make(map[string]File) + files = make([]File, 0) ) // Get files from ResFS - defaultFiles := fs.resFS.ScanDir(path, pattern, recursive...) - for _, file := range defaultFiles { + resFiles := fs.resFS.ScanDir(path, pattern, recursive...) + for _, file := range resFiles { if _, exists := filesMap[file.Path()]; !exists { filesMap[file.Path()] = file } @@ -56,32 +59,21 @@ func (fs *MixedFS) ScanDir(path string, pattern string, recursive ...bool) []*Fi // 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) + // 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 } - -// 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 index 4963396cb..a793bcace 100644 --- a/os/gres/gres_fs_res.go +++ b/os/gres/gres_fs_res.go @@ -23,6 +23,8 @@ type ResFS struct { tree *gtree.BTree // The tree storing all resource files. } +var _ FS = (*ResFS)(nil) + const ( defaultTreeM = 100 ) @@ -37,7 +39,7 @@ func NewResFS() *ResFS { } // Get returns the file with given path. -func (fs *ResFS) Get(path string) *File { +func (fs *ResFS) Get(path string) File { if path == "" { return nil } @@ -50,7 +52,7 @@ func (fs *ResFS) Get(path string) *File { } result := fs.tree.Get(path) if result != nil { - return result.(*File) + return result.(File) } return nil } @@ -62,7 +64,7 @@ func (fs *ResFS) IsEmpty() bool { // 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 *ResFS) ScanDir(path string, pattern string, recursive ...bool) []File { isRecursive := false if len(recursive) > 0 { isRecursive = recursive[0] @@ -77,7 +79,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 *ResFS) doScanDir(path string, pattern string, recursive bool) []File { path = strings.ReplaceAll(path, "\\", "/") path = strings.ReplaceAll(path, "//", "/") if path != "/" { @@ -86,7 +88,7 @@ func (fs *ResFS) doScanDir(path string, pattern string, recursive bool) []*File } } var ( - files = make([]*File, 0) + files = make([]File, 0) patterns = strings.Split(pattern, ",") ) for i := 0; i < len(patterns); i++ { @@ -102,7 +104,7 @@ func (fs *ResFS) doScanDir(path string, pattern string, recursive bool) []*File // Walk through the tree to find matching files fs.tree.IteratorAsc(func(key, value interface{}) bool { var ( - file = value.(*File) + file = value.(File) filePath = key.(string) ) @@ -144,7 +146,7 @@ func (fs *ResFS) Add(content string, prefix ...string) error { namePrefix = prefix[0] } for i := 0; i < len(files); i++ { - files[i].fs = fs + files[i].(*localFile).fs = fs fs.tree.Set(namePrefix+files[i].Path(), files[i]) } intlog.Printf(context.TODO(), "Add %d files to resource manager", fs.tree.Size()) @@ -164,7 +166,7 @@ func (fs *ResFS) Load(path string, prefix ...string) error { func (fs *ResFS) Dump() { var info os.FileInfo fs.tree.Iterator(func(key, value interface{}) bool { - info = value.(*File).FileInfo() + info = value.(File).FileInfo() intlog.Printf( context.TODO(), "%v %8s %s", diff --git a/os/gres/gres_fs_std.go b/os/gres/gres_fs_std.go index feadae2c4..5625c0437 100644 --- a/os/gres/gres_fs_std.go +++ b/os/gres/gres_fs_std.go @@ -20,6 +20,8 @@ type StdFS struct { fs fs.FS } +var _ FS = (*StdFS)(nil) + // NewStdFS creates and returns a new StdFS. func NewStdFS(fs fs.FS) *StdFS { return &StdFS{ @@ -28,7 +30,7 @@ func NewStdFS(fs fs.FS) *StdFS { } // Get returns the file with given path. -func (fs *StdFS) Get(path string) *File { +func (fs *StdFS) Get(path string) File { f, err := fs.fs.Open(path) if err != nil { return nil @@ -37,6 +39,7 @@ func (fs *StdFS) Get(path string) *File { info, err := f.Stat() if err != nil { + panic(err) return nil } @@ -46,12 +49,12 @@ func (fs *StdFS) Get(path string) *File { return nil } - file := &File{ - name: info.Name(), - path: path, - file: info, - resource: content, - fs: fs, + file := &localFile{ + name: info.Name(), + path: path, + file: info, + content: content, + fs: fs, } return file } @@ -72,9 +75,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 *StdFS) ScanDir(path string, pattern string, recursive ...bool) []File { var ( - files = make([]*File, 0) + files = make([]File, 0) isRecursive = len(recursive) > 0 && recursive[0] ) err := fs.walkDir(path, func(path string, d os.DirEntry, err error) error { diff --git a/os/gres/gres_func.go b/os/gres/gres_func.go index a96992bbd..4d8c2e525 100644 --- a/os/gres/gres_func.go +++ b/os/gres/gres_func.go @@ -11,6 +11,7 @@ import ( "bytes" "encoding/hex" "fmt" + "sync" "github.com/gogf/gf/v2/encoding/gbase64" "github.com/gogf/gf/v2/encoding/gcompress" @@ -129,7 +130,7 @@ func PackToGoFileWithOption(srcPath, goFilePath, pkgName string, option Option) } // Unpack unpacks the content specified by `path` to []*File. -func Unpack(path string) ([]*File, error) { +func Unpack(path string) ([]File, error) { realPath, err := gfile.Search(path) if err != nil { return nil, err @@ -137,8 +138,8 @@ func Unpack(path string) ([]*File, error) { return UnpackContent(gfile.GetContents(realPath)) } -// UnpackContent unpacks the content to []*File. -func UnpackContent(content string) ([]*File, error) { +// UnpackContent unpacks the content to []File. +func UnpackContent(content string) ([]File, error) { var ( err error data []byte @@ -171,15 +172,15 @@ func UnpackContent(content string) ([]*File, error) { err = gerror.Wrapf(err, `create zip reader failed`) return nil, err } - array := make([]*File, len(reader.File)) + array := make([]File, len(reader.File)) for i, file := range reader.File { - array[i] = &File{ - name: file.Name, - path: file.Name, - file: file.FileInfo(), - reader: nil, - resource: nil, - fs: nil, + array[i] = &localFile{ + name: file.Name, + path: file.Name, + content: data, + file: file.FileInfo(), + fs: nil, + mu: sync.Mutex{}, } } return array, nil diff --git a/os/gres/gres_http_file.go b/os/gres/gres_http_file.go deleted file mode 100644 index e6d10124c..000000000 --- a/os/gres/gres_http_file.go +++ /dev/null @@ -1,71 +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" - "os" - - "github.com/gogf/gf/v2/errors/gerror" -) - -// Close implements interface of http.File. -func (f *File) Close() error { - return nil -} - -// Readdir implements Readdir interface of http.File. -func (f *File) Readdir(count int) ([]os.FileInfo, error) { - files := f.resource.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 -} - -// Stat implements Stat interface of http.File. -func (f *File) Stat() (os.FileInfo, error) { - return f.FileInfo(), nil -} - -// Read implements the io.Reader interface. -func (f *File) Read(b []byte) (n int, err error) { - reader, err := f.getReader() - if err != nil { - return 0, err - } - if n, err = reader.Read(b); err != nil { - err = gerror.Wrapf(err, `read content failed`) - } - return -} - -// Seek implements the io.Seeker interface. -func (f *File) Seek(offset int64, whence int) (n int64, err error) { - reader, err := f.getReader() - if err != nil { - return 0, err - } - 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 *File) getReader() (*bytes.Reader, error) { - if f.reader == nil { - f.reader = bytes.NewReader(f.Content()) - } - return f.reader, nil -} diff --git a/os/gres/gres_instance.go b/os/gres/gres_instance.go index e04dc4311..5cea31583 100644 --- a/os/gres/gres_instance.go +++ b/os/gres/gres_instance.go @@ -26,6 +26,6 @@ func Instance(name ...string) *Resource { key = name[0] } return instances.GetOrSetFuncLock(key, func() interface{} { - return New() + return NewWithFS(defaultFS) }).(*Resource) } diff --git a/os/gres/gres_resource.go b/os/gres/gres_resource.go index ad6086cea..f490b96ca 100644 --- a/os/gres/gres_resource.go +++ b/os/gres/gres_resource.go @@ -30,14 +30,19 @@ func NewWithFS(fs FS) *Resource { } } +// SetFS sets the underlying file system implementation. +func (r *Resource) SetFS(fs FS) { + r.fs = fs +} + // Get returns the file with given path. -func (r *Resource) Get(path string) *File { +func (r *Resource) Get(path string) File { return r.fs.Get(path) } // GetWithIndex searches file with `path`, if the file is directory // it then does index files searching under this directory. -func (r *Resource) GetWithIndex(path string, indexFiles []string) *File { +func (r *Resource) GetWithIndex(path string, indexFiles []string) File { // Necessary for double char '/' replacement in prefix. path = strings.ReplaceAll(path, "\\", "/") path = strings.ReplaceAll(path, "//", "/") @@ -48,7 +53,7 @@ func (r *Resource) GetWithIndex(path string, indexFiles []string) *File { } if file := r.fs.Get(path); file != nil { if len(indexFiles) > 0 && file.FileInfo().IsDir() { - var f *File + var f File for _, name := range indexFiles { if f = r.fs.Get(path + "/" + name); f != nil { return f @@ -80,15 +85,15 @@ func (r *Resource) IsEmpty() bool { } // ScanDir returns the files under the given path, the parameter `path` should be a folder type. -func (r *Resource) ScanDir(path string, pattern string, recursive ...bool) []*File { +func (r *Resource) ScanDir(path string, pattern string, recursive ...bool) []File { 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. -func (r *Resource) ScanDirFile(path string, pattern string, recursive ...bool) []*File { +func (r *Resource) ScanDirFile(path string, pattern string, recursive ...bool) []File { var ( - result = make([]*File, 0) + result = make([]File, 0) files = r.fs.ScanDir(path, pattern, recursive...) ) for _, file := range files {