justpics 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +15 -0
  2. data/README.markdown +42 -0
  3. data/justpics.gemspec +16 -0
  4. data/lib/justpics.rb +153 -0
  5. metadata +75 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NzM0OWE0ZTc5NjA3ZGIwOWEyMDE0OWExMzlmN2MzMzA0MTc1OTQ4NQ==
5
+ data.tar.gz: !binary |-
6
+ OTg0YzRkMzJhOWQxOTRmYzFhMGI3ZDY1NTZlMzY0NmY1OWE0Mzc1OQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ ZjFhNDZkMzlkODM2OGRlMDJkMTE3Mzc3YThiNTgyOGM5YmRjZjI3YjdhZDU5
10
+ YThkOTYyOGE5MTc5NDdkMmZjYTQ5YmIxZjU2YWRlMTI2MzBmMGViZjQyZDM5
11
+ Y2U3MDU3NThhNTJjNmJkMTU2ZTE5OTdlOGI0ZmUyYjA5MmYzNzA=
12
+ data.tar.gz: !binary |-
13
+ MmU0OTcyMTdkN2MzMDY0YTY2Y2YxNGVjMzk2MjI5OGNiMjRlYTgzNWMyNjM0
14
+ ZDIxNDg1ZDVmYTI2OTQ2MWM2ZDllOWFjNTUyYzdmMWMxZDI5NGFhMDkwZTQ2
15
+ YjIwNTI0OGUxZDhhYjljNjQ3ZWM4YjcxYjljMTZjNTQ0ZTg2MmY=
data/README.markdown ADDED
@@ -0,0 +1,42 @@
1
+ # Just Pics
2
+ ## Really simple image hosting.
3
+
4
+ Your own personal TwitPic, on Heroku & S3.
5
+
6
+ It's designed to be simple and highly cacheable. All images are served with long expiry times,
7
+ and as all URLs are based on the SHA of the image, their content should never change.
8
+
9
+ ## Prerequisites
10
+
11
+ * An Amazon S3 Bucket
12
+
13
+ ## Setup
14
+
15
+ git clone git@github.com:cwninja/justpics.git
16
+ cd justpics
17
+ heroku create
18
+ git push heroku
19
+ heroku config:add \
20
+ AMAZON_ACCESS_KEY_ID=YourAmazonAccessKeyId \
21
+ AMAZON_SECRET_ACCESS_KEY=YourAmazonAccessKey \
22
+ AMAZON_S3_BUCKET=YourAmazonS3BucketName \
23
+ JUSTPICS_MINIMUM_KEY_LENGTH=6 \
24
+ JUSTPICS_POST_PATH=/
25
+ heroku open
26
+
27
+ You can then configure the Heroku app with a custom domain as per usual. If you wan't shorter URLs or a custom (secret) upload path, just tweak the Heroku config.
28
+
29
+ ## iPhone Twitter Client Setup
30
+
31
+ In the advanced settings, simply change your image service to your upload URL (in the example setup above it's the root of your app).
32
+
33
+ ## Costs
34
+
35
+ You should only be billed for the storage costs, as all access in and out of S3 is via the Heroku app, which is hosted inside the AWS platform.
36
+
37
+ This seems to be how it's worked out for me, your costs may vary, keep an eye on your bill.
38
+
39
+ ## Licence
40
+ Copyright Tom Lea. Licensed under the [WTFBPPL][WTFBPPL].
41
+
42
+ [WTFBPPL]: http://tomlea.co.uk/WTFBPPL.txt
data/justpics.gemspec ADDED
@@ -0,0 +1,16 @@
1
+ # coding: utf-8
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.add_dependency 'sinatra'
5
+ spec.add_dependency 'aws-sdk'
6
+ spec.authors = ['Tom Lea']
7
+ spec.description = %q{Simple image serving. Without trying to be too clever.}
8
+ spec.email = ['contrib@tomlea.co.uk']
9
+ spec.files = %w(README.markdown justpics.gemspec)
10
+ spec.files += Dir.glob("lib/**/*.rb")
11
+ spec.homepage = 'http://github.com/cwninja/justpics'
12
+ spec.name = 'justpics'
13
+ spec.require_paths = ['lib']
14
+ spec.summary = spec.description
15
+ spec.version = "1.0"
16
+ end
data/lib/justpics.rb ADDED
@@ -0,0 +1,153 @@
1
+ require 'sinatra/base'
2
+ require 'enumerator'
3
+ require 'digest/sha1'
4
+ require 'aws/s3'
5
+
6
+
7
+
8
+ class Justpics < Sinatra::Base
9
+ end
10
+
11
+ class Justpics::AlwaysFresh
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ if (env["HTTP_IF_MODIFIED_SINCE"] or env["HTTP_IF_NONE_MATCH"]) and env["REQUEST_METHOD"] == "GET" and env["PATH_INFO"].try(:length) > Justpics::MINIMUM_KEY_LENGTH
18
+ puts "Quick 304"
19
+ [304, {}, []]
20
+ else
21
+ @app.call(env)
22
+ end
23
+ end
24
+ end
25
+
26
+
27
+ class Justpics < Sinatra::Base
28
+ BUCKET_NAME = ENV['AMAZON_S3_BUCKET']
29
+ MAX_SIZE = (ENV['JUSTPICS_MAX_SIZE'] || 2 * 1024 * 1024).to_i
30
+ POST_PATH = "/#{ENV["JUSTPICS_POST_PATH"]}".gsub(%r{//+}, "/")
31
+ MINIMUM_KEY_LENGTH = (ENV['JUSTPICS_MINIMUM_KEY_LENGTH'] || 40).to_i
32
+ MINIMUM_KEY_LENGTH > 0 or raise ArgumentError, "JUSTPICS_MINIMUM_KEY_LENGTH must be above 0, otherwise where would the home page go?!?"
33
+ MINIMUM_KEY_LENGTH <= 40 or raise ArgumentError, "JUSTPICS_MINIMUM_KEY_LENGTH cannot be bigger then the full key length (40)"
34
+
35
+ NotFound = Class.new(RuntimeError)
36
+ S3 = AWS::S3.new
37
+
38
+ enable :static, :methodoverride
39
+
40
+ use Justpics::AlwaysFresh
41
+
42
+ get '/favicon.ico' do
43
+ cache_forever
44
+ send_file File.expand_path("../../assets/favicon.ico", __FILE__)
45
+ end
46
+
47
+ get "/" do
48
+ status 404
49
+ render_default
50
+ end
51
+
52
+ get POST_PATH do
53
+ File.read(File.expand_path("../../assets/index.html", __FILE__))
54
+ end
55
+
56
+ post POST_PATH do
57
+ file = params[:file] || params[:media]
58
+
59
+ unless file and tmpfile = file[:tempfile]
60
+ status 510
61
+ return "No file selected"
62
+ end
63
+
64
+ if tmpfile.size > MAX_SIZE
65
+ status 510
66
+ return "File too large. Keep it under #{MAX_SIZE} bytes."
67
+ end
68
+
69
+ resource_url = url upload_image(tmpfile.path, file[:type])
70
+
71
+ if params[:media]
72
+ "<mediaurl>#{resource_url}</mediaurl>"
73
+ else
74
+ redirect resource_url
75
+ end
76
+ end
77
+
78
+ get "/:id*" do
79
+ id = params[:id].to_s[/^[a-zA-Z0-9]*/]
80
+ begin
81
+ cache_forever
82
+ render_image(id)
83
+ rescue NotFound
84
+ status 404
85
+ render_default
86
+ end
87
+ end
88
+
89
+ def cache_forever
90
+ response['Cache-Control'] = "public, max-age=#{60*60*24*356*3}"
91
+ response['ETag'] = "the-same-thing-every-time"
92
+ response['Last-Modified'] = Time.at(1337).httpdate
93
+ end
94
+
95
+ def render_image(id)
96
+ raise NotFound unless sha = expand_sha(id)
97
+ file = bucket.objects[sha]
98
+ content_type file.content_type
99
+ Enumerator.new(file, :read)
100
+ rescue AWS::S3::Errors::NoSuchKey => e
101
+ raise NotFound, e.message
102
+ end
103
+
104
+ def render_default
105
+ send_file File.expand_path('../../assets/default.png', __FILE__)
106
+ end
107
+
108
+ def expand_sha(small)
109
+ small = small.to_s[/^[a-fA-F0-9]*/]
110
+ get_keys_starting_with(small).first unless small.length < MINIMUM_KEY_LENGTH
111
+ end
112
+
113
+ module UploadMethods
114
+ def bucket
115
+ @bucket ||= S3.buckets[BUCKET_NAME]
116
+ end
117
+
118
+ def upload_image(path, type = "application/octet-stream")
119
+ id = Digest::SHA1.file(path).hexdigest
120
+
121
+ unless bucket.objects[id].exists?
122
+ bucket.objects[id].write(
123
+ Pathname.new(path),
124
+ content_type: type,
125
+ cache_control: "public,max-age=999999999",
126
+ acl: :public_read,
127
+ content_disposition: :attachment
128
+ )
129
+ end
130
+
131
+ short_key = find_short_key_for(id)
132
+ "/#{short_key}"
133
+ end
134
+
135
+ def find_short_key_for(key)
136
+ keys = get_keys_starting_with(key[0...MINIMUM_KEY_LENGTH]) - [key]
137
+ MINIMUM_KEY_LENGTH.upto(key.length) do |length|
138
+ candidate = key[0...length]
139
+ keys = keys.grep(/^#{candidate}/)
140
+ return candidate if keys.empty?
141
+ end
142
+ key
143
+ end
144
+
145
+ def get_keys_starting_with(key)
146
+ bucket.objects.with_prefix(key).sort_by(&:last_modified).map(&:key)
147
+ end
148
+ end
149
+
150
+ extend UploadMethods
151
+ include UploadMethods
152
+
153
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: justpics
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ platform: ruby
6
+ authors:
7
+ - Tom Lea
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-06-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sinatra
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Simple image serving. Without trying to be too clever.
42
+ email:
43
+ - contrib@tomlea.co.uk
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - README.markdown
49
+ - justpics.gemspec
50
+ - lib/justpics.rb
51
+ homepage: http://github.com/cwninja/justpics
52
+ licenses: []
53
+ metadata: {}
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 2.0.3
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Simple image serving. Without trying to be too clever.
74
+ test_files: []
75
+ has_rdoc: