murlsh 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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