cached_uploads 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/cached_uploads.rb +239 -0
  3. metadata +58 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d59707f56c697db5af61bdd98144ce4a18f08e54
4
+ data.tar.gz: 2e637d81b72f52c6cc4152f2b02ee0dd9284c3f1
5
+ SHA512:
6
+ metadata.gz: f710fa820e7e70e2107dfa355dbcfb7f929d07c3c9b563f2bad350ef756cc39fbc37fc61a112e84e40ca6877867b8118c473aeb0c933790809ef506770cab0d8
7
+ data.tar.gz: 09362d5bcb469ba58c70369c2ec65daba22c514be4000ad67f42f1b654c48165f7a445133fbbcea76455508dc117b5ded6e2d3ffc04abb8b254991ff7d55ff89
@@ -0,0 +1,239 @@
1
+ require 'fileutils'
2
+ require 'digest/md5'
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext/class/attribute'
5
+ require 'active_support/core_ext/numeric/time'
6
+ require 'active_support/core_ext/hash/reverse_merge'
7
+
8
+ # This module enables you to upload files and persist them in a cache in the event that
9
+ # the form must be redisplayed (e.g. due to validation errors). This module also saves
10
+ # the files to their final location when the record is saved.
11
+ #
12
+ # The controller is still responsible for the overall workflow. It should use the normal
13
+ # ActiveRecord API, with one addition: If the submission is invalid, the controller must
14
+ # call #write_temporary_files.
15
+ #
16
+ # The controller (or cron task, or a worker process) should also call
17
+ # .clean_temporary_files from time to time. An easy option is to clean up any time an
18
+ # invalid submission is received.
19
+ module CachedUploads
20
+ extend ActiveSupport::Concern
21
+
22
+ def delete_permanent_file(file_attr)
23
+ config = self.class.cached_uploads[file_attr.to_sym]
24
+ prm_path = send config[:prm_path_method]
25
+ if File.exists?(prm_path)
26
+ File.delete prm_path
27
+ end
28
+ end
29
+
30
+ def write_permanent_file(file_attr)
31
+ config = self.class.cached_uploads[file_attr.to_sym]
32
+ uploaded_file = send file_attr
33
+
34
+ if uploaded_file.present?
35
+ # This *won't* execute if we've set the temporary file MD5 attribute instead of the
36
+ # file attribute. It will only execute if we're uploading the file for the first time.
37
+ # (Technically, if both the file and the MD5 are present, this would execute. But that
38
+ # would be an error state.)
39
+
40
+ prm_file_path = send config[:prm_path_method]
41
+ File.open(prm_file_path, 'wb') do |out_file|
42
+ uploaded_file.rewind
43
+ out_file.write uploaded_file.read
44
+ end
45
+ elsif send(config[:md5_attr]).present?
46
+ # This executes if we've set the temporary file MD5 attribute instead of the file
47
+ # attribute. This is invoked when the user has submitted invalid data at least once.
48
+ # In which case we've saved the uploaded data to a tempfile on the server. Now the
49
+ # user is resubmitting with correct data. (We know it's correct because
50
+ # #write_permanent_file is triggered by an after_save callback.)
51
+
52
+ tmp_file_path = send config[:tmp_path_method]
53
+ prm_file_path = send config[:prm_path_method]
54
+ FileUtils.cp tmp_file_path, prm_file_path
55
+ end
56
+ end
57
+
58
+ # Writes the temporary file, basing its name on the MD5 hash of the uploaded file.
59
+ # Raises if the uploaded file is #blank?.
60
+ def write_temporary_file(file_attr)
61
+ config = self.class.cached_uploads[file_attr.to_sym]
62
+ file = send file_attr
63
+
64
+ if file.present?
65
+ # Read the uploaded file, calc its MD5, and write the MD5 instance variable.
66
+ file.rewind
67
+ md5 = Digest::MD5.hexdigest(file.read)
68
+ send "#{config[:md5_attr]}=", md5
69
+
70
+ # Write the temporary file, using its MD5 hash to generate the filename.
71
+ file.rewind
72
+ File.open(send(config[:tmp_path_method]), 'wb') do |out_file|
73
+ out_file.write file.read
74
+ end
75
+ else
76
+ raise "Called #write_temporary_file(:#{file_attr}), but ##{file_attr} was not present."
77
+ end
78
+ end
79
+
80
+ # Saves any configured temporary files. Controllers should call this method when an
81
+ # invalid submission is received. Does not save a temporary file if the file itself
82
+ # had a validation error.
83
+ def write_temporary_files
84
+ cached_uploads.each_key do |file_attr|
85
+ if errors[:file].empty? and send(file_attr).present?
86
+ write_temporary_file file_attr
87
+ end
88
+ end
89
+ end
90
+
91
+ included do
92
+ class_attribute :cached_uploads
93
+ end
94
+
95
+ module ClassMethods
96
+ # Cleans out all temporary files older than the given age.
97
+ # Typically called by the controller.
98
+ def clean_temporary_files
99
+ cached_uploads.each do |file_attr, config|
100
+ folder = send config[:tmp_folder_method]
101
+ pattern = File.join folder, '*'
102
+
103
+ Dir.glob(pattern).each do |path|
104
+ if File.basename(path) != '.gitignore' and File.mtime(path) < config[:tmp_file_expiration].ago
105
+ File.delete path
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ # Reader and writer methods will be defined for the +file_attr+ name.
112
+ #
113
+ # CachedUploads looks for class and instance methods defining the permanent file path,
114
+ # the temporary file path, and the temporary file folder path. If the file attribute
115
+ # is for example #screenshot, then the path methods will be, respectively,
116
+ # #screenshot_path, #tmp_screenshot_path, and .tmp_screenshot folder. The names of
117
+ # those methods may be overridden--see the "Options" documentation below.
118
+ #
119
+ # You may define the path methods like any normal instance method. For convenience
120
+ # and to keep all related code in one place, you may also pass Procs when you call
121
+ # .has_cached_uploads, and the methods will be defined for you using the Procs. For
122
+ # example, instead of explicitly defining a #screenshot_path method, you can do this:
123
+ #
124
+ # has_cached_upload(:screenshot, {
125
+ # folder: ->() { File.join Rails.root, 'uploads/screenshots' }
126
+ # filename: ->(obj) { "#{obj.id}.png" }
127
+ # })
128
+ #
129
+ # CachedUploads will then automatically define #screenshot_path using the given
130
+ # +folder+ and +filename+ Procs.
131
+ #
132
+ # You may also define an instance attribute storing the uploaded file's extension.
133
+ # If +file_attr+ is #screenshot, then by default the extension attribute is
134
+ # #screenshot_ext. If the class responds to the extension attribute, then that
135
+ # attribute will be set automatically when the file attribute's writer is called.
136
+ # For example, if the file attribute is #screenshot, then calling #screenshot= will
137
+ # cause #screenshot_ext to be set.
138
+ #
139
+ # Options:
140
+ #
141
+ # - +folder+: A Proc that returns an absolute path to the permanent files' folder.
142
+ #
143
+ # - +filename+: A Proc that accepts an instance of the class and returns the permanent
144
+ # filename. Do not include the folder path in the returned value.
145
+ #
146
+ # - +tmp_folder+: Like +folder+, but for the temporary files.
147
+ #
148
+ # - +tmp_filename+: Like +filename+, but for the temporary files.
149
+ #
150
+ # - +prm_path_method+: Name of the instance method that returns the path to the
151
+ # permanent file. Defaults to +"#{file_attr}_path"+.
152
+ #
153
+ # - +tmp_path_method+: Name of the instance method that returns the path to the
154
+ # temporary file. Defaults to +"tmp_#{file_attr}_path"+.
155
+ #
156
+ # - +tmp_folder_method+: Name of the instance method that returns the path to the
157
+ # temporary files' folder. Defaults to +"tmp_#{file_attr}_folder"+.
158
+ #
159
+ # - +tmp_file_expiration+: Optional. Length of time temporary files last before being
160
+ # cleaned out. Defaults to +48.hours+.
161
+ #
162
+ # - +ext_attr+: Name of the instance attribute storing the file's extension. Defaults
163
+ # to +"#{file_attr}_ext"+.
164
+ #
165
+ # - +md5_attr+: Name of the instance attribute storing the file's MD5 hash. Defaults
166
+ # to +"tmp_#{file_attr}_md5"+.
167
+ def has_cached_upload(file_attr, options = {})
168
+ # Set default configs.
169
+ options.reverse_merge!(
170
+ prm_path_method: "#{file_attr}_path",
171
+ tmp_path_method: "tmp_#{file_attr}_path",
172
+ tmp_folder_method: "tmp_#{file_attr}_folder",
173
+ tmp_file_expiration: 48.hours,
174
+ ext_attr: "#{file_attr}_ext",
175
+ md5_attr: "tmp_#{file_attr}_md5"
176
+ )
177
+
178
+ # Initialize the configs hash.
179
+ self.cached_uploads ||= {}
180
+ cached_uploads[file_attr.to_sym] = options
181
+
182
+ # Define the reader for the file.
183
+ attr_reader file_attr
184
+
185
+ # Define the writer for the file.
186
+ class_eval %Q(
187
+ def #{file_attr}=(f)
188
+ @#{file_attr} = f
189
+ if respond_to?('#{options[:ext_attr]}=')
190
+ self.#{options[:ext_attr]} = File.extname(f.original_filename)
191
+ end
192
+ end
193
+ )
194
+
195
+ # Define the accessor for the temporary file MD5 string.
196
+ attr_accessor options[:md5_attr]
197
+
198
+ # Define the path methods, if given.
199
+ if options[:folder] and options[:filename]
200
+ define_singleton_method "#{file_attr}_folder" do
201
+ options[:folder].call self
202
+ end
203
+
204
+ define_method "#{file_attr}_filename" do
205
+ options[:filename].call(self)
206
+ end
207
+
208
+ define_method "#{file_attr}_path" do
209
+ File.join self.class.send("#{file_attr}_folder"), send("#{file_attr}_filename")
210
+ end
211
+ end
212
+
213
+ # Define the temporary path methods, if given.
214
+ if options[:tmp_folder] and options[:tmp_filename]
215
+ define_singleton_method "tmp_#{file_attr}_folder" do
216
+ options[:tmp_folder].call self
217
+ end
218
+
219
+ define_method "tmp_#{file_attr}_filename" do
220
+ options[:tmp_filename].call(self)
221
+ end
222
+
223
+ define_method "tmp_#{file_attr}_path" do
224
+ File.join self.class.send("tmp_#{file_attr}_folder"), send("tmp_#{file_attr}_filename")
225
+ end
226
+ end
227
+
228
+ # Register the save callback.
229
+ after_save do |obj|
230
+ obj.write_permanent_file file_attr
231
+ end
232
+
233
+ # Register the delete callback.
234
+ after_destroy do |obj|
235
+ obj.delete_permanent_file file_attr
236
+ end
237
+ end
238
+ end
239
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cached_uploads
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jarrett Colby
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: turn
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Caches file uploads when validation fails.
28
+ email: jarrett@madebyhq.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/cached_uploads.rb
34
+ homepage: https://github.com/jarrett/cached_uploads
35
+ licenses:
36
+ - MIT
37
+ metadata: {}
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 2.2.2
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: Validation-friendly uploads for Rails
58
+ test_files: []