aspera-cli 4.0.0.pre1

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3592 -0
  3. data/bin/ascli +7 -0
  4. data/bin/asession +89 -0
  5. data/docs/Makefile +59 -0
  6. data/docs/README.erb.md +3012 -0
  7. data/docs/README.md +13 -0
  8. data/docs/diagrams.txt +49 -0
  9. data/docs/secrets.make +38 -0
  10. data/docs/test_env.conf +117 -0
  11. data/docs/transfer_spec.html +99 -0
  12. data/examples/aoc.rb +17 -0
  13. data/examples/proxy.pac +60 -0
  14. data/examples/transfer.rb +115 -0
  15. data/lib/aspera/api_detector.rb +60 -0
  16. data/lib/aspera/ascmd.rb +151 -0
  17. data/lib/aspera/ats_api.rb +43 -0
  18. data/lib/aspera/cli/basic_auth_plugin.rb +38 -0
  19. data/lib/aspera/cli/extended_value.rb +88 -0
  20. data/lib/aspera/cli/formater.rb +238 -0
  21. data/lib/aspera/cli/listener/line_dump.rb +17 -0
  22. data/lib/aspera/cli/listener/logger.rb +20 -0
  23. data/lib/aspera/cli/listener/progress.rb +52 -0
  24. data/lib/aspera/cli/listener/progress_multi.rb +91 -0
  25. data/lib/aspera/cli/main.rb +304 -0
  26. data/lib/aspera/cli/manager.rb +440 -0
  27. data/lib/aspera/cli/plugin.rb +90 -0
  28. data/lib/aspera/cli/plugins/alee.rb +24 -0
  29. data/lib/aspera/cli/plugins/ats.rb +231 -0
  30. data/lib/aspera/cli/plugins/bss.rb +71 -0
  31. data/lib/aspera/cli/plugins/config.rb +806 -0
  32. data/lib/aspera/cli/plugins/console.rb +62 -0
  33. data/lib/aspera/cli/plugins/cos.rb +106 -0
  34. data/lib/aspera/cli/plugins/faspex.rb +377 -0
  35. data/lib/aspera/cli/plugins/faspex5.rb +93 -0
  36. data/lib/aspera/cli/plugins/node.rb +438 -0
  37. data/lib/aspera/cli/plugins/oncloud.rb +937 -0
  38. data/lib/aspera/cli/plugins/orchestrator.rb +169 -0
  39. data/lib/aspera/cli/plugins/preview.rb +464 -0
  40. data/lib/aspera/cli/plugins/server.rb +216 -0
  41. data/lib/aspera/cli/plugins/shares.rb +63 -0
  42. data/lib/aspera/cli/plugins/shares2.rb +114 -0
  43. data/lib/aspera/cli/plugins/sync.rb +65 -0
  44. data/lib/aspera/cli/plugins/xnode.rb +115 -0
  45. data/lib/aspera/cli/transfer_agent.rb +251 -0
  46. data/lib/aspera/cli/version.rb +5 -0
  47. data/lib/aspera/colors.rb +39 -0
  48. data/lib/aspera/command_line_builder.rb +137 -0
  49. data/lib/aspera/fasp/aoc.rb +24 -0
  50. data/lib/aspera/fasp/connect.rb +99 -0
  51. data/lib/aspera/fasp/error.rb +21 -0
  52. data/lib/aspera/fasp/error_info.rb +60 -0
  53. data/lib/aspera/fasp/http_gw.rb +81 -0
  54. data/lib/aspera/fasp/installation.rb +240 -0
  55. data/lib/aspera/fasp/listener.rb +11 -0
  56. data/lib/aspera/fasp/local.rb +377 -0
  57. data/lib/aspera/fasp/manager.rb +69 -0
  58. data/lib/aspera/fasp/node.rb +88 -0
  59. data/lib/aspera/fasp/parameters.rb +235 -0
  60. data/lib/aspera/fasp/resume_policy.rb +76 -0
  61. data/lib/aspera/fasp/uri.rb +51 -0
  62. data/lib/aspera/faspex_gw.rb +196 -0
  63. data/lib/aspera/hash_ext.rb +28 -0
  64. data/lib/aspera/log.rb +80 -0
  65. data/lib/aspera/nagios.rb +71 -0
  66. data/lib/aspera/node.rb +14 -0
  67. data/lib/aspera/oauth.rb +319 -0
  68. data/lib/aspera/on_cloud.rb +421 -0
  69. data/lib/aspera/open_application.rb +72 -0
  70. data/lib/aspera/persistency_action_once.rb +42 -0
  71. data/lib/aspera/persistency_folder.rb +91 -0
  72. data/lib/aspera/preview/file_types.rb +300 -0
  73. data/lib/aspera/preview/generator.rb +258 -0
  74. data/lib/aspera/preview/image_error.png +0 -0
  75. data/lib/aspera/preview/options.rb +35 -0
  76. data/lib/aspera/preview/utils.rb +131 -0
  77. data/lib/aspera/preview/video_error.png +0 -0
  78. data/lib/aspera/proxy_auto_config.erb.js +287 -0
  79. data/lib/aspera/proxy_auto_config.rb +34 -0
  80. data/lib/aspera/rest.rb +296 -0
  81. data/lib/aspera/rest_call_error.rb +13 -0
  82. data/lib/aspera/rest_error_analyzer.rb +98 -0
  83. data/lib/aspera/rest_errors_aspera.rb +58 -0
  84. data/lib/aspera/ssh.rb +53 -0
  85. data/lib/aspera/sync.rb +82 -0
  86. data/lib/aspera/temp_file_manager.rb +37 -0
  87. data/lib/aspera/uri_reader.rb +25 -0
  88. metadata +288 -0
@@ -0,0 +1,169 @@
1
+ require 'aspera/cli/plugins/node'
2
+ require 'xmlsimple'
3
+
4
+ module Aspera
5
+ module Cli
6
+ module Plugins
7
+ class Orchestrator < BasicAuthPlugin
8
+ def initialize(env)
9
+ super(env)
10
+ self.options.add_opt_simple(:params,"parameters hash table, use @json:{\"param\":\"value\"}")
11
+ self.options.add_opt_simple(:result,"specify result value as: 'work step:parameter'")
12
+ self.options.add_opt_boolean(:synchronous,"work step:parameter expected as result")
13
+ self.options.add_opt_list(:ret_style,[:header,:arg,:ext],'how return type is requested in api')
14
+ self.options.add_opt_list(:auth_style,[:arg_pass,:head_basic,:apikey],'authentication type')
15
+ self.options.set_option(:params,{})
16
+ self.options.set_option(:synchronous,:no)
17
+ self.options.set_option(:ret_style,:arg)
18
+ self.options.set_option(:auth_style,:head_basic)
19
+ self.options.parse_options!
20
+ end
21
+
22
+ ACTIONS=[:info, :workflow, :plugins, :processes]
23
+
24
+ # for JSON format: add extension ".json" or add url parameter: format=json or Accept: application/json
25
+ # id can be: a parameter id=x, or at the end of url /id, for workflows: work_order[workflow_id]=wf_id
26
+ def call_API_orig(endpoint,id=nil,url_params={:format=>:json},accept=nil)
27
+ # calls are GET
28
+ call_args={:operation=>'GET',:subpath=>endpoint}
29
+ # specify id if necessary
30
+ call_args[:subpath]=call_args[:subpath]+'/'+id unless id.nil?
31
+ unless url_params.nil?
32
+ if url_params.has_key?(:format)
33
+ call_args[:headers]={'Accept'=>'application/'+url_params[:format].to_s}
34
+ end
35
+ call_args[:headers]={'Accept'=>accept} unless accept.nil?
36
+ # add params if necessary
37
+ call_args[:url_params]=url_params
38
+ end
39
+ return @api_orch.call(call_args)
40
+ end
41
+
42
+ def call_API(endpoint,opt={})
43
+ opt[:prefix]='api' unless opt.has_key?(:prefix)
44
+ # calls are GET
45
+ call_args={:operation=>'GET',:subpath=>endpoint}
46
+ # specify prefix if necessary
47
+ call_args[:subpath]="#{opt[:prefix]}/#{call_args[:subpath]}" unless opt[:prefix].nil?
48
+ # specify id if necessary
49
+ call_args[:subpath]="#{call_args[:subpath]}/#{opt[:id]}" if opt.has_key?(:id)
50
+ call_type=self.options.get_option(:ret_style,:mandatory)
51
+ call_type=opt[:ret_style] if opt.has_key?(:ret_style)
52
+ format='json'
53
+ format=opt[:format] if opt.has_key?(:format)
54
+ call_args[:url_params]=opt[:args] unless opt[:args].nil?
55
+ unless format.nil?
56
+ case call_type
57
+ when :header
58
+ call_args[:headers]={'Accept'=>'application/'+format}
59
+ when :arg
60
+ call_args[:url_params]||={}
61
+ call_args[:url_params][:format]=format
62
+ when :ext
63
+ call_args[:subpath]="#{call_args[:subpath]}.#{format}"
64
+ else raise "unexpected"
65
+ end
66
+ end
67
+ result=@api_orch.call(call_args)
68
+ result[:data]=XmlSimple.xml_in(result[:http].body, opt[:xml_opt]||{"ForceArray" => true}) if format.eql?('xml')
69
+ return result
70
+ end
71
+
72
+ def execute_action
73
+ rest_params={:base_url => self.options.get_option(:url,:mandatory)}
74
+ case self.options.get_option(:auth_style,:mandatory)
75
+ when :arg_pass
76
+ rest_params[:auth]={
77
+ :type => :url,
78
+ :url_creds => {
79
+ 'login' =>self.options.get_option(:username,:mandatory),
80
+ 'password' =>self.options.get_option(:password,:mandatory) }}
81
+ when :head_basic
82
+ rest_params[:auth]={
83
+ :type => :basic,
84
+ :username =>self.options.get_option(:username,:mandatory),
85
+ :password =>self.options.get_option(:password,:mandatory) }
86
+ when :apikey
87
+ raise "Not implemented"
88
+ end
89
+
90
+ @api_orch=Rest.new(rest_params)
91
+
92
+ command1=self.options.get_next_command(ACTIONS)
93
+ case command1
94
+ when :info
95
+ result=call_API('remote_node_ping',format: 'xml', xml_opt: {"ForceArray" => false})
96
+ return {:type=>:single_object,:data=>result[:data]}
97
+ # result=call_API('workflows',prefix: nil,format: nil)
98
+ # version='unknown'
99
+ # if m=result[:http].body.match(/\(Orchestrator v([1-9]+\.[\.0-9a-f\-]+)\)/)
100
+ # version=m[1]
101
+ # end
102
+ # return {:type=>:single_object,:data=>{'version'=>version}}
103
+ when :processes
104
+ # TODO: Jira ? API has only XML format
105
+ result=call_API('processes_status',format: 'xml')
106
+ return {:type=>:object_list,:data=>result[:data]['process']}
107
+ when :plugins
108
+ # TODO: Jira ? only json format on url
109
+ result=call_API('plugin_version')[:data]
110
+ return {:type=>:object_list,:data=>result['Plugin']}
111
+ when :workflow
112
+ command=self.options.get_next_command([:list, :status, :inputs, :details, :start, :export])
113
+ unless [:list, :status].include?(command)
114
+ wf_id=self.options.get_option(:id,:mandatory)
115
+ end
116
+ case command
117
+ when :status
118
+ result=call_API('workflows_status')[:data]
119
+ return {:type=>:object_list,:data=>result['workflows']['workflow']}
120
+ when :list
121
+ result=call_API('workflows_list',id: 0)[:data]
122
+ return {:type=>:object_list,:data=>result['workflows']['workflow'],:fields=>["id","portable_id","name","published_status","published_revision_id","latest_revision_id","last_modification"]}
123
+ when :details
124
+ result=call_API('workflow_details',id: wf_id)[:data]
125
+ return {:type=>:object_list,:data=>result['workflows']['workflow']['statuses']}
126
+ when :inputs
127
+ result=call_API('workflow_inputs_spec',id: wf_id)[:data]
128
+ return {:type=>:single_object,:data=>result['workflow_inputs_spec']}
129
+ when :export
130
+ result=call_API('export_workflow',id: wf_id,format: nil)[:http]
131
+ return {:type=>:text,:data=>result.body}
132
+ when :start
133
+ result={
134
+ :type=>:single_object,
135
+ :data=>nil
136
+ }
137
+ call_params={:format=>:json}
138
+ override_accept=nil
139
+ # set external parameters if any
140
+ self.options.get_option(:params,:mandatory).each do |name,value|
141
+ call_params["external_parameters[#{name}]"] = value
142
+ end
143
+ # synchronous call ?
144
+ call_params['synchronous']=true if self.options.get_option(:synchronous,:mandatory)
145
+ # expected result for synchro call ?
146
+ expected=self.options.get_option(:result,:optional)
147
+ unless expected.nil?
148
+ result[:type] = :status
149
+ fields=expected.split(/:/)
150
+ raise "Expects: work_step:result_name format, but got #{expected}" if fields.length != 2
151
+ call_params['explicit_output_step']=fields[0]
152
+ call_params['explicit_output_variable']=fields[1]
153
+ # implicitely, call is synchronous
154
+ call_params['synchronous']=true
155
+ end
156
+ if call_params['synchronous']
157
+ result[:type]=:text
158
+ override_accept='text/plain'
159
+ end
160
+ result[:data]=call_API('initiate',id: wf_id,args: call_params,accept: override_accept)[:data]
161
+ return result
162
+ end # wf command
163
+ else raise "ERROR, unknown command: [#{command}]"
164
+ end # case command
165
+ end # execute_action
166
+ end # Orchestrator
167
+ end # Plugins
168
+ end # Cli
169
+ end # Aspera
@@ -0,0 +1,464 @@
1
+ require 'aspera/cli/basic_auth_plugin'
2
+ require 'aspera/preview/generator'
3
+ require 'aspera/preview/options'
4
+ require 'aspera/preview/utils'
5
+ require 'aspera/preview/file_types'
6
+ require 'aspera/persistency_action_once'
7
+ require 'aspera/hash_ext'
8
+ require 'date'
9
+ require 'securerandom'
10
+
11
+ module Aspera
12
+ module Cli
13
+ module Plugins
14
+ class Preview < BasicAuthPlugin
15
+ # special tag to identify transfers related to generator
16
+ PREV_GEN_TAG='preview_generator'
17
+ # defined by node API: suffix for folder containing previews
18
+ PREVIEW_FOLDER_SUFFIX='.asp-preview'
19
+ # basename of preview files
20
+ PREVIEW_BASENAME='preview'
21
+ # subfolder in system tmp folder
22
+ TMP_DIR_PREFIX='prev_tmp'
23
+ DEFAULT_PREVIEWS_FOLDER='previews'
24
+ AK_MARKER_FILE='.aspera_access_key'
25
+ LOCAL_STORAGE_PCVL='file:///'
26
+ private_constant :PREV_GEN_TAG, :PREVIEW_FOLDER_SUFFIX, :PREVIEW_BASENAME, :TMP_DIR_PREFIX, :DEFAULT_PREVIEWS_FOLDER, :LOCAL_STORAGE_PCVL, :AK_MARKER_FILE
27
+
28
+ # option_skip_format has special accessors
29
+ attr_accessor :option_previews_folder
30
+ attr_accessor :option_folder_reset_cache
31
+ attr_accessor :option_skip_folders
32
+ attr_accessor :option_overwrite
33
+ attr_accessor :option_file_access
34
+ def initialize(env)
35
+ super(env)
36
+ @skip_types=[]
37
+ @default_transfer_spec=nil
38
+ # by default generate all supported formats (clone, as altered by options)
39
+ @preview_formats_to_generate=Aspera::Preview::Generator::PREVIEW_FORMATS.clone
40
+ # options for generation
41
+ @gen_options=Aspera::Preview::Options.new
42
+ # link CLI options to gen_info attributes
43
+ self.options.set_obj_attr(:skip_format,self,:option_skip_format,[]) # no skip
44
+ self.options.set_obj_attr(:folder_reset_cache,self,:option_folder_reset_cache,:no)
45
+ self.options.set_obj_attr(:skip_types,self,:option_skip_types)
46
+ self.options.set_obj_attr(:previews_folder,self,:option_previews_folder,DEFAULT_PREVIEWS_FOLDER)
47
+ self.options.set_obj_attr(:skip_folders,self,:option_skip_folders,[]) # no skip
48
+ self.options.set_obj_attr(:overwrite,self,:option_overwrite,:mtime)
49
+ self.options.set_obj_attr(:file_access,self,:option_file_access,:local)
50
+ self.options.add_opt_list(:skip_format,Aspera::Preview::Generator::PREVIEW_FORMATS,'skip this preview format (multiple possible)')
51
+ self.options.add_opt_list(:folder_reset_cache,[:no,:header,:read],'force detection of generated preview by refresh cache')
52
+ self.options.add_opt_simple(:skip_types,'skip types in comma separated list')
53
+ self.options.add_opt_simple(:previews_folder,'preview folder in storage root')
54
+ self.options.add_opt_simple(:temp_folder,'path to temp folder')
55
+ self.options.add_opt_simple(:skip_folders,'list of folder to skip')
56
+ self.options.add_opt_simple(:case,'basename of output for for test')
57
+ self.options.add_opt_simple(:scan_path,'subpath in folder id to start scan in (default=/)')
58
+ self.options.add_opt_simple(:scan_id,'forder id in storage to start scan in, default is access key main folder id')
59
+ self.options.add_opt_list(:overwrite,[:always,:never,:mtime],'when to overwrite result file')
60
+ self.options.add_opt_list(:file_access,[:local,:remote],'how to read and write files in repository')
61
+ self.options.set_option(:temp_folder,Dir.tmpdir)
62
+
63
+ # add other options for generator (and set default values)
64
+ Aspera::Preview::Options::DESCRIPTIONS.each do |opt|
65
+ self.options.set_obj_attr(opt[:name],@gen_options,opt[:name],opt[:default])
66
+ if opt.has_key?(:values)
67
+ self.options.add_opt_list(opt[:name],opt[:values],opt[:description])
68
+ elsif [:yes,:no].include?(opt[:default])
69
+ self.options.add_opt_boolean(opt[:name],opt[:description])
70
+ else
71
+ self.options.add_opt_simple(opt[:name],opt[:description])
72
+ end
73
+ end
74
+
75
+ self.options.parse_options!
76
+ raise 'skip_folder shall be an Array, use @json:[...]' unless @option_skip_folders.is_a?(Array)
77
+ @tmp_folder=File.join(self.options.get_option(:temp_folder,:mandatory),"#{TMP_DIR_PREFIX}.#{SecureRandom.uuid}")
78
+ FileUtils.mkdir_p(@tmp_folder)
79
+ Log.log.debug("tmpdir: #{@tmp_folder}")
80
+ end
81
+
82
+ def option_skip_types=(value)
83
+ @skip_types=[]
84
+ value.split(',').each do |v|
85
+ s=v.to_sym
86
+ raise "not supported: #{v}" unless Aspera::Preview::FileTypes::CONVERSION_TYPES.include?(s)
87
+ @skip_types.push(s)
88
+ end
89
+ end
90
+
91
+ def option_skip_types
92
+ return @skip_types.map{|i|i.to_s}.join(',')
93
+ end
94
+
95
+ def option_skip_format=(value)
96
+ @preview_formats_to_generate.delete(value)
97
+ end
98
+
99
+ def option_skip_format
100
+ return @preview_formats_to_generate.map{|i|i.to_s}.join(',')
101
+ end
102
+
103
+ ACTIONS=[:scan,:events,:trevents,:check,:test]
104
+
105
+ # /files/id/files is normally cached in redis, but we can discard the cache
106
+ # but /files/id is not cached
107
+ def get_folder_entries(file_id,request_args=nil)
108
+ headers={'Accept'=>'application/json'}
109
+ headers.merge!({'X-Aspera-Cache-Control'=>'no-cache'}) if @option_folder_reset_cache.eql?(:header)
110
+ return @api_node.call({:operation=>'GET',:subpath=>"files/#{file_id}/files",:headers=>headers,:url_params=>request_args})[:data]
111
+ #return @api_node.read("files/#{file_id}/files",request_args)[:data]
112
+ end
113
+
114
+ # old version based on folders
115
+ def process_transfer_events(iteration_token)
116
+ events_filter={
117
+ 'access_key'=>@access_key_self['id'],
118
+ 'type'=>'download.ended'
119
+ }
120
+ # optionally by iteration token
121
+ events_filter['iteration_token']=iteration_token unless iteration_token.nil?
122
+ events=@api_node.read('events',events_filter)[:data]
123
+ return if events.empty?
124
+ events.each do |event|
125
+ next unless event['data']['direction'].eql?('receive')
126
+ next unless event['data']['status'].eql?('completed')
127
+ next unless event['data']['error_code'].eql?(0)
128
+ next unless event['data'].dig('tags','aspera',PREV_GEN_TAG).nil?
129
+ folder_id=event.dig('data','tags','aspera','node','file_id')
130
+ folder_id||=event.dig('data','file_id')
131
+ next if folder_id.nil?
132
+ folder_entry=@api_node.read("files/#{folder_id}")[:data] rescue nil
133
+ next if folder_entry.nil?
134
+ scan_folder_files(folder_entry)
135
+ end
136
+ return events.last['id'].to_s
137
+ end
138
+
139
+ # requests recent events on node api and process newly modified folders
140
+ def process_file_events(iteration_token)
141
+ # get new file creation by access key (TODO: what if file already existed?)
142
+ events_filter={
143
+ 'access_key'=>@access_key_self['id'],
144
+ 'type'=>'file.*'
145
+ }
146
+ # and optionally by iteration token
147
+ events_filter['iteration_token']=iteration_token unless iteration_token.nil?
148
+ events=@api_node.read('events',events_filter)[:data]
149
+ return if events.empty?
150
+ events.each do |event|
151
+ # process only files
152
+ next unless event.dig('data','type').eql?('file')
153
+ file_entry=@api_node.read("files/#{event['data']['id']}")[:data] rescue nil
154
+ next if file_entry.nil?
155
+ next unless @option_skip_folders.select{|d|file_entry['path'].start_with?(d)}.empty?
156
+ file_entry['parent_file_id']=event['data']['parent_file_id']
157
+ if event['types'].include?('file.deleted')
158
+ Log.log.error('TODO'.red)
159
+ end
160
+ if event['types'].include?('file.deleted')
161
+ generate_preview(file_entry)
162
+ end
163
+ end
164
+ # write new iteration file
165
+ return events.last['id'].to_s
166
+ end
167
+
168
+ def do_transfer(direction,folder_id,source_filename,destination='/')
169
+ raise "error" if destination.nil? and direction.eql?('receive')
170
+ if @default_transfer_spec.nil?
171
+ # make a dummy call to get some default transfer parameters
172
+ res=@api_node.create('files/upload_setup',{'transfer_requests'=>[{'transfer_request'=>{'paths'=>[{}],'destination_root'=>'/'}}]})
173
+ sample_transfer_spec=res[:data]['transfer_specs'].first['transfer_spec']
174
+ # get ports, anyway that should be 33001 for both. add remote_user ?
175
+ @default_transfer_spec=['ssh_port','fasp_port'].inject({}){|h,e|h[e]=sample_transfer_spec[e];h}
176
+ # note: we use the same address for ascp than for node api instead of the one from upload_setup
177
+ @default_transfer_spec.merge!({
178
+ 'token' => "Basic #{Base64.strict_encode64("#{@access_key_self['id']}:#{self.options.get_option(:password,:mandatory)}")}",
179
+ 'remote_host' => @transfer_server_address,
180
+ 'remote_user' => Fasp::ACCESS_KEY_TRANSFER_USER
181
+ })
182
+ end
183
+ tspec=@default_transfer_spec.merge({
184
+ 'direction' => direction,
185
+ 'paths' => [{'source'=>source_filename}],
186
+ 'tags' => { 'aspera' => {
187
+ PREV_GEN_TAG => true,
188
+ 'node' => {
189
+ 'access_key' => @access_key_self['id'],
190
+ 'file_id' => folder_id }}}
191
+ })
192
+ # force destination
193
+ # tspec['destination_root']=destination
194
+ self.transfer.option_transfer_spec_deep_merge({'destination_root'=>destination})
195
+ Main.result_transfer(self.transfer.start(tspec,{:src=>:node_gen4}))
196
+ end
197
+
198
+ def get_infos_local(gen_infos,entry,local_entry_preview_dir)
199
+ local_original_filepath=File.join(@local_storage_root,entry['path'])
200
+ original_mtime=File.mtime(local_original_filepath)
201
+ # out
202
+ local_entry_preview_dir.replace(File.join(@local_preview_folder, entry_preview_folder_name(entry)))
203
+ gen_infos.each do |gen_info|
204
+ gen_info[:src]=local_original_filepath
205
+ gen_info[:dst]=File.join(local_entry_preview_dir, gen_info[:base_dest])
206
+ gen_info[:preview_exist]=File.exist?(gen_info[:dst])
207
+ gen_info[:preview_newer_than_original] = (gen_info[:preview_exist] and (File.mtime(gen_info[:dst])>original_mtime))
208
+ end
209
+ end
210
+
211
+ def get_infos_remote(gen_infos,entry,local_entry_preview_dir)
212
+ #Log.log.debug(">>>> get_infos_remote #{entry}".red)
213
+ # store source directly here
214
+ local_original_filepath=File.join(@tmp_folder,entry['name'])
215
+ #original_mtime=DateTime.parse(entry['modified_time'])
216
+ # out: where previews are generated
217
+ local_entry_preview_dir.replace(File.join(@tmp_folder,entry_preview_folder_name(entry)))
218
+ file_info=@api_node.read("files/#{entry['id']}")[:data]
219
+ #TODO: this does not work because previews is hidden in api (gen4)
220
+ #this_preview_folder_entries=get_folder_entries(@previews_folder_entry['id'],{:name=>@entry_preview_folder_name})
221
+ # TODO: use gen3 api to list files and get date
222
+ gen_infos.each do |gen_info|
223
+ gen_info[:src]=local_original_filepath
224
+ gen_info[:dst]=File.join(local_entry_preview_dir, gen_info[:base_dest])
225
+ # TODO: use this_preview_folder_entries (but it's hidden)
226
+ gen_info[:preview_exist]=file_info.has_key?('preview')
227
+ # TODO: get change time and compare, useful ?
228
+ gen_info[:preview_newer_than_original] = gen_info[:preview_exist]
229
+ end
230
+ end
231
+
232
+ # defined by node api
233
+ def entry_preview_folder_name(entry)
234
+ "#{entry['id']}#{PREVIEW_FOLDER_SUFFIX}"
235
+ end
236
+
237
+ def preview_filename(preview_format,filename=nil)
238
+ filename||=PREVIEW_BASENAME
239
+ return "#{filename}.#{preview_format.to_s}"
240
+ end
241
+
242
+ # generate preview files for one folder entry (file) if necessary
243
+ # entry must contain "parent_file_id" if remote.
244
+ def generate_preview(entry)
245
+ #Log.log.debug(">>>> #{entry}".red)
246
+ # folder where previews will be generated for this particular entry
247
+ local_entry_preview_dir=String.new
248
+ # prepare generic information
249
+ gen_infos=@preview_formats_to_generate.map do |preview_format|
250
+ {
251
+ :preview_format => preview_format,
252
+ :base_dest => preview_filename(preview_format)
253
+ }
254
+ end
255
+ # lets gather some infos on possibly existing previews
256
+ # it depends if files access locally or remotely
257
+ if @access_remote
258
+ get_infos_remote(gen_infos,entry,local_entry_preview_dir)
259
+ else # direct local file system access
260
+ get_infos_local(gen_infos,entry,local_entry_preview_dir)
261
+ end
262
+ # here we have the status on preview files
263
+ # let's find if they need generation
264
+ gen_infos.select! do |gen_info|
265
+ # if it exists, what about overwrite policy ?
266
+ if gen_info[:preview_exist]
267
+ case @option_overwrite
268
+ when :always
269
+ # continue: generate
270
+ when :never
271
+ # never overwrite
272
+ next false
273
+ when :mtime
274
+ # skip if preview is newer than original
275
+ next false if gen_info[:preview_newer_than_original]
276
+ end
277
+ end
278
+ # need generator for further checks
279
+ gen_info[:generator]=Aspera::Preview::Generator.new(@gen_options,gen_info[:src],gen_info[:dst],@tmp_folder,entry['content_type'],false)
280
+ # get conversion_type (if known) and check if supported
281
+ next false unless gen_info[:generator].supported?
282
+ # shall we skip it ?
283
+ next false if @skip_types.include?(gen_info[:generator].conversion_type)
284
+ # ok we need to generate
285
+ true
286
+ end
287
+ return if gen_infos.empty?
288
+ # create folder if needed
289
+ FileUtils.mkdir_p(local_entry_preview_dir)
290
+ if @access_remote
291
+ raise 'missing parent_file_id in entry' if entry['parent_file_id'].nil?
292
+ # download original file to temp folder
293
+ do_transfer('receive',entry['parent_file_id'],entry['name'],@tmp_folder)
294
+ end
295
+ Log.log.info("source: #{entry['id']}: #{entry['path']})")
296
+ gen_infos.each do |gen_info|
297
+ gen_info[:generator].generate rescue nil
298
+ end
299
+ if @access_remote
300
+ # upload
301
+ do_transfer('send',@previews_folder_entry['id'],local_entry_preview_dir)
302
+ # cleanup after upload
303
+ FileUtils.rm_rf(local_entry_preview_dir)
304
+ File.delete(File.join(@tmp_folder,entry['name']))
305
+ end
306
+ # force read file updated previews
307
+ if @option_folder_reset_cache.eql?(:read)
308
+ @api_node.read("files/#{entry['id']}")
309
+ end
310
+ rescue => e
311
+ Log.log.error("#{e.message}")
312
+ Log.log.debug(e.backtrace.join("\n").red)
313
+ end # generate_preview
314
+
315
+ # scan all files in provided folder entry
316
+ def scan_folder_files(top_entry,scan_start=nil)
317
+ if !scan_start.nil?
318
+ # canonical path: start with / and ends with /
319
+ scan_start='/'+scan_start.split('/').select{|i|!i.empty?}.join('/')
320
+ scan_start="#{scan_start}/" #unless scan_start.end_with?('/')
321
+ end
322
+ Log.log.debug("scan: #{top_entry} : #{scan_start}".green)
323
+ # don't use recursive call, use list instead
324
+ entries_to_process=[top_entry]
325
+ while !entries_to_process.empty?
326
+ entry=entries_to_process.shift
327
+ # process this entry only if it is within the scan_start
328
+ entry_path_with_slash=entry['path']
329
+ entry_path_with_slash="#{entry_path_with_slash}/" unless entry_path_with_slash.end_with?('/')
330
+ if !scan_start.nil? and !scan_start.start_with?(entry_path_with_slash) and !entry_path_with_slash.start_with?(scan_start)
331
+ Log.log.debug("#{entry['path']} folder (skip start)".bg_red)
332
+ next
333
+ end
334
+ Log.log.debug("item:#{entry}")
335
+ case entry['type']
336
+ when 'file'
337
+ generate_preview(entry)
338
+ when 'link'
339
+ Log.log.debug('Ignoring link.')
340
+ when 'folder'
341
+ if @option_skip_folders.include?(entry['path'])
342
+ Log.log.debug("#{entry['path']} folder (skip list)".bg_red)
343
+ else
344
+ Log.log.debug("#{entry['path']} folder".green)
345
+ # get folder content
346
+ folder_entries=get_folder_entries(entry['id'])
347
+ # process all items in current folder
348
+ folder_entries.each do |folder_entry|
349
+ # add path for older versions of ES
350
+ if !folder_entry.has_key?('path')
351
+ folder_entry['path']=entry_path_with_slash+folder_entry['name']
352
+ end
353
+ folder_entry['parent_file_id']=entry['id']
354
+ entries_to_process.push(folder_entry)
355
+ end
356
+ end
357
+ else
358
+ Log.log.warn("unknown entry type: #{entry['type']}")
359
+ end
360
+ end
361
+ end
362
+
363
+ def execute_action
364
+ command=self.options.get_next_command(ACTIONS)
365
+ unless [:check,:test].include?(command)
366
+ # this will use node api
367
+ @api_node=basic_auth_api
368
+ @transfer_server_address=URI.parse(@api_node.params[:base_url]).host
369
+ # get current access key
370
+ @access_key_self=@api_node.read('access_keys/self')[:data]
371
+ # TODO: check events is activated here:
372
+ # note that docroot is good to look at as well
373
+ node_info=@api_node.read('info')[:data]
374
+ Log.log.debug("root: #{node_info['docroot']}")
375
+ @access_remote=@option_file_access.eql?(:remote)
376
+ Log.log.debug("remote: #{@access_remote}")
377
+ Log.log.debug("access key info: #{@access_key_self}")
378
+ #TODO: can the previews folder parameter be read from node api ?
379
+ @option_skip_folders.push('/'+@option_previews_folder)
380
+ if @access_remote
381
+ # note the filter "name", it's why we take the first one
382
+ @previews_folder_entry=get_folder_entries(@access_key_self['root_file_id'],{:name=>@option_previews_folder}).first
383
+ raise CliError,"Folder #{@option_previews_folder} does not exist on node. Please create it in the storage root, or specify an alternate name." if @previews_folder_entry.nil?
384
+ else
385
+ raise "only local storage allowed in this mode" unless @access_key_self['storage']['type'].eql?('local')
386
+ @local_storage_root=@access_key_self['storage']['path']
387
+ #TODO: option to override @local_storage_root='xxx'
388
+ @local_storage_root=@local_storage_root[LOCAL_STORAGE_PCVL.length..-1] if @local_storage_root.start_with?(LOCAL_STORAGE_PCVL)
389
+ #TODO: windows could have "C:" ?
390
+ raise "not local storage: #{@local_storage_root}" unless @local_storage_root.start_with?('/')
391
+ raise CliError,"Local storage root folder #{@local_storage_root} does not exist." unless File.directory?(@local_storage_root)
392
+ @local_preview_folder=File.join(@local_storage_root,@option_previews_folder)
393
+ raise CliError,"Folder #{@local_preview_folder} does not exist locally. Please create it, or specify an alternate name." unless File.directory?(@local_preview_folder)
394
+ # protection to avoid clash of file id for two different access keys
395
+ marker_file=File.join(@local_preview_folder,AK_MARKER_FILE)
396
+ Log.log.debug("marker file: #{marker_file}")
397
+ if File.exist?(marker_file)
398
+ ak=File.read(marker_file)
399
+ raise "mismatch access key in #{marker_file}: contains #{ak}, using #{@access_key_self['id']}" unless @access_key_self['id'].eql?(ak)
400
+ else
401
+ File.write(marker_file,@access_key_self['id'])
402
+ end
403
+ end
404
+ end
405
+ case command
406
+ when :scan
407
+ scan_path=self.options.get_option(:scan_path,:optional)
408
+ scan_id=self.options.get_option(:scan_id,:optional)
409
+ # by default start at root
410
+ folder_info=if scan_id.nil?
411
+ { 'id' => @access_key_self['root_file_id'],
412
+ 'name' => '/',
413
+ 'type' => 'folder',
414
+ 'path' => '/' }
415
+ else
416
+ @api_node.read("files/#{scan_id}")[:data]
417
+ end
418
+ scan_folder_files(folder_info,scan_path)
419
+ return Main.result_status('scan finished')
420
+ when :events
421
+ iteration_data=[]
422
+ iteration_persistency=nil
423
+ if self.options.get_option(:once_only,:mandatory)
424
+ iteration_persistency=PersistencyActionOnce.new(
425
+ manager: @agents[:persistency],
426
+ data: iteration_data,
427
+ ids: ['preview_iteration_events',self.options.get_option(:url,:mandatory),self.options.get_option(:username,:mandatory)])
428
+ end
429
+ iteration_data[0]=process_file_events(iteration_data[0])
430
+ iteration_persistency.save unless iteration_persistency.nil?
431
+ return Main.result_status('events finished')
432
+ when :trevents
433
+ iteration_data=[]
434
+ iteration_persistency=nil
435
+ if self.options.get_option(:once_only,:mandatory)
436
+ iteration_persistency=PersistencyActionOnce.new(
437
+ manager: @agents[:persistency],
438
+ data: iteration_data,
439
+ ids: ['preview_iteration_transfer',self.options.get_option(:url,:mandatory),self.options.get_option(:username,:mandatory)])
440
+ end
441
+ iteration_data[0]=process_transfer_events(iteration_data[0])
442
+ iteration_persistency.save unless iteration_persistency.nil?
443
+ return Main.result_status('trevents finished')
444
+ when :check
445
+ Aspera::Preview::Utils.check_tools(@skip_types)
446
+ return Main.result_status('tools validated')
447
+ when :test
448
+ format = self.options.get_next_argument('format',Aspera::Preview::Generator::PREVIEW_FORMATS)
449
+ source = self.options.get_next_argument('source file')
450
+ dest=preview_filename(format,self.options.get_option(:case,:optional))
451
+ g=Aspera::Preview::Generator.new(@gen_options,source,dest,@tmp_folder)
452
+ raise "format not supported: #{format}" unless g.supported?
453
+ g.generate
454
+ return Main.result_status("generated: #{dest}")
455
+ else
456
+ raise "error"
457
+ end
458
+ ensure
459
+ FileUtils.rm_rf(@tmp_folder)
460
+ end # execute_action
461
+ end # Preview
462
+ end # Plugins
463
+ end # Cli
464
+ end # Aspera