diff --git a/.example/net/ghttp/client/upload/server.go b/.example/net/ghttp/client/upload/server.go index 8c8b50677..27f4322ee 100644 --- a/.example/net/ghttp/client/upload/server.go +++ b/.example/net/ghttp/client/upload/server.go @@ -4,28 +4,36 @@ import ( "github.com/gogf/gf/frame/g" "github.com/gogf/gf/net/ghttp" "github.com/gogf/gf/os/gfile" + "io" ) -// 执行文件上传处理,上传到系统临时目录 /tmp +// Upload uploads file to /tmp . func Upload(r *ghttp.Request) { - if f, h, e := r.FormFile("upload-file"); e == nil { - defer f.Close() - name := gfile.Basename(h.Filename) - buffer := make([]byte, h.Size) - f.Read(buffer) - gfile.PutBytes("/tmp/"+name, buffer) - r.Response.Write(name + " uploaded successly") - } else { - r.Response.Write(e.Error()) + f, h, e := r.FormFile("upload-file") + if e != nil { + r.Response.Write(e) } + defer f.Close() + savePath := "/tmp/" + gfile.Basename(h.Filename) + file, err := gfile.Create(savePath) + if err != nil { + r.Response.Write(err) + return + } + defer file.Close() + if _, err := io.Copy(file, f); err != nil { + r.Response.Write(err) + return + } + r.Response.Write("upload successfully") } -// 展示文件上传页面 +// UploadShow shows uploading page. func UploadShow(r *ghttp.Request) { r.Response.Write(` - 上传文件 + GF UploadFile Demo
@@ -39,8 +47,10 @@ func UploadShow(r *ghttp.Request) { func main() { s := g.Server() - s.BindHandler("/upload", Upload) - s.BindHandler("/upload/show", UploadShow) + s.Group("/upload", func(g *ghttp.RouterGroup) { + g.ALL("/", Upload) + g.ALL("/show", UploadShow) + }) s.SetPort(8199) s.Run() } diff --git a/.example/net/ghttp/server/resource/resource.go b/.example/net/ghttp/server/resource/resource.go index b23c6ee11..50ea7ea50 100644 --- a/.example/net/ghttp/server/resource/resource.go +++ b/.example/net/ghttp/server/resource/resource.go @@ -12,8 +12,8 @@ import ( func main() { gres.Dump() - v := g.View() - v.SetPath("template/layout1") + //v := g.View() + //v.SetPath("template/layout1") s := g.Server() s.SetIndexFolder(true) @@ -22,8 +22,8 @@ func main() { fmt.Println(r.URL.Path, r.IsFileRequest()) }) s.BindHandler("/template", func(r *ghttp.Request) { - r.Response.WriteTpl("layout.html") + r.Response.WriteTpl("layout1/layout.html") }) - s.SetPort(8199) + s.SetPort(8198) s.Run() } diff --git a/container/gmap/gmap_hash_any_any_map.go b/container/gmap/gmap_hash_any_any_map.go index edb207ae3..928e798a7 100644 --- a/container/gmap/gmap_hash_any_any_map.go +++ b/container/gmap/gmap_hash_any_any_map.go @@ -194,7 +194,9 @@ func (m *AnyAnyMap) doSetWithLockCheck(key interface{}, value interface{}) inter if f, ok := value.(func() interface{}); ok { value = f() } - m.data[key] = value + if value != nil { + m.data[key] = value + } return value } diff --git a/container/gmap/gmap_hash_int_str_map.go b/container/gmap/gmap_hash_int_str_map.go index f9ddd5f38..cdd63955b 100644 --- a/container/gmap/gmap_hash_int_str_map.go +++ b/container/gmap/gmap_hash_int_str_map.go @@ -181,12 +181,11 @@ func (m *IntStrMap) Pops(size int) map[int]string { // It returns value with given . func (m *IntStrMap) doSetWithLockCheck(key int, value string) string { m.mu.Lock() + defer m.mu.Unlock() if v, ok := m.data[key]; ok { - m.mu.Unlock() return v } m.data[key] = value - m.mu.Unlock() return value } @@ -223,7 +222,9 @@ func (m *IntStrMap) GetOrSetFuncLock(key int, f func() string) string { return v } v = f() - m.data[key] = v + if v != "" { + m.data[key] = v + } return v } else { return v diff --git a/container/gmap/gmap_hash_str_str_map.go b/container/gmap/gmap_hash_str_str_map.go index 89abbce52..58d8d9551 100644 --- a/container/gmap/gmap_hash_str_str_map.go +++ b/container/gmap/gmap_hash_str_str_map.go @@ -181,12 +181,11 @@ func (m *StrStrMap) Pops(size int) map[string]string { // It returns value with given . func (m *StrStrMap) doSetWithLockCheck(key string, value string) string { m.mu.Lock() + defer m.mu.Unlock() if v, ok := m.data[key]; ok { - m.mu.Unlock() return v } m.data[key] = value - m.mu.Unlock() return value } @@ -225,7 +224,9 @@ func (m *StrStrMap) GetOrSetFuncLock(key string, f func() string) string { return v } v = f() - m.data[key] = v + if v != "" { + m.data[key] = v + } return v } else { return v diff --git a/container/gmap/gmap_list_map.go b/container/gmap/gmap_list_map.go index f4539fe42..a74ac24d4 100644 --- a/container/gmap/gmap_list_map.go +++ b/container/gmap/gmap_list_map.go @@ -246,7 +246,9 @@ func (m *ListMap) doSetWithLockCheck(key interface{}, value interface{}) interfa if f, ok := value.(func() interface{}); ok { value = f() } - m.data[key] = m.list.PushBack(&gListMapNode{key, value}) + if value != nil { + m.data[key] = m.list.PushBack(&gListMapNode{key, value}) + } return value } diff --git a/container/gset/gset_any_set.go b/container/gset/gset_any_set.go index 79c7dbc92..167d958b7 100644 --- a/container/gset/gset_any_set.go +++ b/container/gset/gset_any_set.go @@ -109,7 +109,9 @@ func (set *Set) doAddWithLockCheck(item interface{}, value interface{}) interfac item = value } } - set.data[item] = struct{}{} + if item != nil { + set.data[item] = struct{}{} + } return item } diff --git a/container/gset/gset_str_set.go b/container/gset/gset_str_set.go index 887a000ae..472765fca 100644 --- a/container/gset/gset_str_set.go +++ b/container/gset/gset_str_set.go @@ -103,7 +103,9 @@ func (set *StrSet) doAddWithLockCheck(item string, value interface{}) string { item = value.(string) } } - set.data[item] = struct{}{} + if item != "" { + set.data[item] = struct{}{} + } return item } diff --git a/container/gtree/gtree_avltree.go b/container/gtree/gtree_avltree.go index 848e6099a..3d26390f4 100644 --- a/container/gtree/gtree_avltree.go +++ b/container/gtree/gtree_avltree.go @@ -130,7 +130,9 @@ func (tree *AVLTree) doSetWithLockCheck(key interface{}, value interface{}) inte if f, ok := value.(func() interface{}); ok { value = f() } - tree.put(key, value, nil, &tree.root) + if value != nil { + tree.put(key, value, nil, &tree.root) + } return value } diff --git a/container/gtree/gtree_btree.go b/container/gtree/gtree_btree.go index fb264b82e..6b41ce9f0 100644 --- a/container/gtree/gtree_btree.go +++ b/container/gtree/gtree_btree.go @@ -128,7 +128,9 @@ func (tree *BTree) doSetWithLockCheck(key interface{}, value interface{}) interf if f, ok := value.(func() interface{}); ok { value = f() } - tree.doSet(key, value) + if value != nil { + tree.doSet(key, value) + } return value } diff --git a/container/gtree/gtree_redblacktree.go b/container/gtree/gtree_redblacktree.go index 2bca67bb4..4abb72684 100644 --- a/container/gtree/gtree_redblacktree.go +++ b/container/gtree/gtree_redblacktree.go @@ -170,7 +170,9 @@ func (tree *RedBlackTree) doSetWithLockCheck(key interface{}, value interface{}) if f, ok := value.(func() interface{}); ok { value = f() } - tree.doSet(key, value) + if value != nil { + tree.doSet(key, value) + } return value } diff --git a/os/gcfg/gcfg.go b/os/gcfg/gcfg.go index 16078e8ad..78eee2438 100644 --- a/os/gcfg/gcfg.go +++ b/os/gcfg/gcfg.go @@ -252,16 +252,28 @@ func (c *Config) FilePath(file ...string) (path string) { if len(file) > 0 { name = file[0] } - c.paths.RLockFunc(func(array []string) { - for _, prefix := range array { - // Firstly checking the resource manager. - for _, v := range resourceTryFiles { - if file := gres.Get(prefix + v + name); file != nil { - path = file.Name() - return + // Searching resource manager. + if !gres.IsEmpty() { + for _, v := range resourceTryFiles { + if file := gres.Get(v + name); file != nil { + path = file.Name() + return + } + } + c.paths.RLockFunc(func(array []string) { + for _, prefix := range array { + for _, v := range resourceTryFiles { + if file := gres.Get(prefix + v + name); file != nil { + path = file.Name() + return + } } } - // Secondly checking the file system. + }) + } + // Searching the file system. + c.paths.RLockFunc(func(array []string) { + for _, prefix := range array { if path, _ = gspath.Search(prefix, name); path != "" { return } @@ -270,15 +282,6 @@ func (c *Config) FilePath(file ...string) (path string) { } } }) - // Checking the configuration file in default paths. - if path == "" && !gres.IsEmpty() { - for _, v := range resourceTryFiles { - if file := gres.Get(v + name); file != nil { - path = file.Name() - return - } - } - } return } diff --git a/os/gfile/gfile.go b/os/gfile/gfile.go index b6e8541b0..03de36dbf 100644 --- a/os/gfile/gfile.go +++ b/os/gfile/gfile.go @@ -252,6 +252,7 @@ func CopyDir(src string, dst string) (err error) { } // DirNames returns sub-file names of given directory . +// Note that the returned names are NOT absolute paths. func DirNames(path string) ([]string, error) { f, err := os.Open(path) if err != nil { diff --git a/os/gfile/gfile_scan.go b/os/gfile/gfile_scan.go index 1ecb9c8ce..adf247f96 100644 --- a/os/gfile/gfile_scan.go +++ b/os/gfile/gfile_scan.go @@ -56,6 +56,8 @@ func ScanDirFile(path string, pattern string, recursive ...bool) ([]string, erro // using the ',' symbol to separate multiple patterns. // // It scans directory recursively if given parameter is true. +// +// It returns only files except folders if is true. func doScanDir(path string, pattern string, recursive bool, onlyFile bool) ([]string, error) { list := ([]string)(nil) file, err := os.Open(path) diff --git a/os/gfsnotify/gfsnotify.go b/os/gfsnotify/gfsnotify.go index e70892320..91eca01d0 100644 --- a/os/gfsnotify/gfsnotify.go +++ b/os/gfsnotify/gfsnotify.go @@ -5,13 +5,13 @@ // You can obtain one at https://github.com/gogf/gf. // Package gfsnotify provides a platform-independent interface for file system notifications. -// -// 文件监控. package gfsnotify import ( "errors" "fmt" + "github.com/gogf/gf/container/gset" + "time" "github.com/fsnotify/fsnotify" "github.com/gogf/gf/container/glist" @@ -21,36 +21,37 @@ import ( "github.com/gogf/gf/os/gcache" ) -// 监听管理对象 +// Watcher is the monitor for file changes. type Watcher struct { - watcher *fsnotify.Watcher // 底层fsnotify对象 - events *gqueue.Queue // 过滤后的事件通知,不会出现重复事件 - cache *gcache.Cache // 缓存对象,主要用于事件重复过滤 - callbacks *gmap.StrAnyMap // 注册的所有绝对路径(文件/目录)及其对应的回调函数列表map - closeChan chan struct{} // 关闭事件 + watcher *fsnotify.Watcher // Underlying fsnotify object. + events *gqueue.Queue // Used for internal event management. + cache *gcache.Cache // Used for repeated event filter. + nameSet *gset.StrSet // Used for AddOnce feature. + callbacks *gmap.StrAnyMap // Path(file/folder) to callbacks mapping. + closeChan chan struct{} // Used for watcher closing notification. } -// 注册的监听回调方法 +// Callback is the callback function for Watcher. type Callback struct { - Id int // 唯一ID - Func func(event *Event) // 回调方法 - Path string // 监听的文件/目录 - elem *glist.Element // 指向回调函数链表中的元素项位置(便于删除) - recursive bool // 当目录时,是否递归监听(使用在子文件/目录回溯查找回调函数时) + Id int // Unique id for callback object. + Func func(event *Event) // Callback function. + Path string // Bound file path (absolute). + name string // Registered name for AddOnce. + elem *glist.Element // Element in the callbacks of watcher. + recursive bool // Is bound to path recursively or not. } -// 监听事件对象 +// Event is the event produced by underlying fsnotify. type Event struct { - event fsnotify.Event // 底层事件对象 - Path string // 文件绝对路径 - Op Op // 触发监听的文件操作 - Watcher *Watcher // 事件对应的监听对象 + event fsnotify.Event // Underlying event. + Path string // Absolute file path. + Op Op // File operation. + Watcher *Watcher // Parent watcher. } -// 按位进行识别的操作集合 +// Op is the bits union for file operations. type Op uint32 -// 必须放到一个const分组里面 const ( CREATE Op = 1 << iota WRITE @@ -60,26 +61,24 @@ const ( ) const ( - REPEAT_EVENT_FILTER_INTERVAL = 1 // (毫秒)重复事件过滤间隔 - gFSNOTIFY_EVENT_EXIT = "exit" // 是否退出回调执行 + REPEAT_EVENT_FILTER_DURATION = time.Millisecond // Duration for repeated event filter. + gFSNOTIFY_EVENT_EXIT = "exit" // Custom exit event for internal usage. ) var ( - // 默认的Watcher对象 - defaultWatcher, _ = New() - // 默认的watchers是否初始化,使用时才创建 - watcherInited = gtype.NewBool() - // 回调方法ID与对象指针的映射哈希表,用于根据ID快速查找回调对象 - callbackIdMap = gmap.NewIntAnyMap(true) - // 回调函数的ID生成器(原子操作) - callbackIdGenerator = gtype.NewInt() + defaultWatcher, _ = New() // Default watcher. + callbackIdMap = gmap.NewIntAnyMap(true) // Id to callback mapping. + callbackIdGenerator = gtype.NewInt() // Atomic id generator for callback. ) -// 创建监听管理对象,主要注意的是创建监听对象会占用系统的inotify句柄数量,受到 fs.inotify.max_user_instances 的限制 +// New creates and returns a new watcher. +// Note that the watcher number is limited by the file handle setting of the system. +// Eg: fs.inotify.max_user_instances system variable in linux systems. func New() (*Watcher, error) { w := &Watcher{ cache: gcache.New(), events: gqueue.New(), + nameSet: gset.NewStrSet(true), closeChan: make(chan struct{}), callbacks: gmap.NewStrAnyMap(true), } @@ -93,17 +92,27 @@ func New() (*Watcher, error) { return w, nil } -// 添加对指定文件/目录的监听,并给定回调函数;如果给定的是一个目录,默认递归监控。 +// Add monitors using default watcher with callback function . +// The optional parameter specifies whether monitoring the recursively, which is true in default. func Add(path string, callbackFunc func(event *Event), recursive ...bool) (callback *Callback, err error) { return defaultWatcher.Add(path, callbackFunc, recursive...) } -// 递归移除对指定文件/目录的所有监听回调 +// AddOnce monitors using default watcher with callback function only once using unique name . +// If AddOnce is called multiple times with the same parameter, is only added to monitor once. It returns error +// if it's called twice with the same . +// +// The optional parameter specifies whether monitoring the recursively, which is true in default. +func AddOnce(name, path string, callbackFunc func(event *Event), recursive ...bool) (callback *Callback, err error) { + return defaultWatcher.AddOnce(name, path, callbackFunc, recursive...) +} + +// Remove removes all monitoring callbacks of given from watcher recursively. func Remove(path string) error { return defaultWatcher.Remove(path) } -// 根据指定的回调函数ID,移出指定的inotify回调函数 +// RemoveCallback removes specified callback with given id from watcher. func RemoveCallback(callbackId int) error { callback := (*Callback)(nil) if r := callbackIdMap.Get(callbackId); r != nil { @@ -116,7 +125,7 @@ func RemoveCallback(callbackId int) error { return nil } -// 在回调方法中调用该方法退出回调注册 +// Exit is only used in the callback function, which can be used to remove current callback from the watcher. func Exit() { panic(gFSNOTIFY_EVENT_EXIT) } diff --git a/os/gfsnotify/gfsnotify_event.go b/os/gfsnotify/gfsnotify_event.go index 86b8bf867..4811b6235 100644 --- a/os/gfsnotify/gfsnotify_event.go +++ b/os/gfsnotify/gfsnotify_event.go @@ -6,31 +6,32 @@ package gfsnotify +// String returns current event as string. func (e *Event) String() string { return e.event.String() } -// 文件/目录创建 +// IsCreate checks whether current event contains file/folder create event. func (e *Event) IsCreate() bool { return e.Op == 1 || e.Op&CREATE == CREATE } -// 文件/目录修改 +// IsWrite checks whether current event contains file/folder write event. func (e *Event) IsWrite() bool { return e.Op&WRITE == WRITE } -// 文件/目录删除 +// IsRemove checks whether current event contains file/folder remove event. func (e *Event) IsRemove() bool { return e.Op&REMOVE == REMOVE } -// 文件/目录重命名 +// IsRename checks whether current event contains file/folder rename event. func (e *Event) IsRename() bool { return e.Op&RENAME == RENAME } -// 文件/目录修改权限 +// IsChmod checks whether current event contains file/folder chmod event. func (e *Event) IsChmod() bool { return e.Op&CHMOD == CHMOD } diff --git a/os/gfsnotify/gfsnotify_filefunc.go b/os/gfsnotify/gfsnotify_filefunc.go index 9796c53a2..37f0da364 100644 --- a/os/gfsnotify/gfsnotify_filefunc.go +++ b/os/gfsnotify/gfsnotify_filefunc.go @@ -14,13 +14,19 @@ import ( "strings" ) -// 获取指定文件路径的目录地址绝对路径 +// fileDir returns all but the last element of path, typically the path's directory. +// After dropping the final element, Dir calls Clean on the path and trailing +// slashes are removed. +// If the path is empty, Dir returns ".". +// If the path consists entirely of separators, Dir returns a single separator. +// The returned path does not end in a separator unless it is the root directory. func fileDir(path string) string { return filepath.Dir(path) } -// 将所给定的路径转换为绝对路径 -// 并判断文件路径是否存在,如果文件不存在,那么返回空字符串 +// fileRealPath converts the given to its absolute path +// and checks if the file path exists. +// If the file does not exist, return an empty string. func fileRealPath(path string) string { p, err := filepath.Abs(path) if err != nil { @@ -32,7 +38,7 @@ func fileRealPath(path string) string { return p } -// 判断所给路径文件/文件夹是否存在 +// fileExists checks whether given exist. func fileExists(path string) bool { if _, err := os.Stat(path); !os.IsNotExist(err) { return true @@ -40,7 +46,7 @@ func fileExists(path string) bool { return false } -// 判断所给路径是否为文件夹 +// fileIsDir checks whether given a directory. func fileIsDir(path string) bool { s, err := os.Stat(path) if err != nil { @@ -49,21 +55,18 @@ func fileIsDir(path string) bool { return s.IsDir() } -// 返回制定目录其子级所有的目录绝对路径(包含自身) +// fileAllDirs returns all sub-folders including itself of given recursively. func fileAllDirs(path string) (list []string) { list = []string{path} - // 打开目录 file, err := os.Open(path) if err != nil { return list } defer file.Close() - // 读取目录下的文件列表 names, err := file.Readdirnames(-1) if err != nil { return list } - // 是否递归遍历 for _, name := range names { path := fmt.Sprintf("%s%s%s", path, string(filepath.Separator), name) if fileIsDir(path) { @@ -75,7 +78,8 @@ func fileAllDirs(path string) (list []string) { return } -// 打开目录,并返回其下一级文件列表(绝对路径),按照文件名称大小写进行排序,支持目录递归遍历。 +// fileScanDir returns all sub-files with absolute paths of given , +// It scans directory recursively if given parameter is true. func fileScanDir(path string, pattern string, recursive ...bool) ([]string, error) { list, err := doFileScanDir(path, pattern, recursive...) if err != nil { @@ -87,34 +91,36 @@ func fileScanDir(path string, pattern string, recursive ...bool) ([]string, erro return list, nil } -// 内部检索目录方法,支持递归,返回没有排序的文件绝对路径列表结果。 -// pattern参数支持多个文件名称模式匹配,使用','符号分隔多个模式。 +// doFileScanDir is an internal method which scans directory +// and returns the absolute path list of files that are not sorted. +// +// The pattern parameter supports multiple file name patterns, +// using the ',' symbol to separate multiple patterns. +// +// It scans directory recursively if given parameter is true. func doFileScanDir(path string, pattern string, recursive ...bool) ([]string, error) { list := ([]string)(nil) - // 打开目录 file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() - // 读取目录下的文件列表 names, err := file.Readdirnames(-1) if err != nil { return nil, err } - // 是否递归遍历 + filePath := "" for _, name := range names { - path := fmt.Sprintf("%s%s%s", path, string(filepath.Separator), name) - if fileIsDir(path) && len(recursive) > 0 && recursive[0] { - array, _ := doFileScanDir(path, pattern, true) + filePath = fmt.Sprintf("%s%s%s", path, string(filepath.Separator), name) + if fileIsDir(filePath) && len(recursive) > 0 && recursive[0] { + array, _ := doFileScanDir(filePath, pattern, true) if len(array) > 0 { list = append(list, array...) } } - // 满足pattern才加入结果列表 for _, p := range strings.Split(pattern, ",") { if match, err := filepath.Match(strings.TrimSpace(p), name); err == nil && match { - list = append(list, path) + list = append(list, filePath) } } } diff --git a/os/gfsnotify/gfsnotify_watcher.go b/os/gfsnotify/gfsnotify_watcher.go index 5dbe0d65f..1561041c5 100644 --- a/os/gfsnotify/gfsnotify_watcher.go +++ b/os/gfsnotify/gfsnotify_watcher.go @@ -9,50 +9,69 @@ package gfsnotify import ( "errors" "fmt" + "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/container/glist" ) -// 添加监控,path参数支持文件或者目录路径,recursive为非必需参数,默认为递归监控(当path为目录时)。 -// 如果添加目录,这里只会返回目录的callback,按照callback删除时会递归删除。 +// Add monitors with callback function to the watcher. +// The optional parameter specifies whether monitoring the recursively, which is true in default. func (w *Watcher) Add(path string, callbackFunc func(event *Event), recursive ...bool) (callback *Callback, err error) { - // 首先添加这个文件/目录 - callback, err = w.addWithCallbackFunc(path, callbackFunc, recursive...) - if err != nil { - return nil, err - } - // 如果需要递归,那么递归添加其下的子级目录, - // 注意!! - // 1、这里只递归添加**目录**, 而非文件,因为监控了目录即监控了其下一级的文件; - // 2、这里只是添加底层监控对象对**子级所有目录**的监控,没有任何回调函数的设置,在事件产生时会回溯查找父级的回调函数; - if fileIsDir(path) && (len(recursive) == 0 || recursive[0]) { - for _, subPath := range fileAllDirs(path) { - if fileIsDir(subPath) { - w.watcher.Add(subPath) + return w.AddOnce("", path, callbackFunc, recursive...) +} + +// AddOnce monitors with callback function only once using unique name to the watcher. +// If AddOnce is called multiple times with the same parameter, is only added to monitor once. It returns error +// if it's called twice with the same . +// +// The optional parameter specifies whether monitoring the recursively, which is true in default. +func (w *Watcher) AddOnce(name, path string, callbackFunc func(event *Event), recursive ...bool) (callback *Callback, err error) { + w.nameSet.AddIfNotExistFuncLock(name, func() string { + // Firstly add the path to watcher. + callback, err = w.addWithCallbackFunc(name, path, callbackFunc, recursive...) + if err != nil { + return "" + } + // If it's recursive adding, it then adds all sub-folders to the monitor. + // NOTE: + // 1. It only recursively adds **folders** to the monitor, NOT files, + // because if the folders are monitored and their sub-files are also monitored. + // 2. It bounds no callbacks to the folders, because it will search the callbacks + // from its parent recursively if any event produced. + if fileIsDir(path) && (len(recursive) == 0 || recursive[0]) { + for _, subPath := range fileAllDirs(path) { + if fileIsDir(subPath) { + if err := w.watcher.Add(subPath); err != nil { + intlog.Error(err) + } + } } } - } + return name + }) return } -// 添加对指定文件/目录的监听,并给定回调函数 -func (w *Watcher) addWithCallbackFunc(path string, callbackFunc func(event *Event), recursive ...bool) (callback *Callback, err error) { - // 这里统一转换为当前系统的绝对路径,便于统一监控文件名称 +// addWithCallbackFunc adds the path to underlying monitor, creates and returns a callback object. +func (w *Watcher) addWithCallbackFunc(name, path string, callbackFunc func(event *Event), recursive ...bool) (callback *Callback, err error) { + // Check and convert the given path to absolute path. if t := fileRealPath(path); t == "" { return nil, errors.New(fmt.Sprintf(`"%s" does not exist`, path)) } else { path = t } + // Create callback object. callback = &Callback{ Id: callbackIdGenerator.Add(1), Func: callbackFunc, Path: path, + name: name, recursive: true, } if len(recursive) > 0 { callback.recursive = recursive[0] } - // 注册回调函数 + // Register the callback to watcher. w.callbacks.LockFunc(func(m map[string]interface{}) { list := (*glist.List)(nil) if v, ok := m[path]; !ok { @@ -63,23 +82,29 @@ func (w *Watcher) addWithCallbackFunc(path string, callbackFunc func(event *Even } callback.elem = list.PushBack(callback) }) - // 添加底层监听 - w.watcher.Add(path) - // 添加成功后会注册该callback id到全局的哈希表 + // Add the path to underlying monitor. + if err := w.watcher.Add(path); err != nil { + intlog.Error(err) + } + // Add the callback to global callback map. callbackIdMap.Set(callback.Id, callback) + + intlog.Print("addWithCallbackFunc", name, path, callback.recursive) return } -// 关闭监听管理对象 +// Close closes the watcher. func (w *Watcher) Close() { w.events.Close() - w.watcher.Close() + if err := w.watcher.Close(); err != nil { + intlog.Error(err) + } close(w.closeChan) } -// 递归移除对指定文件/目录的所有监听回调 +// Remove removes monitor and all callbacks associated with the recursively. func (w *Watcher) Remove(path string) error { - // 首先移除path注册的回调注册,以及callbackIdMap中的ID + // Firstly remove the callbacks of the path. if r := w.callbacks.Remove(path); r != nil { list := r.(*glist.List) for { @@ -90,36 +115,39 @@ func (w *Watcher) Remove(path string) error { } } } - // 其次递归判断所有的子级是否可删除监听 + // Secondly remove monitor of all sub-files which have no callbacks. if subPaths, err := fileScanDir(path, "*", true); err == nil && len(subPaths) > 0 { for _, subPath := range subPaths { if w.checkPathCanBeRemoved(subPath) { - w.watcher.Remove(subPath) + if err := w.watcher.Remove(subPath); err != nil { + intlog.Error(err) + } } } } - // 最后移除底层的监听 + // Lastly remove the monitor of the path from underlying monitor. return w.watcher.Remove(path) } -// 判断给定的路径是否可以删除监听(只有所有回调函数都没有了才能删除) +// checkPathCanBeRemoved checks whether the given path have no callbacks bound. func (w *Watcher) checkPathCanBeRemoved(path string) bool { - // 首先检索path对应的回调函数 + // Firstly check the callbacks in the watcher directly. if v := w.callbacks.Get(path); v != nil { return false } - // 其次查找父级目录有无回调注册 + // Secondly check its parent whether has callbacks. dirPath := fileDir(path) - if v := w.callbacks.Get(dirPath); v != nil { + if v := w.callbacks.Get(dirPath); v != nil && v.(*Callback).recursive { return false } - // 最后回溯查找递归回调函数 + // Recursively check its parent. + parentDirPath := "" for { - parentDirPath := fileDir(dirPath) + parentDirPath = fileDir(dirPath) if parentDirPath == dirPath { break } - if v := w.callbacks.Get(parentDirPath); v != nil { + if v := w.callbacks.Get(parentDirPath); v != nil && v.(*Callback).recursive { return false } dirPath = parentDirPath @@ -127,7 +155,7 @@ func (w *Watcher) checkPathCanBeRemoved(path string) bool { return true } -// 根据指定的回调函数ID,移出指定的inotify回调函数 +// RemoveCallback removes callback with given callback id from watcher. func (w *Watcher) RemoveCallback(callbackId int) { callback := (*Callback)(nil) if r := callbackIdMap.Get(callbackId); r != nil { @@ -138,5 +166,8 @@ func (w *Watcher) RemoveCallback(callbackId int) { r.(*glist.List).Remove(callback.elem) } callbackIdMap.Remove(callbackId) + if callback.name != "" { + w.nameSet.Remove(callback.name) + } } } diff --git a/os/gfsnotify/gfsnotify_watcher_loop.go b/os/gfsnotify/gfsnotify_watcher_loop.go index 814b47bbc..dc1c87503 100644 --- a/os/gfsnotify/gfsnotify_watcher_loop.go +++ b/os/gfsnotify/gfsnotify_watcher_loop.go @@ -8,20 +8,22 @@ package gfsnotify import ( "github.com/gogf/gf/container/glist" + "github.com/gogf/gf/internal/intlog" ) -// 监听循环 +// startWatchLoop starts the loop for event listening fro underlying inotify monitor. func (w *Watcher) startWatchLoop() { go func() { for { select { - // 关闭事件 + // Close event. case <-w.closeChan: return - // 监听事件 + // Event listening. case ev := <-w.watcher.Events: - //fmt.Println("ev:", ev.String()) + //intlog.Print(ev.String()) + // Filter the repeated event in custom duration. w.cache.SetIfNotExist(ev.String(), func() interface{} { w.events.Push(&Event{ event: ev, @@ -30,33 +32,36 @@ func (w *Watcher) startWatchLoop() { Watcher: w, }) return struct{}{} - }, REPEAT_EVENT_FILTER_INTERVAL) + }, REPEAT_EVENT_FILTER_DURATION) - case <-w.watcher.Errors: - //fmt.Fprintf(os.Stderr, "[gfsnotify] error: %s\n", err.Error()) + case err := <-w.watcher.Errors: + intlog.Error(err) } } }() } -// 获得文件路径的监听回调,包括层级的监听回调。 +// getCallbacks searches and returns all callbacks with given . +// It also searches its parent for callbacks if they're recursive. func (w *Watcher) getCallbacks(path string) (callbacks []*Callback) { - // 首先检索path对应的回调函数 + // Firstly add the callbacks of itself. if v := w.callbacks.Get(path); v != nil { for _, v := range v.(*glist.List).FrontAll() { callback := v.(*Callback) callbacks = append(callbacks, callback) } } - // 其次查找父级目录有无回调注册 + // Secondly searches its parent for callbacks. dirPath := fileDir(path) if v := w.callbacks.Get(dirPath); v != nil { for _, v := range v.(*glist.List).FrontAll() { callback := v.(*Callback) - callbacks = append(callbacks, callback) + if callback.recursive { + callbacks = append(callbacks, callback) + } } } - // 最后回溯查找递归回调函数 + // Lastly searches the parent recursively for callbacks. for { parentDirPath := fileDir(dirPath) if parentDirPath == dirPath { @@ -75,62 +80,74 @@ func (w *Watcher) getCallbacks(path string) (callbacks []*Callback) { return } -// 事件循环(核心逻辑) +// startEventLoop is the core event handler. func (w *Watcher) startEventLoop() { go func() { for { if v := w.events.Pop(); v != nil { event := v.(*Event) - // 如果该路径一个回调也没有,那么没有必要执行后续逻辑,删除对该文件的监听 + // If there's no any callback of this path, it removes it from monitor. callbacks := w.getCallbacks(event.Path) if len(callbacks) == 0 { - w.watcher.Remove(event.Path) + if err := w.watcher.Remove(event.Path); err != nil { + intlog.Error(err) + } continue } switch { - // 如果是删除操作,那么需要判断是否文件真正不存在了,如果存在,那么将此事件认为“假删除” case event.IsRemove(): + // It should check again the existence of the path. + // It adds it back to the monitor if it still exists. if fileExists(event.Path) { - // 底层重新添加监控(不用担心重复添加) - w.watcher.Add(event.Path) - // 修改事件操作为重命名(相当于重命名为自身名称,最终名称没变) + // It adds the path back to monitor. + // We need no worry about the repeat adding. + if err := w.watcher.Add(event.Path); err != nil { + intlog.Error(err) + } + // Change the event to RENAME, which means it renames itself to its origin name. event.Op = RENAME } - // 如果是重命名操作,那么需要判断是否文件真正不存在了,如果存在,那么将此事件认为“假命名” - // (特别是某些编辑器在编辑文件时会先对文件RENAME再CHMOD) case event.IsRename(): + // It should check again the existence of the path. + // It adds it back to the monitor if it still exists. + // Especially Some editors might do RENAME and then CHMOD when it's editing file. if fileExists(event.Path) { - // 底层有可能去掉了监控, 这里重新添加监控(不用担心重复添加) - w.watcher.Add(event.Path) - // 修改事件操作为修改属性 + // It might lost the monitoring for the path, so we add the path back to monitor. + // We need no worry about the repeat adding. + if err := w.watcher.Add(event.Path); err != nil { + intlog.Error(err) + } + // Change the event to CHMOD. event.Op = CHMOD } - // 创建文件/目录 case event.IsCreate(): // ========================================= - // 注意这里只是添加底层监听,并没有注册任何的回调函数, - // 默认的回调函数为父级的递归回调 + // Note that it here just adds the path to monitor without any callback registering, + // because its parent already has the callbacks. // ========================================= if fileIsDir(event.Path) { - // 递归添加 + // If it's a folder, it then does adding recursively to monitor. for _, subPath := range fileAllDirs(event.Path) { if fileIsDir(subPath) { - w.watcher.Add(subPath) + if err := w.watcher.Add(subPath); err != nil { + intlog.Error(err) + } } } } else { - // 添加文件监听 - w.watcher.Add(event.Path) + // If it's a file, it directly adds it to monitor. + if err := w.watcher.Add(event.Path); err != nil { + intlog.Error(err) + } } } - // 执行回调处理,异步处理 + // Calling the callbacks in order. for _, v := range callbacks { go func(callback *Callback) { defer func() { - // 是否退出监控 if err := recover(); err != nil { switch err { case gFSNOTIFY_EVENT_EXIT: diff --git a/os/gfsnotify/gfsnotify_z_unit_test.go b/os/gfsnotify/gfsnotify_z_unit_test.go index 621247a79..6cbd106e2 100644 --- a/os/gfsnotify/gfsnotify_z_unit_test.go +++ b/os/gfsnotify/gfsnotify_z_unit_test.go @@ -4,8 +4,6 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -// go test *.go -bench=".*" -benchmem - package gfsnotify_test import ( diff --git a/os/gview/gview.go b/os/gview/gview.go index a85aad2b4..76f26128e 100644 --- a/os/gview/gview.go +++ b/os/gview/gview.go @@ -13,6 +13,7 @@ package gview import ( "errors" "fmt" + "github.com/gogf/gf/container/gmap" "sync" "github.com/gogf/gf/i18n/gi18n" @@ -29,12 +30,13 @@ import ( // View object for template engine. type View struct { - mu sync.RWMutex - paths *garray.StrArray // Searching path array. - data map[string]interface{} // Global template variables. - funcMap map[string]interface{} // Global template function map. - i18nManager *gi18n.Manager // I18n manager for this view. - delimiters []string // Customized template delimiters. + mu sync.RWMutex + paths *garray.StrArray // Searching path array, NOT concurrent safe for performance purpose. + data map[string]interface{} // Global template variables. + funcMap map[string]interface{} // Global template function map. + fileCacheMap *gmap.StrAnyMap // File cache map. + i18nManager *gi18n.Manager // I18n manager for this view. + delimiters []string // Customized template delimiters. } // Params is type for template params. @@ -65,11 +67,12 @@ func ParseContent(content string, params Params) (string, error) { // The parameter specifies the template directory path to load template files. func New(path ...string) *View { view := &View{ - paths: garray.NewStrArray(true), - data: make(map[string]interface{}), - funcMap: make(map[string]interface{}), - i18nManager: gi18n.Instance(), - delimiters: make([]string, 2), + paths: garray.NewStrArray(), + data: make(map[string]interface{}), + funcMap: make(map[string]interface{}), + fileCacheMap: gmap.NewStrAnyMap(true), + i18nManager: gi18n.Instance(), + delimiters: make([]string, 2), } if len(path) > 0 && len(path[0]) > 0 { view.SetPath(path[0]) diff --git a/os/gview/gview_doparse.go b/os/gview/gview_doparse.go index 96d65ba54..2756e9ebc 100644 --- a/os/gview/gview_doparse.go +++ b/os/gview/gview_doparse.go @@ -10,22 +10,23 @@ import ( "bytes" "errors" "fmt" + "github.com/gogf/gf/encoding/ghash" + "github.com/gogf/gf/internal/intlog" + "github.com/gogf/gf/os/gfcache" + "github.com/gogf/gf/os/gfsnotify" + "github.com/gogf/gf/os/gmlock" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/util/gconv" + "strconv" "strings" "text/template" - "github.com/gogf/gf/os/gfcache" - "github.com/gogf/gf/os/gres" "github.com/gogf/gf/container/gmap" - "github.com/gogf/gf/encoding/ghash" "github.com/gogf/gf/os/gfile" - "github.com/gogf/gf/os/gfsnotify" "github.com/gogf/gf/os/glog" - "github.com/gogf/gf/os/gmlock" "github.com/gogf/gf/os/gspath" - "github.com/gogf/gf/text/gstr" - "github.com/gogf/gf/util/gconv" ) const ( @@ -40,6 +41,154 @@ var ( resourceTryFiles = []string{"template/", "template", "/template", "/template/"} ) +// ParseContent parses given template file +// with given template parameters and function map +// and returns the parsed string content. +func (view *View) Parse(file string, params ...Params) (result string, err error) { + view.mu.RLock() + defer view.mu.RUnlock() + + var tpl *template.Template + // It here uses map cache to enhance the performance. + r := view.fileCacheMap.GetOrSetFuncLock(file, func() interface{} { + var path, folder string + var resource *gres.File + // Searching the absolute file path for . + path, folder, resource, err = view.searchFile(file) + if err != nil { + return nil + } + // Get the template object instance for . + tpl, err = view.getTemplate(folder, fmt.Sprintf(`*%s`, gfile.Ext(path))) + if err != nil { + return nil + } + // Using memory lock to ensure concurrent safety for template parsing. + gmlock.LockFunc("gview.Parse:"+folder, func() { + if resource != nil { + tpl, err = tpl.Parse(gconv.UnsafeBytesToStr(resource.Content())) + } else { + tpl, err = tpl.Parse(gfcache.GetContents(path)) + } + }) + if err != nil { + return nil + } + // Monitor template files changes using fsnotify asynchronously. + if resource == nil { + if _, err := gfsnotify.AddOnce("gview.Parse:"+folder, folder, func(event *gfsnotify.Event) { + // CLEAR THEM ALL. + view.fileCacheMap.Clear() + templates.Clear() + gfsnotify.Exit() + }); err != nil { + intlog.Error(err) + } + } + return tpl + }) + if r == nil { + return + } + tpl = r.(*template.Template) + // Note that the template variable assignment cannot change the value + // of the existing or view.data because both variables are pointers. + // It needs to merge the values of the two maps into a new map. + var variables map[string]interface{} + length := len(view.data) + if len(params) > 0 { + length += len(params[0]) + } + if length > 0 { + variables = make(map[string]interface{}, length) + } + if len(view.data) > 0 { + if len(params) > 0 { + if variables == nil { + variables = make(map[string]interface{}) + } + for k, v := range params[0] { + variables[k] = v + } + for k, v := range view.data { + variables[k] = v + } + } else { + variables = view.data + } + } else { + if len(params) > 0 { + variables = params[0] + } + } + buffer := bytes.NewBuffer(nil) + if err := tpl.Execute(buffer, variables); err != nil { + return "", err + } + // TODO any graceful plan to replace ""? + result = gstr.Replace(buffer.String(), "", "") + result = view.i18nTranslate(result, variables) + return result, nil +} + +// ParseContent parses given template content +// with given template parameters and function map +// and returns the parsed content in []byte. +func (view *View) ParseContent(content string, params ...Params) (string, error) { + view.mu.RLock() + defer view.mu.RUnlock() + err := (error)(nil) + tpl := templates.GetOrSetFuncLock(gCONTENT_TEMPLATE_NAME, func() interface{} { + return template.New(gCONTENT_TEMPLATE_NAME).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap) + }).(*template.Template) + // Using memory lock to ensure concurrent safety for content parsing. + hash := strconv.FormatUint(ghash.DJBHash64([]byte(content)), 10) + gmlock.LockFunc("gview.ParseContent:"+hash, func() { + tpl, err = tpl.Parse(content) + }) + if err != nil { + return "", err + } + // Note that the template variable assignment cannot change the value + // of the existing or view.data because both variables are pointers. + // It needs to merge the values of the two maps into a new map. + var variables map[string]interface{} + length := len(view.data) + if len(params) > 0 { + length += len(params[0]) + } + if length > 0 { + variables = make(map[string]interface{}, length) + } + if len(view.data) > 0 { + if len(params) > 0 { + if variables == nil { + variables = make(map[string]interface{}) + } + for k, v := range params[0] { + variables[k] = v + } + for k, v := range view.data { + variables[k] = v + } + } else { + variables = view.data + } + } else { + if len(params) > 0 { + variables = params[0] + } + } + buffer := bytes.NewBuffer(nil) + if err := tpl.Execute(buffer, variables); err != nil { + return "", err + } + // TODO any graceful plan to replace ""? + result := gstr.Replace(buffer.String(), "", "") + result = view.i18nTranslate(result, variables) + return result, nil +} + // getTemplate returns the template object associated with given template folder . // It uses template cache to enhance performance, that is, it will return the same template object // with the same given . It will also refresh the template cache @@ -48,16 +197,19 @@ func (view *View) getTemplate(path string, pattern string) (tpl *template.Templa r := templates.GetOrSetFuncLock(path, func() interface{} { tpl = template.New(path).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap) // Firstly checking the resource manager. - if files := gres.ScanDirFile(path, pattern, true); len(files) > 0 { - var err error - for _, v := range files { - _, err = tpl.New(v.FileInfo().Name()).Parse(string(v.Content())) - if err != nil { - glog.Error(err) + if !gres.IsEmpty() { + if files := gres.ScanDirFile(path, pattern, true); len(files) > 0 { + var err error + for _, v := range files { + _, err = tpl.New(v.FileInfo().Name()).Parse(string(v.Content())) + if err != nil { + glog.Error(err) + } } + return tpl } - return tpl } + // Secondly checking the file system. files := ([]string)(nil) files, err = gfile.ScanDir(path, pattern, true) @@ -67,10 +219,6 @@ func (view *View) getTemplate(path string, pattern string) (tpl *template.Templa if tpl, err = tpl.ParseFiles(files...); err != nil { return nil } - _, _ = gfsnotify.Add(path, func(event *gfsnotify.Event) { - templates.Remove(path) - gfsnotify.Exit() - }) return tpl }) if r != nil { @@ -80,24 +228,34 @@ func (view *View) getTemplate(path string, pattern string) (tpl *template.Templa } // searchFile returns the found absolute path for , and its template folder path. -func (view *View) searchFile(file string) (path string, folder string, err error) { +func (view *View) searchFile(file string) (path string, folder string, resource *gres.File, err error) { // Firstly checking the resource manager. - view.paths.RLockFunc(func(array []string) { - f := (*gres.File)(nil) - for _, v := range array { - v = strings.TrimRight(v, "/") - if f = gres.Get(v + "/" + file); f != nil { - path = f.Name() - folder = gfile.Dir(path) - break - } - if f = gres.Get(v + "/template/" + file); f != nil { - path = f.Name() - folder = gfile.Dir(path) - break + if !gres.IsEmpty() { + for _, v := range resourceTryFiles { + if resource = gres.Get(v + file); resource != nil { + path = resource.Name() + folder = v + return } } - }) + + view.paths.RLockFunc(func(array []string) { + for _, v := range array { + v = strings.TrimRight(v, "/"+gfile.Separator) + if resource = gres.Get(v + "/" + file); resource != nil { + path = resource.Name() + folder = v + break + } + if resource = gres.Get(v + "/template/" + file); resource != nil { + path = resource.Name() + folder = v + "/template" + break + } + } + }) + } + // Secondly checking the file system. if path == "" { view.paths.RLockFunc(func(array []string) { @@ -114,16 +272,7 @@ func (view *View) searchFile(file string) (path string, folder string, err error } }) } - // Checking the configuration file in default paths. - if path == "" && !gres.IsEmpty() { - for _, v := range resourceTryFiles { - if file := gres.Get(v + file); file != nil { - path = file.Name() - folder = gfile.Dir(path) - return - } - } - } + // Error checking. if path == "" { buffer := bytes.NewBuffer(nil) @@ -152,118 +301,3 @@ func (view *View) searchFile(file string) (path string, folder string, err error } return } - -// ParseContent parses given template file -// with given template parameters and function map -// and returns the parsed string content. -func (view *View) Parse(file string, params ...Params) (parsed string, err error) { - view.mu.RLock() - defer view.mu.RUnlock() - path, folder, err := view.searchFile(file) - if err != nil { - return "", err - } - tpl, err := view.getTemplate(folder, fmt.Sprintf(`*%s`, gfile.Ext(path))) - if err != nil { - return "", err - } - // Using memory lock to ensure concurrent safety for template parsing. - gmlock.LockFunc("gview-parsing:"+folder, func() { - if file := gres.Get(path); file != nil { - tpl, err = tpl.Parse(string(file.Content())) - } else { - tpl, err = tpl.Parse(gfcache.GetContents(path)) - } - }) - if err != nil { - return "", err - } - // Note that the template variable assignment cannot change the value - // of the existing or view.data because both variables are pointers. - // It's need to merge the values of the two maps into a new map. - variables := (map[string]interface{})(nil) - length := len(view.data) - if len(params) > 0 { - length += len(params[0]) - } - if length > 0 { - variables = make(map[string]interface{}, length) - } - if len(view.data) > 0 { - if len(params) > 0 { - for k, v := range params[0] { - variables[k] = v - } - for k, v := range view.data { - variables[k] = v - } - } else { - variables = view.data - } - } else { - if len(params) > 0 { - variables = params[0] - } - } - buffer := bytes.NewBuffer(nil) - if err := tpl.Execute(buffer, variables); err != nil { - return "", err - } - result := gstr.Replace(buffer.String(), "", "") - result = view.i18nTranslate(result, variables) - return result, nil -} - -// ParseContent parses given template content -// with given template parameters and function map -// and returns the parsed content in []byte. -func (view *View) ParseContent(content string, params ...Params) (string, error) { - view.mu.RLock() - defer view.mu.RUnlock() - err := (error)(nil) - tpl := templates.GetOrSetFuncLock(gCONTENT_TEMPLATE_NAME, func() interface{} { - return template.New(gCONTENT_TEMPLATE_NAME).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap) - }).(*template.Template) - // Using memory lock to ensure concurrent safety for content parsing. - hash := gconv.String(ghash.DJBHash64([]byte(content))) - gmlock.LockFunc("gview-parsing-content:"+hash, func() { - tpl, err = tpl.Parse(content) - }) - if err != nil { - return "", err - } - // Note that the template variable assignment cannot change the value - // of the existing or view.data because both variables are pointers. - // It's need to merge the values of the two maps into a new map. - variables := (map[string]interface{})(nil) - length := len(view.data) - if len(params) > 0 { - length += len(params[0]) - } - if length > 0 { - variables = make(map[string]interface{}, length) - } - if len(view.data) > 0 { - if len(params) > 0 { - for k, v := range params[0] { - variables[k] = v - } - for k, v := range view.data { - variables[k] = v - } - } else { - variables = view.data - } - } else { - if len(params) > 0 { - variables = params[0] - } - } - buffer := bytes.NewBuffer(nil) - if err := tpl.Execute(buffer, variables); err != nil { - return "", err - } - result := gstr.Replace(buffer.String(), "", "") - result = view.i18nTranslate(result, variables) - return result, nil -}