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 +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: []
|