murlsh 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/.htaccess +5 -0
  2. data/COPYING +27 -0
  3. data/README.textile +2 -2
  4. data/Rakefile +133 -66
  5. data/VERSION +1 -1
  6. data/config.ru +9 -8
  7. data/config.yaml +3 -2
  8. data/lib/murlsh/auth.rb +6 -8
  9. data/lib/murlsh/config_server.rb +4 -6
  10. data/lib/murlsh/dispatch.rb +5 -7
  11. data/lib/murlsh/doc.rb +1 -1
  12. data/lib/murlsh/etag_add_encoding.rb +1 -3
  13. data/lib/murlsh/failproof.rb +0 -1
  14. data/lib/murlsh/far_future_expires.rb +2 -4
  15. data/lib/murlsh/head_from_get.rb +2 -2
  16. data/lib/murlsh/image_list.rb +32 -0
  17. data/lib/murlsh/img_store.rb +47 -9
  18. data/lib/murlsh/markup.rb +53 -20
  19. data/lib/murlsh/must_revalidate.rb +2 -4
  20. data/lib/murlsh/plugin.rb +1 -1
  21. data/lib/murlsh/sqlite3_adapter.rb +2 -4
  22. data/lib/murlsh/time_ago.rb +6 -8
  23. data/lib/murlsh/uri.rb +1 -3
  24. data/lib/murlsh/uri_ask.rb +23 -25
  25. data/lib/murlsh/url.rb +4 -6
  26. data/lib/murlsh/url_body.rb +19 -21
  27. data/lib/murlsh/url_server.rb +8 -10
  28. data/lib/murlsh/yaml_ordered_hash.rb +2 -4
  29. data/lib/murlsh.rb +21 -4
  30. data/murlsh.gemspec +95 -90
  31. data/plugins/add_post_50_update_feed.rb +22 -10
  32. data/plugins/add_post_50_update_podcast.rb +3 -5
  33. data/plugins/add_post_50_update_rss.rb +4 -6
  34. data/plugins/add_post_60_notify_hubs.rb +3 -5
  35. data/plugins/add_pre_40_convert_mobile.rb +4 -10
  36. data/plugins/add_pre_50_lookup_content_type_title.rb +4 -6
  37. data/plugins/add_pre_60_flickr.rb +3 -14
  38. data/plugins/add_pre_60_github_title.rb +4 -6
  39. data/plugins/add_pre_60_google_code_title.rb +4 -6
  40. data/plugins/add_pre_60_imgur.rb +4 -16
  41. data/plugins/add_pre_60_s3_image.rb +7 -6
  42. data/plugins/add_pre_60_twitter.rb +3 -14
  43. data/plugins/add_pre_60_vimeo.rb +7 -6
  44. data/plugins/add_pre_60_youtube.rb +8 -7
  45. data/plugins/add_pre_65_html_thumb.rb +41 -0
  46. data/plugins/add_pre_65_img_thumb.rb +39 -0
  47. data/plugins/html_parse_50_hpricot.rb +2 -4
  48. data/plugins/url_display_add_45_audio.rb +28 -0
  49. data/plugins/url_display_add_50_hostrec.rb +15 -18
  50. data/plugins/url_display_add_55_content_type.rb +6 -8
  51. data/plugins/url_display_add_60_via.rb +12 -19
  52. data/plugins/url_display_add_65_time.rb +4 -6
  53. data/public/css/screen.css +2 -3
  54. data/public/img/thumb/.gitignore +3 -0
  55. data/public/js/jquery-1.4.4.min.js +167 -0
  56. data/public/js/js.js +6 -5
  57. data/public/js/{twitter-text-1.0.3.js → twitter-text-1.0.4.js} +3 -1
  58. data/spec/auth_spec.rb +4 -6
  59. data/spec/dispatch_spec.rb +3 -5
  60. data/spec/doc_spec.rb +2 -4
  61. data/spec/img_store_spec.rb +46 -20
  62. data/spec/markup_spec.rb +22 -24
  63. data/spec/uri_ask_spec.rb +5 -7
  64. data/spec/uri_spec.rb +2 -4
  65. data/spec/url_spec.rb +5 -9
  66. data/spec/yaml_ordered_hash_spec.rb +1 -3
  67. metadata +85 -53
  68. data/.gitignore +0 -6
  69. data/plugins/add_pre_60_imageshack.rb +0 -31
  70. data/plugins/url_display_add_45_mp3.rb +0 -30
  71. data/public/img/thumb/README +0 -0
  72. data/public/js/jquery-1.4.3.min.js +0 -166
  73. data/public/swf/player_mp3_mini.swf +0 -0
data/.htaccess CHANGED
@@ -12,3 +12,8 @@ AddOutputFilterByType DEFLATE text/html
12
12
  Header add Expires "Wed, 22 Jun 2019 20:07:00 GMT"
13
13
  FileETag None
14
14
  </FilesMatch>
15
+
16
+ <FilesMatch "[\da-z]{32}\.(gif|jpe?g|png)$">
17
+ Header add Expires "Wed, 22 Jun 2019 20:07:00 GMT"
18
+ FileETag None
19
+ </FilesMatch>
data/COPYING CHANGED
@@ -1,3 +1,30 @@
1
+ Copyright (c) 2010 Matthew M. Boedicker
2
+
3
+ All code is licensed under the GPLv3 with the following exceptions, which are
4
+ included third-party libraries:
5
+
6
+ - jQuery by John Resig
7
+
8
+ License: MIT or GPLv2
9
+
10
+ Files:
11
+ public/js/jquery-1.4.4.min.js
12
+
13
+ - jGrowl by Stan Lemon
14
+
15
+ License: MIT or GPLv2
16
+
17
+ Files:
18
+ public/css/jquery.jgrowl.css
19
+ public/js/jquery.jgrowl_compressed.js
20
+
21
+ - twitter-text-js by Twitter, Inc.
22
+
23
+ License: Apache 2.0
24
+
25
+ Files:
26
+ public/js/twitter-text-1.0.4.js
27
+
1
28
  GNU GENERAL PUBLIC LICENSE
2
29
  Version 3, 29 June 2007
3
30
 
data/README.textile CHANGED
@@ -1,7 +1,7 @@
1
1
  Site for sharing and archiving links.
2
2
 
3
- * looks up url titles
4
- * adds thumbnails for and jGrowls embedded versions of Imageshack, Vimeo and YouTube urls
3
+ * fetches url titles and generates thumbnails
4
+ * jGrowls embedded versions of Imageshack, Vimeo and YouTube urls
5
5
  * converts Twitter status urls to their full text and adds user thumbnail
6
6
  * generates Atom and RSS feeds
7
7
  * regex search
data/Rakefile CHANGED
@@ -1,24 +1,20 @@
1
1
  $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
2
 
3
- %w{
4
- cgi
5
- digest/md5
6
- net/http
7
- pp
8
- uri
9
- yaml
10
-
11
- flog
12
- spec/rake/spectask
13
- sqlite3
14
-
15
- murlsh
16
- }.each { |d| require d }
17
-
18
- # optional libraries
19
- %w{
20
- metric_fu
21
- }.each { |d| Murlsh::failproof { require d } }
3
+ require 'cgi'
4
+ require 'digest/md5'
5
+ require 'net/http'
6
+ require 'pp'
7
+ require 'uri'
8
+ require 'yaml'
9
+
10
+ require 'RMagick'
11
+ require 'sqlite3'
12
+
13
+ require 'murlsh'
14
+
15
+ def gem_not_found(gem_name)
16
+ puts "#{gem_name} not found, install it with: gem install #{gem_name}"
17
+ end
22
18
 
23
19
  config = YAML.load_file('config.yaml')
24
20
 
@@ -37,7 +33,7 @@ end
37
33
  desc 'Combine and compress static files.'
38
34
  task :compress => %w{css:compress js:compress}
39
35
 
40
- desc "Test remote content type fetch for a URL and show errors."
36
+ desc 'Test remote content type fetch for a URL and show errors.'
41
37
  task :content_type, :url do |t, args|
42
38
  puts URI(args.url).extend(Murlsh::UriAsk).content_type(:failproof => false,
43
39
  :debug => STDOUT)
@@ -53,28 +49,28 @@ namespace :db do
53
49
  last = Murlsh::Url.find(:last, :order => 'time')
54
50
  pp last
55
51
  response = ask('Delete this url', '?')
56
- last.destroy if %w{y yes}.include?(response.downcase)
52
+ last.destroy if %w{y yes}.include?(response.downcase)
57
53
  end
58
54
 
59
- desc "Check for duplicate URLs."
55
+ desc 'Check for duplicate URLs.'
60
56
  task :dupcheck do
61
57
  db = SQLite3::Database.new(config.fetch('db_file'))
62
58
  db.results_as_hash = true
63
59
  h = {}
64
- db.execute("SELECT * FROM urls").each do |r|
60
+ db.execute('SELECT * FROM urls').each do |r|
65
61
  h[r['url']] = h.fetch(r['url'], []).push([r['id'], r['time']])
66
62
  end
67
- h.select { |k,v| v.size > 1 }.each do |k,v|
63
+ h.find_all { |k,v| v.size > 1 }.each do |k,v|
68
64
  puts k
69
65
  v.each { |id,time| puts " #{id} #{time}" }
70
66
  end
71
67
  end
72
68
 
73
- desc "Create an empty database."
69
+ desc 'Create an empty database.'
74
70
  task :init do
75
71
  puts "creating #{config.fetch('db_file')}"
76
72
  db = SQLite3::Database.new(config.fetch('db_file'))
77
- db.execute("CREATE TABLE urls (
73
+ db.execute('CREATE TABLE urls (
78
74
  id INTEGER PRIMARY KEY,
79
75
  time TIMESTAMP,
80
76
  url TEXT,
@@ -85,7 +81,7 @@ namespace :db do
85
81
  content_type TEXT,
86
82
  via TEXT,
87
83
  thumbnail_url TEXT);
88
- ")
84
+ ')
89
85
  end
90
86
 
91
87
  desc 'Interact with the database.'
@@ -93,6 +89,17 @@ namespace :db do
93
89
  exec "sqlite3 #{config['db_file']}"
94
90
  end
95
91
 
92
+ desc 'Search urls and titles in the database.'
93
+ task :grep, :search do |t,args|
94
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3',
95
+ :database => config.fetch('db_file'))
96
+
97
+ Murlsh::Url.all(:conditions => [
98
+ 'MURLSHMATCH(title, :search) OR MURLSHMATCH(url, :search)',
99
+ { :search => args.search }]).each do |url|
100
+ puts "#{url.id} #{url.url} #{url.title}"
101
+ end
102
+ end
96
103
  end
97
104
 
98
105
  directory 'tmp'
@@ -106,29 +113,43 @@ namespace :passenger do
106
113
 
107
114
  end
108
115
 
109
- desc "Run flog on ruby and report on complexity."
116
+ desc 'Run flog on ruby and report on complexity.'
110
117
  task :flog do
111
- flog = Flog.new
112
- flog.flog('lib')
113
- flog.report
118
+ begin
119
+ require 'flog'
120
+
121
+ flog = Flog.new
122
+ flog.flog 'lib'
123
+ flog.report
124
+ rescue LoadError
125
+ gem_not_found 'flog'
126
+ end
114
127
  end
115
128
 
116
- desc "Run test suite."
117
- Spec::Rake::SpecTask.new('test') do |t|
118
- t.spec_files = FileList['spec/*_spec.rb']
119
- t.spec_opts = %w{--color}
120
- # list of places to check for unicode_formatter.rb and use it if found
121
- %w{unicode_formatter.rb}.map { |x| File.expand_path(x) }.each do |f|
122
- if File.exists?(f)
123
- t.spec_opts.push(*%W{--require #{f} --format UnicodeFormatter})
124
- break
129
+ desc 'Run test suite.'
130
+ begin
131
+ require 'spec/rake/spectask'
132
+
133
+ Spec::Rake::SpecTask.new('test') do |t|
134
+ t.spec_files = FileList['spec/*_spec.rb']
135
+ t.spec_opts = %w{--color}
136
+ # list of places to check for unicode_formatter.rb and use it if found
137
+ %w{unicode_formatter.rb}.map { |x| File.expand_path(x) }.each do |f|
138
+ if File.exists?(f)
139
+ t.spec_opts.push(*%W{--require #{f} --format UnicodeFormatter})
140
+ break
141
+ end
125
142
  end
143
+ t.verbose = true
144
+ t.warning = true
145
+ end
146
+ rescue LoadError
147
+ task :test do
148
+ gem_not_found 'rspec'
126
149
  end
127
- t.verbose = true
128
- t.warning = true
129
150
  end
130
151
 
131
- desc "Test remote title fetch for a URL and show errors."
152
+ desc 'Test remote title fetch for a URL and show errors.'
132
153
  task :title, :url do |t, args|
133
154
  puts URI(args.url).extend(Murlsh::UriAsk).title(:failproof => false,
134
155
  :debug => STDOUT)
@@ -148,7 +169,7 @@ end
148
169
 
149
170
  namespace :user do
150
171
 
151
- desc "Add a new user."
172
+ desc 'Add a new user.'
152
173
  task :add do
153
174
  puts "adding to #{config.fetch('auth_file')}"
154
175
  username = ask(:username)
@@ -168,7 +189,7 @@ def validate(check_url, options={})
168
189
  :validator_port => 80,
169
190
  :validator_path =>
170
191
  "/check?uri=#{CGI::escape(check_url)}&charset=(detect+automatically)&doctype=Inline&group=0",
171
- }.merge(options)
192
+ }.merge options
172
193
 
173
194
  net_http = Net::HTTP.new(opts[:validator_host], opts[:validator_port])
174
195
  # net_http.set_debug_output(STDOUT)
@@ -229,7 +250,7 @@ def cat(in_files, sep=nil)
229
250
  in_files.each do |fname|
230
251
  open(fname) do |h|
231
252
  while (line = h.gets) do; result << line; end
232
- result << sep if sep
253
+ result << sep if sep
233
254
  end
234
255
  end
235
256
  result
@@ -319,9 +340,49 @@ namespace :js do
319
340
 
320
341
  end
321
342
 
343
+ namespace :thumb do
344
+
345
+ desc 'Check that local thumbnails in database are consistent with filesystem.'
346
+ task :check do
347
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3',
348
+ :database => config.fetch('db_file')
349
+ Murlsh::Url.all(
350
+ :conditions => "thumbnail_url like 'img/thumb/%'").each do |u|
351
+ identity = "url #{u.id} (#{u.url})"
352
+
353
+ path = File.join(%w{public}.concat(File.split(u.thumbnail_url)))
354
+ if File.readable?(path)
355
+ img_data = open(path) { |f| f.read }
356
+
357
+ unless img_data.empty?
358
+ img = Magick::ImageList.new.from_blob(img_data).extend(
359
+ Murlsh::ImageList)
360
+
361
+ ext = File.extname(path)
362
+ expected_ext = img.preferred_extension
363
+ if ext != expected_ext
364
+ puts "#{identity} thumbnail #{path} has an extension of '#{ext}' but is actually a '#{expected_ext}'"
365
+
366
+ end
367
+
368
+ md5 = Digest::MD5.hexdigest(img_data)
369
+ if File.basename(path, ext) != md5
370
+ puts "#{identity} thumbnail #{path} filename does not match file content md5 (#{md5})"
371
+ end
372
+ else
373
+ puts "#{identity} thumbnail #{path} is empty"
374
+ end
375
+ else
376
+ puts "#{identity} thumbnail #{path} does not exist or is not readable"
377
+ end
378
+ end
379
+ end
380
+
381
+ end
382
+
322
383
  def ask(prompt, sep=':')
323
384
  print "#{prompt}#{sep} "
324
- return STDIN.gets.chomp
385
+ STDIN.gets.chomp
325
386
  end
326
387
 
327
388
  begin
@@ -339,25 +400,31 @@ begin
339
400
  # gemspec.cert_chain = %w{/home/mmb/src/keys/gem-public_cert.pem}
340
401
 
341
402
  %w{
342
- activerecord 2.3.4
343
- bcrypt-ruby 2.1.2
344
- builder 2.1.2
345
- flickraw 0.8.3
346
- flog 2.5.0
347
- hpricot 0.8.1
348
- htmlentities 4.2.0
349
- json 1.2.3
350
- push-notify 0.1.0
351
- rack 1.0.0
352
- rack-cache 0.5.2
353
- rack-rewrite 1.0.2
354
- rack-throttle 0.3.0
355
- sqlite3-ruby 1.2.1
356
- tinyatom 0.2.0
357
- twitter 0.9.12
358
- vimeo 1.2.2
359
- }.each_slice(2) { |g,v| gemspec.add_dependency(g, ">= #{v}") }
360
- gemspec.add_dependency('rspec', '~> 1.3')
403
+ activerecord >= 2.3.4
404
+ bcrypt-ruby >= 2.1.2
405
+ builder >= 2.1.2
406
+ flickraw >= 0.8.3
407
+ hpricot >= 0.8.1
408
+ htmlentities >= 4.2.0
409
+ json >= 1.2.3
410
+ plumnailer >= 0.1.0
411
+ push-notify >= 0.1.0
412
+ rack >= 1.0.0
413
+ rack-cache >= 0.5.2
414
+ rack-rewrite >= 1.0.2
415
+ rack-throttle >= 0.3.0
416
+ rmagick >= 1.15.14
417
+ sqlite3-ruby >= 1.2.1
418
+ tinyatom >= 0.3.3
419
+ twitter >= 0.9.12
420
+ vimeo >= 1.2.2
421
+ }.each_slice(3) { |g,o,v| gemspec.add_dependency(g, "#{o} #{v}") }
422
+ %w{
423
+ flog >= 2.5.0
424
+ rspec ~> 1.3
425
+ }.each_slice(3) do |g,o,v|
426
+ gemspec.add_development_dependency(g, "#{o} #{v}")
427
+ end
361
428
  end
362
429
  rescue LoadError
363
430
  puts "Jeweler not available. Install it with: gem install jeweler"
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1.0
data/config.ru CHANGED
@@ -1,14 +1,12 @@
1
1
  $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
2
 
3
- %w{
4
- yaml
3
+ require 'yaml'
5
4
 
6
- rack/cache
7
- rack/rewrite
8
- rack/throttle
5
+ require 'rack/cache'
6
+ require 'rack/rewrite'
7
+ require 'rack/throttle'
9
8
 
10
- murlsh
11
- }.each { |m| require m }
9
+ require 'murlsh'
12
10
 
13
11
  config = YAML.load_file('config.yaml')
14
12
 
@@ -24,7 +22,10 @@ end
24
22
  use Rack::ConditionalGet
25
23
  use Murlsh::EtagAddEncoding
26
24
  use Rack::Deflater
27
- use Murlsh::FarFutureExpires, :patterns => %r{\.gen\.(css|js)$}
25
+ use Murlsh::FarFutureExpires, :patterns => [
26
+ %r{[\da-z]{32}\.(?:gif|jpe?g|png)$}i,
27
+ %r{\.gen\.(css|js)$}
28
+ ]
28
29
 
29
30
  feed_url = URI.join(config.fetch('root_url'), config.fetch('feed_file'))
30
31
  use Murlsh::MustRevalidate, :patterns => %r{^#{Regexp.escape(feed_url.path)}$}
data/config.yaml CHANGED
@@ -12,9 +12,9 @@ feed_file: atom.atom
12
12
  flickr_api_key:
13
13
  gravatar_size: 32
14
14
  js_files:
15
- - js/jquery-1.4.3.min.js
15
+ - js/jquery-1.4.4.min.js
16
16
  - js/jquery.jgrowl_compressed.js
17
- - js/twitter-text-1.0.3.js
17
+ - js/twitter-text-1.0.4.js
18
18
  - js/js.js
19
19
  meta_tag_description: URLs found interesting by Matthew M. Boedicker
20
20
  meta_tag_verify-v1:
@@ -26,4 +26,5 @@ pubsubhubbub_hubs: []
26
26
 
27
27
  root_url: http://urls.matthewm.boedicker.org/
28
28
  show_names: true
29
+ thumbnail_max_side: 90
29
30
  user_agent: murlsh (http://github.com/mmb/murlsh)
data/lib/murlsh/auth.rb CHANGED
@@ -1,9 +1,7 @@
1
- %w{
2
- csv
3
- digest/md5
1
+ require 'csv'
2
+ require 'digest/md5'
4
3
 
5
- bcrypt
6
- }.each { |m| require m }
4
+ require 'bcrypt'
7
5
 
8
6
  module Murlsh
9
7
 
@@ -22,7 +20,7 @@ module Murlsh
22
20
  # Authenticate a user by password. Return their name and email if correct.
23
21
  def auth(password)
24
22
  CSV::Reader.parse(open(@file)) do |row|
25
- return { :name => row[0], :email => row[1] } if
23
+ return { :name => row[0], :email => row[1] } if
26
24
  BCrypt::Password.new(row[2]) == password
27
25
  end
28
26
  end
@@ -30,8 +28,8 @@ module Murlsh
30
28
  # Add a user to the authentication file.
31
29
  def add_user(username, email, password)
32
30
  Murlsh::openlock(@file, 'a') do |f|
33
- f.write("#{[username, Digest::MD5.hexdigest(email),
34
- BCrypt::Password.create(password)].join(',')}\n")
31
+ f.write "#{[username, Digest::MD5.hexdigest(email),
32
+ BCrypt::Password.create(password)].join(',')}\n"
35
33
  end
36
34
  end
37
35
 
@@ -1,9 +1,7 @@
1
- %w{
2
- digest/sha1
1
+ require 'digest/sha1'
3
2
 
4
- json
5
- rack
6
- }.each { |m| require m }
3
+ require 'json'
4
+ require 'rack'
7
5
 
8
6
  module Murlsh
9
7
 
@@ -27,7 +25,7 @@ module Murlsh
27
25
  end
28
26
 
29
27
  # Serve a JSON subset of the configuration.
30
- def get(req); Rack::Response.new(@config_json, 200, @headers); end
28
+ def get(req); Rack::Response.new @config_json, 200, @headers; end
31
29
 
32
30
  end
33
31
 
@@ -1,9 +1,7 @@
1
- %w{
2
- active_record
3
- rack
1
+ require 'active_record'
2
+ require 'rack'
4
3
 
5
- murlsh
6
- }.each { |m| require m }
4
+ require 'murlsh'
7
5
 
8
6
  module Murlsh
9
7
 
@@ -51,11 +49,11 @@ module Murlsh
51
49
 
52
50
  # Called if the request is not found.
53
51
  def not_found(req)
54
- Rack::Response.new("<p>#{req.url} not found</p>
52
+ Rack::Response.new "<p>#{req.url} not found</p>
55
53
 
56
54
  <p><a href=\"#{@config['root_url']}\">root<a></p>
57
55
  ",
58
- 404, { 'Content-Type' => 'text/html' })
56
+ 404, { 'Content-Type' => 'text/html' }
59
57
  end
60
58
 
61
59
  end
data/lib/murlsh/doc.rb CHANGED
@@ -11,7 +11,7 @@ module Murlsh
11
11
  content = content_type['content']
12
12
  unless content.nil?
13
13
  charset = content[/charset=([\w_.:-]+)/, 1]
14
- return charset if charset
14
+ return charset if charset
15
15
  end
16
16
  end
17
17
  end
@@ -1,6 +1,4 @@
1
- %w{
2
- rack/utils
3
- }.each { |m| require m }
1
+ require 'rack/utils'
4
2
 
5
3
  module Murlsh
6
4
 
@@ -1,4 +1,3 @@
1
-
2
1
  module Murlsh
3
2
 
4
3
  module_function
@@ -1,7 +1,5 @@
1
- %w{
2
- rack
3
- rack/utils
4
- }.each { |m| require m }
1
+ require 'rack'
2
+ require 'rack/utils'
5
3
 
6
4
  module Murlsh
7
5
 
@@ -1,9 +1,9 @@
1
1
  module Murlsh
2
2
 
3
- # mixin for adding head() that calls get() and removed the body
3
+ # Mixin for adding head() that calls get() and removed the body.
4
4
  module HeadFromGet
5
5
 
6
- # call get() and remove the body
6
+ # Call get() and remove the body.
7
7
  def head(req)
8
8
  resp = get(req)
9
9
  resp.body = ''
@@ -0,0 +1,32 @@
1
+ require 'base64'
2
+
3
+ module Murlsh
4
+
5
+ # Magick::ImageList mixin.
6
+ module ImageList
7
+
8
+ # For each image, if the width or height is larger than max_side, resize so
9
+ # that the longest side = max_side.
10
+ def resize_down!(max_side)
11
+ each do |i|
12
+ if i.columns > max_side or i.rows > max_side
13
+ i.resize_to_fit! max_side, max_side
14
+ end
15
+ i.strip!
16
+ end
17
+ end
18
+
19
+ # Get the preferred extension for this image.
20
+ def preferred_extension; FormatExtensions[self.format]; end
21
+
22
+ def data_uri; "data:#{mime_type};base64,#{Base64.encode64(to_blob)}"; end
23
+
24
+ FormatExtensions = {
25
+ 'GIF' => '.gif',
26
+ 'JPEG' => '.jpg',
27
+ 'PNG' => '.png',
28
+ }
29
+
30
+ end
31
+
32
+ end
@@ -1,13 +1,20 @@
1
- %w{
2
- cgi
3
- open-uri
4
- }.each { |m| require m }
1
+ require 'cgi'
2
+ require 'digest/md5'
3
+ require 'open-uri'
4
+ require 'uri'
5
+
6
+ require 'RMagick'
7
+
8
+ require 'murlsh'
5
9
 
6
10
  module Murlsh
7
11
 
8
12
  # Fetch images from urls and store them locally.
9
13
  class ImgStore
10
14
 
15
+ # Fetch images from urls and store them locally.
16
+ # Options:
17
+ # * :user_agent - user agent to send with http requests
11
18
  def initialize(storage_dir, options={})
12
19
  @storage_dir = storage_dir
13
20
  @user_agent = options[:user_agent]
@@ -16,16 +23,47 @@ module Murlsh
16
23
  # Build headers to send with request.
17
24
  def headers
18
25
  result = {}
19
- result['User-Agent'] = @user_agent if @user_agent
26
+ result['User-Agent'] = @user_agent if @user_agent
20
27
  result
21
28
  end
22
29
 
23
30
  # Fetch an image from a url and store it locally.
24
- def store(url)
25
- local_file = CGI.escape(url)
31
+ #
32
+ # The filename will be the md5sum of the contents plus the correct
33
+ # extension.
34
+ #
35
+ # If a block is given the Magick::ImageList created will be yielded
36
+ # before storage.
37
+ def store_url(url, &block)
38
+ open(url, headers) { |fin| store_img_data fin.read, &block }
39
+ end
40
+
41
+ # Accept a blob of image data and store it locally.
42
+ #
43
+ # The filename will be the md5sum of the contents plus the correct
44
+ # extension.
45
+ #
46
+ # If a block is given the Magick::ImageList created will be yielded
47
+ # before storage.
48
+ def store_img_data(img_data, &block)
49
+ img = Magick::ImageList.new.from_blob(img_data)
50
+ yield img if block_given?
51
+ store_img img
52
+ end
53
+
54
+ # Accept a Magick::ImageList and store it locally.
55
+ #
56
+ # The filename will be the md5sum of the contents plus the correct
57
+ # extension.
58
+ def store_img(img)
59
+ img.extend(Murlsh::ImageList) unless img.is_a?(Murlsh::ImageList)
60
+ img_data = img.to_blob
61
+ md5 = Digest::MD5.hexdigest(img_data)
62
+
63
+ local_file = "#{md5}#{img.preferred_extension}"
26
64
  local_path = File.join(storage_dir, local_file)
27
- open(url, headers) do |fin|
28
- open(local_path, 'w') { |fout| fout.write(fin.read) }
65
+ unless File.exists?(local_path)
66
+ Murlsh::openlock(local_path, 'w') { |fout| fout.write img_data }
29
67
  end
30
68
  local_file
31
69
  end