mirror of
https://github.com/esimov/caire.git
synced 2024-05-31 14:55:55 +08:00
379 lines
9.2 KiB
Go
379 lines
9.2 KiB
Go
package caire
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/esimov/caire/utils"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// maxWorkers sets the maximum number of concurrently running workers.
|
|
const maxWorkers = 20
|
|
|
|
var (
|
|
// imgFile holds the file being accessed, be it normal file or pipe name.
|
|
imgFile *os.File
|
|
|
|
// Common file related variable
|
|
fs os.FileInfo
|
|
)
|
|
|
|
type Ops struct {
|
|
Src, Dst, PipeName string
|
|
Workers int
|
|
}
|
|
|
|
// result holds the relevant information about the resizing process and the generated image.
|
|
type result struct {
|
|
path string
|
|
err error
|
|
}
|
|
|
|
// Execute executes the image resizing process.
|
|
// In case the preview mode is activated it will be invoked in a separate goroutine
|
|
// in order to not block the main OS thread. Otherwise it will be called normally.
|
|
func (p *Processor) Execute(op *Ops) {
|
|
var err error
|
|
defaultMsg := fmt.Sprintf("%s %s",
|
|
utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
|
|
utils.DecorateText("⇢ resizing image (be patient, it may take a while)...", utils.DefaultMessage),
|
|
)
|
|
p.Spinner = utils.NewSpinner(defaultMsg, time.Millisecond*80)
|
|
|
|
// Supported files
|
|
validExtensions := []string{".jpg", ".png", ".jpeg", ".bmp", ".gif"}
|
|
|
|
// Check if source path is a local image or URL.
|
|
if utils.IsValidUrl(op.Src) {
|
|
src, err := utils.DownloadImage(op.Src)
|
|
if src != nil {
|
|
defer os.Remove(src.Name())
|
|
}
|
|
|
|
if err != nil {
|
|
log.Fatalf(
|
|
utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage),
|
|
utils.DecorateText(err.Error(), utils.DefaultMessage),
|
|
)
|
|
}
|
|
fs, err = src.Stat()
|
|
if err != nil {
|
|
log.Fatalf(
|
|
utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage),
|
|
utils.DecorateText(err.Error(), utils.DefaultMessage),
|
|
)
|
|
}
|
|
img, err := os.Open(src.Name())
|
|
if err != nil {
|
|
log.Fatalf(
|
|
utils.DecorateText("Unable to open the temporary image file: %v", utils.ErrorMessage),
|
|
utils.DecorateText(err.Error(), utils.DefaultMessage),
|
|
)
|
|
}
|
|
|
|
imgFile = img
|
|
} else {
|
|
// Check if the source is a pipe name or a regular file.
|
|
if op.Src == op.PipeName {
|
|
fs, err = os.Stdin.Stat()
|
|
} else {
|
|
fs, err = os.Stat(op.Src)
|
|
}
|
|
if err != nil {
|
|
log.Fatalf(
|
|
utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage),
|
|
utils.DecorateText(err.Error(), utils.DefaultMessage),
|
|
)
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
switch mode := fs.Mode(); {
|
|
case mode.IsDir():
|
|
var wg sync.WaitGroup
|
|
// Read destination file or directory.
|
|
_, err := os.Stat(op.Dst)
|
|
if err != nil {
|
|
err = os.Mkdir(op.Dst, 0755)
|
|
if err != nil {
|
|
log.Fatalf(
|
|
utils.DecorateText("Unable to get dir stats: %v\n", utils.ErrorMessage),
|
|
utils.DecorateText(err.Error(), utils.DefaultMessage),
|
|
)
|
|
}
|
|
}
|
|
p.Preview = false
|
|
|
|
// Limit the concurrently running workers to maxWorkers.
|
|
if op.Workers <= 0 || op.Workers > maxWorkers {
|
|
op.Workers = runtime.NumCPU()
|
|
}
|
|
|
|
// Process recursively the image files from the specified directory concurrently.
|
|
ch := make(chan result)
|
|
done := make(chan interface{})
|
|
defer close(done)
|
|
|
|
paths, errc := walkDir(done, op.Src, validExtensions)
|
|
|
|
wg.Add(op.Workers)
|
|
for i := 0; i < op.Workers; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
op.consumer(p, op.Dst, ch, done, paths)
|
|
}()
|
|
}
|
|
|
|
// Close the channel after the values are consumed.
|
|
go func() {
|
|
defer close(ch)
|
|
wg.Wait()
|
|
}()
|
|
|
|
// Consume the channel values.
|
|
for res := range ch {
|
|
if res.err != nil {
|
|
err = res.err
|
|
}
|
|
op.printOpStatus(res.path, err)
|
|
}
|
|
|
|
if err = <-errc; err != nil {
|
|
fmt.Fprintf(os.Stderr, utils.DecorateText(err.Error(), utils.ErrorMessage))
|
|
}
|
|
|
|
case mode.IsRegular() || mode&os.ModeNamedPipe != 0: // check for regular files or pipe names
|
|
ext := filepath.Ext(op.Dst)
|
|
if !isValidExtension(ext, validExtensions) && op.Dst != op.PipeName {
|
|
log.Fatalf(utils.DecorateText(fmt.Sprintf("%v file type not supported", ext), utils.ErrorMessage))
|
|
}
|
|
|
|
err = op.process(p, op.Src, op.Dst)
|
|
op.printOpStatus(op.Dst, err)
|
|
}
|
|
if err == nil {
|
|
fmt.Fprintf(os.Stderr, "\nExecution time: %s\n", utils.DecorateText(fmt.Sprintf("%s", utils.FormatTime(time.Since(now))), utils.SuccessMessage))
|
|
}
|
|
}
|
|
|
|
// consumer reads the path names from the paths channel and calls the resizing processor against the source image.
|
|
func (op *Ops) consumer(
|
|
p *Processor,
|
|
dest string,
|
|
res chan<- result,
|
|
done <-chan interface{},
|
|
paths <-chan string,
|
|
) {
|
|
for src := range paths {
|
|
dst := filepath.Join(dest, filepath.Base(src))
|
|
err := op.process(p, src, dst)
|
|
|
|
select {
|
|
case <-done:
|
|
return
|
|
case res <- result{
|
|
path: src,
|
|
err: err,
|
|
}:
|
|
}
|
|
}
|
|
}
|
|
|
|
// processor calls the resizer method over the source image and returns the error in case exists.
|
|
func (op *Ops) process(p *Processor, in, out string) error {
|
|
var (
|
|
successMsg string
|
|
errorMsg string
|
|
)
|
|
// Start the progress indicator.
|
|
p.Spinner.Start()
|
|
|
|
successMsg = fmt.Sprintf("%s %s %s",
|
|
utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
|
|
utils.DecorateText("⇢", utils.DefaultMessage),
|
|
utils.DecorateText("the image has been resized successfully ✔", utils.SuccessMessage),
|
|
)
|
|
|
|
errorMsg = fmt.Sprintf("%s %s %s",
|
|
utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
|
|
utils.DecorateText("resizing image failed...", utils.DefaultMessage),
|
|
utils.DecorateText("✘", utils.ErrorMessage),
|
|
)
|
|
|
|
src, dst, err := op.pathToFile(in, out)
|
|
if err != nil {
|
|
p.Spinner.StopMsg = errorMsg
|
|
return err
|
|
}
|
|
|
|
// Capture CTRL-C signal and restores back the cursor visibility.
|
|
signalChan := make(chan os.Signal, 1)
|
|
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
<-signalChan
|
|
func() {
|
|
p.Spinner.RestoreCursor()
|
|
os.Remove(dst.(*os.File).Name())
|
|
os.Exit(1)
|
|
}()
|
|
}()
|
|
|
|
defer func() {
|
|
if img, ok := src.(*os.File); ok {
|
|
if err := img.Close(); err != nil {
|
|
log.Printf("could not close the opened file: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
defer func() {
|
|
if img, ok := dst.(*os.File); ok {
|
|
if err := img.Close(); err != nil {
|
|
log.Printf("could not close the opened file: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = p.Process(src, dst)
|
|
if err != nil {
|
|
// remove the generated image file in case of an error
|
|
os.Remove(dst.(*os.File).Name())
|
|
|
|
p.Spinner.StopMsg = errorMsg
|
|
// Stop the progress indicator.
|
|
p.Spinner.Stop()
|
|
|
|
return err
|
|
} else {
|
|
p.Spinner.StopMsg = successMsg
|
|
// Stop the progress indicator.
|
|
p.Spinner.Stop()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// pathToFile converts the source and destination paths to readable and writable files.
|
|
func (op *Ops) pathToFile(in, out string) (io.Reader, io.Writer, error) {
|
|
var (
|
|
src io.Reader
|
|
dst io.Writer
|
|
err error
|
|
)
|
|
// Check if the source path is a local image or URL.
|
|
if utils.IsValidUrl(in) {
|
|
src = imgFile
|
|
} else {
|
|
// Check if the source is a pipe name or a regular file.
|
|
if in == op.PipeName {
|
|
if term.IsTerminal(int(os.Stdin.Fd())) {
|
|
return nil, nil, errors.New("`-` should be used with a pipe for stdin")
|
|
}
|
|
src = os.Stdin
|
|
} else {
|
|
src, err = os.Open(in)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("unable to open the source file: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if the destination is a pipe name or a regular file.
|
|
if out == op.PipeName {
|
|
if term.IsTerminal(int(os.Stdout.Fd())) {
|
|
return nil, nil, errors.New("`-` should be used with a pipe for stdout")
|
|
}
|
|
dst = os.Stdout
|
|
} else {
|
|
dst, err = os.OpenFile(out, os.O_CREATE|os.O_WRONLY, 0755)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("unable to create the destination file: %v", err)
|
|
}
|
|
}
|
|
return src, dst, nil
|
|
}
|
|
|
|
// printOpStatus displays the relevant information about the image resizing process.
|
|
func (op *Ops) printOpStatus(fname string, err error) {
|
|
if err != nil {
|
|
log.Fatalf(
|
|
utils.DecorateText("\nError resizing the image: %s", utils.ErrorMessage),
|
|
utils.DecorateText(fmt.Sprintf("\n\tReason: %v\n", err.Error()), utils.DefaultMessage),
|
|
)
|
|
} else {
|
|
if fname != op.PipeName {
|
|
fmt.Fprintf(os.Stderr, "\nThe image has been saved as: %s %s\n\n",
|
|
utils.DecorateText(filepath.Base(fname), utils.SuccessMessage),
|
|
utils.DefaultColor,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// walkDir starts a new goroutine to walk the specified directory tree
|
|
// in recursive manner and sends the path of each regular file to a new channel.
|
|
// It finishes in case the done channel is getting closed.
|
|
func walkDir(
|
|
done <-chan interface{},
|
|
src string,
|
|
srcExts []string,
|
|
) (<-chan string, <-chan error) {
|
|
pathChan := make(chan string)
|
|
errChan := make(chan error, 1)
|
|
|
|
go func() {
|
|
// Close the paths channel after Walk returns.
|
|
defer close(pathChan)
|
|
|
|
errChan <- filepath.Walk(src, func(path string, f os.FileInfo, err error) error {
|
|
isFileSupported := false
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !f.Mode().IsRegular() {
|
|
return nil
|
|
}
|
|
|
|
// Get the file base name.
|
|
fx := filepath.Ext(f.Name())
|
|
for _, ext := range srcExts {
|
|
if ext == fx {
|
|
isFileSupported = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if isFileSupported {
|
|
select {
|
|
case <-done:
|
|
return errors.New("directory walk cancelled")
|
|
case pathChan <- path:
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}()
|
|
return pathChan, errChan
|
|
}
|
|
|
|
// isValidExtension checks for the supported extensions.
|
|
func isValidExtension(ext string, extensions []string) bool {
|
|
for _, ex := range extensions {
|
|
if ex == ext {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|