attachment_magic 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+