attach 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bba016e24c62143435904ab7adec8d742dfc2cff
4
+ data.tar.gz: 7fd96092ed4196d2eb765788adf376bbb8caec46
5
+ SHA512:
6
+ metadata.gz: 1728d88028c90ca2701943199a9fecfe5cbe4b6f3358a3533f617aa279942e05974f94d3bb7089e31e99af1082781a2f9979cb2857d617c07d6399f590ef2e09
7
+ data.tar.gz: abb0b019475de3fb68a433aa99a80facdd95c86701e76ba5fea90c7b097e0bb507337a1404f67571f2aceb9aae8cf1f2ef0248fedc87ca37764e007f7d5f33fa
data/lib/attach.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'records_manipulator'
2
+ require 'attach/processor'
3
+ require 'attach/railtie' if defined?(Rails)
4
+
5
+ module Attach
6
+
7
+ def self.backend
8
+ @backend ||= begin
9
+ require 'attach/backends/database'
10
+ Attach::Backends::Database.new
11
+ end
12
+ end
13
+
14
+ def self.backend=(backend)
15
+ @backend = backend
16
+ end
17
+
18
+ def self.use_filesystem!(config = {})
19
+ require 'attach/backends/file_system'
20
+ @backend = Attach::Backends::FileSystem.new(config)
21
+ end
22
+
23
+ end
@@ -0,0 +1,116 @@
1
+ require 'securerandom'
2
+ require 'digest/sha1'
3
+ require 'attach/attachment_binary'
4
+
5
+ module Attach
6
+ class Attachment < ActiveRecord::Base
7
+
8
+ # Set the table name
9
+ self.table_name = 'attachments'
10
+ self.inheritance_column = 'sti_type'
11
+
12
+ # This will be the ActionDispatch::UploadedFile object which be diseminated
13
+ # by the class on save.
14
+ attr_accessor :uploaded_file
15
+ attr_writer :binary
16
+
17
+ # Relationships
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
21
+
22
+ # Validations
23
+ validates :file_name, :presence => true
24
+ validates :file_type, :presence => true
25
+ validates :file_size, :presence => true
26
+ validates :digest, :presence => true
27
+ validates :token, :presence => true, :uniqueness => true
28
+
29
+ # All attachments should have a token assigned to this
30
+ before_validation { self.token = SecureRandom.uuid if self.token.blank? }
31
+
32
+ # Copy values from the `uploaded_file` and set them as the appropriate
33
+ # fields on this model
34
+ before_validation do
35
+ if self.uploaded_file
36
+ self.binary = self.uploaded_file.tempfile.read
37
+ self.file_name = self.uploaded_file.original_filename
38
+ self.file_type = self.uploaded_file.content_type
39
+ end
40
+
41
+ self.digest = Digest::SHA1.hexdigest(self.binary)
42
+ self.file_size = self.binary.bytesize
43
+ end
44
+
45
+ # Write the binary to the backend storage
46
+ after_create do
47
+ Attach.backend.write(self, self.binary)
48
+ end
49
+
50
+ # Remove any old images for this owner/role when this is added
51
+ after_create do
52
+ self.owner.attachments.where.not(:id => self).where(:parent_id => self.parent_id, :role => self.role).destroy_all
53
+ end
54
+
55
+ # Run any post-upload processing after the record has been committed
56
+ after_commit do
57
+ unless self.processed? || self.parent_id
58
+ self.processor.queue_or_process
59
+ end
60
+ end
61
+
62
+ # Remove the file from the backends
63
+ after_destroy do
64
+ Attach.backend.delete(self)
65
+ end
66
+
67
+ # Return the attachment for a given role
68
+ def self.for(role)
69
+ self.where(:role => role).first
70
+ end
71
+
72
+ # Return the binary data for this attachment
73
+ def binary
74
+ @binary ||= persisted? ? Attach.backend.read(self) : nil
75
+ end
76
+
77
+ # Return the path to the attachment
78
+ def url
79
+ Attach.backend.url(self)
80
+ end
81
+
82
+ # Is the attachment an image?
83
+ def image?
84
+ file_type =~ /\Aimage\//
85
+ end
86
+
87
+ # Return a processor for this attachment
88
+ def processor
89
+ @processor ||= Processor.new(self)
90
+ end
91
+
92
+ # Return a child process
93
+ def child(role)
94
+ @cached_children ||= {}
95
+ @cached_children[role.to_sym] ||= begin
96
+ self.children.where(:role => role).first
97
+ end
98
+ end
99
+
100
+ # Add a child attachment
101
+ def add_child(role, &block)
102
+ attachment = self.children.build
103
+ 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
111
+ block.call(attachment)
112
+ attachment.save!
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,11 @@
1
+ require 'securerandom'
2
+ require 'digest/sha1'
3
+
4
+ module Attach
5
+ class AttachmentBinary < ActiveRecord::Base
6
+
7
+ # Set the table name
8
+ self.table_name = 'attachment_binaries'
9
+
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ module Attach
2
+ module Backends
3
+ class Abstract
4
+
5
+ def initialize(config = {})
6
+ @config = config
7
+ end
8
+
9
+ #
10
+ # Return the data for the given attachment
11
+ #
12
+ def read(attachment)
13
+ end
14
+
15
+ #
16
+ # Write data for the given attachment
17
+ #
18
+ def write(attachment, data)
19
+ end
20
+
21
+ #
22
+ # Delete the data for the given attachment
23
+ #
24
+ def delete(attachment)
25
+ end
26
+
27
+ #
28
+ # Return the URL that this attachment can be accessed at
29
+ #
30
+ def url(attachment)
31
+ "/attachment/#{attachment.token}/#{attachment.file_name}"
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ require 'attach/backends/abstract'
2
+
3
+ module Attach
4
+ module Backends
5
+ class Database < Abstract
6
+
7
+ def read(attachment)
8
+ if binary = AttachmentBinary.find_by_attachment_id(attachment.id)
9
+ binary.data
10
+ else
11
+ nil
12
+ end
13
+ end
14
+
15
+ def write(attachment, data)
16
+ binary = AttachmentBinary.where(:attachment_id => attachment.id).first_or_initialize
17
+ binary.data = data
18
+ binary.save!
19
+ end
20
+
21
+ def delete(attachment)
22
+ AttachmentBinary.where(:attachment_id => attachment.id).destroy_all
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ require 'fileutils'
2
+ require 'attach/backends/abstract'
3
+
4
+ module Attach
5
+ module Backends
6
+ class FileSystem < Abstract
7
+
8
+ def read(attachment)
9
+ File.read(path_for_attachment(attachment))
10
+ end
11
+
12
+ def write(attachment, data)
13
+ path = path_for_attachment(attachment)
14
+ FileUtils.mkdir_p(File.dirname(path))
15
+ File.open(path, 'wb') do |f|
16
+ f.write(data)
17
+ end
18
+ end
19
+
20
+ def delete(attachment)
21
+ path = path_for_attachment(attachment)
22
+ FileUtils.rm(path) if File.file?(path)
23
+ end
24
+
25
+ private
26
+
27
+ def root_dir
28
+ @config[:root] ||= Rails.root.join('attachments')
29
+ end
30
+
31
+ def path_for_attachment(attachment)
32
+ File.join(root_dir, attachment.token[0,2], attachment.token[2,2], attachment.token[4,40])
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ require 'attach/attachment'
2
+
3
+ module Attach
4
+ class Middleware
5
+
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ if env['PATH_INFO'] =~ /\A\/attachment\/([a-f0-9\-]{36})\/(.*)/
12
+ if attachment = Attach::Attachment.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, maxage=#{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)
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,126 @@
1
+ require 'attach/attachment'
2
+ require 'attach/processor'
3
+
4
+ module Attach
5
+ module ModelExtension
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ base.after_save do
10
+ if @pending_attachment_deletions
11
+ self.attachments.where(:role => @pending_attachment_deletions).destroy_all
12
+ end
13
+
14
+ if @pending_attachments
15
+ @pending_attachments.each do |pa|
16
+ attachment = self.attachments.build(:uploaded_file => pa[:file], :role => pa[:role])
17
+ if pa[:options]
18
+ pa[:options].each do |key, value|
19
+ attachment.send("#{key}=", value)
20
+ end
21
+ end
22
+ attachment.save!
23
+ end
24
+ @pending_attachments = nil
25
+ end
26
+ end
27
+ end
28
+
29
+ module ClassMethods
30
+
31
+ def includes_attachments(*options)
32
+ manipulate do |records|
33
+ if records.empty?
34
+ # Nothing to do
35
+ else
36
+ if options.first.is_a?(Hash)
37
+ options = options.first
38
+ else
39
+ options = options.each_with_object({}) do |role, hash|
40
+ hash[role.to_sym] = []
41
+ end
42
+ end
43
+
44
+ options.keys.each do |key|
45
+ if options[key].is_a?(Symbol)
46
+ options[key] = [options[key]]
47
+ end
48
+ end
49
+
50
+ root_attachments = {}
51
+ Attachment.where(:owner_id => records.map(&:id), :owner_type => records.first.class.to_s, :role => options.keys).each do |attachment|
52
+ root_attachments[[attachment.owner_id, attachment.role]] = attachment
53
+ end
54
+
55
+ child_attachments = {}
56
+ child_roles = options.values.flatten
57
+ Attachment.where(:parent_id => root_attachments.values.map(&:id), :role => child_roles).each do |attachment|
58
+ child_attachments[[attachment.id, attachment.role]] = attachment
59
+ end
60
+
61
+ root_attachments.values.each do |attachment|
62
+ options[attachment.role.to_sym].each do |role|
63
+ attachment.instance_variable_set("@cached_children", {}) if attachment.instance_variable_get("@cached_children").nil?
64
+ attachment.instance_variable_get("@cached_children")[role.to_sym] = attachment
65
+ end
66
+ end
67
+
68
+ records.each do |record|
69
+ options.keys.each do |role|
70
+ if a = root_attachments[[record.id, role.to_s]]
71
+ record.instance_variable_set("@#{role}", a)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def attachment(name, options = {}, &block)
80
+ unless self.reflect_on_all_associations(:has_many).map(&:name).include?(:attachments)
81
+ has_many :attachments, :as => :owner, :dependent => :destroy, :class_name => 'Attach::Attachment'
82
+ end
83
+
84
+ if block_given?
85
+ Processor.register(self, name, &block)
86
+ end
87
+
88
+ define_method name do
89
+ instance_variable_get("@#{name}") || begin
90
+ attachment = self.attachments.where(:role => name, :parent_id => nil).first
91
+ instance_variable_set("@#{name}", attachment)
92
+ end
93
+ end
94
+
95
+ define_method "#{name}_file" do
96
+ instance_variable_get("@#{name}_file")
97
+ end
98
+
99
+ define_method "#{name}_file=" do |file|
100
+ instance_variable_set("@#{name}_file", file)
101
+ if file.is_a?(ActionDispatch::Http::UploadedFile)
102
+ @pending_attachments ||= []
103
+ @pending_attachments << {:role => name, :file => file, :options => options}
104
+ else
105
+ nil
106
+ end
107
+ end
108
+
109
+ define_method "#{name}_delete" do
110
+ instance_variable_get("@#{name}_delete")
111
+ end
112
+
113
+ define_method "#{name}_delete=" do |delete|
114
+ delete = delete.to_i
115
+ instance_variable_set("@#{name}_delete", delete)
116
+ if delete == 1
117
+ @pending_attachment_deletions ||= []
118
+ @pending_attachment_deletions << name
119
+ end
120
+ end
121
+ end
122
+
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,48 @@
1
+ module Attach
2
+ class Processor
3
+
4
+ def self.background(&block)
5
+ @background_block = block
6
+ end
7
+
8
+ def self.background_block
9
+ @background_block
10
+ end
11
+
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
17
+
18
+ def self.processor(model, attribute)
19
+ @processors && @processors[[model.to_s, attribute.to_s]]
20
+ end
21
+
22
+ def initialize(attachment)
23
+ @attachment = attachment
24
+ end
25
+
26
+ def process
27
+ call_processors(@attachment)
28
+ @attachment.update_column(:processed, true)
29
+ end
30
+
31
+ def queue_or_process
32
+ if self.class.background_block
33
+ self.class.background_block.call(@attachment)
34
+ else
35
+ process
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def call_processors(attachment)
42
+ if p = self.class.processor(attachment.owner_type, attachment.role)
43
+ p.call(attachment)
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ module Attach
2
+ class Railtie < Rails::Engine #:nodoc:
3
+
4
+ engine_name 'attach'
5
+
6
+ initializer 'attach.initialize' do |app|
7
+
8
+ require 'attach/middleware'
9
+ app.config.middleware.use Attach::Middleware
10
+
11
+ ActiveSupport.on_load(:active_record) do
12
+ require 'attach/model_extension'
13
+ ::ActiveRecord::Base.send :include, Attach::ModelExtension
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module Attach
2
+ VERSION = '1.0.0'
3
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attach
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Adam Cooke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: records_manipulator
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ description: Attach documents & files to Active Record models
34
+ email:
35
+ - me@adamcooke.io
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - lib/attach.rb
41
+ - lib/attach/attachment.rb
42
+ - lib/attach/attachment_binary.rb
43
+ - lib/attach/backends/abstract.rb
44
+ - lib/attach/backends/database.rb
45
+ - lib/attach/backends/file_system.rb
46
+ - lib/attach/middleware.rb
47
+ - lib/attach/model_extension.rb
48
+ - lib/attach/processor.rb
49
+ - lib/attach/railtie.rb
50
+ - lib/attach/version.rb
51
+ homepage: https://github.com/adamcooke/attach
52
+ licenses:
53
+ - MIT
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 2.5.1
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: Attach documents & files to Active Record models
75
+ test_files: []