t_bird 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8bed597706b18d287438b31c1a9be55fa0528453
4
- data.tar.gz: 09c9333f2f2946f1a1166e221f0f268310eb7a02
3
+ metadata.gz: 165550328a315a0ceb17e4176ec881efbc24c091
4
+ data.tar.gz: 0f66fcca8da662fb05d55a3575ac8608e49f82b4
5
5
  SHA512:
6
- metadata.gz: af944ed056e75ff4380f215290d5dd354daeacf9b4db1856495fa1b087d2780c119e1269bdb6353ef02259d1d9f33137daaaf9d42b7927e977379fdf4c7bb409
7
- data.tar.gz: 43aa1b46dedb3d0c2d1ed0c3cdfb53ce74fe963ddceee50ade17c376a39bd7322b99a8622b0115e9b1c12f84f0bc3b73cc8966bd33bf379f1f01724fb499907d
6
+ metadata.gz: 38fa5f3ff9c0d32d372d837d8fe9619d9e59216caa75b8085e2d8f2e541963067ace76a422af46cdf18427fdf12b09ab6df727f0b94c7aab04131eb59b912ecb
7
+ data.tar.gz: 17fa255fbc9856f9d653e040ba860de76a3064e19d57b0cc0a3123407c14ce2014b3dd8cf6369a681252538df98d074664aeab527743d28a03f50a177bce88b5
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.0.0
5
+ - 1.9.3
6
+ - 1.9.2
7
+ - jruby-19mode
8
+ - rbx-19mode
data/README.md CHANGED
@@ -1,6 +1,13 @@
1
- # TBird
1
+ # t_bird
2
2
 
3
- Straight forward file uploads for Ruby Apps.
3
+ Uploading is... fun, fun, fun, until daddy takes the `t_bird` away.
4
+
5
+
6
+ ## Project Status
7
+
8
+ - Build: [![Build Status](https://secure.travis-ci.org/xentek/t_bird.png)](http://travis-ci.org/xentek/t_bird)
9
+ - Dependencies: [![Dependency Status](https://gemnasium.com/xentek/t_bird.png)](https://gemnasium.com/xentek/t_bird)
10
+ - Code Quality: [![Code Climate](https://d3s6mut3hikguw.cloudfront.net/github/xentek/t_bird.png)](https://codeclimate.com/github/xentek/t_bird)
4
11
 
5
12
  ## Installation
6
13
 
@@ -15,8 +22,8 @@ And then execute:
15
22
  ## Why?
16
23
 
17
24
  I became frustrated by the very coupled design of the leading ruby
18
- upload libraries and wanted something that was a little more modular (to
19
- allow for flexibility), wasn't glued to a `model` class, and fulfilled my
25
+ upload libraries and wanted something that was a more modular (to
26
+ allow for flexibility), wasn't glued to a `model`, and fulfilled my
20
27
  most common use case out of the box:
21
28
 
22
29
  - Upload an image, posted from a multi-part form,
@@ -26,7 +33,146 @@ most common use case out of the box:
26
33
 
27
34
  ## Usage
28
35
 
29
- _Coming Soon_
36
+ First, configure `t_bird` with a few settings:
37
+
38
+ ````ruby
39
+ TBird::Configuration.configure |config|
40
+ config.aws_key 'amazon access key id'
41
+ config.aws_secret 'amazon secret access key'
42
+ config.aws_bucket 'name of s3 bucket that already exists'
43
+ config.thumbnail_size 100
44
+ end
45
+ ````
46
+ Place this code so that it runs when your app boots.
47
+
48
+ - If you're using Sinatra or Rack, `config.ru` is probably a good spot.
49
+ - If you're using Rails, put the configuration in an initializer, e.g. `config/initializers/t_bird.rb`.
50
+
51
+ Then, assuming you have a multipart form like this:
52
+
53
+ ````ruby
54
+ form enctype="multipart/form-data" method="POST"
55
+ input type="file" name="brand[image]"
56
+ input type="submit"
57
+ ````
58
+ _Example is using [slim](http://slim-lang.com) for clarity and terseness, but that doesn't mean you have to._
59
+
60
+ In the action the form posts to, grab the uploaded file and upload it with `t_bird`:
61
+
62
+ ````ruby
63
+ uploader = TBird::Uploader.new(params[:brand][:image])
64
+
65
+ uploader.upload! # return value is same as uploader.uploads
66
+
67
+ uploader.uploads # returns a hash of urls pointing to your image versions
68
+ # store this in the way that makes the most sense for your app
69
+ ````
70
+
71
+ - By default, there are two `versions` defined: `:thumbnail` and `:original`.
72
+ - `:thumbnail` will create crop a square image from the upload.
73
+ - You can control the size of the square by setting `thumbnail_size` in your configuration (see above).
74
+ - `:original` will be the raw, unadulterated file that the user submitted.
75
+
76
+ ## Options
77
+
78
+ There are three options you can pass into your `TBird::Uploader` instance:
79
+
80
+ - `:identifer`
81
+ - app specific identifer for his upload, e.g. your model's databae ID, timestamp, whatever
82
+ - used as the folder this file's uploads are stored in on s3
83
+ - you can pass a path fragment here to create a folder heirarchy, e.g. 'uploads/images'
84
+ - don't want any folders?, just pass an empty string, e.g. `''`
85
+ - defaults to a `SHA1` digest of the `original_filename`
86
+ - value should be URL safe, no encoding is done for you by `t_bird`
87
+ - `:token`
88
+ - needs to be a unique value per upload, to help avoid name collisions and writing over existing files
89
+ - used as part of the filename, version and extension are
90
+ automatically appended on to the end.
91
+ - defaults to a [UUID](http://en.wikipedia.org/wiki/Universally_unique_identifier), think hard before straying from this strategy
92
+ - value should be URL safe, no encoding is done for you by `t_bird`
93
+ - `:metadata`
94
+ - value must be a hash, as it will be merged in when your uploader is instantiated
95
+ - the file's `content_type` is automatically added to this hash
96
+ - values in this hash will be stored, with your file, on S3 as metadata.
97
+ - this could be used for versioning your files, e.g. `version: 2` , marking them with
98
+ the name/title of the model it's associated with, etc.
99
+ - `t_bird` doesn't provide any read access to this metadata, once it's stored on S3, so it's up to you to use S3's API to do anything with it.
100
+
101
+ ## Custom Uploaders
102
+
103
+ In order to define custom versions, and other advanced customization, create a subclass of `TBird::Uploader`:
104
+
105
+ ````ruby
106
+ class FileUploader < TBird::Uploader
107
+ version :original do
108
+ ->(file) { file.original }
109
+ end
110
+ end
111
+ ````
112
+
113
+ - In this simple example we have created a subclass and defined a single
114
+ version, `:original`, that skips processing the file, making `FileUploader` suitable
115
+ for handling non-image uploads.
116
+ - No other versions were defined, so only `:original` will be created.
117
+ - In other words, if you create a subclass without any `versions`
118
+ defined, your uploader won't upload anything.
119
+
120
+ Here's a more complex example that defines several custom `versions`:
121
+
122
+ ````ruby
123
+ class ComplexUploader < TBird::Uploader
124
+
125
+ # resizes image to a *width* of 200px, maintains aspect ratio
126
+ version :small do
127
+ ->(img) { img.resize '200' }
128
+ end
129
+
130
+ # resizes image to a *height* of 300px, maintains aspect ratio
131
+ version :large do
132
+ ->(img) { img.resize 'x300' }
133
+ end
134
+
135
+ # resizes image to a *height* of 300px, maintains aspect ratio
136
+ version :resized do
137
+ ->(img) { img.resize '500x300' }
138
+ end
139
+
140
+ # custom thumbnail size, allowing your to ignore TBird::Configuration.thumbnail_size
141
+ # on an uploader by uploader basis
142
+ version :thumbnail do
143
+ ->(img) { img.thumbnail 200 }
144
+ end
145
+
146
+ # want MOAR power?
147
+ # use process, which gets you inside a MiniMagick::Image#combine_options block
148
+ version :complex do
149
+ lambda do |img|
150
+ img.process do |magick|
151
+ magick.sample "50%"
152
+ magick.rotate "-90>"
153
+ magick.quality '88'
154
+ end
155
+ end
156
+ end
157
+ end
158
+ ````
159
+ For more information on `MiniMagick::Image#combine_options`, refer to the [mini_magick docs](https://github.com/minimagick/minimagick/blob/master/README.md).
160
+
161
+ :skull: `TBird::Processor#process` can be a _very sharp stick_, so carefully test your processing code with a variety of files in a non-production environment, and as usual be wary of using values from outside of your class as input for your processing routines.
162
+
163
+ ## More customization options
164
+
165
+ - Redefine the `namer` method in your subclass to switch out the object that's responsible for generating the name used on S3.
166
+ - Your object must respond to `new_name`, and take the `version`, which will be a symbol, as input.
167
+ - Redefine the `processor` method in your subclass to switch out the processing library.
168
+ - Blocks defined by the `version` macro will be passed an instance of your `processor`,
169
+ so be sure that your `processor` will respond to any methods the code inside your blocks call on it.
170
+ - Redefine the `upload!` to remix the whole show!
171
+ - really the sky's the limit here, but at some point you might be
172
+ better of just writing your own uploader class.
173
+ - Currently this is the only way to swith out S3 for another storage provider.
174
+ - Better support changing the storage mechanism will likely come out in a future version.
175
+ - Talk to me before working on a patch for this, so we can agree on implementation.
30
176
 
31
177
  ## Contributing
32
178
 
@@ -35,7 +181,10 @@ _Coming Soon_
35
181
  3. Commit your changes (`git commit -am 'Add some feature'`)
36
182
  4. Push to the branch (`git push origin my-new-feature`)
37
183
  5. Create new Pull Request
184
+ 6. ???
185
+ 7. Profit!
38
186
 
39
- ## Shout Outs
187
+ #### Colophon
40
188
 
41
- Sample image, used in tests, was provided by [cherrylet](http://www.flickr.com/photos/cherrylet/10258332985/sizes/o/in/photostream/), under the Creative Commons 2.0. No endorsement of this library by the photographer is intended or implied.
189
+ - This project was not named after the Thunderbird model of car. Ford Motor Company does not have any connection to, or responsibility for this project, and does not endorse it in any way (or even know it exists).
190
+ - Sample image, used in tests, was provided by [cherrylet](http://www.flickr.com/photos/cherrylet/10258332985/sizes/o/in/photostream/), under the Creative Commons 2.0. No endorsement of this library by the photographer is intended or implied.
data/lib/t_bird/namer.rb CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  require 'digest/sha1'
4
4
  require 'pathname'
5
+ require 'securerandom'
5
6
 
6
7
  module TBird
7
8
  class Namer
8
9
  attr_reader :ext, :identifier, :token
9
- def initialize(original_filename, identifier = nil, token = SecureRandom.uuid)
10
+ def initialize(original_filename, identifier = nil, token = nil)
10
11
  @ext = Pathname.new(original_filename).extname
11
12
  @identifier = identifier || Digest::SHA1.hexdigest(original_filename)
12
- @token = token
13
+ @token = token || SecureRandom.uuid
13
14
  end
14
15
 
15
16
  def new_name(version = 'original')
@@ -4,34 +4,41 @@ require 'mini_magick'
4
4
 
5
5
  module TBird
6
6
  class Processor
7
- attr_reader :image, :thumbnail_size
7
+ attr_reader :image
8
8
  def initialize(file_blob)
9
9
  @image = MiniMagick::Image.read(file_blob)
10
- @thumbnail_size = Configuration.thumbnail_size
11
10
  end
12
11
 
13
12
  def process(&block)
14
- image.combine_options do |magick|
15
- block.call(magick) if block_given?
13
+ image.combine_options do |img|
14
+ block.call(img) if block_given?
16
15
  end
17
16
  image
18
17
  end
19
18
 
19
+ def stream
20
+ image.write StringIO.new
21
+ end
22
+
23
+ def original
24
+ image # noop
25
+ end
26
+
20
27
  def resize(size)
21
- process do |magick|
22
- magick.resize size
28
+ process do |img|
29
+ img.resize size
23
30
  end
24
31
  end
25
32
 
26
- def thumbnail
27
- process do |magick|
28
- magick.auto_orient
29
- magick.thumbnail "x#{thumbnail_size*2}"
30
- magick.resize "#{thumbnail_size*2}x<"
31
- magick.resize "50%"
32
- magick.gravity "center"
33
- magick.crop "#{thumbnail_size}x#{thumbnail_size}+0+0"
34
- magick.quality 92
33
+ def thumbnail(thumbnail_size = Configuration.thumbnail_size)
34
+ process do |img|
35
+ img.auto_orient
36
+ img.thumbnail "x#{thumbnail_size*2}"
37
+ img.resize "#{thumbnail_size*2}x<"
38
+ img.resize "50%"
39
+ img.gravity "center"
40
+ img.crop "#{thumbnail_size}x#{thumbnail_size}+0+0"
41
+ img.quality 92
35
42
  end
36
43
  end
37
44
  end
@@ -10,15 +10,19 @@ module TBird
10
10
  @name = name
11
11
  @file = file
12
12
  @metadata = default_metadata.merge(metadata)
13
+ connect!
13
14
  end
14
15
 
15
16
  def transmit!
16
- connect!
17
- @tranmission = S3Object.store(name, file, Configuration.aws_bucket, metadata)
17
+ if @transmission.nil?
18
+ @transmission = S3Object.store(name, file, Configuration.aws_bucket, metadata)
19
+ end
20
+ @success ||= Service.response.success?
21
+ @success
18
22
  end
19
23
 
20
24
  def url
21
- @transmission.url
25
+ @url ||= S3Object.url_for(name, Configuration.aws_bucket, authenticated: false, use_ssl: true)
22
26
  end
23
27
 
24
28
  private
@@ -1,29 +1,59 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module TBird
4
- class Upload
5
- attr_reader :options
4
+ class Uploader
5
+ attr_reader :file, :options, :uploads, :content_type, :original_filename
6
6
  def initialize(file, options = {})
7
- @options = default_options.merge(options)
8
7
  @file = file
8
+ @options = default_options.merge(options)
9
+ @uploads = {}
10
+ @content_type = @file.content_type
11
+ @original_filename = @file.original_filename
12
+ @options[:metadata].merge!(content_type: content_type)
13
+ end
14
+
15
+ def namer
16
+ @namer ||= Namer.new(original_filename, options[:identifier], options[:token])
9
17
  end
10
18
 
11
- def content_type
12
- @file_data ||= @file.content_type
19
+ def processor
20
+ @processor ||= Processor.new(@file)
13
21
  end
14
22
 
15
- def original_filename
16
- @file_data ||= @file.original_filename
23
+ def upload!
24
+ versions.each do |version,block|
25
+ block.call(processor)
26
+ transmission = Transmitter.new(namer.new_name(version), processor.stream, options[:metadata])
27
+ transmission.transmit!
28
+ @uploads[version] = transmission.url
29
+ end
30
+ uploads
17
31
  end
18
32
 
19
- def original_file
20
- @file_data ||= @file.read
33
+ def versions
34
+ self.class.versions
35
+ end
36
+
37
+ def self.versions
38
+ @versions ||= {
39
+ thumbnail: ->(img) { img.thumbnail },
40
+ original: ->(img) { img.original }
41
+ }
42
+ end
43
+
44
+ def self.version(name, &block)
45
+ @versions ||= {}
46
+ @versions[name.to_sym] = block
21
47
  end
22
48
 
23
49
  private
24
50
 
25
51
  def default_options
26
- {}
52
+ {
53
+ identifier: nil,
54
+ token: nil,
55
+ metadata: {}
56
+ }
27
57
  end
28
58
  end
29
59
  end
@@ -1,3 +1,3 @@
1
1
  module TBird
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/spec/spec_helper.rb CHANGED
@@ -16,8 +16,6 @@ MiniTest::Reporters.use! MiniTest::Reporters::SpecReporter.new
16
16
 
17
17
  require 'mocha/setup'
18
18
  require 'rack/test'
19
- #include Rack::Test::Methods
20
-
21
19
  require 'uuid'
22
20
 
23
21
  module TBirdSpecData
@@ -6,38 +6,37 @@ describe TBird::Processor do
6
6
  include TBirdSpecData
7
7
 
8
8
  before do
9
- @custom_process = Proc.new do |magick|
10
- magick.resize '50%'
11
- magick.quality 88
12
- end
13
-
9
+ @custom_process = ->(img) { img.quality 88 }
14
10
  @processor = TBird::Processor.new(upload_file)
15
- @versions = {
16
- thumb: '90x90',
17
- medium: '300',
18
- large: 'x300',
19
- custom: @custom_process
20
- }
21
11
  end
22
12
 
23
13
  it "can process an image" do
24
14
  image = @processor.process(&@custom_process)
25
- image.write StringIO.new
26
15
  image.valid?.must_equal true
27
16
  image.destroy!
28
17
  end
29
18
 
30
- it "can process image into a thubnail" do
19
+ it "can thumbnail an image" do
31
20
  image = @processor.thumbnail
32
- image.write StringIO.new
33
21
  image.valid?.must_equal true
34
22
  image.destroy!
35
23
  end
36
24
 
37
25
  it "can resize an image" do
38
26
  image = @processor.resize('300')
39
- image.write StringIO.new
40
27
  image.valid?.must_equal true
41
28
  image.destroy!
42
29
  end
30
+
31
+ it "can return the original image" do
32
+ image = @processor.original
33
+ image.valid?.must_equal true
34
+ image.destroy!
35
+ end
36
+
37
+ it "can write image to a stream" do
38
+ image = @processor.resize('x200')
39
+ @processor.stream.must_be_instance_of StringIO
40
+ image.destroy!
41
+ end
43
42
  end
@@ -17,13 +17,20 @@ describe TBird::Transmitter do
17
17
 
18
18
  AWS::S3::S3Object.stubs(:connected?).returns(true)
19
19
  AWS::S3::Base.stubs(:establish_connection!).with(keys)
20
+ AWS::S3::Service.stubs(response: stub(:success? => true))
20
21
 
21
22
  @stored_filename = '1/sample_original.jpg'
22
- @transmitter = TBird::Transmitter.new(@stored_filename, upload_filedata, { content_type: 'image/jpeg' })
23
+ @upload = upload_file
24
+ @transmitter = TBird::Transmitter.new(@stored_filename, @upload, { content_type: 'image/jpeg' })
23
25
  end
24
26
 
25
27
  it "transmit file to store" do
26
- AWS::S3::S3Object.expects(:store).with(@stored_filename, upload_filedata, 'bucket', {:access => :public_read, :content_type => 'image/jpeg'})
28
+ AWS::S3::S3Object.expects(:store).with(@stored_filename, @upload, 'bucket', {:access => :public_read, :content_type => 'image/jpeg'})
27
29
  @transmitter.transmit!
28
30
  end
31
+
32
+ it "can return the S3 url for the file uploaded" do
33
+ AWS::S3::S3Object.expects(:url_for).with(@stored_filename, 'bucket', authenticated: false, use_ssl: true)
34
+ @transmitter.url
35
+ end
29
36
  end
@@ -2,22 +2,55 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe TBird::Upload do
5
+ describe TBird::Uploader do
6
6
  include TBirdSpecData
7
7
 
8
8
  before do
9
- @tbird_upload = TBird::Upload.new(upload_file)
9
+ @uploader = TBird::Uploader.new(upload_file)
10
+ @urls = ['https://s3.example.com/5a3f124f68b57512d3865da4c527f6b7bbd4173a/b54c0891-92a1-4641-8334-d396d5cc21c4_thumbnail.jpg',
11
+ 'https://s3.example.com/5a3f124f68b57512d3865da4c527f6b7bbd4173a/b54c0891-92a1-4641-8334-d396d5cc21c4_original.jpg']
10
12
  end
11
13
 
12
- it "can return original filename" do
13
- @tbird_upload.original_filename.must_equal 'sample.jpg'
14
+ it "can return the file" do
15
+ @uploader.file.wont_be_nil
14
16
  end
15
17
 
16
- it "can return content type" do
17
- @tbird_upload.content_type.must_equal 'image/jpeg'
18
+ it "can return the options" do
19
+ @uploader.options.must_equal({ identifier: nil, token: nil, metadata: { content_type: "image/jpeg" } })
18
20
  end
19
21
 
20
- it "can return the original file binary data" do
21
- @tbird_upload.original_file.wont_be_nil
22
+ it "can return the content type" do
23
+ @uploader.content_type.must_equal 'image/jpeg'
24
+ end
25
+
26
+ it "can return the original filename" do
27
+ @uploader.original_filename.must_equal 'sample.jpg'
28
+ end
29
+
30
+ it "can return an instance of Processor" do
31
+ @uploader.processor.must_be_instance_of TBird::Processor
32
+ end
33
+
34
+ it "can return an instance of Namer" do
35
+ @uploader.namer.must_be_instance_of TBird::Namer
36
+ end
37
+
38
+ it "can return the registered versions" do
39
+ @uploader.versions.must_equal TBird::Uploader.versions
40
+ end
41
+
42
+ it "can upload all versions" do
43
+ TBird::Transmitter.any_instance.stubs(:transmit!)
44
+ TBird::Transmitter.any_instance.stubs(:url).returns(*@urls)
45
+
46
+ @uploader.upload!.must_equal({ thumbnail: @urls.first, original: @urls.last })
47
+ end
48
+
49
+ it "can return the upload urls" do
50
+ TBird::Transmitter.any_instance.stubs(:transmit!)
51
+ TBird::Transmitter.any_instance.stubs(:url).returns(*@urls)
52
+
53
+ @uploader.upload!
54
+ @uploader.uploads.must_equal({ thumbnail: @urls.first, original: @urls.last })
22
55
  end
23
56
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: t_bird
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Marden
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-22 00:00:00.000000000 Z
11
+ date: 2013-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mini_magick
@@ -88,6 +88,7 @@ extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
90
  - .gitignore
91
+ - .travis.yml
91
92
  - Gemfile
92
93
  - LICENSE.txt
93
94
  - README.md