├── .gitignore
├── Gemfile
├── LICENSE.md
├── README.md
├── Rakefile
├── bin
└── imgurr
├── imgurr.gemspec
├── lib
├── imgurr.rb
└── imgurr
│ ├── color.rb
│ ├── command.rb
│ ├── imgurAPI.rb
│ ├── imgurErrors.rb
│ ├── numbers.rb
│ ├── platform.rb
│ └── storage.rb
└── test
├── 1-upload.sh
├── 2-info.sh
├── 3-delete.sh
├── 4-misc.sh
├── cli.sh
├── error.sh
├── files
├── github-logo.png
├── habs.png
└── imgurr-old.json
├── roundup
└── run
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | coverage
6 | InstalledFiles
7 | lib/bundler/man
8 | pkg
9 | rdoc
10 | spec/reports
11 | test/tmp
12 | test/version_tmp
13 | tmp
14 | Gemfile.lock
15 |
16 | # YARD artifacts
17 | .yardoc
18 | _yardoc
19 | doc/
20 |
21 | # Rubymine Files
22 | .idea/
23 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 | gemspec
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) Christophe Naud-Dulude, https://github.com/Chris911/
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | imgurr [](http://badge.fury.io/rb/imgurr)
2 | =======
3 | Command line utility for Imgur in Ruby. Imgurr lets you quickly upload images, get info about an image and delete your own images from Imgur.
4 |
5 | ## Install
6 | gem install imgurr
7 |
8 | ## Usage
9 |
10 | 
11 | #### Examples
12 | $ imgurr capture
13 | Uploading screenshot...
14 | Copied http://i.imgur.com/rGoGCNb.png to clipboard
15 |
16 | $ imgurr upload image.gif
17 | Copied http://i.imgur.com/PLWGJlc.gif to clipboard
18 |
19 | $ imgurr upload image.jpg --markdown
20 | Copied  to clipboard
21 |
22 | $ imgurr info 2KxrTAK
23 | Image ID : 2KxrTAK
24 | Views : 14717
25 | Bandwidth : 2.296 GiB
26 | Title : None
27 | Desc : None
28 | Animated : false
29 | Width : 960 px
30 | Height : 540 px
31 | Link : http://i.imgur.com/2KxrTAK.jpg
32 |
33 | $ imgurr delete http://i.imgur.com/2KxrTAK.jpg
34 | Successfully deleted image from Imgur
35 |
36 | ./imgurr --help for more.
37 |
38 | ## How it works
39 | Imgurr stores the delete hash generated by Imgur locally when uploading an image using imgurr. Most of the time the delete hash gets lost if you don't have an account and you want to takedown an image you have to contact Imgur support which can take a while. With imgurr all your delete hashes are saved so you can delete your images later if needed.
40 |
41 |
42 | ## Contributing
43 |
44 | 1. Fork it
45 | 2. Create your feature branch (`git checkout -b my-new-feature`)
46 | 3. Commit your changes (`git commit -am 'Add some feature'`)
47 | 4. Push to the branch (`git push origin my-new-feature`)
48 | 5. Create new Pull Request
49 |
50 | Don't forget to add a test for any new feature.
51 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake'
3 | require 'date'
4 |
5 | def name
6 | @name ||= Dir['*.gemspec'].first.split('.').first
7 | end
8 |
9 | #############################################################################
10 | #
11 | # Tests
12 | #
13 | #############################################################################
14 |
15 | task :default => :test
16 |
17 | desc "Run tests for #{name}"
18 | task :test do
19 | exec "test/run"
20 | end
21 |
22 | desc "Open an irb session preloaded with this library"
23 | task :console do
24 | sh "irb -rubygems -r ./lib/#{name}.rb"
25 | end
--------------------------------------------------------------------------------
/bin/imgurr:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # coding: utf-8
3 |
4 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
5 |
6 | require 'imgurr'
7 |
8 | Imgurr::Command.execute(*ARGV)
--------------------------------------------------------------------------------
/imgurr.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |s|
2 | s.name = 'imgurr'
3 | s.version = '1.0.0'
4 | s.summary = "Imgurr lets you upload images to imgur from the command line"
5 | s.description = "Imgurr is a ruby gem that lets you upload images to Imgur and manage your account"
6 | s.authors = ["Christophe Naud-Dulude"]
7 | s.email = 'christophe.naud.dulude@gmail.com'
8 | s.homepage = 'https://github.com/Chris911/imgurr'
9 |
10 | s.require_paths = %w[lib]
11 | ## If your gem includes any executables, list them here.
12 | s.executables = ["imgurr"]
13 | s.default_executable = 'imgurr'
14 |
15 | s.add_dependency('json')
16 |
17 | s.add_development_dependency('rake', "~> 0.9.2")
18 |
19 | ## Specify any RDoc options here. You'll want to add your README and
20 | ## LICENSE files to the extra_rdoc_files list.
21 | s.rdoc_options = ["--charset=UTF-8"]
22 | s.extra_rdoc_files = %w[README.md LICENSE.md]
23 |
24 | s.license = 'MIT'
25 |
26 | s.files = %w( README.md LICENSE.md Gemfile imgurr.gemspec Rakefile)
27 | s.files += Dir.glob("lib/**/*")
28 | s.files += Dir.glob("bin/**/*")
29 | s.files += Dir.glob("test/**/*")
30 | end
31 |
--------------------------------------------------------------------------------
/lib/imgurr.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | begin
4 | require 'rubygems'
5 | rescue LoadError
6 | end
7 |
8 | # External require
9 | require 'net/http'
10 | require 'net/https'
11 | require 'open-uri'
12 | require 'json'
13 | require 'optparse'
14 |
15 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
16 |
17 | # Internal require
18 | require 'imgurr/color'
19 | require 'imgurr/numbers'
20 | require 'imgurr/imgurAPI'
21 | require 'imgurr/storage'
22 | require 'imgurr/platform'
23 | require 'imgurr/imgurErrors'
24 | require 'imgurr/command'
25 |
26 | module Imgurr
27 | VERSION = '0.2.0'
28 | DEBUG = false
29 |
30 | def self.storage
31 | @storage ||= Storage.new
32 | end
33 |
34 | def self.options
35 | @options ||= {}
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/imgurr/color.rb:
--------------------------------------------------------------------------------
1 | module Imgurr
2 | # Color collects some methods for colorizing terminal output.
3 | # Thanks to https://github.com/holman
4 | module Color
5 | extend self
6 |
7 | CODES = {
8 | :reset => "\e[0m",
9 |
10 | :cyan => "\e[36m",
11 | :magenta => "\e[35m",
12 | :red => "\e[31m",
13 | :yellow => "\e[33m"
14 | }
15 |
16 | # Tries to enable Windows support if on that platform.
17 | #
18 | # Returns nothing.
19 | def self.included(other)
20 | if RUBY_PLATFORM =~ /win32/ || RUBY_PLATFORM =~ /mingw32/
21 | require 'Win32/Console/ANSI'
22 | end
23 | rescue LoadError
24 | # Oh well, we tried.
25 | end
26 |
27 | # Wraps the given string in ANSI color codes
28 | #
29 | # string - The String to wrap.
30 | # color_code - The String representing he ANSI color code
31 | #
32 | # Examples
33 | #
34 | # colorize("Boom!", :magenta)
35 | # # => "\e[35mBoom!\e[0m"
36 | #
37 | # Returns the wrapped String unless the the platform is windows and
38 | # does not have Win32::Console, in which case, returns the String.
39 | def colorize(string, color_code)
40 | if !defined?(Win32::Console) && !!(RUBY_PLATFORM =~ /win32/ || RUBY_PLATFORM =~ /mingw32/)
41 | # looks like this person doesn't have Win32::Console and is on windows
42 | # just return the uncolorized string
43 | return string
44 | end
45 | "#{CODES[color_code] || color_code}#{string}#{CODES[:reset]}"
46 | end
47 |
48 | # Set up shortcut methods to all the codes define in CODES.
49 | self.class_eval(CODES.keys.reject {|color| color == :reset }.map do |color|
50 | "def #{color}(string); colorize(string, :#{color}); end"
51 | end.join("\n"))
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/imgurr/command.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | # Command is the main point of entry for boom commands; shell arguments are
4 | # passed through to Command, which then filters and parses through individual
5 | # commands and reroutes them to constituent object classes.
6 | #
7 | # Highly inspired by @holman/boom
8 | #
9 |
10 | module Imgurr
11 | class Command
12 | class << self
13 |
14 | #include Imgurr::Color
15 |
16 | # Public: executes a command.
17 | #
18 | # args - The actual commands to operate on. Can be as few as zero
19 | # arguments or as many as three.
20 | def execute(*args)
21 | command = args.shift
22 | major = args.shift
23 | minor = args.empty? ? nil : args.join(' ')
24 |
25 | return help unless command
26 | parse_options
27 | delegate(command, major, minor)
28 | end
29 |
30 | # Public: Parse extra options
31 | #
32 | # returns nothing
33 | def parse_options
34 | options[:markdown] = false
35 | o = OptionParser.new do |opts|
36 | opts.on('-m', '--markdown', 'Use Markdown Syntax') do
37 | options[:markdown] = true
38 | end
39 | opts.on('-l', '--html', 'Use HTML Syntax') do
40 | options[:html] = true
41 | end
42 | opts.on('-s', '--size PERCENTAGE', 'Image Size') do |value|
43 | options[:size] = value
44 | end
45 | opts.on('-t', '--title TITLE', 'Image Title') do |value|
46 | options[:title] = value
47 | end
48 | opts.on('-d', '--desc DESC', 'Image Title') do |value|
49 | options[:desc] = value
50 | end
51 | opts.on('-v', '--version', 'Print Version') do
52 | version
53 | quit
54 | end
55 | opts.on('-h', '--help', 'Print Help') do
56 | help
57 | quit
58 | end
59 | end
60 | begin
61 | o.parse!
62 | rescue OptionParser::MissingArgument => e
63 | puts "Error: #{e.message}"
64 | quit
65 | rescue OptionParser::InvalidOption => e
66 | puts "Error: #{e.message}"
67 | quit
68 | end
69 | end
70 |
71 | # Public: gets $stdin.
72 | #
73 | # Returns the $stdin object. This method exists to help with easy mocking
74 | # or overriding.
75 | def stdin
76 | $stdin
77 | end
78 |
79 | # Public: accesses the in-memory JSON representation.
80 | #
81 | # Returns a Storage instance.
82 | def storage
83 | Imgurr.storage
84 | end
85 |
86 | # Public: accesses the global options
87 | #
88 | # Returns Options dictionary
89 | def options
90 | Imgurr.options
91 | end
92 |
93 | # Public: allows main access to most commands.
94 | #
95 | # Returns output based on method calls.
96 | def delegate(command, major, minor)
97 | return help unless command
98 | return no_internet unless self.internet_connection?
99 | return capture if command == 'capture' || command == 'cap'
100 |
101 | if major
102 | return upload(major) if command == 'upload' || command == 'up' || command == 'u'
103 |
104 | # Get image ID from URL
105 | if major =~ /.*imgur\.com\/[a-zA-Z0-9]*\.[a-zA-Z]*/
106 | major = /com\/[a-zA-Z0-9]*/.match(major).to_s.gsub('com/','')
107 | end
108 |
109 | return unless valid_id major
110 | return info(major) if command == 'info' || command == 'i'
111 | return delete(major,minor) if command == 'delete' || command == 'd'
112 | else
113 | return list if command == 'list' || command == 'l'
114 |
115 | puts "Argument required for commmand #{command}."
116 | puts "imgurr --help for more information."
117 | return
118 | end
119 | end
120 |
121 | # Public: Upload an image to Imgur
122 | #
123 | # Returns nothing
124 | def upload(major)
125 | unless File.exist?(major)
126 | puts "File #{major} not found."
127 | return
128 | end
129 | major = File.absolute_path(major)
130 | response, success = ImgurAPI.upload(major)
131 | puts response unless success
132 | if success
133 | response = "![#{options[:title].nil? ? 'Screenshot' : options[:title]}](#{response})" if options[:markdown]
134 | response = build_HTML_response(response) if options[:html]
135 | puts "Copied #{Platform.copy(response)} to clipboard"
136 | end
137 | storage.save
138 | end
139 |
140 | # Public: Capture and image and upload to imgur
141 | #
142 | # Note: Only supported on OS X for now
143 | # Returns nothing
144 | def capture
145 | unless Platform.darwin?
146 | puts "Capture command is only supported on OS X for the time being."
147 | return
148 | end
149 |
150 | image_path = "#{ENV['HOME']}/.imgurr.temp.png"
151 | Platform.capture('-W', image_path)
152 |
153 | # User might have canceled or it takes some time to write to disk.
154 | # Check up to 3 times with 1 sec delay
155 | 3.times do
156 | if File.exist?(image_path)
157 | puts "Uploading screenshot..."
158 | upload(image_path)
159 | File.delete(image_path)
160 | break
161 | end
162 | sleep(1)
163 | end
164 | end
165 |
166 | # Public: List uploaded images
167 | #
168 | # Returns nothing
169 | def list
170 | items = storage.items
171 | if items.empty?
172 | puts 'No items in the list.'
173 | return
174 | end
175 |
176 | storage.items.each do |(id, data)|
177 | puts "#{id} #{data[:stamp]} #{data[:source].ljust(48)}"
178 | end
179 | end
180 |
181 | # Public: build HTML image tag response
182 | #
183 | # Returns a properly formatted HTML
tag
184 | def build_HTML_response(response)
185 | return "
" if options[:size].nil?
186 |
187 | "
"
188 | end
189 |
190 | # Public: Get image info
191 | #
192 | # Returns nothing
193 | def info(major)
194 | response = ImgurAPI.get_info(major)
195 | puts response
196 | end
197 |
198 | # Public: Delete image from imgur
199 | #
200 | # Returns nothing
201 | def delete(major,minor)
202 | major = major.to_sym
203 | if minor
204 | delete_hash = minor
205 | else
206 | if storage.hash_exists?(major)
207 | delete_hash = storage.find(major)
208 | else
209 | puts 'Delete hash not found in storage.'
210 | puts 'Use: imgurr delete '
211 | return
212 | end
213 | end
214 | if ImgurAPI.delete(delete_hash)
215 | puts 'Successfully deleted image from Imgur'
216 | storage.delete(major)
217 | storage.save
218 | else
219 | puts 'Unauthorized Access. Wrong delete hash?'
220 | end
221 | end
222 |
223 | # Public: the version of boom that you're currently running.
224 | #
225 | # Returns a String identifying the version number.
226 | def version
227 | puts "You're running imgurr #{Imgurr::VERSION}."
228 | end
229 |
230 | # Public: Checks is there's an active internet connection
231 | #
232 | # Returns true or false
233 | def internet_connection?
234 | begin
235 | true if open("http://www.google.com/")
236 | rescue
237 | false
238 | end
239 | end
240 |
241 | # Public: No internet error
242 | #
243 | # Returns nothing
244 | def no_internet
245 | puts 'An Internet connection is required to use this command.'
246 | end
247 |
248 | # Public: Quit / Exit program
249 | #
250 | # Returns nothing
251 | def quit
252 | exit(1)
253 | end
254 |
255 | # Public: Validate id (major)
256 | #
257 | # Returns true if valid id
258 | def valid_id(major)
259 | unless major =~ /^[a-zA-Z0-9]*$/
260 | puts "#{major} is not a valid imgur ID or URL"
261 | return false
262 | end
263 | return true
264 | end
265 |
266 | # Public: prints all the commands of boom.
267 | #
268 | # Returns nothing.
269 | def help
270 | text = '
271 | - imgurr: help ---------------------------------------------------
272 |
273 | imgurr --help This help text
274 | imgurr --version Print current version
275 |
276 | imgurr upload Upload image and copy link to clipboard
277 | imgurr upload [-m | --markdown] Upload image and copy link to clipboard with markdown syntax
278 | [-l | --html] Upload image and copy link to clipboard with HTML syntax
279 | [--size=SIZE] Set image size ratio
280 | [--tile="Title"] Set image title
281 | [--desc="Desc" ] Set image description
282 | imgurr capture Capture a screenshot and upload it (OS X only)
283 | imgurr list List uploaded images
284 | imgurr info Print image information
285 | imgurr delete Deletes an image from imgur if the deletehash is found locally
286 | imgurr delete Deletes an image from imgur with the provided deletehash
287 |
288 | all other documentation is located at:
289 | https://github.com/Chris911/imgurr
290 | '.gsub(/^ {8}/, '') # strip the first eight spaces of every line
291 |
292 | puts text
293 | end
294 |
295 | end
296 | end
297 | end
298 |
--------------------------------------------------------------------------------
/lib/imgurr/imgurAPI.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | #
4 | # Interface for the Imgur API
5 | #
6 |
7 | module Imgurr
8 | class ImgurAPI
9 | class << self
10 | API_URI = URI.parse('https://api.imgur.com')
11 | API_PUBLIC_KEY = 'Client-ID 70ff50b8dfc3a53'
12 |
13 | ENDPOINTS = {
14 | :image => '/3/image/',
15 | :gallery => '/3/gallery/'
16 | }
17 |
18 | # Public: accesses the in-memory JSON representation.
19 | #
20 | # Returns a Storage instance.
21 | def storage
22 | Imgurr.storage
23 | end
24 |
25 | # Public: accesses the global options
26 | #
27 | # Returns Options dictionary
28 | def options
29 | Imgurr.options
30 | end
31 |
32 | # HTTP Client used for API requests
33 | # TODO: Confirm SSL Certificate
34 | def web_client
35 | http = Net::HTTP.new(API_URI.host, API_URI.port)
36 | http.use_ssl = true
37 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE
38 | http
39 | end
40 |
41 | # Public: Upload an image
42 | #
43 | # args - The image path for the image to upload
44 | #
45 | def upload(image_path)
46 | params = {:image => File.read(image_path)}
47 | params[:title] = options[:title] unless options[:title].nil?
48 | params[:description] = options[:desc] unless options[:desc].nil?
49 | request = Net::HTTP::Post.new(API_URI.request_uri + ENDPOINTS[:image])
50 | request.set_form_data(params)
51 | request.add_field('Authorization', API_PUBLIC_KEY)
52 |
53 | response = web_client.request(request)
54 | handle_upload_response(response.body, image_path)
55 | end
56 |
57 | # Public: Get info about an image
58 | #
59 | # args - The image imgur id
60 | #
61 | def get_info(image_id)
62 | request = Net::HTTP::Get.new(API_URI.request_uri + ENDPOINTS[:image] + image_id)
63 | request.add_field('Authorization', API_PUBLIC_KEY)
64 |
65 | response = web_client.request(request)
66 | handle_info_response(response.body)
67 | end
68 |
69 | # Public: Upload an image
70 | #
71 | # args - The image path for the image to upload
72 | #
73 | def delete(delete_hash)
74 | request = Net::HTTP::Delete.new(API_URI.request_uri + ENDPOINTS[:image] + delete_hash)
75 | request.add_field('Authorization', API_PUBLIC_KEY)
76 |
77 | response = web_client.request(request)
78 | handle_delete_response(response.body)
79 | end
80 |
81 | # Public: Handle API Response: Uploaded Image
82 | #
83 | # args - Response data
84 | #
85 | def handle_upload_response(response, source_path)
86 | data = JSON.parse(response)
87 | puts JSON.pretty_unparse(data) if Imgurr::DEBUG
88 | if data['success']
89 | storage.add_hash(data['data']['id'], data['data']['deletehash'], source_path)
90 | return [data['data']['link'], true]
91 | end
92 | [ImgurErrors.handle_error(response), false]
93 | end
94 |
95 | # Public: Handle API Response: Get image Info
96 | #
97 | # args - Response data
98 | #
99 | def handle_info_response(response)
100 | data = JSON.parse(response)
101 | puts JSON.pretty_unparse(data) if Imgurr::DEBUG
102 | if data['success']
103 | return "
104 | Image ID : #{data['data']['id']}
105 | Views : #{data['data']['views']}
106 | Bandwidth : #{Numbers.to_human(data['data']['bandwidth'])}
107 | Title : #{data['data']['title'].nil? ? 'None' : data['data']['title']}
108 | Desc : #{data['data']['description'].nil? ? 'None' : data['data']['description']}
109 | Animated : #{data['data']['animated']}
110 | Width : #{data['data']['width']} px
111 | Height : #{data['data']['height']} px
112 | Link : #{data['data']['link']}
113 | ".gsub(/^ {8}/, '')
114 | end
115 | ImgurErrors.handle_error(response)
116 | end
117 |
118 | # Public: Handle API Response: Delete Image
119 | #
120 | # args - Response data
121 | #
122 | def handle_delete_response(response)
123 | data = JSON.parse(response)
124 | puts JSON.pretty_unparse(data) if Imgurr::DEBUG
125 | data['success']
126 | end
127 |
128 | end
129 | end
130 | end
131 |
--------------------------------------------------------------------------------
/lib/imgurr/imgurErrors.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | #
4 | # Interface for the Imgur API
5 | #
6 |
7 | module Imgurr
8 | class ImgurErrors
9 | class << self
10 |
11 | def handle_error(response)
12 | data = JSON.parse(response)
13 | "Imgur Error: #{data['data']['error']} (#{data['status']})"
14 | end
15 |
16 | end
17 | end
18 | end
--------------------------------------------------------------------------------
/lib/imgurr/numbers.rb:
--------------------------------------------------------------------------------
1 | module Imgurr
2 | class Numbers
3 | class << self
4 | def to_human(number)
5 | units = %W(B KiB MiB GiB TiB)
6 |
7 | size, unit = units.reduce(number.to_f) do |(fsize, _), utype|
8 | fsize > 512 ? [fsize / 1024, utype] : (break [fsize, utype])
9 | end
10 | return "#{"%.3f" % size} #{unit}"
11 | end
12 | end
13 | end
14 | end
--------------------------------------------------------------------------------
/lib/imgurr/platform.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | # Platform is a centralized point to shell out platform specific functionality
4 | # like clipboard access or commands to open URLs.
5 | #
6 | #
7 | # Clipboard is a centralized point to shell out to each individual platform's
8 | # clipboard, pasteboard, or whatever they decide to call it.
9 | #
10 | # Source: https://github.com/holman/boom
11 | #
12 | module Imgurr
13 | class Platform
14 | class << self
15 | # Public: tests if currently running on darwin.
16 | #
17 | # Returns true if running on darwin (MacOS X), else false
18 | def darwin?
19 | !!(RUBY_PLATFORM =~ /darwin/)
20 | end
21 |
22 | # Public: tests if currently running on windows.
23 | #
24 | # Apparently Windows RUBY_PLATFORM can be 'win32' or 'mingw32'
25 | #
26 | # Returns true if running on windows (win32/mingw32), else false
27 | def windows?
28 | !!(RUBY_PLATFORM =~ /mswin|mingw/)
29 | end
30 |
31 | # Public: returns the command used to open a file or URL
32 | # for the current platform.
33 | #
34 | # Currently only supports MacOS X and Linux with `xdg-open`.
35 | #
36 | # Returns a String with the bin
37 | def open_command
38 | if darwin?
39 | 'open'
40 | elsif windows?
41 | 'start'
42 | else
43 | 'xdg-open'
44 | end
45 | end
46 |
47 | # Public: opens a given URL in the browser. This
48 | # method is designed to handle multiple platforms.
49 | #
50 | # Returns nothing
51 | def open(url)
52 | unless windows?
53 | system("#{open_command} '#{url.gsub("\'","'\\\\''")}'")
54 | else
55 | system("#{open_command} #{url.gsub("\'","'\\\\''")}")
56 | end
57 | end
58 |
59 | # Public: returns the command used to copy a given Item's value to the
60 | # clipboard for the current platform.
61 | #
62 | # Returns a String with the bin
63 | def copy_command
64 | if darwin?
65 | 'pbcopy'
66 | elsif windows?
67 | 'clip'
68 | else
69 | 'xclip -selection clipboard'
70 | end
71 | end
72 |
73 | # Public: copies a given URL value to the clipboard. This method is
74 | # designed to handle multiple platforms.
75 | #
76 | # Returns nothing
77 | def copy(url)
78 | IO.popen(copy_command,"w") {|cc| cc.write(url)}
79 | url
80 | end
81 |
82 | # Public: returns the command used to capture a screenshot
83 | #
84 | def capture_command
85 | if darwin?
86 | 'screencapture'
87 | end
88 | end
89 |
90 | # Public: captures a screenshot and saves to `output`
91 | #
92 | def capture(args, output)
93 | system("#{capture_command} #{args} #{output}")
94 | end
95 |
96 | # Public: opens the JSON file in an editor for you to edit. Uses the
97 | # $EDITOR environment variable, or %EDITOR% on Windows for editing.
98 | # This method is designed to handle multiple platforms.
99 | # If $EDITOR is nil, try to open using the open_command.
100 | #
101 | # Returns a String with a helpful message.
102 | def edit(json_file)
103 | unless $EDITOR.nil?
104 | unless windows?
105 | system("`echo $EDITOR` #{json_file} &")
106 | else
107 | system("start %EDITOR% #{json_file}")
108 | end
109 | else
110 | system("#{open_command} #{json_file}")
111 | end
112 |
113 | 'Make your edits, and do be sure to save.'
114 | end
115 | end
116 | end
117 | end
118 |
--------------------------------------------------------------------------------
/lib/imgurr/storage.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Storage is the interface between multiple Backends. You can use Storage
3 | # directly without having to worry about which Backend is in use.
4 | #
5 | module Imgurr
6 | class Storage
7 | JSON_FILE = "#{ENV['HOME']}/.imgurr"
8 |
9 | # Public: the path to the Json file used by imgurr.
10 | #
11 | # ENV['IMGURRFILE'] is mostly used for tests
12 | #
13 | # Returns the String path of imgurr's Json representation.
14 | def json_file
15 | ENV['IMGURRFILE'] || JSON_FILE
16 | end
17 |
18 | # Public: initializes a Storage instance by loading in your persisted data from adapter.
19 | #
20 | # Returns the Storage instance.
21 | def initialize
22 | @hashes = Hash.new
23 | bootstrap
24 | populate
25 | end
26 |
27 | # Public: the in-memory collection of all Lists attached to this Storage
28 | # instance.
29 | #
30 | # lists - an Array of individual List items
31 | #
32 | # Returns nothing.
33 | attr_writer :hashes
34 |
35 | # Public: Adds a deletehash to the hashes list
36 | #
37 | # id - Image ID
38 | # hash - Delete hash
39 | #
40 | # Returns nothing
41 | def add_hash(id, hash, source)
42 | @hashes[id] = {:deletehash => hash, :source => source, :stamp => Time.now}
43 | end
44 |
45 | # Public: test whether out storage contains the delete hash for given id
46 | #
47 | # id - ID of the image
48 | #
49 | # Returns true if found, false if not.
50 | def hash_exists?(id)
51 | @hashes.has_key? id
52 | end
53 |
54 | # Public: finds any given delete_hash by id.
55 | #
56 | # name - String name of the list to search for
57 | #
58 | # Returns the first instance of delete_hash that it finds.
59 | def find(id)
60 | hash = @hashes[id]
61 | hash ? hash[:deletehash] : nil
62 | end
63 |
64 | # Public: all Items in storage sorted in chronological order.
65 | #
66 | # Returns an Array of all Items.
67 | def items
68 | @hashes.to_a.sort {|(_, a), (_, b)| a[:stamp] <=> b[:stamp] }
69 | end
70 |
71 | # Public: delete an Item entry from storage.
72 | #
73 | # Returns the deleted Item or nil.
74 | def delete(id)
75 | @hashes.delete(id)
76 | end
77 |
78 | # Public: creates a Hash of the representation of the in-memory data
79 | # structure. This percolates down to Items by calling to_hash on the List,
80 | # which in tern calls to_hash on individual Items.
81 | #
82 | # Returns a Hash of the entire data set.
83 | def to_hash
84 | {:hashes => @hashes}
85 | end
86 |
87 | # Takes care of bootstrapping the Json file, both in terms of creating the
88 | # file and in terms of creating a skeleton Json schema.
89 | #
90 | # Return true if successfully saved.
91 | def bootstrap
92 | return if File.exist?(json_file)
93 | FileUtils.touch json_file
94 | File.open(json_file, 'w') {|f| f.write(to_json) }
95 | save
96 | end
97 |
98 | # Take a JSON representation of data and explode it out into the constituent
99 | # Lists and Items for the given Storage instance.
100 | #
101 | # Returns all hashes.
102 | def populate
103 | file = File.read(json_file)
104 | storage = JSON.parse(file, :symbolize_names => true)
105 |
106 | @hashes = storage[:hashes]
107 | convert if @hashes.is_a? Array
108 |
109 | @hashes
110 | end
111 |
112 | # Public: persists your in-memory objects to disk in Json format.
113 | #
114 | # lists_Json - list in Json format
115 | #
116 | # Returns true if successful, false if unsuccessful.
117 | def save
118 | File.open(json_file, 'w') {|f| f.write(to_json) }
119 | end
120 |
121 | # Public: the Json representation of the current List and Item assortment
122 | # attached to the Storage instance.
123 | #
124 | # Returns a String Json representation of its Lists and their Items.
125 | def to_json
126 | JSON.pretty_generate(to_hash)
127 | end
128 |
129 | private
130 | # Private: convert from old Json representation, filling in the missing data.
131 | # Also print a warning message for the user.
132 | #
133 | # Returns nothing.
134 | def convert
135 | old = @hashes
136 | @hashes = Hash.new
137 |
138 | puts 'Warning: old JSON format detected, converting.'
139 | old.each {|i| add_hash(i[:id], i[:deletehash], 'unknown') }
140 | save
141 | end
142 | end
143 | end
144 |
--------------------------------------------------------------------------------
/test/1-upload.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env roundup
2 | export IMGURRFILE=test/files/imgurr.json
3 | imgurr="./bin/imgurr"
4 | image="test/files/habs.png"
5 | id_file="test/id"
6 |
7 | describe "upload"
8 |
9 | it_uploads_image() {
10 | ${imgurr} upload ${image} >> test/temp
11 | sed 's/.*imgur.com\/\(.*\)\..*/\1/' test/temp > ${id_file}
12 | rm test/temp
13 | expr `cat ${id_file} | wc -c` ">" 0
14 | }
15 |
16 | it_uploads_image_with_markdown() {
17 | ${imgurr} upload ${image} --markdown | grep -m 1 "Copied !\[Screenshot\](http://i.imgur.com"
18 | }
19 |
20 | it_uploads_image_with_html() {
21 | ${imgurr} upload ${image} --html | grep -m 1 "Copied
"
22 | }
23 |
24 | it_uploads_image_with_html_and_size() {
25 | ${imgurr} upload ${image} --html --size 45 | grep -m 1 "Copied
"
26 | }
27 |
28 | it_uploads_image_with_title_desc() {
29 | ${imgurr} upload ${image} --title "Test" --desc "Test" | grep -m 1 "Copied http://i.imgur.com"
30 | }
31 |
--------------------------------------------------------------------------------
/test/2-info.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env roundup
2 | export IMGURRFILE=test/files/imgurr.json
3 | imgurr="./bin/imgurr"
4 | id_file="test/id"
5 |
6 | describe "info"
7 |
8 | it_gets_image_info() {
9 | ${imgurr} info `cat ${id_file}` | grep -m 1 "Width : 48 px"
10 | }
11 |
12 | it_gets_image_info_from_url() {
13 | ${imgurr} info http://i.imgur.com/2KxrTAK.jpg | grep -m 1 "Width : 960 px"
14 | }
15 |
16 | it_gets_image_info_from_url_with_title() {
17 | ${imgurr} info http://i.imgur.com/Wk1iPej.jpg | grep -m 1 "Title : Imgurr Test"
18 | }
19 |
20 | it_gets_image_info_from_url_with_description() {
21 | ${imgurr} info http://i.imgur.com/Wk1iPej.jpg | grep -m 1 "Desc : Imgurr Test"
22 | }
23 |
24 | it_lists_uploaded_images() {
25 | ${imgurr} list | grep -m 1 `cat ${id_file}`
26 | }
27 |
--------------------------------------------------------------------------------
/test/3-delete.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env roundup
2 | export IMGURRFILE=test/files/imgurr.json
3 | imgurr="./bin/imgurr"
4 | id_file="test/id"
5 |
6 | describe "delete"
7 |
8 | it_shows_error_wrong_hash() {
9 | ${imgurr} delete `cat ${id_file}` random | grep -m 1 "Unauthorized Access"
10 | }
11 |
12 | it_deletes_from_storage() {
13 | ${imgurr} delete `cat ${id_file}` | grep -m 1 "Successfully deleted"
14 | }
15 |
--------------------------------------------------------------------------------
/test/4-misc.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env roundup
2 | export IMGURRFILE=test/files/imgurr-old.json
3 | imgurr="./bin/imgurr"
4 |
5 | describe "misc"
6 |
7 | it_converts_old_json_file() {
8 | ${imgurr} list | grep -m 1 'Warning'
9 | }
10 |
--------------------------------------------------------------------------------
/test/cli.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env roundup
2 | export IMGURRFILE=test/files/imgurr.json
3 | imgurr="./bin/imgurr"
4 |
5 | describe "cli"
6 |
7 | it_shows_help() {
8 | ${imgurr} --help | grep "imgurr: help"
9 | }
10 |
11 | it_shows_a_version() {
12 | ${imgurr} --version | grep "running imgurr"
13 | }
--------------------------------------------------------------------------------
/test/error.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env roundup
2 | export IMGURRFILE=test/files/imgurr.json
3 | imgurr="./bin/imgurr"
4 | image="test/files/habs.gif"
5 |
6 | describe "errors"
7 |
8 | it_shows_missing_arg_error() {
9 | ${imgurr} upload ${image} --title | grep "Error: missing argument"
10 | }
11 |
12 | it_shows_invalid_option_error() {
13 | ${imgurr} upload ${image} --unknown | grep "Error: invalid option"
14 | }
--------------------------------------------------------------------------------
/test/files/github-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chris911/imgurr/09a492a80b9ec1ddf827501e6da6ad3dfa6d224d/test/files/github-logo.png
--------------------------------------------------------------------------------
/test/files/habs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chris911/imgurr/09a492a80b9ec1ddf827501e6da6ad3dfa6d224d/test/files/habs.png
--------------------------------------------------------------------------------
/test/files/imgurr-old.json:
--------------------------------------------------------------------------------
1 | {
2 | "hashes": [
3 | {
4 | "id": "77kh0E9",
5 | "deletehash": "zrlumL34gWAg1uv"
6 | },
7 | {
8 | "id": "aWFr3dt",
9 | "deletehash": "sx5cO3ghdf1xmit"
10 | },
11 | {
12 | "id": "Xx5SF7N",
13 | "deletehash": "jAZJSrRLvMmx4yw"
14 | },
15 | {
16 | "id": "qknnpYa",
17 | "deletehash": "luK2SQ6Wzt4KIow"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/test/roundup:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # [r5]: roundup.5.html
3 | # [r1t]: roundup-1-test.sh.html
4 | # [r5t]: roundup-5-test.sh.html
5 | #
6 | # _(c) 2010 Blake Mizerany - MIT License_
7 | #
8 | # Spray **roundup** on your shells to eliminate weeds and bugs. If your shells
9 | # survive **roundup**'s deathly toxic properties, they are considered
10 | # roundup-ready.
11 | #
12 | # **roundup** reads shell scripts to form test plans. Each
13 | # test plan is sourced into a sandbox where each test is executed.
14 | #
15 | # See [roundup-1-test.sh.html][r1t] or [roundup-5-test.sh.html][r5t] for example
16 | # test plans.
17 | #
18 | # __Install__
19 | #
20 | # git clone http://github.com/bmizerany/roundup.git
21 | # cd roundup
22 | # make
23 | # sudo make install
24 | # # Alternatively, copy `roundup` wherever you like.
25 | #
26 | # __NOTE__: Because test plans are sourced into roundup, roundup prefixes its
27 | # variable and function names with `roundup_` to avoid name collisions. See
28 | # "Sandbox Test Runs" below for more insight.
29 |
30 | # Usage and Prerequisites
31 | # -----------------------
32 |
33 | # Exit if any following command exits with a non-zero status.
34 | set -e
35 |
36 | # The current version is set during `make version`. Do not modify this line in
37 | # anyway unless you know what you're doing.
38 | ROUNDUP_VERSION="0.0.5"
39 | export ROUNDUP_VERSION
40 |
41 | # Usage is defined in a specific comment syntax. It is `grep`ed out of this file
42 | # when needed (i.e. The Tomayko Method). See
43 | # [shocco](http://rtomayko.heroku.com/shocco) for more detail.
44 | #/ usage: roundup [--help|-h] [--version|-v] [plan ...]
45 |
46 | roundup_usage() {
47 | grep '^#/' <"$0" | cut -c4-
48 | }
49 |
50 | while test "$#" -gt 0
51 | do
52 | case "$1" in
53 | --help|-h)
54 | roundup_usage
55 | exit 0
56 | ;;
57 | --version|-v)
58 | echo "roundup version $ROUNDUP_VERSION"
59 | exit 0
60 | ;;
61 | --color)
62 | color=always
63 | shift
64 | ;;
65 | -)
66 | echo >&2 "roundup: unknown switch $1"
67 | exit 1
68 | ;;
69 | *)
70 | break
71 | ;;
72 | esac
73 | done
74 |
75 | # Consider all scripts with names matching `*-test.sh` the plans to run unless
76 | # otherwise specified as arguments.
77 | if [ "$#" -gt "0" ]
78 | then
79 | roundup_plans="$@"
80 | else
81 | roundup_plans="$(ls *-test.sh)"
82 | fi
83 |
84 | : ${color:="auto"}
85 |
86 | # Create a temporary storage place for test output to be retrieved for display
87 | # after failing tests.
88 | roundup_tmp="$PWD/.roundup.$$"
89 | mkdir -p "$roundup_tmp"
90 |
91 | trap "rm -rf \"$roundup_tmp\"" EXIT INT
92 |
93 | # __Tracing failures__
94 | roundup_trace() {
95 | # Delete the first two lines that represent roundups execution of the
96 | # test function. They are useless to the user.
97 | sed '1d' |
98 | # Delete the last line which is the "set +x" of the error trap
99 | sed '$d' |
100 | # Replace the rc=$? of the error trap with an verbose string appended
101 | # to the failing command trace line.
102 | sed '$s/.*rc=/exit code /' |
103 | # Trim the two left most `+` signs. They represent the depth at which
104 | # roundup executed the function. They also, are useless and confusing.
105 | sed 's/^++//' |
106 | # Indent the output by 4 spaces to align under the test name in the
107 | # summary.
108 | sed 's/^/ /' |
109 | # Highlight the last line in front of the exit code to bring notice to
110 | # where the error occurred.
111 | #
112 | # The sed magic puts every line into the hold buffer first, then
113 | # substitutes in the previous hold buffer content, prints that and starts
114 | # with the next cycle. At the end the last line (in the hold buffer)
115 | # is printed without substitution.
116 | sed -n "x;1!{ \$s/\(.*\)/$mag\1$clr/; };1!p;\$x;\$p"
117 | }
118 |
119 | # __Other helpers__
120 |
121 | # Track the test stats while outputting a real-time report. This takes input on
122 | # **stdin**. Each input line must come in the format of:
123 | #
124 | # # The plan description to be displayed
125 | # d
126 | #
127 | # # A passing test
128 | # p
129 | #
130 | # # A failed test
131 | # f
132 | roundup_summarize() {
133 | set -e
134 |
135 | # __Colors for output__
136 |
137 | # Use colors if we are writing to a tty device.
138 | if (test -t 1) || (test $color = always)
139 | then
140 | red=$(printf "\033[31m")
141 | grn=$(printf "\033[32m")
142 | mag=$(printf "\033[35m")
143 | clr=$(printf "\033[m")
144 | cols=$(tput cols)
145 | fi
146 |
147 | # Make these available to `roundup_trace`.
148 | export red grn mag clr
149 |
150 | ntests=0
151 | passed=0
152 | failed=0
153 |
154 | : ${cols:=10}
155 |
156 | while read status name
157 | do
158 | case $status in
159 | p)
160 | ntests=$(expr $ntests + 1)
161 | passed=$(expr $passed + 1)
162 | printf " %-48s " "$name:"
163 | printf "$grn[PASS]$clr\n"
164 | ;;
165 | f)
166 | ntests=$(expr $ntests + 1)
167 | failed=$(expr $failed + 1)
168 | printf " %-48s " "$name:"
169 | printf "$red[FAIL]$clr\n"
170 | roundup_trace < "$roundup_tmp/$name"
171 | ;;
172 | d)
173 | printf "%s\n" "$name"
174 | ;;
175 | esac
176 | done
177 | # __Test Summary__
178 | #
179 | # Display the summary now that all tests are finished.
180 | yes = | head -n 57 | tr -d '\n'
181 | printf "\n"
182 | printf "Tests: %3d | " $ntests
183 | printf "Passed: %3d | " $passed
184 | printf "Failed: %3d" $failed
185 | printf "\n"
186 |
187 | # Exit with an error if any tests failed
188 | test $failed -eq 0 || exit 2
189 | }
190 |
191 | # Sandbox Test Runs
192 | # -----------------
193 |
194 | # The above checks guarantee we have at least one test. We can now move through
195 | # each specified test plan, determine its test plan, and administer each test
196 | # listed in a isolated sandbox.
197 | for roundup_p in $roundup_plans
198 | do
199 | # Create a sandbox, source the test plan, run the tests, then leave
200 | # without a trace.
201 | (
202 | # Consider the description to be the `basename` of the plan minus the
203 | # tailing -test.sh.
204 | roundup_desc=$(basename "$roundup_p" -test.sh)
205 |
206 | # Define functions for
207 | # [roundup(5)][r5]
208 |
209 | # A custom description is recommended, but optional. Use `describe` to
210 | # set the description to something more meaningful.
211 | # TODO: reimplement this.
212 | describe() {
213 | roundup_desc="$*"
214 | }
215 |
216 | # Provide default `before` and `after` functions that run only `:`, a
217 | # no-op. They may or may not be redefined by the test plan.
218 | before() { :; }
219 | after() { :; }
220 |
221 | # Seek test methods and aggregate their names, forming a test plan.
222 | # This is done before populating the sandbox with tests to avoid odd
223 | # conflicts.
224 |
225 | # TODO: I want to do this with sed only. Please send a patch if you
226 | # know a cleaner way.
227 | roundup_plan=$(
228 | grep "^it_.*()" $roundup_p |
229 | sed "s/\(it_[a-zA-Z0-9_]*\).*$/\1/g"
230 | )
231 |
232 | # We have the test plan and are in our sandbox with [roundup(5)][r5]
233 | # defined. Now we source the plan to bring its tests into scope.
234 | . ./$roundup_p
235 |
236 | # Output the description signal
237 | printf "d %s" "$roundup_desc" | tr "\n" " "
238 | printf "\n"
239 |
240 | for roundup_test_name in $roundup_plan
241 | do
242 | # Any number of things are possible in `before`, `after`, and the
243 | # test. Drop into an subshell to contain operations that may throw
244 | # off roundup; such as `cd`.
245 | (
246 | # Output `before` trace to temporary file. If `before` runs cleanly,
247 | # the trace will be overwritten by the actual test case below.
248 | {
249 | # redirect tracing output of `before` into file.
250 | {
251 | set -x
252 | # If `before` wasn't redefined, then this is `:`.
253 | before
254 | } &>"$roundup_tmp/$roundup_test_name"
255 | # disable tracing again. Its trace output goes to /dev/null.
256 | set +x
257 | } &>/dev/null
258 |
259 | # exit subshell with return code of last failing command. This
260 | # is needed to see the return code 253 on failed assumptions.
261 | # But, only do this if the error handling is activated.
262 | set -E
263 | trap 'rc=$?; set +x; set -o | grep "errexit.*on" >/dev/null && exit $rc' ERR
264 |
265 | # If `before` wasn't redefined, then this is `:`.
266 | before
267 |
268 | # Momentarily turn off auto-fail to give us access to the tests
269 | # exit status in `$?` for capturing.
270 | set +e
271 | (
272 | # Set `-xe` before the test in the subshell. We want the
273 | # test to fail fast to allow for more accurate output of
274 | # where things went wrong but not in _our_ process because a
275 | # failed test should not immediately fail roundup. Each
276 | # tests trace output is saved in temporary storage.
277 | set -xe
278 | $roundup_test_name
279 | ) >"$roundup_tmp/$roundup_test_name" 2>&1
280 |
281 | # We need to capture the exit status before returning the `set
282 | # -e` mode. Returning with `set -e` before we capture the exit
283 | # status will result in `$?` being set with `set`'s status
284 | # instead.
285 | roundup_result=$?
286 |
287 | # It's safe to return to normal operation.
288 | set -e
289 |
290 | # If `after` wasn't redefined, then this runs `:`.
291 | after
292 |
293 | # This is the final step of a test. Print its pass/fail signal
294 | # and name.
295 | if [ "$roundup_result" -ne 0 ]
296 | then printf "f"
297 | else printf "p"
298 | fi
299 |
300 | printf " $roundup_test_name\n"
301 | )
302 | done
303 | )
304 | done |
305 |
306 | # All signals are piped to this for summary.
307 | roundup_summarize
--------------------------------------------------------------------------------
/test/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # A shim to run our tests in shell.
4 |
5 | ./test/roundup test/*.sh
6 | rm test/id
7 | rm test/files/imgurr.json
8 | git checkout -- test/files/imgurr-old.json
9 |
10 | export IMGURRFILE=~/.imgurr
11 |
--------------------------------------------------------------------------------