activestorage_openstack 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6938f92c6bab2b3a685785a1f9327d45698cdb7ee4c37a19f9919b6fa4beb714
4
+ data.tar.gz: 1d06b1f5924d3e91c41827d6aea85db39c4f7f049571f040b976bd787adfa8d5
5
+ SHA512:
6
+ metadata.gz: bdf19b0ed23b27be6d10191dcea2d7966f425d67dd664ba52464349b3427514469ba9ef3becf6f84e3888780cb0d4758a9c90db92435f081aa3312bcc9094d7c
7
+ data.tar.gz: 3a7bc526129a78ab0323ea7ff0851c7294000ad2f5bd02759cf0cbe562356b8cbe1165bb627403199fb6569551cb620807750208b6416a8f3d5ec1ceae6b7e8e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ [![Maintainability](https://api.codeclimate.com/v1/badges/75b77a2b9d9b42496264/maintainability)](https://codeclimate.com/github/argus-api-team/activestorage-openstack/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/75b77a2b9d9b42496264/test_coverage)](https://codeclimate.com/github/argus-api-team/activestorage-openstack/test_coverage) [![Build Status](https://travis-ci.org/argus-api-team/activestorage-openstack.svg?branch=master)](https://travis-ci.org/argus-api-team/activestorage-openstack)
2
+
3
+ # Activestorage::Openstack
4
+ Short description and motivation.
5
+
6
+ ## Usage
7
+ How to use my plugin.
8
+
9
+ ## Installation
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'activestorage_openstack'
14
+ ```
15
+
16
+ And then execute:
17
+ ```bash
18
+ $ bundle
19
+ ```
20
+
21
+ Or install it yourself as:
22
+ ```bash
23
+ $ gem install activestorage_openstack
24
+ ```
25
+
26
+ ## Contributing
27
+ Contribution directions go here.
28
+
29
+ ## License
30
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'Activestorage::Openstack'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ require 'bundler/gem_tasks'
data/config/spring.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spring.application_root = File.expand_path('../spec/dummy', __dir__)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module Openstack
5
+ # Defines Railtie behaviour.
6
+ class Railtie < Rails::Railtie
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module Openstack
5
+ VERSION = '0.1.2'
6
+ end
7
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Service
5
+ # Wraps OpenStack Object Storage Service as an Active Storage service.
6
+ class OpenstackService < Service
7
+ attr_reader :config, :credentials, :storage, :client
8
+
9
+ def initialize(credentials:, container:, region:, **config)
10
+ @config = config
11
+ @credentials = credentials
12
+ @client = ::Openstack::Client.new username: credentials.fetch(:username),
13
+ password: credentials.fetch(:api_key)
14
+ @storage = client.storage container: container,
15
+ region: region
16
+ end
17
+
18
+ # :reek:LongParameterList
19
+ def upload(key, io, checksum: nil, **_options)
20
+ instrument :upload, key: key, checksum: checksum do
21
+ handle_errors do
22
+ storage.put_object(key, io, checksum: checksum)
23
+ end
24
+ end
25
+ end
26
+
27
+ # :reek:UnusedParameters
28
+ def update_metadata(key, **metadata); end
29
+
30
+ def download(key, &block)
31
+ raise ActiveStorage::FileNotFoundError unless exist?(key)
32
+
33
+ if block_given?
34
+ instrument :streaming_download, key: key do
35
+ stream(key, &block)
36
+ end
37
+ else
38
+ instrument :download, key: key do
39
+ storage.get_object(key).body
40
+ end
41
+ end
42
+ end
43
+
44
+ def download_chunk(key, range)
45
+ raise ActiveStorage::FileNotFoundError unless exist?(key)
46
+
47
+ instrument :download_chunk, key: key, range: range do
48
+ storage.get_object_by_range(key, range).body
49
+ end
50
+ end
51
+
52
+ def delete(key)
53
+ instrument :delete, key: key do
54
+ storage.delete_object(key)
55
+ end
56
+ end
57
+
58
+ def delete_prefixed(prefix)
59
+ instrument :delete_prefixed, prefix: prefix do
60
+ keys = JSON.parse(
61
+ storage.list_objects(prefix: prefix).body
62
+ ).map { |object| "/#{storage.container}/#{object.fetch('name')}" }
63
+
64
+ storage.bulk_delete_objects(keys)
65
+ end
66
+ end
67
+
68
+ def exist?(key)
69
+ instrument :exist, key: key do |payload|
70
+ payload[:exist] = storage.show_object_metadata(key).is_a?(Net::HTTPOK)
71
+ payload.fetch(:exist)
72
+ end
73
+ end
74
+
75
+ # :reek:LongParameterList
76
+ # :reek:UnusedParameters
77
+ # rubocop:disable Lint/UnusedMethodArgument
78
+ def url(key, expires_in:, disposition:, filename:, content_type:)
79
+ instrument :url, key: key do |payload|
80
+ payload[:url] = storage.temporary_url(
81
+ key,
82
+ 'GET',
83
+ expires_in: expires_in,
84
+ disposition: disposition,
85
+ filename: filename
86
+ )
87
+ payload.fetch(:url)
88
+ end
89
+ end
90
+
91
+ # :reek:LongParameterList
92
+ def url_for_direct_upload(key, expires_in:, filename:, **_options)
93
+ instrument :url, key: key do |payload|
94
+ payload[:url] = storage.temporary_url(
95
+ key,
96
+ 'PUT',
97
+ expires_in: expires_in,
98
+ filename: filename
99
+ )
100
+ payload.fetch(:url)
101
+ end
102
+ end
103
+
104
+ # :reek:LongParameterList
105
+ # :reek:UnusedParameters
106
+ # rubocop:disable Metrics/LineLength
107
+ def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:)
108
+ {}
109
+ end
110
+ # rubocop:enable Metrics/LineLength
111
+ # rubocop:enable Lint/UnusedMethodArgument
112
+
113
+ private
114
+
115
+ def handle_errors
116
+ return unless block_given?
117
+
118
+ yield.tap do |request|
119
+ raise ActiveStorage::IntegrityError if request.is_a?(
120
+ Net::HTTPUnprocessableEntity
121
+ )
122
+ end
123
+ end
124
+
125
+ # Reads the file for the given key in chunks, yielding each to the block.
126
+ def stream(key, chunk_size: 5.megabytes)
127
+ blob = storage.show_object_metadata(key)
128
+ offset = 0
129
+
130
+ raise ActiveStorage::FileNotFoundError unless blob.present?
131
+
132
+ while offset < Integer(blob.fetch('Content-Length'))
133
+ yield storage.get_object_by_range(
134
+ key, offset..(offset + chunk_size - 1)
135
+ ).body
136
+ offset += chunk_size
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ GEM_ROOT = __dir__
4
+
5
+ require 'net/http'
6
+ require_relative 'support/zeitwerk'
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # Defines OpenStack client behaviours.
5
+ class Client
6
+ include ActiveModel::Model
7
+
8
+ attr_reader :username, :password, :cache
9
+
10
+ validates :username,
11
+ :password,
12
+ presence: true
13
+
14
+ delegate :authenticate, :authenticate_request, to: :authenticator
15
+
16
+ def initialize(username:, password:, cache: Rails.cache)
17
+ @username = username
18
+ @password = password
19
+ @cache = cache
20
+ end
21
+
22
+ def authenticator
23
+ @authenticator ||= Authenticator.new(
24
+ username: username,
25
+ password: password,
26
+ cache: cache
27
+ )
28
+ end
29
+
30
+ def storage(container:, region:)
31
+ @storage ||= Storage.new(
32
+ authenticator: authenticator,
33
+ container: container,
34
+ region: region
35
+ )
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ class Client
5
+ # It retrieves token from OpenStack API and caches it.
6
+ class Authenticator
7
+ include ActiveModel::Model
8
+ include Helpers::CacheReaderable
9
+
10
+ attr_reader :cache,
11
+ :password,
12
+ :uri,
13
+ :username
14
+
15
+ validates :password,
16
+ :username,
17
+ presence: true
18
+
19
+ def initialize(
20
+ username:,
21
+ password:,
22
+ uri: Rails.application.config.x.openstack.fetch(:authentication_url),
23
+ cache: Rails.cache
24
+ )
25
+ @username = username
26
+ @password = password
27
+ @uri = URI(uri)
28
+ @cache = cache
29
+ end
30
+
31
+ def authenticate
32
+ cache_response if token_expired?
33
+ authentication_succeed?
34
+ end
35
+
36
+ def authenticate_request(&_request)
37
+ return unless block_given?
38
+
39
+ authenticate
40
+ yield.tap do |request|
41
+ request.add_field('x-auth-token', token)
42
+ end
43
+ end
44
+
45
+ def token
46
+ read_from_cache.fetch('token')
47
+ end
48
+
49
+ private
50
+
51
+ def cache_response
52
+ cache.write(cache_key, request.body_to_cache)
53
+ end
54
+
55
+ def token_expired?
56
+ read_from_cache.fetch('expires_at') < Time.now
57
+ rescue TypeError, NoMethodError
58
+ true
59
+ end
60
+
61
+ def authentication_succeed?
62
+ case read_from_cache.fetch('code')
63
+ when 201
64
+ true
65
+ else
66
+ false
67
+ end
68
+ end
69
+
70
+ def request
71
+ @request ||= Request.new(
72
+ credentials: credentials,
73
+ uri: uri
74
+ ).call.extend(Helpers::CacheableBody)
75
+ end
76
+
77
+ def credentials
78
+ OpenStruct.new(username: username, password: password)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # :reek:IrresponsibleModule
5
+ class Client
6
+ # :reek:IrresponsibleModule
7
+ class Authenticator
8
+ # Handles authentication request.
9
+ class Request
10
+ include Helpers::HTTPSClient
11
+
12
+ attr_reader :credentials, :uri
13
+
14
+ delegate :username, :password, to: :credentials
15
+
16
+ def initialize(credentials:, uri:)
17
+ @credentials = credentials
18
+ @uri = uri
19
+ end
20
+
21
+ def call
22
+ set_headers
23
+ set_body
24
+ https_client.request(request)
25
+ end
26
+
27
+ private
28
+
29
+ def set_headers
30
+ request.add_field('Content-Type', 'application/json')
31
+ end
32
+
33
+ def request
34
+ @request ||= Net::HTTP::Post.new(uri)
35
+ end
36
+
37
+ def set_body
38
+ request.body = payload
39
+ end
40
+
41
+ def payload
42
+ <<~JSON.squish
43
+ {
44
+ "auth": {
45
+ "identity": {
46
+ "methods": [
47
+ "password"
48
+ ],
49
+ "password": {
50
+ "user": {
51
+ "name": "#{username}",
52
+ "domain": {
53
+ "id": "default"
54
+ },
55
+ "password": "#{password}"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ JSON
62
+ end
63
+ end
64
+ private_constant :Request
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # :reek:IrresponsibleModule
5
+ class Client
6
+ # It interacts with Containers/Objects OpenStack API.
7
+ class Storage
8
+ include ActiveModel::Model
9
+ include Helpers::HTTPSClient
10
+
11
+ attr_reader :authenticator, :container, :region
12
+
13
+ delegate :authenticate_request,
14
+ to: :authenticator
15
+
16
+ validates :authenticator,
17
+ :container,
18
+ :region,
19
+ presence: true
20
+
21
+ def initialize(authenticator:, container:, region:)
22
+ @authenticator = authenticator
23
+ @container = container
24
+ @region = region
25
+ end
26
+
27
+ def uri
28
+ URI(ObjectStoreURL.new(
29
+ authenticator: authenticator,
30
+ container: container,
31
+ region: region
32
+ ).call)
33
+ end
34
+
35
+ def get_object(key, **options)
36
+ https_client.request(
37
+ prepare_request do
38
+ GetObject.new(uri: absolute_uri(key), options: options).request
39
+ end
40
+ )
41
+ end
42
+
43
+ def get_object_by_range(key, range, **options)
44
+ https_client.request(
45
+ prepare_request do
46
+ GetObjectByRange.new(
47
+ uri: absolute_uri(key), range: range, options: options
48
+ ).request
49
+ end
50
+ )
51
+ end
52
+
53
+ def put_object(key, io, checksum: nil)
54
+ https_client.request(
55
+ prepare_request do
56
+ PutObject.new(
57
+ io: io,
58
+ uri: absolute_uri(key),
59
+ checksum: checksum
60
+ ).request
61
+ end
62
+ )
63
+ end
64
+
65
+ def delete_object(key)
66
+ https_client.request(
67
+ prepare_request do
68
+ DeleteObject.new(uri: absolute_uri(key)).request
69
+ end
70
+ )
71
+ end
72
+
73
+ def show_object_metadata(key)
74
+ https_client.request(
75
+ prepare_request do
76
+ ShowObjectMetadata.new(uri: absolute_uri(key)).request
77
+ end
78
+ )
79
+ end
80
+
81
+ def list_objects(**options)
82
+ https_client.request(
83
+ prepare_request do
84
+ ListObjects.new(uri: uri, options: options).request
85
+ end
86
+ )
87
+ end
88
+
89
+ def create_temporary_uri(key, http_method, **options)
90
+ CreateTemporaryURI.new(
91
+ uri: absolute_uri(key),
92
+ http_method: http_method,
93
+ options: options
94
+ ).generate
95
+ end
96
+
97
+ def temporary_url(key, http_method, **options)
98
+ create_temporary_uri(key, http_method, options).to_s
99
+ end
100
+
101
+ def bulk_delete_objects(key_collection)
102
+ https_client.request(
103
+ prepare_request do
104
+ BulkDeleteObjects.new(uri: uri, keys: key_collection).request
105
+ end
106
+ )
107
+ end
108
+
109
+ private
110
+
111
+ def prepare_request
112
+ return unless block_given?
113
+
114
+ authenticate_request do
115
+ yield.tap do |request|
116
+ request.add_field('Accept', 'application/json')
117
+ end
118
+ end
119
+ end
120
+
121
+ def absolute_uri(key)
122
+ URI("#{uri}/#{key}")
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # :reek:IrresponsibleModule
5
+ class Client
6
+ # :reek:IrresponsibleModule
7
+ class Storage
8
+ # Deletes objects in bulk.
9
+ # More details here: https://docs.openstack.org/swift/latest/middleware.html#bulk-delete
10
+ class BulkDeleteObjects
11
+ attr_reader :uri, :keys
12
+
13
+ def initialize(uri:, keys: [])
14
+ @uri = uri
15
+ @keys = keys
16
+ end
17
+
18
+ def request
19
+ add_params
20
+ Net::HTTP::Post.new(uri).tap do |request|
21
+ request.add_field('Content-type', 'text/plain')
22
+ request.add_field('Accept', 'application/json')
23
+ request.body = keys.join("\n")
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def add_params
30
+ uri.query = URI.encode_www_form('bulk-delete' => nil)
31
+ end
32
+ end
33
+ private_constant :BulkDeleteObjects
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # :reek:IrresponsibleModule
5
+ class Client
6
+ # :reek:IrresponsibleModule
7
+ class Storage
8
+ # Create a tempory URL according to OpenStack specification.
9
+ # More details here: https://docs.openstack.org/swift/latest/api/temporary_url_middleware.html
10
+ class CreateTemporaryURI
11
+ attr_reader :uri, :http_method, :options
12
+
13
+ def initialize(uri:, http_method:, options: {})
14
+ @uri = uri
15
+ @http_method = http_method
16
+ @options = options
17
+ end
18
+
19
+ def generate
20
+ add_params
21
+ uri
22
+ end
23
+
24
+ private
25
+
26
+ def add_params
27
+ uri.query = URI.encode_www_form(
28
+ temp_url_sig: signature,
29
+ temp_url_expires: expires_in,
30
+ filename: filename,
31
+ disposition => nil
32
+ )
33
+ end
34
+
35
+ def signature
36
+ OpenSSL::HMAC.hexdigest(algorithm, temporary_url_key, hmac_body)
37
+ end
38
+
39
+ def expires_in
40
+ @expires_in ||= options.fetch(:expires_in) do
41
+ 15.minutes
42
+ end.from_now.to_i
43
+ end
44
+
45
+ def filename
46
+ options.fetch(:filename) { File.basename(uri.path) }
47
+ end
48
+
49
+ def disposition
50
+ options.fetch(:disposition) { 'inline' }
51
+ end
52
+
53
+ def algorithm
54
+ 'SHA1'
55
+ end
56
+
57
+ def temporary_url_key
58
+ options.fetch(:temporary_url_key) do
59
+ Rails.application.credentials.openstack.fetch(:temporary_url_key)
60
+ end
61
+ end
62
+
63
+ def hmac_body
64
+ [http_method, expires_in, uri.path].join("\n")
65
+ end
66
+ end
67
+ private_constant :CreateTemporaryURI
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # :reek:IrresponsibleModule
5
+ class Client
6
+ # :reek:IrresponsibleModule
7
+ class Storage
8
+ # Deletes object at the specified URI.
9
+ class DeleteObject
10
+ attr_reader :uri
11
+
12
+ def initialize(uri:)
13
+ @uri = uri
14
+ end
15
+
16
+ def request
17
+ Net::HTTP::Delete.new(uri)
18
+ end
19
+ end
20
+ private_constant :DeleteObject
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # :reek:IrresponsibleModule
5
+ class Client
6
+ # :reek:IrresponsibleModule
7
+ class Storage
8
+ # Downloads the object at the specified URI.
9
+ class GetObject
10
+ attr_reader :uri, :options
11
+
12
+ def initialize(uri:, options: {})
13
+ @uri = uri
14
+ @options = options
15
+ end
16
+
17
+ def request
18
+ Net::HTTP::Get.new(uri)
19
+ end
20
+ end
21
+ private_constant :GetObject
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # :reek:IrresponsibleModule
5
+ class Client
6
+ # :reek:IrresponsibleModule
7
+ class Storage
8
+ # Downloads the object by chunks at the specified URI.
9
+ # It uses the `Range` header with byte range.
10
+ class GetObjectByRange < GetObject
11
+ attr_reader :range
12
+
13
+ def initialize(uri:, range:, options: {})
14
+ super(uri: uri, options: options)
15
+ @range = range
16
+ end
17
+
18
+ def request
19
+ super.tap do |request|
20
+ request.add_field('Range', byte_range)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def byte_range
27
+ "bytes=#{first_byte}-#{last_byte}"
28
+ end
29
+
30
+ def first_byte
31
+ range.begin
32
+ end
33
+
34
+ def last_byte
35
+ range.exclude_end? ? range_end - 1 : range_end
36
+ end
37
+
38
+ def range_end
39
+ range.end
40
+ end
41
+ end
42
+ private_constant :GetObjectByRange
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # :reek:IrresponsibleModule
5
+ class Client
6
+ # :reek:IrresponsibleModule
7
+ class Storage
8
+ # List objects at the specified URI.
9
+ # Generally a container is specified.
10
+ # The `prefix=` url variable filters the list retrieved.
11
+ class ListObjects
12
+ attr_reader :uri, :options
13
+
14
+ def initialize(uri:, options: {})
15
+ @uri = uri
16
+ @options = options
17
+ end
18
+
19
+ def request
20
+ add_params
21
+ Net::HTTP::Get.new(uri)
22
+ end
23
+
24
+ private
25
+
26
+ def add_params
27
+ uri.query = URI.encode_www_form(options)
28
+ end
29
+ end
30
+ private_constant :ListObjects
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # :reek:IrresponsibleModule
5
+ class Client
6
+ # :reek:IrresponsibleModule
7
+ class Storage
8
+ # Extracts the object store URL from cached payload mathing the
9
+ # specified region.
10
+ class ObjectStoreURL
11
+ include Helpers::CacheReaderable
12
+
13
+ attr_reader :authenticator, :container, :region
14
+
15
+ delegate :cache, :cache_key, :authenticate, to: :authenticator
16
+
17
+ def initialize(authenticator:, container:, region:)
18
+ @authenticator = authenticator
19
+ @container = container
20
+ @region = region
21
+ authenticate
22
+ end
23
+
24
+ def call
25
+ "#{regionized_object_store_url}/#{container}"
26
+ end
27
+
28
+ private
29
+
30
+ def regionized_object_store_url
31
+ object_store_endpoints.find do |endpoint|
32
+ endpoint.fetch('region') == region
33
+ end.fetch('url')
34
+ end
35
+
36
+ def object_store_endpoints
37
+ catalog_collection.find do |catalog|
38
+ catalog.fetch('type') == 'object-store'
39
+ end.fetch('endpoints', [])
40
+ end
41
+
42
+ def catalog_collection
43
+ read_from_cache.dig('body', 'token', 'catalog') || []
44
+ end
45
+ end
46
+ private_constant :ObjectStoreURL
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'mimemagic'
5
+
6
+ module Openstack
7
+ # :reek:IrresponsibleModule
8
+ class Client
9
+ # :reek:IrresponsibleModule
10
+ class Storage
11
+ # Uploads a file to the Object Store.
12
+ # Checksum is validated after upload.
13
+ class PutObject
14
+ attr_reader :checksum, :io, :uri
15
+
16
+ def initialize(io:, uri:, checksum: nil)
17
+ @io = io
18
+ @uri = uri
19
+ @checksum = checksum
20
+ end
21
+
22
+ def request
23
+ Net::HTTP::Put.new(uri).tap do |request|
24
+ request.add_field('Content-Type', content_type)
25
+ request.add_field('ETag', md5_checksum)
26
+ request.body = io.read
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def content_type
33
+ Marcel::MimeType.for io,
34
+ name: io.try(:original_filename),
35
+ declared_type: io.try(:content_type)
36
+ end
37
+
38
+ def md5_checksum
39
+ return checksum_to_hexdigest if checksum.present?
40
+
41
+ Digest::MD5.file(io).hexdigest
42
+ end
43
+
44
+ # ActiveStorage sends a `Digest::MD5.base64digest` checksum
45
+ # OpenStack expects a `Digest::MD5.hexdigest` ETag
46
+ def checksum_to_hexdigest
47
+ checksum.unpack1('m0').unpack1('H*')
48
+ end
49
+ end
50
+ private_constant :PutObject
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ # :reek:IrresponsibleModule
5
+ class Client
6
+ # :reek:IrresponsibleModule
7
+ class Storage
8
+ # Shows the object metadata without downloading it.
9
+ # Useful to check if object exists.
10
+ class ShowObjectMetadata
11
+ attr_reader :uri
12
+
13
+ def initialize(uri:)
14
+ @uri = uri
15
+ end
16
+
17
+ def request
18
+ Net::HTTP::Head.new(uri)
19
+ end
20
+ end
21
+ private_constant :ShowObjectMetadata
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ module Helpers
5
+ # Methods to interact with cache.
6
+ module CacheReaderable
7
+ def cache_key
8
+ "openstack/token-#{username}"
9
+ end
10
+
11
+ def read_from_cache
12
+ @read_from_cache ||= JSON.parse(cache.read(cache_key))
13
+ rescue TypeError
14
+ null_cache_placeholder
15
+ end
16
+
17
+ def null_cache_placeholder
18
+ {
19
+ 'headers' => nil,
20
+ 'token' => nil,
21
+ 'expires_at' => nil,
22
+ 'code' => nil,
23
+ 'message' => nil,
24
+ 'body' => nil
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ module Helpers
5
+ # cache-friendly response body.
6
+ module CacheableBody
7
+ def body_to_cache
8
+ # We cache JSON rather than ruby object. Simple object.
9
+ {
10
+ headers: headers,
11
+ token: token,
12
+ expires_at: expires_at,
13
+ code: Integer(code),
14
+ message: message,
15
+ body: body_as_hash
16
+ }.to_json
17
+ end
18
+
19
+ private
20
+
21
+ def headers
22
+ each_header.to_h
23
+ end
24
+
25
+ def token
26
+ header.fetch('X-Subject-Token') { nil }
27
+ end
28
+
29
+ def expires_at
30
+ Time.parse(body_as_hash.dig('token', 'expires_at'))
31
+ rescue TypeError
32
+ nil
33
+ end
34
+
35
+ def body_as_hash
36
+ JSON.parse(body)
37
+ rescue JSON::ParserError
38
+ {}
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openstack
4
+ module Helpers
5
+ # Enables SSL mode for the specified uri.
6
+ module HTTPSClient
7
+ def https_client
8
+ Net::HTTP.new(uri.host, uri.port).tap do |client|
9
+ client.use_ssl = true
10
+ client.verify_mode = OpenSSL::SSL::VERIFY_NONE
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+
5
+ # Inflector for Zeitwerk
6
+ class CustomInflector < Zeitwerk::Inflector
7
+ # :reek:ControlParameter imposed by Zeitwerk gem
8
+ def camelize(basename, _abspath)
9
+ case basename
10
+ when 'https_client'
11
+ 'HTTPSClient'
12
+ when 'object_store_url'
13
+ 'ObjectStoreURL'
14
+ when 'create_temporary_uri'
15
+ 'CreateTemporaryURI'
16
+ else
17
+ super
18
+ end
19
+ end
20
+ end
21
+
22
+ loader = Zeitwerk::Loader.new
23
+ # loader.logger = method(:puts)
24
+ loader.inflector = CustomInflector.new
25
+ loader.preload("#{GEM_ROOT}/active_storage/service/openstack_service.rb")
26
+ loader.push_dir(GEM_ROOT)
27
+ loader.ignore(__dir__)
28
+ loader.ignore("#{GEM_ROOT}/activestorage_openstack.rb")
29
+ loader.setup
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activestorage_openstack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Mickael Palma
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-03-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: marcel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 6.0.0.beta2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 6.0.0.beta2
41
+ - !ruby/object:Gem::Dependency
42
+ name: tzinfo-data
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.8'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.3.6
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.3.6
97
+ description: OpenStack ActiveStorage service without dependencies.
98
+ email:
99
+ - mpalma@largus.fr
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - MIT-LICENSE
105
+ - README.md
106
+ - Rakefile
107
+ - config/spring.rb
108
+ - lib/active_storage/openstack/railtie.rb
109
+ - lib/active_storage/openstack/version.rb
110
+ - lib/active_storage/service/openstack_service.rb
111
+ - lib/activestorage_openstack.rb
112
+ - lib/openstack/client.rb
113
+ - lib/openstack/client/authenticator.rb
114
+ - lib/openstack/client/authenticator/request.rb
115
+ - lib/openstack/client/storage.rb
116
+ - lib/openstack/client/storage/bulk_delete_objects.rb
117
+ - lib/openstack/client/storage/create_temporary_uri.rb
118
+ - lib/openstack/client/storage/delete_object.rb
119
+ - lib/openstack/client/storage/get_object.rb
120
+ - lib/openstack/client/storage/get_object_by_range.rb
121
+ - lib/openstack/client/storage/list_objects.rb
122
+ - lib/openstack/client/storage/object_store_url.rb
123
+ - lib/openstack/client/storage/put_object.rb
124
+ - lib/openstack/client/storage/show_object_metadata.rb
125
+ - lib/openstack/helpers/cache_readerable.rb
126
+ - lib/openstack/helpers/cacheable_body.rb
127
+ - lib/openstack/helpers/https_client.rb
128
+ - lib/support/zeitwerk.rb
129
+ homepage: https://github.com/mickael-palma-argus/activestorage-openstack
130
+ licenses:
131
+ - MIT
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 3.0.3
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: OpenStack ActiveStorage service.
152
+ test_files: []