├── utils ├── exit.go ├── banner.go ├── exec.go ├── file.go ├── colorprint.go └── log.go ├── .version.toml ├── .gitignore ├── .goreleaser.yml ├── internal ├── parse │ └── template.go ├── structs │ └── template.go └── core │ ├── load_template.go │ ├── send_notification.go │ └── watch_and_diff.go ├── README.md ├── webhooks ├── bark.yml ├── feishu.yml └── dingding.yml ├── cmd └── FileNotifier │ ├── main.go │ └── run.go ├── .github └── workflows │ └── releases.yml ├── go.mod └── go.sum /utils/exit.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | cli "github.com/jawher/mow.cli" 5 | ) 6 | 7 | // 输出错误并退出 8 | func CliError(message string, exitCode int) { 9 | Exit(message) 10 | cli.Exit(exitCode) 11 | } 12 | -------------------------------------------------------------------------------- /.version.toml: -------------------------------------------------------------------------------- 1 | [main] 2 | extraCommands = ["git push", "git push --tags"] 3 | serialize = "{version}-{banner}" 4 | tag = true 5 | version = "1.4.0" 6 | 7 | [[operate]] 8 | location = "cmd/FileNotifier/main.go" 9 | replace = "__version__ = \"{}\"" 10 | search = "__version__ = \"{}\"" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # test 18 | tests/ 19 | feishu-my.yml -------------------------------------------------------------------------------- /utils/banner.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func Banner() { 4 | println(` 5 | ______ _ _ _ _ _ _ __ _ 6 | | ___(_) | | \ | | | | (_)/ _(_) 7 | | |_ _| | ___| \| | ___ | |_ _| |_ _ ___ _ __ 8 | | _| | | |/ _ \ . |/ _ \| __| | _| |/ _ \ '__| 9 | | | | | | __/ |\ | (_) | |_| | | | | __/ | 10 | \_| |_|_|\___\_| \_/\___/ \__|_|_| |_|\___|_| 11 | `) 12 | } 13 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - binary: FileNotifier 3 | main: ./cmd/FileNotifier/ 4 | goos: 5 | - linux 6 | - windows 7 | - darwin 8 | goarch: 9 | - amd64 10 | - 386 11 | - arm 12 | - arm64 13 | 14 | archives: 15 | - id: tgz 16 | format: tar.gz 17 | replacements: 18 | darwin: macOS 19 | format_overrides: 20 | - goos: windows 21 | format: zip -------------------------------------------------------------------------------- /internal/parse/template.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/WAY29/FileNotifier/internal/structs" 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | func ParseTemplate(filename string) (*structs.Template, error) { 11 | template := &structs.Template{} 12 | 13 | f, err := os.Open(filename) 14 | if err != nil { 15 | return nil, err 16 | } 17 | defer f.Close() 18 | 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | err = yaml.NewDecoder(f).Decode(template) 24 | 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return template, nil 30 | } 31 | -------------------------------------------------------------------------------- /utils/exec.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os/exec" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | IsWindows = false 10 | ShellPath string 11 | ShellRunArg string 12 | ) 13 | 14 | func init() { 15 | if runtime.GOOS == "windows" { 16 | IsWindows = true 17 | ShellPath, _ = exec.LookPath("cmd.exe") 18 | ShellRunArg = "/c" 19 | } else { 20 | ShellPath, _ = exec.LookPath("sh") 21 | ShellRunArg = "-c" 22 | } 23 | } 24 | 25 | func ExecCommand(command string) (*exec.Cmd, error) { 26 | args := []string{ShellRunArg, command} 27 | 28 | return exec.Command(ShellPath, args...), nil 29 | 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FileNotifier 2 | 一款用于监测文件(夹)变化并发送webhook通知工具 / A tool for monitoring file (folder) changes and sending webhook notifications 3 | 4 | ## Install 5 | ```bash 6 | git clone http://github.com/WAY29/FileNotifier 7 | go build -ldflags "-w -s" ./cmd/FileNotifier # 或从github releases中下载 / or download from github releases 8 | vi webhooks/feishu.yml # 修改webhook templates / edit webhook templates 9 | ``` 10 | 11 | ## Usage / Quickstart 12 | ```bash 13 | FileNotifier -t ./webhooks/feishu.yml -f /tmp/something -d /tmp/somedir -e write,rename,remove 14 | ``` 15 | 16 | ## Proclamations 17 | 工具仅用于安全研究,由于使用该工具造成的任何后果使用者负责 -------------------------------------------------------------------------------- /internal/structs/template.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | type Template struct { 4 | Name string `yaml:"name"` 5 | Url string `yaml:"url"` 6 | Method string `yaml:"method"` 7 | Headers map[string]string `yaml:"headers"` 8 | Body string `yaml:"body"` 9 | TextCommandChain []string `yaml:"text_command_chain"` 10 | FilenameCommandChain []string `yaml:"filename_command_chain"` 11 | UrlEncodeText bool `yaml:"urlencode_text"` 12 | EscapeJson bool `yaml:"escape_json"` 13 | } 14 | -------------------------------------------------------------------------------- /webhooks/bark.yml: -------------------------------------------------------------------------------- 1 | name: bark 2 | url: "https://api.day.app/xxxxxxxxxxxx/%E6%96%87%E4%BB%B6%E7%9B%91%E6%8E%A7%E9%80%9A%E7%9F%A5/%E6%96%87%E4%BB%B6%E5%90%8D:%20{{filename}}%0d%0a%E4%BF%AE%E6%94%B9%E5%86%85%E5%AE%B9:%20{{text}}" # 由于bark的发送信息比较特殊,所以记得把所有的内容进行url编码 3 | method: GET 4 | # text_command_chain: # 是一个数组,在urlencode_text与escape_json之前按顺序执行命令,text的值将会被每一条命令的输出覆盖,用于调用外部命令修改/过滤text,text值为空时则停止发送通知。注意这里的{{text}}与{{filename}}会转义\r\n,并且在所有命令执行完之后存在反转义\r\n的行为 5 | # - echo {{text}} 6 | # filename_command_chain: # filename是文件的绝对路径。与text_commond_chain类似,但是对filename进行处理。 7 | # - echo {{filename}} 8 | urlencode_text: true # 将text url编码 9 | escape_json: true # 将text json编码,但除去包裹的双引号 -------------------------------------------------------------------------------- /cmd/FileNotifier/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/WAY29/FileNotifier/utils" 7 | cli "github.com/jawher/mow.cli" 8 | ) 9 | 10 | const ( 11 | __version__ = "1.4.0" 12 | ) 13 | 14 | var ( 15 | app *cli.Cli 16 | ) 17 | 18 | func main() { 19 | // 输出banner 20 | utils.Banner() 21 | // 解析参数 22 | app = cli.App("FileNotifier", "一款用于监测文件(夹)变化并发送webhook通知工具 / A tool for monitoring file (folder) changes and sending webhook notifications") 23 | app.Command("run", "Run FileNotifier to watch file(s) and callback webhook", cmdRun) 24 | 25 | app.Version("V version", "FileNotifier "+__version__) 26 | app.Spec = "[-V]" 27 | 28 | app.Run(os.Args) 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/releases.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | create: 4 | tags: 5 | - "*" 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - 12 | name: "Check out code" 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - 17 | name: "Set up Go" 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.17 21 | - 22 | env: 23 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 24 | name: "Create release on GitHub" 25 | uses: goreleaser/goreleaser-action@v2 26 | with: 27 | args: "release --rm-dist" 28 | version: latest -------------------------------------------------------------------------------- /webhooks/feishu.yml: -------------------------------------------------------------------------------- 1 | name: feishu 2 | url: https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxxxxxxxx 3 | method: POST 4 | headers: 5 | Content-Type: application/json 6 | body: | 7 | {"msg_type":"post","content":{"post":{"zh_cn":{"title":"文件监控通知","content":[[{"tag":"text","text":"文件名: {{filename}}"}],[{"tag":"text","text":"修改内容: {{text}}"}]]}}}} 8 | # text_command_chain: # 是一个数组,在urlencode_text与escape_json之前按顺序执行命令,text的值将会被每一条命令的输出覆盖,用于调用外部命令修改/过滤text,text值为空时则停止发送通知。注意这里的{{text}}与{{filename}}会转义\r\n,并且在所有命令执行完之后存在反转义\r\n的行为 9 | # - echo {{text}} 10 | # filename_command_chain: # filename是文件的绝对路径。与text_commond_chain类似,但是对filename进行处理。 11 | # - echo {{filename}} 12 | urlencode_text: false # 将text url编码 13 | escape_json: true # 将text json编码,但除去包裹的双引号 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/WAY29/FileNotifier 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/WAY29/errors v1.6.0 7 | github.com/fsnotify/fsnotify v1.4.9 8 | github.com/go-resty/resty/v2 v2.7.0 9 | github.com/jawher/mow.cli v1.2.0 10 | github.com/logrusorgru/aurora v2.0.3+incompatible 11 | github.com/mattn/go-colorable v0.1.12 // indirect 12 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 13 | github.com/onsi/ginkgo v1.16.5 // indirect 14 | github.com/onsi/gomega v1.18.1 // indirect 15 | github.com/sergi/go-diff v1.2.0 16 | github.com/sirupsen/logrus v1.8.1 17 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 18 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect 19 | gopkg.in/yaml.v2 v2.4.0 20 | ) 21 | -------------------------------------------------------------------------------- /webhooks/dingding.yml: -------------------------------------------------------------------------------- 1 | name: dingding 2 | url: https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxxxxxxxx 3 | method: POST 4 | headers: 5 | Content-Type: application/json 6 | body: | 7 | {"msgtype":"markdown","markdown":{"title":"文件监控通知","text":"#### 文件监控通知\n 文件名: {{filename}}\n\n 修改内容: {{text}}"},"at":{"atMobiles":[],"atUserIds":[],"isAtAll":false}} 8 | # text_command_chain: # 是一个数组,在urlencode_text与escape_json之前按顺序执行命令,text的值将会被每一条命令的输出覆盖,用于调用外部命令修改/过滤text,text值为空时则停止发送通知。注意这里的{{text}}与{{filename}}会转义\r\n,并且在所有命令执行完之后存在反转义\r\n的行为 9 | # - echo {{text}} 10 | # filename_command_chain: # filename是文件的绝对路径。与text_commond_chain类似,但是对filename进行处理。 11 | # - echo {{filename}} 12 | urlencode_text: false # 将text url编码 13 | escape_json: true # 将text json编码,但除去包裹的双引号 -------------------------------------------------------------------------------- /internal/core/load_template.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/WAY29/FileNotifier/internal/parse" 5 | "github.com/WAY29/FileNotifier/internal/structs" 6 | "github.com/WAY29/FileNotifier/utils" 7 | "github.com/WAY29/errors" 8 | ) 9 | 10 | var ( 11 | Templates []*structs.Template 12 | ) 13 | 14 | func LoadTemplates(templatePaths []string) { 15 | Templates = make([]*structs.Template, 0, len(templatePaths)) 16 | 17 | var ( 18 | template *structs.Template 19 | err error 20 | ) 21 | 22 | for _, path := range templatePaths { 23 | template, err = parse.ParseTemplate(path) 24 | if err != nil { 25 | nErr := errors.Wrapf(err, "Can't Parse template '%s'", path) 26 | utils.ErrorP(nErr) 27 | } 28 | Templates = append(Templates, template) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // 判断所给路径文件/文件夹是否存在 10 | func Exists(path string) bool { 11 | _, err := os.Stat(path) //os.Stat获取文件信息 12 | if err != nil { 13 | if os.IsExist(err) { 14 | return true 15 | } 16 | return false 17 | } 18 | return true 19 | } 20 | 21 | // 判断所给路径是否为文件夹 22 | func IsDir(path string) bool { 23 | s, err := os.Stat(path) 24 | if err != nil { 25 | return false 26 | } 27 | return s.IsDir() 28 | } 29 | 30 | // 判断所给路径是否为文件 31 | func IsFile(path string) bool { 32 | return !IsDir(path) 33 | } 34 | 35 | // 读取文件并返回一个字符串 36 | func ReadFileAsString(path string) (string, error) { 37 | contents, err := ioutil.ReadFile(path) 38 | return string(contents), err 39 | } 40 | 41 | func AbsFilePath(path string) (string, error) { 42 | absPath, err := filepath.Abs(path) 43 | if err != nil { 44 | return "", err 45 | } 46 | return absPath, err 47 | } 48 | -------------------------------------------------------------------------------- /utils/colorprint.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "github.com/logrusorgru/aurora" 7 | ) 8 | 9 | func Success(message string) { 10 | fmt.Println(Cyan("[+]"), message) 11 | } 12 | func SuccessF(message string, args ...interface{}) { 13 | fmt.Println(Cyan("[+]"), fmt.Sprintf(message, args...)) 14 | } 15 | 16 | func Message(message string) { 17 | fmt.Println(Gray(8, "[#]"), message) 18 | } 19 | func MessageF(message string, args ...interface{}) { 20 | fmt.Println(Gray(8, "[#]"), fmt.Sprintf(message, args...)) 21 | } 22 | 23 | func Failure(message string) { 24 | fmt.Println(Red("[-]"), message) 25 | } 26 | func FailureF(message string, args ...interface{}) { 27 | fmt.Println(Red("[-]"), fmt.Sprintf(message, args...)) 28 | } 29 | 30 | func Exit(message string) { 31 | fmt.Println(Red("[-]"), message) 32 | } 33 | func ExitF(message string, args ...interface{}) { 34 | fmt.Println(Red("[-]"), fmt.Sprintf(message, args...)) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/FileNotifier/run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/WAY29/FileNotifier/internal/core" 9 | "github.com/WAY29/FileNotifier/utils" 10 | cli "github.com/jawher/mow.cli" 11 | ) 12 | 13 | func cmdRun(cmd *cli.Cmd) { 14 | 15 | // 定义选项 16 | var ( 17 | template = cmd.StringsOpt("t template", make([]string, 0), "Webhook template(s)") 18 | file = cmd.StringsOpt("f file", make([]string, 0), "The file(s) will be watch") 19 | dir = cmd.StringsOpt("d dir", make([]string, 0), "The directory(s) will be watch") 20 | excludes = cmd.StringsOpt("x exclude", make([]string, 0), "The directory(s) or file(s) will be exclude in watch") 21 | event = cmd.StringsOpt("e event", make([]string, 0), "File event you want to watch, must be write/rename/remove") 22 | debug = cmd.BoolOpt("debug", false, "Debug this program") 23 | verbose = cmd.BoolOpt("v verbose", false, "Print verbose messages") 24 | ) 25 | 26 | cmd.Spec = "(-t=