etna 0.1.28 → 0.1.29

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f72e476ca58afe56ac2d2334ae35dfe159409892f54bda2624b73d83da574309
4
- data.tar.gz: 89e4ccce963ca79bddcd668da5817c94d95ce609612d2d34a635707588dfe88a
3
+ metadata.gz: 8d4c9465edd6fd280a9cd91b9c3abbc3ef08114a46e3ffb41e14a20ce612fe7f
4
+ data.tar.gz: bfd29114b8db331c92f79be1fe765fa6b64c7b5ee4a8543cd75f6c9fe7c38a84
5
5
  SHA512:
6
- metadata.gz: 76f01865ad042215703f305f1ceaf7eeee4febacb114490481e91c9cbbfbf83bedb004d725d18f90d4ffa34efb8ed91741c91e0244fb60f704fba7e3f2e65057
7
- data.tar.gz: e8c352582c61d655881d8290a3be01a03cd27c4b3167d60bab077256a5222cfba94a7401130cff14582230fdcef2068498abaf66fda4176b0c3fc486b3cf202e
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['first']} #{user['last']}", 'editor')
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
- :skip_tmpdir, keyword_init: true)
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
- tmpdir = skip_tmpdir ? nil : filesystem.tmpdir
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
- begin
31
- model_walker.walk_from(
32
- model_name,
33
- model_attributes_mask: model_attributes_mask,
34
- model_filters: model_filters,
35
- ) do |template, document|
36
- logger&.info("Materializing #{template.name}##{document[template.identifier]}")
37
- materialize_record(dest, tmpdir, template, document)
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
- def each_root_record
45
- request = RetrievalRequest.new(project_name: project_name, model_name: model_name, record_names: "all",
46
- filter: filter, page_size: 100, page: 1)
47
- magma_crud.page_records(model_name, request) do |response|
48
- model = response.models.model(model_name)
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, tmpdir, template, record)
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(bin_root_dir: dest_dir, tmpdir: tmpdir, dest: dest_file, url: url, stub: stub_files)
95
- record_to_serialize[attr_name] << { file: dest_file, original_filename: filename }
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
- next if !attributes_mask.nil? && !attributes_mask.include?(attr_name) && attr_name != template.identifier
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
@@ -98,7 +98,7 @@ module Etna
98
98
  @etna_client.get(download_path) do |response|
99
99
  return {
100
100
  etag: response['ETag'].gsub(/"/, ''),
101
- size: response['Content-Length'],
101
+ size: response['Content-Length'].to_i,
102
102
  }
103
103
  end
104
104
  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(dest_file, metis_file, &block)
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
- ::File.open(dest_file, "w") do |io|
22
- metis_client.download_file(metis_file) do |chunk|
23
- io.write chunk
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(source_file, dest_path, &block)
17
- upload = Upload.new(source_file: source_file)
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
- m = yield [:error, e] unless block.nil?
72
- if m == false
73
- raise e
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 < Struct.new(:source_file, :next_blob_size, :current_byte_position, keyword_init: true)
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
- def initialize(**args)
99
- super
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
- MAX_BLOB_SIZE,
112
- # in fact we should stop when we hit the end of the file
113
- file_size - current_byte_position
114
- ].min
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
- Digest::MD5.hexdigest(next_blob_bytes)
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
- :logger, :skip_tmpdir, keyword_init: true)
11
- def copy_directory(src, dest, root = dest, tmpdir = nil)
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
- response.files.all.each do |file|
21
- logger&.info("Copying file #{file.file_path} (#{Etna::Formatting.as_size(file.size)})")
22
- copy_file(bin_root_dir: root, tmpdir: tmpdir, dest: ::File.join(dest, file.file_name), url: file.download_url)
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
- def with_maybe_intermediate_tmp_dest(bin_file_name:, tmpdir:, dest_file_name:, &block)
38
- filesystem.mkdir_p(::File.dirname(dest_file_name))
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(bin_root_dir:, tmpdir:, dest:, url:, stub: false)
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
- dest_bin_file = ::File.join(bin_root_dir, bin_file_name(etag: etag))
54
- # Already materialized, continue
55
- if filesystem.exist?(dest_bin_file)
56
- return
57
- end
58
-
59
- with_maybe_intermediate_tmp_dest(bin_file_name: dest_bin_file, tmpdir: tmpdir, dest_file_name: dest) do |tmp_file|
60
- upload_timings = []
61
- upload_amount = 0
62
- last_rate = 0.00001
63
-
64
- filesystem.with_writeable(tmp_file, "w", size_hint: size) do |io|
65
- if stub
66
- io.write("(stub) #{size} bytes")
67
- else
68
- metis_client.download_file(url) do |chunk|
69
- io.write(chunk)
70
-
71
- upload_timings << [chunk.length, Time.now.to_f]
72
- upload_amount += chunk.length
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
- _, start_time = upload_timings.first
80
- _, end_time = upload_timings.last
48
+ _, start_time = upload_timings.first
49
+ _, end_time = upload_timings.last
81
50
 
82
- if start_time == end_time
83
- next
84
- end
51
+ if start_time == end_time
52
+ next
53
+ end
85
54
 
86
- rate = upload_amount / (end_time - start_time)
55
+ rate = upload_amount / (end_time - start_time)
87
56
 
88
- if rate / last_rate > 1.3 || rate / last_rate < 0.7
89
- logger&.info("Uploading #{Etna::Formatting.as_size(rate)} per second")
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
- if rate == 0
92
- last_rate = 0.0001
93
- else
94
- last_rate = rate
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
- filesystem.mkdir_p(::File.dirname(dest_bin_file))
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
@@ -584,6 +584,10 @@ module Etna
584
584
  def default
585
585
  @attributes['default']
586
586
  end
587
+
588
+ def type
589
+ @attributes['type']
590
+ end
587
591
  end
588
592
 
589
593
  class StepOutput < Cwl
@@ -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]
@@ -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 << { out: wd }
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 << { in: rd }
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 = {}
@@ -2,22 +2,22 @@ module Etna::Spec
2
2
  module Auth
3
3
  AUTH_USERS = {
4
4
  superuser: {
5
- email: 'zeus@olympus.org', first: 'Zeus', perm: 'A:administration'
5
+ email: 'zeus@olympus.org', name: 'Zeus', perm: 'A:administration'
6
6
  },
7
7
  admin: {
8
- email: 'hera@olympus.org', first: 'Hera', perm: 'a:labors'
8
+ email: 'hera@olympus.org', name: 'Hera', perm: 'a:labors'
9
9
  },
10
10
  editor: {
11
- email: 'eurystheus@twelve-labors.org', first: 'Eurystheus', perm: 'E:labors'
11
+ email: 'eurystheus@twelve-labors.org', name: 'Eurystheus', perm: 'E:labors'
12
12
  },
13
13
  restricted_editor: {
14
- email: 'copreus@twelve-labors.org', first: 'Copreus', perm: 'e:labors'
14
+ email: 'copreus@twelve-labors.org', name: 'Copreus', perm: 'e:labors'
15
15
  },
16
16
  viewer: {
17
- email: 'hercules@twelve-labors.org', first: 'Hercules', perm: 'v:labors'
17
+ email: 'hercules@twelve-labors.org', name: 'Hercules', perm: 'v:labors'
18
18
  },
19
19
  non_user: {
20
- email: 'nessus@centaurs.org', first: 'Nessus', perm: ''
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
- @first, @last, @email, @encoded_permissions, encoded_flags = params.values_at(:first, :last, :email, :perm, :flags)
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 :first, :last, :email, :token
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.28
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-17 00:00:00.000000000 Z
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: