labimotion 2.1.1 → 2.1.2

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: f544de920e1626452d5422b5dd60e29c65f6459c223c92c84176df619e8ddf2e
4
- data.tar.gz: 16f3da4ab02efa4ac7e4bd0848640b35df87e3d5115809543dde0279cb0e43a1
3
+ metadata.gz: 2be4b8936c18a5560a7aed2c4ca7ad3ea6f3fc17245b7442e3658255a0e84494
4
+ data.tar.gz: 88f5beed8cea656e69f5e9ba13aaa3acbe92dddce489a131e5238b09ca63cc87
5
5
  SHA512:
6
- metadata.gz: 356d0de559510fbce5b90eb65db22363c4163d6aabb8281753edf953086ac504b25eea8b9dd1ecf5412873f192027ef8e38b0a38394d62fb79e7e9aa8338a617
7
- data.tar.gz: 6e4fe1c4dba90988961c704671bc43ed57a310fc0fcf3c410b99e5797de3a6e247e82635ac94edd3d3f271b61091bc83230f57888a94decdc3254de6e73d1d53
6
+ metadata.gz: 98cfc7e3e0f672a3371a6fc523fd58de260e2cde2e83696be0976ad44c2efc79ebdf60fd414969b996cfa09f7d4f2c384168fd04503f0713c6ccdf4f5905da87
7
+ data.tar.gz: 22052eb592542f2e8a43368b6c08dac67e301352234f968ffc175a12d6af66133857876884b85ee1f35e449a4edcae3e37ff39c0233673f73323f9c7a39b8812
@@ -6,6 +6,7 @@ require 'json'
6
6
  require 'date'
7
7
  require 'labimotion/version'
8
8
  require 'labimotion/utils/utils'
9
+ require 'labimotion/utils/mapper_utils'
9
10
  require 'labimotion/helpers/converter_helpers'
10
11
 
11
12
  # rubocop: disable Metrics/AbcSize
@@ -20,27 +21,36 @@ module Labimotion
20
21
  end
21
22
 
22
23
  def self.process_ds(id, current_user = {})
23
- att = Attachment.find_by(id: id, con_state: Labimotion::ConState::CONVERTED)
24
+ ctx = { att: nil, ols: nil, dsr: [] }
25
+ # Savepoint: a failure leaves no half-built dataset and keeps the caller's transaction usable
26
+ Attachment.transaction(requires_new: true) { map_ds(id, current_user, ctx) }
27
+ rescue StandardError => e
28
+ Labimotion::Converter.logger.error ["Att ID: #{ctx[:att]&.id}, OLS: #{ctx[:ols]}", "DSR: #{ctx[:dsr]}", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR)
29
+ Labimotion.report_exception(e)
30
+ # Terminal state: do not fail the request, and do not retry on the next save
31
+ ctx[:att]&.update_column(:con_state, Labimotion::ConState::ERROR)
32
+ Labimotion::ConState::ERROR
33
+ end
34
+
35
+ def self.map_ds(id, current_user, ctx)
36
+ att = ctx[:att] = Attachment.find_by(id: id, con_state: Labimotion::ConState::CONVERTED)
24
37
  return if att.nil? || att.attachable_id.nil? || att.attachable_type != 'Container' || att.filename.split('.')&.last != 'zip'
25
38
 
26
39
  eln_ds = Container.find_by(id: att.attachable_id, container_type: 'dataset')
27
40
  return if eln_ds.nil? || eln_ds.parent.nil? || eln_ds.parent&.container_type != 'analysis'
28
41
 
29
- dsr = []
30
- ols = nil
31
- Zip::File.open(att.attachment_attacher.file.url) do |zip_file|
32
- res = Labimotion::Converter.collect_metadata(zip_file, current_user) if att.filename.split('.')&.last == 'zip'
33
- ols = res[:o] unless res&.dig(:o).nil?
34
- dsr.push(res[:d]) unless res&.dig(:d).nil?
35
- end
36
- dsr.flatten!
37
- dataset = build_ds(att.attachable_id, ols)
38
- update_ds(dataset, dsr, current_user) if dataset.present?
42
+ # Never close: closing can rewrite the stored file in place
43
+ zip_file = Zip::File.new(att.attachment_attacher.file.url)
44
+ res = Labimotion::Converter.collect_metadata(zip_file, current_user)
45
+ ctx[:ols] = res[:o] unless res&.dig(:o).nil?
46
+ ctx[:dsr].push(res[:d]) unless res&.dig(:d).nil?
47
+ ctx[:dsr].flatten!
48
+ dataset = build_ds(att.attachable_id, ctx[:ols])
49
+ update_ds(dataset, ctx[:dsr], current_user) if dataset.present?
39
50
  att.update_column(:con_state, Labimotion::ConState::COMPLETED)
40
- rescue StandardError => e
41
- Labimotion::Converter.logger.error ["Att ID: #{att&.id}, OLS: #{ols}", "DSR: #{dsr}", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR)
42
- raise e
51
+ Labimotion::ConState::COMPLETED
43
52
  end
53
+ private_class_method :map_ds
44
54
 
45
55
  def self.uri(api_name)
46
56
  url = Rails.configuration.converter.url
@@ -99,7 +109,7 @@ module Labimotion
99
109
  zip_file.each do |entry|
100
110
  next unless entry.name == 'metadata/converter.json'
101
111
 
102
- metadata = entry.get_input_stream.read.force_encoding('UTF-8')
112
+ metadata = Labimotion::MapperUtils.read_entry(entry).force_encoding('UTF-8')
103
113
  jdata = JSON.parse(metadata)
104
114
 
105
115
  ols = jdata['ols']
@@ -164,8 +174,9 @@ module Labimotion
164
174
  )
165
175
  end
166
176
  if response.ok?
167
- Labimotion::Converter.handle_response(data[:a], response, current_user)
168
- Labimotion::ConState::PROCESSED
177
+ state = Labimotion::Converter.handle_response(data[:a], response, current_user)
178
+ # A failed dataset mapping also marks the source attachment as ERROR
179
+ state == Labimotion::ConState::ERROR ? state : Labimotion::ConState::PROCESSED
169
180
  else
170
181
  Labimotion::Converter.logger.error ["Converter Response Error: id: [#{data[:a]&.id}], filename: [#{data[:a]&.filename}], response: #{response}"].join($INPUT_RECORD_SEPARATOR)
171
182
  Labimotion::ConState::ERROR
@@ -30,7 +30,21 @@ module Labimotion
30
30
 
31
31
  class << self
32
32
  def process_ds(id, current_user = {})
33
- att = find_attachment(id)
33
+ ctx = { att: nil }
34
+ # Savepoint: a failure leaves no half-built dataset and keeps the caller's transaction usable
35
+ Attachment.transaction(requires_new: true) { map_ds(id, current_user, ctx) }
36
+ rescue StandardError => e
37
+ Labimotion.log_exception(e, current_user)
38
+ # Not a readable zip: nothing to map, and not an error
39
+ return Labimotion::ConState::NONE if invalid_archive?(e, ctx[:att])
40
+
41
+ # Terminal state: do not fail the request, and do not retry on the next save
42
+ Labimotion.report_exception(e)
43
+ Labimotion::ConState::ERROR
44
+ end
45
+
46
+ def map_ds(id, current_user, ctx)
47
+ att = ctx[:att] = find_attachment(id)
34
48
  return Labimotion::ConState::NONE if att.nil?
35
49
 
36
50
  result = process(att)
@@ -132,6 +146,20 @@ module Labimotion
132
146
  Attachment.find_by(id: id, con_state: Labimotion::ConState::NMR)
133
147
  end
134
148
 
149
+ # True when the upload itself is not a readable zip. A missing or changed file, an encrypted zip
150
+ # or an unsupported compression method is not covered and stays ERROR.
151
+ def invalid_archive?(error, att)
152
+ return false unless error.instance_of?(Zip::Error) || error.is_a?(Zip::DecompressionError)
153
+
154
+ file = att&.attachment_attacher&.file
155
+ path = file&.url
156
+ on_disk = path && File.size?(path)
157
+ return false if on_disk.nil?
158
+
159
+ recorded = file.size
160
+ recorded.nil? || recorded == on_disk
161
+ end
162
+
135
163
  def handle_process_result(result, att, id, current_user)
136
164
  if result[:is_bagit]
137
165
  handle_bagit_result(att, id, current_user)
@@ -145,7 +173,8 @@ module Labimotion
145
173
  def handle_bagit_result(att, id, current_user)
146
174
  att.update_column(:con_state, Labimotion::ConState::CONVERTED)
147
175
  Labimotion::Converter.metadata(id, current_user)
148
- Labimotion::ConState::COMPLETED
176
+ # Return the state Converter.metadata stored; CONVERTED means still waiting for an analysis dataset
177
+ Attachment.find_by(id: id)&.con_state || Labimotion::ConState::NONE
149
178
  end
150
179
 
151
180
  def invalid_metadata?(result)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'zip'
4
+
3
5
  module Labimotion
4
6
  module AttachmentConverter
5
7
  ACCEPTED_FORMATS = (Rails.configuration.try(:converter).try(:ext) || []).freeze
@@ -27,6 +29,8 @@ module Labimotion
27
29
 
28
30
  return if attachable_id.nil? && con_state != Labimotion::ConState::WAIT
29
31
 
32
+ # No attachable_id-changed guard: an attachment created already attached must still be processed.
33
+ # Each branch leaves its pending state, so the work runs once.
30
34
  current_user = User.find_by(id: created_by)
31
35
  case con_state
32
36
  when Labimotion::ConState::NMR
@@ -39,6 +43,26 @@ module Labimotion
39
43
  Labimotion::Converter.metadata(id, current_user)
40
44
  end
41
45
  end
46
+
47
+ # Run the zip mapping again for an attachment in ERROR, e.g. after its file was restored.
48
+ # Returns the new con_state.
49
+ def reprocess_converter!
50
+ return con_state unless has_attribute?(:con_state) && File.extname(filename.to_s.downcase) == '.zip'
51
+
52
+ path = attachment_attacher&.file&.url
53
+ return con_state unless path && File.size?(path)
54
+
55
+ self.con_state = converter_output?(path) ? Labimotion::ConState::CONVERTED : Labimotion::ConState::NMR
56
+ update_column(:con_state, con_state)
57
+ exec_converter
58
+ reload.con_state
59
+ end
60
+
61
+ private def converter_output?(path)
62
+ !Zip::File.new(path).find_entry('metadata/converter.json').nil?
63
+ rescue Zip::Error
64
+ false # not a readable zip: the NMR mapper classifies it
65
+ end
42
66
  end
43
67
  end
44
68
  end
@@ -6,8 +6,18 @@ require 'zip'
6
6
  require 'labimotion/constants'
7
7
 
8
8
  module Labimotion
9
+ # rubyzip 2.x does not detect encrypted entries and would inflate them as garbage
10
+ class EncryptedZipError < Zip::Error; end
11
+
9
12
  class MapperUtils
10
13
  class << self
14
+ # Read an entry and close its stream, so no handle on the stored file stays open
15
+ def read_entry(entry)
16
+ raise EncryptedZipError, "Encrypted zip entry: #{entry.name}" if entry.encrypted?
17
+
18
+ entry.get_input_stream(&:read)
19
+ end
20
+
11
21
  def load_config(config_json)
12
22
  JSON.parse(config_json)
13
23
  rescue JSON::ParserError => e
@@ -37,13 +47,8 @@ module Labimotion
37
47
  def extract_data_from_zip(zip_file_url, source_map)
38
48
  return nil if zip_file_url.nil?
39
49
 
50
+ # nil means nothing to map; errors are raised so the caller can record ERROR
40
51
  process_zip_file(zip_file_url, source_map)
41
- rescue Zip::Error => e
42
- Rails.logger.error "Zip file error: #{e.message}"
43
- nil
44
- rescue StandardError => e
45
- Rails.logger.error "Unexpected error extracting metadata: #{e.message}"
46
- nil
47
52
  end
48
53
 
49
54
  def extract_parameters(file_content, parameter_names)
@@ -121,10 +126,12 @@ module Labimotion
121
126
  def process_zip_file(zip_file_url, source_map)
122
127
  final_parameters = {}
123
128
 
124
- Zip::File.open(zip_file_url) do |zip_file|
125
- source_map['sourceSelector'].each do |source_name|
126
- process_source(zip_file, source_map[source_name], final_parameters)
127
- end
129
+ # Never close: closing can rewrite the stored file in place
130
+ zip_file = Zip::File.new(zip_file_url)
131
+ source_map['sourceSelector'].each do |source_name|
132
+ result = process_source(zip_file, source_map[source_name], final_parameters)
133
+ # A BagIt zip is mapped by the converter instead
134
+ return result if result&.dig(:is_bagit)
128
135
  end
129
136
 
130
137
  return { is_bagit: false, metadata: final_parameters } if final_parameters.present?
@@ -136,6 +143,8 @@ module Labimotion
136
143
  return if invalid_source_config?(source_config)
137
144
 
138
145
  zip_file.each do |entry|
146
+ next if entry.directory?
147
+
139
148
  if source_file?(entry, source_config)
140
149
  process_file_entry(entry, source_config['parameters'], final_parameters)
141
150
  elsif bagit_metadata_file?(entry)
@@ -145,7 +154,7 @@ module Labimotion
145
154
  end
146
155
 
147
156
  def process_file_entry(entry, parameters, final_parameters)
148
- file_content = entry.get_input_stream.read.force_encoding(Constants::File::ENCODING)
157
+ file_content = read_entry(entry).force_encoding(Constants::File::ENCODING)
149
158
  extracted_parameters = extract_parameters(file_content, parameters)
150
159
  final_parameters.merge!(extracted_parameters) if extracted_parameters.present?
151
160
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.1.1'
5
+ VERSION = '2.1.2'
6
6
  end
data/lib/labimotion.rb CHANGED
@@ -13,6 +13,11 @@ module Labimotion
13
13
  Labimotion.logger.error(exception.backtrace.join("\n"))
14
14
  end
15
15
 
16
+ # Report a rescued exception to Sentry, if the host app uses it
17
+ def self.report_exception(exception)
18
+ Sentry.capture_exception(exception) if defined?(Sentry)
19
+ end
20
+
16
21
  autoload :MapperUtils, 'labimotion/utils/mapper_utils'
17
22
  autoload :Utils, 'labimotion/utils/utils'
18
23
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: labimotion
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chia-Lin Lin
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-03-25 00:00:00.000000000 Z
12
+ date: 2026-10-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx