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 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: