attachment_magic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in attachment_magic.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "attachment_magic/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "attachment_magic"
7
+ s.version = AttachmentMagic::VERSION
8
+ s.authors = ["Thomas von Deyen"]
9
+ s.email = ["tvdeyen@gmail.com"]
10
+ s.homepage = "https://github.com/magiclabs/attachment_magic"
11
+ s.summary = %q{A simple file attachment gem for Rails 3}
12
+ s.description = %q{A Rails 3 Gem based on attachment_fu, but without the image processing fudge and multiple backend crap! Just simple file attachments with a little mime type magic ;)}
13
+
14
+ s.rubyforge_project = "attachment_magic"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+ s.add_runtime_dependency(%q<rails>, ["< 3.1", ">= 3.0.7"])
21
+ s.add_runtime_dependency(%q<mimetype-fu>, ["~> 0.1.2"])
22
+ end
@@ -0,0 +1,299 @@
1
+ require "fileutils"
2
+ require 'mimetype_fu'
3
+ require "attachment_magic/version"
4
+ require "attachment_magic/backends/file_system_backend"
5
+
6
+ module AttachmentMagic
7
+ @@content_types = [
8
+ 'image/jpeg',
9
+ 'image/pjpeg',
10
+ 'image/jpg',
11
+ 'image/gif',
12
+ 'image/png',
13
+ 'image/x-png',
14
+ 'image/jpg',
15
+ 'image/x-ms-bmp',
16
+ 'image/bmp',
17
+ 'image/x-bmp',
18
+ 'image/x-bitmap',
19
+ 'image/x-xbitmap',
20
+ 'image/x-win-bitmap',
21
+ 'image/x-windows-bmp',
22
+ 'image/ms-bmp',
23
+ 'application/bmp',
24
+ 'application/x-bmp',
25
+ 'application/x-win-bitmap',
26
+ 'application/preview',
27
+ 'image/jp_',
28
+ 'application/jpg',
29
+ 'application/x-jpg',
30
+ 'image/pipeg',
31
+ 'image/vnd.swiftview-jpeg',
32
+ 'image/x-xbitmap',
33
+ 'application/png',
34
+ 'application/x-png',
35
+ 'image/gi_',
36
+ 'image/x-citrix-pjpeg'
37
+ ]
38
+ mattr_reader :content_types, :tempfile_path
39
+ mattr_writer :tempfile_path
40
+
41
+ class ThumbnailError < StandardError; end
42
+ class AttachmentError < StandardError; end
43
+
44
+ def self.tempfile_path
45
+ @@tempfile_path ||= Rails.root.join('tmp', 'attachment_magic')
46
+ end
47
+
48
+ module ActMethods
49
+ # Options:
50
+ # * <tt>:content_type</tt> - Allowed content types. Allows all by default. Use :image to allow all standard image types.
51
+ # * <tt>:min_size</tt> - Minimum size allowed. 1 byte is the default.
52
+ # * <tt>:max_size</tt> - Maximum size allowed. 1.megabyte is the default.
53
+ # * <tt>:size</tt> - Range of sizes allowed. (1..1.megabyte) is the default. This overrides the :min_size and :max_size options.
54
+ # * <tt>:path_prefix</tt> - path to store the uploaded files. Uses public/#{table_name} by default.
55
+ # * <tt>:storage</tt> - Use :file_system to specify the attachment data is stored with the file system. Defaults to :file_system.
56
+ #
57
+ # Examples:
58
+ # has_attachment :max_size => 1.kilobyte
59
+ # has_attachment :size => 1.megabyte..2.megabytes
60
+ # has_attachment :content_type => 'application/pdf'
61
+ # has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain']
62
+ def has_attachment(options = {})
63
+ # this allows you to redefine the acts' options for each subclass, however
64
+ options[:min_size] ||= 1
65
+ options[:max_size] ||= 1.megabyte
66
+ options[:size] ||= (options[:min_size]..options[:max_size])
67
+ options[:content_type] = [options[:content_type]].flatten.collect! { |t| t == :image ? AttachmentMagic.content_types : t }.flatten unless options[:content_type].nil?
68
+
69
+ extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods)
70
+ include InstanceMethods unless included_modules.include?(InstanceMethods)
71
+
72
+ parent_options = attachment_options || {}
73
+ # doing these shenanigans so that #attachment_options is available to processors and backends
74
+ self.attachment_options = options
75
+
76
+ attachment_options[:storage] ||= :file_system
77
+ attachment_options[:storage] ||= parent_options[:storage]
78
+ attachment_options[:path_prefix] ||= attachment_options[:file_system_path]
79
+ if attachment_options[:path_prefix].nil?
80
+ File.join("public", table_name)
81
+ end
82
+ attachment_options[:path_prefix] = attachment_options[:path_prefix][1..-1] if options[:path_prefix].first == '/'
83
+
84
+ unless File.directory?(AttachmentMagic.tempfile_path)
85
+ FileUtils.mkdir_p(AttachmentMagic.tempfile_path)
86
+ end
87
+
88
+ storage_mod = AttachmentMagic::Backends.const_get("#{options[:storage].to_s.classify}Backend")
89
+ include storage_mod unless included_modules.include?(storage_mod)
90
+
91
+ end
92
+
93
+ def load_related_exception?(e) #:nodoc: implementation specific
94
+ case
95
+ when e.kind_of?(LoadError), e.kind_of?(MissingSourceFile), $!.class.name == "CompilationError"
96
+ # We can't rescue CompilationError directly, as it is part of the RubyInline library.
97
+ # We must instead rescue RuntimeError, and check the class' name.
98
+ true
99
+ else
100
+ false
101
+ end
102
+ end
103
+ private :load_related_exception?
104
+ end
105
+
106
+ module ClassMethods
107
+ delegate :content_types, :to => AttachmentMagic
108
+
109
+ # Performs common validations for attachment models.
110
+ def validates_as_attachment
111
+ validates_presence_of :size, :content_type, :filename
112
+ validate :attachment_attributes_valid?
113
+ end
114
+
115
+ # Returns true or false if the given content type is recognized as an image.
116
+ def image?(content_type)
117
+ content_types.include?(content_type)
118
+ end
119
+
120
+ def self.extended(base)
121
+ base.class_inheritable_accessor :attachment_options
122
+ base.before_validation :set_size_from_temp_path
123
+ base.after_save :after_process_attachment
124
+ base.after_destroy :destroy_file
125
+ base.after_validation :process_attachment
126
+ end
127
+
128
+ # Copies the given file path to a new tempfile, returning the closed tempfile.
129
+ def copy_to_temp_file(file, temp_base_name)
130
+ Tempfile.new(temp_base_name, AttachmentMagic.tempfile_path).tap do |tmp|
131
+ tmp.close
132
+ FileUtils.cp file, tmp.path
133
+ end
134
+ end
135
+
136
+ # Writes the given data to a new tempfile, returning the closed tempfile.
137
+ def write_to_temp_file(data, temp_base_name)
138
+ Tempfile.new(temp_base_name, AttachmentMagic.tempfile_path).tap do |tmp|
139
+ tmp.binmode
140
+ tmp.write data
141
+ tmp.close
142
+ end
143
+ end
144
+ end
145
+
146
+ module InstanceMethods
147
+ def self.included(base)
148
+ base.define_callbacks *[:after_attachment_saved] if base.respond_to?(:define_callbacks)
149
+ end
150
+
151
+ # Sets the content type.
152
+ def content_type=(new_type)
153
+ write_attribute :content_type, new_type.to_s.strip
154
+ end
155
+
156
+ # Detects the mime-type if content_type is 'application/octet-stream'
157
+ def detect_mimetype(file_data)
158
+ if file_data.content_type.strip == "application/octet-stream"
159
+ return File.mime_type?(file_data.original_filename)
160
+ else
161
+ return file_data.content_type
162
+ end
163
+ end
164
+
165
+ # Sanitizes a filename.
166
+ def filename=(new_name)
167
+ write_attribute :filename, sanitize_filename(new_name)
168
+ end
169
+
170
+ # Returns true if the attachment data will be written to the storage system on the next save
171
+ def save_attachment?
172
+ File.file?(temp_path.class == String ? temp_path : temp_path.to_filename)
173
+ end
174
+
175
+ # nil placeholder in case this field is used in a form.
176
+ def uploaded_data() nil; end
177
+
178
+ # This method handles the uploaded file object. If you set the field name to uploaded_data, you don't need
179
+ # any special code in your controller.
180
+ #
181
+ # <% form_for :attachment, :html => { :multipart => true } do |f| -%>
182
+ # <p><%= f.file_field :uploaded_data %></p>
183
+ # <p><%= submit_tag :Save %>
184
+ # <% end -%>
185
+ #
186
+ # @attachment = Attachment.create! params[:attachment]
187
+ #
188
+ # TODO: Allow it to work with Merb tempfiles too.
189
+ def uploaded_data=(file_data)
190
+ if file_data.respond_to?(:content_type)
191
+ return nil if file_data.size == 0
192
+ self.content_type = detect_mimetype(file_data)
193
+ self.filename = file_data.original_filename if respond_to?(:filename)
194
+ else
195
+ return nil if file_data.blank? || file_data['size'] == 0
196
+ self.content_type = file_data['content_type']
197
+ self.filename = file_data['filename']
198
+ file_data = file_data['tempfile']
199
+ end
200
+ if file_data.is_a?(StringIO)
201
+ file_data.rewind
202
+ set_temp_data file_data.read
203
+ else
204
+ self.temp_paths.unshift file_data.tempfile.path
205
+ end
206
+ end
207
+
208
+ # Gets the latest temp path from the collection of temp paths. While working with an attachment,
209
+ # multiple Tempfile objects may be created for various processing purposes (resizing, for example).
210
+ # An array of all the tempfile objects is stored so that the Tempfile instance is held on to until
211
+ # it's not needed anymore. The collection is cleared after saving the attachment.
212
+ def temp_path
213
+ p = temp_paths.first
214
+ p.respond_to?(:path) ? p.path : p.to_s
215
+ end
216
+
217
+ # Gets an array of the currently used temp paths. Defaults to a copy of #full_filename.
218
+ def temp_paths
219
+ @temp_paths ||= (new_record? || !respond_to?(:full_filename) || !File.exist?(full_filename) ?
220
+ [] : [copy_to_temp_file(full_filename)])
221
+ end
222
+
223
+ # Gets the data from the latest temp file. This will read the file into memory.
224
+ def temp_data
225
+ save_attachment? ? File.read(temp_path) : nil
226
+ end
227
+
228
+ # Writes the given data to a Tempfile and adds it to the collection of temp files.
229
+ def set_temp_data(data)
230
+ temp_paths.unshift write_to_temp_file data unless data.nil?
231
+ end
232
+
233
+ # Copies the given file to a randomly named Tempfile.
234
+ def copy_to_temp_file(file)
235
+ self.class.copy_to_temp_file file, random_tempfile_filename
236
+ end
237
+
238
+ # Writes the given file to a randomly named Tempfile.
239
+ def write_to_temp_file(data)
240
+ self.class.write_to_temp_file data, random_tempfile_filename
241
+ end
242
+
243
+ # Stub for creating a temp file from the attachment data. This should be defined in the backend module.
244
+ def create_temp_file() end
245
+
246
+ protected
247
+ # Generates a unique filename for a Tempfile.
248
+ def random_tempfile_filename
249
+ "#{rand Time.now.to_i}#{filename || 'attachment'}"
250
+ end
251
+
252
+ def sanitize_filename(filename)
253
+ return unless filename
254
+ filename.strip.tap do |name|
255
+ # NOTE: File.basename doesn't work right with Windows paths on Unix
256
+ # get only the filename, not the whole path
257
+ name.gsub! /^.*(\\|\/)/, ''
258
+
259
+ # Finally, replace all non alphanumeric, underscore or periods with underscore
260
+ name.gsub! /[^A-Za-z0-9\.\-]/, '_'
261
+ end
262
+ end
263
+
264
+ # before_validation callback.
265
+ def set_size_from_temp_path
266
+ self.size = File.size(temp_path) if save_attachment?
267
+ end
268
+
269
+ # validates the size and content_type attributes according to the current model's options
270
+ def attachment_attributes_valid?
271
+ [:size, :content_type].each do |attr_name|
272
+ enum = attachment_options[attr_name]
273
+ errors.add attr_name, I18n.translate("activerecord.errors.messages.inclusion", attr_name => enum) unless enum.nil? || enum.include?(send(attr_name))
274
+ end
275
+ end
276
+
277
+ # Stub for a #process_attachment method in a processor
278
+ def process_attachment
279
+ @saved_attachment = save_attachment?
280
+ end
281
+
282
+ # Cleans up after processing. Thumbnails are created, the attachment is stored to the backend, and the temp_paths are cleared.
283
+ def after_process_attachment
284
+ if @saved_attachment
285
+ save_to_storage
286
+ @temp_paths.clear
287
+ @saved_attachment = nil
288
+ end
289
+ end
290
+
291
+ def callback_with_args(method, arg = self)
292
+ send(method, arg) if respond_to?(method)
293
+ end
294
+
295
+ end
296
+
297
+ end
298
+
299
+ ActiveRecord::Base.send(:extend, AttachmentMagic::ActMethods)
@@ -0,0 +1,122 @@
1
+ require 'fileutils'
2
+ require 'digest/sha2'
3
+
4
+ module AttachmentMagic # :nodoc:
5
+ module Backends
6
+ # Methods for file system backed attachments
7
+ module FileSystemBackend
8
+ def self.included(base) #:nodoc:
9
+ base.before_update :rename_file
10
+ end
11
+
12
+ # Gets the full path to the filename in this format:
13
+ #
14
+ # # This assumes a model name like MyModel
15
+ # # public/#{table_name} is the default filesystem path
16
+ # Rails.root.to_s/public/my_models/5/blah.jpg
17
+ #
18
+ # Overwrite this method in your model to customize the filename.
19
+ def full_filename
20
+ file_system_path = self.attachment_options[:path_prefix].to_s
21
+ Rails.root.join(file_system_path, *partitioned_path(filename)).to_s
22
+ end
23
+
24
+ # Used as the base path that #public_filename strips off full_filename to create the public path
25
+ def base_path
26
+ @base_path ||= Rails.root.join('public')
27
+ end
28
+
29
+ # The attachment ID used in the full path of a file
30
+ def attachment_path_id
31
+ ((respond_to?(:parent_id) && parent_id) || id) || 0
32
+ end
33
+
34
+ # Partitions the given path into an array of path components.
35
+ #
36
+ # For example, given an <tt>*args</tt> of ["foo", "bar"], it will return
37
+ # <tt>["0000", "0001", "foo", "bar"]</tt> (assuming that that id returns 1).
38
+ #
39
+ # If the id is not an integer, then path partitioning will be performed by
40
+ # hashing the string value of the id with SHA-512, and splitting the result
41
+ # into 4 components. If the id a 128-bit UUID (as set by :uuid_primary_key => true)
42
+ # then it will be split into 2 components.
43
+ #
44
+ # To turn this off entirely, set :partition => false.
45
+ def partitioned_path(*args)
46
+ if respond_to?(:attachment_options) && attachment_options[:partition] == false
47
+ args
48
+ elsif attachment_options[:uuid_primary_key]
49
+ # Primary key is a 128-bit UUID in hex format. Split it into 2 components.
50
+ path_id = attachment_path_id.to_s
51
+ component1 = path_id[0..15] || "-"
52
+ component2 = path_id[16..-1] || "-"
53
+ [component1, component2] + args
54
+ else
55
+ path_id = attachment_path_id
56
+ if path_id.is_a?(Integer)
57
+ # Primary key is an integer. Split it after padding it with 0.
58
+ ("%08d" % path_id).scan(/..../) + args
59
+ else
60
+ # Primary key is a String. Hash it, then split it into 4 components.
61
+ hash = Digest::SHA512.hexdigest(path_id.to_s)
62
+ [hash[0..31], hash[32..63], hash[64..95], hash[96..127]] + args
63
+ end
64
+ end
65
+ end
66
+
67
+ # Gets the public path to the file
68
+ def public_filename()
69
+ full_filename.gsub %r(^#{Regexp.escape(base_path)}), ''
70
+ end
71
+
72
+ def filename=(value)
73
+ @old_filename = full_filename unless filename.nil? || @old_filename
74
+ write_attribute :filename, sanitize_filename(value)
75
+ end
76
+
77
+ # Creates a temp file from the currently saved file.
78
+ def create_temp_file
79
+ copy_to_temp_file full_filename
80
+ end
81
+
82
+ protected
83
+ # Destroys the file. Called in the after_destroy callback
84
+ def destroy_file
85
+ FileUtils.rm full_filename
86
+ # remove directory also if it is now empty
87
+ Dir.rmdir(File.dirname(full_filename)) if (Dir.entries(File.dirname(full_filename))-['.','..']).empty?
88
+ rescue
89
+ logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}"
90
+ logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n")
91
+ end
92
+
93
+ # Renames the given file before saving
94
+ def rename_file
95
+ return unless @old_filename && @old_filename != full_filename
96
+ if save_attachment? && File.exists?(@old_filename)
97
+ FileUtils.rm @old_filename
98
+ elsif File.exists?(@old_filename)
99
+ FileUtils.mv @old_filename, full_filename
100
+ end
101
+ @old_filename = nil
102
+ true
103
+ end
104
+
105
+ # Saves the file to the file system
106
+ def save_to_storage
107
+ if save_attachment?
108
+ # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
109
+ FileUtils.mkdir_p(File.dirname(full_filename))
110
+ FileUtils.cp(temp_path, full_filename)
111
+ FileUtils.chmod(attachment_options[:chmod] || 0644, full_filename)
112
+ end
113
+ @old_filename = nil
114
+ true
115
+ end
116
+
117
+ def current_data
118
+ File.file?(full_filename) ? File.read(full_filename) : nil
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,3 @@
1
+ module AttachmentMagic
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attachment_magic
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Thomas von Deyen
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-08-09 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rails
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - <
28
+ - !ruby/object:Gem::Version
29
+ hash: 5
30
+ segments:
31
+ - 3
32
+ - 1
33
+ version: "3.1"
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ hash: 9
37
+ segments:
38
+ - 3
39
+ - 0
40
+ - 7
41
+ version: 3.0.7
42
+ type: :runtime
43
+ version_requirements: *id001
44
+ - !ruby/object:Gem::Dependency
45
+ name: mimetype-fu
46
+ prerelease: false
47
+ requirement: &id002 !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ~>
51
+ - !ruby/object:Gem::Version
52
+ hash: 31
53
+ segments:
54
+ - 0
55
+ - 1
56
+ - 2
57
+ version: 0.1.2
58
+ type: :runtime
59
+ version_requirements: *id002
60
+ description: A Rails 3 Gem based on attachment_fu, but without the image processing fudge and multiple backend crap! Just simple file attachments with a little mime type magic ;)
61
+ email:
62
+ - tvdeyen@gmail.com
63
+ executables: []
64
+
65
+ extensions: []
66
+
67
+ extra_rdoc_files: []
68
+
69
+ files:
70
+ - .gitignore
71
+ - Gemfile
72
+ - Rakefile
73
+ - attachment_magic.gemspec
74
+ - lib/attachment_magic.rb
75
+ - lib/attachment_magic/backends/file_system_backend.rb
76
+ - lib/attachment_magic/version.rb
77
+ has_rdoc: true
78
+ homepage: https://github.com/magiclabs/attachment_magic
79
+ licenses: []
80
+
81
+ post_install_message:
82
+ rdoc_options: []
83
+
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ requirements: []
105
+
106
+ rubyforge_project: attachment_magic
107
+ rubygems_version: 1.6.2
108
+ signing_key:
109
+ specification_version: 3
110
+ summary: A simple file attachment gem for Rails 3
111
+ test_files: []
112
+