mirror of
https://gitee.com/johng/gf.git
synced 2024-12-01 19:57:40 +08:00
初步完成第一阶段的性能改进
This commit is contained in:
parent
778595dd8f
commit
b190ee9fe6
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,6 +13,6 @@ gitpush.sh
|
||||
pkg/
|
||||
bin/
|
||||
cbuild
|
||||
*/.DS_Store
|
||||
**/.DS_Store
|
||||
.vscode/
|
||||
go.sum
|
||||
|
@ -159,7 +159,7 @@ func (r *Response) ServeFile(path string) {
|
||||
r.request.isFileServe = true
|
||||
// 首先判断是否给定的path已经是一个绝对路径
|
||||
if !gfile.Exists(path) {
|
||||
path = r.Server.paths.Search(path)
|
||||
path, _ = r.Server.paths.Search(path)
|
||||
}
|
||||
if path == "" {
|
||||
r.WriteStatus(http.StatusNotFound)
|
||||
|
@ -237,6 +237,18 @@ func (s *Server) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 配置相关相对路径处理
|
||||
if !gfile.Exists(s.config.HTTPSCertPath) {
|
||||
if t, _ := s.paths.Search(s.config.HTTPSCertPath); t != "" {
|
||||
s.config.HTTPSCertPath = t
|
||||
}
|
||||
}
|
||||
if !gfile.Exists(s.config.HTTPSKeyPath) {
|
||||
if t, _ := s.paths.Search(s.config.HTTPSKeyPath); t != "" {
|
||||
s.config.HTTPSKeyPath = t
|
||||
}
|
||||
}
|
||||
|
||||
// gzip压缩文件类型
|
||||
//if s.config.GzipContentTypes != nil {
|
||||
// for _, v := range s.config.GzipContentTypes {
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"gitee.com/johng/gf/g/encoding/ghtml"
|
||||
"gitee.com/johng/gf/g/os/gfile"
|
||||
"gitee.com/johng/gf/g/os/glog"
|
||||
"gitee.com/johng/gf/g/os/gtime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -55,12 +54,28 @@ func (s *Server)handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
s.callHookHandler(HOOK_AFTER_CLOSE, request)
|
||||
}()
|
||||
|
||||
// ============================================================
|
||||
// 优先级控制:
|
||||
// 静态文件 > 动态服务 > 静态目录
|
||||
// ============================================================
|
||||
|
||||
// 优先执行静态文件检索(检测是否存在对应的静态文件,包括index files处理)
|
||||
staticFile := s.paths.Search(r.URL.Path, s.config.IndexFiles...)
|
||||
staticFile, isStaticDir := s.paths.Search(r.URL.Path, s.config.IndexFiles...)
|
||||
if staticFile != "" {
|
||||
request.isFileRequest = true
|
||||
}
|
||||
glog.Info(staticFile)
|
||||
|
||||
// 动态服务检索
|
||||
handler := (*handlerItem)(nil)
|
||||
if !request.IsFileRequest() || isStaticDir {
|
||||
if parsedItem := s.getServeHandlerWithCache(request); parsedItem != nil {
|
||||
handler = parsedItem.handler
|
||||
for k, v := range parsedItem.values {
|
||||
request.routerVars[k] = v
|
||||
}
|
||||
request.Router = parsedItem.handler.router
|
||||
}
|
||||
}
|
||||
|
||||
// 事件 - BeforeServe
|
||||
s.callHookHandler(HOOK_BEFORE_SERVE, request)
|
||||
@ -68,22 +83,20 @@ func (s *Server)handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// 执行静态文件服务/回调控制器/执行对象/方法
|
||||
if !request.IsExited() {
|
||||
// 需要再次判断文件是否真实存在,因为文件检索可能使用了缓存,从健壮性考虑这里需要二次判断
|
||||
if request.IsFileRequest() && gfile.Exists(staticFile) && gfile.SelfPath() != staticFile {
|
||||
if request.IsFileRequest() && !isStaticDir /* && gfile.Exists(staticFile) */ && gfile.SelfPath() != staticFile {
|
||||
// 静态文件
|
||||
s.serveFile(request, staticFile)
|
||||
} else {
|
||||
// 动态服务检索
|
||||
handler := (*handlerItem)(nil)
|
||||
if parsedItem := s.getServeHandlerWithCache(request); parsedItem != nil {
|
||||
handler = parsedItem.handler
|
||||
for k, v := range parsedItem.values {
|
||||
request.routerVars[k] = v
|
||||
}
|
||||
request.Router = parsedItem.handler.router
|
||||
}
|
||||
if handler != nil {
|
||||
// 动态服务
|
||||
s.callServeHandler(handler, request)
|
||||
} else {
|
||||
request.Response.WriteStatus(http.StatusNotFound)
|
||||
if isStaticDir {
|
||||
// 静态目录
|
||||
s.serveFile(request, staticFile)
|
||||
} else {
|
||||
request.Response.WriteStatus(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,8 +71,8 @@ func (s *Server) setHandler(pattern string, handler *handlerItem, hook ... strin
|
||||
}
|
||||
regkey := s.hookHandlerKey(hookName, method, uri, domain)
|
||||
caller := s.getHandlerRegisterCallerLine(handler)
|
||||
if line, ok := s.routesMap[regkey]; ok {
|
||||
s := fmt.Sprintf(`duplicated route registry "%s" in %s , former in %s`, pattern, caller, line)
|
||||
if item, ok := s.routesMap[regkey]; ok {
|
||||
s := fmt.Sprintf(`duplicated route registry "%s", already registered in %s`, pattern, item.file)
|
||||
glog.Errorfln(s)
|
||||
return errors.New(s)
|
||||
} else {
|
||||
|
@ -10,7 +10,8 @@ import "gitee.com/johng/gf/g/os/gtime"
|
||||
|
||||
// 判断缓存项是否已过期
|
||||
func (item *memCacheItem) IsExpired() bool {
|
||||
if item.e > gtime.Millisecond() {
|
||||
// 注意这里应当包含等于,试想一下缓存时间只有最小粒度为1毫秒的情况
|
||||
if item.e >= gtime.Millisecond() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
@ -53,7 +53,8 @@ func (c *Config) filePath(file...string) string {
|
||||
if len(file) > 0 {
|
||||
name = file[0]
|
||||
}
|
||||
return c.paths.Search(name)
|
||||
path, _ := c.paths.Search(name)
|
||||
return path
|
||||
}
|
||||
|
||||
// 设置配置管理器的配置文件存放目录绝对路径
|
||||
@ -92,7 +93,8 @@ func (c *Config) GetFilePath(file...string) string {
|
||||
if len(file) > 0 {
|
||||
name = file[0]
|
||||
}
|
||||
return c.paths.Search(name)
|
||||
path, _ := c.paths.Search(name)
|
||||
return path
|
||||
}
|
||||
|
||||
// 设置配置管理对象的默认文件名称
|
||||
|
@ -255,15 +255,15 @@ func ScanDir(path string, pattern string, recursive ... bool) ([]string, error)
|
||||
// 内部检索目录方法,支持递归,返回没有排序的文件绝对路径列表结果。
|
||||
// pattern参数支持多个文件名称模式匹配,使用','符号分隔多个模式。
|
||||
func doScanDir(path string, pattern string, recursive ... bool) ([]string, error) {
|
||||
var list []string
|
||||
list := ([]string)(nil)
|
||||
// 打开目录
|
||||
dfile, err := os.Open(path)
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dfile.Close()
|
||||
defer file.Close()
|
||||
// 读取目录下的文件列表
|
||||
names, err := dfile.Readdirnames(-1)
|
||||
names, err := file.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -11,36 +11,29 @@ import (
|
||||
"container/list"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitee.com/johng/gf/g/container/glist"
|
||||
"gitee.com/johng/gf/g/container/gmap"
|
||||
"gitee.com/johng/gf/g/container/gqueue"
|
||||
"gitee.com/johng/gf/g/container/gtype"
|
||||
"gitee.com/johng/gf/g/os/gcache"
|
||||
"gitee.com/johng/gf/g/os/gcmd"
|
||||
"gitee.com/johng/gf/g/os/genv"
|
||||
"gitee.com/johng/gf/g/util/gconv"
|
||||
"gitee.com/johng/gf/third/github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// 监听管理对象
|
||||
type Watcher struct {
|
||||
watchers []*fsnotify.Watcher // 底层fsnotify对象,支持多个,以避免单个inotify对象监听队列上限问题
|
||||
watcher *fsnotify.Watcher // 底层fsnotify对象
|
||||
events *gqueue.Queue // 过滤后的事件通知,不会出现重复事件
|
||||
cache *gcache.Cache // 缓存对象,主要用于事件重复过滤
|
||||
callbacks *gmap.StringInterfaceMap // 注册的所有绝对路径机器对应的回调函数列表map
|
||||
recursivePaths *gmap.StringInterfaceMap // 支持递归监听的目录绝对路径及其对应的回调函数列表map
|
||||
callbacks *gmap.StringInterfaceMap // 注册的所有绝对路径(文件/目录)及其对应的回调函数列表map
|
||||
closeChan chan struct{} // 关闭事件
|
||||
}
|
||||
|
||||
// 注册的监听回调方法
|
||||
type Callback struct {
|
||||
Id int // 唯一ID
|
||||
Func func(event *Event) // 回调方法
|
||||
Path string // 监听的文件/目录
|
||||
addr string // Func对应的内存地址,用以判断回调的重复
|
||||
elem *list.Element // 指向监听链表中的元素项位置
|
||||
parent *Callback // 父级callback,有这个属性表示该callback为被自动管理的callback
|
||||
subs *glist.List // 子级回调对象指针列表
|
||||
Id int // 唯一ID
|
||||
Func func(event *Event) // 回调方法
|
||||
Path string // 监听的文件/目录
|
||||
elem *list.Element // 指向回调函数链表中的元素项位置(便于删除)
|
||||
recursive bool // 当目录时,是否递归监听(使用在子文件/目录回溯查找回调函数时)
|
||||
}
|
||||
|
||||
// 监听事件对象
|
||||
@ -65,77 +58,45 @@ const (
|
||||
|
||||
const (
|
||||
REPEAT_EVENT_FILTER_INTERVAL = 1 // (毫秒)重复事件过滤间隔
|
||||
DEFAULT_WATCHER_COUNT = 1 // 默认创建的监控对象数量(使用哈希取模)
|
||||
gDEFAULT_PKG_WATCHER_COUNT = 4 // 默认创建的包监控对象数量(使用哈希取模)
|
||||
)
|
||||
|
||||
var (
|
||||
// 默认的Watcher对象
|
||||
defaultWatcher *Watcher
|
||||
defaultWatcher, _ = New()
|
||||
// 默认的watchers是否初始化,使用时才创建
|
||||
watcherInited = gtype.NewBool()
|
||||
watcherInited = gtype.NewBool()
|
||||
// 回调方法ID与对象指针的映射哈希表,用于根据ID快速查找回调对象
|
||||
callbackIdMap = gmap.NewIntInterfaceMap()
|
||||
callbackIdMap = gmap.NewIntInterfaceMap()
|
||||
// 回调函数的ID生成器(原子操作)
|
||||
callbackIdGenerator = gtype.NewInt()
|
||||
)
|
||||
|
||||
// 初始化创建watcher对象,用于包默认管理监听
|
||||
func initWatcher() {
|
||||
if !watcherInited.Set(true) {
|
||||
pkgWatcherCount := gconv.Int(genv.Get("GF_INOTIFY_COUNT"))
|
||||
if pkgWatcherCount == 0 {
|
||||
pkgWatcherCount = gconv.Int(gcmd.Option.Get("gf.inotify-count"))
|
||||
}
|
||||
if pkgWatcherCount == 0 {
|
||||
pkgWatcherCount = gDEFAULT_PKG_WATCHER_COUNT
|
||||
}
|
||||
if w, err := New(pkgWatcherCount); err == nil {
|
||||
defaultWatcher = w
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建监听管理对象,主要注意的是创建监听对象会占用系统的inotify句柄数量,受到 fs.inotify.max_user_instances 的限制
|
||||
func New(inotifyCount...int) (*Watcher, error) {
|
||||
count := DEFAULT_WATCHER_COUNT
|
||||
if len(inotifyCount) > 0 {
|
||||
count = inotifyCount[0]
|
||||
}
|
||||
func New() (*Watcher, error) {
|
||||
w := &Watcher {
|
||||
cache : gcache.New(),
|
||||
watchers : make([]*fsnotify.Watcher, count),
|
||||
events : gqueue.New(),
|
||||
closeChan : make(chan struct{}),
|
||||
callbacks : gmap.NewStringInterfaceMap(),
|
||||
recursivePaths : gmap.NewStringInterfaceMap(),
|
||||
cache : gcache.New(),
|
||||
events : gqueue.New(),
|
||||
closeChan : make(chan struct{}),
|
||||
callbacks : gmap.NewStringInterfaceMap(),
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
if watcher, err := fsnotify.NewWatcher(); err == nil {
|
||||
w.watchers[i] = watcher
|
||||
} else {
|
||||
// 出错,关闭已创建的底层watcher对象
|
||||
for j := 0; j < i; j++ {
|
||||
w.watchers[j].Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if watcher, err := fsnotify.NewWatcher(); err == nil {
|
||||
w.watcher = watcher
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
w.startWatchLoop()
|
||||
w.startEventLoop()
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// 添加对指定文件/目录的监听,并给定回调函数;如果给定的是一个目录,默认递归监控。
|
||||
// 添加对指定文件/目录的监听,并给定回调函数;如果给定的是一个目录,默认非递归监控。
|
||||
func Add(path string, callbackFunc func(event *Event), recursive...bool) (callback *Callback, err error) {
|
||||
return watcher().Add(path, callbackFunc, recursive...)
|
||||
return defaultWatcher.Add(path, callbackFunc, recursive...)
|
||||
}
|
||||
|
||||
// 递归移除对指定文件/目录的所有监听回调
|
||||
func Remove(path string) error {
|
||||
return watcher().Remove(path)
|
||||
return defaultWatcher.Remove(path)
|
||||
}
|
||||
|
||||
// 根据指定的回调函数ID,移出指定的inotify回调函数
|
||||
@ -147,11 +108,6 @@ func RemoveCallback(callbackId int) error {
|
||||
if callback == nil {
|
||||
return errors.New(fmt.Sprintf(`callback for id %d not found`, callbackId))
|
||||
}
|
||||
return watcher().RemoveCallback(callbackId)
|
||||
}
|
||||
|
||||
// 获得默认的包watcher
|
||||
func watcher() *Watcher {
|
||||
initWatcher()
|
||||
return defaultWatcher
|
||||
defaultWatcher.RemoveCallback(callbackId)
|
||||
return nil
|
||||
}
|
||||
|
@ -49,9 +49,35 @@ func fileIsDir(path string) bool {
|
||||
return s.IsDir()
|
||||
}
|
||||
|
||||
// 返回制定目录其子级所有的目录绝对路径(包含自身)
|
||||
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) {
|
||||
if array := fileAllDirs(path); len(array) > 0 {
|
||||
list = append(list, array...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 打开目录,并返回其下一级文件列表(绝对路径),按照文件名称大小写进行排序,支持目录递归遍历。
|
||||
func fileScanDir(path string, pattern string, recursive ... bool) ([]string, error) {
|
||||
list, err := fileDoScanDir(path, pattern, recursive...)
|
||||
list, err := doFileScanDir(path, pattern, recursive...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -63,16 +89,16 @@ func fileScanDir(path string, pattern string, recursive ... bool) ([]string, err
|
||||
|
||||
// 内部检索目录方法,支持递归,返回没有排序的文件绝对路径列表结果。
|
||||
// pattern参数支持多个文件名称模式匹配,使用','符号分隔多个模式。
|
||||
func fileDoScanDir(path string, pattern string, recursive ... bool) ([]string, error) {
|
||||
var list []string
|
||||
func doFileScanDir(path string, pattern string, recursive ... bool) ([]string, error) {
|
||||
list := ([]string)(nil)
|
||||
// 打开目录
|
||||
dfile, err := os.Open(path)
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dfile.Close()
|
||||
defer file.Close()
|
||||
// 读取目录下的文件列表
|
||||
names, err := dfile.Readdirnames(-1)
|
||||
names, err := file.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -80,7 +106,7 @@ func fileDoScanDir(path string, pattern string, recursive ... bool) ([]string, e
|
||||
for _, name := range names {
|
||||
path := fmt.Sprintf("%s%s%s", path, string(filepath.Separator), name)
|
||||
if fileIsDir(path) && len(recursive) > 0 && recursive[0] {
|
||||
array, _ := fileDoScanDir(path, pattern, true)
|
||||
array, _ := doFileScanDir(path, pattern, true)
|
||||
if len(array) > 0 {
|
||||
list = append(list, array...)
|
||||
}
|
||||
@ -93,4 +119,4 @@ func fileDoScanDir(path string, pattern string, recursive ... bool) ([]string, e
|
||||
}
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
}
|
||||
|
@ -10,74 +10,45 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitee.com/johng/gf/g/container/glist"
|
||||
"gitee.com/johng/gf/g/encoding/ghash"
|
||||
"gitee.com/johng/gf/third/github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// 添加监控,path参数支持文件或者目录路径,recursive为非必需参数,默认为递归添加监控(当path为目录时)。
|
||||
// 添加监控,path参数支持文件或者目录路径,recursive为非必需参数,默认为非递归监控(当path为目录时)。
|
||||
// 如果添加目录,这里只会返回目录的callback,按照callback删除时会递归删除。
|
||||
func (w *Watcher) Add(path string, callbackFunc func(event *Event), recursive...bool) (callback *Callback, err error) {
|
||||
return w.addWithCallbackFunc(nil, path, callbackFunc, recursive...)
|
||||
}
|
||||
|
||||
// 添加监控,path参数支持文件或者目录路径,recursive为非必需参数,默认为递归添加监控(当path为目录时)。
|
||||
// 如果添加目录,这里只会返回目录的callback,按照callback删除时会递归删除。
|
||||
func (w *Watcher) addWithCallbackFunc(parentCallback *Callback, path string, callbackFunc func(event *Event), recursive...bool) (callback *Callback, err error) {
|
||||
// 首先添加这个文件/目录
|
||||
callback, err = w.doAddWithCallbackFunc(path, callbackFunc, parentCallback)
|
||||
callback, err = w.addWithCallbackFunc(path, callbackFunc, recursive...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 其次递归添加其下的文件/目录
|
||||
// 如果需要递归,那么递归添加其下的子级目录,
|
||||
// 注意!!
|
||||
// 1、这里只递归添加**目录**, 而非文件,因为监控了目录即监控了其下一级的文件;
|
||||
// 2、这里只是添加底层监控对象对**子级所有目录**的监控,没有任何回调函数的设置,在事件产生时会回溯查找父级的回调函数;
|
||||
if fileIsDir(path) && (len(recursive) == 0 || recursive[0]) {
|
||||
// 追加递归监控的回调到recursivePaths中
|
||||
w.recursivePaths.LockFunc(func(m map[string]interface{}) {
|
||||
list := (*glist.List)(nil)
|
||||
if v, ok := m[path]; !ok {
|
||||
list = glist.New()
|
||||
m[path] = list
|
||||
} else {
|
||||
list = v.(*glist.List)
|
||||
for _, subPath := range fileAllDirs(path) {
|
||||
if fileIsDir(subPath) {
|
||||
w.watcher.Add(subPath)
|
||||
}
|
||||
list.PushBack(callback)
|
||||
})
|
||||
// 递归添加监控
|
||||
paths, _ := fileScanDir(path, "*", true)
|
||||
for _, v := range paths {
|
||||
w.doAddWithCallbackFunc(v, callbackFunc, callback)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 添加对指定文件/目录的监听,并给定回调函数
|
||||
func (w *Watcher) doAddWithCallbackFunc(path string, callbackFunc func(event *Event), parentCallback *Callback) (callback *Callback, err error) {
|
||||
func (w *Watcher) addWithCallbackFunc(path string, callbackFunc func(event *Event), recursive...bool) (callback *Callback, err error) {
|
||||
// 这里统一转换为当前系统的绝对路径,便于统一监控文件名称
|
||||
if t := fileRealPath(path); t == "" {
|
||||
return nil, errors.New(fmt.Sprintf(`"%s" does not exist`, path))
|
||||
} else {
|
||||
path = t
|
||||
}
|
||||
// 添加成功后会注册该callback id到全局的哈希表,并绑定到父级的注册回调中
|
||||
defer func() {
|
||||
if err == nil {
|
||||
if parentCallback == nil {
|
||||
// 只有主callback才记录到id map中,因为子callback是自动管理的无需添加到全局id映射map中
|
||||
callbackIdMap.Set(callback.Id, callback)
|
||||
}
|
||||
if parentCallback != nil {
|
||||
// 添加到直属父级的subs属性中,建立关联关系,便于后续删除
|
||||
parentCallback.subs.PushBack(callback)
|
||||
}
|
||||
}
|
||||
}()
|
||||
callback = &Callback {
|
||||
Id : callbackIdGenerator.Add(1),
|
||||
Func : callbackFunc,
|
||||
Path : path,
|
||||
addr : fmt.Sprintf("%p", callbackFunc)[2:],
|
||||
subs : glist.New(),
|
||||
parent : parentCallback,
|
||||
}
|
||||
if len(recursive) > 0 {
|
||||
callback.recursive = recursive[0]
|
||||
}
|
||||
// 注册回调函数
|
||||
w.callbacks.LockFunc(func(m map[string]interface{}) {
|
||||
@ -91,93 +62,80 @@ func (w *Watcher) doAddWithCallbackFunc(path string, callbackFunc func(event *Ev
|
||||
callback.elem = list.PushBack(callback)
|
||||
})
|
||||
// 添加底层监听
|
||||
w.watcher(path).Add(path)
|
||||
w.watcher.Add(path)
|
||||
// 添加成功后会注册该callback id到全局的哈希表
|
||||
callbackIdMap.Set(callback.Id, callback)
|
||||
return
|
||||
}
|
||||
|
||||
// 根据path查询对应的底层watcher对象
|
||||
func (w *Watcher) watcher(path string) *fsnotify.Watcher {
|
||||
return w.watchers[ghash.BKDRHash([]byte(path)) % uint32(len(w.watchers))]
|
||||
}
|
||||
|
||||
// 关闭监听管理对象
|
||||
func (w *Watcher) Close() {
|
||||
for _, watcher := range w.watchers {
|
||||
watcher.Close()
|
||||
}
|
||||
w.events.Close()
|
||||
w.watcher.Close()
|
||||
close(w.closeChan)
|
||||
}
|
||||
|
||||
// 递归移除对指定文件/目录的所有监听回调
|
||||
func (w *Watcher) Remove(path string) error {
|
||||
if fileIsDir(path) && fileExists(path) {
|
||||
paths, _ := fileScanDir(path, "*", true)
|
||||
paths = append(paths, path)
|
||||
for _, v := range paths {
|
||||
if err := w.removeWatch(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return w.removeWatch(path)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除对指定文件/目录的所有监听
|
||||
func (w *Watcher) removeWatch(path string) error {
|
||||
// 首先移除所有该path的回调注册
|
||||
if r := w.callbacks.Get(path); r != nil {
|
||||
// 首先移除path注册的回调注册,以及callbackIdMap中的ID
|
||||
if r := w.callbacks.Remove(path); r != nil {
|
||||
list := r.(*glist.List)
|
||||
for {
|
||||
if r := list.PopFront(); r != nil {
|
||||
w.removeCallback(r.(*Callback))
|
||||
callbackIdMap.Remove(r.(*Callback).Id)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 其次移除该path的监听注册
|
||||
w.callbacks.Remove(path)
|
||||
// 其次递归判断所有的子级是否可删除监听
|
||||
if subPaths, err := fileScanDir(path, "*", true); err == nil && len(subPaths) > 0 {
|
||||
for _, subPath := range subPaths {
|
||||
if w.checkPathCanBeRemoved(subPath) {
|
||||
w.watcher.Remove(subPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 最后移除底层的监听
|
||||
return w.watcher(path).Remove(path)
|
||||
return w.watcher.Remove(path)
|
||||
}
|
||||
|
||||
// 判断给定的路径是否可以删除监听(只有所有回调函数都没有了才能删除)
|
||||
func (w *Watcher) checkPathCanBeRemoved(path string) bool {
|
||||
// 首先检索path对应的回调函数
|
||||
if v := w.callbacks.Get(path); v != nil {
|
||||
return false
|
||||
}
|
||||
// 其次查找父级目录有无回调注册
|
||||
dirPath := fileDir(path)
|
||||
if v := w.callbacks.Get(dirPath); v != nil {
|
||||
return false
|
||||
}
|
||||
// 最后回溯查找递归回调函数
|
||||
for {
|
||||
parentDirPath := fileDir(dirPath)
|
||||
if parentDirPath == dirPath {
|
||||
break
|
||||
}
|
||||
if v := w.callbacks.Get(parentDirPath); v != nil {
|
||||
return false
|
||||
}
|
||||
dirPath = parentDirPath
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 根据指定的回调函数ID,移出指定的inotify回调函数
|
||||
func (w *Watcher) RemoveCallback(callbackId int) error {
|
||||
func (w *Watcher) RemoveCallback(callbackId int) {
|
||||
callback := (*Callback)(nil)
|
||||
if r := callbackIdMap.Get(callbackId); r != nil {
|
||||
callback = r.(*Callback)
|
||||
}
|
||||
if callback == nil {
|
||||
return errors.New(fmt.Sprintf(`callback for id %d not found`, callbackId))
|
||||
if callback != nil {
|
||||
if r := w.callbacks.Get(callback.Path); r != nil {
|
||||
r.(*glist.List).Remove(callback.elem)
|
||||
}
|
||||
callbackIdMap.Remove(callbackId)
|
||||
}
|
||||
w.removeCallback(callback)
|
||||
return nil
|
||||
}
|
||||
|
||||
// (递归)移除对指定文件/目录的所有监听
|
||||
func (w *Watcher) removeCallback(callback *Callback) error {
|
||||
if r := w.callbacks.Get(callback.Path); r != nil {
|
||||
list := r.(*glist.List)
|
||||
list.Remove(callback.elem)
|
||||
// 如果存在子级callback,那么也一并递归删除
|
||||
if callback.subs.Len() > 0 {
|
||||
for {
|
||||
if r := callback.subs.PopFront(); r != nil {
|
||||
w.removeCallback(r.(*Callback))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果该文件/目录的所有回调都被删除,那么移除底层的监听
|
||||
if list.Len() == 0 {
|
||||
return w.watcher(callback.Path).Remove(callback.Path)
|
||||
}
|
||||
} else {
|
||||
return errors.New(fmt.Sprintf(`callbacks not found for "%s"`, callback.Path))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -9,67 +9,70 @@ package gfsnotify
|
||||
import (
|
||||
"fmt"
|
||||
"gitee.com/johng/gf/g/container/glist"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 监听循环
|
||||
func (w *Watcher) startWatchLoop() {
|
||||
for i := 0; i < len(w.watchers); i++ {
|
||||
go func(i int) {
|
||||
for {
|
||||
select {
|
||||
// 关闭事件
|
||||
case <- w.closeChan: return
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
// 关闭事件
|
||||
case <- w.closeChan: return
|
||||
|
||||
// 监听事件
|
||||
case ev := <- w.watchers[i].Events:
|
||||
//fmt.Println("ev:", ev.String())
|
||||
w.cache.SetIfNotExist(ev.String(), func() interface{} {
|
||||
w.events.Push(&Event{
|
||||
event : ev,
|
||||
Path : ev.Name,
|
||||
Op : Op(ev.Op),
|
||||
Watcher : w,
|
||||
})
|
||||
return struct {}{}
|
||||
}, REPEAT_EVENT_FILTER_INTERVAL)
|
||||
// 监听事件
|
||||
case ev := <- w.watcher.Events:
|
||||
//fmt.Println("ev:", ev.String())
|
||||
w.cache.SetIfNotExist(ev.String(), func() interface{} {
|
||||
w.events.Push(&Event{
|
||||
event : ev,
|
||||
Path : ev.Name,
|
||||
Op : Op(ev.Op),
|
||||
Watcher : w,
|
||||
})
|
||||
return struct {}{}
|
||||
}, REPEAT_EVENT_FILTER_INTERVAL)
|
||||
|
||||
case err := <- w.watchers[i].Errors:
|
||||
fmt.Errorf("error: %s\n" + err.Error());
|
||||
}
|
||||
case err := <- w.watcher.Errors:
|
||||
fmt.Errorf("error: %s\n" + err.Error());
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 获得真正监听的文件路径及回调函数列表,假如是临时文件或者新增文件,是无法搜索都监听回调的。
|
||||
// 判断规则:
|
||||
// 1、在 callbacks 中应当有回调注册函数(否则监听根本没意义);
|
||||
// 2、如果该path下不存在回调注册函数,则按照path长度从右往左递减,直到减到目录地址为止(不包含);
|
||||
// 3、如果仍旧无法匹配回调函数,那么忽略,否则使用查找到的新path覆盖掉event的path;
|
||||
// 解决问题:
|
||||
// 1、部分IDE修改文件时生成的临时文件,如: /index.html -> /index.html__jdold__;
|
||||
func (w *Watcher) getWatchPathAndCallbacks(path string) (watchPath string, callbacks *glist.List) {
|
||||
if path == "" {
|
||||
return "", nil
|
||||
// 获得文件路径的监听回调,包括层级的监听回调。
|
||||
func (w *Watcher) getCallbacks(path string) (callbacks []*Callback) {
|
||||
// 首先检索path对应的回调函数
|
||||
if v := w.callbacks.Get(path); v != nil {
|
||||
for _, v := range v.(*glist.List).FrontAll() {
|
||||
callback := v.(*Callback)
|
||||
callbacks = append(callbacks, callback)
|
||||
}
|
||||
}
|
||||
// 其次查找父级目录有无回调注册
|
||||
dirPath := fileDir(path)
|
||||
for {
|
||||
if v := w.callbacks.Get(path); v != nil {
|
||||
return path, v.(*glist.List)
|
||||
}
|
||||
path = path[0 : len(path) - 1]
|
||||
// 递减到上一级目录为止
|
||||
if path == dirPath {
|
||||
break
|
||||
}
|
||||
// 如果不能再继续递减,那么退出
|
||||
if len(path) == 0 {
|
||||
break
|
||||
if v := w.callbacks.Get(dirPath); v != nil {
|
||||
for _, v := range v.(*glist.List).FrontAll() {
|
||||
callback := v.(*Callback)
|
||||
callbacks = append(callbacks, callback)
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
// 最后回溯查找递归回调函数
|
||||
for {
|
||||
parentDirPath := fileDir(dirPath)
|
||||
if parentDirPath == dirPath {
|
||||
break
|
||||
}
|
||||
if v := w.callbacks.Get(parentDirPath); v != nil {
|
||||
for _, v := range v.(*glist.List).FrontAll() {
|
||||
callback := v.(*Callback)
|
||||
if callback.recursive {
|
||||
callbacks = append(callbacks, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
dirPath = parentDirPath
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 事件循环
|
||||
@ -78,63 +81,54 @@ func (w *Watcher) startEventLoop() {
|
||||
for {
|
||||
if v := w.events.Pop(); v != nil {
|
||||
event := v.(*Event)
|
||||
// watchPath是注册回调的路径,可能和event.Path不一样
|
||||
watchPath, callbacks := w.getWatchPathAndCallbacks(event.Path)
|
||||
if callbacks == nil {
|
||||
// 如果该路径一个回调也没有,那么没有必要执行后续逻辑,删除对该文件的监听
|
||||
callbacks := w.getCallbacks(event.Path)
|
||||
if len(callbacks) == 0 {
|
||||
w.watcher.Remove(event.Path)
|
||||
continue
|
||||
}
|
||||
fmt.Println("event:", event.String(), watchPath, fileExists(watchPath))
|
||||
switch {
|
||||
// 如果是删除操作,那么需要判断是否文件真正不存在了,如果存在,那么将此事件认为“假删除”
|
||||
case event.IsRemove():
|
||||
if fileExists(watchPath) {
|
||||
// 重新添加监控(底层fsnotify会自动删除掉监控,这里重新添加回去)
|
||||
// 注意这里调用的是底层fsnotify添加监控,只会产生回调事件,并不会使回调函数重复注册
|
||||
w.watcher(watchPath).Add(event.Path)
|
||||
if fileExists(event.Path) {
|
||||
// 底层重新添加监控(不用担心重复添加)
|
||||
w.watcher.Add(event.Path)
|
||||
// 修改事件操作为重命名(相当于重命名为自身名称,最终名称没变)
|
||||
event.Op = RENAME
|
||||
} else {
|
||||
// 删除之前需要执行一遍回调,否则Remove之后就无法执行了
|
||||
// 由于是异步回调,这里保证所有回调都开始执行后再执行删除
|
||||
wg := sync.WaitGroup{}
|
||||
for _, v := range callbacks.FrontAll() {
|
||||
wg.Add(1)
|
||||
go func(callback *Callback) {
|
||||
wg.Done()
|
||||
callback.Func(event)
|
||||
}(v.(*Callback))
|
||||
}
|
||||
wg.Wait()
|
||||
time.Sleep(time.Second)
|
||||
// 如果是真实删除,那么递归删除监控信息
|
||||
fmt.Println("remove", watchPath)
|
||||
w.Remove(watchPath)
|
||||
}
|
||||
|
||||
// 如果是重命名操作,那么需要判断是否文件真正不存在了,如果存在,那么将此事件认为“假命名”
|
||||
// (特别是某些编辑器在编辑文件时会先对文件RENAME再CHMOD)
|
||||
case event.IsRename():
|
||||
if fileExists(watchPath) {
|
||||
// 重新添加监控
|
||||
w.watcher(watchPath).Add(watchPath)
|
||||
} else if watchPath != event.Path && fileExists(event.Path) {
|
||||
for _, v := range callbacks.FrontAll() {
|
||||
callback := v.(*Callback)
|
||||
w.addWithCallbackFunc(callback, event.Path, callback.Func)
|
||||
}
|
||||
if fileExists(event.Path) {
|
||||
// 底层有可能去掉了监控, 这里重新添加监控(不用担心重复添加)
|
||||
w.watcher.Add(event.Path)
|
||||
// 修改事件操作为修改属性
|
||||
event.Op = CHMOD
|
||||
}
|
||||
|
||||
// 创建文件/目录
|
||||
case event.IsCreate():
|
||||
for _, v := range callbacks.FrontAll() {
|
||||
callback := v.(*Callback)
|
||||
w.addWithCallbackFunc(callback, event.Path, callback.Func)
|
||||
// =========================================
|
||||
// 注意这里只是添加底层监听,并没有注册任何的回调函数,
|
||||
// 默认的回调函数为父级的递归回调
|
||||
// =========================================
|
||||
if fileIsDir(event.Path) {
|
||||
// 递归添加
|
||||
for _, subPath := range fileAllDirs(event.Path) {
|
||||
if fileIsDir(subPath) {
|
||||
w.watcher.Add(subPath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 添加文件监听
|
||||
w.watcher.Add(event.Path)
|
||||
}
|
||||
|
||||
}
|
||||
// 执行回调处理,异步处理
|
||||
for _, v := range callbacks.FrontAll() {
|
||||
go v.(*Callback).Func(event)
|
||||
for _, callback := range callbacks {
|
||||
go callback.Func(event)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -15,9 +15,9 @@ import (
|
||||
"gitee.com/johng/gf/g/container/gmap"
|
||||
"gitee.com/johng/gf/g/os/gfile"
|
||||
"gitee.com/johng/gf/g/os/gfsnotify"
|
||||
"gitee.com/johng/gf/g/os/glog"
|
||||
"gitee.com/johng/gf/g/util/gstr"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -33,7 +33,6 @@ type SPathCacheItem struct {
|
||||
isDir bool // 是否目录
|
||||
}
|
||||
|
||||
|
||||
// 创建一个搜索对象
|
||||
func New () *SPath {
|
||||
return &SPath {
|
||||
@ -46,7 +45,7 @@ func New () *SPath {
|
||||
func (sp *SPath) Set(path string) (realPath string, err error) {
|
||||
realPath = gfile.RealPath(path)
|
||||
if realPath == "" {
|
||||
realPath = sp.Search(path)
|
||||
realPath, _ = sp.Search(path)
|
||||
if realPath == "" {
|
||||
realPath = gfile.RealPath(gfile.Pwd() + gfile.Separator + path)
|
||||
}
|
||||
@ -80,7 +79,7 @@ func (sp *SPath) Set(path string) (realPath string, err error) {
|
||||
func (sp *SPath) Add(path string) (realPath string, err error) {
|
||||
realPath = gfile.RealPath(path)
|
||||
if realPath == "" {
|
||||
realPath = sp.Search(path)
|
||||
realPath, _ = sp.Search(path)
|
||||
if realPath == "" {
|
||||
realPath = gfile.RealPath(gfile.Pwd() + gfile.Separator + path)
|
||||
}
|
||||
@ -107,22 +106,32 @@ func (sp *SPath) Add(path string) (realPath string, err error) {
|
||||
}
|
||||
|
||||
// 给定的name只是相对文件路径,找不到该文件时,返回空字符串;
|
||||
// 当给定indexFiles时,如果name时一个目录,那么会进一步检索其下对应的indexFiles文件是否存在,存在则返回indexFile 绝对路径;
|
||||
// 当给定indexFiles时,如果name时一个目录,那么会进一步检索其下对应的indexFiles文件是否存在,存在则返回indexFile绝对路径;
|
||||
// 否则返回name目录绝对路径。
|
||||
func (sp *SPath) Search(name string, indexFiles...string) string {
|
||||
func (sp *SPath) Search(name string, indexFiles...string) (path string, isDir bool) {
|
||||
name = sp.formatCacheName(name)
|
||||
if v := sp.cache.Get(name); v != nil {
|
||||
item := v.(*SPathCacheItem)
|
||||
if len(indexFiles) > 0 && item.isDir {
|
||||
for _, file := range indexFiles {
|
||||
if v := sp.cache.Get(name + "/" + file); v != nil {
|
||||
return v.(*SPathCacheItem).path
|
||||
item := v.(*SPathCacheItem)
|
||||
return item.path, item.isDir
|
||||
}
|
||||
}
|
||||
}
|
||||
return item.path
|
||||
return item.path, item.isDir
|
||||
}
|
||||
return ""
|
||||
return "", false
|
||||
}
|
||||
|
||||
// 返回当前对象缓存的所有路径列表
|
||||
func (sp *SPath) AllPaths() []string {
|
||||
paths := sp.cache.Keys()
|
||||
if len(paths) > 0 {
|
||||
sort.Strings(paths)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// 当前的搜索路径数量
|
||||
@ -140,6 +149,7 @@ func (sp *SPath) formatCacheName(name string) string {
|
||||
if name == "" {
|
||||
return "/"
|
||||
}
|
||||
name = strings.TrimLeft(name, ".")
|
||||
if runtime.GOOS != "linux" {
|
||||
name = gstr.Replace(name, "\\", "/")
|
||||
}
|
||||
@ -180,7 +190,7 @@ func (sp *SPath) addToCache(filePath, dirPath string) {
|
||||
// 这里需要注意的点是,由于添加监听是递归添加的,那么假如删除一个目录,那么该目录下的文件(包括目录)也会产生一条删除事件,总共会产生N条事件。
|
||||
func (sp *SPath) addMonitorByPath(path string) {
|
||||
gfsnotify.Add(path, func(event *gfsnotify.Event) {
|
||||
glog.Debug(event.String())
|
||||
//glog.Debug(event.String())
|
||||
switch {
|
||||
case event.IsRemove():
|
||||
sp.cache.Remove(sp.nameFromPath(event.Path, path))
|
||||
@ -193,7 +203,7 @@ func (sp *SPath) addMonitorByPath(path string) {
|
||||
case event.IsCreate():
|
||||
sp.addToCache(event.Path, path)
|
||||
}
|
||||
})
|
||||
}, true)
|
||||
}
|
||||
|
||||
// 删除监听(递归)
|
||||
|
@ -141,7 +141,7 @@ func (view *View) Assign(key string, value interface{}) {
|
||||
|
||||
// 解析模板,返回解析后的内容
|
||||
func (view *View) Parse(file string, params Params, funcmap...map[string]interface{}) ([]byte, error) {
|
||||
path := view.paths.Search(file)
|
||||
path, _ := view.paths.Search(file)
|
||||
if path == "" {
|
||||
return nil, errors.New("tpl \"" + file + "\" not found")
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ func main() {
|
||||
s.BindHandler("/", func(r *ghttp.Request){
|
||||
r.Response.Writeln("您可以同时通过HTTP和HTTPS方式看到该内容!")
|
||||
})
|
||||
s.EnableHTTPS("/home/john/temp/server.crt", "/home/john/temp/server.key")
|
||||
s.EnableHTTPS("./server.crt", "./server.key")
|
||||
s.SetHTTPSPort(8198, 8199)
|
||||
s.SetPort(8200, 8300)
|
||||
s.EnableAdmin()
|
||||
|
@ -6,7 +6,7 @@ import "gitee.com/johng/gf/g"
|
||||
func main() {
|
||||
s := g.Server()
|
||||
s.SetIndexFolder(true)
|
||||
s.SetServerRoot("/Users/john/Documents")
|
||||
s.SetServerRoot("/Users/john/Temp")
|
||||
s.SetPort(8199)
|
||||
s.Run()
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitee.com/johng/gf/g/os/gcfg"
|
||||
"gitee.com/johng/gf/g"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := gcfg.New("/home/john/Workspace/Go/GOPATH/src/gitee.com/johng/gf/geg/os/gcfg")
|
||||
c := g.Config()
|
||||
redisConfig := c.GetArray("redis-cache", "redis.toml")
|
||||
memConfig := c.GetArray("", "memcache.yml")
|
||||
fmt.Println(redisConfig)
|
||||
|
@ -2,11 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitee.com/johng/gf/g/os/gcfg"
|
||||
"gitee.com/johng/gf/g"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := gcfg.New("/home/john/Workspace/Go/GOPATH/src/gitee.com/johng/gf/geg/os/gcfg")
|
||||
c := g.Config()
|
||||
fmt.Println(c.GetArray("memcache"))
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ func main() {
|
||||
defer watch.Close()
|
||||
//添加要监控的对象,文件或文件夹
|
||||
//err = watch.Add("D:\\Workspace\\Go\\GOPATH\\src\\gitee.com\\johng\\gf\\geg\\other\\test.go")
|
||||
err = watch.Add("/Users/john/Temp/1/2")
|
||||
err = watch.Add("/Users/john/Workspace/Go/GOPATH/src/gitee.com/johng/gf/geg/other/test.go")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ import (
|
||||
|
||||
func main() {
|
||||
//path := "D:\\Workspace\\Go\\GOPATH\\src\\gitee.com\\johng\\gf\\geg\\other\\test.go"
|
||||
path := "/Users/john/Temp/1/2/3"
|
||||
path := "/Users/john/Temp/"
|
||||
_, err := gfsnotify.Add(path, func(event *gfsnotify.Event) {
|
||||
glog.Println(event)
|
||||
})
|
||||
}, true)
|
||||
|
||||
// 移除对该path的监听
|
||||
//gfsnotify.Remove(path)
|
||||
|
@ -2,8 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitee.com/johng/gf/g"
|
||||
"gitee.com/johng/gf/g/os/gspath"
|
||||
"gitee.com/johng/gf/g/os/gtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -12,8 +14,13 @@ func main() {
|
||||
rp, err := sp.Add(path)
|
||||
fmt.Println(err)
|
||||
fmt.Println(rp)
|
||||
fmt.Println(gtime.FuncCost(func() {
|
||||
sp.Search("1")
|
||||
}))
|
||||
fmt.Println(sp.Search("1", "index.html"))
|
||||
|
||||
gtime.SetInterval(5*time.Second, func() bool {
|
||||
g.Dump(sp.AllPaths())
|
||||
return true
|
||||
})
|
||||
|
||||
select {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"fmt"
|
||||
"gitee.com/johng/gf/g/os/gfile"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("123"[2:])
|
||||
fmt.Println(gfile.TempDir())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user