├── .gitignore ├── .gitlab-ci.yml ├── README.md ├── glide.yaml ├── main.go ├── run ├── targets.go ├── targets_test.go ├── upload.go ├── upload_test.go ├── utils.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.png 3 | build/ 4 | vendor/ 5 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: golang:latest 2 | 3 | stages: 4 | - test 5 | - build 6 | - deploy 7 | 8 | before_script: 9 | - cd $CI_PROJECT_DIR 10 | - export GOBIN=$GOROOT/bin 11 | - chmod +x run 12 | - ./run prebuild 13 | 14 | test: 15 | stage: test 16 | except: [tags] 17 | script: [./run test] 18 | 19 | buildall: 20 | stage: build 21 | script: [./run buildall] 22 | artifacts: 23 | paths: [imgops*] 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImgOps 2 | 3 | This is a CLI tool for reverse searching images through ImgOps website. 4 | It supports files and URLs. 5 | 6 | ## How It Works 7 | 8 | 1. You upload a file or pass a URL to the tool 9 | 2. Depending on your choice, it will open search results from sites you want. 10 | These are Google, TinEye, Yandex, Bing, Reddit and few others 11 | If you don't provide a “target”, it will open ImgOps page with the provided image. 12 | 13 | ## Download 14 | 15 | [Click here](https://github.com/dogancelik/imgops/releases/latest) to download latest version. 16 | 17 | ## How To Use 18 | 19 | See `imgops -h` or check out [the Wiki](https://github.com/dogancelik/imgops/wiki/Examples). 20 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: . 2 | import: 3 | - package: github.com/PuerkitoBio/goquery 4 | - package: github.com/parnurzeal/gorequest 5 | - package: github.com/skratchdot/open-golang 6 | subpackages: 7 | - open 8 | - package: github.com/tj/go-debug 9 | - package: github.com/urfave/cli 10 | version: v1.20.0 11 | - package: github.com/eiannone/keyboard 12 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/eiannone/keyboard" 8 | "github.com/pkg/browser" 9 | "github.com/urfave/cli" 10 | ) 11 | 12 | import . "github.com/visionmedia/go-debug" 13 | 14 | var debug = Debug("imgops") 15 | 16 | var authors = []cli.Author{ 17 | { 18 | Name: "Doğan Çelik", 19 | Email: "dogancelik.com", 20 | }, 21 | } 22 | 23 | var Version string 24 | 25 | func cliSelect() string { 26 | err := keyboard.Open() 27 | if err != nil { 28 | panic(err) 29 | } 30 | defer keyboard.Close() 31 | 32 | fmt.Println(genSelectText(false)) 33 | ret := "" 34 | 35 | for { 36 | char, key, err := keyboard.GetKey() 37 | if err != nil { 38 | panic(err) 39 | } else if key == keyboard.KeyEsc { 40 | break 41 | } 42 | 43 | m := getKeyToNameTargets(availableTargets) 44 | target, mapOk := m[char] 45 | if mapOk { 46 | return target 47 | } else if char == 'i' { 48 | return defaultTarget 49 | } 50 | } 51 | 52 | return ret 53 | } 54 | 55 | func cliSearch(c *cli.Context) error { 56 | 57 | if c.NArg() == 0 { 58 | return cli.NewExitError("No file or URL is given", 1) 59 | } 60 | 61 | srcPath := c.Args().First() 62 | targets := c.String("targets") 63 | 64 | debug("Path: %s", srcPath) 65 | debug("Targets: %s", targets) 66 | 67 | var urls []string 68 | var errUpload error 69 | 70 | // Select flag 71 | if c.Bool("select") { 72 | targets = cliSelect() 73 | if targets == "" { 74 | return cli.NewExitError("Upload cancelled", 4) 75 | } 76 | } 77 | 78 | // Input flag 79 | if c.Bool("input") { 80 | fmt.Print(genSelectText(true)) 81 | fmt.Scanln(&targets) 82 | targets = initialsToTargets(targets) 83 | if targets == "" { 84 | return cli.NewExitError("No target is specified", 5) 85 | } 86 | } 87 | 88 | // Upload 89 | if isUrl(srcPath) == true { 90 | debug("Start URL upload") 91 | urls, errUpload = UploadURL(srcPath, targets) 92 | } else { 93 | if _, err := os.Stat(srcPath); os.IsNotExist(err) { 94 | return cli.NewExitError("File doesn't exist: "+srcPath, 2) 95 | } 96 | debug("Start file upload") 97 | urls, errUpload = UploadFile(srcPath, targets) 98 | } 99 | 100 | // Upload result 101 | if errUpload != nil && len(urls) == 0 { 102 | return cli.NewExitError("Error during upload: "+errUpload.Error(), 3) 103 | } else { 104 | if errUpload != nil { 105 | fmt.Fprintf(os.Stderr, "Unknown targets '%s', will open default page instead", targets) 106 | } 107 | 108 | for _, url := range urls { 109 | if c.Bool("return") { 110 | fmt.Println(url) 111 | } else { 112 | browser.OpenURL(url) 113 | } 114 | } 115 | } 116 | 117 | return nil 118 | } 119 | 120 | func cliMain(c *cli.Context) error { 121 | cli.ShowAppHelp(c) 122 | return nil 123 | } 124 | 125 | func main() { 126 | app := cli.NewApp() 127 | app.Name = "ImgOps CLI" 128 | app.Usage = "Reverse search images" 129 | app.Version = Version 130 | app.Authors = authors 131 | app.Action = cliMain 132 | app.Commands = []cli.Command{ 133 | { 134 | Name: "search", 135 | Aliases: []string{"a"}, 136 | Usage: "Search a file or a URL", 137 | Action: cliSearch, 138 | Flags: []cli.Flag{ 139 | cli.StringFlag{ 140 | Name: "targets, t", 141 | Value: defaultTarget, 142 | Usage: "Target website to search at (e.g. Google)", 143 | }, 144 | cli.BoolFlag{ 145 | Name: "select, s", 146 | Usage: "Show a list of targets to select from", 147 | }, 148 | cli.BoolFlag{ 149 | Name: "input, i", 150 | Usage: "Type the targets you want to open", 151 | }, 152 | cli.BoolFlag{ 153 | Name: "return, r", 154 | Usage: "Output the result URL", 155 | }, 156 | }, 157 | }, 158 | } 159 | app.Run(os.Args) 160 | } 161 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export COMMIT_HASH=`git rev-parse --short @` 4 | export COMMIT_DATE=`git log -1 --pretty=format:%cI | sed -r -e 's/T.*//' -e 's/-//g'` 5 | export BUILD_DIR="build/" 6 | 7 | function PREBUILD { 8 | go get ./... 9 | } 10 | 11 | function BUILD { 12 | echo "=== ${FUNCNAME[0]} ===" 13 | echo "GOOS: $GOOS" 14 | echo "GOARCH: $GOARCH" 15 | 16 | # If $GOOS or $GOARCH is emtpy, remove dashes 17 | OUT_FILE=`echo "imgops-${GOOS:-_}-${GOARCH:-_}" | sed s/-_//g` 18 | OUT_FILE+=`go env GOEXE` 19 | echo "Filename: $OUT_FILE" 20 | 21 | # Use $VERSION if empty use $CI_BUILD_TAG if empty use $CI_BUILD_REF 22 | VERSION=${VERSION:-${CI_BUILD_TAG:-#${CI_BUILD_REF:0:6}}} 23 | echo "Version: ${VERSION:-\$VERSION is empty}" 24 | 25 | go build -ldflags "-X main.Version=$VERSION" -o "$BUILD_DIR$OUT_FILE" 26 | } 27 | 28 | function TEST { 29 | echo "=== ${FUNCNAME[0]} ===" 30 | TEST_URL="https://encrypted.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" 31 | OUT_FILE="test.png" 32 | if [[ ! -f $OUT_FILE ]]; then 33 | wget -nv $TEST_URL -O $OUT_FILE 34 | fi 35 | go test 36 | } 37 | 38 | function BUILDALL { 39 | echo "=== ${FUNCNAME[0]} ===" 40 | OSES="windows linux darwin" 41 | for OS in $OSES; do 42 | export GOOS=$OS 43 | export GOARCH=amd64 44 | BUILD 45 | done 46 | } 47 | 48 | function ZIP { 49 | cd `dirname $1` 50 | ZIP_FILE=`basename $1 .exe`.zip 51 | ADD_FILE=`basename $1` 52 | 7za a -tzip $ZIP_FILE $ADD_FILE > /dev/null 2>&1 53 | echo SHA1: `sha1sum $ZIP_FILE` 54 | } 55 | 56 | function ZIPALL { 57 | export -f ZIP 58 | find $BUILD_DIR -iname "*" ! -iname "*.zip" -type f -exec bash -c 'ZIP "$1"' - {} \; 59 | } 60 | 61 | function HELP { 62 | echo -e "ImgOps Build Tool\nCommands: prebuild, build, test, buildall, zipall, help" 63 | } 64 | 65 | case $1 in 66 | prebuild) 67 | PREBUILD ;; 68 | build) 69 | BUILD ;; 70 | test) 71 | TEST ;; 72 | buildall) 73 | BUILDALL ;; 74 | zipall) 75 | ZIPALL ;; 76 | *) 77 | HELP ;; 78 | esac 79 | -------------------------------------------------------------------------------- /targets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Target struct { 4 | Name string 5 | Key rune 6 | Id string 7 | } 8 | 9 | var availableTargets = []Target{ 10 | { 11 | Name: "google", 12 | Key: 'g', 13 | Id: "#t85", 14 | }, 15 | { 16 | Name: "bing", 17 | Key: 'b', 18 | Id: "#t101", 19 | }, 20 | { 21 | Name: "tineye", 22 | Key: 't', 23 | Id: "#t11", 24 | }, 25 | { 26 | Name: "reddit", 27 | Key: 'r', 28 | Id: "#t97", 29 | }, 30 | { 31 | Name: "yandex", 32 | Key: 'y', 33 | Id: "#t72", 34 | }, 35 | { 36 | Name: "baidu", 37 | Key: 'a', 38 | Id: "#t74", 39 | }, 40 | { 41 | Name: "so", 42 | Key: 's', 43 | Id: "#t109", 44 | }, 45 | { 46 | Name: "sogou", 47 | Key: 'u', 48 | Id: "#t110", 49 | }, 50 | } 51 | 52 | func getKeyToNameTargets(targets []Target) map[rune]string { 53 | m := make(map[rune]string) 54 | 55 | for _, target := range targets { 56 | m[target.Key] = target.Name 57 | } 58 | 59 | return m 60 | } 61 | 62 | func getNameToIdTargets(targets []Target) map[string]string { 63 | m := make(map[string]string) 64 | 65 | for _, target := range targets { 66 | m[target.Name] = target.Id 67 | } 68 | 69 | return m 70 | } 71 | -------------------------------------------------------------------------------- /targets_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestKeyToId(t *testing.T) { 8 | var bing, id string 9 | var ok bool 10 | 11 | mx := getKeyToNameTargets(availableTargets) 12 | my := getNameToIdTargets(availableTargets) 13 | 14 | if bing, ok = mx['b']; !ok && (bing != "bing") { 15 | t.Error("'b' key doesn't refer to 'bing'") 16 | } 17 | 18 | if id, ok = my[bing]; id != "#t101" { 19 | t.Error("'bing' ID is not correct") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/parnurzeal/gorequest" 10 | ) 11 | 12 | const uploadUrl string = "https://imgops.com/store" 13 | const uploadSearch string = "userUploadTempCache" 14 | const defaultTarget = "imgops" 15 | 16 | var defaultAction bool 17 | var finalUrl string 18 | 19 | func setDefaultAction(targetAction string) { 20 | if targetAction != defaultTarget { 21 | defaultAction = false 22 | } else { 23 | defaultAction = true 24 | } 25 | } 26 | 27 | func UploadURL(targetUrl string, targetAction string) ([]string, error) { 28 | setDefaultAction(targetAction) 29 | newUrl := "https://imgops.com/" + targetUrl 30 | finalUrl = newUrl 31 | 32 | if defaultAction { 33 | return strings.Fields(newUrl), nil 34 | } else { 35 | _, body, errs := gorequest.New().Get(newUrl).End() 36 | if errs != nil { 37 | return strings.Fields(""), errors.New("GET error") 38 | } else { 39 | return findHref(body, targetAction, finalUrl) 40 | } 41 | } 42 | } 43 | 44 | func redirectPolicy(req gorequest.Request, via []gorequest.Request) error { 45 | finalUrl = req.URL.String() 46 | if defaultAction { 47 | return errors.New("Stop redirection") 48 | } else { 49 | return nil 50 | } 51 | } 52 | 53 | func UploadFile(targetPath string, targetAction string) ([]string, error) { 54 | setDefaultAction(targetAction) 55 | 56 | debug("Read file from path") 57 | bytes, _ := ioutil.ReadFile(targetPath) 58 | 59 | debug("Make a POST request") 60 | _, body, errs := gorequest.New(). 61 | Post(uploadUrl). 62 | RedirectPolicy(redirectPolicy). 63 | Type("multipart"). 64 | SendFile(bytes, filepath.Base(targetPath), "photo"). 65 | End() 66 | debug("POST request errors: %v", errs) 67 | 68 | debug("Final URL: %s", finalUrl) 69 | debug("Stop redirection: %b", defaultAction) 70 | if defaultAction { 71 | if len(finalUrl) > 0 { 72 | return strings.Fields(finalUrl), nil 73 | } else { 74 | return strings.Fields(""), errors.New("Could not get final URL after Redirect") 75 | } 76 | } else { 77 | return findHref(body, targetAction, finalUrl) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /upload_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | const testUrl string = "https://encrypted.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png" 9 | const testFile string = "test.png" 10 | 11 | func TestFileUpload(t *testing.T) { 12 | var err error 13 | 14 | if _, err = UploadFile(testFile, defaultTarget); err != nil { 15 | t.Error(err) 16 | } 17 | 18 | if _, err = UploadFile(testFile, "google"); err != nil { 19 | t.Error(err) 20 | } 21 | 22 | if _, err = UploadURL(testFile, "wrong"); !strings.Contains(err.Error(), "No link") { 23 | t.Error(err) 24 | } 25 | } 26 | 27 | func TestUrlUpload(t *testing.T) { 28 | var err error 29 | 30 | if _, err = UploadURL(testUrl, defaultTarget); err != nil { 31 | t.Error(err) 32 | } 33 | 34 | if _, err = UploadURL(testUrl, "google, yandex"); err != nil { 35 | t.Error(err) 36 | } 37 | 38 | if _, err = UploadURL(testUrl, "wrong"); !strings.Contains(err.Error(), "No link") { 39 | t.Error(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/PuerkitoBio/goquery" 9 | ) 10 | 11 | func isUrl(targetPath string) bool { 12 | return strings.Contains(targetPath, "http:") || strings.Contains(targetPath, "https:") 13 | } 14 | 15 | func findHref(document, targetStr, finalUrl string) ([]string, error) { 16 | doc, err := goquery.NewDocumentFromReader(strings.NewReader(document)) 17 | if err != nil { 18 | return strings.Fields(""), err 19 | } else { 20 | queryList := getQueryList(targetStr) 21 | debug("Queries: %v", queryList) 22 | foundUrls := make([]string, 0, len(queryList)) 23 | 24 | for _, query := range queryList { 25 | href, attrOk := doc.Find(query).Attr("href") 26 | debug("Get href from query '%s': %v", query, attrOk) 27 | 28 | if attrOk { 29 | foundUrls = append(foundUrls, href) 30 | } 31 | } 32 | 33 | if len(foundUrls) > 0 { 34 | return foundUrls, nil 35 | } else { 36 | return strings.Fields(finalUrl), errors.New(fmt.Sprintf("No link is found in targets: %s", queryList)) 37 | } 38 | } 39 | } 40 | 41 | // Returns queries of found targets 42 | func getQueryList(s string) []string { 43 | split := strings.Split(s, ",") 44 | m := getNameToIdTargets(availableTargets) 45 | 46 | found := make([]string, 0, len(split)) 47 | for _, val := range split { 48 | key := strings.TrimSpace(val) 49 | query, mapOk := m[key] 50 | if mapOk { 51 | debug("Query for '%s' is found: %s", key, query) 52 | found = append(found, query) 53 | } 54 | } 55 | 56 | return found 57 | } 58 | 59 | // Create a nice select list 60 | 61 | func genSelectText(inputMode bool) string { 62 | text := "" 63 | 64 | if inputMode { 65 | text += "Available targets:\n\n" 66 | } else { 67 | text += "Select a target:\n\n" 68 | } 69 | 70 | text += " > [i]mgops\n" 71 | for _, target := range availableTargets { 72 | text += " > " + strings.Replace(target.Name, string(target.Key), "["+string(target.Key)+"]", 1) + "\n" 73 | } 74 | 75 | if inputMode { 76 | text += "\nType initials: " 77 | } else { 78 | text += "\n(Press ESC to cancel)" 79 | } 80 | 81 | return text 82 | } 83 | 84 | // Initials to targets with comma 85 | 86 | func initialsToTargets(initials string) string { 87 | targets := "" 88 | m := getKeyToNameTargets(availableTargets) 89 | 90 | for i := range initials { 91 | if val, ok := m[rune(initials[i])]; ok { 92 | targets += val 93 | if len(initials)-1 != i { 94 | targets += "," 95 | } 96 | } 97 | } 98 | 99 | return targets 100 | } 101 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTargetSplit(t *testing.T) { 8 | queryList := getQueryList("google") 9 | if len(queryList) != 1 { 10 | t.Errorf("Expected 1 query: %v", queryList) 11 | } 12 | 13 | queryList = getQueryList("google, yandex, wrong") 14 | if len(queryList) != 2 { 15 | t.Error("Expected 2 queries") 16 | } 17 | } 18 | --------------------------------------------------------------------------------