attach 1.1.2 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b8c3f1129d2c35650ecb7d9b0c137d20a6a2265f2304c5976b4d4dfc252ee15
4
- data.tar.gz: ac3a03c541d799dcae47442d84fd47249ec257d2566da242d6e8d4d09ad8b0e2
3
+ metadata.gz: 3042851dcc040ea003ba31b827da5b7be92a0f63227a803d8243491005967b1d
4
+ data.tar.gz: 025f0b73b80914ce34c1f31ad1462c993516253a5dc3d177b8b4680c910664a0
5
5
  SHA512:
6
- metadata.gz: 6195ce53ff7b626f3051448e00c8ebec9cbca5dd715c728bcc68052e1ec54753fc65d31c3f4cced08bfc04a0768672191921dbe3620299848b0a8688e82e082d
7
- data.tar.gz: 8a877be8bac066f7fa36ab33f81481c38aa15f96f63df7f3c9462ee256ec97ed650c116b74a7598cd34d6be458e9fca2b1ba9c35c5fcbfa42a9933cbc6b543fe
6
+ metadata.gz: 28728c88c83ce64bed3ca9e05509b0aeeaaaa8c3e8388d3a5a19359f144ec91e94c6a425262985befee48ffb374e18c4a8f3652f46d149d23f80021809fcb531
7
+ data.tar.gz: '0285b9d74f7bf30507940bce4cb82b10089ea2f7b563ba83551ddcb55020485fa407b12d4c058d22a4ba49450ea34b419a250151002d0dd800289deadbbd562d'
@@ -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
28
  serialize :custom, Hash
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.2'
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.2
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-05-16 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