├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── docs └── images │ └── fsweeper.png ├── examples ├── .gitignore ├── test.yaml └── vars.yaml ├── go.mod ├── go.sum ├── internal └── ospkg │ ├── linux.go │ └── windows.go ├── main.go └── rules ├── actions.go ├── configuration.go ├── filters.go ├── functions.go ├── utils.go └── vars.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | go-version: [1.16.x] 12 | 13 | steps: 14 | - name: Set up Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | 19 | - name: Checkout code 20 | uses: actions/checkout@v2 21 | 22 | - name: Build 23 | run: go build . -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 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.16.x 21 | 22 | - name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v2 24 | with: 25 | version: latest 26 | workdir: . 27 | args: release --rm-dist 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | fsweeper 4 | conf.yaml 5 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: fsweeper 2 | builds: 3 | - env: [CGO_ENABLED=0] 4 | goos: 5 | - linux 6 | - windows 7 | - darwin 8 | goarch: 9 | - amd64 10 | - arm64 11 | archives: 12 | - replacements: 13 | darwin: macos 14 | amd64: x86_64 15 | format_overrides: 16 | - goos: windows 17 | format: zip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2020 reugn 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FSweeper 2 | 3 | 4 | [![Build](https://github.com/reugn/fsweeper/actions/workflows/build.yml/badge.svg)](https://github.com/reugn/fsweeper/actions/workflows/build.yml) 5 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/reugn/fsweeper)](https://pkg.go.dev/github.com/reugn/fsweeper) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/reugn/fsweeper)](https://goreportcard.com/report/github.com/reugn/fsweeper) 7 | 8 | An intuitive and simple file management automation tool. 9 | Read this guide and write rules for organizing file storage in just a couple of minutes. 10 | 11 | ## Installation 12 | Pick a binary from the [releases](https://github.com/reugn/fsweeper/releases). 13 | 14 | ### Build from source 15 | Download and install Go https://golang.org/doc/install. 16 | 17 | Get the package: 18 | ```sh 19 | go get github.com/reugn/fsweeper 20 | ``` 21 | 22 | Read this [guide](https://golang.org/doc/tutorial/compile-install) on how to compile and install the application. 23 | 24 | ## Usage 25 | Command line options list: 26 | ``` 27 | ./fsweeper --help 28 | 29 | Usage of ./fsweeper: 30 | -actions 31 | Show supported actions 32 | -conf string 33 | Configuration file path (default "conf.yaml") 34 | -configure 35 | Open default configuration file in $EDITOR 36 | -filters 37 | Show supported filters 38 | -version 39 | Show version 40 | ``` 41 | 42 | ## Configuration 43 | `fsweeper` uses YAML as a configuration file format. 44 | A default configuration file name could be set using the `FSWEEPER_CONFIG_FILE` environment variable. It will fallback to `conf.yaml` otherwise. 45 | Use `--conf=custom.yaml` parameter to run against a custom configuration file. 46 | 47 | Let's start with a simple example: 48 | ```yaml 49 | rules: 50 | - path: ./examples/files 51 | recursive: true 52 | op: OR 53 | actions: 54 | - action: echo 55 | payload: "Found size" 56 | filters: 57 | - filter: size 58 | payload: gt 10000 59 | - filter: size 60 | payload: eq 0 61 | ``` 62 | This configuration will look for files that are bigger than 10000 bytes or empty and print out "Found size" message on each. 63 | 64 | What if we want to move all JSON files to an archive folder: 65 | ```yaml 66 | rules: 67 | - path: ./examples/files 68 | recursive: true 69 | actions: 70 | - action: move 71 | payload: ./examples/archive 72 | - action: echo 73 | payload: "Found JSON file" 74 | filters: 75 | - filter: ext 76 | payload: .json 77 | ``` 78 | 79 | ### Variables and pipelines 80 | We can build dynamic configurations using variables and pipelines. 81 | Variables introduce a set of dynamic placeholders to be substituted in runtime. 82 | Pipelines chain together a series of template commands to compactly express a series of transformations. 83 | 84 | Let's see an example: 85 | ```yaml 86 | vars: 87 | dateTimeFormat: "2006.01.02[15-04-05]" 88 | rules: 89 | - path: ./examples/files 90 | recursive: true 91 | op: AND 92 | actions: 93 | - action: echo 94 | payload: "Found {{ .FileName | upper | quote }} at {{ .DateTime }}, size {{ .FileSize }}" 95 | filters: 96 | - filter: name 97 | payload: "[0-9]+" 98 | - filter: contains 99 | payload: "1234" 100 | ``` 101 | 102 | | Variables | Description | 103 | | ----------- | --- | 104 | | `.FileSize` | Returns the context file size | 105 | | `.FileName` | Returns the context file name | 106 | | `.FilePath` | Returns the context file full path | 107 | | `.FileExt` | Returns the context file extension | 108 | | `.Time` | Current Time (default format: "15-04-05")[1](#format) | 109 | | `.Date` | Current Date (default format: "2006-01-02")[1](#format) | 110 | | `.DateTime` | Current DateTime (default format: "2006-01-02[15-04-05]")[1](#format) | 111 | | `.Ts` | Current Unix epoch time | 112 | 113 | 1You can override `timeFormat`, `dateFormat`, and `dateTimeFormat` in the configuration file. 114 | 115 | | Pipeline functions | Description | 116 | | -------- | --- | 117 | | `trim` | Removes leading and trailing white spaces | 118 | | `upper` | Returns a string with all Unicode letters mapped to their upper case | 119 | | `lower` | Returns a string with all Unicode letters mapped to their lower case | 120 | | `quote` | Wraps a string with double quotes | 121 | | `head n` | Takes the first "n" characters of the input string | 122 | | `len` | Returns a string representation of the input length | 123 | 124 | ## License 125 | MIT 126 | -------------------------------------------------------------------------------- /docs/images/fsweeper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reugn/fsweeper/78f59e91210524441ec55ab76e80f65e3faeb7ae/docs/images/fsweeper.png -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | files/ 2 | -------------------------------------------------------------------------------- /examples/test.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | - path: ./examples/files 3 | recursive: true 4 | op: AND 5 | actions: 6 | - action: touch 7 | - action: echo 8 | payload: "Found name && contains" 9 | filters: 10 | - filter: name 11 | payload: "[0-9]+" 12 | - filter: contains 13 | payload: "1234" 14 | 15 | - path: ./examples/files 16 | recursive: true 17 | actions: 18 | - action: rename 19 | payload: foo.json 20 | - action: echo 21 | payload: "Found ext" 22 | filters: 23 | - filter: ext 24 | payload: .json 25 | 26 | - path: ./examples/files 27 | recursive: true 28 | op: OR 29 | actions: 30 | - action: echo 31 | payload: "Found size" 32 | filters: 33 | - filter: size 34 | payload: gt 10000 35 | - filter: size 36 | payload: eq 0 -------------------------------------------------------------------------------- /examples/vars.yaml: -------------------------------------------------------------------------------- 1 | vars: 2 | dateTimeFormat: "2006.01.02[15-04-05]" 3 | rules: 4 | - path: ./examples/files 5 | recursive: true 6 | op: AND 7 | actions: 8 | - action: echo 9 | payload: "Found {{ .FileName | upper | quote }} at {{ .DateTime }}, size {{ .FileSize }}" 10 | filters: 11 | - filter: name 12 | payload: "[0-9]+" 13 | - filter: contains 14 | payload: "1234" -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/reugn/fsweeper 2 | 3 | go 1.16 4 | 5 | require gopkg.in/yaml.v2 v2.4.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 2 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 3 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 4 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 5 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 6 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 7 | -------------------------------------------------------------------------------- /internal/ospkg/linux.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package ospkg 4 | 5 | // PathSeparator for a Unix-like os 6 | const PathSeparator = "/" 7 | -------------------------------------------------------------------------------- /internal/ospkg/windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package ospkg 4 | 5 | // PathSeparator for Windows 6 | const PathSeparator = "\\" 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "time" 10 | 11 | "github.com/reugn/fsweeper/rules" 12 | ) 13 | 14 | const version = "0.2.0" 15 | 16 | var ( 17 | configFile = flag.String("conf", rules.GetDefaultConfigFile(), "Configuration file path") 18 | 19 | configureParam = flag.Bool("configure", false, "Open default configuration file in $EDITOR") 20 | versionParam = flag.Bool("version", false, "Show version") 21 | filtersParam = flag.Bool("filters", false, "Show supported filters") 22 | actionsParam = flag.Bool("actions", false, "Show supported actions") 23 | ) 24 | 25 | func main() { 26 | flag.Parse() 27 | 28 | if *configureParam { 29 | err := openFileInEditor(rules.GetDefaultConfigFile()) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | return 34 | } 35 | 36 | if rt := handleInfoFlags(); rt { 37 | return 38 | } 39 | 40 | // read a configuration file 41 | config := rules.ReadConfigFromFile(*configFile) 42 | 43 | // execute rules 44 | log.Println("Starting execute rules...") 45 | start := time.Now() 46 | config.Execute() 47 | 48 | log.Printf("Done in %v.\n", time.Since(start)) 49 | } 50 | 51 | func handleInfoFlags() bool { 52 | var rt bool 53 | 54 | if *versionParam { 55 | fmt.Println("Version: " + version) 56 | rt = true 57 | } 58 | 59 | if *filtersParam { 60 | fmt.Println("Filters:") 61 | for _, filter := range rules.Filters { 62 | fmt.Printf("\t- %s\n", filter) 63 | } 64 | rt = true 65 | } 66 | 67 | if *actionsParam { 68 | fmt.Println("Actions:") 69 | for _, action := range rules.Actions { 70 | fmt.Printf("\t- %s\n", action) 71 | } 72 | rt = true 73 | } 74 | 75 | return rt 76 | } 77 | 78 | func openFileInEditor(filename string) error { 79 | editor := os.Getenv("EDITOR") 80 | if editor == "" { 81 | editor = "vi" 82 | } 83 | 84 | // get the full executable path for the editor 85 | executable, err := exec.LookPath(editor) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | cmd := exec.Command(executable, filename) 91 | cmd.Stdin = os.Stdin 92 | cmd.Stdout = os.Stdout 93 | cmd.Stderr = os.Stderr 94 | 95 | return cmd.Run() 96 | } 97 | -------------------------------------------------------------------------------- /rules/actions.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path" 7 | "time" 8 | ) 9 | 10 | // Action represents an action to perform on a file. 11 | type Action struct { 12 | Action string `yaml:"action"` 13 | Payload string `yaml:"payload"` 14 | } 15 | 16 | func (action *Action) echoAction(filePath string, vars *Vars) { 17 | msg := vars.Process(action.Payload, filePath) 18 | log.Printf("[%s] %s\n", filePath, msg) 19 | } 20 | 21 | func (action *Action) touchAction(filePath string) { 22 | now := time.Now().Local() 23 | err := os.Chtimes(filePath, now, now) 24 | if err != nil { 25 | log.Fatalf("Failed to touch file: %s", filePath) 26 | } 27 | } 28 | 29 | func (action *Action) moveAction(filePath string, vars *Vars) { 30 | newPath := vars.Process(action.Payload, filePath) 31 | err := os.Rename(filePath, newPath) 32 | if err != nil { 33 | log.Fatalf("Failed to move file: %s to: %s", filePath, newPath) 34 | } 35 | } 36 | 37 | func (action *Action) renameAction(filePath string, vars *Vars) { 38 | newFileName := vars.Process(action.Payload, filePath) 39 | err := os.Rename(filePath, validatePath(path.Dir(filePath))+newFileName) 40 | if err != nil { 41 | log.Fatalf("Failed to rename file: %s to: %s", filePath, newFileName) 42 | } 43 | } 44 | 45 | func (action *Action) deleteAction(filePath string) { 46 | err := os.Remove(filePath) 47 | if err != nil { 48 | log.Fatalf("Failed to remove file: %s", filePath) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rules/configuration.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/reugn/fsweeper/internal/ospkg" 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | const ( 14 | operatorAnd = "AND" 15 | operatorOr = "OR" 16 | ) 17 | 18 | const ( 19 | filterName = "name" 20 | filterExtension = "ext" 21 | filterSize = "size" 22 | filterLastEdited = "lastEdited" 23 | filterContains = "contains" 24 | ) 25 | 26 | const ( 27 | actionEcho = "echo" 28 | actionTouch = "touch" 29 | actionMove = "move" 30 | actionRename = "rename" 31 | actionDelete = "delete" 32 | ) 33 | 34 | var ( 35 | // Filters represents the supported filters list. 36 | Filters = [...]string{filterName, filterExtension, filterSize, filterLastEdited, filterContains} 37 | 38 | // Actions represents the supported actions list. 39 | Actions = [...]string{actionEcho, actionTouch, actionMove, actionRename, actionDelete} 40 | ) 41 | 42 | // Rule represents a configuration to apply on a file. 43 | type Rule struct { 44 | Path string `yaml:"path"` 45 | Recursive bool `yaml:"recursive"` 46 | Operator string `yaml:"op" default:"AND"` 47 | Actions []Action `yaml:"actions"` 48 | Filters []Filter `yaml:"filters"` 49 | } 50 | 51 | func (r *Rule) checkFilters(filePath string) bool { 52 | res := make([]bool, len(r.Filters)) 53 | var i int 54 | for _, filter := range r.Filters { 55 | if r.Operator == operatorOr { 56 | for _, b := range res { 57 | if b { 58 | return true 59 | } 60 | } 61 | } 62 | 63 | switch filter.Filter { 64 | case filterName: 65 | res[i] = filter.nameFilter(filePath) 66 | case filterExtension: 67 | res[i] = filter.extensionFilter(filePath) 68 | case filterSize: 69 | res[i] = filter.sizeFilter(filePath) 70 | case filterLastEdited: 71 | res[i] = filter.lastEditedFilter(filePath) 72 | case filterContains: 73 | res[i] = filter.containsFilter(filePath) 74 | default: 75 | log.Fatalf("Unknown filter %s", filter.Filter) 76 | } 77 | 78 | i++ 79 | } 80 | 81 | return r.filtersResult(res) 82 | } 83 | 84 | func (r *Rule) filtersResult(res []bool) bool { 85 | if r.Operator == operatorOr { 86 | for _, b := range res { 87 | if b { 88 | return true 89 | } 90 | } 91 | 92 | return false 93 | } 94 | 95 | for _, b := range res { 96 | if !b { 97 | return false 98 | } 99 | } 100 | 101 | return true 102 | } 103 | 104 | func (r *Rule) runActions(filePath string, vars *Vars) { 105 | for _, action := range r.Actions { 106 | switch action.Action { 107 | case actionEcho: 108 | action.echoAction(filePath, vars) 109 | case actionTouch: 110 | action.touchAction(filePath) 111 | case actionMove: 112 | action.moveAction(filePath, vars) 113 | case actionRename: 114 | action.renameAction(filePath, vars) 115 | case actionDelete: 116 | action.deleteAction(filePath) 117 | default: 118 | log.Fatalf("Unknown action %s", action.Action) 119 | } 120 | } 121 | } 122 | 123 | // Config is a multiple rules container. 124 | type Config struct { 125 | Vars Vars `yaml:"vars"` 126 | Rules []Rule `yaml:"rules"` 127 | wg sync.WaitGroup 128 | } 129 | 130 | // ReadConfig reads configuration from the default configuration file. 131 | func ReadConfig() *Config { 132 | return ReadConfigFromFile(GetDefaultConfigFile()) 133 | } 134 | 135 | // ReadConfigFromFile reads configuration from a custom configuration file. 136 | func ReadConfigFromFile(file string) *Config { 137 | c := &Config{} 138 | 139 | confFile, err := ioutil.ReadFile(file) 140 | if err != nil { 141 | log.Fatalf("Failed to read yaml configuration from file %s, #%v ", file, err) 142 | return nil 143 | } 144 | 145 | err = yaml.Unmarshal(confFile, c) 146 | if err != nil { 147 | log.Fatalf("Invalid configuration: %v", err) 148 | return nil 149 | } 150 | 151 | // m, _ := yaml.Marshal(c) 152 | // log.Printf("Parsed configuration file\n%s\n", string(m)) 153 | 154 | c.Vars.init() 155 | return c 156 | } 157 | 158 | // ReadConfigFromByteArray reads configuration from the given byte array. 159 | func ReadConfigFromByteArray(configYaml []byte) *Config { 160 | c := &Config{} 161 | 162 | err := yaml.Unmarshal(configYaml, c) 163 | if err != nil { 164 | log.Fatalf("Invalid configuration: %v", err) 165 | return nil 166 | } 167 | 168 | c.Vars.init() 169 | return c 170 | } 171 | 172 | // Execute executes rules. 173 | func (c *Config) Execute() { 174 | for _, rule := range c.Rules { 175 | c.wg.Add(1) 176 | go c.iterate(rule) 177 | } 178 | 179 | c.wg.Wait() 180 | } 181 | 182 | func (c *Config) iterate(rule Rule) { 183 | defer c.wg.Done() 184 | 185 | files, err := ioutil.ReadDir(rule.Path) 186 | if err != nil { 187 | log.Fatal(err) 188 | } 189 | 190 | for _, f := range files { 191 | filePath := validatePath(rule.Path) + f.Name() 192 | if f.IsDir() { 193 | if rule.Recursive { 194 | rule.Path = filePath 195 | c.wg.Add(1) 196 | go c.iterate(rule) 197 | } 198 | } else { 199 | c.wg.Add(1) 200 | go func(path string) { 201 | defer c.wg.Done() 202 | 203 | if rule.checkFilters(path) { 204 | rule.runActions(path, &c.Vars) 205 | } 206 | }(filePath) 207 | } 208 | } 209 | } 210 | 211 | func validatePath(dirPath string) string { 212 | if strings.HasSuffix(dirPath, ospkg.PathSeparator) { 213 | return dirPath 214 | } 215 | 216 | return dirPath + ospkg.PathSeparator 217 | } 218 | -------------------------------------------------------------------------------- /rules/filters.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Filter represents filter rules to apply on files. 14 | type Filter struct { 15 | Filter string `yaml:"filter"` 16 | Payload string `yaml:"payload"` 17 | } 18 | 19 | func (f *Filter) nameFilter(filePath string) bool { 20 | regex := regexp.MustCompile(f.Payload) 21 | return regex.MatchString(filePath) 22 | } 23 | 24 | func (f *Filter) extensionFilter(filePath string) bool { 25 | return strings.HasSuffix(filePath, f.Payload) 26 | } 27 | 28 | func (f *Filter) containsFilter(filePath string) bool { 29 | data, err := ioutil.ReadFile(filePath) 30 | if err != nil { 31 | log.Printf("Failed to read file %s. %s\n", filePath, err.Error()) 32 | return false 33 | } 34 | 35 | regex := regexp.MustCompile(f.Payload) 36 | return regex.MatchString(string(data)) 37 | } 38 | 39 | func (f *Filter) sizeFilter(filePath string) bool { 40 | p := strings.Split(f.Payload, " ") 41 | if len(p) != 2 { 42 | log.Fatalf("Invalid payload for sizeFilter %s", f.Payload) 43 | } 44 | 45 | size, err := strconv.ParseInt(p[1], 10, 64) 46 | if err != nil { 47 | log.Fatalf("Invalid filter size configuration %s", p[1]) 48 | } 49 | 50 | fi, err := os.Stat(filePath) 51 | if err != nil { 52 | log.Fatalf("Failed to get %s Stat()", filePath) 53 | } 54 | 55 | if lt(p[0]) { 56 | return fi.Size() < size 57 | } else if eq(p[0]) { 58 | return fi.Size() == size 59 | } 60 | 61 | // default gt operator 62 | return fi.Size() > size 63 | } 64 | 65 | func (f *Filter) lastEditedFilter(filePath string) bool { 66 | p := strings.Split(f.Payload, " ") 67 | if len(p) != 2 { 68 | log.Fatalf("Invalid payload for lastEditedFilter %s", f.Payload) 69 | } 70 | 71 | ts, err := strconv.ParseInt(p[1], 10, 64) 72 | if err != nil { 73 | log.Fatalf("Invalid last edited configuration %s", p[1]) 74 | } 75 | 76 | fi, err := os.Stat(filePath) 77 | if err != nil { 78 | log.Fatalf("Failed to get %s Stat()", filePath) 79 | } 80 | 81 | if lt(p[0]) { 82 | return fi.ModTime().Before(time.Unix(ts, 0)) 83 | } else if eq(p[0]) { 84 | return fi.ModTime().Equal(time.Unix(ts, 0)) 85 | } 86 | 87 | // default gt operator 88 | return fi.ModTime().After(time.Unix(ts, 0)) 89 | } 90 | 91 | func gt(op string) bool { 92 | return op == "gt" || op == ">" 93 | } 94 | 95 | func lt(op string) bool { 96 | return op == "lt" || op == "<" 97 | } 98 | 99 | func eq(op string) bool { 100 | return op == "eq" || op == "=" || op == "==" 101 | } 102 | -------------------------------------------------------------------------------- /rules/functions.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | trimFunc = "trim" 12 | upperFunc = "upper" 13 | lowerFunc = "lower" 14 | quoteFunc = "quote" 15 | headFunc = "head" 16 | lenFunc = "len" 17 | ) 18 | 19 | // Pipeline chains together series of template commands to compactly express 20 | // series of transformations. 21 | func Pipeline(in string, chain []string) string { 22 | out := in 23 | 24 | for _, f := range chain { 25 | switch strings.ToLower(f) { 26 | case trimFunc: 27 | out = strings.TrimSpace(out) 28 | case upperFunc: 29 | out = strings.ToUpper(out) 30 | case lowerFunc: 31 | out = strings.ToLower(out) 32 | case quoteFunc: 33 | out = fmt.Sprintf("\"%s\"", out) 34 | case lenFunc: 35 | out = strconv.Itoa(len(out)) 36 | } 37 | 38 | if strings.HasPrefix(f, headFunc) { 39 | tkn := strings.Split(f, " ") 40 | if len(tkn) != 2 { 41 | log.Fatalf("Invalid len function configuration: %s", f) 42 | } 43 | out = out[:len(tkn[1])] 44 | } 45 | } 46 | 47 | return out 48 | } 49 | -------------------------------------------------------------------------------- /rules/utils.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import "os" 4 | 5 | // DefaultConfigFile is the configuration file default path. 6 | const DefaultConfigFile string = "conf.yaml" 7 | 8 | // GetDefaultConfigFile returns a configuration file path. 9 | func GetDefaultConfigFile() string { 10 | confFile, ok := os.LookupEnv("FSWEEPER_CONFIG_FILE") 11 | if !ok { 12 | confFile = DefaultConfigFile 13 | } 14 | return confFile 15 | } 16 | -------------------------------------------------------------------------------- /rules/vars.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/reugn/fsweeper/internal/ospkg" 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | const ( 16 | defaultTimeFormat = "15-04-05" 17 | defaultDateFormat = "2006-01-02" 18 | defaultDateTimeFormat = "2006-01-02[15-04-05]" 19 | ) 20 | 21 | const ( 22 | fileSizeVar = ".FileSize" 23 | fileNameVar = ".FileName" 24 | filePathVar = ".FilePath" 25 | fileExtVar = ".FileExt" 26 | timeVar = ".Time" 27 | dateVar = ".Date" 28 | dateTimeVar = ".DateTime" 29 | tsVar = ".Ts" 30 | ) 31 | 32 | var re *regexp.Regexp = regexp.MustCompile(`\{\{(.+?)\}\}`) 33 | 34 | // Vars represents configuration variables for substitution. 35 | type Vars struct { 36 | TimeFormat string `yaml:"timeFormat"` 37 | DateFormat string `yaml:"dateFormat"` 38 | DateTimeFormat string `yaml:"dateTimeFormat"` 39 | now time.Time 40 | } 41 | 42 | // String is the `fmt.Stringer` interface implementation. 43 | func (v *Vars) String() string { 44 | arr, _ := yaml.Marshal(*v) 45 | return string(arr) 46 | } 47 | 48 | // Process processes the payload string. 49 | func (v *Vars) Process(str string, ctx string) string { 50 | compile := func(s string) string { 51 | return v.processVariableBlock(s, ctx) 52 | } 53 | return re.ReplaceAllStringFunc(str, compile) 54 | } 55 | 56 | // compile a single variable block. 57 | func (v *Vars) processVariableBlock(variable string, ctx string) string { 58 | variable = strings.Trim(variable, "{{") 59 | variable = strings.Trim(variable, "}}") 60 | tokens := strings.Split(variable, "|") 61 | 62 | // trim spaces 63 | for i := range tokens { 64 | tokens[i] = strings.TrimSpace(tokens[i]) 65 | } 66 | 67 | variable, chain := tokens[0], tokens[1:] 68 | 69 | var value string 70 | switch variable { 71 | case fileSizeVar: 72 | value = getFileSize(ctx) 73 | case fileNameVar: 74 | value = getFileName(ctx) 75 | case filePathVar: 76 | value = ctx 77 | case fileExtVar: 78 | value = getFileExtension(ctx) 79 | case timeVar: 80 | value = v.getTime() 81 | case dateVar: 82 | value = v.getDate() 83 | case dateTimeVar: 84 | value = v.getDateTime() 85 | case tsVar: 86 | value = strconv.FormatInt(v.now.Unix(), 10) 87 | default: 88 | log.Fatalf("Unknown variable configuration: %s", variable) 89 | } 90 | 91 | return Pipeline(value, chain) 92 | } 93 | 94 | // init variables, set default value if not set 95 | func (v *Vars) init() { 96 | if v.TimeFormat == "" { 97 | v.TimeFormat = defaultTimeFormat 98 | } 99 | 100 | if v.DateFormat == "" { 101 | v.DateFormat = defaultDateFormat 102 | } 103 | 104 | if v.DateTimeFormat == "" { 105 | v.DateTimeFormat = defaultDateTimeFormat 106 | } 107 | 108 | v.now = time.Now() 109 | } 110 | 111 | // returns the file size 112 | func getFileSize(filePath string) string { 113 | fi, err := os.Stat(filePath) 114 | if err != nil { 115 | log.Fatalf("Failed to get %s Stat()", filePath) 116 | } 117 | return strconv.FormatInt(fi.Size(), 10) 118 | } 119 | 120 | // returns the file name 121 | func getFileName(filePath string) string { 122 | p := strings.Split(filePath, ospkg.PathSeparator) 123 | return p[len(p)-1] 124 | } 125 | 126 | // returns the file extension 127 | func getFileExtension(filePath string) string { 128 | p := strings.Split(filePath, ".") 129 | return p[len(p)-1] 130 | } 131 | 132 | // returns the formatted time 133 | func (v *Vars) getTime() string { 134 | return v.now.Format(v.TimeFormat) 135 | } 136 | 137 | // returns the formatted date 138 | func (v *Vars) getDate() string { 139 | return v.now.Format(v.DateFormat) 140 | } 141 | 142 | // returns the formatted datetime 143 | func (v *Vars) getDateTime() string { 144 | return v.now.Format(v.DateTimeFormat) 145 | } 146 | --------------------------------------------------------------------------------