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.
@@ -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