attach 1.1.2 → 2.0.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b8c3f1129d2c35650ecb7d9b0c137d20a6a2265f2304c5976b4d4dfc252ee15
4
- data.tar.gz: ac3a03c541d799dcae47442d84fd47249ec257d2566da242d6e8d4d09ad8b0e2
3
+ metadata.gz: 67e480487c042fe887ff3c518fc372df0e9ed21890f25df6b15b7ac408ff63f9
4
+ data.tar.gz: 046166beef6d41bea1fe55927d5006ab7512aa57a2201b15abfcc8cca94a8832
5
5
  SHA512:
6
- metadata.gz: 6195ce53ff7b626f3051448e00c8ebec9cbca5dd715c728bcc68052e1ec54753fc65d31c3f4cced08bfc04a0768672191921dbe3620299848b0a8688e82e082d
7
- data.tar.gz: 8a877be8bac066f7fa36ab33f81481c38aa15f96f63df7f3c9462ee256ec97ed650c116b74a7598cd34d6be458e9fca2b1ba9c35c5fcbfa42a9933cbc6b543fe
6
+ metadata.gz: b8b1e30d31ef5782738cd01e4bc4ce6528472e014a77e7ef1e8be80407f787390be4cf422e7eee13f6baed150591fbc1e68a4a1f968a85a4109901d2d50e8c65
7
+ data.tar.gz: 8d74cb792d7a9981ca063cc1c9b0b667f2419d36368c600e56bfdb9f351b426d3fa49b0cdbbca98447d8086c0a287f194c2b2d03437d2a3a699a9669acc8c119
@@ -1,18 +1,23 @@
1
- class CreateAttachmentTables < ActiveRecord::Migration
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAttachmentTables < ActiveRecord::Migration[6.0]
4
+
2
5
  def up
3
6
  create_table :attachments do |t|
4
- t.integer :owner_id
5
- t.string :owner_type, :token, :digest, :role, :type, :file_name, :file_type, :cache_type, :cache_max_age, :disposition
6
- t.integer :file_size
7
- t.integer :parent_id
8
- t.boolean :processed, :default => false
7
+ t.belongs_to :owner, polymorphic: true
8
+ t.string :token, :digest, :role, :type, :file_name, :file_type, :cache_type, :cache_max_age, :disposition
9
+ t.bigint :file_size
10
+ t.belongs_to :parent
11
+ t.boolean :processed, default: false
12
+ t.text :custom
13
+ t.boolean :serve, default: false
9
14
  t.timestamps
10
- t.index :owner_id
15
+ t.index :token, length: 16
11
16
  end
12
17
 
13
18
  create_table :attachment_binaries do |t|
14
- t.integer :attachment_id
15
- t.binary :data, :limit => 10.megabytes
19
+ t.belongs_to :attachment
20
+ t.binary :data, limit: 10_485_760
16
21
  t.timestamps
17
22
  end
18
23
  end
@@ -21,4 +26,5 @@ class CreateAttachmentTables < ActiveRecord::Migration
21
26
  drop_table :attachments
22
27
  drop_table :attachment_binaries
23
28
  end
29
+
24
30
  end
@@ -1,116 +1,173 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
  require 'digest/sha1'
3
5
  require 'attach/attachment_binary'
6
+ require 'attach/processor'
7
+ require 'attach/blob_types/raw'
8
+ require 'attach/blob_types/file'
4
9
 
5
10
  module Attach
6
11
  class Attachment < ActiveRecord::Base
7
12
 
8
- # Set the table name
9
13
  self.table_name = 'attachments'
10
14
  self.inheritance_column = 'sti_type'
11
15
 
12
- # This will be the ActionDispatch::UploadedFile object which be diseminated
13
- # by the class on save.
14
- attr_writer :binary
16
+ attr_writer :backend
15
17
 
16
- # Relationships
17
- belongs_to :owner, :polymorphic => true
18
- belongs_to :parent, :class_name => 'Attach::Attachment', :optional => true
19
- has_many :children, :class_name => 'Attach::Attachment', :dependent => :destroy, :foreign_key => :parent_id
18
+ belongs_to :owner, polymorphic: true
19
+ belongs_to :parent, class_name: 'Attach::Attachment', optional: true
20
+ has_many :children, class_name: 'Attach::Attachment', dependent: :destroy, foreign_key: :parent_id
20
21
 
21
- # Validations
22
- validates :file_name, :presence => true
23
- validates :file_type, :presence => true
24
- validates :file_size, :presence => true
25
- validates :digest, :presence => true
26
- validates :token, :presence => true, :uniqueness => true
22
+ validates :file_name, presence: true
23
+ validates :file_type, presence: true
24
+ validates :file_size, presence: true
25
+ validates :digest, presence: true
26
+ validates :token, presence: true, uniqueness: { case_sensitive: false }
27
27
 
28
- # Allow custom data to be stored on the attachment
29
- serialize :custom, Hash
28
+ serialize :custom, type: Hash, default: {}
30
29
 
31
- # Set size and digest
32
- before_validation do
33
- self.token ||= SecureRandom.uuid
34
- self.digest ||= self.binary.is_a?(String) ? Digest::SHA1.hexdigest(self.binary) : Attach.backend.digest(self.binary)
35
- self.file_size ||= self.binary.is_a?(String) ? self.binary.bytesize : Attach.backend.bytesize(self.binary)
36
- end
30
+ before_validation :set_token
31
+ before_validation :set_digest
32
+ before_validation :set_file_size
37
33
 
38
- # Write the binary to the backend storage
39
- after_create do
40
- if self.binary
41
- Attach.backend.write(self, self.binary)
42
- end
43
- end
34
+ after_create :write_blob_to_backend
35
+ after_create :destroy_other_attachments_for_same_parent
44
36
 
45
- # Remove any old images for this owner/role when this is added
46
- after_create do
47
- self.owner.attachments.where.not(:id => self).where(:parent_id => self.parent_id, :role => self.role).destroy_all
48
- end
37
+ after_commit :queue_or_process_with_processor
49
38
 
50
- # Run any post-upload processing after the record has been committed
51
- after_commit do
52
- unless self.processed? || self.parent_id
53
- self.processor.queue_or_process
54
- end
55
- end
39
+ after_destroy :remove_from_backend
56
40
 
57
- # Remove the file from the backends
58
- after_destroy do
59
- Attach.backend.delete(self)
60
- end
41
+ def blob
42
+ return @blob if instance_variable_defined?('@blob')
43
+ return nil unless persisted?
61
44
 
62
- # Return the attachment for a given role
63
- def self.for(role)
64
- self.where(:role => role).first
45
+ @blob = backend.read(self)
65
46
  end
66
47
 
67
- # Return the binary data for this attachment
68
- def binary
69
- @binary ||= persisted? ? Attach.backend.read(self) : nil
70
- @binary == :nil ? nil : @binary
48
+ def blob=(blob)
49
+ unless blob.nil? || blob.is_a?(BlobTypes::File) || blob.is_a?(BlobTypes::Raw)
50
+ raise ArgumentError, 'Only nil or a File/Raw blob type can be set as a blob for an attachment'
51
+ end
52
+
53
+ @blob = blob
71
54
  end
72
55
 
73
- # Return the path to the attachment
74
56
  def url
75
- Attach.backend.url(self)
57
+ backend.url(self)
76
58
  end
77
59
 
78
- # Is the attachment an image?
79
60
  def image?
80
61
  file_type =~ /\Aimage\//
81
62
  end
82
63
 
83
- # Return a processor for this attachment
84
64
  def processor
85
65
  @processor ||= Processor.new(self)
86
66
  end
87
67
 
88
- # Return a child process
89
68
  def child(role)
90
69
  @cached_children ||= {}
91
- @cached_children[role.to_sym] ||= self.children.where(:role => role).first || :nil
92
- @cached_children[role.to_sym] == :nil ? nil : @cached_children[role.to_sym]
70
+ if @cached_children.key?(role.to_sym)
71
+ return @cached_children[role.to_sym]
72
+ end
73
+
74
+ @cached_children[role.to_sym] = children.where(role: role).first
93
75
  end
94
76
 
95
- # Try to return a given otherwise revert to the parent
96
77
  def try(role)
97
78
  child(role) || self
98
79
  end
99
80
 
100
- # Add a child attachment
81
+ # rubocop:disable Metrics/AbcSize
101
82
  def add_child(role, &block)
102
- attachment = self.children.build
83
+ attachment = children.build
103
84
  attachment.role = role
104
- attachment.owner = self.owner
105
- attachment.file_name = self.file_name
106
- attachment.file_type = self.file_type
107
- attachment.disposition = self.disposition
108
- attachment.cache_type = self.cache_type
109
- attachment.cache_max_age = self.cache_max_age
110
- attachment.type = self.type
85
+ attachment.owner = owner
86
+ attachment.file_name = file_name
87
+ attachment.file_type = file_type
88
+ attachment.disposition = disposition
89
+ attachment.cache_type = cache_type
90
+ attachment.cache_max_age = cache_max_age
91
+ attachment.serve = serve
92
+ attachment.type = type
111
93
  block.call(attachment)
112
94
  attachment.save!
113
95
  end
96
+ # rubocop:enable Metrics/AbcSize
97
+
98
+ # rubocop:disable Metrics/AbcSize
99
+ def copy_attributes_from_file(file)
100
+ case file.class.name
101
+ when 'ActionDispatch::Http::UploadedFile'
102
+ self.blob = BlobTypes::File.new(file.tempfile)
103
+ self.file_name = file.original_filename
104
+ self.file_type = file.content_type
105
+ when 'Attach::File'
106
+ self.blob = BlobTypes::Raw.new(file.data)
107
+ self.file_name = file.name
108
+ self.file_type = file.type
109
+ else
110
+ self.blob = BlobTypes::Raw.new(file)
111
+ self.file_name = 'untitled'
112
+ self.file_type = 'application/octet-stream'
113
+ end
114
+ end
115
+ # rubocop:enable Metrics/AbcSize
116
+
117
+ private
118
+
119
+ def backend
120
+ @backend || Attach.backend
121
+ end
122
+
123
+ def write_blob_to_backend
124
+ return if blob.blank?
125
+
126
+ backend.write(self, blob)
127
+ end
128
+
129
+ def destroy_other_attachments_for_same_parent
130
+ owner.attachments.where.not(id: self).where(parent_id: parent_id, role: role).destroy_all
131
+ end
132
+
133
+ def set_token
134
+ return if token.present?
135
+
136
+ self.token = SecureRandom.uuid
137
+ end
138
+
139
+ def set_digest
140
+ return if digest.present?
141
+ return if blob.blank?
142
+
143
+ self.digest = blob.digest
144
+ end
145
+
146
+ def set_file_size
147
+ return if file_size.present?
148
+ return if blob.blank?
149
+
150
+ self.file_size = blob.size
151
+ end
152
+
153
+ def remove_from_backend
154
+ backend.delete(self)
155
+ end
156
+
157
+ def queue_or_process_with_processor
158
+ return if processed?
159
+ return if parent_id
160
+
161
+ processor.queue_or_process
162
+ end
163
+
164
+ class << self
165
+
166
+ def for(role)
167
+ where(role: role).first
168
+ end
169
+
170
+ end
114
171
 
115
172
  end
116
173
  end
@@ -1,11 +1,11 @@
1
- require 'securerandom'
2
- require 'digest/sha1'
1
+ # frozen_string_literal: true
3
2
 
4
3
  module Attach
5
4
  class AttachmentBinary < ActiveRecord::Base
6
5
 
7
- # Set the table name
8
6
  self.table_name = 'attachment_binaries'
9
7
 
8
+ belongs_to :attachment
9
+
10
10
  end
11
11
  end
@@ -1,15 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attach
2
4
  class AttachmentDSL
3
5
 
4
- attr_reader :processors
5
- attr_reader :validators
6
+ attr_reader :processors, :validators
6
7
 
7
8
  def initialize(&block)
8
9
  @processors = []
9
10
  @validators = []
10
- if block_given?
11
- instance_eval(&block)
12
- end
11
+ instance_eval(&block) if block_given?
13
12
  end
14
13
 
15
14
  def processor(*processors, &block)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attach
2
4
  module Backends
3
5
  class Abstract
@@ -7,19 +9,19 @@ module Attach
7
9
  end
8
10
 
9
11
  #
10
- # Return the data for the given attachment
12
+ #  Return the data for the given attachment
11
13
  #
12
14
  def read(attachment)
13
15
  end
14
16
 
15
17
  #
16
- # Write data for the given attachment
18
+ #  Write data for the given attachment
17
19
  #
18
20
  def write(attachment, data)
19
21
  end
20
22
 
21
23
  #
22
- # Delete the data for the given attachment
24
+ #  Delete the data for the given attachment
23
25
  #
24
26
  def delete(attachment)
25
27
  end
@@ -32,7 +34,7 @@ module Attach
32
34
  end
33
35
 
34
36
  #
35
- # Return binaries for a set of files. They should be returned as a hash consisting
37
+ #  Return binaries for a set of files. They should be returned as a hash consisting
36
38
  # of the attachment ID followed by the data
37
39
  #
38
40
  def read_multi(attachments)
@@ -1,37 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'attach/attachment_binary'
1
4
  require 'attach/backends/abstract'
5
+ require 'attach/blob_types/raw'
2
6
 
3
7
  module Attach
4
8
  module Backends
5
9
  class Database < Abstract
6
10
 
7
11
  def read(attachment)
8
- if binary = AttachmentBinary.find_by_attachment_id(attachment.id)
9
- binary.data
10
- else
11
- nil
12
- end
12
+ binary = AttachmentBinary.find_by(attachment_id: attachment.id)
13
+ return if binary.nil?
14
+
15
+ BlobTypes::Raw.new(binary.data)
13
16
  end
14
17
 
15
- def write(attachment, binary)
16
- binary_object = AttachmentBinary.where(:attachment_id => attachment.id).first_or_initialize
17
- if binary.respond_to?(:path)
18
- binary.rewind
19
- binary_object.data = binary.read
20
- else
21
- binary_object.data = binary
22
- end
18
+ def write(attachment, blob)
19
+ binary_object = AttachmentBinary.where(attachment_id: attachment.id).first_or_initialize
20
+ binary_object.data = blob.read
23
21
  binary_object.save!
24
22
  binary_object
25
23
  end
26
24
 
27
25
  def delete(attachment)
28
- AttachmentBinary.where(:attachment_id => attachment.id).destroy_all
29
- end
30
-
31
- def read_multi(attachments)
32
- AttachmentBinary.where(:attachment_id => attachments.map(&:id)).each_with_object({}) do |binary, hash|
33
- hash[binary.attachment_id] = binary.data
34
- end
26
+ AttachmentBinary.where(attachment_id: attachment.id).destroy_all
35
27
  end
36
28
 
37
29
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fileutils'
2
4
  require 'attach/backends/abstract'
3
5
 
@@ -6,24 +8,28 @@ module Attach
6
8
  class FileSystem < Abstract
7
9
 
8
10
  def read(attachment)
9
- ::File.read(path_for_attachment(attachment))
11
+ file = File.new(path_for_attachment(attachment))
12
+ BlobTypes::File.new(file)
10
13
  end
11
14
 
12
- def write(attachment, data)
15
+ def write(attachment, blob)
13
16
  path = path_for_attachment(attachment)
14
17
  FileUtils.mkdir_p(::File.dirname(path))
15
- if data.respond_to?(:path)
16
- FileUtils.mv(data.path, path)
17
- else
18
- ::File.open(path, 'wb') do |f|
19
- f.write(data)
20
- end
18
+
19
+ if blob.is_a?(BlobTypes::File)
20
+ FileUtils.mv(blob.file.path, path)
21
+ return path
21
22
  end
23
+
24
+ ::File.binwrite(path, blob.read)
25
+
26
+ path
22
27
  end
23
28
 
24
29
  def delete(attachment)
25
30
  path = path_for_attachment(attachment)
26
31
  FileUtils.rm(path) if ::File.file?(path)
32
+ path
27
33
  end
28
34
 
29
35
  private
@@ -33,7 +39,7 @@ module Attach
33
39
  end
34
40
 
35
41
  def path_for_attachment(attachment)
36
- ::File.join(root_dir, attachment.token[0,2], attachment.token[2,2], attachment.token[4,40])
42
+ ::File.join(root_dir, attachment.token[0, 2], attachment.token[2, 2], attachment.token[4, 40])
37
43
  end
38
44
 
39
45
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Attach
4
+ module BlobTypes
5
+ class File
6
+
7
+ attr_reader :file
8
+
9
+ def initialize(file)
10
+ @file = file
11
+ end
12
+
13
+ def read
14
+ @file.rewind
15
+ @file.read
16
+ end
17
+
18
+ def size
19
+ @file.size
20
+ end
21
+
22
+ def digest
23
+ Digest::SHA1.file(@file.path).to_s
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Attach
4
+ module BlobTypes
5
+ class Raw
6
+
7
+ def initialize(data)
8
+ @data = data
9
+ end
10
+
11
+ def read
12
+ @data
13
+ end
14
+
15
+ def size
16
+ @data.bytesize
17
+ end
18
+
19
+ def digest
20
+ Digest::SHA1.hexdigest(@data)
21
+ end
22
+
23
+ end
24
+ end
25
+ end
data/lib/attach/file.rb CHANGED
@@ -1,11 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attach
2
4
  class File
3
5
 
4
- attr_accessor :data
5
- attr_accessor :name
6
- attr_accessor :type
6
+ attr_accessor :data, :name, :type
7
7
 
8
- def initialize(data, name = "untitled", type = "application/octet-stream")
8
+ def initialize(data, name = 'untitled', type = 'application/octet-stream')
9
9
  @data = data
10
10
  @name = name
11
11
  @type = type
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attach/attachment'
2
4
 
3
5
  module Attach
@@ -8,21 +10,28 @@ module Attach
8
10
  end
9
11
 
10
12
  def call(env)
11
- if env['PATH_INFO'] =~ /\A\/attachment\/([a-f0-9\-]{36})\/(.*)/
12
- if attachment = Attach::Attachment.where(:serve => true).find_by_token($1)
13
- [200, {
14
- 'Content-Length' => attachment.file_size.to_s,
15
- 'Content-Type' => attachment.file_type,
16
- 'Cache-Control' => "#{attachment.cache_type || 'private'}, immutable, max-age=#{attachment.cache_max_age || 30.days.to_i}",
17
- 'Content-Disposition' => "#{attachment.disposition || 'attachment'}; filename=\"#{attachment.file_name}\""
18
- },
19
- [attachment.binary]]
20
- else
21
- [404, {}, ["Attachment not found"]]
22
- end
23
- else
24
- @app.call(env)
13
+ unless env['PATH_INFO'] =~ /\A\/attachment\/([a-f0-9-]{36})\/(.*)/
14
+ return @app.call(env)
15
+ end
16
+
17
+ attachment = Attach::Attachment.where(serve: true).find_by(token: Regexp.last_match(1))
18
+ if attachment.nil?
19
+ return [404, {}, ['Attachment not found']]
25
20
  end
21
+
22
+ [200, headers_for_attachment(attachment), [attachment.blob.read]]
23
+ end
24
+
25
+ private
26
+
27
+ def headers_for_attachment(attachment)
28
+ max_age = attachment.cache_max_age || 30.days.to_i
29
+ {
30
+ 'Content-Length' => attachment.file_size.to_s,
31
+ 'Content-Type' => attachment.file_type,
32
+ 'Cache-Control' => "#{attachment.cache_type || 'private'}, immutable, max-age=#{max_age}",
33
+ 'Content-Disposition' => "#{attachment.disposition || 'attachment'}; filename=\"#{attachment.file_name}\""
34
+ }
26
35
  end
27
36
 
28
37
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'attach/attachment_dsl'
4
+ require 'attach/attachment'
5
+ require 'attach/model_extension/inclusion'
6
+
7
+ require 'records_manipulator/relation_extension'
8
+ ActiveRecord::Relation.include RecordsManipulator::RelationExtension
9
+
10
+ require 'records_manipulator/base_extension'
11
+ ActiveRecord::Base.include RecordsManipulator::BaseExtension
12
+
13
+ module Attach
14
+ module ModelExtension
15
+ module ClassMethods
16
+
17
+ def attachment_validators
18
+ @attachment_validators ||= {}
19
+ end
20
+
21
+ def attachment_processors
22
+ @attachment_processors ||= {}
23
+ end
24
+
25
+ def attachment(name, **options, &block)
26
+ setup_model
27
+ parse_dsl(name, &block)
28
+
29
+ define_method name do
30
+ get_attachment(name)
31
+ end
32
+
33
+ define_method "#{name}=" do |file|
34
+ set_attachment(name, file, **options)
35
+ end
36
+
37
+ define_method "#{name}_delete" do
38
+ instance_variable_get("@#{name}_delete")
39
+ end
40
+
41
+ define_method "#{name}_delete=" do |delete|
42
+ delete = delete.to_i
43
+ instance_variable_set("@#{name}_delete", delete)
44
+ if delete == 1
45
+ @pending_attachment_deletions ||= []
46
+ @pending_attachment_deletions << name
47
+ end
48
+ end
49
+ end
50
+
51
+ def includes_attachment(*args, **options)
52
+ manipulate do |scope|
53
+ inclusion = Inclusion.new(scope, *args, **options)
54
+ inclusion.prepare
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def setup_model
61
+ return if reflect_on_all_associations(:has_many).map(&:name).include?(:attachments)
62
+
63
+ has_many :attachments, as: :owner, dependent: :destroy, class_name: 'Attach::Attachment'
64
+ validate :validate_attachments
65
+ after_save :process_pending_attachments
66
+ end
67
+
68
+ def parse_dsl(name, &block)
69
+ dsl = AttachmentDSL.new(&block)
70
+ attachment_validators[name.to_sym] = dsl.validators
71
+ attachment_processors[name.to_sym] = dsl.processors
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Attach
4
+ module ModelExtension
5
+ class Inclusion
6
+
7
+ def initialize(scope, *options)
8
+ @scope = scope
9
+ @options = options
10
+ @fields = {}
11
+ end
12
+
13
+ def prepare
14
+ return if @scope.empty?
15
+
16
+ prepare_fields
17
+ find_all_attachments
18
+ find_child_attachments
19
+ add_attachments_to_records
20
+ end
21
+
22
+ private
23
+
24
+ def prepare_fields
25
+ @options.each do |field|
26
+ case field
27
+ when Symbol
28
+ @fields[field] = []
29
+ when Hash
30
+ field.each do |k, v|
31
+ case v
32
+ when Array
33
+ @fields.merge!(field)
34
+ when Symbol
35
+ @fields[k] = [v]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def find_all_attachments
43
+ @attachment_ids = []
44
+ @attachments_map = Attachment.where(
45
+ owner_id: @scope.map(&:id),
46
+ owner_type: @scope.first.class.name,
47
+ role: @fields.keys
48
+ ).each_with_object({}) do |attachment, hash|
49
+ hash[attachment.owner_id] ||= {}
50
+ hash[attachment.owner_id][attachment.role] = attachment
51
+ @attachment_ids << attachment.id
52
+ end
53
+ end
54
+
55
+ def find_child_attachments
56
+ @child_attachments = Attachment.where(
57
+ parent: @attachment_ids,
58
+ role: @fields.values.flatten.compact
59
+ ).each_with_object({}) do |attachment, hash|
60
+ hash[attachment.parent_id] ||= {}
61
+ hash[attachment.parent_id][attachment.role] = attachment
62
+ end
63
+ end
64
+
65
+ def add_attachments_to_records
66
+ @scope.each do |record|
67
+ preloaded_attachments = @attachments_map[record.id] || {}
68
+ @fields.each_key do |role|
69
+ cache_attachment(
70
+ record,
71
+ role,
72
+ preloaded_attachments[role.to_s]
73
+ )
74
+ end
75
+ end
76
+ end
77
+
78
+ def cache_attachment(record, role, attachment)
79
+ record.instance_variable_set("@#{role}", attachment)
80
+ return if attachment.nil?
81
+
82
+ @fields[role.to_sym].each do |child_role|
83
+ child_attachment = @child_attachments.dig(attachment.id, child_role.to_s)
84
+ if attachment.instance_variable_get('@cached_children').nil?
85
+ attachment.instance_variable_set('@cached_children', {})
86
+ end
87
+ attachment.instance_variable_get('@cached_children')[child_role] = child_attachment
88
+ end
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'attach/attachment'
4
+
5
+ module Attach
6
+ module ModelExtension
7
+ module InstanceMethods
8
+
9
+ def process_pending_attachments
10
+ attachments.where(role: @pending_attachment_deletions).destroy_all if @pending_attachment_deletions
11
+
12
+ return if @pending_attachments.nil? || @pending_attachments.empty?
13
+
14
+ @pending_attachments.each_value(&:save!)
15
+ @pending_attachments = nil
16
+ end
17
+
18
+ private
19
+
20
+ def get_attachment(name)
21
+ iv_name = "@#{name}"
22
+ return instance_variable_get(iv_name) if instance_variable_defined?(iv_name)
23
+
24
+ if attachment = attachments.where(role: name, parent_id: nil).first
25
+ return instance_variable_set(iv_name, attachment)
26
+ end
27
+
28
+ instance_variable_set(iv_name, nil)
29
+ end
30
+
31
+ def set_attachment(name, file, **options)
32
+ attachment = Attachment.new({ owner: self, role: name }.merge(options))
33
+ attachment.copy_attributes_from_file(file)
34
+ @pending_attachments ||= {}
35
+ @pending_attachments[name] = attachment
36
+ instance_variable_set("@#{name}", attachment)
37
+ end
38
+
39
+ def validate_attachments
40
+ return if @pending_attachments.nil? || @pending_attachments.empty?
41
+
42
+ @pending_attachments.each_value do |attachment|
43
+ validators = self.class.attachment_validators[attachment.role.to_sym]
44
+ next if validators.blank?
45
+
46
+ validators.each do |validator|
47
+ validator.call(attachment, errors)
48
+ end
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -1,192 +1,16 @@
1
- require 'attach/attachment'
2
- require 'attach/processor'
3
- require 'attach/attachment_dsl'
1
+ # frozen_string_literal: true
2
+
3
+ require 'attach/model_extension/class_methods'
4
+ require 'attach/model_extension/instance_methods'
4
5
 
5
6
  module Attach
6
7
  module ModelExtension
7
8
 
8
- def self.included(base)
9
- base.extend ClassMethods
10
- base.after_save do
11
- if @pending_attachment_deletions
12
- self.attachments.where(:role => @pending_attachment_deletions).destroy_all
13
- end
14
-
15
- @replaced_attachment&.destroy
16
-
17
- if @pending_attachments
18
- @pending_attachments.values.each(&:save!)
19
- @pending_attachments = nil
20
- end
21
- end
22
- end
23
-
24
- module ClassMethods
25
-
26
- def includes_attachments(*options)
27
- manipulate do |records|
28
- if records.empty?
29
- # Nothing to do
30
- else
31
-
32
- if options.first.is_a?(Hash)
33
- options = options.first
34
- binaries_to_include = options.delete(:_include_binaries) || {}
35
- else
36
- binaries_to_include = {}
37
- options = options.each_with_object({}) do |opt, hash|
38
- if opt.is_a?(Symbol) || opt.is_a?(String)
39
- hash[opt.to_sym] = []
40
- elsif opt.is_a?(Hash)
41
- opt.each do |key, value|
42
- if key == :_include_binaries
43
- binaries_to_include = value
44
- else
45
- hash[key.to_sym] = value
46
- end
47
- end
48
- end
49
- end
50
-
51
- end
52
-
53
- options.keys.each do |key|
54
- if options[key].is_a?(Symbol)
55
- options[key] = [options[key]]
56
- elsif options[key] == true
57
- options[key] = []
58
- end
59
- end
60
-
61
- attachments_for_binary_preload = []
62
- root_attachments = {}
63
- Attachment.where(:owner_id => records.map(&:id), :owner_type => records.first.class.to_s, :role => options.keys).each do |attachment|
64
- root_attachments[[attachment.owner_id, attachment.role]] = attachment
65
- if binaries_to_include[attachment.role.to_sym] && binaries_to_include[attachment.role.to_sym].include?(:_self)
66
- attachments_for_binary_preload << attachment
67
- end
68
- end
69
-
70
- child_roles = options.values.flatten
71
- unless child_roles.empty?
72
- child_attachments = {}
73
- Attachment.where(:parent_id => root_attachments.values.map(&:id), :role => child_roles).each do |attachment|
74
- child_attachments[[attachment.parent_id, attachment.role]] = attachment
75
- end
76
-
77
- root_attachments.values.each do |attachment|
78
- options[attachment.role.to_sym].each do |role|
79
- child_attachment = child_attachments[[attachment.id, role.to_s]]
80
-
81
- if child_attachment && binaries_to_include[attachment.role.to_sym] && binaries_to_include[attachment.role.to_sym].include?(role)
82
- attachments_for_binary_preload << child_attachment
83
- end
84
-
85
- attachment.instance_variable_set("@cached_children", {}) if attachment.instance_variable_get("@cached_children").nil?
86
- attachment.instance_variable_get("@cached_children")[role.to_sym] = child_attachments[[attachment.id, role.to_s]] || :nil
87
- end
88
- end
89
- end
90
-
91
- if binaries = Attach.backend.read_multi(attachments_for_binary_preload)
92
- attachments_for_binary_preload.each do |attachment|
93
- attachment.instance_variable_set("@binary", binaries[attachment.id] || :nil)
94
- end
95
- else
96
- # Preloading binaries isn't supported by the backend
97
- end
98
-
99
- records.each do |record|
100
- options.keys.each do |role|
101
- record.instance_variable_set("@#{role}", root_attachments[[record.id, role.to_s]] || :nil)
102
- end
103
- end
104
- end
105
- end
106
- end
107
-
108
- def attachment(name, options = {}, &block)
109
- unless self.reflect_on_all_associations(:has_many).map(&:name).include?(:attachments)
110
- has_many :attachments, :as => :owner, :dependent => :destroy, :class_name => 'Attach::Attachment'
111
- end
112
-
113
- dsl = AttachmentDSL.new(&block)
114
-
115
- dsl.processors.each do |processor|
116
- Processor.register(self, name, &processor)
117
- end
118
-
119
- if dsl.validators.size > 0
120
- validate do
121
- attachment = @pending_attachments && @pending_attachments[name] ? @pending_attachments[name] : send(name)
122
- file_errors = []
123
- dsl.validators.each do |validator|
124
- validator.call(attachment, file_errors)
125
- end
126
- file_errors.each { |e| errors.add("#{name}_file", e) }
127
- end
128
- end
129
-
130
- define_method name do
131
- var = instance_variable_get("@#{name}")
132
- if var
133
- var == :nil ? nil : var
134
- else
135
- if attachment = self.attachments.where(:role => name, :parent_id => nil).first
136
- instance_variable_set("@#{name}", attachment)
137
- else
138
- instance_variable_set("@#{name}", :nil)
139
- nil
140
- end
141
- end
142
- end
143
-
144
- define_method "#{name}=" do |file|
145
- if file.is_a?(Attach::Attachment)
146
- @replaced_attachment = self.try(name)
147
- attachment = file
148
- attachment.owner = self
149
- attachment.role = name
150
- attachment.processed = false
151
- elsif file
152
- attachment = Attachment.new({:owner => self, :role => name}.merge(options))
153
- case file
154
- when ActionDispatch::Http::UploadedFile
155
- attachment.binary = file.tempfile
156
- attachment.file_name = file.original_filename
157
- attachment.file_type = file.content_type
158
- when Attach::File
159
- attachment.binary = file.data
160
- attachment.file_name = file.name
161
- attachment.file_type = file.type
162
- else
163
- attachment.binary = file
164
- attachment.file_name = "untitled"
165
- attachment.file_type = "application/octet-stream"
166
- end
167
- end
168
-
169
- if attachment
170
- @pending_attachments ||= {}
171
- @pending_attachments[name] = attachment
172
- end
173
- instance_variable_set("@#{name}", attachment)
174
- end
175
-
176
- define_method "#{name}_delete" do
177
- instance_variable_get("@#{name}_delete")
178
- end
179
-
180
- define_method "#{name}_delete=" do |delete|
181
- delete = delete.to_i
182
- instance_variable_set("@#{name}_delete", delete)
183
- if delete == 1
184
- @pending_attachment_deletions ||= []
185
- @pending_attachment_deletions << name
186
- end
187
- end
188
- end
9
+ extend ActiveSupport::Concern
189
10
 
11
+ included do
12
+ extend ClassMethods
13
+ include InstanceMethods
190
14
  end
191
15
 
192
16
  end
@@ -1,49 +1,50 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attach
2
4
  class Processor
3
5
 
4
- def self.background(&block)
5
- @background_block = block
6
- end
6
+ class << self
7
7
 
8
- def self.background_block
9
- @background_block
10
- end
8
+ attr_reader :background_block
11
9
 
12
- def self.register(model, attribute, &block)
13
- @processors ||= {}
14
- @processors[[model.to_s, attribute.to_s]] ||= []
15
- @processors[[model.to_s, attribute.to_s]] = block
16
- end
10
+ def background(&block)
11
+ @background_block = block
12
+ end
17
13
 
18
- def self.processor(model, attribute)
19
- @processors && @processors[[model.to_s, attribute.to_s]]
20
14
  end
21
-
22
15
  def initialize(attachment)
23
16
  @attachment = attachment
24
17
  end
25
18
 
26
19
  def process
27
- call_processors(@attachment)
28
- @attachment.processed = true
29
- @attachment.save(:validate => false)
20
+ call_processors
21
+ mark_as_processed
30
22
  end
31
23
 
32
24
  def queue_or_process
33
- if self.class.background_block
34
- self.class.background_block.call(@attachment)
35
- else
36
- process
37
- end
25
+ return self.class.background_block.call(@attachment) if self.class.background_block
26
+
27
+ process
38
28
  end
39
29
 
40
30
  private
41
31
 
42
- def call_processors(attachment)
43
- if p = self.class.processor(attachment.owner_type, attachment.role)
44
- p.call(attachment)
32
+ def call_processors
33
+ return if @attachment.role.blank?
34
+ return if @attachment.owner.nil?
35
+
36
+ processors = @attachment.owner.class.attachment_processors[@attachment.role.to_sym]
37
+ return if processors.nil? || processors.empty?
38
+
39
+ processors.each do |processor|
40
+ processor.call(@attachment)
45
41
  end
46
42
  end
47
43
 
44
+ def mark_as_processed
45
+ @attachment.processed = true
46
+ @attachment.save!
47
+ end
48
+
48
49
  end
49
50
  end
@@ -1,18 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attach
2
- class Railtie < Rails::Engine #:nodoc:
4
+ class Railtie < Rails::Engine # :nodoc:
3
5
 
4
6
  engine_name 'attach'
5
7
 
6
8
  initializer 'attach.initialize' do |app|
7
-
8
9
  require 'attach/middleware'
9
10
  app.config.middleware.use Attach::Middleware
10
11
 
11
12
  ActiveSupport.on_load(:active_record) do
12
13
  require 'attach/model_extension'
13
- ::ActiveRecord::Base.send :include, Attach::ModelExtension
14
+ ::ActiveRecord::Base.include Attach::ModelExtension
14
15
  end
15
-
16
16
  end
17
17
 
18
18
  end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Attach
2
- VERSION = '1.1.2'.freeze
4
+
5
+ VERSION = '2.0.3'
6
+
3
7
  end
data/lib/attach.rb CHANGED
@@ -1,32 +1,26 @@
1
- require 'records_manipulator'
2
- require 'attach/processor'
3
- require 'attach/file'
1
+ # frozen_string_literal: true
2
+
4
3
  require 'attach/railtie' if defined?(Rails)
5
4
 
6
5
  module Attach
7
6
 
8
- def self.backend
9
- @backend ||= begin
10
- require 'attach/backends/database'
11
- Attach::Backends::Database.new
12
- end
13
- end
7
+ class << self
14
8
 
15
- def self.backend=(backend)
16
- @backend = backend
17
- end
9
+ attr_writer :backend
10
+ attr_accessor :asset_host
18
11
 
19
- def self.asset_host
20
- @asset_host
21
- end
12
+ def backend
13
+ @backend ||= begin
14
+ require 'attach/backends/database'
15
+ Attach::Backends::Database.new
16
+ end
17
+ end
22
18
 
23
- def self.asset_host=(host)
24
- @asset_host = host
25
- end
19
+ def use_filesystem!(config = {})
20
+ require 'attach/backends/file_system'
21
+ @backend = Attach::Backends::FileSystem.new(config)
22
+ end
26
23
 
27
- def self.use_filesystem!(config = {})
28
- require 'attach/backends/file_system'
29
- @backend = Attach::Backends::FileSystem.new(config)
30
24
  end
31
25
 
32
26
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attach
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 2.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Cooke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-30 00:00:00.000000000 Z
11
+ date: 2023-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: records_manipulator
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,8 +52,6 @@ extensions: []
38
52
  extra_rdoc_files: []
39
53
  files:
40
54
  - db/migrate/20170301120000_create_attachment_tables.rb
41
- - db/migrate/20170314113014_add_custom_data_to_attachments.rb
42
- - db/migrate/20181123160000_add_index_to_attachment_binaries.rb
43
55
  - lib/attach.rb
44
56
  - lib/attach/attachment.rb
45
57
  - lib/attach/attachment_binary.rb
@@ -47,13 +59,18 @@ files:
47
59
  - lib/attach/backends/abstract.rb
48
60
  - lib/attach/backends/database.rb
49
61
  - lib/attach/backends/file_system.rb
62
+ - lib/attach/blob_types/file.rb
63
+ - lib/attach/blob_types/raw.rb
50
64
  - lib/attach/file.rb
51
65
  - lib/attach/middleware.rb
52
66
  - lib/attach/model_extension.rb
67
+ - lib/attach/model_extension/class_methods.rb
68
+ - lib/attach/model_extension/inclusion.rb
69
+ - lib/attach/model_extension/instance_methods.rb
53
70
  - lib/attach/processor.rb
54
71
  - lib/attach/railtie.rb
55
72
  - lib/attach/version.rb
56
- homepage: https://github.com/adamcooke/attach
73
+ homepage: https://github.com/krystal/attach
57
74
  licenses:
58
75
  - MIT
59
76
  metadata: {}
@@ -65,14 +82,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
65
82
  requirements:
66
83
  - - ">="
67
84
  - !ruby/object:Gem::Version
68
- version: '0'
85
+ version: '2.6'
69
86
  required_rubygems_version: !ruby/object:Gem::Requirement
70
87
  requirements:
71
88
  - - ">="
72
89
  - !ruby/object:Gem::Version
73
90
  version: '0'
74
91
  requirements: []
75
- rubygems_version: 3.0.3
92
+ rubygems_version: 3.4.10
76
93
  signing_key:
77
94
  specification_version: 4
78
95
  summary: Attach documents & files to Active Record models
@@ -1,15 +0,0 @@
1
- class AddCustomDataToAttachments < ActiveRecord::Migration
2
-
3
- def up
4
- add_column :attachments, :custom, :text
5
- add_column :attachments, :serve, :boolean, :default => true
6
- add_index :attachments, :token, :length => 10
7
- end
8
-
9
- def down
10
- remove_index :attachments, :token
11
- remove_column :attachments, :custom
12
- remove_column :attachments, :serve
13
- end
14
-
15
- end
@@ -1,11 +0,0 @@
1
- class AddIndexToAttachmentBinaries < ActiveRecord::Migration
2
-
3
- def up
4
- add_index :attachment_binaries, :attachment_id
5
- end
6
-
7
- def down
8
- remove_index :attachment_binaries, :attachment_id
9
- end
10
-
11
- end