attached 0.0.9 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -1
- data/README.rdoc +38 -13
- data/lib/attached.rb +18 -9
- data/lib/attached/attachment.rb +85 -23
- data/lib/attached/image.rb +68 -0
- data/lib/attached/processor.rb +48 -0
- data/lib/attached/storage.rb +2 -2
- data/lib/attached/storage/s3.rb +1 -1
- metadata +18 -3
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
= Attached
|
2
2
|
|
3
|
-
Attached is a Ruby on Rails file attachment tool that lets users upload to the cloud, then process in the cloud. The tool supports Amazon S3 by default. It surpasses Paperclip by providing built-in support for direct uploads to the cloud and by allowing integration with cloud based processing tools. However, in almost every other way Paperclip is better. The source code inspired (and copied) from Paperclip. If you aren't working in the cloud exclusively then this isn't for you!
|
3
|
+
Attached is a Ruby on Rails file attachment tool that lets users upload to the cloud, then process in the cloud. The tool supports Amazon S3 by default. It surpasses Paperclip by providing built-in support for direct uploads to the cloud and by allowing integration with cloud based processing tools. However, in almost every other way Paperclip is better. The source code is inspired (and copied) from Paperclip. If you aren't working in the cloud exclusively then this isn't for you!
|
4
4
|
|
5
5
|
== Installation
|
6
6
|
|
@@ -13,9 +13,9 @@ Migration:
|
|
13
13
|
class CreateVideo < ActiveRecord::Migration
|
14
14
|
def self.up
|
15
15
|
create_table :videos do |t|
|
16
|
-
t.string :
|
17
|
-
t.string :
|
18
|
-
t.integer :
|
16
|
+
t.string :encoding_identifier
|
17
|
+
t.string :encoding_extension
|
18
|
+
t.integer :encoding_size
|
19
19
|
|
20
20
|
t.timestamps
|
21
21
|
end
|
@@ -28,27 +28,52 @@ Migration:
|
|
28
28
|
|
29
29
|
Model:
|
30
30
|
|
31
|
-
has_attached :
|
32
|
-
:mp4_720p => { :extension => 'mp4' },
|
33
|
-
:mp4_480p => { :extension => 'mp4' },
|
34
|
-
:
|
35
|
-
:
|
31
|
+
has_attached :encoding, :styles => {
|
32
|
+
:mp4_720p => { :extension => '.mp4' },
|
33
|
+
:mp4_480p => { :extension => '.mp4' },
|
34
|
+
:ogg_720p => { :extension => '.ogv' },
|
35
|
+
:ogg_480p => { :extension => '.ogv' },
|
36
36
|
}
|
37
37
|
|
38
|
+
after_save do
|
39
|
+
remote.encode(self.encoding.url)
|
40
|
+
end
|
41
|
+
|
38
42
|
Form:
|
39
43
|
|
40
44
|
<%= form_for @video, :html => { :multipart => true } do |form| %>
|
41
|
-
<%= form.file_field :
|
42
|
-
<p class="errors"><%= @video.errors[:
|
45
|
+
<%= form.file_field :encoding %>
|
46
|
+
<p class="errors"><%= @video.errors[:encoding] %></p>
|
43
47
|
<% end %>
|
44
48
|
|
45
49
|
View:
|
46
50
|
|
47
51
|
<video>
|
48
|
-
<source src="<%= @video.
|
49
|
-
<source src="<%= @video.
|
52
|
+
<source src="<%= @video.encoding.url(:mp4_480p) %>" />
|
53
|
+
<source src="<%= @video.encoding.url(:ogg_480p) %>" />
|
50
54
|
</video>
|
51
55
|
|
56
|
+
== Advanced
|
57
|
+
|
58
|
+
=== Storage
|
59
|
+
|
60
|
+
has_attached :avatar, :provider => :amazon, :credentials => {
|
61
|
+
:secret_access_key => "*****",
|
62
|
+
:access_key_id => "*****",
|
63
|
+
}
|
64
|
+
|
65
|
+
has_attached :avatar, :provider => :google, :credentials => {
|
66
|
+
:secret_access_key => "*****",
|
67
|
+
:access_key_id => "*****",
|
68
|
+
}
|
69
|
+
|
70
|
+
=== Processor
|
71
|
+
|
72
|
+
has_attached :avatar, :processor => :resize, :styles => {
|
73
|
+
:small => { :size => "200x200#" }
|
74
|
+
:large => { :size => "200x200#" }
|
75
|
+
}
|
76
|
+
|
52
77
|
== Copyright
|
53
78
|
|
54
79
|
Copyright (c) 2010 Kevin Sylvestre. See LICENSE for details.
|
data/lib/attached.rb
CHANGED
@@ -31,8 +31,9 @@ module Attached
|
|
31
31
|
#
|
32
32
|
# Options:
|
33
33
|
#
|
34
|
-
# * :styles
|
35
|
-
# * :storage
|
34
|
+
# * :styles -
|
35
|
+
# * :storage -
|
36
|
+
# * :processor -
|
36
37
|
#
|
37
38
|
# Usage:
|
38
39
|
#
|
@@ -118,8 +119,8 @@ module Attached
|
|
118
119
|
|
119
120
|
range = minimum..maximum
|
120
121
|
|
121
|
-
message.gsub!(/:minimum/,
|
122
|
-
message.gsub!(/:maximum/,
|
122
|
+
message.gsub!(/:minimum/, number_to_size(minimum)) unless minimum == zero
|
123
|
+
message.gsub!(/:maximum/, number_to_size(maximum)) unless maximum == infi
|
123
124
|
|
124
125
|
validates_inclusion_of :"#{name}_size", :in => range, :message => message,
|
125
126
|
:if => options[:if], :unless => options[:unless]
|
@@ -151,21 +152,29 @@ module Attached
|
|
151
152
|
private
|
152
153
|
|
153
154
|
|
154
|
-
|
155
|
+
# Convert a number to a human readable size.
|
156
|
+
#
|
157
|
+
# Usage:
|
158
|
+
#
|
159
|
+
# number_to_size(1) # 1 byte
|
160
|
+
# number_to_size(2) # 2 bytes
|
161
|
+
# number_to_size(1024) # 1 kilobyte
|
162
|
+
# number_to_size(2048) # 2 kilobytes
|
155
163
|
|
156
|
-
def
|
164
|
+
def number_to_size(number, options = {})
|
157
165
|
return if number == 0.0 / 1.0
|
158
166
|
return if number == 1.0 / 0.0
|
159
167
|
|
160
|
-
|
161
|
-
|
168
|
+
singular = options['singular'] || 1
|
169
|
+
base = options['base'] || 1024
|
170
|
+
units = options['units'] || ["byte", "kilobyte", "megabyte", "gigabyte", "terabyte", "petabyte"]
|
162
171
|
|
163
172
|
exponent = (Math.log(number) / Math.log(base)).floor
|
164
173
|
|
165
174
|
number /= base ** exponent
|
166
175
|
unit = units[exponent]
|
167
176
|
|
168
|
-
number ==
|
177
|
+
number == singular ? unit.gsub!(/s$/, '') : unit.gsub!(/$/, 's')
|
169
178
|
|
170
179
|
"#{number} #{unit}"
|
171
180
|
end
|
data/lib/attached/attachment.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'guid'
|
2
2
|
|
3
3
|
require 'attached/storage'
|
4
|
+
require 'attached/processor'
|
5
|
+
require 'attached/image'
|
4
6
|
|
5
7
|
module Attached
|
6
8
|
|
@@ -11,13 +13,28 @@ module Attached
|
|
11
13
|
attr_reader :name
|
12
14
|
attr_reader :instance
|
13
15
|
attr_reader :options
|
16
|
+
attr_reader :queue
|
17
|
+
attr_reader :path
|
18
|
+
attr_reader :styles
|
19
|
+
attr_reader :default
|
20
|
+
attr_reader :medium
|
21
|
+
attr_reader :credentials
|
22
|
+
attr_reader :processors
|
23
|
+
attr_reader :processor
|
14
24
|
|
15
25
|
|
26
|
+
# A default set of options that can be extended to customize the path, storage or credentials.
|
27
|
+
#
|
28
|
+
# Usage:
|
29
|
+
#
|
30
|
+
# Attached::Attachment.options = { :storage => :fs, :path => "/:name/:style/:identifier:extension" }
|
31
|
+
|
16
32
|
def self.options
|
17
33
|
@options ||= {
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:styles
|
34
|
+
:path => "/:name/:style/:identifier:extension",
|
35
|
+
:default => :original,
|
36
|
+
:styles => {},
|
37
|
+
:processors => [],
|
21
38
|
}
|
22
39
|
end
|
23
40
|
|
@@ -34,16 +51,28 @@ module Attached
|
|
34
51
|
# * :path - The location where the attachment is stored
|
35
52
|
# * :storage - The storage medium represented as a symbol such as ':s3'
|
36
53
|
# * :credentials - A file, hash, or path used to authenticate with the specified storage medium
|
54
|
+
# * :styles - A hash containing optional parameters including extension and identifier
|
37
55
|
|
38
56
|
def initialize(name, instance, options = {})
|
39
|
-
@name
|
40
|
-
@instance
|
57
|
+
@name = name
|
58
|
+
@instance = instance
|
59
|
+
@options = self.class.options.merge(options)
|
60
|
+
|
61
|
+
@queue = {}
|
62
|
+
|
63
|
+
@path = @options[:path]
|
64
|
+
@styles = @options[:styles]
|
65
|
+
@default = @options[:default]
|
66
|
+
@medium = @options[:medium]
|
67
|
+
@credentials = @options[:credentials]
|
68
|
+
@processors = @options[:processors]
|
69
|
+
@processor = @options[:processor]
|
41
70
|
|
42
|
-
@
|
71
|
+
@processors << @processor if @processor
|
43
72
|
end
|
44
73
|
|
45
74
|
|
46
|
-
#
|
75
|
+
# Check if an attachment has been modified.
|
47
76
|
#
|
48
77
|
# Usage:
|
49
78
|
#
|
@@ -54,6 +83,8 @@ module Attached
|
|
54
83
|
end
|
55
84
|
|
56
85
|
|
86
|
+
# Assign an attachment to a file.
|
87
|
+
#
|
57
88
|
# Usage:
|
58
89
|
#
|
59
90
|
# @object.avatar.assign(...)
|
@@ -62,34 +93,47 @@ module Attached
|
|
62
93
|
@file = file.tempfile
|
63
94
|
|
64
95
|
extension = File.extname(file.original_filename)
|
65
|
-
|
96
|
+
|
66
97
|
instance_set :size, file.size
|
67
98
|
instance_set :extension, extension
|
68
99
|
instance_set :identifier, identifier
|
100
|
+
|
101
|
+
self.queue[self.default] = self.file
|
102
|
+
|
103
|
+
process
|
69
104
|
end
|
70
105
|
|
71
106
|
|
107
|
+
# Save an attachment.
|
108
|
+
#
|
72
109
|
# Usage:
|
73
110
|
#
|
74
111
|
# @object.avatar.save
|
75
112
|
|
76
113
|
def save
|
77
|
-
@storage ||= Attached::Storage.medium
|
114
|
+
@storage ||= Attached::Storage.storage(self.medium, self.credentials)
|
115
|
+
|
116
|
+
@queue.each do |style, file|
|
117
|
+
@storage.save(file, self.path(style)) if file and self.path(style)
|
118
|
+
end
|
78
119
|
|
79
|
-
@
|
120
|
+
@queue = {}
|
80
121
|
end
|
81
122
|
|
82
123
|
|
124
|
+
# Destroy an attachment.
|
125
|
+
#
|
83
126
|
# Usage:
|
84
127
|
#
|
85
128
|
# @object.avatar.destroy
|
86
129
|
|
87
130
|
def destroy
|
88
|
-
@storage ||= Attached::Storage.medium
|
131
|
+
@storage ||= Attached::Storage.storage(self.medium, self.credentials)
|
89
132
|
|
90
133
|
@storage.destroy(self.path) if self.path
|
91
134
|
end
|
92
135
|
|
136
|
+
|
93
137
|
# Acesss the URL for an attachment.
|
94
138
|
#
|
95
139
|
# Usage:
|
@@ -98,8 +142,8 @@ module Attached
|
|
98
142
|
# @object.avatar.url(:small)
|
99
143
|
# @object.avatar.url(:large)
|
100
144
|
|
101
|
-
def url(style =
|
102
|
-
@storage ||= Attached::Storage.medium
|
145
|
+
def url(style = self.default)
|
146
|
+
@storage ||= Attached::Storage.storage(self.medium, self.credentials)
|
103
147
|
|
104
148
|
return "#{@storage.host}#{path(style)}"
|
105
149
|
end
|
@@ -113,8 +157,8 @@ module Attached
|
|
113
157
|
# @object.avatar.url(:small)
|
114
158
|
# @object.avatar.url(:large)
|
115
159
|
|
116
|
-
def path(style =
|
117
|
-
path =
|
160
|
+
def path(style = self.default)
|
161
|
+
path = @path.clone
|
118
162
|
|
119
163
|
path.gsub!(/:name/, name.to_s)
|
120
164
|
path.gsub!(/:style/, style.to_s)
|
@@ -124,6 +168,7 @@ module Attached
|
|
124
168
|
return path
|
125
169
|
end
|
126
170
|
|
171
|
+
|
127
172
|
# Access the size for an attachment.
|
128
173
|
#
|
129
174
|
# Usage:
|
@@ -144,12 +189,13 @@ module Attached
|
|
144
189
|
|
145
190
|
def extension(style = nil)
|
146
191
|
style and
|
147
|
-
|
148
|
-
|
149
|
-
|
192
|
+
self.styles and
|
193
|
+
self.styles[style] and
|
194
|
+
self.styles[style][:extension] or
|
150
195
|
instance_get(:extension)
|
151
196
|
end
|
152
197
|
|
198
|
+
|
153
199
|
# Access the identifier for an attachment. It will first check the styles
|
154
200
|
# to see if one is specified before checking the instance.
|
155
201
|
#
|
@@ -159,9 +205,9 @@ module Attached
|
|
159
205
|
|
160
206
|
def identifier(style = nil)
|
161
207
|
style and
|
162
|
-
|
163
|
-
|
164
|
-
|
208
|
+
self.styles and
|
209
|
+
self.styles[style] and
|
210
|
+
self.styles[style][:identifier] or
|
165
211
|
instance_get(:identifier)
|
166
212
|
end
|
167
213
|
|
@@ -191,6 +237,22 @@ module Attached
|
|
191
237
|
|
192
238
|
private
|
193
239
|
|
240
|
+
# Helper function for calling processors.
|
241
|
+
#
|
242
|
+
# Usage:
|
243
|
+
#
|
244
|
+
# self.process
|
245
|
+
|
246
|
+
def process
|
247
|
+
@processors.each do |processor|
|
248
|
+
self.styles.each do |style, options|
|
249
|
+
case processor
|
250
|
+
when :image then self.queue[style] = Attached::Image.process(self.queue[style] || self.file, options, self)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
194
256
|
|
195
257
|
# Helper function for setting instance variables.
|
196
258
|
#
|
@@ -200,7 +262,7 @@ module Attached
|
|
200
262
|
|
201
263
|
def instance_set(attribute, value)
|
202
264
|
setter = :"#{self.name}_#{attribute}="
|
203
|
-
self.instance.send(setter, value)
|
265
|
+
self.instance.send(setter, value) if instance.respond_to?(setter)
|
204
266
|
end
|
205
267
|
|
206
268
|
|
@@ -212,7 +274,7 @@ module Attached
|
|
212
274
|
|
213
275
|
def instance_get(attribute)
|
214
276
|
getter = :"#{self.name}_#{attribute}"
|
215
|
-
self.instance.send(getter)
|
277
|
+
self.instance.send(getter) if instance.respond_to?(getter)
|
216
278
|
end
|
217
279
|
|
218
280
|
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'attached/processor'
|
2
|
+
|
3
|
+
require 'rmagick'
|
4
|
+
|
5
|
+
module Attached
|
6
|
+
|
7
|
+
class Image < Processor
|
8
|
+
|
9
|
+
|
10
|
+
attr_reader :path
|
11
|
+
attr_reader :extname
|
12
|
+
|
13
|
+
# Create a processor.
|
14
|
+
#
|
15
|
+
# Parameters:
|
16
|
+
#
|
17
|
+
# * file - The file to be processed.
|
18
|
+
# * options - The options to be applied to the processing.
|
19
|
+
# * attachment - The attachment the processor is being run for.
|
20
|
+
|
21
|
+
def initialize(file, options = {}, attachment = nil)
|
22
|
+
super
|
23
|
+
|
24
|
+
@path = @file.path
|
25
|
+
@extname = File.extname(@file.path)
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# Helper function for calling processors.
|
30
|
+
#
|
31
|
+
# Usage:
|
32
|
+
#
|
33
|
+
# self.process
|
34
|
+
|
35
|
+
def process
|
36
|
+
result = Tempfile.new(["", options['extension'] || self.extname])
|
37
|
+
result.binmode
|
38
|
+
|
39
|
+
image = ::Magick::Image.read(self.path)
|
40
|
+
image_list = ::Magick::ImageList.new
|
41
|
+
|
42
|
+
width, height, operation = self.options[:size].match(/\b(\d*)x?(\d*)\b([\#\<\>])?/)[1..3] if self.options[:size]
|
43
|
+
|
44
|
+
width ||= self.options[:width]
|
45
|
+
height ||= self.options[:height]
|
46
|
+
operation ||= self.options[:operation]
|
47
|
+
|
48
|
+
width = width.to_i
|
49
|
+
height = height.to_i
|
50
|
+
|
51
|
+
image.each do |frame|
|
52
|
+
case operation
|
53
|
+
when /!/ then puts "hi"
|
54
|
+
when /#/ then image_list << frame.resize_to_fill(width, height)
|
55
|
+
when /</ then image_list << frame.resize_to_fit(width, height)
|
56
|
+
when />/ then image_list << frame.resize_to_fit(width, height)
|
57
|
+
else image_list << frame.resize(width, height)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
image_list.write(result.path)
|
62
|
+
|
63
|
+
return result
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Attached
|
2
|
+
|
3
|
+
class Processor
|
4
|
+
|
5
|
+
|
6
|
+
attr_accessor :file
|
7
|
+
attr_accessor :options
|
8
|
+
attr_accessor :attachment
|
9
|
+
|
10
|
+
|
11
|
+
# Create and run a processor.
|
12
|
+
#
|
13
|
+
# Parameters:
|
14
|
+
#
|
15
|
+
# * file - The file to be processed.
|
16
|
+
# * options - The options to be applied to the processing.
|
17
|
+
# * attachment - The attachment the processor is being run for.
|
18
|
+
|
19
|
+
def self.process(file, options = {}, attachment = nil)
|
20
|
+
new(file, options, attachment).process
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# Create a processor.
|
25
|
+
#
|
26
|
+
# Parameters:
|
27
|
+
#
|
28
|
+
# * file - The file to be processed.
|
29
|
+
# * options - The options to be applied to the processing.
|
30
|
+
# * attachment - The attachment the processor is being run for.
|
31
|
+
|
32
|
+
def initialize(file, options = {}, attachment = nil)
|
33
|
+
@file = file
|
34
|
+
@options = options
|
35
|
+
@attachment = attachment
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# Run the processor.
|
40
|
+
|
41
|
+
def process
|
42
|
+
raise NotImplementedError.new
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/lib/attached/storage.rb
CHANGED
@@ -9,9 +9,9 @@ module Attached
|
|
9
9
|
#
|
10
10
|
# Attached::Storage.medium(s3)
|
11
11
|
|
12
|
-
def self.medium
|
12
|
+
def self.storage(medium = :s3, credentials = nil)
|
13
13
|
|
14
|
-
case
|
14
|
+
case medium
|
15
15
|
when :s3 then return Attached::Storage::S3.new(credentials)
|
16
16
|
end
|
17
17
|
|
data/lib/attached/storage/s3.rb
CHANGED
@@ -50,7 +50,7 @@ module Attached
|
|
50
50
|
def save(file, path)
|
51
51
|
connect()
|
52
52
|
begin
|
53
|
-
AWS::S3::S3Object.store(path, file, bucket, :access => :
|
53
|
+
AWS::S3::S3Object.store(path, file, bucket, :access => :public_read)
|
54
54
|
rescue AWS::S3::NoSuchBucket => e
|
55
55
|
AWS::S3::Bucket.create(bucket)
|
56
56
|
retry
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
+
- 1
|
7
8
|
- 0
|
8
|
-
|
9
|
-
version: 0.0.9
|
9
|
+
version: 0.1.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Kevin Sylvestre
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-12-
|
17
|
+
date: 2010-12-15 00:00:00 -05:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -43,6 +43,19 @@ dependencies:
|
|
43
43
|
version: "0"
|
44
44
|
type: :runtime
|
45
45
|
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rmagick
|
48
|
+
prerelease: false
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
type: :runtime
|
58
|
+
version_requirements: *id003
|
46
59
|
description: Attached is a Ruby on Rails cloud attachment and processor library inspired by Paperclip. Attached lets users push files to the cloud, then perform remote processing on the files.
|
47
60
|
email:
|
48
61
|
- kevin@ksylvest.com
|
@@ -54,6 +67,8 @@ extra_rdoc_files: []
|
|
54
67
|
|
55
68
|
files:
|
56
69
|
- lib/attached/attachment.rb
|
70
|
+
- lib/attached/image.rb
|
71
|
+
- lib/attached/processor.rb
|
57
72
|
- lib/attached/railtie.rb
|
58
73
|
- lib/attached/storage/base.rb
|
59
74
|
- lib/attached/storage/s3.rb
|