diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f6bc953 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" # Location of package manifests + schedule: + interval: "quarterly" + - package-ecosystem: "github-actions" + directory: "/" # Location of package manifests + schedule: + interval: "quarterly" diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..2597d67 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,35 @@ +name: Release Go Binaries + +on: + release: + types: [created] + +jobs: + releases-matrix: + name: Release Go Binary + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux, darwin] + goarch: ["386", amd64, arm, arm64] + exclude: + - goarch: "386" + goos: darwin + - goarch: arm + goos: darwin + + steps: + - uses: actions/checkout@v4 + + - uses: wangyoucao577/go-release-action@v1 + with: + binary_name: "goptimize" + asset_name: "goptimize-${{ matrix.goos }}-${{ matrix.goarch }}" + extra_files: LICENSE README.md + github_token: ${{ secrets.GITHUB_TOKEN }} + md5sum: false + overwrite: true + goos: ${{ matrix.goos }} + goarch: ${{ matrix.goarch }} + pre_command: export GO386=softfloat CGO_ENABLED=0 + ldflags: -s -w -X "main.version=${{ github.ref_name }}" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..847bbea --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "develop" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "develop" ] + schedule: + - cron: '34 23 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c2d062a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,20 @@ +name: Tests +on: + pull_request: + branches: [develop, "feature/**"] + push: + branches: [develop, "feature/**"] + +jobs: + golangci: + name: golangci-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9efeb42..d1ba8fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,42 +1,44 @@ # Changelog +## [1.0.0] + +- Use ghru/v2 for latest version checks and self-update functionality +- Refactor code for consistency and error handling improvements +- Update Go dependencies +- Use GitHub Actions to build release binaries +- Add quarterly dependabot module update +- Add code quality checks & linting (golangci-lint) + ## [0.2.3] - Switch to kovidgoyal/imaging to address CVE-2023-36308 - ## [0.2.2] - Optionally preserve exif data for supported formats - Update Go modules - ## [0.2.1] - Update core modules - ## [0.2.0] - Add threaded option (`-t`) to use all CPU cores - ## [0.1.0] - Switch to go mods - go (>= 1.11 required) - Switch to axllent/semver for app updating - ## [0.0.3] - Detect & skip animated GIFs - ## [0.0.2] - Switch to [pflag](https://github.com/spf13/pflag) for better flag management - ## [0.0.1] - Initial release diff --git a/LICENSE b/LICENSE index 2b9c19d..d81408c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,16 +1,21 @@ -Copyright 2019 Ralph Slooten +The MIT License (MIT) +Copyright (c) 2019-Now() Ralph Slooten -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: +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 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. +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/Makefile b/Makefile deleted file mode 100644 index 2f772a3..0000000 --- a/Makefile +++ /dev/null @@ -1,23 +0,0 @@ -TAG=`git describe --tags` -VERSION ?= `git describe --tags` -LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}" - -build = echo "\n\nBuilding $(1)-$(2)" && GOOS=$(1) GOARCH=$(2) go build ${LDFLAGS} -o dist/goptimize_${VERSION}_$(1)_$(2) \ - && bzip2 dist/goptimize_${VERSION}_$(1)_$(2) - -goptimize: *.go go.* - go build ${LDFLAGS} -o goptimize - rm -rf /tmp/go-* - -clean: - rm -f goptimize - -release: - mkdir -p dist - rm -f dist/goptimize_${VERSION}_* - $(call build,linux,amd64) - $(call build,linux,386) - $(call build,linux,arm) - $(call build,linux,arm64) - $(call build,darwin,amd64) - $(call build,darwin,arm64) diff --git a/README.md b/README.md index b19cba6..c0e4d1b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Goptimizer - downscales and optimizes images +# Goptimize - downscales and optimizes images [![Go Report Card](https://goreportcard.com/badge/github.com/axllent/goptimize)](https://goreportcard.com/report/github.com/axllent/goptimize) -Goptimizer is a commandline utility written in Golang. It downscales and optimizes JPEG, PNG, GIF, TIFF and BMP files. +Goptimize is a command-line utility written in Golang. It downscales and optimizes JPEG, PNG, GIF, TIFF and BMP files. Image downscaling/rotation is done within goptimize (`-m x`, see [Usage](#usage-options)), however optimization is done using the following additional tools (if they are installed): @@ -11,19 +11,17 @@ Image downscaling/rotation is done within goptimize (`-m x`, see - pngquant - gifsicle - ## Notes Both `jpegoptim` & `jpegtran` have almost identical optimization, so if both are installed then just `jpegtran` is used for JPG optimization. PNG optimization however will run through both `optipng` & `pngquant` (if installed) as this can result in better optimization. It is highly recommended to install the necessary optimization tools, however they are not required to run goptimize. -Goptimize will remove all exif data from JPEG files, auto-rotating those that depend on it for orientation. +GGoptimize will by default remove all EXIF data from JPEG files and automatically rotate those that rely on it for orientation. This can be disabled by adding the `-e` flag. It will also preserve (by default) the file's original modification times (`-p=false` to disable). -Animated GIF files are not supported and automatically get skipped. - +Animated GIF files are not supported and are ignored. ## Usage options @@ -46,7 +44,6 @@ Options: --optipng string optipng binary (default "optipng") ``` - ## Examples - `./goptimize image.png` - optimize a PNG file @@ -54,23 +51,10 @@ Options: - `./goptimize -m 1200x0 image.jpg` - optimize and downscale a JPG file to a maximum size of width of 1200px - `./goptimize -o out/ image.jpg` - optimize a JPG file and save it to `out/` - ## Install -Download the appropriate binary from the [releases](https://github.com/axllent/goptimize/releases/latest), or if you have golang installed - - -### Build requirements - -Go >= 1.21 required. +Download the appropriate binary from the [releases](https://github.com/axllent/goptimize/releases/latest), or if you have golang installed you can install from source: ``` -go get github.com/axllent/goptimize +go install github.com/axllent/goptimize@latest ``` - -## TODO - -Some ideas for the future: - -- Dry run -- Option to copy exif data (how?) diff --git a/exifcopy.go b/exifcopy.go index 8285782..f28b7c6 100644 --- a/exifcopy.go +++ b/exifcopy.go @@ -115,21 +115,21 @@ func copyMetadata(outImagePath, imagePath, metadataImagePath string) error { if err != nil { return err } - defer outFile.Close() + defer func() { _ = outFile.Close() }() writer := bufio.NewWriter(outFile) imageFile, err := os.Open(imagePath) if err != nil { return err } - defer imageFile.Close() + defer func() { _ = imageFile.Close() }() imageReader := bufio.NewReader(imageFile) metaFile, err := os.Open(metadataImagePath) if err != nil { return err } - defer metaFile.Close() + defer func() { _ = metaFile.Close() }() metaReader := bufio.NewReader(metaFile) _, err = writer.Write([]byte{0xFF, soi}) @@ -166,6 +166,6 @@ func exifCopy(fromPath, toPath string) error { if err != nil { return err } - defer os.Remove(copyPath) + defer func() { _ = os.Remove(copyPath) }() return copyMetadata(toPath, copyPath, fromPath) } diff --git a/go.mod b/go.mod index 9a42328..511d29d 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/axllent/goptimize -go 1.21 +go 1.23.0 -toolchain go1.22.2 +toolchain go1.23.1 require ( - github.com/axllent/ghru v1.2.1 - github.com/kovidgoyal/imaging v1.6.3 - github.com/spf13/pflag v1.0.5 - golang.org/x/image v0.15.0 + github.com/axllent/ghru/v2 v2.0.1 + github.com/kovidgoyal/imaging v1.6.4 + github.com/spf13/pflag v1.0.6 + golang.org/x/image v0.28.0 ) -require github.com/axllent/semver v0.0.1 // indirect +require golang.org/x/mod v0.25.0 // indirect diff --git a/go.sum b/go.sum index 0dec89a..5a445fd 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,10 @@ -github.com/axllent/ghru v1.2.1 h1:PuJQQeILJJ42O9nvjTwObWQGZDyZ9/F71st9jNAqgoU= -github.com/axllent/ghru v1.2.1/go.mod h1:YgznIILRJpnII5x8N080q/G8Milzk3sy9Sh4dV1eGQw= -github.com/axllent/semver v0.0.1 h1:QqF+KSGxgj8QZzSXAvKFqjGWE5792ksOnQhludToK8E= -github.com/axllent/semver v0.0.1/go.mod h1:2xSPzvG8n9mRfdtxSvWvfTfQGWfHsMsHO1iZnKATMSc= -github.com/kovidgoyal/imaging v1.6.3 h1:iNPpv7ygiaB/NOztc6APMT7yr9UwBS+rOZwIbAdtyY8= -github.com/kovidgoyal/imaging v1.6.3/go.mod h1:sHvcLOOVhJuto2IoNdPLEqnAUoL5ZfHEF0PpNH+882g= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= -golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +github.com/axllent/ghru/v2 v2.0.1 h1:/9XHbMqJRGRxlyNMq2XiHQpBLz7hlq8xCdgJ4ZhjCoM= +github.com/axllent/ghru/v2 v2.0.1/go.mod h1:seMMjx8/0r5ZAL7c0vwTPIRoyN0AoTUqAylZEWZWGK4= +github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk= +github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= +golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= diff --git a/goptimize.go b/goptimize.go index f1669b0..9b51230 100644 --- a/goptimize.go +++ b/goptimize.go @@ -18,7 +18,7 @@ import ( ) // Goptimize downscales and optimizes an existing image -func Goptimize(file string) { +func goptimize(file string) { info, err := os.Stat(file) if err != nil { @@ -57,7 +57,7 @@ func Goptimize(file string) { if format.String() == "GIF" { // return if GIF is animated - unsupported - if err := IsGIFAnimated(file); err != nil { + if err := isGIFAnimated(file); err != nil { fmt.Printf("Error: animated GIF not supported (%v)\n", file) return } @@ -99,7 +99,7 @@ func Goptimize(file string) { return } - defer os.Remove(tmpFile.Name()) + defer func() { _ = os.Remove(tmpFile.Name()) }() switch imgType := format.String(); imgType { case "JPEG": @@ -127,15 +127,18 @@ func Goptimize(file string) { // immediately close the temp file to release pointers // so we can modify it with system processes - tmpFile.Close() + if err := tmpFile.Close(); err != nil { + fmt.Printf("Error closing temporary file: %v\n", err) + return + } // run through optimizers if format.String() == "JPEG" { // run one or the other, running both has no advantage if jpegtran != "" { - RunOptimizer(tmpFilename, true, jpegtran, "-optimize", "-outfile") + runOptimizer(tmpFilename, true, jpegtran, "-optimize", "-outfile") } else if jpegoptim != "" { - RunOptimizer(tmpFilename, false, jpegoptim, "-f", "-s", "-o") + runOptimizer(tmpFilename, false, jpegoptim, "-f", "-s", "-o") } if copyExif { @@ -146,13 +149,13 @@ func Goptimize(file string) { } } else if format.String() == "PNG" { if pngquant != "" { - RunOptimizer(tmpFilename, true, pngquant, "-f", "--output") + runOptimizer(tmpFilename, true, pngquant, "-f", "--output") } if optipng != "" { - RunOptimizer(tmpFilename, true, optipng, "-out") + runOptimizer(tmpFilename, true, optipng, "-out") } } else if format.String() == "GIF" && gifsicle != "" { - RunOptimizer(tmpFilename, true, gifsicle, "-o") + runOptimizer(tmpFilename, true, gifsicle, "-o") } // re-open modified temporary file @@ -162,7 +165,7 @@ func Goptimize(file string) { return } - defer tmpFile.Close() + defer func() { _ = tmpFile.Close() }() // get th eoriginal file stats srcStat, _ := os.Stat(file) @@ -187,7 +190,7 @@ func Goptimize(file string) { return } - defer out.Close() + defer func() { _ = out.Close() }() if _, err := io.Copy(out, tmpFile); err != nil { fmt.Printf("Error overwriting original file: %v\n", err) @@ -201,22 +204,26 @@ func Goptimize(file string) { } } - fmt.Printf("Goptimized %s (%dx%d %s > %s %v%%)\n", dstFile, resultW, resultH, ByteCountSI(srcSize), ByteCountSI(dstSize), savedPercent) + fmt.Printf("Optimized %s (%dx%d %s > %s %v%%)\n", dstFile, resultW, resultH, byteCountSI(srcSize), byteCountSI(dstSize), savedPercent) } else { // if the output directory is not the same, // then write a copy of the original file if outputDir != "" { out, err := os.Create(dstFile) + if err != nil { + fmt.Printf("Error creating new file: %v\n", err) + return + } + + defer func() { _ = out.Close() }() + + orig, err := os.Open(file) if err != nil { fmt.Printf("Error opening original file: %v\n", err) return } - defer out.Close() - - orig, _ := os.Open(file) - - defer orig.Close() + defer func() { _ = orig.Close() }() if _, err := io.Copy(out, orig); err != nil { fmt.Printf("Error ovewriting original file: %v\n", err) @@ -230,36 +237,35 @@ func Goptimize(file string) { } } - fmt.Printf("Copied %s (%dx%d %s %v%%)\n", dstFile, srcW, srcH, ByteCountSI(srcSize), 0) + fmt.Printf("Copied %s (%dx%d %s %v%%)\n", dstFile, srcW, srcH, byteCountSI(srcSize), 0) } else { // we didn't actually change anything - fmt.Printf("Skipped %s (%dx%d %s %v%%)\n", dstFile, srcW, srcH, ByteCountSI(srcSize), 0) + fmt.Printf("Skipped %s (%dx%d %s %v%%)\n", dstFile, srcW, srcH, byteCountSI(srcSize), 0) } } } -// RunOptimizer will run the specified command on a copy of the temporary file, +// runOptimizer will run the specified command on a copy of the temporary file, // and overwrite it if the output is smaller than the original -func RunOptimizer(src string, outFileArg bool, args ...string) { +func runOptimizer(src string, outFileArg bool, args ...string) { // create a new temp file - tmpFile, err := os.CreateTemp(os.TempDir(), "Goptimized-") + tmpFile, err := os.CreateTemp(os.TempDir(), "goptimize-") if err != nil { fmt.Printf("Cannot create temporary file: %v\n", err) return } - defer os.Remove(tmpFile.Name()) + defer func() { _ = os.Remove(tmpFile.Name()) }() source, err := os.Open(src) - if err != nil { fmt.Printf("Cannot open temporary file: %v\n", err) return } - defer source.Close() + defer func() { _ = source.Close() }() if _, err := io.Copy(tmpFile, source); err != nil { fmt.Printf("Cannot copy source file: %v\n", err) @@ -289,8 +295,8 @@ func RunOptimizer(src string, outFileArg bool, args ...string) { dstSize := dstStat.Size() // ensure file pointers are closed before renaming - tmpFile.Close() - source.Close() + func() { _ = tmpFile.Close() }() + func() { _ = source.Close() }() if dstSize < srcSize { if err := os.Rename(tmpFilename, src); err != nil { @@ -301,7 +307,7 @@ func RunOptimizer(src string, outFileArg bool, args ...string) { } // ByteCountSI returns a human readable size from int64 bytes -func ByteCountSI(b int64) string { +func byteCountSI(b int64) string { const unit = 1000 if b < unit { return fmt.Sprintf("%dB", b) @@ -315,9 +321,12 @@ func ByteCountSI(b int64) string { } // IsGIFAnimated will return an error if the GIF file has more than 1 frame -func IsGIFAnimated(gifFile string) error { - file, _ := os.Open(gifFile) - defer file.Close() +func isGIFAnimated(gifFile string) error { + file, err := os.Open(gifFile) + if err != nil { + return fmt.Errorf("cannot open GIF file: %v", err) + } + defer func() { _ = file.Close() }() g, err := gif.DecodeAll(file) if err != nil { @@ -329,5 +338,5 @@ func IsGIFAnimated(gifFile string) error { return nil } - return fmt.Errorf("Animated gif") + return fmt.Errorf("cannot optimize an animated gif") } diff --git a/main.go b/main.go index 23eb3e6..3baf765 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,4 @@ +// Package main is the main application package main import ( @@ -9,7 +10,7 @@ import ( "strconv" "sync" - "github.com/axllent/ghru" + "github.com/axllent/ghru/v2" "github.com/spf13/pflag" ) @@ -27,6 +28,13 @@ var ( copyExif bool threads = 1 version = "dev" + // ghruConf is the configuration for the ghru package + ghruConf = ghru.Config{ + Repo: "axllent/goptimize", + ArchiveName: "goptimize-{{.OS}}-{{.Arch}}", + BinaryName: "goptimize", + CurrentVersion: version, + } ) func main() { @@ -47,15 +55,15 @@ func main() { fmt.Println("\nDetected optimizers:") if err := displayDetectedOptimizer("jpegtran ", jpegtran); err != nil { - displayDetectedOptimizer("jpegoptim", jpegoptim) + _ = displayDetectedOptimizer("jpegoptim", jpegoptim) } - displayDetectedOptimizer("optipng ", optipng) - displayDetectedOptimizer("pngquant ", pngquant) - displayDetectedOptimizer("gifsicle ", gifsicle) + _ = displayDetectedOptimizer("optipng ", optipng) + _ = displayDetectedOptimizer("pngquant ", pngquant) + _ = displayDetectedOptimizer("gifsicle ", gifsicle) } var maxSizes string - var multiThreaded, update, showversion, showhelp bool + var multiThreaded, update, showVersion, showHelp bool flag.IntVarP(&quality, "quality", "q", 75, "quality, JPEG only") flag.StringVarP(&maxSizes, "max", "m", "", "downscale to a maximum width & height in pixels (x)") @@ -64,8 +72,8 @@ func main() { flag.BoolVarP(©Exif, "exif", "e", false, "copy exif data") flag.BoolVarP(&update, "update", "u", false, "update to latest release") flag.BoolVarP(&multiThreaded, "threaded", "t", false, "run multi-threaded (use all CPU cores)") - flag.BoolVarP(&showversion, "version", "v", false, "show version number") - flag.BoolVarP(&showhelp, "help", "h", false, "show help") + flag.BoolVarP(&showVersion, "version", "v", false, "show version number") + flag.BoolVarP(&showHelp, "help", "h", false, "show help") // third-party optimizers flag.StringVar(&jpegtran, "jpegtran", "jpegtran", "jpegtran binary") @@ -77,7 +85,9 @@ func main() { flag.SortFlags = false // parse args excluding os.Args[0] - flag.Parse(os.Args[1:]) + if err := flag.Parse(os.Args[1:]); err != nil { + fmt.Printf("Error parsing flags: %s\n", err.Error()) + } // detect optimizer paths gifsicle, _ = exec.LookPath(gifsicle) @@ -86,28 +96,44 @@ func main() { optipng, _ = exec.LookPath(optipng) pngquant, _ = exec.LookPath(pngquant) - if showhelp { + if showHelp { flag.Usage() os.Exit(1) } - if showversion { - fmt.Println(fmt.Sprintf("Version: %s", version)) - latest, _, _, err := ghru.Latest("axllent/goptimize", "goptimize") - if err == nil && ghru.GreaterThan(latest, version) { - fmt.Printf("Update available: %s\nRun `%s -u` to update.\n", latest, os.Args[0]) + if showVersion { + fmt.Printf("Version: %s\n", version) + + release, err := ghruConf.Latest() + if err != nil { + fmt.Printf("Error getting latest release: %s\n", err.Error()) + os.Exit(1) } - return + + // The latest version is the same version + if release.Tag == version { + os.Exit(0) + } + + // A newer release is available + fmt.Printf( + "Update available: %s\nRun `%s -u` to update (requires read/write access to install directory).\n", + release.Tag, + os.Args[0], + ) + os.Exit(0) } if update { - rel, err := ghru.Update("axllent/goptimize", "goptimize", version) + // Update the app + rel, err := ghruConf.SelfUpdate() if err != nil { - fmt.Println(err) + fmt.Println(err.Error()) os.Exit(1) } - fmt.Printf("Updated %s to version %s\n", os.Args[0], rel) - return + + fmt.Printf("Updated %s to version %s\n", os.Args[0], rel.Tag) + os.Exit(0) } if len(flag.Args()) < 1 { @@ -155,7 +181,7 @@ func main() { for i := 0; i < threads; i++ { go func() { for nextFile := range processChan { - Goptimize(nextFile) + goptimize(nextFile) } // Channel was closed, so we finished this goroutine. wg.Done() // Let main goroutine know we are done.