refile 0.5.5 → 0.6.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/lib/refile.rb +252 -27
  3. data/lib/refile/app.rb +55 -14
  4. data/lib/refile/attacher.rb +39 -40
  5. data/lib/refile/attachment.rb +28 -13
  6. data/lib/refile/attachment/active_record.rb +90 -1
  7. data/lib/refile/attachment_definition.rb +47 -0
  8. data/lib/refile/backend/s3.rb +1 -147
  9. data/lib/refile/backend_macros.rb +13 -5
  10. data/lib/refile/custom_logger.rb +3 -1
  11. data/lib/refile/file.rb +9 -0
  12. data/lib/refile/image_processing.rb +1 -143
  13. data/lib/refile/rails.rb +30 -0
  14. data/lib/refile/rails/attachment_helper.rb +27 -16
  15. data/lib/refile/signature.rb +5 -0
  16. data/lib/refile/simple_form.rb +17 -0
  17. data/lib/refile/version.rb +1 -1
  18. data/spec/refile/active_record_helper.rb +11 -0
  19. data/spec/refile/app_spec.rb +197 -20
  20. data/spec/refile/attachment/active_record_spec.rb +298 -1
  21. data/spec/refile/attachment_helper_spec.rb +39 -0
  22. data/spec/refile/attachment_spec.rb +53 -5
  23. data/spec/refile/backend_examples.rb +13 -2
  24. data/spec/refile/backend_macros_spec.rb +27 -6
  25. data/spec/refile/custom_logger_spec.rb +2 -3
  26. data/spec/refile/features/direct_upload_spec.rb +18 -0
  27. data/spec/refile/features/multiple_upload_spec.rb +122 -0
  28. data/spec/refile/features/normal_upload_spec.rb +5 -3
  29. data/spec/refile/features/presigned_upload_spec.rb +4 -0
  30. data/spec/refile/features/simple_form_spec.rb +8 -0
  31. data/spec/refile/fixtures/monkey.txt +1 -0
  32. data/spec/refile/fixtures/world.txt +1 -0
  33. data/spec/refile/spec_helper.rb +21 -11
  34. data/spec/refile_spec.rb +253 -24
  35. metadata +12 -303
  36. data/.gitignore +0 -27
  37. data/.rspec +0 -2
  38. data/.rubocop.yml +0 -68
  39. data/.travis.yml +0 -21
  40. data/.yardopts +0 -1
  41. data/CONTRIBUTING.md +0 -33
  42. data/Gemfile +0 -3
  43. data/History.md +0 -96
  44. data/LICENSE.txt +0 -22
  45. data/README.md +0 -651
  46. data/Rakefile +0 -19
  47. data/app/assets/javascripts/refile.js +0 -63
  48. data/config.ru +0 -8
  49. data/config/locales/en.yml +0 -8
  50. data/config/routes.rb +0 -5
  51. data/refile.gemspec +0 -42
  52. data/spec/refile/backend/s3_spec.rb +0 -11
  53. data/spec/refile/test_app.rb +0 -65
  54. data/spec/refile/test_app/app/assets/javascripts/application.js +0 -42
  55. data/spec/refile/test_app/app/controllers/application_controller.rb +0 -2
  56. data/spec/refile/test_app/app/controllers/direct_posts_controller.rb +0 -15
  57. data/spec/refile/test_app/app/controllers/home_controller.rb +0 -4
  58. data/spec/refile/test_app/app/controllers/normal_posts_controller.rb +0 -48
  59. data/spec/refile/test_app/app/controllers/presigned_posts_controller.rb +0 -31
  60. data/spec/refile/test_app/app/models/post.rb +0 -5
  61. data/spec/refile/test_app/app/views/direct_posts/new.html.erb +0 -20
  62. data/spec/refile/test_app/app/views/home/index.html.erb +0 -1
  63. data/spec/refile/test_app/app/views/layouts/application.html.erb +0 -14
  64. data/spec/refile/test_app/app/views/normal_posts/_form.html.erb +0 -28
  65. data/spec/refile/test_app/app/views/normal_posts/edit.html.erb +0 -1
  66. data/spec/refile/test_app/app/views/normal_posts/index.html +0 -5
  67. data/spec/refile/test_app/app/views/normal_posts/new.html.erb +0 -1
  68. data/spec/refile/test_app/app/views/normal_posts/show.html.erb +0 -19
  69. data/spec/refile/test_app/app/views/presigned_posts/new.html.erb +0 -16
  70. data/spec/refile/test_app/config/database.yml +0 -7
  71. data/spec/refile/test_app/config/routes.rb +0 -17
  72. data/spec/refile/test_app/public/favicon.ico +0 -0
@@ -1,27 +1,32 @@
1
1
  module Refile
2
2
  # @api private
3
3
  class Attacher
4
- attr_reader :record, :name, :cache, :store, :options, :errors, :type, :valid_extensions, :valid_content_types
4
+ attr_reader :definition, :record, :errors
5
5
  attr_accessor :remove
6
6
 
7
7
  Presence = ->(val) { val if val != "" }
8
8
 
9
- def initialize(record, name, cache:, store:, raise_errors: true, type: nil, extension: nil, content_type: nil)
9
+ def initialize(definition, record)
10
+ @definition = definition
10
11
  @record = record
11
- @name = name
12
- @raise_errors = raise_errors
13
- @cache = Refile.backends.fetch(cache.to_s)
14
- @store = Refile.backends.fetch(store.to_s)
15
- @type = type
16
- @valid_extensions = [extension].flatten if extension
17
- @valid_content_types = [content_type].flatten if content_type
18
- @valid_content_types ||= Refile.types.fetch(type).content_type if type
19
12
  @errors = []
20
13
  @metadata = {}
21
14
  end
22
15
 
16
+ def name
17
+ @definition.name
18
+ end
19
+
20
+ def cache
21
+ @definition.cache
22
+ end
23
+
24
+ def store
25
+ @definition.store
26
+ end
27
+
23
28
  def id
24
- Presence[read(:id)]
29
+ Presence[read(:id, true)]
25
30
  end
26
31
 
27
32
  def size
@@ -66,7 +71,7 @@ module Refile
66
71
  end
67
72
 
68
73
  def set(value)
69
- if value.is_a?(String)
74
+ if value.is_a?(String) or value.is_a?(Hash)
70
75
  retrieve!(value)
71
76
  else
72
77
  cache!(value)
@@ -74,9 +79,12 @@ module Refile
74
79
  end
75
80
 
76
81
  def retrieve!(value)
77
- @metadata = JSON.parse(value, symbolize_names: true) || {}
82
+ if value.is_a?(String)
83
+ @metadata = Refile.parse_json(value, symbolize_names: true) || {}
84
+ elsif value.is_a?(Hash)
85
+ @metadata = value
86
+ end
78
87
  write_metadata if cache_id
79
- rescue JSON::ParserError
80
88
  end
81
89
 
82
90
  def cache!(uploadable)
@@ -88,7 +96,7 @@ module Refile
88
96
  if valid?
89
97
  @metadata[:id] = cache.upload(uploadable).id
90
98
  write_metadata
91
- elsif @raise_errors
99
+ elsif @definition.raise_errors?
92
100
  raise Refile::Invalid, @errors.join(", ")
93
101
  end
94
102
  end
@@ -98,29 +106,30 @@ module Refile
98
106
  response = RestClient::Request.new(method: :get, url: url, raw_response: true).execute
99
107
  @metadata = {
100
108
  size: response.file.size,
101
- filename: ::File.basename(url),
109
+ filename: URI.parse(url).path.split("/").last,
102
110
  content_type: response.headers[:content_type]
103
111
  }
104
112
  if valid?
113
+ response.file.open if response.file.closed? # https://github.com/refile/refile/pull/210
105
114
  @metadata[:id] = cache.upload(response.file).id
106
115
  write_metadata
107
- elsif @raise_errors
116
+ elsif @definition.raise_errors?
108
117
  raise Refile::Invalid, @errors.join(", ")
109
118
  end
110
119
  end
111
120
  rescue RestClient::Exception
112
121
  @errors = [:download_failed]
113
- raise if @raise_errors
122
+ raise if @definition.raise_errors?
114
123
  end
115
124
 
116
125
  def store!
117
126
  if remove?
118
127
  delete!
119
- write(:id, nil)
128
+ write(:id, nil, true)
120
129
  elsif cache_id
121
130
  file = store.upload(get)
122
131
  delete!
123
- write(:id, file.id)
132
+ write(:id, file.id, true)
124
133
  end
125
134
  write_metadata
126
135
  @metadata = {}
@@ -132,14 +141,6 @@ module Refile
132
141
  @metadata = {}
133
142
  end
134
143
 
135
- def accept
136
- if valid_content_types
137
- valid_content_types.join(",")
138
- elsif valid_extensions
139
- valid_extensions.map { |e| ".#{e}" }.join(",")
140
- end
141
- end
142
-
143
144
  def remove?
144
145
  remove and remove != "" and remove !~ /\A0|false$\z/
145
146
  end
@@ -148,29 +149,27 @@ module Refile
148
149
  not @metadata.empty?
149
150
  end
150
151
 
151
- def valid?
152
- @errors = []
153
- @errors << :invalid_extension if valid_extensions and not valid_extensions.include?(extension)
154
- @errors << :invalid_content_type if valid_content_types and not valid_content_types.include?(content_type)
155
- @errors << :too_large if cache.max_size and size and size >= cache.max_size
156
- @errors.empty?
157
- end
158
-
159
152
  def data
160
153
  @metadata if valid?
161
154
  end
162
155
 
156
+ def valid?
157
+ @errors = @definition.validate(self)
158
+ @errors.empty?
159
+ end
160
+
163
161
  private
164
162
 
165
- def read(column)
163
+ def read(column, strict = false)
166
164
  m = "#{name}_#{column}"
167
- value ||= record.send(m) if record.respond_to?(m)
165
+ value ||= record.send(m) if strict or record.respond_to?(m)
168
166
  value
169
167
  end
170
168
 
171
- def write(column, value)
169
+ def write(column, value, strict = false)
170
+ return if record.frozen?
172
171
  m = "#{name}_#{column}="
173
- record.send(m, value) if record.respond_to?(m) and not record.frozen?
172
+ record.send(m, value) if strict or record.respond_to?(m)
174
173
  end
175
174
 
176
175
  def write_metadata
@@ -17,10 +17,11 @@ module Refile
17
17
  # - `remove_image=`
18
18
  # - `remote_image_url`
19
19
  # - `remote_image_url=`
20
+ # - `image_url`
20
21
  #
21
22
  # @example
22
23
  # class User
23
- # extends Refile::Attachment
24
+ # extend Refile::Attachment
24
25
  #
25
26
  # attachment :image
26
27
  # attr_accessor :image_id
@@ -34,24 +35,30 @@ module Refile
34
35
  # @param [String, Array<String>, nil] extension Limit the uploaded file to the given extension or list of extensions
35
36
  # @param [String, Array<String>, nil] content_type Limit the uploaded file to the given content type or list of content types
36
37
  # @return [void]
37
- # @ignore
38
- # rubocop:disable Metrics/MethodLength
39
38
  def attachment(name, cache: :cache, store: :store, raise_errors: true, type: nil, extension: nil, content_type: nil)
39
+ definition = AttachmentDefinition.new(name,
40
+ cache: cache,
41
+ store: store,
42
+ raise_errors: raise_errors,
43
+ type: type,
44
+ extension: extension,
45
+ content_type: content_type
46
+ )
47
+
48
+ define_singleton_method :"#{name}_attachment_definition" do
49
+ definition
50
+ end
51
+
40
52
  mod = Module.new do
41
53
  attacher = :"#{name}_attacher"
42
54
 
55
+ define_method :"#{name}_attachment_definition" do
56
+ definition
57
+ end
58
+
43
59
  define_method attacher do
44
60
  ivar = :"@#{attacher}"
45
- instance_variable_get(ivar) or begin
46
- instance_variable_set(ivar, Attacher.new(self, name,
47
- cache: cache,
48
- store: store,
49
- raise_errors: raise_errors,
50
- type: type,
51
- extension: extension,
52
- content_type: content_type
53
- ))
54
- end
61
+ instance_variable_get(ivar) or instance_variable_set(ivar, Attacher.new(definition, self))
55
62
  end
56
63
 
57
64
  define_method "#{name}=" do |value|
@@ -77,6 +84,14 @@ module Refile
77
84
  define_method "remote_#{name}_url" do
78
85
  end
79
86
 
87
+ define_method "#{name}_url" do |*args|
88
+ Refile.attachment_url(self, name, *args)
89
+ end
90
+
91
+ define_method "#{name}_data" do
92
+ send(attacher).data
93
+ end
94
+
80
95
  define_singleton_method("to_s") { "Refile::Attachment(#{name})" }
81
96
  define_singleton_method("inspect") { "Refile::Attachment(#{name})" }
82
97
  end
@@ -5,6 +5,7 @@ module Refile
5
5
 
6
6
  # Attachment method which hooks into ActiveRecord models
7
7
  #
8
+ # @return [void]
8
9
  # @see Refile::Attachment#attachment
9
10
  def attachment(name, raise_errors: false, **options)
10
11
  super
@@ -15,10 +16,27 @@ module Refile
15
16
  if send(attacher).present?
16
17
  send(attacher).valid?
17
18
  errors = send(attacher).errors
18
- self.errors.add(name, *errors) unless errors.empty?
19
+ errors.each do |error|
20
+ self.errors.add(name, error)
21
+ end
19
22
  end
20
23
  end
21
24
 
25
+ define_method "#{name}=" do |value|
26
+ send("#{name}_id_will_change!")
27
+ super(value)
28
+ end
29
+
30
+ define_method "remove_#{name}=" do |value|
31
+ send("#{name}_id_will_change!")
32
+ super(value)
33
+ end
34
+
35
+ define_method "remote_#{name}_url=" do |value|
36
+ send("#{name}_id_will_change!")
37
+ super(value)
38
+ end
39
+
22
40
  before_save do
23
41
  send(attacher).store!
24
42
  end
@@ -27,6 +45,77 @@ module Refile
27
45
  send(attacher).delete!
28
46
  end
29
47
  end
48
+
49
+ # Macro which generates accessors for assigning multiple attachments at
50
+ # once. This is primarily useful together with multiple file uploads.
51
+ #
52
+ # The name of the generated accessors will be the name of the association
53
+ # and the name of the attachment in the associated model. So if a `Post`
54
+ # accepts attachments for `images`, and the attachment in the `Image`
55
+ # model is named `file`, then the accessors will be named `images_files`.
56
+ #
57
+ # @example in model
58
+ # class Post
59
+ # has_many :images, dependent: :destroy
60
+ # accepts_attachments_for :images
61
+ # end
62
+ #
63
+ # @example in associated model
64
+ # class Image
65
+ # attachment :image
66
+ # end
67
+ #
68
+ # @example in form
69
+ # <%= form_for @post do |form| %>
70
+ # <%= form.attachment_field :images_files, multiple: true %>
71
+ # <% end %>
72
+ #
73
+ # @param [Symbol] association_name Name of the association
74
+ # @param [Symbol] attachment Name of the attachment in the associated model
75
+ # @param [Symbol] append If true, new files are appended instead of replacing the entire list of associated models.
76
+ # @return [void]
77
+ def accepts_attachments_for(association_name, attachment: :file, append: false)
78
+ association = reflect_on_association(association_name)
79
+ name = "#{association_name}_#{attachment.to_s.pluralize}"
80
+
81
+ mod = Module.new do
82
+ define_method :"#{name}_attachment_definition" do
83
+ association.klass.send("#{attachment}_attachment_definition")
84
+ end
85
+
86
+ define_method :"#{name}_data" do
87
+ if send(association_name).all? { |record| record.send("#{attachment}_attacher").valid? }
88
+ send(association_name).map(&:"#{attachment}_data").select(&:present?)
89
+ end
90
+ end
91
+
92
+ define_method :"#{name}" do
93
+ send(association_name).map(&attachment)
94
+ end
95
+
96
+ define_method :"#{name}=" do |files|
97
+ cache, files = files.partition { |file| file.is_a?(String) }
98
+
99
+ cache = Refile.parse_json(cache.first)
100
+
101
+ if not append and (files.present? or cache.present?)
102
+ send("#{association_name}=", [])
103
+ end
104
+
105
+ if files.empty? and cache.present?
106
+ cache.select(&:present?).each do |file|
107
+ send(association_name).build(attachment => file.to_json)
108
+ end
109
+ else
110
+ files.select(&:present?).each do |file|
111
+ send(association_name).build(attachment => file)
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ include mod
118
+ end
30
119
  end
31
120
  end
32
121
  end
@@ -0,0 +1,47 @@
1
+ module Refile
2
+ # @api private
3
+ class AttachmentDefinition
4
+ attr_reader :record, :name, :cache, :store, :options, :type, :valid_extensions, :valid_content_types
5
+ attr_accessor :remove
6
+
7
+ def initialize(name, cache:, store:, raise_errors: true, type: nil, extension: nil, content_type: nil)
8
+ @name = name
9
+ @raise_errors = raise_errors
10
+ @cache_name = cache
11
+ @store_name = store
12
+ @type = type
13
+ @valid_extensions = [extension].flatten if extension
14
+ @valid_content_types = [content_type].flatten if content_type
15
+ @valid_content_types ||= Refile.types.fetch(type).content_type if type
16
+ end
17
+
18
+ def cache
19
+ Refile.backends.fetch(@cache_name.to_s)
20
+ end
21
+
22
+ def store
23
+ Refile.backends.fetch(@store_name.to_s)
24
+ end
25
+
26
+ def accept
27
+ if valid_content_types
28
+ valid_content_types.join(",")
29
+ elsif valid_extensions
30
+ valid_extensions.map { |e| ".#{e}" }.join(",")
31
+ end
32
+ end
33
+
34
+ def raise_errors?
35
+ @raise_errors
36
+ end
37
+
38
+ def validate(attacher)
39
+ errors = []
40
+ extension_included = valid_extensions && valid_extensions.map(&:downcase).include?(attacher.extension.to_s.downcase)
41
+ errors << :invalid_extension if valid_extensions and not extension_included
42
+ errors << :invalid_content_type if valid_content_types and not valid_content_types.include?(attacher.content_type)
43
+ errors << :too_large if cache.max_size and attacher.size and attacher.size >= cache.max_size
44
+ errors
45
+ end
46
+ end
47
+ end
@@ -1,147 +1 @@
1
- require "aws-sdk"
2
- require "open-uri"
3
-
4
- module Refile
5
- module Backend
6
- # A refile backend which stores files in Amazon S3
7
- #
8
- # @example
9
- # backend = Refile::Backend::S3.new(
10
- # access_key_id: "xyz",
11
- # secret_access_key: "abcd1234",
12
- # bucket: "my-bucket",
13
- # prefix: "files"
14
- # )
15
- # file = backend.upload(StringIO.new("hello"))
16
- # backend.read(file.id) # => "hello"
17
- class S3
18
- extend Refile::BackendMacros
19
-
20
- attr_reader :access_key_id, :max_size
21
-
22
- # Sets up an S3 backend with the given credentials.
23
- #
24
- # @param [String] access_key_id
25
- # @param [String] secret_access_key
26
- # @param [String] bucket The name of the bucket where files will be stored
27
- # @param [String] prefix A prefix to add to all files. Prefixes on S3 are kind of like folders.
28
- # @param [Integer, nil] max_size The maximum size of an uploaded file
29
- # @param [#hash] hasher A hasher which is used to generate ids from files
30
- # @param [Hash] s3_options Additional options to initialize S3 with
31
- # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/Core/Configuration.html
32
- # @see http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3.html
33
- def initialize(access_key_id:, secret_access_key:, bucket:, max_size: nil, prefix: nil, hasher: Refile::RandomHasher.new, **s3_options)
34
- @access_key_id = access_key_id
35
- @secret_access_key = secret_access_key
36
- @s3_options = { access_key_id: access_key_id, secret_access_key: secret_access_key }.merge s3_options
37
- @s3 = AWS::S3.new @s3_options
38
- @bucket_name = bucket
39
- @bucket = @s3.buckets[@bucket_name]
40
- @hasher = hasher
41
- @prefix = prefix
42
- @max_size = max_size
43
- end
44
-
45
- # Upload a file into this backend
46
- #
47
- # @param [IO] uploadable An uploadable IO-like object.
48
- # @return [Refile::File] The uploaded file
49
- verify_uploadable def upload(uploadable)
50
- id = @hasher.hash(uploadable)
51
-
52
- if uploadable.is_a?(Refile::File) and uploadable.backend.is_a?(S3) and uploadable.backend.access_key_id == access_key_id
53
- uploadable.backend.object(uploadable.id).copy_to(object(id))
54
- else
55
- object(id).write(uploadable, content_length: uploadable.size)
56
- end
57
-
58
- Refile::File.new(self, id)
59
- end
60
-
61
- # Get a file from this backend.
62
- #
63
- # Note that this method will always return a {Refile::File} object, even
64
- # if a file with the given id does not exist in this backend. Use
65
- # {FileSystem#exists?} to check if the file actually exists.
66
- #
67
- # @param [Sring] id The id of the file
68
- # @return [Refile::File] The retrieved file
69
- verify_id def get(id)
70
- Refile::File.new(self, id)
71
- end
72
-
73
- # Delete a file from this backend
74
- #
75
- # @param [Sring] id The id of the file
76
- # @return [void]
77
- verify_id def delete(id)
78
- object(id).delete
79
- end
80
-
81
- # Return an IO object for the uploaded file which can be used to read its
82
- # content.
83
- #
84
- # @param [Sring] id The id of the file
85
- # @return [IO] An IO object containing the file contents
86
- verify_id def open(id)
87
- Kernel.open(object(id).url_for(:read))
88
- end
89
-
90
- # Return the entire contents of the uploaded file as a String.
91
- #
92
- # @param [String] id The id of the file
93
- # @return [String] The file's contents
94
- verify_id def read(id)
95
- object(id).read
96
- rescue AWS::S3::Errors::NoSuchKey
97
- nil
98
- end
99
-
100
- # Return the size in bytes of the uploaded file.
101
- #
102
- # @param [Sring] id The id of the file
103
- # @return [Integer] The file's size
104
- verify_id def size(id)
105
- object(id).content_length
106
- rescue AWS::S3::Errors::NoSuchKey
107
- nil
108
- end
109
-
110
- # Return whether the file with the given id exists in this backend.
111
- #
112
- # @param [Sring] id The id of the file
113
- # @return [Boolean]
114
- verify_id def exists?(id)
115
- object(id).exists?
116
- end
117
-
118
- # Remove all files in this backend. You must confirm the deletion by
119
- # passing the symbol `:confirm` as an argument to this method.
120
- #
121
- # @example
122
- # backend.clear!(:confirm)
123
- # @raise [Refile::Confirm] Unless the `:confirm` symbol has been passed.
124
- # @param [:confirm] confirm Pass the symbol `:confirm` to confirm deletion.
125
- # @return [void]
126
- def clear!(confirm = nil)
127
- raise Refile::Confirm unless confirm == :confirm
128
- @bucket.objects.with_prefix(@prefix).delete_all
129
- end
130
-
131
- # Return a presign signature which can be used to upload a file into this
132
- # backend directly.
133
- #
134
- # @return [Refile::Signature]
135
- def presign
136
- id = RandomHasher.new.hash
137
- signature = @bucket.presigned_post(key: [*@prefix, id].join("/"))
138
- signature.where(content_length: @max_size) if @max_size
139
- Signature.new(as: "file", id: id, url: signature.url.to_s, fields: signature.fields)
140
- end
141
-
142
- verify_id def object(id)
143
- @bucket.objects[[*@prefix, id].join("/")]
144
- end
145
- end
146
- end
147
- end
1
+ raise "[Refile] the S3 backend has been extracted into a separate gem, see https://github.com/refile/refile-s3"