attachie 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
@@ -0,0 +1,13 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ addons:
6
+ hosts:
7
+ - bucket.localhost
8
+ before_install:
9
+ - docker-compose up -d
10
+ - sleep 10
11
+ install:
12
+ - travis_retry bundle install
13
+ script: rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in attachments.gemspec
4
+ gemspec
@@ -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.
@@ -0,0 +1,103 @@
1
+ # Attachie
2
+
3
+ [![Build Status](https://secure.travis-ci.org/mrkamel/attachie.png?branch=master)](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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -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
@@ -0,0 +1,9 @@
1
+
2
+ version: '2'
3
+
4
+ services:
5
+ fakes3:
6
+ image: lphoward/fake-s3
7
+ ports:
8
+ - 4569:4569
9
+
@@ -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,3 @@
1
+ module Attachie
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
+
@@ -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