attached 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'rails'
4
+
5
+ gem 'aws-s3', :require => 'aws/s3'
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Kevin Sylvestre
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,53 @@
1
+ = Attached
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!
4
+
5
+ == Installation
6
+
7
+ gem install attached
8
+
9
+ == Examples
10
+
11
+ Migration:
12
+
13
+ class CreateVideo < ActiveRecord::Migration
14
+ def self.up
15
+ create_table :videos do |t|
16
+ t.string :video_extension
17
+ t.integer :video_size
18
+
19
+ t.timestamps
20
+ end
21
+ end
22
+
23
+ def self.down
24
+ drop_table :videos
25
+ end
26
+ end
27
+
28
+ Model:
29
+
30
+ has_attached :video, :styles => {
31
+ :mp4_720p => { :extension => 'mp4' },
32
+ :mp4_480p => { :extension => 'mp4' },
33
+ :ogv_720p => { :extension => 'ogv' },
34
+ :ogv_480p => { :extension => 'ogv' },
35
+ }
36
+
37
+ Form:
38
+
39
+ <%= form_for @video, :html => { :multipart => true } do |form| %>
40
+ <%= form.file_field :video %>
41
+ <p class="errors"><%= @video.errors[:video] %></p>
42
+ <% end %>
43
+
44
+ View:
45
+
46
+ <video>
47
+ <source src="<%= @video.video.url(:mp4_480p) %>" />
48
+ <source src="<%= @video.video.url(:ogv_480p) %>" />
49
+ </video>
50
+
51
+ == Copyright
52
+
53
+ Copyright (c) 2010 Kevin Sylvestre. See LICENSE for details.
@@ -0,0 +1,189 @@
1
+ require 'attached/attachment'
2
+ require 'attached/railtie'
3
+
4
+
5
+ module Attached
6
+
7
+
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ end
11
+
12
+
13
+ module ClassMethods
14
+
15
+
16
+ # Initialize attached options used for communicating between class and instance methods.
17
+
18
+ def initialize_attached_options
19
+ write_inheritable_attribute(:attached_options, {})
20
+ end
21
+
22
+
23
+ # Access attached options used for communicating between class and instance methods.
24
+
25
+ def attached_options
26
+ read_inheritable_attribute(:attached_options)
27
+ end
28
+
29
+
30
+ # Add an attachment to a class.
31
+ #
32
+ # Options:
33
+ #
34
+ # * :styles -
35
+ # * :storage -
36
+ #
37
+ # Usage:
38
+ #
39
+ # has_attached :avatar
40
+ # has_attached :avatar, :storage => :s3
41
+ # has_attached :clip, styles => [ :mp4, :ogv ]
42
+ # has_attached :clip, styles => { :main => { :size => "480p", :format => "mp4" } }
43
+
44
+ def has_attached(name, options = {})
45
+
46
+ include InstanceMethods
47
+
48
+ initialize_attached_options unless attached_options
49
+ attached_options[name] = options
50
+
51
+ after_save :save_attached
52
+ after_destroy :destroy_attached
53
+
54
+ define_method name do
55
+ attachment_for(name)
56
+ end
57
+
58
+ define_method "#{name}=" do |file|
59
+ attachment_for(name).assign(file)
60
+ end
61
+
62
+ define_method "#{name}?" do
63
+ attachment_for(name).file?
64
+ end
65
+
66
+ after_validation do
67
+
68
+ self.errors[:"#{name}_size"].each do |message|
69
+ self.errors.add(name, message)
70
+ end
71
+
72
+ self.errors[:"#{name}_extension"].each do |message|
73
+ self.errors.add(name, message)
74
+ end
75
+
76
+ self.errors.delete(:"#{name}_size")
77
+ self.errors.delete(:"#{name}_extension")
78
+
79
+ end
80
+
81
+ end
82
+
83
+
84
+ # Validates an attached size in a specified range or minimum and maximum.
85
+ #
86
+ # Options:
87
+ #
88
+ # * :message - string to be displayed with :minimum and :maximum variables
89
+ # * :minimum - integer for the minimum byte size of the attached
90
+ # * :maximum - integer for the maximum byte size of teh attached
91
+ # * :in - range of bytes for file
92
+ #
93
+ # Usage:
94
+ #
95
+ # validates_attached_size :avatar, :range => 10.megabytes .. 20.megabytes
96
+ # validates_attached_size :avatar, :minimum => 10.megabytes, :maximum => 20.megabytes
97
+ # validates_attached_size :avatar, :message => "size must be between :minimum and :maximum bytes"
98
+
99
+ def validates_attached_size(name, options = {})
100
+
101
+ message = options[:message] || "size must be between :minimum and :maximum bytes"
102
+
103
+ minimum = options[:minimum] || options[:in] && options[:in].first || (0.0 / 1.0)
104
+ maximum = options[:maximum] || options[:in] && options[:in].last || (1.0 / 0.0)
105
+
106
+ range = minimum..maximum
107
+
108
+ message.gsub!(/:minimum/, minimum.to_s)
109
+ message.gsub!(/:maximum/, maximum.to_s)
110
+
111
+ validates_inclusion_of :"#{name}_size", :in => range, :message => message,
112
+ :if => options[:if], :unless => options[:unless]
113
+
114
+ end
115
+
116
+
117
+ # Validates that an attachment is included.
118
+ #
119
+ # Options:
120
+ #
121
+ # * :message - string to be displayed
122
+ #
123
+ # Usage:
124
+ #
125
+ # validates_attached_presence :avatar
126
+ # validates_attached_presence :avatar, :message => "must be attached"
127
+
128
+ def validates_attached_presence(name, options = {})
129
+
130
+ message = options[:message] || "must be attached"
131
+
132
+ validates_presence_of :"#{name}_extension", :message => message,
133
+ :if => options[:if], :unless => options[:unless]
134
+
135
+ end
136
+
137
+
138
+ end
139
+
140
+
141
+ module InstanceMethods
142
+
143
+
144
+ # Create or access attachment.
145
+ #
146
+ # Usage:
147
+ #
148
+ # attachment_for :avatar
149
+
150
+ def attachment_for(name)
151
+ @_attached_attachments ||= {}
152
+ @_attached_attachments[name] ||= Attachment.new(name, self, self.class.attached_options[name])
153
+ end
154
+
155
+
156
+ # Log and save all attached (using specified storage).
157
+ #
158
+ # Usage:
159
+ #
160
+ # before_save :save_attached
161
+
162
+ def save_attached
163
+ logger.info "[attached] save attached"
164
+
165
+ self.class.attached_options.each do |name, options|
166
+ attachment_for(name).save
167
+ end
168
+ end
169
+
170
+
171
+ # Log and destroy all attached (using specified storage).
172
+ #
173
+ # Usage:
174
+ #
175
+ # before_save :destroy_attached
176
+
177
+ def destroy_attached
178
+ logger.info "[attached] destroy attached"
179
+
180
+ self.class.attached_options.each do |name, options|
181
+ attachment_for(name).destroy
182
+ end
183
+ end
184
+
185
+
186
+ end
187
+
188
+
189
+ end
@@ -0,0 +1,167 @@
1
+ require 'attached/storage'
2
+
3
+ module Attached
4
+
5
+ class Attachment
6
+
7
+
8
+ attr_reader :file
9
+ attr_reader :name
10
+ attr_reader :instance
11
+ attr_reader :options
12
+ attr_reader :storage
13
+
14
+
15
+ def self.options
16
+ @options ||= {
17
+ :storage => :fs,
18
+ :protocol => 'http',
19
+ :path => "/:name/:style/:id:extension",
20
+ :styles => {},
21
+ }
22
+ end
23
+
24
+
25
+ # Initialize a new attachment by providing a name and the instance the attachment is associated with.
26
+ #
27
+ # Parameters:
28
+ #
29
+ # * name - The name for the attachment such as 'avatar' or 'photo'
30
+ # * instance - The instance the attachment is attached to
31
+ #
32
+ # Options:
33
+ #
34
+ # * :path - The location where the attachment is stored
35
+ # * :storage - The storage medium represented as a symbol such as ':s3' or ':fs'
36
+ # * :credentials - A file, hash, or path used to authenticate with the specified storage medium
37
+
38
+ def initialize(name, instance, options = {})
39
+ @name = name
40
+ @instance = instance
41
+
42
+ @options = self.class.options.merge(options)
43
+ end
44
+
45
+
46
+ # Usage:
47
+ #
48
+ # @object.avatar.assign(...)
49
+
50
+ def assign(file)
51
+ @file = file
52
+
53
+ extension = File.extname(file.original_filename)
54
+
55
+ instance_set :size, file.size
56
+ instance_set :extension, extension
57
+ end
58
+
59
+
60
+ # Usage:
61
+ #
62
+ # @object.avatar.save
63
+
64
+ def save
65
+ @storage ||= Attached::Storage.medium(options[:storage], options[:credentials])
66
+
67
+ storage.save(self.file, self.path) if self.file
68
+ end
69
+
70
+
71
+ # Usage:
72
+ #
73
+ # @object.avatar.destroy
74
+
75
+ def destroy
76
+ @storage ||= Attached::Storage.medium(options[:storage], options[:credentials])
77
+
78
+ storage.destroy(self.path)
79
+ end
80
+
81
+
82
+ # Usage:
83
+ #
84
+ # @object.avatar.url
85
+ # @object.avatar.url(:small)
86
+ # @object.avatar.url(:large)
87
+
88
+ def url(style = :original)
89
+ @storage ||= Attached::Storage.medium(options[:storage], options[:credentials])
90
+
91
+ return "#{options[:protocol]}://#{@storage.host}#{path(style)}"
92
+ end
93
+
94
+
95
+ # Access the URL
96
+ #
97
+ # Usage:
98
+ #
99
+ # @object.avatar.url
100
+ # @object.avatar.url(:small)
101
+ # @object.avatar.url(:large)
102
+
103
+ def path(style = :original)
104
+ path = String.new(options[:path])
105
+
106
+ path.gsub!(/:id/, instance.id.to_s)
107
+ path.gsub!(/:name/, name.to_s)
108
+ path.gsub!(/:style/, style.to_s)
109
+ path.gsub!(/:extension/, extension(style).to_s)
110
+
111
+ return path
112
+ end
113
+
114
+ # Access the size for an attachment.
115
+ #
116
+ # Usage:
117
+ #
118
+ # @object.avatar.size
119
+
120
+ def size
121
+ return instance_get(:size)
122
+ end
123
+
124
+
125
+ # Access the extension for an attachment.
126
+ #
127
+ # Usage:
128
+ #
129
+ # @object.avatar.extension
130
+
131
+ def extension(style)
132
+ return options[:styles][style][:extension] if style and options[:styles][style]
133
+
134
+ return instance_get(:extension)
135
+ end
136
+
137
+
138
+ private
139
+
140
+
141
+ # Helper function for setting instance variables.
142
+ #
143
+ # Usage:
144
+ #
145
+ # self.instance_set(size, 12345)
146
+
147
+ def instance_set(attribute, value)
148
+ setter = :"#{self.name}_#{attribute}="
149
+ self.instance.send(setter, value)
150
+ end
151
+
152
+
153
+ # Helper function for getting instance variables.
154
+ #
155
+ # Usage:
156
+ #
157
+ # self.instance_get(size)
158
+
159
+ def instance_get(attribute)
160
+ getter = :"#{self.name}_#{attribute}"
161
+ self.instance.send(getter)
162
+ end
163
+
164
+
165
+ end
166
+
167
+ end
@@ -0,0 +1,12 @@
1
+ require 'attached'
2
+ require 'rails'
3
+
4
+ module Attached
5
+ class Railtie < Rails::Railtie
6
+ initializer 'attached.initialize' do
7
+ ActiveSupport.on_load(:active_record) do
8
+ ActiveRecord::Base.send :include, Attached
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ require 'attached/storage/base'
2
+ require 'attached/storage/fs'
3
+ require 'attached/storage/s3'
4
+
5
+ module Attached
6
+ module Storage
7
+
8
+ # Create a storage object given a medium and credentials.
9
+ #
10
+ # Usage:
11
+ #
12
+ # Attached::Storage.medium(:fs)
13
+ # Attached::Storage.medium(:s3)
14
+
15
+ def self.medium(storage = :fs, credentials = nil)
16
+
17
+ case storage
18
+ when :fs then return Attached::Storage::FS.new(credentials)
19
+ when :s3 then return Attached::Storage::S3.new(credentials)
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,72 @@
1
+ require 'attached/storage/base'
2
+
3
+ module Attached
4
+ module Storage
5
+ class Base
6
+
7
+
8
+ # Helper for parsing credentials from a hash, file, or string.
9
+ #
10
+ # Usage:
11
+ #
12
+ # parse({...})
13
+ # parse(File.open(...))
14
+ # Parse("...")
15
+
16
+ def parse(credentials)
17
+ case credentials
18
+ when Hash then credentials
19
+ when File then YAML::load(credentials)[Rails.env]
20
+ when String then YAML::load(File.read(credentials))[Rails.env]
21
+ else raise ArgumentError.new("credentials must be a hash, file, or string")
22
+ end
23
+ end
24
+
25
+
26
+ # Create a new file system storage interface supporting save and destroy operations.
27
+ #
28
+ # Usage:
29
+ #
30
+ # Base.new()
31
+
32
+ def initialize(credentials = nil)
33
+ raise NotImplementedError.new
34
+ end
35
+
36
+
37
+ # Access the host (e.g. localhost:3000) for a storage service.
38
+ #
39
+ # Usage:
40
+ #
41
+ # storage.host
42
+
43
+ def host()
44
+ raise NotImplementedError.new
45
+ end
46
+
47
+
48
+ # Save a file to a given path (abstract).
49
+ #
50
+ # Parameters:
51
+ #
52
+ # * file - The file to save.
53
+ # * path - The path to save.
54
+
55
+ def save(file, path)
56
+ raise NotImplementedError.new
57
+ end
58
+
59
+
60
+ # Destroy a file at a given path (abstract).
61
+ #
62
+ # Parameters:
63
+ #
64
+ # * path - The path to destroy.
65
+
66
+ def destroy(path)
67
+ raise NotImplementedError.new
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,39 @@
1
+ module Attached
2
+ module Storage
3
+ class FS < Base
4
+
5
+
6
+ # Create a new file system storage interface supporting save and destroy operations.
7
+ #
8
+ # Usage:
9
+ #
10
+ # FS.new()
11
+
12
+ def initialize(credentials = nil)
13
+ credentials = parse(credentials)
14
+ end
15
+
16
+
17
+ # Save a file to a given path on a file system.
18
+ #
19
+ # Parameters:
20
+ #
21
+ # * file - The file to save.
22
+ # * path - The path to save.
23
+
24
+ def save(file, path)
25
+ end
26
+
27
+
28
+ # Destroy a file at a given path on a file system.
29
+ #
30
+ # Parameters:
31
+ #
32
+ # * path - The path to destroy.
33
+
34
+ def destroy(path)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,77 @@
1
+ module Attached
2
+ module Storage
3
+ class S3 < Base
4
+
5
+ attr_reader :bucket
6
+ attr_reader :access_key_id
7
+ attr_reader :secret_access_key
8
+
9
+
10
+ # Create a new Amazon S3 storage interface supporting save and destroy operations.
11
+ #
12
+ # Usage:
13
+ #
14
+ # Attached::Storage::S3.new()
15
+ # Attached::Storage::S3.new("#{Rails.root}/config/s3.yml")
16
+
17
+ def initialize(credentials = "#{Rails.root}/config/s3.yml")
18
+ credentials = parse(credentials)
19
+
20
+ @bucket = credentials[:bucket] || credentials['bucket']
21
+ @access_key_id = credentials[:access_key_id] || credentials['access_key_id']
22
+ @secret_access_key = credentials[:secret_access_key] || credentials['secret_access_key']
23
+ end
24
+
25
+
26
+ # Access the host (e.g. bucket.s3.amazonaws.com) for a storage service.
27
+ #
28
+ # Usage:
29
+ #
30
+ # storage.host
31
+
32
+ def host()
33
+ "#{self.bucket}.s3.amazonaws.com"
34
+ end
35
+
36
+
37
+ # Save a file to a given path on Amazon S3.
38
+ #
39
+ # Parameters:
40
+ #
41
+ # * file - The file to save.
42
+ # * path - The path to save.
43
+
44
+ def save(file, path)
45
+ connect()
46
+ AWS::S3::S3Object.store(path, file, bucket)
47
+ end
48
+
49
+
50
+ # Destroy a file at a given path on Amazon S3.
51
+ #
52
+ # Parameters:
53
+ #
54
+ # * path - The path to destroy.
55
+
56
+ def destroy(path)
57
+ connect()
58
+ AWS::S3::S3Object.delete(path, bucket)
59
+ end
60
+
61
+
62
+ private
63
+
64
+
65
+ # Connect to an Amazon S3 server.
66
+
67
+ def connect
68
+ return AWS::S3::Base.establish_connection!(
69
+ :access_key_id => self.access_key_id,
70
+ :secret_access_key => self.secret_access_key
71
+ )
72
+ end
73
+
74
+
75
+ end
76
+ end
77
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attached
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Kevin Sylvestre
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-12-01 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: aws-s3
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :runtime
32
+ version_requirements: *id001
33
+ 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.
34
+ email:
35
+ - kevin@ksylvest.com
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files: []
41
+
42
+ files:
43
+ - lib/attached/attachment.rb
44
+ - lib/attached/railtie.rb
45
+ - lib/attached/storage/base.rb
46
+ - lib/attached/storage/fs.rb
47
+ - lib/attached/storage/s3.rb
48
+ - lib/attached/storage.rb
49
+ - lib/attached.rb
50
+ - README.rdoc
51
+ - LICENSE
52
+ - Gemfile
53
+ has_rdoc: true
54
+ homepage: http://github.com/ksylvest/attached
55
+ licenses: []
56
+
57
+ post_install_message:
58
+ rdoc_options: []
59
+
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.7
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: An attachment library designed with cloud processors in mind
85
+ test_files: []
86
+