├── .gitignore ├── doc ├── logo.png ├── logo2.png ├── screen.png ├── meg-after.jpg └── meg-before.jpg ├── go.mod ├── core └── register.go ├── fswatch └── fswatch.go ├── go.sum ├── LICENSE ├── tinyjpg_install.sh ├── .travis.yml ├── config.yml ├── settings └── settings.go ├── compress └── convert.go ├── cmd └── tinyjpg │ └── main.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ./idea 2 | .idea 3 | build 4 | ./build -------------------------------------------------------------------------------- /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OrlovEvgeny/TinyJPG/HEAD/doc/logo.png -------------------------------------------------------------------------------- /doc/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OrlovEvgeny/TinyJPG/HEAD/doc/logo2.png -------------------------------------------------------------------------------- /doc/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OrlovEvgeny/TinyJPG/HEAD/doc/screen.png -------------------------------------------------------------------------------- /doc/meg-after.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OrlovEvgeny/TinyJPG/HEAD/doc/meg-after.jpg -------------------------------------------------------------------------------- /doc/meg-before.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OrlovEvgeny/TinyJPG/HEAD/doc/meg-before.jpg -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OrlovEvgeny/TinyJPG 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/OrlovEvgeny/logger v0.0.0-20181108090900-fd2dc780dbd5 7 | github.com/rjeczalik/notify v0.9.2 8 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 9 | gopkg.in/yaml.v2 v2.2.2 10 | ) 11 | -------------------------------------------------------------------------------- /core/register.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "context" 4 | 5 | type KeyType int 6 | 7 | const ( 8 | ImageMagic KeyType = iota 9 | Log 10 | ) 11 | 12 | //Register 13 | func Register(ctx context.Context, key KeyType, value interface{}) context.Context { 14 | return context.WithValue(ctx, key, value) 15 | } 16 | -------------------------------------------------------------------------------- /fswatch/fswatch.go: -------------------------------------------------------------------------------- 1 | package fswatch 2 | 3 | import ( 4 | "github.com/rjeczalik/notify" 5 | "log" 6 | "path/filepath" 7 | ) 8 | 9 | //Watch 10 | type FSWatcher struct { 11 | FChan chan notify.EventInfo 12 | Paths []string 13 | } 14 | 15 | //watcherStart 16 | func (w *FSWatcher) FSWatcherStart() { 17 | // 18 | for _, path := range w.Paths { 19 | go watcherInit(w.FChan, path) 20 | } 21 | } 22 | 23 | //watcherStop 24 | func (w *FSWatcher) FSWatcherStop() { 25 | notify.Stop(w.FChan) 26 | } 27 | 28 | //watcherRestart 29 | func (w *FSWatcher) FSWatcherRestart() { 30 | w.FSWatcherStop() 31 | w.FSWatcherStart() 32 | } 33 | 34 | //watcherInit 35 | func watcherInit(ec chan notify.EventInfo, path string) { 36 | path = filepath.Join(path, "/...") 37 | if err := notify.Watch(path, ec, notify.Create); err != nil { 38 | log.Fatalf("watch path %s error: %s\n", path, err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/OrlovEvgeny/logger v0.0.0-20181108090900-fd2dc780dbd5 h1:/9klHhoT8eqvYAyzRX9XF5/lP/F5JbYnigO5m++FaYs= 2 | github.com/OrlovEvgeny/logger v0.0.0-20181108090900-fd2dc780dbd5/go.mod h1:wbEqmapeGM+FPqWCaz8FXEMEwuotahQR4mSo/IY/dQk= 3 | github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= 4 | github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= 5 | golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 6 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 7 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 8 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 9 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 urShadow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tinyjpg_install.sh: -------------------------------------------------------------------------------- 1 | VERSION="$1" 2 | 3 | PATH="$PATH:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin" 4 | TARGET_DIR=/usr/local/bin/tinyjpg 5 | CONF_DIR=/etc/tinyjpg 6 | LOG_DIR=/var/log/tinyjpg 7 | PERM="chmod +x /usr/local/bin/tinyjpg" 8 | 9 | if [ `getconf LONG_BIT` = "32" ]; then 10 | ARCH="386" 11 | else 12 | ARCH="amd64" 13 | fi 14 | 15 | URL="https://github.com/OrlovEvgeny/TinyJPG/releases/download/$VERSION/tinyjpg-$ARCH" 16 | CONF_URL="https://raw.githubusercontent.com/OrlovEvgeny/TinyJPG/master/config.yml" 17 | 18 | if [ -n "`which curl`" ]; then 19 | download_cmd="curl -L $URL --output $TARGET_DIR" 20 | conf_download_cmd="curl -L $CONF_URL --output $CONF_DIR/config.yml" 21 | else 22 | die "Failed to download TinyJPG: curl not found, plz install curl" 23 | fi 24 | 25 | mkdir -p $CONF_DIR $LOG_DIR 26 | 27 | echo -n "Fetching TinyJPG from $URL: " 28 | $download_cmd || die "Error when downloading TinyJPG from $URL" 29 | $conf_download_cmd || die "Error when downloading config file TinyJPG from $CONF_URL" 30 | /bin/echo -e "Install TinyJPG: \x1B[32m done \x1B[0m" 31 | 32 | echo -n "Set permission execute TinyJPG: " 33 | $PERM || die "Error permission execut TinyJPG from $TARGET_DIR" 34 | /bin/echo -e "\x1B[32m done \x1B[0m" 35 | tinyjpg -v 36 | /bin/echo -e "\x1B[32m Finished \x1B[0m" 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip # The latest version of Go. 4 | 5 | install: true 6 | 7 | env: 8 | global: 9 | - MYAPP=tinyjpg 10 | - MYEMAIL=dev@meetapp.io 11 | - secure: ${GH_TOKEN} 12 | 13 | before_install: 14 | - sudo apt-get -qq update 15 | 16 | install: 17 | - go get -u github.com/OrlovEvgeny/TinyJPG 18 | - cd $GOPATH/src/github.com/OrlovEvgeny/TinyJPG 19 | - go install 20 | 21 | script: echo "pass" 22 | 23 | # build the app,build the package 24 | before_deploy: 25 | - mkdir -p build/{386,amd64} 26 | - GOOS=linux GOARCH=386 go build --ldflags "-X main.version=${TRAVIS_TAG} -X main.build=${TRAVIS_BUILD_NUMBER} -X main.commit=${TRAVIS_COMMIT} -X main.docs=https://github.com/OrlovEvgeny/TinyJPG/blob/master/README.md" -o build/386/${MYAPP}-386 ./cmd/tinyjpg/main.go 27 | - GOOS=linux GOARCH=amd64 go build --ldflags "-X main.version=${TRAVIS_TAG} -X main.build=${TRAVIS_BUILD_NUMBER} -X main.commit=${TRAVIS_COMMIT} -X main.docs=https://github.com/OrlovEvgeny/TinyJPG/blob/master/README.md" -o build/amd64/${MYAPP}-amd64 ./cmd/tinyjpg/main.go 28 | 29 | deploy: 30 | provider: releases 31 | email: dev@meetapp.io 32 | api_key: 33 | secure: ${GH_TOKEN} 34 | file: 35 | - build/386/${MYAPP}-386 36 | - build/amd64/${MYAPP}-amd64 37 | skip_cleanup: true 38 | on: 39 | tags: true 40 | all_branches: true -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | ## 2 | # TinyJPG v0.0.8 3 | # 4 | # worker - maximum amount workers, Default value - 5 5 | # verbose - verbose log, Default value - true 6 | # worker_buffer - maximum buffer queue workers, Default value - 100 7 | # event_buffer - maximum buffer an event reported by the underlying filesystem notification subsystem, Default value - 100 8 | ## 9 | general: 10 | worker: 5 11 | worker_buffer: 100 12 | event_buffer: 300 13 | verbose: false 14 | info_log: '/var/log/tinyjpg/info.log' 15 | 16 | ### 17 | # Image compress settings 18 | # 19 | # paths - directories you need to track 20 | # prefix - prefix of files to be processed, Default value all files - * 21 | # 22 | # file will be observed: 23 | # orig-beatiful-kitten-2019.jpg 24 | # medium-beatiful-dog-600x800.jpg 25 | # 26 | # file will not be observed: 27 | # myphoto-beatiful-kitten-2019.jpg 28 | # simplephoto-beatiful-dog-600x80.jpg 29 | # 30 | # example use 31 | # 32 | # prefix: 33 | # - 'orig' 34 | # - 'medium' 35 | # - 'full' 36 | # 37 | # quality - This param image quality level in percentage. 38 | # If the original image quality is lower than the quality of the parameter - quality 39 | # the image will not be processed 40 | ### 41 | compress: 42 | paths: 43 | - '/home/www/example.com/uploads' 44 | - '/home/www/site.org/uploads' 45 | prefix: 46 | - '*' 47 | quality: 82 -------------------------------------------------------------------------------- /settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/yaml.v2" 6 | "io/ioutil" 7 | "log" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | AppName = "TinyJPG" 14 | ) 15 | 16 | var setting settingModel 17 | 18 | var ( 19 | SettingFile string 20 | Logger *log.Logger 21 | 22 | Debug bool 23 | General = &setting.General 24 | Compress = &setting.Compress 25 | ) 26 | 27 | //settingModel 28 | type settingModel struct { 29 | General struct { 30 | Worker int `yaml:"worker"` 31 | WorkerBuffer int `yaml:"worker_buffer"` 32 | EventBuffer int `yaml:"event_buffer"` 33 | Verbose bool `yaml:"verbose"` 34 | ErrorLog string `yaml:"error_log"` 35 | InfoLog string `yaml:"info_log"` 36 | PidFile string `yaml:"pid_file"` 37 | } `yaml:"general"` 38 | 39 | Compress struct { 40 | Prefix []string `yaml:"prefix"` 41 | Path string `yaml:"path"` 42 | Paths []string `yaml:"paths"` 43 | Quality int `yaml:"quality"` 44 | } `yaml:"compress"` 45 | } 46 | 47 | //Read and parse config file 48 | func LoadSettings(configFile string) (error error) { 49 | error = nil 50 | filename, err := filepath.Abs(configFile) 51 | if err != nil { 52 | fmt.Println("Fail find settings") 53 | return err 54 | } 55 | yamlFile, err := ioutil.ReadFile(filename) 56 | if err != nil { 57 | fmt.Println("Fail open settings") 58 | return err 59 | } 60 | err = yaml.Unmarshal(yamlFile, &setting) 61 | if err != nil { 62 | log.Printf("[%s] error: %s parse from file %s\n", AppName, err, filename) 63 | return err 64 | } 65 | log.Println("load settings √") 66 | return error 67 | } 68 | 69 | //ReloadSettings 70 | func ReloadSettings() error { 71 | filename, err := filepath.Abs(SettingFile) 72 | if err != nil { 73 | return fmt.Errorf("[%s] can not be reloaded, filepath Abs error: %s\n", AppName, err.Error()) 74 | } 75 | yamlFile, err := ioutil.ReadFile(filename) 76 | if err != nil { 77 | return fmt.Errorf("[%s] can not be reloaded, can not read yaml-File: %s\n", AppName, err.Error()) 78 | } 79 | err = yaml.Unmarshal(yamlFile, &setting) 80 | if err != nil { 81 | return fmt.Errorf("[%s] error: %s parse from file %s\n", AppName, err, filename) 82 | } 83 | log.Printf("[%s] - Setting file re-load: %s\n", AppName, filename) 84 | return nil 85 | } 86 | 87 | //getRegexp 88 | func Regexp() string { 89 | if len(Compress.Prefix) > 0 && Compress.Prefix[0] != "*" { 90 | prefix := fmt.Sprintf(`(%s).*.(JPG|jpeg|JPEG|jpg|png|PNG)$`, strings.Join(Compress.Prefix, "|")) 91 | return prefix 92 | } 93 | return `^.*.(JPG|jpeg|JPEG|jpg|png|PNG)$` 94 | } 95 | -------------------------------------------------------------------------------- /compress/convert.go: -------------------------------------------------------------------------------- 1 | package compress 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/OrlovEvgeny/TinyJPG/core" 7 | "github.com/OrlovEvgeny/TinyJPG/settings" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "regexp" 12 | "strconv" 13 | ) 14 | 15 | const ( 16 | PNG = "PNG" 17 | JPEG = "JPEG" 18 | ) 19 | 20 | //Imagemagic 21 | type Imagemagic struct { 22 | ctx context.Context 23 | Log *log.Logger 24 | Quality int 25 | } 26 | 27 | //NewImagemagic cmd wrapper 28 | func NewImagemagic(ctx context.Context) *Imagemagic { 29 | return &Imagemagic{ 30 | Log: ctx.Value(core.Log).(*log.Logger), 31 | ctx: ctx, 32 | Quality: settings.Compress.Quality, 33 | } 34 | } 35 | 36 | func (im *Imagemagic) Run(c chan string) { 37 | for i := 0; i < settings.General.Worker; i++ { 38 | go im.process(c) 39 | } 40 | } 41 | 42 | //process 43 | func (im *Imagemagic) process(c chan string) { 44 | jpg, err := regexp.Compile(`^.*.(JPG|jpeg|JPEG|jpg)$`) 45 | if err != nil { 46 | im.Log.Printf("Error: There is a problem with your regexp\n") 47 | os.Exit(1) 48 | } 49 | 50 | for { 51 | select { 52 | case imagePath := <-c: 53 | fi, _ := os.Stat(imagePath) 54 | beforeSize := fi.Size() 55 | 56 | if !im.qualityCheck(82, imagePath) { 57 | im.Log.Printf("File %s already compressed\n", imagePath) 58 | continue 59 | } 60 | 61 | interlace := PNG 62 | if jpg.MatchString(imagePath) == true { 63 | interlace = JPEG 64 | } 65 | 66 | cmd := im.buildArgs(im.Quality, imagePath, interlace) 67 | 68 | if _, err := exec.Command("bash", "-c", cmd).Output(); err != nil { 69 | im.Log.Printf("Commpress imagemagic error: %s\n", err.Error()) 70 | continue 71 | } 72 | 73 | fl, _ := os.Stat(imagePath) 74 | afterSize := fl.Size() 75 | im.Log.Printf("Compress file %s is done, filesize before %d, after %d\n", fi.Name(), beforeSize, afterSize) 76 | 77 | case <-im.ctx.Done(): 78 | return 79 | } 80 | } 81 | } 82 | 83 | //check quality 84 | func (im *Imagemagic) qualityCheck(quality int, file string) bool { 85 | cmd := fmt.Sprintf("identify -format %s %s", "'%Q'", file) 86 | out, err := exec.Command("bash", "-c", cmd).Output() 87 | if err != nil { 88 | im.Log.Printf("Incorect file name %s", file) 89 | } 90 | qualityNum, _ := strconv.ParseInt(string(out), 10, 0) 91 | if int64(quality) >= qualityNum { 92 | return false 93 | } 94 | return true 95 | } 96 | 97 | //buildArgs 98 | func (im *Imagemagic) buildArgs(quality int, imagePath, interlace string) string { 99 | return fmt.Sprintf("convert %s -sampling-factor 4:2:0 -strip -quality %d -interlace %s -colorspace sRGB %s", 100 | imagePath, 101 | quality, 102 | interlace, 103 | imagePath) 104 | } 105 | -------------------------------------------------------------------------------- /cmd/tinyjpg/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "github.com/OrlovEvgeny/TinyJPG/compress" 8 | "github.com/OrlovEvgeny/TinyJPG/core" 9 | "github.com/OrlovEvgeny/TinyJPG/fswatch" 10 | "github.com/OrlovEvgeny/TinyJPG/settings" 11 | "github.com/OrlovEvgeny/logger" 12 | "github.com/rjeczalik/notify" 13 | "log" 14 | "os" 15 | "regexp" 16 | ) 17 | 18 | //ldflags override 19 | var ( 20 | version string 21 | build string 22 | commit string 23 | docs string 24 | ) 25 | 26 | //init 27 | func init() { 28 | //print helps if not require args 29 | if len(os.Args) < 2 { 30 | fmt.Printf("Usage: TinyJPG -options=param\n\n") 31 | flag.PrintDefaults() 32 | os.Exit(0) 33 | } 34 | 35 | if len(os.Args) == 2 && (os.Args[1] == "--version" || os.Args[1] == "-v" || os.Args[1] == "ver") { 36 | printVersion() 37 | os.Exit(0) 38 | } 39 | 40 | flag.BoolVar(&settings.Debug, "debug", false, "example --debug=true") 41 | flag.StringVar(&settings.SettingFile, "c", "/etc/tinyjpg/config.yml", "example --c=config.yml") 42 | flag.Parse() 43 | 44 | if err := settings.LoadSettings(settings.SettingFile); err != nil { 45 | log.Fatalf("fatal open setting file %s, error: %s\n", settings.SettingFile, err) 46 | } 47 | 48 | //Initial global logger 49 | settings.Logger = logger.New(&logger.Config{ 50 | AppName: settings.AppName, 51 | Debug: settings.Debug, 52 | LogFile: settings.General.InfoLog, 53 | }) 54 | } 55 | 56 | //main 57 | func main() { 58 | ctx := context.Background() 59 | ctx = context.WithValue(ctx, core.Log, settings.Logger) 60 | 61 | c := make(chan string, settings.General.WorkerBuffer) 62 | fchan := make(chan notify.EventInfo, settings.General.EventBuffer) 63 | done := make(chan struct{}, 1) 64 | 65 | compress.NewImagemagic(ctx).Run(c) 66 | 67 | FSWatcher := &fswatch.FSWatcher{ 68 | FChan: fchan, 69 | Paths: settings.Compress.Paths, 70 | } 71 | 72 | regexpTeml := settings.Regexp() 73 | re, err := regexp.Compile(regexpTeml) 74 | if err != nil { 75 | settings.Logger.Printf("Error: There is a problem with your regexp: %s\n", regexpTeml) 76 | os.Exit(1) 77 | } 78 | 79 | FSWatcher.FSWatcherStart() 80 | defer notify.Stop(fchan) 81 | 82 | // Process events 83 | go func() { 84 | for { 85 | select { 86 | case ev := <-fchan: 87 | if re.MatchString(ev.Path()) == true { 88 | c <- ev.Path() 89 | } 90 | case <-ctx.Done(): 91 | return 92 | } 93 | } 94 | }() 95 | 96 | <-done 97 | fmt.Println("exit.") 98 | 99 | } 100 | 101 | //printVersion program build data 102 | func printVersion() { 103 | fmt.Print(` 104 | ________ ______ __ __ __ __ _____ _______ ______ 105 | / |/ |/ \ / |/ \ / | / |/ \ / \ 106 | $$$$$$$$/ $$$$$$/ $$ \ $$ |$$ \ /$$/ $$$$$ |$$$$$$$ |/$$$$$$ | 107 | $$ | $$ | $$$ \$$ | $$ \/$$/ $$ |$$ |__$$ |$$ | _$$/ 108 | $$ | $$ | $$$$ $$ | $$ $$/ __ $$ |$$ $$/ $$ |/ | 109 | $$ | $$ | $$ $$ $$ | $$$$/ / | $$ |$$$$$$$/ $$ |$$$$ | 110 | $$ | _$$ |_ $$ |$$$$ | $$ | $$ \__$$ |$$ | $$ \__$$ | 111 | $$ | / $$ |$$ | $$$ | $$ | $$ $$/ $$ | $$ $$/ 112 | $$/ $$$$$$/ $$/ $$/ $$/ $$$$$$/ $$/ $$$$$$/ 113 | 114 | `) 115 | fmt.Printf("Version: %s\nBuild Time: %s\nGit Commit Hash: %s\nDocs: %s\n\n\n", version, build, commit, docs) 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
