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.
- checksums.yaml +7 -0
- data/README.md +3592 -0
- data/bin/ascli +7 -0
- data/bin/asession +89 -0
- data/docs/Makefile +59 -0
- data/docs/README.erb.md +3012 -0
- data/docs/README.md +13 -0
- data/docs/diagrams.txt +49 -0
- data/docs/secrets.make +38 -0
- data/docs/test_env.conf +117 -0
- data/docs/transfer_spec.html +99 -0
- data/examples/aoc.rb +17 -0
- data/examples/proxy.pac +60 -0
- data/examples/transfer.rb +115 -0
- data/lib/aspera/api_detector.rb +60 -0
- data/lib/aspera/ascmd.rb +151 -0
- data/lib/aspera/ats_api.rb +43 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +38 -0
- data/lib/aspera/cli/extended_value.rb +88 -0
- data/lib/aspera/cli/formater.rb +238 -0
- data/lib/aspera/cli/listener/line_dump.rb +17 -0
- data/lib/aspera/cli/listener/logger.rb +20 -0
- data/lib/aspera/cli/listener/progress.rb +52 -0
- data/lib/aspera/cli/listener/progress_multi.rb +91 -0
- data/lib/aspera/cli/main.rb +304 -0
- data/lib/aspera/cli/manager.rb +440 -0
- data/lib/aspera/cli/plugin.rb +90 -0
- data/lib/aspera/cli/plugins/alee.rb +24 -0
- data/lib/aspera/cli/plugins/ats.rb +231 -0
- data/lib/aspera/cli/plugins/bss.rb +71 -0
- data/lib/aspera/cli/plugins/config.rb +806 -0
- data/lib/aspera/cli/plugins/console.rb +62 -0
- data/lib/aspera/cli/plugins/cos.rb +106 -0
- data/lib/aspera/cli/plugins/faspex.rb +377 -0
- data/lib/aspera/cli/plugins/faspex5.rb +93 -0
- data/lib/aspera/cli/plugins/node.rb +438 -0
- data/lib/aspera/cli/plugins/oncloud.rb +937 -0
- data/lib/aspera/cli/plugins/orchestrator.rb +169 -0
- data/lib/aspera/cli/plugins/preview.rb +464 -0
- data/lib/aspera/cli/plugins/server.rb +216 -0
- data/lib/aspera/cli/plugins/shares.rb +63 -0
- data/lib/aspera/cli/plugins/shares2.rb +114 -0
- data/lib/aspera/cli/plugins/sync.rb +65 -0
- data/lib/aspera/cli/plugins/xnode.rb +115 -0
- data/lib/aspera/cli/transfer_agent.rb +251 -0
- data/lib/aspera/cli/version.rb +5 -0
- data/lib/aspera/colors.rb +39 -0
- data/lib/aspera/command_line_builder.rb +137 -0
- data/lib/aspera/fasp/aoc.rb +24 -0
- data/lib/aspera/fasp/connect.rb +99 -0
- data/lib/aspera/fasp/error.rb +21 -0
- data/lib/aspera/fasp/error_info.rb +60 -0
- data/lib/aspera/fasp/http_gw.rb +81 -0
- data/lib/aspera/fasp/installation.rb +240 -0
- data/lib/aspera/fasp/listener.rb +11 -0
- data/lib/aspera/fasp/local.rb +377 -0
- data/lib/aspera/fasp/manager.rb +69 -0
- data/lib/aspera/fasp/node.rb +88 -0
- data/lib/aspera/fasp/parameters.rb +235 -0
- data/lib/aspera/fasp/resume_policy.rb +76 -0
- data/lib/aspera/fasp/uri.rb +51 -0
- data/lib/aspera/faspex_gw.rb +196 -0
- data/lib/aspera/hash_ext.rb +28 -0
- data/lib/aspera/log.rb +80 -0
- data/lib/aspera/nagios.rb +71 -0
- data/lib/aspera/node.rb +14 -0
- data/lib/aspera/oauth.rb +319 -0
- data/lib/aspera/on_cloud.rb +421 -0
- data/lib/aspera/open_application.rb +72 -0
- data/lib/aspera/persistency_action_once.rb +42 -0
- data/lib/aspera/persistency_folder.rb +91 -0
- data/lib/aspera/preview/file_types.rb +300 -0
- data/lib/aspera/preview/generator.rb +258 -0
- data/lib/aspera/preview/image_error.png +0 -0
- data/lib/aspera/preview/options.rb +35 -0
- data/lib/aspera/preview/utils.rb +131 -0
- data/lib/aspera/preview/video_error.png +0 -0
- data/lib/aspera/proxy_auto_config.erb.js +287 -0
- data/lib/aspera/proxy_auto_config.rb +34 -0
- data/lib/aspera/rest.rb +296 -0
- data/lib/aspera/rest_call_error.rb +13 -0
- data/lib/aspera/rest_error_analyzer.rb +98 -0
- data/lib/aspera/rest_errors_aspera.rb +58 -0
- data/lib/aspera/ssh.rb +53 -0
- data/lib/aspera/sync.rb +82 -0
- data/lib/aspera/temp_file_manager.rb +37 -0
- data/lib/aspera/uri_reader.rb +25 -0
- 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
|