activestorage_openstack 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: []