diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ce5adb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +vendor diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..66271f6 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,18 @@ +project_name: streetsweeper +builds: + - main: ./cmd/main.go + binary: streetsweeper + goos: + - darwin + - linux + goarch: + - amd64 + - 386 + - arm +archive: + format: tar.gz + replacements: + darwin: macOS + files: + - README.md + - LICENSE diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..4c56f77 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,25 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "v1.4.2" + +[[projects]] + name = "github.com/urfave/cli" + packages = ["."] + revision = "v1.19.1" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = ["unix"] + revision = "07c182904dbd53199946ba614a412c61d3c548f5" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "23ffd0d5c5f77a19ea97dab74c2d76cb63e8c2fec6901933487c83ecd3ad4eeb" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..e1f69b0 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,7 @@ +[[constraint]] + name = "github.com/fsnotify/fsnotify" + revision = "v1.4.2" + +[[constraint]] + name = "github.com/urfave/cli" + revision = "v1.19.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..56e8ebb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Rafael Sales + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f28a14b --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Street sweeper + +Street sweeper is a program that monitors a directory and removes old files whenever +the storage use meets certain condition. + +# Usage and examples + +* All options: + + `streetsweeper --help` + +* When the directory `/etc/lib/motion` has used more than 4GB, +delete oldest video files until it only has 3.5GB used. + + `streetsweeper --max-size 4000MB --target-size 3500MB /etc/lib/motion` diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..c983495 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "log" + "github.com/fsnotify/fsnotify" + //"github.com/urfave/cli" + "../internal/file_sweeper" + "fmt" + "strings" + "errors" + "os" +) + +func main() { + maxSize, err := parseSize(os.Args[1]) + if err != nil { log.Fatal(err) } + targetSize, err := parseSize(os.Args[2]) + if err != nil { log.Fatal(err) } + path := os.Args[3] + + log.Printf("Initializing. Path: %s | Max size: %d | Target size: %d", path, maxSize, targetSize) + + onWrite := func() { file_sweeper.Run(path, maxSize, targetSize) } + onWrite() + fileWatcher(path, onWrite) +} + +func parseSize(formattedSize string) (bytes int64, err error) { + formattedSize = strings.TrimSpace(formattedSize) + + _, err = fmt.Sscanf(formattedSize, "%dKB", &bytes) + if err == nil { return bytes * 1024, nil } + + _, err = fmt.Sscanf(formattedSize, "%dMB", &bytes) + if err == nil { return bytes * 1024 * 1024, nil} + + _, err = fmt.Sscanf(formattedSize, "%dGB", &bytes) + if err == nil { return bytes * 1024 * 1024 * 1024, nil } + + return -1, errors.New("Unknown size format: " + formattedSize) +} + +func fileWatcher(path string, onWrite func()) { + watcher, err := fsnotify.NewWatcher() + if err != nil { log.Fatal(err) } + defer watcher.Close() + + done := make(chan bool) + go func() { + for { + select { + case event := <-watcher.Events: + if event.Op == fsnotify.Write || event.Op == fsnotify.Create { + log.Printf("Write detected: %s", event.Name) + onWrite() + } + case err := <-watcher.Errors: + log.Println("Error: %s", err) + } + } + }() + + err = watcher.Add(path) + if err != nil { log.Fatal(err) } + <-done +} diff --git a/internal/file_sweeper/main.go b/internal/file_sweeper/main.go new file mode 100644 index 0000000..137ed13 --- /dev/null +++ b/internal/file_sweeper/main.go @@ -0,0 +1,61 @@ +package file_sweeper + +import ( + "os" + "path/filepath" + "log" + "sort" +) + +func Run(path string, maxSize int64, targetSize int64) { + currentSize, _ := dirSize(path) + log.Printf("Path: %s | Current size: %d", path, currentSize) + + if currentSize > maxSize { + log.Printf( "Path: %s | Over max size - Current size: %d | Max size: %d", path, currentSize, maxSize) + deleteOldFiles(path, currentSize, targetSize) + } +} + +func dirSize(path string) (int64, error) { + var size int64 + os.Open(path) + err := filepath.Walk(path, func(a string, info os.FileInfo, err error) error { + if !info.IsDir() { + size += info.Size() + } + return err + }) + return size, err +} + +func deleteOldFiles(path string, currentSize int64, targetSize int64) { + var sizeDeleted int64 + + dir, err := os.Open(path) + if err != nil { log.Fatal(err) } + + entries, err := dir.Readdir(-1) + if err != nil { log.Fatal(err) } + + sort.SliceStable(entries, func(i, j int) bool { + return entries[i].ModTime().Before(entries[j].ModTime()) + }) + + for _, entry := range entries { + if !entry.IsDir() { + filePath := filepath.Join(path, entry.Name()) + modifiedAt := entry.ModTime().UTC().String() + size := entry.Size() + + log.Printf( "Deleting: %s | Modified at: %s | Size: %d", filePath, modifiedAt, size) + os.Remove(filePath) + sizeDeleted += size + } + + if currentSize - sizeDeleted < targetSize { + log.Printf( "Deleted a total of %d", sizeDeleted) + return + } + } +} \ No newline at end of file