etna 0.1.28 → 0.1.29
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 +4 -4
- data/lib/etna/clients/magma/workflows/create_project_workflow.rb +1 -1
- data/lib/etna/clients/magma/workflows/crud_workflow.rb +9 -0
- data/lib/etna/clients/magma/workflows/materialize_magma_record_files_workflow.rb +43 -28
- data/lib/etna/clients/magma/workflows/walk_model_tree_workflow.rb +7 -2
- data/lib/etna/clients/metis/client.rb +1 -1
- data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +15 -11
- data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +83 -13
- data/lib/etna/clients/metis/workflows/sync_metis_data_workflow.rb +43 -79
- data/lib/etna/cwl.rb +4 -0
- data/lib/etna/directed_graph.rb +1 -1
- data/lib/etna/filesystem.rb +143 -15
- data/lib/etna/spec/auth.rb +6 -6
- data/lib/etna/user.rb +2 -6
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d4c9465edd6fd280a9cd91b9c3abbc3ef08114a46e3ffb41e14a20ce612fe7f
|
4
|
+
data.tar.gz: bfd29114b8db331c92f79be1fe765fa6b64c7b5ee4a8543cd75f6c9fe7c38a84
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ea7b21a563d743cdb6f8a06f92ca15fdeb56ecb291b27ff32a555a8f863cd6ce4400fc6954ae5620a9766bbafbed861609f52309f4de3d407f117d4f9b3f1b1
|
7
|
+
data.tar.gz: 6a9530ee4beb8e6a6d7940c30a2a2bc7412dc4420492e650cb731eac4e5e958a4ca630a966aede4591706ae79212e3290b1a080b5fafe5549df24cc302cbc82b
|
@@ -70,7 +70,7 @@ module Etna
|
|
70
70
|
puts "Creating Janus project."
|
71
71
|
create_janus_project!
|
72
72
|
puts "Done! Adding you as an administrator on the project."
|
73
|
-
add_janus_user(user['email'], "#{user['
|
73
|
+
add_janus_user(user['email'], "#{user['name']}", 'editor')
|
74
74
|
promote_to_administrator(user['email'])
|
75
75
|
update_magma_client_token!
|
76
76
|
|
@@ -36,8 +36,17 @@ module Etna
|
|
36
36
|
documents = Documents.new({})
|
37
37
|
last_page = nil
|
38
38
|
while last_page.nil? || last_page.models.model_keys.map { |k| last_page.models.model(k).documents.raw.length }.sum > 0
|
39
|
+
attempts = 0
|
39
40
|
begin
|
41
|
+
attempts += 1
|
40
42
|
last_page = magma_client.retrieve(request)
|
43
|
+
# Unfortunately, paging in magma is not great and times out from time to time.
|
44
|
+
rescue Net::ReadTimeout => e
|
45
|
+
if attempts > 5
|
46
|
+
raise e
|
47
|
+
end
|
48
|
+
|
49
|
+
retry
|
41
50
|
rescue Etna::Error => e
|
42
51
|
raise e unless e.message.include?('not found')
|
43
52
|
break
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'ostruct'
|
2
|
-
require 'digest'
|
3
2
|
require 'fileutils'
|
4
3
|
require 'tempfile'
|
5
4
|
|
@@ -9,11 +8,11 @@ module Etna
|
|
9
8
|
class MaterializeDataWorkflow < Struct.new(
|
10
9
|
:metis_client, :magma_client, :project_name,
|
11
10
|
:model_name, :model_filters, :model_attributes_mask,
|
12
|
-
:filesystem, :logger, :stub_files,
|
13
|
-
:
|
11
|
+
:filesystem, :logger, :stub_files, :concurrency,
|
12
|
+
:record_names, keyword_init: true)
|
14
13
|
|
15
14
|
def initialize(**kwds)
|
16
|
-
super(**({filesystem: Etna::Filesystem.new}.update(kwds)))
|
15
|
+
super(**({filesystem: Etna::Filesystem.new, concurrency: 10, record_names: "all"}.update(kwds)))
|
17
16
|
end
|
18
17
|
|
19
18
|
def magma_crud
|
@@ -25,31 +24,47 @@ module Etna
|
|
25
24
|
end
|
26
25
|
|
27
26
|
def materialize_all(dest)
|
28
|
-
|
27
|
+
templates = {}
|
28
|
+
|
29
|
+
semaphore = Concurrent::Semaphore.new(concurrency)
|
30
|
+
errors = Queue.new
|
31
|
+
|
32
|
+
model_walker.walk_from(
|
33
|
+
model_name,
|
34
|
+
record_names,
|
35
|
+
model_attributes_mask: model_attributes_mask,
|
36
|
+
model_filters: model_filters,
|
37
|
+
page_size: 500,
|
38
|
+
) do |template, document|
|
39
|
+
logger&.info("Materializing #{template.name}##{document[template.identifier]}")
|
40
|
+
templates[template.name] = template
|
41
|
+
|
42
|
+
begin
|
43
|
+
if (error = errors.pop(true))
|
44
|
+
raise error
|
45
|
+
end
|
46
|
+
rescue ThreadError
|
47
|
+
end
|
29
48
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
49
|
+
semaphore.acquire
|
50
|
+
Thread.new do
|
51
|
+
begin
|
52
|
+
materialize_record(dest, template, document)
|
53
|
+
rescue => e
|
54
|
+
errors << e
|
55
|
+
ensure
|
56
|
+
semaphore.release
|
57
|
+
end
|
38
58
|
end
|
39
|
-
ensure
|
40
|
-
filesystem.rm_rf(tmpdir) unless skip_tmpdir
|
41
59
|
end
|
42
|
-
end
|
43
60
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
template = model.template
|
50
|
-
model.documents.document_keys.each do |key|
|
51
|
-
yield template, model.documents.document(key)
|
61
|
+
semaphore.acquire(concurrency)
|
62
|
+
|
63
|
+
begin
|
64
|
+
if (error = errors.pop(true))
|
65
|
+
raise error
|
52
66
|
end
|
67
|
+
rescue ThreadError
|
53
68
|
end
|
54
69
|
end
|
55
70
|
|
@@ -78,11 +93,10 @@ module Etna
|
|
78
93
|
@sync_metis_data_workflow ||= Etna::Clients::Metis::SyncMetisDataWorkflow.new(
|
79
94
|
metis_client: metis_client,
|
80
95
|
logger: logger,
|
81
|
-
skip_tmpdir: skip_tmpdir,
|
82
96
|
filesystem: filesystem)
|
83
97
|
end
|
84
98
|
|
85
|
-
def materialize_record(dest_dir,
|
99
|
+
def materialize_record(dest_dir, template, record)
|
86
100
|
record_to_serialize = record.dup
|
87
101
|
|
88
102
|
each_file(template, record) do |attr_name, url, filename, idx|
|
@@ -91,13 +105,14 @@ module Etna
|
|
91
105
|
end
|
92
106
|
|
93
107
|
dest_file = File.join(dest_dir, metadata_file_name(record_name: record[template.identifier], record_model_name: template.name, ext: "_#{attr_name}_#{idx}#{File.extname(filename)}"))
|
94
|
-
sync_metis_data_workflow.copy_file(
|
95
|
-
record_to_serialize[attr_name] << {
|
108
|
+
sync_metis_data_workflow.copy_file(dest: dest_file, url: url, stub: stub_files)
|
109
|
+
record_to_serialize[attr_name] << {file: dest_file, original_filename: filename}
|
96
110
|
end
|
97
111
|
|
98
112
|
dest_file = File.join(dest_dir, metadata_file_name(record_name: record[template.identifier], record_model_name: template.name, ext: '.json'))
|
99
113
|
filesystem.mkdir_p(File.dirname(dest_file))
|
100
114
|
json = record_to_serialize.to_json
|
115
|
+
|
101
116
|
filesystem.with_writeable(dest_file, "w", size_hint: json.bytes.length) do |io|
|
102
117
|
io.write(json)
|
103
118
|
end
|
@@ -7,6 +7,10 @@ module Etna
|
|
7
7
|
module Clients
|
8
8
|
class Magma
|
9
9
|
class WalkModelTreeWorkflow < Struct.new(:magma_crud, :logger, keyword_init: true)
|
10
|
+
def initialize(**args)
|
11
|
+
super(**({}.update(args)))
|
12
|
+
end
|
13
|
+
|
10
14
|
def walk_from(
|
11
15
|
model_name,
|
12
16
|
record_names = 'all',
|
@@ -43,7 +47,8 @@ module Etna
|
|
43
47
|
|
44
48
|
template.attributes.attribute_keys.each do |attr_name|
|
45
49
|
attributes_mask = model_attributes_mask[model_name]
|
46
|
-
|
50
|
+
black_listed = !attributes_mask.nil? && !attributes_mask.include?(attr_name)
|
51
|
+
next if black_listed && attr_name != template.identifier && attr_name != 'parent'
|
47
52
|
attributes << attr_name
|
48
53
|
|
49
54
|
attr = template.attributes.attribute(attr_name)
|
@@ -58,7 +63,7 @@ module Etna
|
|
58
63
|
elsif attr.attribute_type == AttributeType::CHILD
|
59
64
|
related_models[attr.link_model_name] ||= Set.new
|
60
65
|
links << attr_name
|
61
|
-
elsif attr.attribute_type == AttributeType::PARENT
|
66
|
+
elsif attr.attribute_type == AttributeType::PARENT && !black_listed
|
62
67
|
related_models[attr.link_model_name] ||= Set.new
|
63
68
|
links << attr_name
|
64
69
|
end
|
@@ -13,23 +13,27 @@ module Etna
|
|
13
13
|
end
|
14
14
|
|
15
15
|
# TODO: Might be possible to use range headers to select and resume downloads on failure in the future.
|
16
|
-
def do_download(
|
16
|
+
def do_download(dest_file_or_io, metis_file, &block)
|
17
17
|
size = metis_file.size
|
18
18
|
completed = 0.0
|
19
19
|
start = Time.now
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
completed += chunk.size
|
25
|
-
|
26
|
-
block.call([
|
27
|
-
:progress,
|
28
|
-
size == 0 ? 1 : completed / size,
|
29
|
-
(completed / (Time.now - start)).round(2),
|
30
|
-
]) unless block.nil?
|
21
|
+
unless dest_file_or_io.is_a?(IO)
|
22
|
+
::File.open(dest_file_or_io, 'w') do |io|
|
23
|
+
return do_download(dest_file_or_io, metis_file, &block)
|
31
24
|
end
|
32
25
|
end
|
26
|
+
|
27
|
+
metis_client.download_file(metis_file) do |chunk|
|
28
|
+
dest_file_or_io.write chunk
|
29
|
+
completed += chunk.size
|
30
|
+
|
31
|
+
block.call([
|
32
|
+
:progress,
|
33
|
+
size == 0 ? 1 : completed / size,
|
34
|
+
(completed / (Time.now - start)).round(2),
|
35
|
+
]) unless block.nil?
|
36
|
+
end
|
33
37
|
end
|
34
38
|
end
|
35
39
|
end
|
@@ -4,17 +4,26 @@ require 'fileutils'
|
|
4
4
|
require 'tempfile'
|
5
5
|
require 'securerandom'
|
6
6
|
|
7
|
+
$digest_mutx = Mutex.new
|
8
|
+
|
7
9
|
module Etna
|
8
10
|
module Clients
|
9
11
|
class Metis
|
10
12
|
class MetisUploadWorkflow < Struct.new(:metis_client, :metis_uid, :project_name, :bucket_name, :max_attempts, keyword_init: true)
|
13
|
+
class StreamingUploadError < StandardError
|
14
|
+
end
|
15
|
+
|
11
16
|
|
12
17
|
def initialize(args)
|
13
18
|
super({max_attempts: 3, metis_uid: SecureRandom.hex}.update(args))
|
14
19
|
end
|
15
20
|
|
16
|
-
def do_upload(
|
17
|
-
|
21
|
+
def do_upload(source_file_or_upload, dest_path, &block)
|
22
|
+
unless source_file_or_upload.is_a?(Upload)
|
23
|
+
upload = Upload.new(source_file: source_file_or_upload)
|
24
|
+
else
|
25
|
+
upload = source_file_or_upload
|
26
|
+
end
|
18
27
|
|
19
28
|
dir = ::File.dirname(dest_path)
|
20
29
|
metis_client.create_folder(CreateFolderRequest.new(
|
@@ -68,9 +77,11 @@ module Etna
|
|
68
77
|
|
69
78
|
unsent_zero_byte_file = false
|
70
79
|
rescue Etna::Error => e
|
71
|
-
|
72
|
-
|
73
|
-
|
80
|
+
unless block.nil?
|
81
|
+
m = yield [:error, e]
|
82
|
+
if m == false
|
83
|
+
raise e
|
84
|
+
end
|
74
85
|
end
|
75
86
|
|
76
87
|
if e.status == 422
|
@@ -90,13 +101,17 @@ module Etna
|
|
90
101
|
end
|
91
102
|
end
|
92
103
|
|
93
|
-
class Upload
|
104
|
+
class Upload
|
94
105
|
INITIAL_BLOB_SIZE = 2 ** 10
|
95
106
|
MAX_BLOB_SIZE = 2 ** 22
|
96
107
|
ZERO_HASH = 'd41d8cd98f00b204e9800998ecf8427e'
|
97
108
|
|
98
|
-
|
99
|
-
|
109
|
+
attr_accessor :source_file, :next_blob_size, :current_byte_position
|
110
|
+
|
111
|
+
def initialize(source_file: nil, next_blob_size: nil, current_byte_position: nil)
|
112
|
+
self.source_file = source_file
|
113
|
+
self.next_blob_size = next_blob_size
|
114
|
+
self.current_byte_position = current_byte_position
|
100
115
|
self.next_blob_size = [file_size, INITIAL_BLOB_SIZE].min
|
101
116
|
self.current_byte_position = 0
|
102
117
|
end
|
@@ -108,10 +123,10 @@ module Etna
|
|
108
123
|
def advance_position!
|
109
124
|
self.current_byte_position = self.current_byte_position + self.next_blob_size
|
110
125
|
self.next_blob_size = [
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
126
|
+
MAX_BLOB_SIZE,
|
127
|
+
# in fact we should stop when we hit the end of the file
|
128
|
+
file_size - current_byte_position
|
129
|
+
].min
|
115
130
|
end
|
116
131
|
|
117
132
|
def complete?
|
@@ -119,7 +134,14 @@ module Etna
|
|
119
134
|
end
|
120
135
|
|
121
136
|
def next_blob_hash
|
122
|
-
|
137
|
+
bytes = next_blob_bytes
|
138
|
+
if bytes.empty?
|
139
|
+
return ZERO_HASH
|
140
|
+
end
|
141
|
+
|
142
|
+
$digest_mutx.synchronize do
|
143
|
+
return Digest::MD5.hexdigest(bytes)
|
144
|
+
end
|
123
145
|
end
|
124
146
|
|
125
147
|
def next_blob_bytes
|
@@ -131,6 +153,54 @@ module Etna
|
|
131
153
|
self.next_blob_size = upload_response.next_blob_size
|
132
154
|
end
|
133
155
|
end
|
156
|
+
|
157
|
+
class StreamingIOUpload < Upload
|
158
|
+
def initialize(readable_io:, size_hint: 0, **args)
|
159
|
+
@readable_io = readable_io
|
160
|
+
@size_hint = size_hint
|
161
|
+
@read_position = 0
|
162
|
+
@last_bytes = ""
|
163
|
+
super(**args)
|
164
|
+
end
|
165
|
+
|
166
|
+
def file_size
|
167
|
+
@size_hint
|
168
|
+
end
|
169
|
+
|
170
|
+
def next_blob_bytes
|
171
|
+
next_left = current_byte_position
|
172
|
+
next_right = current_byte_position + next_blob_size
|
173
|
+
|
174
|
+
if next_right < @read_position
|
175
|
+
raise StreamingUploadError.new("Upload needs restart, but source is streaming and ephemeral. #{next_right} #{@read_position} You need to restart the source stream and create a new upload.")
|
176
|
+
elsif @read_position < next_left
|
177
|
+
# read from the stream and discard until we are positioned for the next read.
|
178
|
+
data = @readable_io.read(next_left - @read_position)
|
179
|
+
raise StreamingUploadError.new("Unexpected EOF in read stream") if data.nil?
|
180
|
+
|
181
|
+
@read_position += data.bytes.length
|
182
|
+
end
|
183
|
+
|
184
|
+
# If we have consumed all requested data, return what we have consumed.
|
185
|
+
# If we have requested no data, make sure to provide "" as the result.
|
186
|
+
if next_right == @read_position
|
187
|
+
return @last_bytes
|
188
|
+
end
|
189
|
+
|
190
|
+
if @read_position != next_left
|
191
|
+
raise StreamingUploadError.new("Alignment error, source data does not match expected upload resume. #{@read_position} #{next_left} Restart the upload to address.")
|
192
|
+
end
|
193
|
+
|
194
|
+
@last_bytes = "".tap do |bytes|
|
195
|
+
while @read_position < next_right
|
196
|
+
bytes << @readable_io.read(next_right - @read_position).tap do |data|
|
197
|
+
raise StreamingUploadError.new("Unexpected EOF in read stream") if data.nil?
|
198
|
+
@read_position += data.bytes.length
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
134
204
|
end
|
135
205
|
end
|
136
206
|
end
|
@@ -1,107 +1,71 @@
|
|
1
1
|
require 'ostruct'
|
2
|
-
require 'digest'
|
3
2
|
require 'fileutils'
|
4
3
|
require 'tempfile'
|
5
4
|
|
6
5
|
module Etna
|
7
6
|
module Clients
|
8
7
|
class Metis
|
9
|
-
class SyncMetisDataWorkflow < Struct.new(:metis_client, :filesystem, :project_name, :bucket_name,
|
10
|
-
|
11
|
-
|
12
|
-
own_tmpdir = tmpdir.nil? && !skip_tmpdir
|
13
|
-
if own_tmpdir
|
14
|
-
tmpdir = filesystem.tmpdir
|
15
|
-
end
|
16
|
-
|
17
|
-
begin
|
18
|
-
response = metis_client.list_folder(ListFolderRequest.new(project_name: project_name, bucket_name: bucket_name, folder_path: src))
|
8
|
+
class SyncMetisDataWorkflow < Struct.new(:metis_client, :filesystem, :project_name, :bucket_name, :logger, keyword_init: true)
|
9
|
+
def copy_directory(src, dest, root = dest)
|
10
|
+
response = metis_client.list_folder(ListFolderRequest.new(project_name: project_name, bucket_name: bucket_name, folder_path: src))
|
19
11
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
response.folders.all.each do |folder|
|
26
|
-
copy_directory(::File.join(src, folder.folder_name), ::File.join(dest, folder.folder_name), root, tmpdir)
|
27
|
-
end
|
28
|
-
ensure
|
29
|
-
filesystem.rm_rf(tmpdir) if own_tmpdir
|
12
|
+
response.files.all.each do |file|
|
13
|
+
logger&.info("Copying file #{file.file_path} (#{Etna::Formatting.as_size(file.size)})")
|
14
|
+
copy_file(dest: ::File.join(dest, file.file_name), url: file.download_url)
|
30
15
|
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def bin_file_name(etag:)
|
34
|
-
"bin/#{etag}"
|
35
|
-
end
|
36
16
|
|
37
|
-
|
38
|
-
|
39
|
-
if tmpdir.nil?
|
40
|
-
yield dest_file_name
|
41
|
-
else
|
42
|
-
tmp_file = ::File.join(tmpdir, ::File.basename(bin_file_name))
|
43
|
-
yield tmp_file
|
44
|
-
filesystem.mv(tmp_file, dest_file_name)
|
17
|
+
response.folders.all.each do |folder|
|
18
|
+
copy_directory(::File.join(src, folder.folder_name), ::File.join(dest, folder.folder_name), root)
|
45
19
|
end
|
46
20
|
end
|
47
21
|
|
48
|
-
def copy_file(
|
22
|
+
def copy_file(dest:, url:, stub: false)
|
49
23
|
metadata = metis_client.file_metadata(url)
|
50
|
-
etag = metadata[:etag]
|
51
24
|
size = metadata[:size]
|
52
25
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
upload_amount
|
73
|
-
|
74
|
-
if upload_timings.length > 150
|
75
|
-
s, _ = upload_timings.shift
|
76
|
-
upload_amount -= s
|
77
|
-
end
|
26
|
+
tmp_file = dest
|
27
|
+
upload_timings = []
|
28
|
+
upload_amount = 0
|
29
|
+
last_rate = 0.00001
|
30
|
+
remaining = size
|
31
|
+
|
32
|
+
filesystem.with_writeable(tmp_file, "w", size_hint: size) do |io|
|
33
|
+
if stub
|
34
|
+
io.write("(stub) #{size} bytes")
|
35
|
+
else
|
36
|
+
metis_client.download_file(url) do |chunk|
|
37
|
+
io.write(chunk)
|
38
|
+
|
39
|
+
upload_timings << [chunk.length, Time.now.to_f]
|
40
|
+
upload_amount += chunk.length
|
41
|
+
remaining -= chunk.length
|
42
|
+
|
43
|
+
if upload_timings.length > 150
|
44
|
+
s, _ = upload_timings.shift
|
45
|
+
upload_amount -= s
|
46
|
+
end
|
78
47
|
|
79
|
-
|
80
|
-
|
48
|
+
_, start_time = upload_timings.first
|
49
|
+
_, end_time = upload_timings.last
|
81
50
|
|
82
|
-
|
83
|
-
|
84
|
-
|
51
|
+
if start_time == end_time
|
52
|
+
next
|
53
|
+
end
|
85
54
|
|
86
|
-
|
55
|
+
rate = upload_amount / (end_time - start_time)
|
87
56
|
|
88
|
-
|
89
|
-
|
57
|
+
if rate / last_rate > 1.3 || rate / last_rate < 0.7
|
58
|
+
logger&.info("Uploading #{Etna::Formatting.as_size(rate)} per second, #{Etna::Formatting.as_size(remaining)} remaining")
|
90
59
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
end
|
60
|
+
if rate == 0
|
61
|
+
last_rate = 0.0001
|
62
|
+
else
|
63
|
+
last_rate = rate
|
96
64
|
end
|
97
65
|
end
|
98
66
|
end
|
99
|
-
end
|
100
|
-
end
|
101
67
|
|
102
|
-
|
103
|
-
filesystem.with_writeable(dest_bin_file, 'w', size_hint: 0) do |io|
|
104
|
-
# empty file marking that this etag has been moved, to save a future write.
|
68
|
+
end
|
105
69
|
end
|
106
70
|
end
|
107
71
|
end
|
data/lib/etna/cwl.rb
CHANGED
data/lib/etna/directed_graph.rb
CHANGED
@@ -23,7 +23,7 @@ class DirectedGraph
|
|
23
23
|
parents_of_map = descendants(root)
|
24
24
|
seen = Set.new
|
25
25
|
|
26
|
-
parents_of_map.to_a.sort_by { |k, parents| [-parents.length, k] }.each do |k, parents|
|
26
|
+
parents_of_map.to_a.sort_by { |k, parents| [-parents.length, k.inspect] }.each do |k, parents|
|
27
27
|
unless seen.include?(k)
|
28
28
|
if @children[k].keys.empty?
|
29
29
|
result << parents + [k]
|
data/lib/etna/filesystem.rb
CHANGED
@@ -1,36 +1,52 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
require 'fileutils'
|
3
3
|
require 'open3'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'concurrent-ruby'
|
4
6
|
|
5
7
|
module Etna
|
6
8
|
# A class that encapsulates opening / reading file system entries that abstracts normal file access in order
|
7
9
|
# to make stubbing, substituting, and testing easier.
|
8
10
|
class Filesystem
|
9
11
|
def with_writeable(dest, opts = 'w', size_hint: nil, &block)
|
12
|
+
raise "with_writeable not supported by #{self.class.name}" unless self.class == Filesystem
|
10
13
|
::File.open(dest, opts, &block)
|
11
14
|
end
|
12
15
|
|
16
|
+
def ls(dir)
|
17
|
+
raise "ls not supported by #{self.class.name}" unless self.class == Filesystem
|
18
|
+
::Dir.entries(dir).select { |p| !p.start_with?('.') }.map do |path|
|
19
|
+
::File.file?(::File.join(dir, path)) ? [:file, path] : [:dir, path]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
13
23
|
def with_readable(src, opts = 'r', &block)
|
24
|
+
raise "with_readable not supported by #{self.class.name}" unless self.class == Filesystem
|
14
25
|
::File.open(src, opts, &block)
|
15
26
|
end
|
16
27
|
|
17
28
|
def mkdir_p(dir)
|
29
|
+
raise "mkdir_p not supported by #{self.class.name}" unless self.class == Filesystem
|
18
30
|
::FileUtils.mkdir_p(dir)
|
19
31
|
end
|
20
32
|
|
21
33
|
def rm_rf(dir)
|
34
|
+
raise "rm_rf not supported by #{self.class.name}" unless self.class == Filesystem
|
22
35
|
::FileUtils.rm_rf(dir)
|
23
36
|
end
|
24
37
|
|
25
38
|
def tmpdir
|
39
|
+
raise "tmpdir not supported by #{self.class.name}" unless self.class == Filesystem
|
26
40
|
::Dir.mktmpdir
|
27
41
|
end
|
28
42
|
|
29
43
|
def exist?(src)
|
44
|
+
raise "exist? not supported by #{self.class.name}" unless self.class == Filesystem
|
30
45
|
::File.exist?(src)
|
31
46
|
end
|
32
47
|
|
33
48
|
def mv(src, dest)
|
49
|
+
raise "mv not supported by #{self.class.name}" unless self.class == Filesystem
|
34
50
|
::FileUtils.mv(src, dest)
|
35
51
|
end
|
36
52
|
|
@@ -179,7 +195,7 @@ module Etna
|
|
179
195
|
cmd << remote_path
|
180
196
|
cmd << local_path
|
181
197
|
|
182
|
-
cmd << {
|
198
|
+
cmd << {out: wd}
|
183
199
|
elsif opts.include?('w')
|
184
200
|
cmd << '--mode=send'
|
185
201
|
cmd << "--host=#{@host}"
|
@@ -187,7 +203,7 @@ module Etna
|
|
187
203
|
cmd << local_path
|
188
204
|
cmd << remote_path
|
189
205
|
|
190
|
-
cmd << {
|
206
|
+
cmd << {in: rd}
|
191
207
|
end
|
192
208
|
|
193
209
|
cmd
|
@@ -196,23 +212,10 @@ module Etna
|
|
196
212
|
|
197
213
|
# Genentech's aspera deployment doesn't support modern commands, unfortunately...
|
198
214
|
class GeneAsperaCliFilesystem < AsperaCliFilesystem
|
199
|
-
def tmpdir
|
200
|
-
raise "tmpdir is not supported"
|
201
|
-
end
|
202
|
-
|
203
|
-
def rm_rf
|
204
|
-
raise "rm_rf is not supported"
|
205
|
-
end
|
206
|
-
|
207
215
|
def mkdir_p(dest)
|
208
216
|
# Pass through -- this file system creates containing directories by default, womp womp.
|
209
217
|
end
|
210
218
|
|
211
|
-
def mv
|
212
|
-
raise "mv is not supported"
|
213
|
-
end
|
214
|
-
|
215
|
-
|
216
219
|
def mkcommand(rd, wd, file, opts, size_hint: nil)
|
217
220
|
if opts.include?('w')
|
218
221
|
super.map do |e|
|
@@ -241,6 +244,131 @@ module Etna
|
|
241
244
|
end
|
242
245
|
end
|
243
246
|
|
247
|
+
class Metis < Filesystem
|
248
|
+
def initialize(metis_client:, project_name:, bucket_name:, root: '/', uuid: SecureRandom.uuid)
|
249
|
+
@metis_client = metis_client
|
250
|
+
@project_name = project_name
|
251
|
+
@bucket_name = bucket_name
|
252
|
+
@root = root
|
253
|
+
@metis_uid = uuid
|
254
|
+
end
|
255
|
+
|
256
|
+
def metis_path_of(path)
|
257
|
+
joined = ::File.join(@root, path)
|
258
|
+
joined[0] == "/" ? joined.slice(1..-1) : joined
|
259
|
+
end
|
260
|
+
|
261
|
+
def create_upload_workflow
|
262
|
+
Etna::Clients::Metis::MetisUploadWorkflow.new(metis_client: @metis_client, metis_uid: @metis_uid, project_name: @project_name, bucket_name: @bucket_name, max_attempts: 3)
|
263
|
+
end
|
264
|
+
|
265
|
+
def with_hot_pipe(opts, receiver, *args, &block)
|
266
|
+
rp, wp = IO.pipe
|
267
|
+
begin
|
268
|
+
executor = Concurrent::SingleThreadExecutor.new(fallback_policy: :abort)
|
269
|
+
begin
|
270
|
+
if opts.include?('w')
|
271
|
+
future = Concurrent::Promises.future_on(executor) do
|
272
|
+
self.send(receiver, rp, *args)
|
273
|
+
rescue => e
|
274
|
+
Etna::Application.instance.logger.log_error(e)
|
275
|
+
raise e
|
276
|
+
ensure
|
277
|
+
rp.close
|
278
|
+
end
|
279
|
+
|
280
|
+
yield wp
|
281
|
+
else
|
282
|
+
future = Concurrent::Promises.future_on(executor) do
|
283
|
+
self.send(receiver, wp, *args)
|
284
|
+
rescue => e
|
285
|
+
Etna::Application.instance.logger.log_error(e)
|
286
|
+
raise e
|
287
|
+
ensure
|
288
|
+
wp.close
|
289
|
+
end
|
290
|
+
|
291
|
+
yield rp
|
292
|
+
end
|
293
|
+
|
294
|
+
future.wait!
|
295
|
+
ensure
|
296
|
+
executor.shutdown
|
297
|
+
executor.kill unless executor.wait_for_termination(5)
|
298
|
+
end
|
299
|
+
ensure
|
300
|
+
rp.close
|
301
|
+
wp.close
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def do_streaming_upload(rp, dest, size_hint)
|
306
|
+
streaming_upload = Etna::Clients::Metis::MetisUploadWorkflow::StreamingIOUpload.new(readable_io: rp, size_hint: size_hint)
|
307
|
+
create_upload_workflow.do_upload(
|
308
|
+
streaming_upload,
|
309
|
+
metis_path_of(dest)
|
310
|
+
)
|
311
|
+
end
|
312
|
+
|
313
|
+
def with_writeable(dest, opts = 'w', size_hint: nil, &block)
|
314
|
+
self.with_hot_pipe(opts, :do_streaming_upload, dest, size_hint) do |wp|
|
315
|
+
yield wp
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def create_download_workflow
|
320
|
+
Etna::Clients::Metis::MetisDownloadWorkflow.new(metis_client: @metis_client, project_name: @project_name, bucket_name: @bucket_name, max_attempts: 3)
|
321
|
+
end
|
322
|
+
|
323
|
+
def do_streaming_download(wp, metis_file)
|
324
|
+
create_download_workflow.do_download(wp, metis_file)
|
325
|
+
end
|
326
|
+
|
327
|
+
def with_readable(src, opts = 'r', &block)
|
328
|
+
metis_file = list_metis_directory(::File.dirname(src)).files.all.find { |f| f.file_name == ::File.basename(src) }
|
329
|
+
raise "Metis file at #{@project_name}/#{@bucket_name}/#{@root}/#{src} not found. No such file" if metis_file.nil?
|
330
|
+
|
331
|
+
self.with_hot_pipe(opts, :do_streaming_download, metis_file) do |rp|
|
332
|
+
yield rp
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def list_metis_directory(path)
|
337
|
+
@metis_client.list_folder(Etna::Clients::Metis::ListFolderRequest.new(project_name: @project_name, bucket_name: @bucket_name, folder_path: metis_path_of(path)))
|
338
|
+
end
|
339
|
+
|
340
|
+
def mkdir_p(dir)
|
341
|
+
create_folder_request = Etna::Clients::Metis::CreateFolderRequest.new(
|
342
|
+
project_name: @project_name,
|
343
|
+
bucket_name: @bucket_name,
|
344
|
+
folder_path: metis_path_of(dir),
|
345
|
+
)
|
346
|
+
@metis_client.create_folder(create_folder_request)
|
347
|
+
end
|
348
|
+
|
349
|
+
def ls(dir)
|
350
|
+
response = list_metis_directory(::File.dirname(dir))
|
351
|
+
response.files.map { |f| [:file, f.file_name] } + response.folders.map { |f| [:folder, f.folder_name] }
|
352
|
+
end
|
353
|
+
|
354
|
+
def exist?(src)
|
355
|
+
begin
|
356
|
+
response = list_metis_directory(::File.dirname(src))
|
357
|
+
rescue Etna::Error => e
|
358
|
+
if e.status == 404
|
359
|
+
return false
|
360
|
+
elsif e.message =~ /Invalid folder/
|
361
|
+
return false
|
362
|
+
end
|
363
|
+
|
364
|
+
raise e
|
365
|
+
end
|
366
|
+
|
367
|
+
response.files.all.any? { |f| f.file_name == ::File.basename(src) } ||
|
368
|
+
response.folders.all.any? { |f| f.folder_name == ::File.basename(src) }
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
244
372
|
class Mock < Filesystem
|
245
373
|
def initialize(&new_io)
|
246
374
|
@files = {}
|
data/lib/etna/spec/auth.rb
CHANGED
@@ -2,22 +2,22 @@ module Etna::Spec
|
|
2
2
|
module Auth
|
3
3
|
AUTH_USERS = {
|
4
4
|
superuser: {
|
5
|
-
email: 'zeus@olympus.org',
|
5
|
+
email: 'zeus@olympus.org', name: 'Zeus', perm: 'A:administration'
|
6
6
|
},
|
7
7
|
admin: {
|
8
|
-
email: 'hera@olympus.org',
|
8
|
+
email: 'hera@olympus.org', name: 'Hera', perm: 'a:labors'
|
9
9
|
},
|
10
10
|
editor: {
|
11
|
-
email: 'eurystheus@twelve-labors.org',
|
11
|
+
email: 'eurystheus@twelve-labors.org', name: 'Eurystheus', perm: 'E:labors'
|
12
12
|
},
|
13
13
|
restricted_editor: {
|
14
|
-
email: 'copreus@twelve-labors.org',
|
14
|
+
email: 'copreus@twelve-labors.org', name: 'Copreus', perm: 'e:labors'
|
15
15
|
},
|
16
16
|
viewer: {
|
17
|
-
email: 'hercules@twelve-labors.org',
|
17
|
+
email: 'hercules@twelve-labors.org', name: 'Hercules', perm: 'v:labors'
|
18
18
|
},
|
19
19
|
non_user: {
|
20
|
-
email: 'nessus@centaurs.org',
|
20
|
+
email: 'nessus@centaurs.org', name: 'Nessus', perm: ''
|
21
21
|
}
|
22
22
|
}
|
23
23
|
|
data/lib/etna/user.rb
CHANGED
@@ -7,14 +7,14 @@ module Etna
|
|
7
7
|
}
|
8
8
|
|
9
9
|
def initialize params, token=nil
|
10
|
-
@
|
10
|
+
@name, @email, @encoded_permissions, encoded_flags = params.values_at(:name, :email, :perm, :flags)
|
11
11
|
|
12
12
|
@flags = encoded_flags&.split(/;/) || []
|
13
13
|
@token = token unless !token
|
14
14
|
raise ArgumentError, "No email given!" unless @email
|
15
15
|
end
|
16
16
|
|
17
|
-
attr_reader :
|
17
|
+
attr_reader :name, :email, :token
|
18
18
|
|
19
19
|
def permissions
|
20
20
|
@permissions ||= @encoded_permissions.split(/\;/).map do |roles|
|
@@ -36,10 +36,6 @@ module Etna
|
|
36
36
|
@flags.include?(flag)
|
37
37
|
end
|
38
38
|
|
39
|
-
def name
|
40
|
-
"#{first} #{last}"
|
41
|
-
end
|
42
|
-
|
43
39
|
def projects
|
44
40
|
permissions.keys
|
45
41
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: etna
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.29
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Saurabh Asthana
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-02-
|
11
|
+
date: 2021-02-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -80,6 +80,34 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: concurrent-ruby
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: concurrent-ruby-ext
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
83
111
|
description: See summary
|
84
112
|
email: Saurabh.Asthana@ucsf.edu
|
85
113
|
executables:
|