attach 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/attach.rb +23 -0
- data/lib/attach/attachment.rb +116 -0
- data/lib/attach/attachment_binary.rb +11 -0
- data/lib/attach/backends/abstract.rb +36 -0
- data/lib/attach/backends/database.rb +27 -0
- data/lib/attach/backends/file_system.rb +37 -0
- data/lib/attach/middleware.rb +29 -0
- data/lib/attach/model_extension.rb +126 -0
- data/lib/attach/processor.rb +48 -0
- data/lib/attach/railtie.rb +19 -0
- data/lib/attach/version.rb +3 -0
- metadata +75 -0
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,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
|
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: []
|