attachie 0.0.1
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/.gitignore +14 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +103 -0
- data/Rakefile +2 -0
- data/attachie.gemspec +31 -0
- data/docker-compose.yml +9 -0
- data/lib/attachie.rb +172 -0
- data/lib/attachie/fake_driver.rb +83 -0
- data/lib/attachie/file_driver.rb +109 -0
- data/lib/attachie/interpolation.rb +25 -0
- data/lib/attachie/s3_driver.rb +119 -0
- data/lib/attachie/swift_driver.rb +66 -0
- data/lib/attachie/version.rb +3 -0
- data/spec/attachie/fake_driver_spec.rb +61 -0
- data/spec/attachie/file_driver_spec.rb +44 -0
- data/spec/attachie/s3_driver_spec.rb +69 -0
- data/spec/attachie/version_spec.rb +60 -0
- data/spec/attachie_spec.rb +57 -0
- data/spec/spec_helper.rb +33 -0
- metadata +197 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9fce9a92b9b729e8d2a453178293afaf74b1658321cdbc81d4d0ec5fd95794d8
|
4
|
+
data.tar.gz: 3bab92b185f8d97105155c686c87153f81cb994242df9b848c30601a830283a0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1548848d991986d9bb32442dfe5fa5cc1c55822aa6b6e5e47f5fa7004a222cd5840cd4bac172d631c0c2085bef5eeeaeb98476f24340bde131ec244d2a068e7d
|
7
|
+
data.tar.gz: d3ca7add377253d86228affb874d839720c3275b8005322b84c36eab65ad517aed7d10f62a694165ca536d0ca54341d35ab9911bc7284a7beb17bc730ca93eb7
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Benjamin Vetter
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# Attachie
|
2
|
+
|
3
|
+
[](http://travis-ci.org/mrkamel/attachie)
|
4
|
+
|
5
|
+
Declarative and flexible attachments.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'attachie'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install attachie
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
First, `include Attachie` and specify an attachment:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class User
|
29
|
+
include Attachie
|
30
|
+
|
31
|
+
attachment :avatar, versions: {
|
32
|
+
icon: { path: "users/:id/avatar/icon.jpg" },
|
33
|
+
thumbnail: { path: "users/:id/avatar/thumbnail.jpg" },
|
34
|
+
original: { path: "users/:id/avatar/original.jpg" }
|
35
|
+
}
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
Second, store blobs for your version:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
user.avatar(:icon).store("blob")
|
43
|
+
user.avatar(:thumbnail).store("blob")
|
44
|
+
user.avatar(:original).store("blob")
|
45
|
+
```
|
46
|
+
|
47
|
+
or via multipart upload
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
user.avatar(:icon).store_multipart do |upload|
|
51
|
+
upload.upload_part "chunk1"
|
52
|
+
upload.upload_part "chunk2"
|
53
|
+
# ...
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
Third, add the images url to your views:
|
58
|
+
|
59
|
+
```
|
60
|
+
image_tag user.avatar(:thumbnail).url
|
61
|
+
```
|
62
|
+
|
63
|
+
More methods to manipulate the blobs:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
user.avatar(:icon).delete
|
67
|
+
user.avatar(:icon).exists?
|
68
|
+
user.avatar(:icon).value
|
69
|
+
user.avatar(:icon).temp_url(expires_in: 2.days) # Must be supported by the driver
|
70
|
+
```
|
71
|
+
|
72
|
+
## Drivers
|
73
|
+
|
74
|
+
The `attachie` gem ships with the following drivers:
|
75
|
+
|
76
|
+
* `Attachie::FileDriver`: To store files on the local file system
|
77
|
+
* `Attachie::FakeDriver`: To store files in memory (for testing)
|
78
|
+
* `Attachie::S3Driver`: To store files on S3
|
79
|
+
* `Attachie::SwiftDriver`: To store files on an Openstack Swift provider
|
80
|
+
|
81
|
+
You can eg use the file system driver:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
require "attachie/file_driver"
|
85
|
+
|
86
|
+
Attachie.default_options[:driver] = Attachie::FileDriver.new("/path/to/attachments")
|
87
|
+
|
88
|
+
class User
|
89
|
+
include Attachie
|
90
|
+
|
91
|
+
attachment :avatar, host: "www.example.com", versions: {
|
92
|
+
# ...
|
93
|
+
}
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
## Contributing
|
98
|
+
|
99
|
+
1. Fork it ( https://github.com/mrkamel/attachie/fork )
|
100
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
101
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
102
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
103
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/attachie.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'attachie/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "attachie"
|
8
|
+
spec.version = Attachie::VERSION
|
9
|
+
spec.authors = ["Benjamin Vetter"]
|
10
|
+
spec.email = ["vetter@plainpicture.de"]
|
11
|
+
spec.summary = %q{Declarative and flexible attachments}
|
12
|
+
spec.description = %q{Declarative and flexible attachments}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "swift_client"
|
22
|
+
spec.add_dependency "aws-sdk-s3"
|
23
|
+
spec.add_dependency "mime-types"
|
24
|
+
spec.add_dependency "connection_pool"
|
25
|
+
spec.add_dependency "activesupport"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler"
|
28
|
+
spec.add_development_dependency "rake"
|
29
|
+
spec.add_development_dependency "rspec"
|
30
|
+
spec.add_development_dependency "fakes3"
|
31
|
+
end
|
data/docker-compose.yml
ADDED
data/lib/attachie.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
|
2
|
+
require "attachie/version"
|
3
|
+
require "attachie/interpolation"
|
4
|
+
require "active_support/all"
|
5
|
+
|
6
|
+
module Attachie
|
7
|
+
class UnknownAttachment < StandardError; end
|
8
|
+
class NoSuchVersion < StandardError; end
|
9
|
+
class InterpolationError < StandardError; end
|
10
|
+
|
11
|
+
def self.default_options
|
12
|
+
@default_options ||= { :protocol => "http" }
|
13
|
+
end
|
14
|
+
|
15
|
+
class Attachment
|
16
|
+
class Version
|
17
|
+
attr_accessor :attachment, :name, :options
|
18
|
+
|
19
|
+
def initialize(attachment, name, options)
|
20
|
+
self.attachment = attachment
|
21
|
+
self.name = name
|
22
|
+
self.options = options
|
23
|
+
end
|
24
|
+
|
25
|
+
def url
|
26
|
+
"#{interpolate option(:protocol)}://#{interpolate option(:host)}/#{interpolate(option(:url_prefix)).to_s + "/" if option(:url_prefix)}#{path}#{interpolate(option(:url_suffix)) if option(:url_suffix)}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def path
|
30
|
+
"#{interpolate(option(:path_prefix)) + "/" if option(:path_prefix)}#{path_without_prefix}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def path_without_prefix
|
34
|
+
interpolate option(:path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def container
|
38
|
+
interpolate option(:container) || option(:bucket)
|
39
|
+
end
|
40
|
+
|
41
|
+
alias_method :bucket, :container
|
42
|
+
|
43
|
+
def temp_url(opts = {})
|
44
|
+
return url unless option(:driver).respond_to?(:temp_url)
|
45
|
+
|
46
|
+
option(:driver).temp_url(path, container, opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
def value
|
50
|
+
option(:driver).value(path, container)
|
51
|
+
end
|
52
|
+
|
53
|
+
def store(data_or_io, opts = {})
|
54
|
+
option(:driver).store(path, data_or_io, container, opts)
|
55
|
+
end
|
56
|
+
|
57
|
+
def store_multipart(opts = {}, &block)
|
58
|
+
option(:driver).store_multipart(path, container, opts, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete
|
62
|
+
option(:driver).delete(path, container)
|
63
|
+
end
|
64
|
+
|
65
|
+
def exists?
|
66
|
+
option(:driver).exists?(path, container)
|
67
|
+
end
|
68
|
+
|
69
|
+
def inspect
|
70
|
+
to_s
|
71
|
+
end
|
72
|
+
|
73
|
+
def object
|
74
|
+
attachment.object
|
75
|
+
end
|
76
|
+
|
77
|
+
def method_missing(method_name, *args, &block)
|
78
|
+
return attachment.options[:versions][name][method_name.to_sym] if attachment.options[:versions][name].key?(method_name.to_sym)
|
79
|
+
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
def respond_to_missing?(method_name, *args)
|
84
|
+
attachment.options[:versions][name].key?(method_name.to_sym)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def option(option_name)
|
90
|
+
return attachment.options[:versions][name][option_name] if attachment.options[:versions][name].key?(option_name)
|
91
|
+
return options[option_name] if options.key?(option_name)
|
92
|
+
return attachment.options[option_name] if attachment.options.key?(option_name)
|
93
|
+
|
94
|
+
Attachie.default_options[option_name]
|
95
|
+
end
|
96
|
+
|
97
|
+
def interpolate(str)
|
98
|
+
raise(InterpolationError) unless str.is_a?(String)
|
99
|
+
|
100
|
+
str.gsub(/(?<!\\):[a-zA-Z][a-zA-Z0-9_]*/) do |attribute_name|
|
101
|
+
Interpolation.new(self).send(attribute_name.gsub(/^:/, ""))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
attr_accessor :object, :name, :options
|
107
|
+
|
108
|
+
def initialize(object, name, options)
|
109
|
+
self.object = object
|
110
|
+
self.name = name
|
111
|
+
self.options = options
|
112
|
+
end
|
113
|
+
|
114
|
+
def version(version_name, opts = {})
|
115
|
+
raise(NoSuchVersion, "No such version: #{version_name}") unless options[:versions][version_name]
|
116
|
+
|
117
|
+
Version.new self, version_name, opts
|
118
|
+
end
|
119
|
+
|
120
|
+
def versions
|
121
|
+
options[:versions].collect { |version_name, _| version version_name }
|
122
|
+
end
|
123
|
+
|
124
|
+
def method_missing(method_name, *args, &block)
|
125
|
+
return options[method_name.to_sym] if options.key?(method_name.to_sym)
|
126
|
+
|
127
|
+
super
|
128
|
+
end
|
129
|
+
|
130
|
+
def respond_to_missing?(method_name, *args)
|
131
|
+
options.key?(method_name.to_sym)
|
132
|
+
end
|
133
|
+
|
134
|
+
def inspect
|
135
|
+
to_s
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.included(base)
|
140
|
+
base.class_attribute :attachments
|
141
|
+
base.attachments = {}
|
142
|
+
|
143
|
+
base.extend ClassMethods
|
144
|
+
end
|
145
|
+
|
146
|
+
def attachment(name)
|
147
|
+
definition = self.class.attachments[name]
|
148
|
+
|
149
|
+
raise(UnknownAttachment) unless definition
|
150
|
+
|
151
|
+
Attachment.new self, name, definition
|
152
|
+
end
|
153
|
+
|
154
|
+
module ClassMethods
|
155
|
+
def attachment(name, options = {})
|
156
|
+
self.attachments = attachments.merge(name => options)
|
157
|
+
|
158
|
+
define_method name do |version = nil, options = {}|
|
159
|
+
return instance_variable_get("@#{name}") if version.nil?
|
160
|
+
|
161
|
+
attachment(name).version(version, options)
|
162
|
+
end
|
163
|
+
|
164
|
+
define_method "#{name}=" do |value|
|
165
|
+
self.updated_at = Time.now if respond_to?(:updated_at=) && !value.nil?
|
166
|
+
|
167
|
+
instance_variable_set "@#{name}", value
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
@@ -0,0 +1,83 @@
|
|
1
|
+
|
2
|
+
module Attachie
|
3
|
+
class FakeMultipartUpload
|
4
|
+
include MonitorMixin
|
5
|
+
|
6
|
+
def initialize(name, bucket, options, &block)
|
7
|
+
super()
|
8
|
+
|
9
|
+
@name = name
|
10
|
+
@bucket = bucket
|
11
|
+
|
12
|
+
block.call(self) if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
def upload_part(data)
|
16
|
+
synchronize do
|
17
|
+
@data ||= ""
|
18
|
+
@data << data
|
19
|
+
end
|
20
|
+
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def data
|
25
|
+
synchronize do
|
26
|
+
@data
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def abort_upload; end
|
31
|
+
def complete_upload; end
|
32
|
+
end
|
33
|
+
|
34
|
+
class FakeDriver
|
35
|
+
class ItemNotFound < StandardError; end
|
36
|
+
|
37
|
+
def list(bucket, prefix: nil)
|
38
|
+
return enum_for(:list, bucket, prefix: prefix) unless block_given?
|
39
|
+
|
40
|
+
objects(bucket).each do |key, _|
|
41
|
+
yield key if prefix.nil? || key.start_with?(prefix)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def store(name, data_or_io, bucket, options = {})
|
46
|
+
objects(bucket)[name] = data_or_io.respond_to?(:read) ? data_or_io.read : data_or_io
|
47
|
+
end
|
48
|
+
|
49
|
+
def store_multipart(name, bucket, options = {}, &block)
|
50
|
+
objects(bucket)[name] = FakeMultipartUpload.new(name, bucket, options, &block).data
|
51
|
+
end
|
52
|
+
|
53
|
+
def exists?(name, bucket)
|
54
|
+
objects(bucket).key?(name)
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete(name, bucket)
|
58
|
+
objects(bucket).delete(name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def value(name, bucket)
|
62
|
+
raise(ItemNotFound) unless objects(bucket).key?(name)
|
63
|
+
|
64
|
+
objects(bucket)[name]
|
65
|
+
end
|
66
|
+
|
67
|
+
def temp_url(name, bucket, options = {})
|
68
|
+
"https://example.com/#{bucket}/#{name}?signature=signature&expires=expires"
|
69
|
+
end
|
70
|
+
|
71
|
+
def flush
|
72
|
+
@objects = {}
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def objects(bucket)
|
78
|
+
@objects ||= {}
|
79
|
+
@objects[bucket] ||= {}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
@@ -0,0 +1,109 @@
|
|
1
|
+
|
2
|
+
require "fileutils"
|
3
|
+
|
4
|
+
module Attachie
|
5
|
+
class FileDriver
|
6
|
+
class FileMultipartUpload
|
7
|
+
include MonitorMixin
|
8
|
+
|
9
|
+
def initialize(name, bucket, driver, &block)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@name = name
|
13
|
+
@bucket = bucket
|
14
|
+
@driver = driver
|
15
|
+
|
16
|
+
@stream = open(driver.path_for(name, bucket), "wb")
|
17
|
+
|
18
|
+
if block_given?
|
19
|
+
begin
|
20
|
+
block.call(self)
|
21
|
+
rescue => e
|
22
|
+
abort_upload
|
23
|
+
|
24
|
+
raise e
|
25
|
+
end
|
26
|
+
|
27
|
+
complete_upload
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def upload_part(data)
|
32
|
+
synchronize do
|
33
|
+
@stream.write(data)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def abort_upload
|
38
|
+
@stream.close
|
39
|
+
|
40
|
+
@target.delete(name, bucket)
|
41
|
+
end
|
42
|
+
|
43
|
+
def complete_upload
|
44
|
+
@stream.close
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize(base_path)
|
49
|
+
@base_path = base_path
|
50
|
+
end
|
51
|
+
|
52
|
+
def store(name, data_or_io, bucket, options = {})
|
53
|
+
path = path_for(name, bucket)
|
54
|
+
|
55
|
+
FileUtils.mkdir_p File.dirname(path)
|
56
|
+
|
57
|
+
open(path, "wb") do |stream|
|
58
|
+
io = data_or_io.respond_to?(:read) ? data_or_io : StringIO.new(data_or_io)
|
59
|
+
|
60
|
+
while chunk = io.read(1024)
|
61
|
+
stream.write chunk
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
def store_multipart(name, bucket, options = {}, &block)
|
69
|
+
path = path_for(name, bucket)
|
70
|
+
|
71
|
+
FileUtils.mkdir_p File.dirname(path)
|
72
|
+
|
73
|
+
FileMultipartUpload.new(name, bucket, self, &block)
|
74
|
+
end
|
75
|
+
|
76
|
+
def value(name, bucket)
|
77
|
+
File.binread path_for(name, bucket)
|
78
|
+
end
|
79
|
+
|
80
|
+
def delete(name, bucket)
|
81
|
+
path = path_for(name, bucket)
|
82
|
+
|
83
|
+
FileUtils.rm_f(path)
|
84
|
+
|
85
|
+
begin
|
86
|
+
dir = File.dirname(File.join(bucket, name))
|
87
|
+
|
88
|
+
until dir == bucket
|
89
|
+
Dir.rmdir File.join(@base_path, dir)
|
90
|
+
|
91
|
+
dir = File.dirname(dir)
|
92
|
+
end
|
93
|
+
rescue Errno::ENOTEMPTY, Errno::ENOENT
|
94
|
+
# nothing
|
95
|
+
end
|
96
|
+
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
def exists?(name, bucket)
|
101
|
+
File.exists? path_for(name, bucket)
|
102
|
+
end
|
103
|
+
|
104
|
+
def path_for(name, bucket)
|
105
|
+
File.join(@base_path, bucket, name)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
module Attachie
|
3
|
+
class Interpolation
|
4
|
+
attr_accessor :version
|
5
|
+
|
6
|
+
def initialize(version)
|
7
|
+
self.version = version
|
8
|
+
end
|
9
|
+
|
10
|
+
def container
|
11
|
+
version.container
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method :bucket, :container
|
15
|
+
|
16
|
+
def method_missing(name, *args, &block)
|
17
|
+
version.object.send name
|
18
|
+
end
|
19
|
+
|
20
|
+
def respond_to?(name, *args)
|
21
|
+
super(name, *args) || version.object.respond_to?(name, *args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,119 @@
|
|
1
|
+
|
2
|
+
require "aws-sdk-s3"
|
3
|
+
require "mime-types"
|
4
|
+
|
5
|
+
module Attachie
|
6
|
+
class S3MultipartUpload
|
7
|
+
include MonitorMixin
|
8
|
+
|
9
|
+
def initialize(s3_client, name, bucket, options, &block)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@s3_client = s3_client
|
13
|
+
@bucket = bucket
|
14
|
+
@name = name
|
15
|
+
|
16
|
+
@parts = []
|
17
|
+
|
18
|
+
@upload_id = @s3_client.create_multipart_upload(options.merge(bucket: bucket, key: name)).to_h[:upload_id]
|
19
|
+
|
20
|
+
if block_given?
|
21
|
+
begin
|
22
|
+
block.call(self)
|
23
|
+
rescue => e
|
24
|
+
abort_upload
|
25
|
+
|
26
|
+
raise e
|
27
|
+
end
|
28
|
+
|
29
|
+
complete_upload
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def upload_part(data)
|
34
|
+
index = synchronize do
|
35
|
+
part_number = @parts.size + 1
|
36
|
+
|
37
|
+
@parts << { part_number: part_number, etag: "\"#{Digest::MD5.hexdigest(data)}\"" }
|
38
|
+
|
39
|
+
part_number
|
40
|
+
end
|
41
|
+
|
42
|
+
@s3_client.upload_part(body: data, bucket: @bucket, key: @name, upload_id: @upload_id, part_number: index)
|
43
|
+
end
|
44
|
+
|
45
|
+
def abort_upload
|
46
|
+
@s3_client.abort_multipart_upload(bucket: @bucket, key: @name, upload_id: @upload_id)
|
47
|
+
end
|
48
|
+
|
49
|
+
def complete_upload
|
50
|
+
@s3_client.complete_multipart_upload(bucket: @bucket, key: @name, upload_id: @upload_id, multipart_upload: { parts: @parts })
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class S3Driver
|
55
|
+
attr_accessor :s3_client, :s3_resource
|
56
|
+
|
57
|
+
def initialize(s3_client)
|
58
|
+
self.s3_client = s3_client
|
59
|
+
self.s3_resource = Aws::S3::Resource.new(client: s3_client)
|
60
|
+
end
|
61
|
+
|
62
|
+
def list(bucket, prefix: nil)
|
63
|
+
return enum_for(:list, bucket, prefix: prefix) unless block_given?
|
64
|
+
|
65
|
+
options = {}
|
66
|
+
options[:prefix] = prefix if prefix
|
67
|
+
|
68
|
+
s3_resource.bucket(bucket).objects(options).each do |object|
|
69
|
+
yield object.key
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def store(name, data_or_io, bucket, options = {})
|
74
|
+
opts = options.dup
|
75
|
+
|
76
|
+
mime_type = MIME::Types.of(name).first
|
77
|
+
|
78
|
+
opts[:content_type] ||= mime_type.content_type if mime_type
|
79
|
+
opts[:content_type] ||= "application/octet-stream"
|
80
|
+
|
81
|
+
opts[:body] = data_or_io
|
82
|
+
|
83
|
+
s3_resource.bucket(bucket).object(name).put(opts)
|
84
|
+
end
|
85
|
+
|
86
|
+
def store_multipart(name, bucket, options = {}, &block)
|
87
|
+
opts = options.dup
|
88
|
+
|
89
|
+
mime_type = MIME::Types.of(name).first
|
90
|
+
|
91
|
+
opts[:content_type] ||= mime_type.content_type if mime_type
|
92
|
+
opts[:content_type] ||= "application/octet-stream"
|
93
|
+
|
94
|
+
S3MultipartUpload.new(s3_client, name, bucket, opts, &block)
|
95
|
+
end
|
96
|
+
|
97
|
+
def value(name, bucket)
|
98
|
+
s3_resource.bucket(bucket).object(name).get.body.read.force_encoding(Encoding::BINARY)
|
99
|
+
end
|
100
|
+
|
101
|
+
def delete(name, bucket)
|
102
|
+
s3_resource.bucket(bucket).object(name).delete
|
103
|
+
end
|
104
|
+
|
105
|
+
def exists?(name, bucket)
|
106
|
+
s3_resource.bucket(bucket).object(name).exists?
|
107
|
+
end
|
108
|
+
|
109
|
+
def temp_url(name, bucket, options = {})
|
110
|
+
opts = options.dup
|
111
|
+
opts[:expires_in] = opts.delete(:expires_in).to_i if opts.key?(:expires_in)
|
112
|
+
|
113
|
+
method = opts.delete(:method) || :get
|
114
|
+
|
115
|
+
s3_resource.bucket(bucket).object(name).presigned_url(method, opts)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
|
2
|
+
require "swift_client"
|
3
|
+
require "connection_pool"
|
4
|
+
|
5
|
+
module Attachie
|
6
|
+
class SwiftDriver
|
7
|
+
attr_accessor :swift_client_pool
|
8
|
+
|
9
|
+
def initialize(swift_client_pool)
|
10
|
+
self.swift_client_pool = swift_client_pool
|
11
|
+
end
|
12
|
+
|
13
|
+
def list(container, prefix: nil)
|
14
|
+
return enum_for(:list, container, prefix: prefix) unless block_given?
|
15
|
+
|
16
|
+
swift_client_pool.with do |swift_client|
|
17
|
+
swift_client.paginate_objects(container, prefix: prefix) do |response|
|
18
|
+
response.parsed_response.each do |source_object|
|
19
|
+
yield source_object["name"]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def store(name, data_or_io, container, headers = {})
|
26
|
+
swift_client_pool.with do |swift_client|
|
27
|
+
swift_client.put_object name, data_or_io, container, headers
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def value(name, container)
|
32
|
+
swift_client_pool.with do |swift_client|
|
33
|
+
swift_client.get_object(name, container).body
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete(name, container)
|
38
|
+
swift_client_pool.with do |swift_client|
|
39
|
+
swift_client.delete_object name, container
|
40
|
+
end
|
41
|
+
rescue SwiftClient::ResponseError => e
|
42
|
+
return true if e.code == 404
|
43
|
+
|
44
|
+
raise e
|
45
|
+
end
|
46
|
+
|
47
|
+
def exists?(name, container)
|
48
|
+
swift_client_pool.with do |swift_client|
|
49
|
+
swift_client.head_object name, container
|
50
|
+
end
|
51
|
+
|
52
|
+
true
|
53
|
+
rescue SwiftClient::ResponseError => e
|
54
|
+
return false if e.code == 404
|
55
|
+
|
56
|
+
raise e
|
57
|
+
end
|
58
|
+
|
59
|
+
def temp_url(name, container, options = {})
|
60
|
+
swift_client_pool.with do |swift_client|
|
61
|
+
swift_client.temp_url name, container, options
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
3
|
+
|
4
|
+
RSpec.describe Attachie::FakeDriver do
|
5
|
+
let(:driver) { Attachie::FakeDriver.new }
|
6
|
+
|
7
|
+
it "should list objects" do
|
8
|
+
begin
|
9
|
+
driver.store("object1", "blob", "bucket1")
|
10
|
+
driver.store("object2", "blob", "bucket1")
|
11
|
+
driver.store("other", "blob", "bucket1")
|
12
|
+
driver.store("object", "blob", "bucket3")
|
13
|
+
|
14
|
+
expect(driver.list("bucket1", prefix: "object").to_a).to eq(["object1", "object2"])
|
15
|
+
ensure
|
16
|
+
driver.flush
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should store a blob" do
|
21
|
+
begin
|
22
|
+
driver.store("name", "blob", "bucket")
|
23
|
+
|
24
|
+
expect(driver.exists?("name", "bucket")).to be(true)
|
25
|
+
expect(driver.value("name", "bucket")).to eq("blob")
|
26
|
+
ensure
|
27
|
+
driver.flush
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should store a blob via multipart upload" do
|
32
|
+
begin
|
33
|
+
driver.store_multipart("name", "bucket") do |upload|
|
34
|
+
upload.upload_part("chunk1")
|
35
|
+
upload.upload_part("chunk2")
|
36
|
+
end
|
37
|
+
|
38
|
+
expect(driver.exists?("name", "bucket")).to be(true)
|
39
|
+
expect(driver.value("name", "bucket")).to eq("chunk1chunk2")
|
40
|
+
ensure
|
41
|
+
driver.flush
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should delete a blob" do
|
46
|
+
begin
|
47
|
+
driver.store("name", "blob", "bucket")
|
48
|
+
expect(driver.exists?("name", "bucket")).to be(true)
|
49
|
+
|
50
|
+
driver.delete("name", "bucket")
|
51
|
+
expect(driver.exists?("name", "bucket")).to be(false)
|
52
|
+
ensure
|
53
|
+
driver.flush
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should generate a temp_url" do
|
58
|
+
expect(driver.temp_url("name", "bucket")).to eq("https://example.com/bucket/name?signature=signature&expires=expires")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
3
|
+
|
4
|
+
RSpec.describe Attachie::FileDriver do
|
5
|
+
let(:driver) { Attachie::FileDriver.new("/tmp/attachie") }
|
6
|
+
|
7
|
+
it "should store a blob" do
|
8
|
+
begin
|
9
|
+
driver.store("name", "blob", "bucket")
|
10
|
+
|
11
|
+
expect(driver.exists?("name", "bucket")).to be(true)
|
12
|
+
expect(driver.value("name", "bucket")).to eq("blob")
|
13
|
+
ensure
|
14
|
+
driver.delete("name", "bucket")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should store a blob via multipart upload" do
|
19
|
+
begin
|
20
|
+
driver.store_multipart("name", "bucket") do |upload|
|
21
|
+
upload.upload_part("chunk1")
|
22
|
+
upload.upload_part("chunk2")
|
23
|
+
end
|
24
|
+
|
25
|
+
expect(driver.exists?("name", "bucket")).to be(true)
|
26
|
+
expect(driver.value("name", "bucket")).to eq("chunk1chunk2")
|
27
|
+
ensure
|
28
|
+
driver.delete("name", "bucket")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should delete a blob" do
|
33
|
+
begin
|
34
|
+
driver.store("name", "blob", "bucket")
|
35
|
+
expect(driver.exists?("name", "bucket")).to be(true)
|
36
|
+
|
37
|
+
driver.delete("name", "bucket")
|
38
|
+
expect(driver.exists?("name", "bucket")).to be(false)
|
39
|
+
ensure
|
40
|
+
driver.delete("name", "bucket")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
3
|
+
|
4
|
+
RSpec.describe Attachie::S3Driver do
|
5
|
+
let(:driver) do
|
6
|
+
Attachie::S3Driver.new(Aws::S3::Client.new(
|
7
|
+
access_key_id: "access_key_id",
|
8
|
+
secret_access_key: "secret_access_key",
|
9
|
+
endpoint: "http://localhost:4569",
|
10
|
+
region: "us-east-1"
|
11
|
+
))
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should list objects" do
|
15
|
+
begin
|
16
|
+
driver.store("object1", "blob", "bucket")
|
17
|
+
driver.store("object2", "blob", "bucket")
|
18
|
+
driver.store("other", "blob", "bucket")
|
19
|
+
|
20
|
+
expect(driver.list("bucket", prefix: "object").to_a).to eq(["object1", "object2"])
|
21
|
+
ensure
|
22
|
+
driver.delete("object1", "bucket")
|
23
|
+
driver.delete("object2", "bucket")
|
24
|
+
driver.delete("other", "bucket")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should store a blob" do
|
29
|
+
begin
|
30
|
+
driver.store("name", "blob", "bucket")
|
31
|
+
|
32
|
+
expect(driver.exists?("name", "bucket")).to be(true)
|
33
|
+
expect(driver.value("name", "bucket")).to eq("blob")
|
34
|
+
ensure
|
35
|
+
driver.delete("name", "bucket")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should store a blob via multipart upload" do
|
40
|
+
begin
|
41
|
+
driver.store_multipart("name", "bucket") do |upload|
|
42
|
+
upload.upload_part("chunk1")
|
43
|
+
upload.upload_part("chunk2")
|
44
|
+
end
|
45
|
+
|
46
|
+
expect(driver.exists?("name", "bucket")).to be(true)
|
47
|
+
expect(driver.value("name", "bucket")).to eq("chunk1chunk2")
|
48
|
+
ensure
|
49
|
+
driver.delete("name", "bucket")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should delete a blob" do
|
54
|
+
begin
|
55
|
+
driver.store("name", "blob", "bucket")
|
56
|
+
expect(driver.exists?("name", "bucket")).to be(true)
|
57
|
+
|
58
|
+
driver.delete("name", "bucket")
|
59
|
+
expect(driver.exists?("name", "bucket")).to be(false)
|
60
|
+
ensure
|
61
|
+
driver.delete("name", "bucket")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should generate a temp_url" do
|
66
|
+
expect(driver.temp_url("name", "bucket")).to be_url
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
|
2
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
3
|
+
|
4
|
+
RSpec.describe Attachie::Attachment::Version do
|
5
|
+
it "should interpolate the host, path_prefix and path" do
|
6
|
+
expect(Product.new(id: 1).image(:thumbnail).url).to eq("http://images.example.com/images/products/1/thumbnail.jpg")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should know the path without prefix" do
|
10
|
+
expect(Product.new(id: 1).image(:thumbnail).path_without_prefix).to eq("products/1/thumbnail.jpg")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should know the bucket" do
|
14
|
+
expect(Product.new(id: 1).image(:thumbnail).bucket).to eq("images")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should store a blob" do
|
18
|
+
product = Product.new(id: 1)
|
19
|
+
|
20
|
+
begin
|
21
|
+
product.image(:thumbnail).store("blob")
|
22
|
+
|
23
|
+
expect(product.image(:thumbnail).exists?).to be(true)
|
24
|
+
expect(product.image(:thumbnail).value).to eq("blob")
|
25
|
+
ensure
|
26
|
+
product.image(:thumbnail).delete
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should support multipart uploads" do
|
31
|
+
product = Product.new(id: 1)
|
32
|
+
|
33
|
+
begin
|
34
|
+
product.image(:thumbnail).store_multipart do |upload|
|
35
|
+
upload.upload_part("chunk1")
|
36
|
+
upload.upload_part("chunk2")
|
37
|
+
end
|
38
|
+
|
39
|
+
expect(product.image(:thumbnail).exists?).to be(true)
|
40
|
+
expect(product.image(:thumbnail).value).to eq("chunk1chunk2")
|
41
|
+
ensure
|
42
|
+
product.image(:thumbnail).delete
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should delete a blob" do
|
47
|
+
product = Product.new(id: 1)
|
48
|
+
|
49
|
+
begin
|
50
|
+
product.image(:thumbnail).store("blob")
|
51
|
+
expect(product.image(:thumbnail).exists?).to be(true)
|
52
|
+
|
53
|
+
product.image(:thumbnail).delete
|
54
|
+
expect(product.image(:thumbnail).exists?).to be(false)
|
55
|
+
ensure
|
56
|
+
product.image(:thumbnail).delete
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
require File.expand_path("../spec_helper", __FILE__)
|
3
|
+
|
4
|
+
class TestModel
|
5
|
+
include Attachie
|
6
|
+
|
7
|
+
attachment :file, driver: Attachie::FakeDriver.new, bucket: "bucket", host: "www.example.com", versions: {
|
8
|
+
small: { path: "path/to/small/:filename", attribute: "value" },
|
9
|
+
large: { path: "path/to/large/:filename" }
|
10
|
+
}
|
11
|
+
|
12
|
+
attr_accessor :filename, :updated_at
|
13
|
+
|
14
|
+
def initialize(filename:)
|
15
|
+
self.filename = filename
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
RSpec.describe TestModel do
|
20
|
+
it "should interpolate the path" do
|
21
|
+
test_model = TestModel.new(filename: "file.jpg")
|
22
|
+
|
23
|
+
expect(test_model.file(:small).path).to eq("path/to/small/file.jpg")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should allow arbitrary version methods" do
|
27
|
+
test_model = TestModel.new(filename: "file.jpg")
|
28
|
+
|
29
|
+
expect(test_model.file(:small).attribute).to eq("value")
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should espect the host" do
|
33
|
+
test_model = TestModel.new(filename: "file.jpg")
|
34
|
+
|
35
|
+
expect(test_model.file(:large).url).to eq("http://www.example.com/path/to/large/file.jpg")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should correctly use the driver" do
|
39
|
+
test_model = TestModel.new(filename: "blob.txt")
|
40
|
+
test_model.file(:large).store "blob"
|
41
|
+
|
42
|
+
expect(test_model.file(:large).value).to eq("blob")
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should set updated_at" do
|
46
|
+
test_model = TestModel.new(filename: "file.jpg")
|
47
|
+
test_model.file = "file"
|
48
|
+
|
49
|
+
expect(test_model.updated_at).to_not be_nil
|
50
|
+
|
51
|
+
test_model = TestModel.new(filename: "file.jpg")
|
52
|
+
test_model.file = nil
|
53
|
+
|
54
|
+
expect(test_model.updated_at).to be_nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
require File.expand_path("../../lib/attachie", __FILE__)
|
3
|
+
|
4
|
+
require "attachie/file_driver"
|
5
|
+
require "attachie/fake_driver"
|
6
|
+
require "attachie/s3_driver"
|
7
|
+
|
8
|
+
class Product
|
9
|
+
include Attachie
|
10
|
+
|
11
|
+
attr_accessor :id
|
12
|
+
|
13
|
+
def initialize(attributes = {})
|
14
|
+
attributes.each do |key, value|
|
15
|
+
self.send("#{key}=", value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attachment :image, host: ":subdomain.example.com", path_prefix: ":bucket", bucket: "images", driver: Attachie::FileDriver.new("/tmp/attachie"), versions: {
|
20
|
+
thumbnail: { path: "products/:id/thumbnail.jpg" },
|
21
|
+
original: { path: "products/:id/original.jpg" }
|
22
|
+
}
|
23
|
+
|
24
|
+
def subdomain
|
25
|
+
"images"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec::Matchers.define :be_url do |expected|
|
30
|
+
match do |actual|
|
31
|
+
URI.parse(actual) rescue false
|
32
|
+
end
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attachie
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Benjamin Vetter
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-08-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: swift_client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: aws-sdk-s3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mime-types
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: connection_pool
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bundler
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: fakes3
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: Declarative and flexible attachments
|
140
|
+
email:
|
141
|
+
- vetter@plainpicture.de
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- ".travis.yml"
|
148
|
+
- Gemfile
|
149
|
+
- LICENSE.txt
|
150
|
+
- README.md
|
151
|
+
- Rakefile
|
152
|
+
- attachie.gemspec
|
153
|
+
- docker-compose.yml
|
154
|
+
- lib/attachie.rb
|
155
|
+
- lib/attachie/fake_driver.rb
|
156
|
+
- lib/attachie/file_driver.rb
|
157
|
+
- lib/attachie/interpolation.rb
|
158
|
+
- lib/attachie/s3_driver.rb
|
159
|
+
- lib/attachie/swift_driver.rb
|
160
|
+
- lib/attachie/version.rb
|
161
|
+
- spec/attachie/fake_driver_spec.rb
|
162
|
+
- spec/attachie/file_driver_spec.rb
|
163
|
+
- spec/attachie/s3_driver_spec.rb
|
164
|
+
- spec/attachie/version_spec.rb
|
165
|
+
- spec/attachie_spec.rb
|
166
|
+
- spec/spec_helper.rb
|
167
|
+
homepage: ''
|
168
|
+
licenses:
|
169
|
+
- MIT
|
170
|
+
metadata: {}
|
171
|
+
post_install_message:
|
172
|
+
rdoc_options: []
|
173
|
+
require_paths:
|
174
|
+
- lib
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
requirements: []
|
186
|
+
rubyforge_project:
|
187
|
+
rubygems_version: 2.7.3
|
188
|
+
signing_key:
|
189
|
+
specification_version: 4
|
190
|
+
summary: Declarative and flexible attachments
|
191
|
+
test_files:
|
192
|
+
- spec/attachie/fake_driver_spec.rb
|
193
|
+
- spec/attachie/file_driver_spec.rb
|
194
|
+
- spec/attachie/s3_driver_spec.rb
|
195
|
+
- spec/attachie/version_spec.rb
|
196
|
+
- spec/attachie_spec.rb
|
197
|
+
- spec/spec_helper.rb
|