morpheus-cli 2.12.5 → 3.1.0

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