volcano-sdk 0.30.0 → 0.32.0
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 +4 -4
- data/lib/volcano/auth.rb +4 -48
- data/lib/volcano/auth_session_construction.rb +67 -0
- data/lib/volcano/auth_sign_up.rb +2 -0
- data/lib/volcano/realtime/broadcast_delivery.rb +26 -2
- data/lib/volcano/realtime/channel_callbacks.rb +19 -14
- data/lib/volcano/realtime/channel_lifecycle.rb +9 -9
- data/lib/volcano/realtime/connection.rb +9 -5
- data/lib/volcano/realtime/connection_callbacks.rb +5 -1
- data/lib/volcano/realtime/lifecycle.rb +2 -1
- data/lib/volcano/realtime/postgres_batch.rb +11 -3
- data/lib/volcano/realtime/postgres_changes.rb +31 -16
- data/lib/volcano/realtime/postgres_database.rb +14 -1
- data/lib/volcano/realtime/postgres_delivery.rb +6 -1
- data/lib/volcano/realtime/postgres_expansion.rb +10 -4
- data/lib/volcano/realtime/presence.rb +18 -4
- data/lib/volcano/realtime/presence_state.rb +23 -6
- data/lib/volcano/realtime/protocol.rb +14 -9
- data/lib/volcano/realtime/protocol_dispatch.rb +25 -7
- data/lib/volcano/realtime/protocol_io.rb +8 -4
- data/lib/volcano/realtime/protocol_lifecycle.rb +2 -0
- data/lib/volcano/realtime/protocol_publication_dispatch.rb +12 -10
- data/lib/volcano/realtime/protocol_recovery.rb +7 -3
- data/lib/volcano/realtime/protocol_recovery_dispatch.rb +2 -0
- data/lib/volcano/realtime/reconnect.rb +4 -1
- data/lib/volcano/realtime.rb +122 -99
- data/lib/volcano/storage.rb +18 -34
- data/lib/volcano/storage_models.rb +18 -5
- data/lib/volcano/storage_mutations.rb +8 -3
- data/lib/volcano/storage_object_response.rb +88 -0
- data/lib/volcano/storage_response.rb +92 -0
- data/lib/volcano/storage_resumable_upload.rb +13 -2
- data/lib/volcano/storage_session_adapter.rb +61 -0
- data/lib/volcano/storage_transport_adapter.rb +73 -0
- data/lib/volcano/storage_upload_models.rb +51 -27
- data/lib/volcano/storage_upload_sessions.rb +1 -35
- data/lib/volcano/version.rb +1 -1
- data/lib/volcano.rb +6 -1
- data/sig/auth_credentials.rbs +15 -0
- data/sig/realtime.rbs +46 -0
- data/sig/realtime_postgres_change.rbs +1 -2
- data/sig/storage.rbs +29 -0
- data/sig/storage_records.rbs +3 -1
- data/sig/upload_session_status.rbs +3 -1
- metadata +9 -1
data/lib/volcano/realtime.rb
CHANGED
|
@@ -32,134 +32,148 @@ module Volcano
|
|
|
32
32
|
|
|
33
33
|
# Recursively snapshots JSON-compatible values for callback and state safety.
|
|
34
34
|
module Immutable
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def optional(value)
|
|
38
|
-
value && call(value)
|
|
35
|
+
def self.optional(value)
|
|
36
|
+
value && snapshot(value)
|
|
39
37
|
end
|
|
40
38
|
|
|
41
|
-
def call(value)
|
|
39
|
+
def self.call(value) = snapshot(value)
|
|
40
|
+
|
|
41
|
+
def self.snapshot(value)
|
|
42
42
|
case value
|
|
43
43
|
when Hash then call_hash(value)
|
|
44
|
-
when Array then value.map { |child|
|
|
44
|
+
when Array then value.map { |child| snapshot(child) }.freeze
|
|
45
45
|
when String then value.dup.freeze
|
|
46
46
|
else value.freeze
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def call_hash(value)
|
|
51
|
-
value.to_h { |key, item| [
|
|
50
|
+
def self.call_hash(value)
|
|
51
|
+
value.to_h { |key, item| [snapshot(key), snapshot(item)] }.freeze
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
private_constant :Immutable
|
|
55
55
|
|
|
56
|
-
PresenceInfo = Data.define(:client, :user, :data)
|
|
56
|
+
PresenceInfo = Data.define(:client, :user, :data)
|
|
57
|
+
|
|
58
|
+
# An immutable snapshot of one presence client.
|
|
59
|
+
class PresenceInfo
|
|
60
|
+
# @dynamic client, user, data, members, with, to_h, deconstruct, deconstruct_keys
|
|
61
|
+
# @dynamic self.[], self.members
|
|
57
62
|
def initialize(client:, user: nil, data: {})
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
+
client = Immutable.call(client.to_s)
|
|
64
|
+
user = user.nil? ? nil : Immutable.call(user.to_s)
|
|
65
|
+
data = Immutable.call(data)
|
|
66
|
+
super
|
|
63
67
|
end
|
|
64
68
|
end
|
|
65
69
|
|
|
66
70
|
CHANNEL_TYPES = %i[broadcast presence postgres].freeze
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@
|
|
71
|
-
@
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
# Owns the public realtime facade and its channel collection.
|
|
73
|
+
module Core
|
|
74
|
+
# @dynamic initialize_postgres_database, connect_protocol, close_protocol, public_error
|
|
75
|
+
# @dynamic protocol_error, open_socket, default_reconnect_delay, __send__
|
|
76
|
+
def initialize(client, api_url:, socket_factory: nil, reconnect_delay: nil)
|
|
77
|
+
@client = client
|
|
78
|
+
@api_url = api_url
|
|
79
|
+
@socket_factory = socket_factory || ->(address) { open_socket(address) }
|
|
80
|
+
@reconnect_delay = reconnect_delay || ->(attempt) { default_reconnect_delay(attempt) }
|
|
81
|
+
initialize_realtime_state
|
|
82
|
+
initialize_postgres_database
|
|
83
|
+
end
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
type = normalize_channel_type(type)
|
|
95
|
-
batch_config = PostgresBatchConfig.new(
|
|
96
|
-
auto_fetch:, batch_window_ms: fetch_batch_window_ms,
|
|
97
|
-
max_batch_size: fetch_max_batch_size
|
|
85
|
+
def initialize_realtime_state
|
|
86
|
+
@protocol = nil
|
|
87
|
+
@protocol_lock = nil
|
|
88
|
+
@channel_lock = nil
|
|
89
|
+
@channels = {}
|
|
90
|
+
@connection_callbacks = { connect: {}, disconnect: {}, error: {} }
|
|
91
|
+
@next_connection_callback_id = 0
|
|
92
|
+
@protocol_session_lineage = nil
|
|
93
|
+
@reconnect_task = nil
|
|
94
|
+
@closed = false
|
|
95
|
+
end
|
|
96
|
+
private :initialize_realtime_state
|
|
97
|
+
|
|
98
|
+
def channel(
|
|
99
|
+
name, type: :broadcast, auto_fetch: true,
|
|
100
|
+
fetch_batch_window_ms: 20, fetch_max_batch_size: 50
|
|
98
101
|
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
type = normalize_channel_type(type)
|
|
103
|
+
batch_config = PostgresBatchConfig.new(
|
|
104
|
+
auto_fetch:, batch_window_ms: fetch_batch_window_ms,
|
|
105
|
+
max_batch_size: fetch_max_batch_size
|
|
106
|
+
)
|
|
107
|
+
channel_lock.acquire do
|
|
108
|
+
ensure_open!
|
|
109
|
+
channel_name = "#{type}:#{name}"
|
|
110
|
+
channel = @channels[channel_name]
|
|
111
|
+
next cached_channel(channel, batch_config) if channel
|
|
104
112
|
|
|
105
|
-
|
|
113
|
+
@channels[channel_name] = build_channel(channel_name, type, batch_config)
|
|
114
|
+
end
|
|
106
115
|
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def protocol
|
|
110
|
-
ensure_open!
|
|
111
|
-
return @protocol if @protocol
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
def protocol
|
|
114
118
|
ensure_open!
|
|
115
|
-
@protocol
|
|
119
|
+
current_protocol = @protocol
|
|
120
|
+
return current_protocol if current_protocol
|
|
121
|
+
|
|
122
|
+
protocol_lock.acquire do
|
|
123
|
+
ensure_open!
|
|
124
|
+
@protocol ||= connect_protocol
|
|
125
|
+
end
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
raise public_error(e), cause: nil
|
|
116
128
|
end
|
|
117
|
-
|
|
118
|
-
raise public_error(e), cause: nil
|
|
119
|
-
end
|
|
120
|
-
private :protocol
|
|
129
|
+
private :protocol
|
|
121
130
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
def disconnect
|
|
132
|
+
lock = @protocol_lock
|
|
133
|
+
lock ? lock.acquire { close_protocol } : close_protocol
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
raise public_error(e), cause: nil
|
|
136
|
+
end
|
|
127
137
|
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
def ensure_open!
|
|
139
|
+
raise ClosedError, 'realtime connection closed' if @closed
|
|
130
140
|
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
self
|
|
142
|
+
end
|
|
133
143
|
|
|
134
|
-
|
|
144
|
+
private
|
|
135
145
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
def normalize_channel_type(type)
|
|
147
|
+
normalized = type.to_s.to_sym
|
|
148
|
+
return normalized if CHANNEL_TYPES.include?(normalized)
|
|
139
149
|
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
raise ArgumentError, "unsupported realtime channel type: #{type}"
|
|
151
|
+
end
|
|
142
152
|
|
|
143
|
-
|
|
153
|
+
def report_channel_error(error) = protocol_error(public_error(error))
|
|
144
154
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
155
|
+
def cached_channel(channel, batch_config)
|
|
156
|
+
channel.__send__(:ensure_fetch_config!, batch_config)
|
|
157
|
+
channel
|
|
158
|
+
end
|
|
149
159
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
def build_channel(name, type, batch_config)
|
|
161
|
+
Channel.new(self, -> { protocol }, name, type, batch_config:)
|
|
162
|
+
end
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
164
|
+
def protocol_lock
|
|
165
|
+
require 'async/semaphore'
|
|
166
|
+
@protocol_lock ||= Async::Semaphore.new(1)
|
|
167
|
+
end
|
|
158
168
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
def channel_lock
|
|
170
|
+
require 'async/semaphore'
|
|
171
|
+
@channel_lock ||= Async::Semaphore.new(1)
|
|
172
|
+
end
|
|
162
173
|
end
|
|
174
|
+
include Core
|
|
175
|
+
|
|
176
|
+
private_constant :Core
|
|
163
177
|
|
|
164
178
|
# Represents one realtime broadcast channel.
|
|
165
179
|
class Channel
|
|
@@ -175,14 +189,15 @@ module Volcano
|
|
|
175
189
|
|
|
176
190
|
attr_reader :name
|
|
177
191
|
|
|
192
|
+
# @dynamic name
|
|
193
|
+
|
|
178
194
|
def initialize(realtime, protocol_provider, name, type, batch_config:)
|
|
179
195
|
@realtime = realtime
|
|
180
196
|
@protocol_provider = protocol_provider
|
|
181
197
|
@name = name.freeze
|
|
182
198
|
@handler_registered = @subscribed = @subscription_desired = @closed = false
|
|
183
199
|
@lifecycle_lock = nil
|
|
184
|
-
|
|
185
|
-
@recovery_lineage = nil
|
|
200
|
+
initialize_recovery_state
|
|
186
201
|
initialize_callback_dispatch
|
|
187
202
|
initialize_presence(type)
|
|
188
203
|
initialize_postgres_delivery(batch_config)
|
|
@@ -208,7 +223,7 @@ module Volcano
|
|
|
208
223
|
|
|
209
224
|
def unsubscribe
|
|
210
225
|
state = with_lifecycle_lock { unsubscribe_protocol }
|
|
211
|
-
emit_presence_sync(state)
|
|
226
|
+
emit_presence_sync(state) if state.is_a?(Hash)
|
|
212
227
|
nil
|
|
213
228
|
end
|
|
214
229
|
|
|
@@ -218,11 +233,12 @@ module Volcano
|
|
|
218
233
|
|
|
219
234
|
def restore_subscription(protocol)
|
|
220
235
|
state = with_lifecycle_lock do
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
236
|
+
restoration_needed? ? subscribe_protocol(protocol) : nil
|
|
237
|
+
end
|
|
238
|
+
if state
|
|
239
|
+
subscribed_protocol, epoch = state
|
|
240
|
+
sync_presence(subscribed_protocol, epoch)
|
|
224
241
|
end
|
|
225
|
-
sync_presence(*state) if state
|
|
226
242
|
nil
|
|
227
243
|
end
|
|
228
244
|
|
|
@@ -240,6 +256,13 @@ module Volcano
|
|
|
240
256
|
|
|
241
257
|
private
|
|
242
258
|
|
|
259
|
+
def initialize_recovery_state
|
|
260
|
+
# @type var recovery_position: Hash[Symbol, String | Integer]
|
|
261
|
+
recovery_position = {}
|
|
262
|
+
@recovery_position = recovery_position.freeze
|
|
263
|
+
@recovery_lineage = nil
|
|
264
|
+
end
|
|
265
|
+
|
|
243
266
|
def subscribe_with_intent
|
|
244
267
|
ensure_open!
|
|
245
268
|
raise DuplicateSubscriptionError, "already subscribed to #{@name}" if @subscription_desired
|
|
@@ -254,8 +277,8 @@ module Volcano
|
|
|
254
277
|
|
|
255
278
|
def with_lifecycle_lock(&)
|
|
256
279
|
require 'async/semaphore'
|
|
257
|
-
@lifecycle_lock ||= Async::Semaphore.new(1)
|
|
258
|
-
|
|
280
|
+
lock = @lifecycle_lock ||= Async::Semaphore.new(1)
|
|
281
|
+
lock.acquire(&)
|
|
259
282
|
end
|
|
260
283
|
|
|
261
284
|
def ensure_open!
|
data/lib/volcano/storage.rb
CHANGED
|
@@ -6,8 +6,8 @@ module Volcano
|
|
|
6
6
|
# Entry point for project object storage.
|
|
7
7
|
class Storage
|
|
8
8
|
def initialize(client, transport, api_url:, anon_key:)
|
|
9
|
-
@client = client
|
|
10
|
-
@transport = transport
|
|
9
|
+
@client = client.is_a?(StorageSessionAdapter) ? client : StorageSessionAdapter.new(client)
|
|
10
|
+
@transport = transport.is_a?(StorageTransportAdapter) ? transport : StorageTransportAdapter.new(transport)
|
|
11
11
|
@api_url = api_url
|
|
12
12
|
@anon_key = anon_key
|
|
13
13
|
end
|
|
@@ -25,9 +25,11 @@ module Volcano
|
|
|
25
25
|
|
|
26
26
|
# Operates on objects in one storage bucket.
|
|
27
27
|
class StorageBucket
|
|
28
|
+
include StorageObjectResponse
|
|
29
|
+
|
|
28
30
|
def initialize(client, transport, name, api_url:, anon_key:)
|
|
29
|
-
@client = client
|
|
30
|
-
@transport = transport
|
|
31
|
+
@client = client.is_a?(StorageSessionAdapter) ? client : StorageSessionAdapter.new(client)
|
|
32
|
+
@transport = transport.is_a?(StorageTransportAdapter) ? transport : StorageTransportAdapter.new(transport)
|
|
31
33
|
@name = name.dup.freeze
|
|
32
34
|
@api_url = api_url.dup.freeze
|
|
33
35
|
@anon_key = anon_key.dup.freeze
|
|
@@ -36,7 +38,7 @@ module Volcano
|
|
|
36
38
|
|
|
37
39
|
def upload(path, value, content_type: nil)
|
|
38
40
|
mime_type = upload_content_type(content_type)
|
|
39
|
-
path = storage_paths(path).
|
|
41
|
+
path = storage_paths(path).fetch(0)
|
|
40
42
|
binding = @client.capture_session_binding
|
|
41
43
|
@client.session_token
|
|
42
44
|
content = upload_bytes(value).freeze
|
|
@@ -49,11 +51,11 @@ module Volcano
|
|
|
49
51
|
content_type: mime_type
|
|
50
52
|
)
|
|
51
53
|
end
|
|
52
|
-
Transport.body(response, 201)
|
|
54
|
+
storage_payload(Transport.body(response, 201))
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
def download(path, range: nil)
|
|
56
|
-
path = storage_paths(path).
|
|
58
|
+
path = storage_paths(path).fetch(0)
|
|
57
59
|
range = range&.dup&.freeze
|
|
58
60
|
response = storage_request do |token|
|
|
59
61
|
@transport.download_storage_object(
|
|
@@ -65,7 +67,7 @@ module Volcano
|
|
|
65
67
|
end
|
|
66
68
|
expected_status = range && response.status == 206 ? 206 : 200
|
|
67
69
|
Transport.body(response, expected_status)
|
|
68
|
-
response.data.b
|
|
70
|
+
storage_string(response.data).b
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
def list(prefix = '', limit: nil, cursor: nil)
|
|
@@ -80,11 +82,10 @@ module Volcano
|
|
|
80
82
|
cursor: cursor
|
|
81
83
|
)
|
|
82
84
|
end
|
|
83
|
-
payload = Transport.body(response, 200)
|
|
84
|
-
next_cursor = payload['next_cursor']
|
|
85
|
+
payload = storage_payload(Transport.body(response, 200))
|
|
85
86
|
StoragePage.new(
|
|
86
|
-
objects: payload.fetch('objects', [])
|
|
87
|
-
next_cursor: next_cursor
|
|
87
|
+
objects: storage_object_list(payload.fetch('objects', [])),
|
|
88
|
+
next_cursor: storage_cursor(payload['next_cursor'])
|
|
88
89
|
)
|
|
89
90
|
end
|
|
90
91
|
|
|
@@ -102,31 +103,14 @@ module Volcano
|
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
def upload_bytes(value)
|
|
105
|
-
bytes = value.
|
|
106
|
+
bytes = if value.is_a?(String)
|
|
107
|
+
value
|
|
108
|
+
elsif value.respond_to?(:read)
|
|
109
|
+
value.read
|
|
110
|
+
end
|
|
106
111
|
raise ArgumentError, 'upload data must be a String or IO' unless bytes.is_a?(String)
|
|
107
112
|
|
|
108
113
|
bytes.b
|
|
109
114
|
end
|
|
110
|
-
|
|
111
|
-
def storage_object(payload)
|
|
112
|
-
StorageObject.new(
|
|
113
|
-
id: payload.fetch('id'),
|
|
114
|
-
bucket_id: payload.fetch('bucket_id'),
|
|
115
|
-
name: payload.fetch('name'),
|
|
116
|
-
size: payload.fetch('size'),
|
|
117
|
-
mime_type: payload.fetch('mime_type'),
|
|
118
|
-
is_public: payload.fetch('is_public'),
|
|
119
|
-
owner_id: payload['owner_id'],
|
|
120
|
-
etag: payload['etag'],
|
|
121
|
-
metadata: payload['metadata'],
|
|
122
|
-
created_at: parse_time(payload['created_at']),
|
|
123
|
-
updated_at: parse_time(payload['updated_at']),
|
|
124
|
-
public_url: payload['public_url']
|
|
125
|
-
)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def parse_time(value)
|
|
129
|
-
value.is_a?(String) ? Time.iso8601(value) : value
|
|
130
|
-
end
|
|
131
115
|
end
|
|
132
116
|
end
|
|
@@ -6,7 +6,13 @@ module Volcano
|
|
|
6
6
|
].freeze
|
|
7
7
|
private_constant :STORAGE_OBJECT_ATTRIBUTES
|
|
8
8
|
|
|
9
|
-
StorageObject = Data.define(*STORAGE_OBJECT_ATTRIBUTES)
|
|
9
|
+
StorageObject = Data.define(*STORAGE_OBJECT_ATTRIBUTES)
|
|
10
|
+
|
|
11
|
+
# Validates and freezes object metadata returned by storage.
|
|
12
|
+
class StorageObject
|
|
13
|
+
# @dynamic id, bucket_id, name, size, mime_type, is_public, owner_id, etag
|
|
14
|
+
# @dynamic metadata, created_at, updated_at, public_url
|
|
15
|
+
# @dynamic members, with, to_h, deconstruct, deconstruct_keys, self.[], self.members
|
|
10
16
|
def initialize(**attributes)
|
|
11
17
|
unknown = attributes.keys - STORAGE_OBJECT_ATTRIBUTES
|
|
12
18
|
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
@@ -17,7 +23,8 @@ module Volcano
|
|
|
17
23
|
values = STORAGE_OBJECT_ATTRIBUTES.to_h do |name|
|
|
18
24
|
[name, immutable_value(attributes[name])]
|
|
19
25
|
end
|
|
20
|
-
|
|
26
|
+
attributes = values
|
|
27
|
+
super
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
private
|
|
@@ -40,10 +47,16 @@ module Volcano
|
|
|
40
47
|
end
|
|
41
48
|
end
|
|
42
49
|
|
|
43
|
-
StoragePage = Data.define(:objects, :next_cursor)
|
|
50
|
+
StoragePage = Data.define(:objects, :next_cursor)
|
|
51
|
+
|
|
52
|
+
# Freezes a page of storage objects and its cursor.
|
|
53
|
+
class StoragePage
|
|
54
|
+
# @dynamic objects, next_cursor, members, with, to_h, deconstruct, deconstruct_keys
|
|
55
|
+
# @dynamic self.[], self.members
|
|
44
56
|
def initialize(objects:, next_cursor: nil)
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
objects = objects.to_a.dup.freeze
|
|
58
|
+
next_cursor = next_cursor.nil? ? nil : next_cursor.dup.freeze
|
|
59
|
+
super
|
|
47
60
|
end
|
|
48
61
|
end
|
|
49
62
|
end
|
|
@@ -11,7 +11,9 @@ module Volcano
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def move(from_path, to_path)
|
|
14
|
-
|
|
14
|
+
paths = storage_paths([from_path, to_path])
|
|
15
|
+
source = paths.fetch(0)
|
|
16
|
+
destination = paths.fetch(1)
|
|
15
17
|
response = storage_request do |token|
|
|
16
18
|
@transport.move_storage_object(
|
|
17
19
|
authorization: token,
|
|
@@ -24,7 +26,9 @@ module Volcano
|
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def copy(from_path, to_path)
|
|
27
|
-
|
|
29
|
+
paths = storage_paths([from_path, to_path])
|
|
30
|
+
source = paths.fetch(0)
|
|
31
|
+
destination = paths.fetch(1)
|
|
28
32
|
response = storage_request do |token|
|
|
29
33
|
@transport.copy_storage_object(
|
|
30
34
|
authorization: token,
|
|
@@ -75,7 +79,8 @@ module Volcano
|
|
|
75
79
|
end
|
|
76
80
|
|
|
77
81
|
def visibility_value(value)
|
|
78
|
-
return
|
|
82
|
+
return true if value == true
|
|
83
|
+
return false if value == false
|
|
79
84
|
|
|
80
85
|
raise ArgumentError, 'public must be true or false'
|
|
81
86
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Validates object metadata and paginated storage responses.
|
|
5
|
+
module StorageObjectResponse
|
|
6
|
+
include StorageResponse
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def storage_object(payload)
|
|
11
|
+
payload = storage_payload(payload)
|
|
12
|
+
required = storage_object_required(payload)
|
|
13
|
+
optional = storage_object_optional(payload)
|
|
14
|
+
StorageObject.new(
|
|
15
|
+
id: required.fetch(0), bucket_id: required.fetch(1), name: required.fetch(2),
|
|
16
|
+
size: required.fetch(3), mime_type: required.fetch(4), is_public: required.fetch(5),
|
|
17
|
+
owner_id: optional.fetch(0), etag: optional.fetch(1), metadata: optional.fetch(2),
|
|
18
|
+
created_at: optional.fetch(3), updated_at: optional.fetch(4), public_url: optional.fetch(5)
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def storage_object_required(payload)
|
|
23
|
+
[
|
|
24
|
+
storage_string(payload.fetch('id')), storage_string(payload.fetch('bucket_id')),
|
|
25
|
+
storage_string(payload.fetch('name')), storage_integer(payload.fetch('size')),
|
|
26
|
+
storage_string(payload.fetch('mime_type')), storage_boolean(payload.fetch('is_public'))
|
|
27
|
+
]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def storage_object_optional(payload)
|
|
31
|
+
[
|
|
32
|
+
storage_optional_string(payload['owner_id']), storage_optional_string(payload['etag']),
|
|
33
|
+
storage_optional_metadata(payload['metadata']), storage_optional_time(payload['created_at']),
|
|
34
|
+
storage_optional_time(payload['updated_at']), storage_optional_string(payload['public_url'])
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def storage_object_list(value)
|
|
39
|
+
raise Error::TransportError, 'invalid storage objects' unless value.is_a?(Array)
|
|
40
|
+
|
|
41
|
+
value.map { |object| storage_object(object) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def storage_cursor(value)
|
|
45
|
+
return if value.nil? || value == ''
|
|
46
|
+
|
|
47
|
+
storage_string(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def storage_boolean(value)
|
|
51
|
+
return true if value == true
|
|
52
|
+
return false if value == false
|
|
53
|
+
|
|
54
|
+
raise Error::TransportError, 'invalid storage boolean'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def storage_optional_string(value)
|
|
58
|
+
value.nil? ? nil : storage_string(value)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def storage_optional_time(value)
|
|
62
|
+
value.nil? ? nil : storage_time(value)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def storage_optional_metadata(value)
|
|
66
|
+
value.nil? ? nil : storage_json_hash(value)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def storage_json_hash(value)
|
|
70
|
+
raise Error::TransportError, 'invalid storage JSON object' unless value.is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
value.to_h do |key, item|
|
|
73
|
+
raise Error::TransportError, 'invalid storage JSON key' unless key.is_a?(String)
|
|
74
|
+
|
|
75
|
+
[key, storage_json_value(item)]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def storage_json_value(value)
|
|
80
|
+
case value
|
|
81
|
+
when NilClass, TrueClass, FalseClass, Integer, Float, String then value
|
|
82
|
+
when Array then value.map { |item| storage_json_value(item) }
|
|
83
|
+
when Hash then storage_json_hash(value)
|
|
84
|
+
else raise Error::TransportError, 'invalid storage JSON value'
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Decodes resumable-upload responses before creating public records.
|
|
5
|
+
module StorageResponse
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def upload_session(payload)
|
|
9
|
+
payload = storage_payload(payload)
|
|
10
|
+
UploadSession.new(
|
|
11
|
+
session_id: storage_string(payload.fetch('session_id')),
|
|
12
|
+
part_size: storage_integer(payload.fetch('part_size')),
|
|
13
|
+
total_parts: storage_integer(payload.fetch('total_parts')),
|
|
14
|
+
expires_at: storage_time(payload.fetch('expires_at'))
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def upload_part_metadata(payload)
|
|
19
|
+
payload = storage_payload(payload)
|
|
20
|
+
UploadPart.new(
|
|
21
|
+
part_number: storage_integer(payload.fetch('part_number')),
|
|
22
|
+
etag: storage_string(payload.fetch('etag')),
|
|
23
|
+
size: storage_integer(payload.fetch('size'))
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def upload_session_status(payload)
|
|
28
|
+
payload = storage_payload(payload)
|
|
29
|
+
text = storage_status_text(payload)
|
|
30
|
+
counts = storage_status_counts(payload)
|
|
31
|
+
storage_status_record(payload, text, counts)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def storage_status_record(payload, text, counts)
|
|
35
|
+
UploadSessionStatus.new(
|
|
36
|
+
session_id: text.fetch(0), status: text.fetch(1), path: text.fetch(2),
|
|
37
|
+
content_type: text.fetch(3), total_size: counts.fetch(0), part_size: counts.fetch(1),
|
|
38
|
+
total_parts: counts.fetch(2), parts_uploaded: counts.fetch(3),
|
|
39
|
+
bytes_uploaded: counts.fetch(4), parts: storage_parts(payload.fetch('parts', [])),
|
|
40
|
+
expires_at: storage_time(payload.fetch('expires_at')),
|
|
41
|
+
created_at: storage_time(payload.fetch('created_at'))
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def storage_status_text(payload)
|
|
46
|
+
[
|
|
47
|
+
storage_string(payload.fetch('session_id')), storage_string(payload.fetch('status')),
|
|
48
|
+
storage_string(payload.fetch('path')), storage_string(payload.fetch('content_type'))
|
|
49
|
+
]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def storage_status_counts(payload)
|
|
53
|
+
[
|
|
54
|
+
storage_integer(payload.fetch('total_size')), storage_integer(payload.fetch('part_size')),
|
|
55
|
+
storage_integer(payload.fetch('total_parts')), storage_integer(payload.fetch('parts_uploaded')),
|
|
56
|
+
storage_integer(payload.fetch('bytes_uploaded'))
|
|
57
|
+
]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def storage_parts(value)
|
|
61
|
+
raise Error::TransportError, 'invalid upload parts' unless value.is_a?(Array)
|
|
62
|
+
|
|
63
|
+
value.map { |part| upload_part_metadata(part) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def storage_payload(value)
|
|
67
|
+
raise Error::TransportError, 'invalid storage response' unless value.is_a?(Hash)
|
|
68
|
+
raise Error::TransportError, 'invalid storage response key' unless value.keys.all?(String)
|
|
69
|
+
|
|
70
|
+
value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def storage_string(value)
|
|
74
|
+
raise Error::TransportError, 'invalid storage string' unless value.is_a?(String)
|
|
75
|
+
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def storage_integer(value)
|
|
80
|
+
raise Error::TransportError, 'invalid storage integer' unless value.is_a?(Integer)
|
|
81
|
+
|
|
82
|
+
value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def storage_time(value)
|
|
86
|
+
return value if value.is_a?(Time)
|
|
87
|
+
return Time.iso8601(value) if value.is_a?(String)
|
|
88
|
+
|
|
89
|
+
raise Error::TransportError, 'invalid storage time'
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|