cached_uploads 0.0.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.
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: []