attach 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []