morpheus-cli 2.12.5 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +5 -0
  3. data/lib/morpheus/api/api_client.rb +15 -30
  4. data/lib/morpheus/api/app_templates_interface.rb +34 -7
  5. data/lib/morpheus/api/apps_interface.rb +20 -1
  6. data/lib/morpheus/api/archive_buckets_interface.rb +124 -0
  7. data/lib/morpheus/api/archive_files_interface.rb +182 -0
  8. data/lib/morpheus/api/{network_pools_interface.rb → image_builder_boot_scripts_interface.rb} +6 -6
  9. data/lib/morpheus/api/{policies_interface.rb → image_builder_image_builds_interface.rb} +20 -15
  10. data/lib/morpheus/api/image_builder_interface.rb +26 -0
  11. data/lib/morpheus/api/{network_proxies_interface.rb → image_builder_preseed_scripts_interface.rb} +6 -6
  12. data/lib/morpheus/cli.rb +10 -9
  13. data/lib/morpheus/cli/alias_command.rb +10 -9
  14. data/lib/morpheus/cli/app_templates.rb +1566 -457
  15. data/lib/morpheus/cli/apps.rb +284 -108
  16. data/lib/morpheus/cli/archives_command.rb +2184 -0
  17. data/lib/morpheus/cli/boot_scripts_command.rb +382 -0
  18. data/lib/morpheus/cli/cli_command.rb +9 -35
  19. data/lib/morpheus/cli/error_handler.rb +2 -0
  20. data/lib/morpheus/cli/hosts.rb +15 -3
  21. data/lib/morpheus/cli/image_builder_command.rb +1208 -0
  22. data/lib/morpheus/cli/instances.rb +118 -47
  23. data/lib/morpheus/cli/man_command.rb +27 -24
  24. data/lib/morpheus/cli/mixins/print_helper.rb +19 -5
  25. data/lib/morpheus/cli/mixins/provisioning_helper.rb +20 -20
  26. data/lib/morpheus/cli/option_types.rb +45 -14
  27. data/lib/morpheus/cli/preseed_scripts_command.rb +381 -0
  28. data/lib/morpheus/cli/remote.rb +1 -0
  29. data/lib/morpheus/cli/roles.rb +2 -2
  30. data/lib/morpheus/cli/shell.rb +3 -2
  31. data/lib/morpheus/cli/version.rb +1 -1
  32. data/lib/morpheus/ext/hash.rb +22 -0
  33. data/lib/morpheus/formatters.rb +33 -0
  34. data/lib/morpheus/terminal.rb +1 -1
  35. metadata +13 -21
  36. data/lib/morpheus/api/cloud_policies_interface.rb +0 -47
  37. data/lib/morpheus/api/group_policies_interface.rb +0 -47
  38. data/lib/morpheus/api/network_domains_interface.rb +0 -47
  39. data/lib/morpheus/api/network_groups_interface.rb +0 -47
  40. data/lib/morpheus/api/network_pool_servers_interface.rb +0 -47
  41. data/lib/morpheus/api/network_services_interface.rb +0 -47
  42. data/lib/morpheus/api/networks_interface.rb +0 -54
  43. data/lib/morpheus/cli/network_domains_command.rb +0 -571
  44. data/lib/morpheus/cli/network_groups_command.rb +0 -602
  45. data/lib/morpheus/cli/network_pool_servers_command.rb +0 -430
  46. data/lib/morpheus/cli/network_pools_command.rb +0 -495
  47. data/lib/morpheus/cli/network_proxies_command.rb +0 -594
  48. data/lib/morpheus/cli/network_services_command.rb +0 -148
  49. data/lib/morpheus/cli/networks_command.rb +0 -855
  50. data/lib/morpheus/cli/policies_command.rb +0 -847
  51. data/scripts/generate_morpheus_commands_help.morpheus +0 -1313
@@ -0,0 +1,2184 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'yaml'
4
+ require 'rest_client'
5
+ require 'optparse'
6
+ require 'filesize'
7
+ require 'table_print'
8
+ require 'morpheus/cli/cli_command'
9
+
10
+ class Morpheus::Cli::ArchivesCommand
11
+ include Morpheus::Cli::CliCommand
12
+ include Morpheus::Cli::ProvisioningHelper
13
+
14
+ set_command_name :archives
15
+
16
+ # bucket commands
17
+ # register_subcommands :list_buckets, :get_bucket, :add_bucket, :update_bucket, :remove_bucket
18
+ register_subcommands :'list' => :list_buckets
19
+ register_subcommands :'get' => :get_bucket
20
+ register_subcommands :'add' => :add_bucket
21
+ register_subcommands :'update' => :update_bucket
22
+ register_subcommands :'remove' => :remove_bucket
23
+ register_subcommands :'download-bucket' => :download_bucket_zip
24
+ # file commands
25
+ register_subcommands :'list-files' => :list_files
26
+ register_subcommands :'ls' => :ls
27
+ register_subcommands :'file' => :get_file
28
+ register_subcommands :'file-history' => :file_history
29
+ register_subcommands :'file-links' => :file_links
30
+ # register_subcommands :'history' => :file_history
31
+ register_subcommands :'upload' => :upload_file
32
+ register_subcommands :'download' => :download_file
33
+ register_subcommands :'read' => :read_file
34
+ register_subcommands :'remove-file' => :remove_file
35
+ register_subcommands :'rm' => :remove_file
36
+
37
+ # file link commands
38
+ register_subcommands :'add-file-link' => :add_file_link
39
+ # register_subcommands :'get-file-link' => :get_file_link
40
+ register_subcommands :'remove-file-link' => :remove_file_link
41
+ register_subcommands :'download-link' => :download_file_link
42
+
43
+
44
+ # set_default_subcommand :list
45
+
46
+ def initialize()
47
+ # @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
48
+ end
49
+
50
+ def connect(opts)
51
+ @api_client = establish_remote_appliance_connection(opts)
52
+ @archive_buckets_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).archive_buckets
53
+ @archive_files_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).archive_files
54
+ @options_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).options
55
+ # @active_group_id = Morpheus::Cli::Groups.active_groups[@appliance_name]
56
+ end
57
+
58
+ def handle(args)
59
+ handle_subcommand(args)
60
+ end
61
+
62
+ def list_buckets(args)
63
+ options = {}
64
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
65
+ opts.banner = subcommand_usage()
66
+ build_common_options(opts, options, [:list, :json, :dry_run])
67
+ opts.footer = "List archive buckets."
68
+ end
69
+ optparse.parse!(args)
70
+ if args.count != 0
71
+ raise_command_error "#{command_name} list expects 0 arguments\n#{optparse}"
72
+ end
73
+ connect(options)
74
+ begin
75
+ params = {}
76
+ [:phrase, :offset, :max, :sort, :direction].each do |k|
77
+ params[k] = options[k] unless options[k].nil?
78
+ end
79
+
80
+ if options[:dry_run]
81
+ print_dry_run @archive_buckets_interface.dry.list(params)
82
+ return
83
+ end
84
+
85
+ json_response = @archive_buckets_interface.list(params)
86
+ if options[:json]
87
+ print JSON.pretty_generate(json_response)
88
+ print "\n"
89
+ return
90
+ end
91
+ archive_buckets = json_response['archiveBuckets']
92
+ title = "Morpheus Archive Buckets"
93
+ subtitles = []
94
+ # if group
95
+ # subtitles << "Group: #{group['name']}".strip
96
+ # end
97
+ # if cloud
98
+ # subtitles << "Cloud: #{cloud['name']}".strip
99
+ # end
100
+ if params[:phrase]
101
+ subtitles << "Search: #{params[:phrase]}".strip
102
+ end
103
+ print_h1 title, subtitles
104
+ if archive_buckets.empty?
105
+ print cyan,"No archive buckets found.",reset,"\n"
106
+ else
107
+ rows = archive_buckets.collect {|archive_bucket|
108
+ row = {
109
+ id: archive_bucket['id'],
110
+ name: archive_bucket['name'],
111
+ description: archive_bucket['description'],
112
+ storageProvider: archive_bucket['storageProvider'] ? archive_bucket['storageProvider']['name'] : 'N/A',
113
+ fileCount: archive_bucket['fileCount'],
114
+ # createdBy: archive_bucket['createdBy'] ? archive_bucket['createdBy']['username'] : '',
115
+ size: format_bytes(archive_bucket['rawSize']),
116
+ owner: archive_bucket['owner'] ? archive_bucket['owner']['name'] : '',
117
+ tenants: archive_bucket['accounts'] ? archive_bucket['accounts'].collect {|it| it['name'] }.join(', ') : '',
118
+ visibility: archive_bucket['visibility'] ? archive_bucket['visibility'].capitalize() : '',
119
+ isPublic: archive_bucket['isPublic'] ? 'Yes' : 'No'
120
+ }
121
+ row
122
+ }
123
+ columns = [
124
+ :id,
125
+ :name,
126
+ {:storageProvider => {label: 'Storage'.upcase}},
127
+ {:fileCount => {label: '# Files'.upcase}},
128
+ :size,
129
+ :owner,
130
+ :tenants,
131
+ :visibility,
132
+ {:isPublic => {label: 'Public URL'.upcase}}
133
+ ]
134
+ term_width = current_terminal_width()
135
+ # if term_width > 170
136
+ # columns += [:cpu, :memory, :storage]
137
+ # end
138
+ # custom pretty table columns ...
139
+ if options[:include_fields]
140
+ columns = options[:include_fields]
141
+ end
142
+ print cyan
143
+ print as_pretty_table(rows, columns, options)
144
+ print reset
145
+ print_results_pagination(json_response, {:label => "bucket", :n_label => "buckets"})
146
+ print reset,"\n"
147
+ end
148
+ return 0
149
+ rescue RestClient::Exception => e
150
+ print_rest_exception(e, options)
151
+ exit 1
152
+ end
153
+ end
154
+
155
+ def get_bucket(args)
156
+ options = {}
157
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
158
+ opts.banner = subcommand_usage("[bucket:/path]")
159
+ build_common_options(opts, options, [:json, :dry_run])
160
+ opts.footer = "Display archive bucket details and files. " +
161
+ "\nThe [bucket] component of the argument is the name or id of an archive bucket." +
162
+ "\nThe [:/path] component is optional and can be used to display files under a sub-directory."
163
+ end
164
+ optparse.parse!(args)
165
+ if args.count != 1
166
+ print_error Morpheus::Terminal.angry_prompt
167
+ puts_error "#{command_name} get expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
168
+ return 1
169
+ end
170
+ bucket_id, search_file_path = parse_bucket_id_and_file_path(args[0])
171
+ connect(options)
172
+ begin
173
+ if options[:dry_run]
174
+ if args[0].to_s =~ /\A\d{1,}\Z/
175
+ print_dry_run @archive_buckets_interface.dry.get(bucket_id.to_i)
176
+ else
177
+ print_dry_run @archive_buckets_interface.dry.list({name:bucket_id})
178
+ end
179
+ return
180
+ end
181
+ archive_bucket = find_archive_bucket_by_name_or_id(bucket_id)
182
+ return 1 if archive_bucket.nil?
183
+ json_response = {'archiveBucket' => archive_bucket} # skip redundant request
184
+ # json_response = @archive_buckets_interface.get(archive_bucket['id'])
185
+ archive_bucket = json_response['archiveBucket']
186
+ if options[:json]
187
+ print JSON.pretty_generate(json_response)
188
+ return
189
+ end
190
+ subtitles = []
191
+ if search_file_path != "/"
192
+ subtitles << "Path: #{search_file_path}"
193
+ end
194
+ print_h1 "Archive Bucket Details", subtitles
195
+ print cyan
196
+
197
+ description_cols = {
198
+ "ID" => 'id',
199
+ "Name" => 'name',
200
+ "Description" => 'description',
201
+ # "Created By" => lambda {|it| it['createdBy'] ? it['createdBy']['username'] : '' },
202
+ "Owner" => lambda {|it| it['owner'] ? it['owner']['name'] : '' },
203
+ "Tenants" => lambda {|it| it['accounts'] ? it['accounts'].collect {|acnt| acnt['name']}.join(', ') : '' },
204
+ "Visibility" => lambda {|it| it['visibility'] ? it['visibility'].capitalize() : '' },
205
+ "Public URL" => lambda {|it| it['isPublic'] ? 'Yes' : 'No' },
206
+ "Storage" => lambda {|it| it['storageProvider'] ? it['storageProvider']['name'] : '' },
207
+ "# Files" => lambda {|it| it['fileCount'] },
208
+ "Size" => lambda {|it| format_bytes(it['rawSize']) },
209
+ "Date Created" => lambda {|it| format_local_dt(it['dateCreated']) },
210
+ "Last Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
211
+ }
212
+ print_description_list(description_cols, archive_bucket)
213
+
214
+ # show files
215
+ # search_file_path = "/"
216
+ # if args[1]
217
+ # search_file_path = args[1]
218
+ # end
219
+ # if search_file_path[0].chr != "/"
220
+ # search_file_path = "/" + search_file_path
221
+ # end
222
+ # print_h2 "Path: #{search_file_path}"
223
+ print "\n"
224
+ archive_files_json_response = @archive_buckets_interface.list_files(archive_bucket['name'], search_file_path)
225
+ archive_files = archive_files_json_response['archiveFiles']
226
+ if archive_files && archive_files.size > 0
227
+ # archive_files.each do |archive_file|
228
+ # puts " = #{archive_file['name']}"
229
+ # end
230
+ print_archive_files_table(archive_files)
231
+ else
232
+ if search_file_path.empty? || search_file_path == "/"
233
+ puts "This archive bucket has no files."
234
+ else
235
+ puts "No files found for path #{search_file_path}"
236
+ end
237
+ end
238
+ print cyan
239
+
240
+ print reset,"\n"
241
+
242
+ rescue RestClient::Exception => e
243
+ print_rest_exception(e, options)
244
+ return 1
245
+ end
246
+ end
247
+
248
+ def add_bucket(args)
249
+ options = {}
250
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
251
+ opts.banner = subcommand_usage("[options]")
252
+ opts.on('--name VALUE', String, "Name") do |val|
253
+ options['name'] = val
254
+ end
255
+ opts.on('--description VALUE', String, "Description") do |val|
256
+ options['description'] = val
257
+ end
258
+ opts.on('--storageProvider VALUE', String, "Storage Provider ID") do |val|
259
+ options['storageProvider'] = val.to_s
260
+ end
261
+ opts.on('--payload JSON', String, "JSON Payload") do |val|
262
+ options['payload'] = JSON.parse(val.to_s)
263
+ end
264
+ opts.on('--payload-file FILE', String, "JSON Payload from a local file") do |val|
265
+ payload_file = val.to_s
266
+ options['payload'] = JSON.parse(File.read(payload_file))
267
+ end
268
+
269
+ opts.on('--visibility [private|public]', String, "Visibility determines if read access is restricted to the specified Tenants (Private) or all tenants (Public).") do |val|
270
+ options['visibility'] = val.to_s
271
+ end
272
+ opts.on('--accounts LIST', String, "Tenant Accounts (comma separated ids)") do |val|
273
+ # uh don't put commas or leading/trailing spaces in script names pl
274
+ options['accounts'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
275
+ end
276
+ opts.on('--isPublic [on|off]', String, "Enabling Public URL allows files to be downloaded without any authentication.") do |val|
277
+ options['isPublic'] = (val.to_s == 'on' || val.to_s == 'true')
278
+ end
279
+ build_common_options(opts, options, [:options, :json, :dry_run, :quiet])
280
+ opts.footer = "Create a new archive bucket."
281
+ end
282
+ optparse.parse!(args)
283
+ connect(options)
284
+ begin
285
+ options.merge!(options[:options]) if options[:options] # so -O var= works..
286
+
287
+ # use the -g GROUP or active group by default
288
+ # options['group'] ||= @active_group_id
289
+
290
+ # support first arg as name instead of --name
291
+ if args[0] && !options['name']
292
+ options['name'] = args[0]
293
+ end
294
+
295
+ archive_bucket_payload = prompt_new_archive_bucket(options)
296
+ return 1 if !archive_bucket_payload
297
+ payload = {'archiveBucket' => archive_bucket_payload}
298
+
299
+ if options[:dry_run]
300
+ print_dry_run @archive_buckets_interface.dry.create(payload)
301
+ return
302
+ end
303
+ json_response = @archive_buckets_interface.create(payload)
304
+ if options[:json]
305
+ print JSON.pretty_generate(json_response)
306
+ print "\n"
307
+ elsif !options[:quiet]
308
+ new_archive_bucket = json_response['archiveBucket']
309
+ print_green_success "Added archive bucket #{new_archive_bucket['name']}"
310
+ get_bucket([new_archive_bucket['id']])
311
+ # list([])
312
+ end
313
+
314
+ rescue RestClient::Exception => e
315
+ print_rest_exception(e, options)
316
+ return 1
317
+ end
318
+ end
319
+
320
+ def update_bucket(args)
321
+ options = {}
322
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
323
+ opts.banner = subcommand_usage("[bucket] [options]")
324
+ opts.on('--name VALUE', String, "Name") do |val|
325
+ options['name'] = val
326
+ end
327
+ opts.on('--description VALUE', String, "Description") do |val|
328
+ options['description'] = val
329
+ end
330
+ # storage provider cannot be changed
331
+ # opts.on('--storageProvider VALUE', String, "Storage Provider ID") do |val|
332
+ # options['storageProvider'] = val.to_s
333
+ # end
334
+ opts.on('--payload JSON', String, "JSON Payload") do |val|
335
+ options['payload'] = JSON.parse(val.to_s)
336
+ end
337
+ opts.on('--payload-file FILE', String, "JSON Payload from a local file") do |val|
338
+ payload_file = val.to_s
339
+ options['payload'] = JSON.parse(File.read(payload_file))
340
+ end
341
+
342
+ opts.on('--visibility [private|public]', String, "Visibility determines if read access is restricted to the specified Tenants (Private) or all tenants (Public).") do |val|
343
+ options['visibility'] = val.to_s
344
+ end
345
+ opts.on('--accounts LIST', String, "Tenant Accounts (comma separated ids)") do |val|
346
+ # uh don't put commas or leading/trailing spaces in script names pl
347
+ options['accounts'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
348
+ end
349
+ opts.on('--isPublic [on|off]', String, "Enabling Public URL allows files to be downloaded without any authentication.") do |val|
350
+ options['isPublic'] = (val.to_s == 'on' || val.to_s == 'true')
351
+ end
352
+ build_common_options(opts, options, [:options, :json, :dry_run, :quiet])
353
+ opts.footer = "Update an existing archive bucket."
354
+ end
355
+ optparse.parse!(args)
356
+ if args.count < 1
357
+ puts optparse
358
+ return 1
359
+ end
360
+ connect(options)
361
+
362
+ begin
363
+ archive_bucket = find_archive_bucket_by_name_or_id(args[0])
364
+
365
+ archive_bucket_payload = prompt_edit_archive_bucket(archive_bucket, options)
366
+ return 1 if !archive_bucket_payload
367
+ payload = {'archiveBucket' => archive_bucket_payload}
368
+
369
+ if options[:dry_run]
370
+ print_dry_run @archive_buckets_interface.dry.update(archive_bucket["id"], payload)
371
+ return
372
+ end
373
+
374
+ json_response = @archive_buckets_interface.update(archive_bucket["id"], payload)
375
+ if options[:json]
376
+ print JSON.pretty_generate(json_response)
377
+ print "\n"
378
+ elsif !options[:quiet]
379
+ print_green_success "Updated archive bucket #{archive_bucket['name']}"
380
+ get([archive_bucket['id']])
381
+ end
382
+ return 0
383
+ rescue RestClient::Exception => e
384
+ print_rest_exception(e, options)
385
+ return 1
386
+ end
387
+ end
388
+
389
+ def remove_bucket(args)
390
+ full_command_string = "#{command_name} remove #{args.join(' ')}".strip
391
+ options = {}
392
+ query_params = {}
393
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
394
+ opts.banner = subcommand_usage("[bucket]")
395
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run])
396
+ end
397
+ optparse.parse!(args)
398
+
399
+ if args.count < 1
400
+ print_error Morpheus::Terminal.angry_prompt
401
+ puts_error "#{command_name} remove expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
402
+ return 1
403
+ end
404
+ bucket_id = args[0]
405
+ connect(options)
406
+ begin
407
+ # archive_bucket = find_archive_bucket_by_name_or_id(args[0])
408
+ json_response = @archive_buckets_interface.get(bucket_id, {})
409
+ archive_bucket = json_response['archiveBucket']
410
+ is_owner = json_response['isOwner']
411
+ return 1 if archive_bucket.nil?
412
+ if is_owner == false
413
+ print_red_alert "You must be the owner of archive bucket to remove it."
414
+ return 3
415
+ end
416
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the archive bucket: #{archive_bucket['name']}?")
417
+ return 9, "aborted command"
418
+ end
419
+ if options[:dry_run]
420
+ print_dry_run @archive_buckets_interface.dry.destroy(archive_bucket['id'], query_params), full_command_string
421
+ return 0
422
+ end
423
+ json_response = @archive_buckets_interface.destroy(archive_bucket['id'], query_params)
424
+ if options[:json]
425
+ print JSON.pretty_generate(json_response)
426
+ print "\n"
427
+ else
428
+ print_green_success "Removed archive bucket #{archive_bucket['name']}"
429
+ # list([])
430
+ end
431
+ return 0
432
+ rescue RestClient::Exception => e
433
+ print_rest_exception(e, options)
434
+ return 1
435
+ end
436
+ end
437
+
438
+ def upload_file(args)
439
+ options = {}
440
+ query_params = {}
441
+ do_recursive = false
442
+ ignore_regexp = nil
443
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
444
+ opts.banner = subcommand_usage("[local-file] [bucket:/path]")
445
+ # opts.on('--filename FILEPATH', String, "Remote file path for the file or folder being uploaded, this is an alternative to [remote-file-path]." ) do |val|
446
+ # options['type'] = val
447
+ # end
448
+ opts.on( '-R', '--recursive', "Upload a directory and all of its files. This must be passed if [local-file] is a directory." ) do
449
+ do_recursive = true
450
+ end
451
+ opts.on('--ignore-files PATTERN', String, "Pattern of files to be ignored when uploading a directory." ) do |val|
452
+ ignore_regexp = /#{Regexp.escape(val)}/
453
+ end
454
+ opts.footer = "Upload a local file or folder to an archive bucket. " +
455
+ "\nThe first argument [local-file] should be the path of a local file or directory." +
456
+ "\nThe second argument [bucket:/path] should contain the bucket name." +
457
+ "\nThe [:/path] component is optional and can be used to specify the destination of the uploaded file or folder." +
458
+ "\nThe default destination is the same name as the [local-file], under the root bucket directory '/'. " +
459
+ "\nThis will overwrite any existing remote files that match the destination /path."
460
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run])
461
+ end
462
+ optparse.parse!(args)
463
+
464
+ if args.count != 2
465
+ print_error Morpheus::Terminal.angry_prompt
466
+ puts_error "#{command_name} upload expects 2 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
467
+ return 1
468
+ end
469
+ # validate local file path
470
+ local_file_path = File.expand_path(args[0].squeeze('/'))
471
+ if local_file_path == "" || local_file_path == "/" || local_file_path == "."
472
+ print_error Morpheus::Terminal.angry_prompt
473
+ puts_error "#{command_name} missing argument: [local-file]\n#{optparse}"
474
+ return 1
475
+ end
476
+ if !File.exists?(local_file_path)
477
+ print_error Morpheus::Terminal.angry_prompt
478
+ puts_error "#{command_name} bad argument: [local-file]\nFile '#{local_file_path}' was not found.\n#{optparse}"
479
+ return 1
480
+ end
481
+
482
+ # validate bucket:/path
483
+ bucket_id, remote_file_path = parse_bucket_id_and_file_path(args[1])
484
+
485
+ # if local_file_path.include?('../') # || options[:yes]
486
+ # raise_command_error "Sorry, you may not use relative paths in your local filepath."
487
+ # end
488
+
489
+ # validate bucket name (or id)
490
+ if !bucket_id
491
+ print_error Morpheus::Terminal.angry_prompt
492
+ puts_error "#{command_name} missing argument: [bucket]\n#{optparse}"
493
+ return 1
494
+ end
495
+
496
+ # strip leading slash of remote name
497
+ # if remote_file_path[0].chr == "/"
498
+ # remote_file_path = remote_file_path[1..-1]
499
+ # end
500
+
501
+ if remote_file_path.include?('./') # || options[:yes]
502
+ raise_command_error "Sorry, you may not use relative paths in your remote filepath."
503
+ end
504
+
505
+ # if !options[:yes]
506
+ scary_local_paths = ["/", "/root", "C:\\"]
507
+ if scary_local_paths.include?(local_file_path)
508
+ unless Morpheus::Cli::OptionTypes.confirm("Are you sure you want to upload all the files in local directory '#{local_file_path}' !?")
509
+ return 9, "aborted command"
510
+ end
511
+ end
512
+ # end
513
+
514
+ connect(options)
515
+ begin
516
+ archive_bucket = find_archive_bucket_by_name_or_id(bucket_id)
517
+ return 1 if archive_bucket.nil?
518
+
519
+ # how many files we dealing with?
520
+ files_to_upload = []
521
+ if File.directory?(local_file_path)
522
+ # upload directory
523
+ if !do_recursive
524
+ print_error Morpheus::Terminal.angry_prompt
525
+ puts_error "bad argument: '#{local_file_path}' is a directory. Use -R or --recursive to upload a directory.\n#{optparse}"
526
+ return 1
527
+ end
528
+ found_files = Dir.glob("#{local_file_path}/**/*")
529
+ # note: api call for directories is not needed
530
+ found_files = found_files.select {|file| File.file?(file) }
531
+ if ignore_regexp
532
+ found_files = found_files.reject {|it| it =~ ignore_regexp}
533
+ end
534
+ files_to_upload = found_files
535
+
536
+ if files_to_upload.size == 0
537
+ print_error Morpheus::Terminal.angry_prompt
538
+ puts_error "bad argument: Local directory '#{local_file_path}' contains 0 files."
539
+ return 1
540
+ end
541
+
542
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to upload directory #{local_file_path} (#{files_to_upload.size} files) to #{archive_bucket['name']}:#{remote_file_path}?")
543
+ return 9, "aborted command"
544
+ end
545
+
546
+ if !options[:yes]
547
+ if files_to_upload.size > 100
548
+ unless Morpheus::Cli::OptionTypes.confirm("Are you REALLY sure you want to upload #{files_to_upload.size} files ?")
549
+ return 9, "aborted command"
550
+ end
551
+ end
552
+ end
553
+
554
+ # local_dirname = File.dirname(local_file_path)
555
+ # local_basename = File.basename(local_file_path)
556
+ upload_file_list = []
557
+ files_to_upload.each do |file|
558
+ destination = file.sub(local_file_path, (remote_file_path || "")).squeeze('/')
559
+ upload_file_list << {file: file, destination: destination}
560
+ end
561
+
562
+ if options[:dry_run]
563
+ print_h1 "DRY RUN"
564
+ print "\n",cyan, bold, "Uploading #{upload_file_list.size} Files...", reset, "\n"
565
+ upload_file_list.each do |obj|
566
+ file, destination = obj[:file], obj[:destination]
567
+ #print_dry_run @archive_buckets_interface.dry.upload_file(bucket_id, file, destination)
568
+ print cyan,bold, " - Uploading #{file} to #{bucket_id}:#{destination}", reset, "\n"
569
+ end
570
+ return 0
571
+ end
572
+
573
+ print "\n",cyan, bold, "Uploading #{upload_file_list.size} Files...", reset, "\n"
574
+ bad_upload_responses = []
575
+ upload_file_list.each do |obj|
576
+ file, destination = obj[:file], obj[:destination]
577
+ print cyan,bold, " - Uploading #{file} to #{bucket_id}:#{destination}", reset
578
+ upload_response = @archive_buckets_interface.upload_file(bucket_id, file, destination)
579
+ if upload_response['success']
580
+ print bold," #{green}SUCCESS#{reset}"
581
+ else
582
+ print bold," #{red}ERROR#{reset}"
583
+ if upload_response['msg']
584
+ bad_upload_responses << upload_response
585
+ print " #{upload_response['msg']}#{reset}"
586
+ end
587
+ end
588
+ print "\n"
589
+ end
590
+ if bad_upload_responses.size > 0
591
+ print cyan, bold, "Completed Upload of #{upload_file_list.size} Files. #{red}#{bad_upload_responses.size} Errors!", reset, "\n"
592
+ else
593
+ print cyan, bold, "Completed Upload of #{upload_file_list.size} Files!", reset, "\n"
594
+ end
595
+
596
+ else
597
+
598
+ # upload file
599
+ if !File.exists?(local_file_path) && !File.file?(local_file_path)
600
+ print_error Morpheus::Terminal.angry_prompt
601
+ puts_error "#{command_name} bad argument: [local-file]\nFile '#{local_file_path}' was not found.\n#{optparse}"
602
+ return 1
603
+ end
604
+
605
+ # local_dirname = File.dirname(local_file_path)
606
+ # local_basename = File.basename(local_file_path)
607
+
608
+ file = local_file_path
609
+ destination = File.basename(file)
610
+ if remote_file_path[-1].chr == "/"
611
+ # work like `cp`, and place into the directory
612
+ destination = remote_file_path + File.basename(file)
613
+ elsif remote_file_path
614
+ # renaming file
615
+ destination = remote_file_path
616
+ end
617
+ destination = destination.squeeze('/')
618
+
619
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to upload #{local_file_path} to #{archive_bucket['name']}:#{destination}?")
620
+ return 9, "aborted command"
621
+ end
622
+
623
+ if options[:dry_run]
624
+ print_h1 "DRY RUN"
625
+ #print_dry_run @archive_buckets_interface.dry.upload_file(bucket_id, file, destination)
626
+ print cyan,bold, " - Uploading #{file} to #{bucket_id}:#{destination} SKIPPED", reset, "\n"
627
+ return 0
628
+ end
629
+
630
+ print cyan,bold, " - Uploading #{file} to #{bucket_id}:#{destination}", reset
631
+ upload_response = @archive_buckets_interface.upload_file(bucket_id, file, destination)
632
+ if upload_response['success']
633
+ print bold," #{green}Success#{reset}"
634
+ else
635
+ print bold," #{red}Error#{reset}"
636
+ if upload_response['msg']
637
+ print " #{upload_response['msg']}#{reset}"
638
+ end
639
+ end
640
+ print "\n"
641
+
642
+ end
643
+ #print cyan, bold, "Upload Complete!", reset, "\n"
644
+
645
+ return 0
646
+ rescue RestClient::Exception => e
647
+ print_rest_exception(e, options)
648
+ return 1
649
+ end
650
+ end
651
+
652
+ def list_files(args)
653
+ options = {}
654
+ params = {}
655
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
656
+ opts.banner = subcommand_usage("[bucket:/path]")
657
+ opts.on('-a', '--all', "Show all files, including subdirectories under the /path.") do
658
+ params[:fullTree] = true
659
+ end
660
+ build_common_options(opts, options, [:list, :json, :dry_run])
661
+ opts.footer = "List files in an archive bucket. \nInclude [/path] to show files under a directory."
662
+ end
663
+ optparse.parse!(args)
664
+ if args.count != 1
665
+ print_error Morpheus::Terminal.angry_prompt
666
+ puts_error "#{command_name} list-files expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
667
+ return 1
668
+ end
669
+ bucket_id, search_file_path = parse_bucket_id_and_file_path(args[0])
670
+ connect(options)
671
+ begin
672
+ archive_bucket = find_archive_bucket_by_name_or_id(bucket_id)
673
+ return 1 if archive_bucket.nil?
674
+ [:phrase, :offset, :max, :sort, :direction, :fullTree].each do |k|
675
+ params[k] = options[k] unless options[k].nil?
676
+ end
677
+ if params[:phrase]
678
+ params[:fullTree] = true # these are not exclusively supported by api yet
679
+ end
680
+ if options[:dry_run]
681
+ print_dry_run @archive_buckets_interface.dry.list_files(bucket_id, search_file_path, params)
682
+ return
683
+ end
684
+ json_response = @archive_buckets_interface.list_files(bucket_id, search_file_path, params)
685
+ archive_files = json_response['archiveFiles']
686
+ # archive_bucket = json_response['archiveBucket']
687
+ if options[:json]
688
+ print JSON.pretty_generate(json_response)
689
+ return
690
+ end
691
+ # print_h1 "Archive Files"
692
+ # print_h1 "Archive Files", ["Bucket: [#{archive_bucket['id']}] #{archive_bucket['name']}", "Path: #{search_file_path}"]
693
+ print_h1 "Archive Files", ["#{archive_bucket['name']}:#{search_file_path}"]
694
+ print cyan
695
+ description_cols = {
696
+ "Bucket ID" => 'id',
697
+ "Bucket Name" => 'name',
698
+ #"Path" => lambda {|it| search_file_path }
699
+ }
700
+ #print_description_list(description_cols, archive_bucket)
701
+ #print "\n"
702
+ #print_h2 "Path: #{search_file_path}"
703
+ # print "Directory: #{search_file_path}"
704
+ if archive_files && archive_files.size > 0
705
+ print_archive_files_table(archive_files, {fullTree: params[:fullTree]})
706
+ print_results_pagination(json_response, {:label => "file", :n_label => "files"})
707
+ else
708
+ # puts "No files found for path #{search_file_path}"
709
+ if search_file_path.empty? || search_file_path == "/"
710
+ puts "This archive bucket has no files."
711
+ else
712
+ puts "No files found for path #{search_file_path}"
713
+ return 1
714
+ end
715
+ end
716
+ print reset,"\n"
717
+ return 0
718
+ rescue RestClient::Exception => e
719
+ print_rest_exception(e, options)
720
+ return 1
721
+ end
722
+ end
723
+
724
+ def ls(args)
725
+ options = {}
726
+ params = {}
727
+ do_one_file_per_line = false
728
+ do_long_format = false
729
+ do_human_bytes = false
730
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
731
+ opts.banner = subcommand_usage("[bucket/path]")
732
+ opts.on('-a', '--all', "Show all files, including subdirectories under the /path.") do
733
+ params[:fullTree] = true
734
+ do_one_file_per_line = true
735
+ end
736
+ opts.on('-l', '--long', "Lists files in the long format, which contains lots of useful information, e.g. the exact size of the file, the file type, and when it was last modified.") do
737
+ do_long_format = true
738
+ do_one_file_per_line
739
+ end
740
+ opts.on('-H', '--human', "Humanized file sizes. The default is just the number of bytes.") do
741
+ do_human_bytes = true
742
+ end
743
+ opts.on('-1', '--oneline', "One file per line. The default delimiter is a single space.") do
744
+ do_one_file_per_line = true
745
+ end
746
+ build_common_options(opts, options, [:list, :json, :dry_run])
747
+ opts.footer = "Print filenames for a given archive location.\nPass archive location in the format bucket/path."
748
+ end
749
+ optparse.parse!(args)
750
+ if args.count != 1
751
+ print_error Morpheus::Terminal.angry_prompt
752
+ puts_error "#{command_name} ls expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
753
+ return 1
754
+ end
755
+ bucket_id, search_file_path = parse_bucket_id_and_file_path(args[0])
756
+ connect(options)
757
+ begin
758
+ # archive_bucket = find_archive_bucket_by_name_or_id(bucket_id)
759
+ # return 1 if archive_bucket.nil?
760
+ [:phrase, :offset, :max, :sort, :direction, :fullTree].each do |k|
761
+ params[k] = options[k] unless options[k].nil?
762
+ end
763
+ if options[:dry_run]
764
+ print_dry_run @archive_buckets_interface.dry.list_files(bucket_id, search_file_path, params)
765
+ return 0
766
+ end
767
+ json_response = @archive_buckets_interface.list_files(bucket_id, search_file_path, params)
768
+ if options[:json]
769
+ puts as_json(json_response, options)
770
+ # no files is an error condition for this command
771
+ if !json_response['archiveFiles'] || json_response['archiveFiles'].size == 0
772
+ return 1
773
+ end
774
+ return 0
775
+ end
776
+ archive_bucket = json_response['archiveBucket'] # yep, this is returned too
777
+ archive_files = json_response['archiveFiles']
778
+ # archive_bucket = json_response['archiveBucket']
779
+ # print_h2 "Directory: #{search_file_path}"
780
+ # print "Directory: #{search_file_path}"
781
+ if archive_files && archive_files.size > 0
782
+ if do_long_format
783
+ # ls long format
784
+ # owner groups filesize type filename
785
+ now = Time.now
786
+ archive_files.each do |archive_file|
787
+ # -rw-r--r-- 1 jdickson staff 1361 Oct 23 08:00 voltron_2.10.log
788
+ file_color = cyan # reset
789
+ if archive_file['isDirectory']
790
+ file_color = blue
791
+ end
792
+ file_info = []
793
+ # Number of links
794
+ # file_info << file["linkCount"].to_i + 1
795
+ # Owner
796
+ owner_str = ""
797
+ if archive_file['owner']
798
+ owner_str = archive_file['owner']['name']
799
+ elsif archive_bucket['owner']
800
+ owner_str = archive_bucket['owner']['name']
801
+ else
802
+ owner_str = "noone"
803
+ end
804
+ file_info << truncate_string(owner_str, 15).ljust(15, " ")
805
+ # Group (Tenants)
806
+ groups_str = ""
807
+ if archive_file['visibility'] == 'public'
808
+ # this is confusing because of Public URL (isPublic) setting
809
+ groups_str = "public"
810
+ else
811
+ if archive_file['accounts'].instance_of?(Array) && archive_file['accounts'].size > 0
812
+ # groups_str = archive_file['accounts'].collect {|it| it['name'] }.join(',')
813
+ groups_str = (archive_file['accounts'].size == 1) ? "#{archive_file['accounts'][0]['name']}" : "#{archive_file['accounts'].size} tenants"
814
+ elsif archive_bucket['accounts'].instance_of?(Array) && archive_bucket['accounts'].size > 0
815
+ # groups_str = archive_bucket['accounts'].collect {|it| it['name'] }.join(',')
816
+ groups_str = (archive_bucket['accounts'].size == 1) ? "#{archive_bucket['accounts'][0]['name']}" : "#{archive_bucket['accounts'].size} tenants"
817
+ else
818
+ groups_str = owner_str
819
+ end
820
+ end
821
+ groups_str =
822
+ file_info << truncate_string(groups_str, 15).ljust(15, " ")
823
+ # File Type
824
+ content_type = archive_file['contentType'].to_s
825
+ if archive_file['isDirectory']
826
+ content_type = "directory"
827
+ else
828
+ content_type = archive_file['contentType'].to_s
829
+ end
830
+ file_info << content_type.ljust(25, " ")
831
+ filesize_str = ""
832
+ if do_human_bytes
833
+ # filesize_str = format_bytes(archive_file['rawSize'])
834
+ filesize_str = format_bytes_short(archive_file['rawSize'])
835
+ else
836
+ filesize_str = archive_file['rawSize'].to_i.to_s
837
+ end
838
+ # file_info << filesize_str.ljust(12, " ")
839
+ file_info << filesize_str.ljust(7, " ")
840
+ mtime = ""
841
+ last_updated = parse_time(archive_file['lastUpdated'])
842
+ if last_updated
843
+ if last_updated.year == now.year
844
+ mtime = format_local_dt(last_updated, {format: "%b %e %H:%M"})
845
+ else
846
+ mtime = format_local_dt(last_updated, {format: "%b %e %Y"})
847
+ end
848
+ end
849
+ file_info << mtime # .ljust(21, " ")
850
+ if params[:fullTree]
851
+ file_info << file_color + archive_file["filePath"].to_s + cyan
852
+ else
853
+ file_info << file_color + archive_file["name"].to_s + cyan
854
+ end
855
+ print cyan, file_info.join(" "), reset, "\n"
856
+ end
857
+ else
858
+ file_names = archive_files.collect do |archive_file|
859
+ file_color = cyan # reset
860
+ if archive_file['isDirectory']
861
+ file_color = blue
862
+ end
863
+ if params[:fullTree]
864
+ file_color + archive_file["filePath"].to_s + reset
865
+ else
866
+ file_color + archive_file["name"].to_s + reset
867
+ end
868
+ end
869
+ if do_one_file_per_line
870
+ print file_names.join("\n")
871
+ else
872
+ print file_names.join(" ")
873
+ end
874
+ print "\n"
875
+ end
876
+ else
877
+ print_error yellow, "No files found for path: #{search_file_path}", reset, "\n"
878
+ return 1
879
+ end
880
+
881
+ return 0
882
+ rescue RestClient::Exception => e
883
+ print_rest_exception(e, options)
884
+ return 1
885
+ end
886
+ end
887
+
888
+ def get_file(args)
889
+ full_command_string = "#{command_name} get-file #{args.join(' ')}".strip
890
+ options = {}
891
+ max_links = 10
892
+ max_history = 10
893
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
894
+ opts.banner = subcommand_usage("[bucket:/path]")
895
+ opts.on('-L', '--all-links', "Display all links instead of only 10." ) do
896
+ max_links = 10000
897
+ end
898
+ opts.on('-H', '--all-history', "Display all history instead of only 10." ) do
899
+ max_history = 10000
900
+ end
901
+ build_common_options(opts, options, [:json, :dry_run])
902
+ opts.footer = "Get details about an archive file.\n" +
903
+ "[bucket:/path] is required. This is the name of the bucket and /path the file or folder to be fetched." + "\n" +
904
+ "[id] can be passed instead of [bucket:/path]. This is the numeric File ID."
905
+ end
906
+ optparse.parse!(args)
907
+ # consider only allowing args.count == 1 here in the format [bucket:/path]
908
+ if args.count != 1
909
+ print_error Morpheus::Terminal.angry_prompt
910
+ puts_error "#{command_name} get-file expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
911
+ return 1
912
+ end
913
+ file_id = nil
914
+ bucket_id = nil
915
+ file_path = nil
916
+ # allow id in place of bucket:path
917
+ if args[0].to_s =~ /\A\d{1,}\Z/
918
+ file_id = args[0]
919
+ else
920
+ bucket_id, file_path = parse_bucket_id_and_file_path(args[0])
921
+ end
922
+
923
+ connect(options)
924
+ begin
925
+ # archive_bucket = find_archive_bucket_by_name_or_id(bucket_id)
926
+ # return 1 if archive_bucket.nil?
927
+ params = {}
928
+ if options[:dry_run]
929
+ if file_id
930
+ print_dry_run @archive_files_interface.dry.get(file_id, params), full_command_string
931
+ else
932
+ print_dry_run @archive_buckets_interface.dry.list_files(bucket_id, file_path, params), full_command_string
933
+ end
934
+ return 0
935
+ end
936
+ archive_file = nil
937
+ json_response = nil
938
+ if !file_id
939
+ archive_file = find_archive_file_by_bucket_and_path(bucket_id, file_path)
940
+ return 1 if archive_file.nil?
941
+ file_id = archive_file['id']
942
+ end
943
+ # archive_file = find_archive_file_by_id(file_id)
944
+ json_response = @archive_files_interface.get(file_id, params)
945
+ if options[:json]
946
+ puts as_json(json_response, options)
947
+ return 0
948
+ end
949
+ archive_file = json_response['archiveFile']
950
+ archive_logs = json_response['archiveLogs']
951
+ is_owner = json_response['isOwner']
952
+ if !bucket_id && archive_file["archiveBucket"]
953
+ bucket_id = archive_file["archiveBucket"]["name"]
954
+ end
955
+
956
+ print_h1 "Archive File Details"
957
+ print cyan
958
+ description_cols = {
959
+ "File ID" => 'id',
960
+ "Bucket" => lambda {|it| bucket_id },
961
+ "File Path" => lambda {|it| it['filePath'] },
962
+ "Type" => lambda {|it| it['isDirectory'] ? 'directory' : (it['contentType']) },
963
+ "Size" => lambda {|it| format_bytes(it['rawSize']) },
964
+ "Downloads" => lambda {|it| it['downloadCount'] },
965
+ "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
966
+ "Last Modified" => lambda {|it| format_local_dt(it['lastUpdated']) }
967
+ }
968
+ print_description_list(description_cols, archive_file)
969
+
970
+ # print "\n"
971
+
972
+
973
+ print_h2 "Download URLs"
974
+ private_download_url = "#{@appliance_url}/api/archives/download/#{URI.escape(bucket_id)}" + "/#{URI.escape(archive_file['filePath'])}".squeeze('/')
975
+ public_download_url = nil
976
+ if archive_file['archiveBucket'] && archive_file['archiveBucket']['isPublic']
977
+ public_download_url = "#{@appliance_url}/public-archives/download/#{URI.escape(bucket_id)}" + "/#{URI.escape(archive_file['filePath'])}".squeeze('/')
978
+ end
979
+ print cyan
980
+ puts "Private URL: #{private_download_url}"
981
+ if public_download_url
982
+ puts " Public URL: #{public_download_url}"
983
+ end
984
+
985
+ do_show_links = is_owner
986
+ if do_show_links
987
+ links_json_response = @archive_files_interface.list_links(archive_file['id'], {max: max_links})
988
+ archive_file_links = links_json_response['archiveFileLinks']
989
+ if archive_file_links && archive_file_links.size > 0
990
+ print_h2 "Links"
991
+ print_archive_file_links_table(archive_file_links)
992
+ print_results_pagination(links_json_response, {:label => "link", :n_label => "links"})
993
+ else
994
+ print_h2 "File Links"
995
+ puts "No links found"
996
+ end
997
+ end
998
+ # print "\n"
999
+ do_show_history = is_owner
1000
+ if do_show_history
1001
+ history_json_response = @archive_files_interface.history(archive_file['id'], {max: max_history})
1002
+ archive_logs = history_json_response['archiveLogs']
1003
+ print_h2 "History"
1004
+ if archive_logs && archive_logs.size > 0
1005
+ print_archive_logs_table(archive_logs, {exclude:[:bucket]})
1006
+ print_results_pagination(history_json_response, {:label => "history record", :n_label => "history records"})
1007
+ else
1008
+ puts "No history found"
1009
+ end
1010
+ end
1011
+ print reset,"\n"
1012
+ return 0
1013
+ rescue RestClient::Exception => e
1014
+ print_rest_exception(e, options)
1015
+ return 1
1016
+ end
1017
+ end
1018
+
1019
+ # Use upload file bucket:/path
1020
+ # def add_file(args)
1021
+ # raise "not yet implemented"
1022
+ # end
1023
+
1024
+ def remove_file(args)
1025
+ options = {}
1026
+ query_params = {}
1027
+ do_recursive = false
1028
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1029
+ opts.banner = subcommand_usage("[bucket:/path]")
1030
+ opts.on( '-R', '--recursive', "Delete a directory and all of its files. This must be passed if specifying a directory." ) do
1031
+ do_recursive = true
1032
+ end
1033
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run])
1034
+ opts.footer = "Delete an archive file or directory."
1035
+ end
1036
+ optparse.parse!(args)
1037
+ # consider only allowing args.count == 1 here in the format [bucket:/path]
1038
+ if args.count != 1
1039
+ print_error Morpheus::Terminal.angry_prompt
1040
+ puts_error "#{command_name} remove-file expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1041
+ return 1
1042
+ end
1043
+ bucket_id, file_path = parse_bucket_id_and_file_path(args[0])
1044
+ connect(options)
1045
+ begin
1046
+
1047
+ archive_file = find_archive_file_by_bucket_and_path(bucket_id, file_path)
1048
+ return 1 if archive_file.nil?
1049
+ if archive_file['isDirectory']
1050
+ if !do_recursive
1051
+ print_error Morpheus::Terminal.angry_prompt
1052
+ puts_error "bad argument: '#{file_path}' is a directory. Use -R or --recursive to delete a directory.\n#{optparse}"
1053
+ return 1
1054
+ end
1055
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the archive directory: #{args[0]}?")
1056
+ return 9, "aborted command"
1057
+ end
1058
+ else
1059
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the archive file: #{args[0]}?")
1060
+ return 9, "aborted command"
1061
+ end
1062
+ end
1063
+
1064
+ if options[:dry_run]
1065
+ print_dry_run @archive_files_interface.dry.destroy(archive_file['id'], query_params)
1066
+ return 0
1067
+ end
1068
+ json_response = @archive_files_interface.destroy(archive_file['id'], query_params)
1069
+ if options[:json]
1070
+ print JSON.pretty_generate(json_response)
1071
+ print "\n"
1072
+ else
1073
+ print_green_success "Removed archive file #{args[0]}"
1074
+ end
1075
+ return 0
1076
+
1077
+ rescue RestClient::Exception => e
1078
+ print_rest_exception(e, options)
1079
+ return 1
1080
+ end
1081
+
1082
+ end
1083
+
1084
+ def file_history(args)
1085
+ options = {}
1086
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1087
+ opts.banner = subcommand_usage("[bucket:/path]")
1088
+ build_common_options(opts, options, [:list, :json, :dry_run])
1089
+ opts.footer = "List history log events for an archive file."
1090
+ end
1091
+ optparse.parse!(args)
1092
+ if args.count != 1
1093
+ print_error Morpheus::Terminal.angry_prompt
1094
+ puts_error "#{command_name} file-history expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1095
+ return 1
1096
+ end
1097
+ bucket_id, file_path = parse_bucket_id_and_file_path(args[0])
1098
+ connect(options)
1099
+ begin
1100
+ # todo: only 1 api call needed here.
1101
+ # archive_bucket = find_archive_bucket_by_name_or_id(bucket_id)
1102
+ # return 1 if archive_bucket.nil?
1103
+ archive_file = find_archive_file_by_bucket_and_path(bucket_id, file_path)
1104
+ return 1 if archive_file.nil?
1105
+ # ok, load history
1106
+ params = {}
1107
+ [:phrase, :offset, :max, :sort, :direction].each do |k|
1108
+ params[k] = options[k] unless options[k].nil?
1109
+ end
1110
+ if options[:dry_run]
1111
+ print_dry_run @archive_files_interface.dry.history(archive_file['id'], params)
1112
+ return
1113
+ end
1114
+ json_response = @archive_files_interface.history(archive_file['id'], params)
1115
+ archive_logs = json_response['archiveLogs']
1116
+
1117
+ if options[:json]
1118
+ print JSON.pretty_generate(json_response)
1119
+ return
1120
+ end
1121
+
1122
+ print_h1 "Archive File History", ["#{bucket_id}:#{file_path}"]
1123
+ # print cyan
1124
+ # description_cols = {
1125
+ # "File ID" => 'id',
1126
+ # "Bucket" => lambda {|it| bucket_id },
1127
+ # "File Path" => lambda {|it| file_path }
1128
+ # }
1129
+ # print_description_list(description_cols, archive_file)
1130
+ # print "\n"
1131
+ # print_h2 "History"
1132
+ if archive_logs && archive_logs.size > 0
1133
+ print_archive_logs_table(archive_logs)
1134
+ print_results_pagination(json_response, {:label => "history record", :n_label => "history records"})
1135
+ else
1136
+ puts "No history found"
1137
+ end
1138
+ print reset,"\n"
1139
+ return 0
1140
+ rescue RestClient::Exception => e
1141
+ print_rest_exception(e, options)
1142
+ return 1
1143
+ end
1144
+ end
1145
+
1146
+ def file_links(args)
1147
+ options = {}
1148
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1149
+ opts.banner = subcommand_usage("[bucket:/path]")
1150
+ build_common_options(opts, options, [:list, :json, :dry_run])
1151
+ opts.footer = "List links for an archive file."
1152
+ end
1153
+ optparse.parse!(args)
1154
+ if args.count != 1
1155
+ print_error Morpheus::Terminal.angry_prompt
1156
+ puts_error "#{command_name} file-history expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1157
+ return 1
1158
+ end
1159
+ bucket_id, file_path = parse_bucket_id_and_file_path(args[0])
1160
+ connect(options)
1161
+ begin
1162
+ # todo: only 1 api call needed here.
1163
+ # archive_bucket = find_archive_bucket_by_name_or_id(bucket_id)
1164
+ # return 1 if archive_bucket.nil?
1165
+ archive_file = find_archive_file_by_bucket_and_path(bucket_id, file_path)
1166
+ return 1 if archive_file.nil?
1167
+ # ok, load links
1168
+ params = {}
1169
+ [:phrase, :offset, :max, :sort, :direction].each do |k|
1170
+ params[k] = options[k] unless options[k].nil?
1171
+ end
1172
+ if options[:dry_run]
1173
+ print_dry_run @archive_files_interface.dry.list_links(archive_file['id'], params)
1174
+ return
1175
+ end
1176
+ json_response = @archive_files_interface.list_links(archive_file['id'], params)
1177
+ archive_file_links = json_response['archiveFileLinks']
1178
+
1179
+ if options[:json]
1180
+ print JSON.pretty_generate(json_response)
1181
+ return
1182
+ end
1183
+
1184
+ print_h1 "Archive File Links", ["#{bucket_id}:#{file_path}"]
1185
+ # print_h1 "Archive File"
1186
+ # print cyan
1187
+ # description_cols = {
1188
+ # "File ID" => 'id',
1189
+ # "Bucket" => lambda {|it| bucket_id },
1190
+ # "File Path" => lambda {|it| file_path }
1191
+ # }
1192
+ # print_description_list(description_cols, archive_file)
1193
+ # print "\n"
1194
+ # print_h2 "Links"
1195
+ if archive_file_links && archive_file_links.size > 0
1196
+ print_archive_file_links_table(archive_file_links)
1197
+ print_results_pagination(json_response, {:label => "link", :n_label => "links"})
1198
+ else
1199
+ puts "No history found"
1200
+ end
1201
+ print reset,"\n"
1202
+ return 0
1203
+ rescue RestClient::Exception => e
1204
+ print_rest_exception(e, options)
1205
+ return 1
1206
+ end
1207
+ end
1208
+
1209
+ def download_file(args)
1210
+ full_command_string = "#{command_name} download #{args.join(' ')}".strip
1211
+ options = {}
1212
+ outfile = nil
1213
+ do_overwrite = false
1214
+ do_mkdir = false
1215
+ use_public_url = false
1216
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1217
+ opts.banner = subcommand_usage("[bucket:/path] [local-file]")
1218
+ opts.on( '-f', '--force', "Overwrite existing [local-file] if it exists." ) do
1219
+ do_overwrite = true
1220
+ # do_mkdir = true
1221
+ end
1222
+ opts.on( '-p', '--mkdir', "Create missing directories for [local-file] if they do not exist." ) do
1223
+ do_mkdir = true
1224
+ end
1225
+ opts.on( '-p', '--public', "Use Public Download URL instead of Private. The file must be in a public archives." ) do
1226
+ use_public_url = true
1227
+ # do_mkdir = true
1228
+ end
1229
+ build_common_options(opts, options, [:dry_run, :quiet])
1230
+ opts.footer = "Download an archive file or directory.\n" +
1231
+ "[bucket:/path] is required. This is the name of the bucket and /path the file or folder to be downloaded.\n" +
1232
+ "[local-file] is required. This is the full local filepath for the downloaded file.\n" +
1233
+ "Directories will be downloaded as a .zip file, so you'll want to specify a [local-file] with a .zip extension."
1234
+ end
1235
+ optparse.parse!(args)
1236
+ if args.count != 2
1237
+ print_error Morpheus::Terminal.angry_prompt
1238
+ puts_error "#{command_name} download expects 2 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1239
+ return 1
1240
+ end
1241
+ connect(options)
1242
+ begin
1243
+ bucket_id, file_path = parse_bucket_id_and_file_path(args[0])
1244
+ # just make 1 api call for now
1245
+ # archive_file = find_archive_file_by_bucket_and_path(bucket_id, file_path)
1246
+ # return 1 if archive_file.nil?
1247
+ full_file_path = "#{bucket_id}/#{file_path}".squeeze('/')
1248
+ # full_file_path = args[0]
1249
+ # end download destination with a slash to use the local file basename
1250
+ outfile = args[1]
1251
+ # if outfile[-1] == "/" || outfile[-1] == "\\"
1252
+ # outfile = File.join(outfile, File.basename(full_file_path))
1253
+ # end
1254
+ outfile = File.expand_path(outfile)
1255
+ if Dir.exists?(outfile)
1256
+ outfile = File.join(outfile, File.basename(full_file_path))
1257
+ end
1258
+ if Dir.exists?(outfile)
1259
+ print_red_alert "[local-file] is invalid. It is the name of an existing directory: #{outfile}"
1260
+ return 1
1261
+ end
1262
+ destination_dir = File.dirname(outfile)
1263
+ if !Dir.exists?(destination_dir)
1264
+ if do_mkdir
1265
+ print cyan,"Creating local directory #{destination_dir}",reset,"\n"
1266
+ FileUtils.mkdir_p(destination_dir)
1267
+ else
1268
+ print_red_alert "[local-file] is invalid. Directory not found: #{destination_dir}"
1269
+ return 1
1270
+ end
1271
+ end
1272
+ if File.exists?(outfile)
1273
+ if do_overwrite
1274
+ # uhh need to be careful wih the passed filepath here..
1275
+ # don't delete, just overwrite.
1276
+ # File.delete(outfile)
1277
+ else
1278
+ print_error Morpheus::Terminal.angry_prompt
1279
+ puts_error "[local-file] is invalid. File already exists: #{outfile}", "Use -f to overwrite the existing file."
1280
+ # puts_error optparse
1281
+ return 1
1282
+ end
1283
+ end
1284
+ begin
1285
+ if options[:dry_run]
1286
+ # print_dry_run @archive_files_interface.dry.download_file_by_path(full_file_path), full_command_string
1287
+ if use_public_url
1288
+ print_dry_run @archive_files_interface.dry.download_file_by_path_chunked(full_file_path, outfile), full_command_string
1289
+ else
1290
+ print_dry_run @archive_files_interface.dry.download_public_file_by_path_chunked(full_file_path, outfile), full_command_string
1291
+ end
1292
+ return 1
1293
+ end
1294
+ if !options[:quiet]
1295
+ print cyan + "Downloading archive file #{bucket_id}:#{file_path} to #{outfile} ... "
1296
+ end
1297
+ # file_response = @archive_files_interface.download_file_by_path(full_file_path)
1298
+ # File.write(outfile, file_response.body)
1299
+ # err, maybe write to a random tmp file, then mv to outfile
1300
+ # currently, whatever the response is, it's written to the outfile. eg. 404 html
1301
+ http_response = nil
1302
+ if use_public_url
1303
+ http_response = @archive_files_interface.download_public_file_by_path_chunked(full_file_path, outfile)
1304
+ else
1305
+ http_response = @archive_files_interface.download_file_by_path_chunked(full_file_path, outfile)
1306
+ end
1307
+
1308
+ # FileUtils.chmod(0600, outfile)
1309
+ success = http_response.code.to_i == 200
1310
+ if success
1311
+ if !options[:quiet]
1312
+ print green + "SUCCESS" + reset + "\n"
1313
+ end
1314
+ return 0
1315
+ else
1316
+ if !options[:quiet]
1317
+ print red + "ERROR" + reset + " HTTP #{http_response.code}" + "\n"
1318
+ end
1319
+ # F it, just remove a bad result
1320
+ if File.exists?(outfile) && File.file?(outfile)
1321
+ Morpheus::Logging::DarkPrinter.puts "Deleting bad file download: #{outfile}" if Morpheus::Logging.debug?
1322
+ File.delete(outfile)
1323
+ end
1324
+ if options[:debug]
1325
+ puts_error http_response.inspect
1326
+ end
1327
+ return 1
1328
+ end
1329
+ rescue RestClient::Exception => e
1330
+ # this is not reached
1331
+ if e.response && e.response.code == 404
1332
+ print_red_alert "Archive file not found by path #{full_file_path}"
1333
+ return nil
1334
+ else
1335
+ raise e
1336
+ end
1337
+ end
1338
+ rescue RestClient::Exception => e
1339
+ print_rest_exception(e, options)
1340
+ return 1
1341
+ end
1342
+
1343
+ end
1344
+
1345
+ def read_file(args)
1346
+ full_command_string = "archives read #{args.join(' ')}".strip
1347
+ options = {}
1348
+ outfile = nil
1349
+ do_overwrite = false
1350
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1351
+ opts.banner = subcommand_usage("[bucket:/path]")
1352
+ build_common_options(opts, options, [:auto_confirm, :dry_run])
1353
+ opts.footer = "Print the contents of an archive file.\n" +
1354
+ "[bucket:/path] is required. This is the name of the bucket and /path the file or folder to be downloaded.\n" +
1355
+ "Confirmation is needed if the specified file is more than 1KB.\n" +
1356
+ "This confirmation can be skipped with the -y option."
1357
+ end
1358
+ optparse.parse!(args)
1359
+ if args.count != 1
1360
+ print_error Morpheus::Terminal.angry_prompt
1361
+ puts_error "#{command_name} read expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1362
+ return 1
1363
+ end
1364
+ connect(options)
1365
+ begin
1366
+ bucket_id, file_path = parse_bucket_id_and_file_path(args[0])
1367
+ archive_file = find_archive_file_by_bucket_and_path(bucket_id, file_path)
1368
+ return 1 if archive_file.nil?
1369
+ full_file_path = "#{bucket_id}/#{file_path}".squeeze('/')
1370
+ if options[:dry_run]
1371
+ print_dry_run @archive_files_interface.dry.download_file_by_path(full_file_path), full_command_string
1372
+ return 1
1373
+ end
1374
+ if archive_file['rawSize'].to_i > 1024
1375
+ pretty_size = format_bytes(archive_file['rawSize'])
1376
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to print the contents of this file (#{pretty_size}) ?")
1377
+ return 9, "aborted command"
1378
+ end
1379
+ end
1380
+ file_response = @archive_files_interface.download_file_by_path(full_file_path)
1381
+ puts file_response.body.to_s
1382
+ return 0
1383
+ rescue RestClient::Exception => e
1384
+ print_rest_exception(e, options)
1385
+ return 1
1386
+ end
1387
+
1388
+ end
1389
+
1390
+ def add_file_link(args)
1391
+ options = {}
1392
+ expiration_seconds = 20*60 # default expiration is 20 minutes
1393
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1394
+ opts.banner = subcommand_usage("[bucket:/path]")
1395
+ opts.on('-e', '--expire SECONDS', "The time to live for this link. The default is 1200 (20 minutes). A value less than 1 means never expire.") do |val|
1396
+ expiration_seconds = val.to_i
1397
+ end
1398
+ build_common_options(opts, options, [:json, :dry_run, :quiet])
1399
+ opts.footer = "Create a public link to a file.\n" +
1400
+ "[bucket:/path] is required. This is the name of the bucket and /path the file or folder to be fetched."
1401
+ end
1402
+ optparse.parse!(args)
1403
+ # consider only allowing args.count == 1 here in the format [bucket:/path]
1404
+ if args.count != 1
1405
+ print_error Morpheus::Terminal.angry_prompt
1406
+ puts_error "#{command_name} add-file-link expects 1 argument and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1407
+ return 1
1408
+ end
1409
+ bucket_id, file_path = parse_bucket_id_and_file_path(args[0])
1410
+ connect(options)
1411
+ begin
1412
+ archive_file = find_archive_file_by_bucket_and_path(bucket_id, file_path)
1413
+ return 1 if archive_file.nil?
1414
+
1415
+ params = {}
1416
+ if expiration_seconds.to_i > 0
1417
+ params['expireSeconds'] = expiration_seconds.to_i
1418
+ end
1419
+ if options[:dry_run]
1420
+ print_dry_run @archive_files_interface.dry.create_file_link(archive_file['id'], params)
1421
+ return
1422
+ end
1423
+ json_response = @archive_files_interface.create_file_link(archive_file['id'], params)
1424
+
1425
+ if options[:json]
1426
+ print JSON.pretty_generate(json_response)
1427
+ return 0
1428
+ elsif !options[:quiet]
1429
+ print_green_success "Created archive file link #{bucket_id}:/#{archive_file['filePath']} token: #{json_response['secretAccessKey']}"
1430
+ end
1431
+ return 0
1432
+ rescue RestClient::Exception => e
1433
+ print_rest_exception(e, options)
1434
+ return 1
1435
+ end
1436
+ end
1437
+
1438
+ def remove_file_link(args)
1439
+ options = {}
1440
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1441
+ opts.banner = subcommand_usage("[bucket:/path] [token]")
1442
+ build_common_options(opts, options, [:auto_confirm, :dry_run, :quiet])
1443
+ opts.footer = "Delete a public link to a file.\n" +
1444
+ "[bucket:/path] is required. This is the name of the bucket and /path the file or folder to be fetched." +
1445
+ "[token] is required. This is the secret access key that identifies the link."
1446
+ end
1447
+ optparse.parse!(args)
1448
+ # consider only allowing args.count == 1 here in the format [bucket:/path]
1449
+ if args.count != 2
1450
+ print_error Morpheus::Terminal.angry_prompt
1451
+ puts_error "#{command_name} remove-file-link expects 2 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1452
+ return 1
1453
+ end
1454
+ bucket_id, file_path = parse_bucket_id_and_file_path(args[0])
1455
+ connect(options)
1456
+ begin
1457
+ archive_file = find_archive_file_by_bucket_and_path(bucket_id, file_path)
1458
+ return 1 if archive_file.nil?
1459
+ link_id = nil
1460
+ secret_access_key = args[1]
1461
+ secret_access_key = secret_access_key.sub('/public-archives/link?s=', '')
1462
+ # find the int id via token...
1463
+ links_json_response = @archive_files_interface.list_links(archive_file['id'], {s: secret_access_key})
1464
+ if links_json_response['archiveFileLinks'] && links_json_response['archiveFileLinks'][0]
1465
+ link_id = links_json_response['archiveFileLinks'][0]['id']
1466
+ end
1467
+ if !link_id
1468
+ print_red_alert "Archive file link not found for #{bucket_id}:/#{archive_file['filePath']} token: #{secret_access_key}"
1469
+ return 1
1470
+ end
1471
+ params = {}
1472
+ if options[:dry_run]
1473
+ print_dry_run @archive_files_interface.dry.destroy_file_link(archive_file['id'], link_id, params)
1474
+ return
1475
+ end
1476
+ json_response = @archive_files_interface.destroy_file_link(archive_file['id'], link_id, params)
1477
+
1478
+ if options[:json]
1479
+ print JSON.pretty_generate(json_response)
1480
+ return 0
1481
+ elsif !options[:quiet]
1482
+ print_green_success "Deleted archive file link #{bucket_id}:/#{archive_file['filePath']} token: #{secret_access_key}"
1483
+ end
1484
+ return 0
1485
+ rescue RestClient::Exception => e
1486
+ print_rest_exception(e, options)
1487
+ return 1
1488
+ end
1489
+ end
1490
+
1491
+ def download_file_link(args)
1492
+ full_command_string = "archives download-link #{args.join(' ')}".strip
1493
+ options = {}
1494
+ outfile = nil
1495
+ do_overwrite = false
1496
+ dor_mkdir = false
1497
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1498
+ opts.banner = subcommand_usage("[link-key] [local-file]")
1499
+ opts.on( '-f', '--force', "Overwrite existing [local-file] if it exists." ) do
1500
+ do_overwrite = true
1501
+ # do_mkdir = true
1502
+ end
1503
+ opts.on( '-p', '--mkdir', "Create missing directories for [local-file] if they do not exist." ) do
1504
+ do_mkdir = true
1505
+ end
1506
+ build_common_options(opts, options, [:dry_run, :quiet])
1507
+ opts.footer = "Download an archive file link.\n" +
1508
+ "[link-key] is required. This is the secret access key for the archive file link.\n" +
1509
+ "[local-file] is required. This is the full local filepath for the downloaded file."
1510
+ end
1511
+ optparse.parse!(args)
1512
+ if args.count != 2
1513
+ print_error Morpheus::Terminal.angry_prompt
1514
+ puts_error "#{command_name} download-link expects 2 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1515
+ return 1
1516
+ end
1517
+ connect(options)
1518
+ begin
1519
+ link_key = args[0]
1520
+ # archive_file_link = find_archive_file_link_by_key(link_key)
1521
+ # just make 1 api call for now
1522
+ # archive_file = find_archive_file_by_bucket_and_path(bucket_id, file_path)
1523
+ # return 1 if archive_file.nil?
1524
+ full_file_path = "#{bucket_id}/#{file_path}".squeeze('/')
1525
+ # full_file_path = args[0]
1526
+ outfile = File.expand_path(args[1])
1527
+ # [local-file] must include the full file name when downloading a link
1528
+ # if Dir.exists?(outfile)
1529
+ # outfile = File.join(outfile, File.basename(archive_file['name']))
1530
+ # end
1531
+ if Dir.exists?(outfile)
1532
+ print_red_alert "[local-file] is invalid. It is the name of an existing directory: #{outfile}"
1533
+ return 1
1534
+ end
1535
+ destination_dir = File.dirname(outfile)
1536
+ if !Dir.exists?(destination_dir)
1537
+ if do_mkdir
1538
+ print cyan,"Creating local directory #{destination_dir}",reset,"\n"
1539
+ FileUtils.mkdir_p(destination_dir)
1540
+ else
1541
+ print_red_alert "[local-file] is invalid. Directory not found: #{destination_dir}"
1542
+ return 1
1543
+ end
1544
+ end
1545
+ if File.exists?(outfile)
1546
+ if do_overwrite
1547
+ # uhh need to be careful wih the passed filepath here..
1548
+ # don't delete, just overwrite.
1549
+ # File.delete(outfile)
1550
+ else
1551
+ print_red_alert "[local-file] is invalid. File already exists: #{outfile}"
1552
+ # print_error Morpheus::Terminal.angry_prompt
1553
+ # puts_error "[local-file] is invalid. File already exists: #{outfile}\n#{optparse}"
1554
+ puts_error "Use -f to overwrite the existing file."
1555
+ # puts_error optparse
1556
+ return 1
1557
+ end
1558
+ end
1559
+
1560
+ if options[:dry_run]
1561
+ # print_dry_run @archive_files_interface.dry.download_file_by_path(full_file_path), full_command_string
1562
+ print_dry_run @archive_files_interface.dry.download_file_by_link_chunked(link_key, outfile), full_command_string
1563
+ return 1
1564
+ end
1565
+ if !options[:quiet]
1566
+ print cyan + "Downloading archive file link #{link_key} to #{outfile} ... "
1567
+ end
1568
+ # file_response = @archive_files_interface.download_file_by_path(full_file_path)
1569
+ # File.write(outfile, file_response.body)
1570
+ # err, maybe write to a random tmp file, then mv to outfile
1571
+ # currently, whatever the response is, it's written to the outfile. eg. 404 html
1572
+ http_response = @archive_files_interface.download_file_by_link_chunked(link_key, outfile)
1573
+
1574
+ # FileUtils.chmod(0600, outfile)
1575
+ success = http_response.code.to_i == 200
1576
+ if success
1577
+ if !options[:quiet]
1578
+ print green + "SUCCESS" + reset + "\n"
1579
+ end
1580
+ return 0
1581
+ else
1582
+ if !options[:quiet]
1583
+ print red + "ERROR" + reset + " HTTP #{http_response.code}" + "\n"
1584
+ end
1585
+ # F it, just remove a bad result
1586
+ if File.exists?(outfile) && File.file?(outfile)
1587
+ Morpheus::Logging::DarkPrinter.puts "Deleting bad file download: #{outfile}" if Morpheus::Logging.debug?
1588
+ File.delete(outfile)
1589
+ end
1590
+ if options[:debug]
1591
+ puts_error http_response.inspect
1592
+ end
1593
+ return 1
1594
+ end
1595
+ rescue RestClient::Exception => e
1596
+ print_rest_exception(e, options)
1597
+ return 1
1598
+ end
1599
+
1600
+ end
1601
+
1602
+ def download_bucket_zip(args)
1603
+ full_command_string = "#{command_name} download-bucket #{args.join(' ')}".strip
1604
+ options = {}
1605
+ outfile = nil
1606
+ do_overwrite = false
1607
+ do_mkdir = false
1608
+ use_public_url = false
1609
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
1610
+ opts.banner = subcommand_usage("[bucket] [local-file]")
1611
+ opts.on( '-f', '--force', "Overwrite existing [local-file] if it exists." ) do
1612
+ do_overwrite = true
1613
+ # do_mkdir = true
1614
+ end
1615
+ opts.on( '-p', '--mkdir', "Create missing directories for [local-file] if they do not exist." ) do
1616
+ do_mkdir = true
1617
+ end
1618
+ # api endpoint needed still for public bucket.zip
1619
+ # opts.on( '-p', '--public', "Use Public Download URL instead of Private. The bucket must be have Public URL enabled." ) do
1620
+ # use_public_url = true
1621
+ # # do_mkdir = true
1622
+ # end
1623
+ build_common_options(opts, options, [:dry_run, :quiet])
1624
+ opts.footer = "Download an entire archive bucket as a .zip file.\n" +
1625
+ "[bucket] is required. This is the name of the bucket.\n" +
1626
+ "[local-file] is required. This is the full local filepath for the downloaded file.\n" +
1627
+ "Buckets are be downloaded as a .zip file, so you'll want to specify a [local-file] with a .zip extension."
1628
+ end
1629
+ optparse.parse!(args)
1630
+ if args.count != 2
1631
+ print_error Morpheus::Terminal.angry_prompt
1632
+ puts_error "#{command_name} download-bucket expects 2 arguments and received #{args.count}: #{args.join(' ')}\n#{optparse}"
1633
+ return 1
1634
+ end
1635
+ connect(options)
1636
+ begin
1637
+ bucket_id = args[0].to_s
1638
+ archive_bucket = find_archive_bucket_by_name_or_id(bucket_id)
1639
+ return 1 if archive_bucket.nil?
1640
+
1641
+ outfile = args[1]
1642
+ # if outfile[-1] == "/" || outfile[-1] == "\\"
1643
+ # outfile = File.join(outfile, archive_bucket['name'].to_s) + ".zip"
1644
+ # end
1645
+ outfile = File.expand_path(outfile)
1646
+ if Dir.exists?(outfile)
1647
+ outfile = File.join(outfile, archive_bucket['name'].to_s) + ".zip"
1648
+ end
1649
+ if Dir.exists?(outfile)
1650
+ print_red_alert "[local-file] is invalid. It is the name of an existing directory: #{outfile}"
1651
+ return 1
1652
+ end
1653
+ # always a .zip
1654
+ if outfile[-4..-1] != ".zip"
1655
+ outfile << ".zip"
1656
+ end
1657
+ destination_dir = File.dirname(outfile)
1658
+ if !Dir.exists?(destination_dir)
1659
+ if do_mkdir
1660
+ print cyan,"Creating local directory #{destination_dir}",reset,"\n"
1661
+ FileUtils.mkdir_p(destination_dir)
1662
+ else
1663
+ print_red_alert "[local-file] is invalid. Directory not found: #{destination_dir}"
1664
+ return 1
1665
+ end
1666
+ end
1667
+ if File.exists?(outfile)
1668
+ if do_overwrite
1669
+ # uhh need to be careful wih the passed filepath here..
1670
+ # don't delete, just overwrite.
1671
+ # File.delete(outfile)
1672
+ else
1673
+ print_error Morpheus::Terminal.angry_prompt
1674
+ puts_error "[local-file] is invalid. File already exists: #{outfile}", "Use -f to overwrite the existing file."
1675
+ # puts_error optparse
1676
+ return 1
1677
+ end
1678
+ end
1679
+
1680
+ if options[:dry_run]
1681
+ print_dry_run @archive_buckets_interface.dry.download_bucket_zip_chunked(bucket_id, outfile), full_command_string
1682
+ return 1
1683
+ end
1684
+ if !options[:quiet]
1685
+ print cyan + "Downloading archive bucket #{bucket_id} to #{outfile} ... "
1686
+ end
1687
+
1688
+ http_response = @archive_buckets_interface.download_bucket_zip_chunked(bucket_id, outfile)
1689
+
1690
+ # FileUtils.chmod(0600, outfile)
1691
+ success = http_response.code.to_i == 200
1692
+ if success
1693
+ if !options[:quiet]
1694
+ print green + "SUCCESS" + reset + "\n"
1695
+ end
1696
+ return 0
1697
+ else
1698
+ if !options[:quiet]
1699
+ print red + "ERROR" + reset + " HTTP #{http_response.code}" + "\n"
1700
+ end
1701
+ # F it, just remove a bad result
1702
+ if File.exists?(outfile) && File.file?(outfile)
1703
+ Morpheus::Logging::DarkPrinter.puts "Deleting bad file download: #{outfile}" if Morpheus::Logging.debug?
1704
+ File.delete(outfile)
1705
+ end
1706
+ if options[:debug]
1707
+ puts_error http_response.inspect
1708
+ end
1709
+ return 1
1710
+ end
1711
+
1712
+ rescue RestClient::Exception => e
1713
+ print_rest_exception(e, options)
1714
+ return 1
1715
+ end
1716
+ end
1717
+
1718
+
1719
+ private
1720
+
1721
+ def find_archive_bucket_by_name_or_id(val)
1722
+ return find_archive_bucket_by_id(val)
1723
+ # if val.to_s =~ /\A\d{1,}\Z/
1724
+ # return find_archive_bucket_by_id(val)
1725
+ # else
1726
+ # return find_archive_bucket_by_name(val)
1727
+ # end
1728
+ end
1729
+
1730
+ def find_archive_bucket_by_id(id)
1731
+ begin
1732
+ # this is typically passed as name, the api supports either name or id
1733
+ json_response = @archive_buckets_interface.get(id.to_s)
1734
+ archive_bucket = json_response['archiveBucket']
1735
+ archive_bucket['isOwner'] = !!json_response['isOwner']
1736
+ return archive_bucket
1737
+ rescue RestClient::Exception => e
1738
+ if e.response && e.response.code == 404
1739
+ print_red_alert "Archive bucket not found by id #{id}"
1740
+ return nil
1741
+ else
1742
+ raise e
1743
+ end
1744
+ end
1745
+ end
1746
+
1747
+ def find_archive_bucket_by_name(name)
1748
+ archive_buckets = @archive_buckets_interface.list({name: name.to_s})['archiveBuckets']
1749
+ if archive_buckets.empty?
1750
+ print_red_alert "Archive bucket not found by name #{name}"
1751
+ return nil
1752
+ elsif archive_buckets.size > 1
1753
+ print_red_alert "#{archive_buckets.size} archive buckets found by name #{name}"
1754
+ # print_archive_buckets_table(archive_buckets, {color: red})
1755
+ rows = archive_buckets.collect do |it|
1756
+ {id: it['id'], name: it['name']}
1757
+ end
1758
+ print red
1759
+ tp rows, [:id, :name]
1760
+ print reset,"\n"
1761
+ return nil
1762
+ else
1763
+ return archive_buckets[0]
1764
+ end
1765
+ end
1766
+
1767
+ def find_archive_file_by_id(id)
1768
+ begin
1769
+ json_response = @archive_files_interface.get(id.to_s)
1770
+ return json_response['archiveFile']
1771
+ rescue RestClient::Exception => e
1772
+ if e.response && e.response.code == 404
1773
+ print_red_alert "Archive file not found by id #{id}"
1774
+ return nil
1775
+ else
1776
+ raise e
1777
+ end
1778
+ end
1779
+ end
1780
+
1781
+ def find_archive_file_by_bucket_and_path(bucket_id, file_path)
1782
+ if file_path.to_s.empty? || file_path.to_s.strip == "/"
1783
+ print_red_alert "Archive file not found for bucket: '#{bucket_id}' file: (blank)"
1784
+ return nil
1785
+ end
1786
+ # chomp leading and trailing slashes, the api isn't doin this right now.
1787
+ if file_path.size > 1 && file_path[-1] == "/"
1788
+ file_path = file_path[0..-2]
1789
+ end
1790
+ if file_path[0] && file_path[0].chr == "/"
1791
+ file_path = file_path[1..-1]
1792
+ end
1793
+ # ok, find the file id by searching /archives/buckets/$bucketId/files/$filePath
1794
+ json_response = @archive_buckets_interface.list_files(bucket_id, file_path)
1795
+ # json_response = @archive_buckets_interface.list_files(bucket_id, "/", {phrase: file_path})
1796
+ # json_response = @archive_buckets_interface.list_files(bucket_id, "/", {absoluteFilePath: file_path})
1797
+ # puts "find_archive_file() json_response is: ", JSON.pretty_generate(json_response)
1798
+ archive_file = nil
1799
+ archive_files = json_response['archiveFiles']
1800
+ # silly hack, not needed while using ?absoluteFilePath=
1801
+ if json_response['parentDirectory'] && json_response['parentDirectory']['filePath'] == file_path
1802
+ archive_file = json_response['parentDirectory']
1803
+ else
1804
+ archive_file = archive_files[0]
1805
+ end
1806
+ if archive_file.nil?
1807
+ print_red_alert "Archive file not found for bucket: '#{bucket_id}' file: '#{file_path}'"
1808
+ return nil
1809
+ end
1810
+ return archive_file
1811
+ end
1812
+
1813
+ # def find_group_by_name(name)
1814
+ # group_results = @groups_interface.get(name)
1815
+ # if group_results['groups'].empty?
1816
+ # print_red_alert "Group not found by name #{name}"
1817
+ # return nil
1818
+ # end
1819
+ # return group_results['groups'][0]
1820
+ # end
1821
+
1822
+ # def find_cloud_by_name(group_id, name)
1823
+ # option_results = @options_interface.options_for_source('clouds',{groupId: group_id})
1824
+ # match = option_results['data'].find { |grp| grp['value'].to_s == name.to_s || grp['name'].downcase == name.downcase}
1825
+ # if match.nil?
1826
+ # print_red_alert "Cloud not found by name #{name}"
1827
+ # return nil
1828
+ # else
1829
+ # return match['value']
1830
+ # end
1831
+ # end
1832
+
1833
+
1834
+ def format_archive_bucket_full_status(archive_bucket, return_color=cyan)
1835
+ out = ""
1836
+ if archive_bucket['lastResult']
1837
+ out << format_archive_bucket_execution_status(archive_bucket['lastResult'])
1838
+ else
1839
+ out << ""
1840
+ end
1841
+ out
1842
+ end
1843
+
1844
+ def format_archive_bucket_status(archive_bucket, return_color=cyan)
1845
+ out = ""
1846
+ if archive_bucket['lastResult']
1847
+ out << format_archive_bucket_execution_status(archive_bucket['lastResult'])
1848
+ else
1849
+ out << ""
1850
+ end
1851
+ out
1852
+ end
1853
+
1854
+ def format_archive_bucket_execution_status(archive_bucket_execution, return_color=cyan)
1855
+ out = ""
1856
+ status_string = archive_bucket_execution['status']
1857
+ if status_string == 'running'
1858
+ out << "#{green}#{status_string.upcase}#{return_color}"
1859
+ elsif status_string == 'success'
1860
+ out << "#{green}#{status_string.upcase}#{return_color}"
1861
+ elsif status_string == 'failed'
1862
+ out << "#{red}#{status_string.upcase}#{return_color}"
1863
+ elsif status_string == 'pending'
1864
+ out << "#{white}#{status_string.upcase}#{return_color}"
1865
+ elsif status_string
1866
+ out << "#{yellow}#{status_string.upcase}#{return_color}"
1867
+ else
1868
+ out << ""
1869
+ end
1870
+ out
1871
+ end
1872
+
1873
+ def format_archive_bucket_execution_result(archive_bucket_execution, return_color=cyan)
1874
+ out = ""
1875
+ status_string = archive_bucket_execution['status']
1876
+ if status_string == 'running' # || status_string == 'pending'
1877
+ out << generate_usage_bar(archive_bucket_execution['statusPercent'], 100, {max_bars: 10})
1878
+ out << return_color if return_color
1879
+ out << " - #{archive_bucket_execution['statusMessage']}"
1880
+ elsif archive_bucket_execution['statusMessage']
1881
+ out << "#{archive_bucket_execution['statusMessage']}"
1882
+ end
1883
+ if archive_bucket_execution['errorMessage']
1884
+ out << " - #{red}#{archive_bucket_execution['errorMessage']}#{return_color}"
1885
+ end
1886
+ out
1887
+ end
1888
+
1889
+ # def get_available_boot_scripts()
1890
+ # boot_scripts_dropdown = []
1891
+ # scripts = @boot_scripts_interface.list({max:1000})['bootScripts']
1892
+ # scripts.each do |it|
1893
+ # boot_scripts_dropdown << {'name'=>it['fileName'],'value'=>it['id']}
1894
+ # end
1895
+ # boot_scripts_dropdown << {'name'=>'Custom','value'=> 'custom'}
1896
+ # return boot_scripts_dropdown
1897
+ # end
1898
+
1899
+ def get_available_boot_scripts(refresh=false)
1900
+ if !@available_boot_scripts || refresh
1901
+ # option_results = options_interface.options_for_source('bootScripts',{})['data']
1902
+ boot_scripts_dropdown = []
1903
+ scripts = @boot_scripts_interface.list({max:1000})['bootScripts']
1904
+ scripts.each do |it|
1905
+ boot_scripts_dropdown << {'name'=>it['fileName'],'value'=>it['id'],'id'=>it['id']}
1906
+ end
1907
+ boot_scripts_dropdown << {'name'=>'Custom','value'=> 'custom','id'=> 'custom'}
1908
+ @available_boot_scripts = boot_scripts_dropdown
1909
+ end
1910
+ #puts "available_boot_scripts() rtn: #{@available_boot_scripts.inspect}"
1911
+ return @available_boot_scripts
1912
+ end
1913
+
1914
+ def find_boot_script(val)
1915
+ if val.nil? || val.to_s.empty?
1916
+ return nil
1917
+ else
1918
+ return get_available_boot_scripts().find { |it|
1919
+ (it['id'].to_s.downcase == val.to_s.downcase) ||
1920
+ (it['name'].to_s.downcase == val.to_s.downcase)
1921
+ }
1922
+ end
1923
+ end
1924
+
1925
+ def get_available_preseed_scripts(refresh=false)
1926
+ if !@available_preseed_scripts || refresh
1927
+ # option_results = options_interface.options_for_source('preseedScripts',{})['data']
1928
+ preseed_scripts_dropdown = []
1929
+ scripts = @preseed_scripts_interface.list({max:1000})['preseedScripts']
1930
+ scripts.each do |it|
1931
+ preseed_scripts_dropdown << {'name'=>it['fileName'],'value'=>it['id'],'id'=>it['id']}
1932
+ end
1933
+ # preseed_scripts_dropdown << {'name'=>'Custom','value'=> 'custom','value'=> 'custom'}
1934
+ @available_preseed_scripts = preseed_scripts_dropdown
1935
+ end
1936
+ #puts "available_preseed_scripts() rtn: #{@available_preseed_scripts.inspect}"
1937
+ return @available_preseed_scripts
1938
+ end
1939
+
1940
+ def find_preseed_script(val)
1941
+ if val.nil? || val.to_s.empty?
1942
+ return nil
1943
+ else
1944
+ return get_available_preseed_scripts().find { |it|
1945
+ (it['id'].to_s.downcase == val.to_s.downcase) ||
1946
+ (it['name'].to_s.downcase == val.to_s.downcase)
1947
+ }
1948
+ end
1949
+ end
1950
+
1951
+ def prompt_edit_archive_bucket(archive_bucket, options={})
1952
+ # populate default prompt values with the existing archive bucket
1953
+ default_values = archive_bucket.dup # lazy, but works as long as GET matches POST api structure
1954
+ # storage provider (cannot be edited anyhow..)
1955
+ if archive_bucket['storageProvider'].kind_of?(Hash)
1956
+ default_values['storageProvider'] = archive_bucket['storageProvider']['id']
1957
+ end
1958
+ # tenants
1959
+ if archive_bucket['accounts'].kind_of?(Array) && archive_bucket['accounts'].size > 0
1960
+ default_values['accounts'] = archive_bucket['accounts'].collect {|it| it['name'] }.join(", ")
1961
+ end
1962
+ # any other mismatches? preseedScript, bootScript?
1963
+ options[:is_edit] = true
1964
+ return prompt_new_archive_bucket(options, default_values)
1965
+ end
1966
+
1967
+ def prompt_new_archive_bucket(options={}, default_values={})
1968
+ payload = {}
1969
+
1970
+ # Name
1971
+ if options['name']
1972
+ payload['name'] = options['name']
1973
+ else
1974
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'description' => 'Enter a name for this archive bucket.', 'defaultValue' => default_values['name']}], options, @api_client)
1975
+ payload['name'] = v_prompt['name']
1976
+ end
1977
+
1978
+ # Description
1979
+ if options['description']
1980
+ payload['description'] = options['description']
1981
+ else
1982
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'defaultValue' => default_values['description']}], options, @api_client)
1983
+ payload['description'] = v_prompt['description']
1984
+ end
1985
+
1986
+ # Storage Provider
1987
+ unless options[:is_edit]
1988
+ if options['storageProvider']
1989
+ # payload['storageProvider'] = options['storageProvider']
1990
+ # prompt is skipped when options['fieldName'] is passed in, this will return an id from a name
1991
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'storageProvider', 'fieldLabel' => 'Storage Provider', 'type' => 'select', 'optionSource' => 'storageProviders', 'description' => 'Storage Provider', 'defaultValue' => options['storageProvider'], 'required' => true}], options, @api_client, {})
1992
+ payload['storageProvider'] = {id: v_prompt['storageProvider']}
1993
+ else
1994
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'storageProvider', 'fieldLabel' => 'Storage Provider', 'type' => 'select', 'optionSource' => 'storageProviders', 'description' => 'Storage Provider', 'defaultValue' => default_values['storageProvider'], 'required' => true}], options, @api_client, {})
1995
+ payload['storageProvider'] = {id: v_prompt['storageProvider']}
1996
+ end
1997
+ end
1998
+
1999
+ # Tenants
2000
+ # TODO: a nice select component for adding/removing from this array
2001
+ if options['accounts']
2002
+ payload['accounts'] = options['accounts'] #.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
2003
+ else
2004
+ tenants_default_value = default_values['accounts']
2005
+ if tenants_default_value.kind_of?(Array)
2006
+ tenants_default_value = tenants_default_value.collect {|it| it["id"] }.join(", ")
2007
+ end
2008
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'accounts', 'fieldLabel' => 'Tenants', 'type' => 'text', 'description' => 'Tenant Accounts (comma separated ids)', 'defaultValue' => tenants_default_value}], options, @api_client)
2009
+ payload['accounts'] = v_prompt['accounts'].to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
2010
+ end
2011
+
2012
+ # Visibility
2013
+ if options['visibility']
2014
+ payload['visibility'] = options['visibility']
2015
+ else
2016
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'visibility', 'fieldLabel' => 'Visibility', 'type' => 'select', 'selectOptions' => [{'name' => 'Private', 'value' => 'private'}, {'name' => 'Public', 'value' => 'public'}], 'defaultValue' => (default_values['visibility'] || 'private'), 'required' => true}], options, @api_client, {})
2017
+ payload['visibility'] = v_prompt['visibility']
2018
+ end
2019
+
2020
+ # Public URL
2021
+ if options['isPublic']
2022
+ payload['isPublic'] = options['isPublic']
2023
+ else
2024
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'isPublic', 'fieldLabel' => 'Public URL', 'type' => 'checkbox', 'description' => 'Enabling Public URL allows files to be downloaded without any authentication.', 'defaultValue' => (default_values['isPublic'].nil? ? false : default_values['isPublic']), 'required' => true}], options, @api_client, {})
2025
+ payload['isPublic'] = v_prompt['isPublic']
2026
+ end
2027
+
2028
+ return payload
2029
+ end
2030
+
2031
+ def print_archive_files_table(archive_files, options={})
2032
+ table_color = options[:color] || cyan
2033
+ rows = archive_files.collect do |archive_file|
2034
+ {
2035
+ id: archive_file['id'],
2036
+ name: options[:fullTree] ? archive_file['filePath'] : archive_file['name'],
2037
+ type: archive_file['isDirectory'] ? 'directory' : (archive_file['contentType']),
2038
+ size: format_bytes(archive_file['rawSize']),
2039
+ dateCreated: format_local_dt(archive_file['dateCreated']),
2040
+ lastUpdated: format_local_dt(archive_file['lastUpdated'])
2041
+ }
2042
+ end
2043
+ columns = [
2044
+ # :id,
2045
+ {:name => {:display_name => "File".upcase} },
2046
+ :type,
2047
+ :size,
2048
+ # {:dateCreated => {:display_name => "Date Created"} },
2049
+ {:lastUpdated => {:display_name => "Last Modified".upcase} }
2050
+ ]
2051
+ print table_color
2052
+ print as_pretty_table(rows, columns, options)
2053
+ print reset
2054
+ end
2055
+
2056
+ def print_archive_logs_table(archive_logs, opts={})
2057
+ table_color = opts[:color] || cyan
2058
+ rows = archive_logs.collect do |archive_log|
2059
+ response_str = ""
2060
+ if archive_log['responseCode']
2061
+ if archive_log['responseCode'].to_i == 200
2062
+ response_str = "#{green}#{archive_log['responseCode']} #{archive_log['responseMessage']}".strip + table_color
2063
+ else
2064
+ response_str = "#{red}#{archive_log['responseCode']}#{reset} #{archive_log['responseMessage']}".strip + table_color
2065
+ end
2066
+ end
2067
+ if archive_log['fileSize'] && archive_log['fileSize'].to_i > 0
2068
+ response_str << " (#{format_bytes(archive_log['fileSize'])})"
2069
+ end
2070
+ request_str = "#{archive_log['requestUrl']}".strip
2071
+ # if archive_log['archiveFileLink']
2072
+ # request_str << " [Link #{archive_log['archiveFileLink']['shortKey']}]"
2073
+ # end
2074
+ {
2075
+ id: archive_log['id'],
2076
+ eventType: archive_log['eventType'] ? archive_log['eventType'] : '',
2077
+ bucket: archive_log['archiveBucket'] ? archive_log['archiveBucket']['name'] : '',
2078
+ file: archive_log['archiveFile'] ? archive_log['archiveFile']['filePath'] : '',
2079
+ link: archive_log['archiveFileLink'] ? archive_log['archiveFileLink']['shortKey'] : '',
2080
+ description: archive_log['description'] ? archive_log['description'] : '',
2081
+ fileSize: format_bytes(archive_log['fileSize']),
2082
+ dateCreated: format_local_dt(archive_log['dateCreated']),
2083
+ user: archive_log['user'] ? archive_log['user']['username'] : '',
2084
+ webInterface: archive_log['webInterface'],
2085
+ #request: request_str,
2086
+ response: response_str
2087
+ }
2088
+ end
2089
+ columns = [
2090
+ {:dateCreated => {:display_name => "Date".upcase} },
2091
+ {:eventType => {:display_name => "Event".upcase} },
2092
+ :bucket,
2093
+ :file,
2094
+ :link,
2095
+ # :fileSize,
2096
+ :description,
2097
+ :user,
2098
+ {:webInterface => {:display_name => "Interface".upcase} },
2099
+ #:request,
2100
+ :response
2101
+ ]
2102
+ if opts[:exclude]
2103
+ columns = columns.reject {|c|
2104
+ c.is_a?(Hash) ? opts[:exclude].include?(c.keys[0]) : opts[:exclude].include?(c)
2105
+ }
2106
+ end
2107
+ print table_color
2108
+ print as_pretty_table(rows, columns, opts)
2109
+ print reset
2110
+ end
2111
+
2112
+ def print_archive_file_links_table(archive_file_links, opts={})
2113
+ table_color = opts[:color] || cyan
2114
+ rows = archive_file_links.collect do |archive_file_link|
2115
+ status_str = ""
2116
+ begin
2117
+ if archive_file_link['expirationDate'] && Time.now.utc > parse_time(archive_file_link['expirationDate'])
2118
+ status_str = red + "EXPIRED" + table_color
2119
+ else
2120
+ status_str = green + "ACTIVE" + table_color
2121
+ end
2122
+ rescue => ex
2123
+ Morpheus::Logging::DarkPrinter.puts "trouble parsing expiration date: #{ex.inspect}" if Morpheus::Logging.debug?
2124
+ end
2125
+ link_url = "/public-archives/link?s=" + archive_file_link['secretAccessKey'].to_s
2126
+ {
2127
+ url: link_url,
2128
+ created: format_local_dt(archive_file_link['dateCreated']),
2129
+ expires: archive_file_link['expirationDate'] ? format_local_dt(archive_file_link['expirationDate']) : 'Never',
2130
+ downloads: archive_file_link['downloadCount'],
2131
+ status: status_str
2132
+ }
2133
+ end
2134
+ columns = [
2135
+ {:url => {:display_name => "Link URL".upcase} },
2136
+ :created,
2137
+ :expires,
2138
+ :downloads,
2139
+ :status
2140
+ ]
2141
+ print table_color
2142
+ print as_pretty_table(rows, columns, opts)
2143
+ print reset
2144
+ end
2145
+
2146
+ # parse_bucket_id_and_file_path() provides flexible argument formats for bucket and path
2147
+ # it looks for [bucket:/path] or [bucket] [path]
2148
+ # @param delim [String] Default is a comma and any surrounding white space.
2149
+ # @return [Array] 2 elements, bucket name (or id) and the file path.
2150
+ # The default file path is "/".
2151
+ # Examples:
2152
+ # parse_bucket_id_and_file_path("test") == ["test", "/"]
2153
+ # parse_bucket_id_and_file_path("test:/global.cfg") == ["test", "/global.cfg"]
2154
+ # parse_bucket_id_and_file_path("test:/node1/node.cfg") == ["test", "/node1/node.cfg"]
2155
+ # parse_bucket_id_and_file_path("test/node1/node.cfg") == ["test", "/node1/node.cfg"]
2156
+ # parse_bucket_id_and_file_path("test", "node1/node.cfg") == ["test", "/node1/node.cfg"]
2157
+ #
2158
+ def parse_bucket_id_and_file_path(*args)
2159
+ if args.size < 1 || args.size > 2
2160
+ return nil, nil
2161
+ end
2162
+ if !args[0]
2163
+ return nil, nil
2164
+ end
2165
+ full_path = args[0].to_s
2166
+ if args[1]
2167
+ if full_path.include?(":")
2168
+ full_path = "#{full_path}/#{args[1]}"
2169
+ else
2170
+ full_path = "#{full_path}:#{args[1]}"
2171
+ end
2172
+ end
2173
+ # ok fine, allow just bucketId/filePath, without a colon.
2174
+ if !full_path.include?(":") && full_path.include?("/")
2175
+ path_elements = full_path.split("/")
2176
+ full_path = path_elements[0] + ":" + path_elements[1..-1].join("/")
2177
+ end
2178
+ uri_elements = full_path.split(":")
2179
+ bucket_id = uri_elements[0]
2180
+ file_path = uri_elements[1..-1].join("/") # [1]
2181
+ file_path = "/#{file_path}".squeeze("/")
2182
+ return bucket_id, file_path
2183
+ end
2184
+ end