cantemo-portal-agent 1.1.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/cantemo/portal/agent/cli/commands/watch_folders-working.rb +2 -2
- data/lib/cantemo/portal/agent/cli/commands/watch_folders.rb +23 -7
- data/lib/cantemo/portal/agent/version.rb +1 -1
- data/lib/envoi/mam/agent.rb +5 -2
- data/lib/envoi/mam/agent/transfer_client/aspera.rb +39 -12
- data/lib/envoi/mam/cantemo/agent.rb +106 -22
- data/lib/envoi/mam/cantemo/agent/watch_folder_handler-working.rb +5 -2
- data/lib/envoi/mam/cantemo/agent/watch_folder_manager.rb +426 -0
- data/lib/envoi/mam/vidispine/agent.rb +1 -1
- data/lib/envoi/watch_folder_utility/watch_folder/handler/listen.rb +152 -23
- metadata +3 -3
- data/lib/envoi/mam/cantemo/agent/watch_folder_handler.rb +0 -176
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1a098feb7ef559fc883d25d942ee7f501599684
|
4
|
+
data.tar.gz: 2b90e43a0ccdd4ad7e2283b8ddb09495a15998e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 853bd438e02b10f756fb8524671c2bfa3884bf6d8fe4cfde3b189117d8681deb5f230b290ca28df524b1ce7e9ede16d07ce13044a38ac97d1ac611beea1c57f6
|
7
|
+
data.tar.gz: '078cc907795490344fa612380e21fc4f240ab6edbbf4118d7d2063ea438796564e908352c525106e865cd215d3d011e5efc7763b78d990793fb503ad2269a855'
|
@@ -226,10 +226,10 @@ class Watcher
|
|
226
226
|
w.run
|
227
227
|
end
|
228
228
|
|
229
|
-
def self.run_as_daemon(args)
|
229
|
+
def self.run_as_daemon(args, options = { })
|
230
230
|
# ARGV.unshift 'run' unless %w(start stop restart run zap killall status).include? ARGV.first
|
231
231
|
require 'daemons'
|
232
|
-
Daemons.run_proc('cantemo-portal-agent-watch-folders') { self.run(args) }
|
232
|
+
Daemons.run_proc('cantemo-portal-agent-watch-folders', options) { self.run(args) }
|
233
233
|
end
|
234
234
|
|
235
235
|
end
|
@@ -10,7 +10,7 @@ require 'pp'
|
|
10
10
|
require 'envoi/mam/agent/cli'
|
11
11
|
require 'envoi/mam/cantemo/agent'
|
12
12
|
# require 'envoi/aspera/watch_service/watch_folder'
|
13
|
-
require 'envoi/mam/cantemo/agent/
|
13
|
+
require 'envoi/mam/cantemo/agent/watch_folder_manager'
|
14
14
|
|
15
15
|
Envoi::Mam::Agent::CLI::CONFIG_FILE_PATHS.clear
|
16
16
|
Envoi::Mam::Agent::CLI::CONFIG_FILE_PATHS.concat [
|
@@ -32,7 +32,7 @@ ARGV << 'run' if ARGV.empty?
|
|
32
32
|
:config_file_path => default_config_file_paths,
|
33
33
|
:dry_run => false,
|
34
34
|
:operation => :upload,
|
35
|
-
:preserve_path =>
|
35
|
+
:preserve_path => false,
|
36
36
|
:transfer_type => '',
|
37
37
|
}
|
38
38
|
def args; @args end
|
@@ -49,15 +49,31 @@ op.parse!
|
|
49
49
|
config_file_path = args[:config_file_path]
|
50
50
|
args[:config_file_path].map! { |v| File.expand_path(v) } if config_file_path.is_a?(Array)
|
51
51
|
|
52
|
-
|
52
|
+
command = ARGV.first.dup
|
53
53
|
|
54
|
-
|
55
|
-
|
54
|
+
control_command_present = command && begin
|
55
|
+
command.downcase!
|
56
|
+
%w(start stop restart run zap killall status).include?(command)
|
56
57
|
end
|
57
58
|
|
59
|
+
if command
|
60
|
+
case command
|
61
|
+
when 'install'
|
62
|
+
puts
|
63
|
+
when 'uninstall'
|
64
|
+
puts
|
65
|
+
else
|
66
|
+
puts
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
daemonize = (control_command_present && %(start restart).include?(command)) || args[:daemonize]
|
72
|
+
|
58
73
|
# puts "#{__FILE__}:#{__LINE__ } #{args}"
|
59
74
|
# next_command = ARGV.shift
|
60
75
|
# puts "COMMAND: #{next_command}"
|
61
76
|
|
62
|
-
class Watcher < Envoi::Mam::Cantemo::Agent::
|
63
|
-
daemonize ? Watcher.run_as_daemon(args) : Watcher.run(args)
|
77
|
+
class Watcher < Envoi::Mam::Cantemo::Agent::WatchFolderManager; end
|
78
|
+
# daemonize ? Watcher.run_as_daemon(args, { force: true }) : Watcher.run(args)
|
79
|
+
daemonize ? Watcher.run_as_daemon(args) : Watcher.run(args)
|
data/lib/envoi/mam/agent.rb
CHANGED
@@ -48,10 +48,13 @@ module Envoi
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
+
# @param [String] command
|
52
|
+
# @param [Boolean] dry_run
|
53
|
+
# @return [Hash]
|
51
54
|
def shell_execute(command, dry_run = @dry_run)
|
52
55
|
if dry_run
|
53
56
|
logger.debug { "Skipping Execution of Command: '#{command}' " }
|
54
|
-
return
|
57
|
+
return { }
|
55
58
|
end
|
56
59
|
logger.debug { "Executing Command: '#{command}'" }
|
57
60
|
|
@@ -74,7 +77,7 @@ module Envoi
|
|
74
77
|
success = thread.value == 0 ? true : false
|
75
78
|
end
|
76
79
|
|
77
|
-
success
|
80
|
+
{ success: success }
|
78
81
|
end
|
79
82
|
|
80
83
|
def self.load_from_config_file(args)
|
@@ -105,6 +105,14 @@ module Envoi
|
|
105
105
|
end
|
106
106
|
|
107
107
|
aspera_token = config['aspera_transfer_token'] || config['aspera_token'] || config['token'] || config['transfer_token']
|
108
|
+
if aspera_username && aspera_password && (aspera_token.nil? || aspera_token.empty?)
|
109
|
+
_token_string = %(Basic #{["#{aspera_username}:#{aspera_password}"]
|
110
|
+
.pack('m')
|
111
|
+
.delete("\r\n")})
|
112
|
+
aspera_token = _token_string
|
113
|
+
aspera_password = nil
|
114
|
+
aspera_username = 'xfer'
|
115
|
+
end
|
108
116
|
|
109
117
|
# @ascp_path = config['ascp_path'] || default_ascp_path
|
110
118
|
# @ascp_path = File.expand_path(@ascp_path)
|
@@ -112,11 +120,12 @@ module Envoi
|
|
112
120
|
|
113
121
|
env_vars = { }
|
114
122
|
env_vars['ASPERA_SCP_PASS'] = aspera_password if aspera_password
|
123
|
+
# env_vars['ASPERA_SCP_TOKEN'] = aspera_token if aspera_token
|
115
124
|
|
116
125
|
ascp_args = config['ascp_args'] || default_ascp_args || agent.default_ascp_args
|
117
126
|
|
118
|
-
tags = config['tags']
|
119
|
-
aspera_tags = tags['aspera']
|
127
|
+
tags = config['tags'] ||= { }
|
128
|
+
aspera_tags = tags['aspera'] ||= { }
|
120
129
|
aspera_tags['xfer_id'] ||= SecureRandom.uuid
|
121
130
|
|
122
131
|
cmdline_args = [
|
@@ -127,7 +136,9 @@ module Envoi
|
|
127
136
|
]
|
128
137
|
cmdline_args.concat [ '-P', aspera_ssh_port ] if aspera_ssh_port
|
129
138
|
cmdline_args.concat ascp_args.is_a?(Array) ? ascp_args : ascp_args.split(' ') if ascp_args && !ascp_args.empty?
|
130
|
-
|
139
|
+
if aspera_token && !aspera_token.empty?
|
140
|
+
cmdline_args.concat ['-W', aspera_token, '-i', 'asperaweb_id_dsa.openssh' ]
|
141
|
+
end
|
131
142
|
cmdline_args.concat [ source_path.gsub('"', '\"'), target_path.gsub('"', '\"') ]
|
132
143
|
|
133
144
|
{ env: env_vars, args: cmdline_args, ascp_version: :ascp }
|
@@ -150,7 +161,12 @@ module Envoi
|
|
150
161
|
end
|
151
162
|
|
152
163
|
aspera_token = config['aspera_transfer_token'] || config['aspera_token'] || config['token'] || config['transfer_token']
|
153
|
-
|
164
|
+
if aspera_username && aspera_password && (aspera_token.nil? || aspera_token.empty?)
|
165
|
+
_token_string = %(Basic #{["#{aspera_username}:#{aspera_password}"]
|
166
|
+
.pack('m')
|
167
|
+
.delete("\r\n")})
|
168
|
+
aspera_username = 'xfer'
|
169
|
+
end
|
154
170
|
@ascp_path = config['ascp_path'] || default_ascp_path
|
155
171
|
@ascp_path = File.expand_path(@ascp_path)
|
156
172
|
|
@@ -169,14 +185,16 @@ module Envoi
|
|
169
185
|
command << %("#{aspera_ascp_path}" --mode=#{mode} --host="#{aspera_host_address}" --user="#{aspera_username}")
|
170
186
|
command << %( -P #{aspera_ssh_port}) if aspera_ssh_port
|
171
187
|
command << %(--tags64 #{Base64.strict_encode64(JSON.generate(tags))}) if tags && !tags.empty?
|
172
|
-
|
188
|
+
if ascp_args && !ascp_args.empty?
|
189
|
+
command << (ascp_args.is_a?(Array)) ? ascp_args.join(' ') : ascp_args
|
190
|
+
end
|
173
191
|
command << %( -W "#{aspera_token}") if aspera_token && !aspera_token.empty?
|
174
192
|
command << %( "#{source_path.gsub('"', '\"')}" "#{target_path.gsub('"', '\"')}")
|
175
193
|
end
|
176
194
|
|
177
195
|
def download(config, path, destination_path = DEFAULT_DESTINATION_PATH)
|
178
196
|
aspera_base_path = config['base_path'] || ''
|
179
|
-
source_path = File.join(aspera_base_path, path)
|
197
|
+
source_path = aspera_base_path.empty? ? path : File.join(aspera_base_path, path)
|
180
198
|
|
181
199
|
mode = 'recv'
|
182
200
|
transfer(config, mode, source_path, destination_path)
|
@@ -192,7 +210,7 @@ module Envoi
|
|
192
210
|
end
|
193
211
|
|
194
212
|
def transfer(config, mode, source_path, destination_path)
|
195
|
-
if
|
213
|
+
if true
|
196
214
|
transfer_using_asperala(config, mode, source_path, destination_path)
|
197
215
|
else
|
198
216
|
transfer_using_shell_execute(config, mode, source_path, destination_path)
|
@@ -201,21 +219,30 @@ module Envoi
|
|
201
219
|
|
202
220
|
def transfer_using_asperala(config, mode, source_path, destination_path)
|
203
221
|
args_out = build_asperala_transfer_args(config, mode, source_path, destination_path)
|
204
|
-
fasp
|
205
|
-
Asperalm::Log.instance.level = :debug
|
206
|
-
fasp.start_transfer_with_args_env(args_out, {})
|
222
|
+
@fasp ||= Asperalm::Fasp::Local.instance
|
223
|
+
Asperalm::Log.instance.level = :debug #logger.level
|
224
|
+
@fasp.start_transfer_with_args_env(args_out, {})
|
225
|
+
{ success: true }
|
207
226
|
end
|
208
227
|
|
209
228
|
def transfer_using_shell_execute(config, mode, source_path, destination_path)
|
210
229
|
command = build_ascp_command(config, mode, source_path, destination_path)
|
211
230
|
|
212
231
|
unless ascp_path_exists?
|
213
|
-
|
214
|
-
|
232
|
+
msg = "ASCP not found. '#{ascp_path}'"
|
233
|
+
warn msg
|
234
|
+
return { message: msg, success: false }
|
215
235
|
end
|
236
|
+
|
216
237
|
agent.shell_execute(command)
|
217
238
|
end
|
218
239
|
|
240
|
+
def shutdown(graceful = true)
|
241
|
+
if @fasp && @fasp.respond_to?(:shutdown)
|
242
|
+
@fasp.shutdown(graceful)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
219
246
|
# AsperaTransferClient
|
220
247
|
end
|
221
248
|
|
@@ -13,15 +13,18 @@ module Envoi
|
|
13
13
|
|
14
14
|
DEFAULT_SHAPE_TAG = 'original'
|
15
15
|
DEFAULT_DESTINATION_PATH = '.'
|
16
|
+
DEFAULT_PRESERVE_FILE_PATH = true
|
16
17
|
|
17
18
|
attr_accessor :default_aspera_ascp_args,
|
18
19
|
:default_aspera_ascp_path,
|
19
|
-
:default_vidispine_shape_tag
|
20
|
+
:default_vidispine_shape_tag,
|
21
|
+
:default_preserve_file_path
|
20
22
|
|
21
23
|
def after_initialize
|
22
24
|
args = initial_args
|
23
|
-
@default_aspera_ascp_path
|
24
|
-
@default_aspera_args = args
|
25
|
+
@default_aspera_ascp_path = args[:default_aspera_ascp_path]
|
26
|
+
@default_aspera_args = args.fetch(:default_ascp_args, Envoi::Mam::Agent::TransferClient::Aspera::DEFAULT_ASCP_ARGS)
|
27
|
+
@default_preserve_file_path = args.fetch(:default_preserve_file_path, DEFAULT_PRESERVE_FILE_PATH)
|
25
28
|
end
|
26
29
|
|
27
30
|
def dry_run?; @dry_run end
|
@@ -35,8 +38,8 @@ module Envoi
|
|
35
38
|
@api_client = args[:api_client] || begin
|
36
39
|
|
37
40
|
api_host = api_config['host']
|
38
|
-
api_host_use_ssl = api_config['use_ssl']
|
39
41
|
api_port = api_config['port']
|
42
|
+
api_host_use_ssl = api_config['ssl']
|
40
43
|
api_username = api_config['username']
|
41
44
|
api_password = api_config['password']
|
42
45
|
api_auth_token = api_config['api_auth_token']
|
@@ -54,13 +57,17 @@ module Envoi
|
|
54
57
|
api_username ||= _api_username
|
55
58
|
api_password ||= _api_password
|
56
59
|
end
|
60
|
+
api_host_use_ssl = api_uri.scheme == 'https' if api_host_use_ssl.nil?
|
57
61
|
api_uri_query = api_uri.query
|
58
62
|
api_default_query_data ||= Hash[api_uri_query.split('&').map { |kp| kp.split('=') }]
|
59
63
|
api_base_path ||= api_uri.path
|
60
64
|
end
|
61
65
|
|
62
66
|
api_port ||= (api_host_use_ssl ? 443 : 80)
|
63
|
-
api_base_path ||= '
|
67
|
+
api_base_path ||= '/'
|
68
|
+
|
69
|
+
api_endpoint_prefix = 'VSAPI'
|
70
|
+
api_noauth_endpoint_prefix = 'APInoauth'
|
64
71
|
|
65
72
|
client_args = { }
|
66
73
|
client_args[:http_host_address] = api_host if api_host
|
@@ -70,6 +77,8 @@ module Envoi
|
|
70
77
|
client_args[:password] = api_password if api_password
|
71
78
|
client_args[:default_base_path] = api_base_path if api_base_path
|
72
79
|
client_args[:default_query_data] = api_default_query_data if api_default_query_data
|
80
|
+
client_args[:api_endpoint_prefix] = api_endpoint_prefix
|
81
|
+
client_args[:api_noauth_endpoint_prefix] = api_noauth_endpoint_prefix
|
73
82
|
|
74
83
|
if api_auth_token
|
75
84
|
# Cantemo Portal supports an auth token for authentication, replace basic auth with auth-token
|
@@ -78,10 +87,16 @@ module Envoi
|
|
78
87
|
end
|
79
88
|
|
80
89
|
_client = ::Vidispine::API::Utilities.new(client_args)
|
81
|
-
_client
|
82
|
-
end
|
83
90
|
|
91
|
+
begin
|
92
|
+
_client.version
|
93
|
+
rescue => e
|
94
|
+
e.message = "Error connecting to Portal: #{e.message}"
|
95
|
+
raise e
|
96
|
+
end
|
84
97
|
|
98
|
+
_client
|
99
|
+
end
|
85
100
|
|
86
101
|
@default_vidispine_shape_tag = args[:default_shape_tag] || api_config['default_shape_tag'] || api_config['shape_tag'] || DEFAULT_SHAPE_TAG
|
87
102
|
|
@@ -133,7 +148,7 @@ module Envoi
|
|
133
148
|
end
|
134
149
|
|
135
150
|
logger.info { "Transferring File Path: '#{file_path}'" }
|
136
|
-
preserve_path = args.fetch(:preserve_path, file_storage_config.fetch('preserve_path',
|
151
|
+
preserve_path = args.fetch(:preserve_path, file_storage_config.fetch('preserve_path', default_preserve_file_path))
|
137
152
|
|
138
153
|
destination_path = args[:destination_path] || file_storage_config['destination_path'] || DEFAULT_DESTINATION_PATH
|
139
154
|
relative_path = preserve_path ? File.dirname(file_path) : nil
|
@@ -161,6 +176,8 @@ module Envoi
|
|
161
176
|
end
|
162
177
|
|
163
178
|
def upload(args = { })
|
179
|
+
_response = { }
|
180
|
+
|
164
181
|
file_path = args[:file_path]
|
165
182
|
raise ArgumentError, "Path not found: '#{file_path}'" unless File.exists?(file_path)
|
166
183
|
|
@@ -183,7 +200,7 @@ module Envoi
|
|
183
200
|
|
184
201
|
should_import_file = args.fetch(:import_file, vidispine_storage_config.fetch('import', true))
|
185
202
|
|
186
|
-
should_preserve_path = args.fetch(:preserve_path, vidispine_storage_config.fetch('preserve_path',
|
203
|
+
should_preserve_path = args.fetch(:preserve_path, vidispine_storage_config.fetch('preserve_path', default_preserve_file_path))
|
187
204
|
|
188
205
|
destination_path = args[:destination_path] || vidispine_storage_config['destination_path'] || '/'
|
189
206
|
relative_path = should_preserve_path ? File.dirname(file_path) : nil
|
@@ -194,7 +211,7 @@ module Envoi
|
|
194
211
|
|
195
212
|
|
196
213
|
# upload file
|
197
|
-
|
214
|
+
|
198
215
|
transfer_response = begin
|
199
216
|
response = nil
|
200
217
|
aspera_config = vidispine_storage_config['aspera']
|
@@ -204,7 +221,7 @@ module Envoi
|
|
204
221
|
response = client.upload(aspera_config, file_path, target_path)
|
205
222
|
end
|
206
223
|
rescue => e
|
207
|
-
logger.error { "Aspera Transfer Failed. '#{e.message}'" }
|
224
|
+
logger.error { "Aspera Transfer Failed. '#{e.message}'\n#{e.backtrace.first}" }
|
208
225
|
end
|
209
226
|
|
210
227
|
s3_config = vidispine_storage_config['s3']
|
@@ -222,28 +239,95 @@ module Envoi
|
|
222
239
|
|
223
240
|
response
|
224
241
|
end
|
225
|
-
|
226
|
-
|
242
|
+
transfer_response = { success: transfer_response } if transfer_response == true || transfer_response == false
|
243
|
+
|
244
|
+
logger.debug { "Transfer Response: #{transfer_response}" }
|
245
|
+
_response[:transfer_response] = transfer_response
|
227
246
|
|
228
|
-
|
247
|
+
if transfer_response.nil?
|
248
|
+
logger.warn { "No supported TransferClient configuration#{transfer_type && !transfer_type.empty? ? " for transfer type '#{transfer_type}' " : ''}found in storage configuration." }
|
249
|
+
_response[:success] = false
|
250
|
+
return _response
|
251
|
+
end
|
252
|
+
|
253
|
+
unless transfer_response[:success]
|
254
|
+
logger.error { "Error transferring file." }
|
255
|
+
_response[:success] = false
|
256
|
+
return _response
|
257
|
+
end
|
229
258
|
|
230
|
-
|
259
|
+
unless should_import_file
|
260
|
+
_response[:success] = transfer_response[:success]
|
261
|
+
return _response
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
### IMPORT - START
|
266
|
+
import_file_args = args.dup
|
267
|
+
import_file_args[:response_object] = _response
|
268
|
+
import_file_args[:target_path] = target_path
|
269
|
+
import_file(import_file_args)
|
270
|
+
_response[:success] = true unless _response[:success] === false
|
271
|
+
### IMPORT - END
|
272
|
+
|
273
|
+
_response
|
274
|
+
rescue => e
|
275
|
+
logger.error { "Exception: #{e.message}" }
|
276
|
+
_response[:exception] = e
|
277
|
+
return _response
|
278
|
+
end
|
279
|
+
|
280
|
+
def import_file(args = { })
|
281
|
+
_response = args[:response_object] || { }
|
282
|
+
|
283
|
+
file_path = args[:file_path]
|
284
|
+
storage_id = args[:storage_id]
|
285
|
+
|
286
|
+
target_path = args[:target_path]
|
231
287
|
|
232
288
|
item_id = args[:item_id]
|
233
289
|
shape_tag = args[:shape_tag] || default_vidispine_shape_tag
|
234
290
|
|
235
291
|
# attach file to item as shape
|
236
292
|
path_on_storage = File.join(target_path, File.basename(file_path))
|
237
|
-
|
238
|
-
|
293
|
+
path_on_storage = path_on_storage[1..-1] if path_on_storage.start_with?('/')
|
294
|
+
# file_create_response = api_client.storage_file_create(storage_id: storage_id,
|
295
|
+
# path: path_on_storage, state: 'CLOSED')
|
296
|
+
file_create_response = api_client.storage_file_get_or_create(storage_id, path_on_storage, { :extended_response => true })
|
297
|
+
_response[:file_create_response] = file_create_response
|
298
|
+
file = file_create_response[:file]
|
299
|
+
|
300
|
+
file_id = file['id']
|
301
|
+
|
302
|
+
unless file_id
|
303
|
+
_file = file.dup
|
304
|
+
_file.keep_if { |k,v| v }
|
305
|
+
logger.error { "Failed to create file. #{_file}" }
|
306
|
+
_response[:success] = false
|
307
|
+
return _response
|
308
|
+
end
|
309
|
+
|
310
|
+
item = (file['item'] || []).first
|
311
|
+
|
312
|
+
if item
|
313
|
+
shape = (item['shape'] || []).first
|
314
|
+
msg = "File already exist and is associated to item #{item['id']} as shape #{shape['id']}."
|
315
|
+
logger.warn { "#{msg} #{file}" }
|
316
|
+
_response[:error] = { :message => msg }
|
317
|
+
_response[:success] = false
|
318
|
+
return _response
|
319
|
+
end
|
239
320
|
|
240
321
|
if item_id
|
241
|
-
item_shape_import_response = api_client.item_shape_import
|
322
|
+
item_shape_import_response = api_client.item_shape_import(item_id: item_id,
|
323
|
+
tag: shape_tag, fileId: file_id)
|
324
|
+
|
325
|
+
|
242
326
|
else
|
243
|
-
item_shape_import_response = api_client.item_add_using_file_path
|
244
|
-
|
245
|
-
|
246
|
-
|
327
|
+
item_shape_import_response = api_client.item_add_using_file_path(storage_id: storage_id,
|
328
|
+
file_path: path_on_storage,
|
329
|
+
fileId: file_id,
|
330
|
+
storage_path_map: { '/' => storage_id })
|
247
331
|
end
|
248
332
|
_response[:import_response] = item_shape_import_response
|
249
333
|
|