├── .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 | [](https://github.com/reugn/fsweeper/actions/workflows/build.yml)
5 | [](https://pkg.go.dev/github.com/reugn/fsweeper)
6 | [](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 |
--------------------------------------------------------------------------------