ecoportal-api-v2 1.1.7 → 2.0.0
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/.markdownlint.json +4 -0
- data/.rubocop.yml +54 -15
- data/.ruby-version +1 -0
- data/CHANGELOG.md +485 -373
- data/ecoportal-api-v2.gemspec +13 -12
- data/lib/ecoportal/api/common/concerns/benchmarkable.rb +47 -34
- data/lib/ecoportal/api/common/concerns/threadable.rb +41 -0
- data/lib/ecoportal/api/common/concerns.rb +1 -0
- data/lib/ecoportal/api/common/content/array_model.rb +85 -79
- data/lib/ecoportal/api/common/content/class_helpers.rb +34 -31
- data/lib/ecoportal/api/common/content/collection_model.rb +77 -65
- data/lib/ecoportal/api/common/content/double_model.rb +105 -87
- data/lib/ecoportal/api/common/content/wrapped_response.rb +11 -11
- data/lib/ecoportal/api/v2/page/component/reference_field.rb +17 -13
- data/lib/ecoportal/api/v2/page/component.rb +67 -68
- data/lib/ecoportal/api/v2/page/components.rb +9 -9
- data/lib/ecoportal/api/v2/page/force.rb +6 -7
- data/lib/ecoportal/api/v2/page/stages.rb +5 -6
- data/lib/ecoportal/api/v2/page.rb +35 -33
- data/lib/ecoportal/api/v2/pages/page_stage.rb +22 -20
- data/lib/ecoportal/api/v2/pages.rb +18 -14
- data/lib/ecoportal/api/v2/people.rb +2 -3
- data/lib/ecoportal/api/v2/registers.rb +28 -13
- data/lib/ecoportal/api/v2/s3/data.rb +27 -0
- data/lib/ecoportal/api/v2/s3/files/batch_upload.rb +110 -0
- data/lib/ecoportal/api/v2/s3/files/poll.rb +82 -0
- data/lib/ecoportal/api/v2/s3/files/poll_status.rb +52 -0
- data/lib/ecoportal/api/v2/s3/files.rb +132 -0
- data/lib/ecoportal/api/v2/s3/upload.rb +154 -0
- data/lib/ecoportal/api/v2/s3.rb +66 -0
- data/lib/ecoportal/api/v2.rb +10 -3
- data/lib/ecoportal/api/v2_version.rb +1 -1
- metadata +53 -54
@@ -2,26 +2,29 @@ require 'base64'
|
|
2
2
|
module Ecoportal
|
3
3
|
module API
|
4
4
|
class V2
|
5
|
-
# @attr_reader client [Common::Client] a `Common::Client` object that
|
5
|
+
# @attr_reader client [Common::Client] a `Common::Client` object that
|
6
|
+
# holds the configuration of the api connection.
|
6
7
|
class Registers
|
7
8
|
extend Common::BaseClass
|
8
9
|
include Enumerable
|
9
10
|
include Common::Content::DocHelpers
|
10
11
|
|
11
|
-
class_resolver :register_class,
|
12
|
+
class_resolver :register_class, "Ecoportal::API::V2::Registers::Register"
|
12
13
|
class_resolver :register_search_result_class, "Ecoportal::API::V2::Registers::PageResult"
|
13
14
|
class_resolver :register_search_results, "Ecoportal::API::V2::Registers::SearchResults"
|
14
15
|
|
15
16
|
attr_reader :client
|
16
17
|
|
17
|
-
# @param client [Common::Client] a `Common::Client` object that
|
18
|
+
# @param client [Common::Client] a `Common::Client` object that
|
19
|
+
# holds the configuration of the api connection.
|
18
20
|
# @return [Registers] an instance object ready to make registers api requests.
|
19
21
|
def initialize(client)
|
20
22
|
@client = client
|
21
23
|
end
|
22
24
|
|
23
25
|
def each(params: {}, &block)
|
24
|
-
return to_enum(:each) unless block
|
26
|
+
return to_enum(:each, params: params) unless block
|
27
|
+
|
25
28
|
get.each(&block)
|
26
29
|
end
|
27
30
|
|
@@ -29,7 +32,11 @@ module Ecoportal
|
|
29
32
|
# @return [Enumerable<Register>] an `Enumerable` with all schemas already wrapped as `Register` objects.
|
30
33
|
def get
|
31
34
|
response = client.get("/templates")
|
32
|
-
Common::Content::WrappedResponse.new(
|
35
|
+
Common::Content::WrappedResponse.new(
|
36
|
+
response,
|
37
|
+
register_class,
|
38
|
+
key: "registers"
|
39
|
+
)
|
33
40
|
end
|
34
41
|
|
35
42
|
# Gets all the oozes/pages of `register_id` matching the `options`
|
@@ -41,18 +48,21 @@ module Ecoportal
|
|
41
48
|
# @yield [result] something to do with search page-result.
|
42
49
|
# @yieldparam result [Ecoportal::V2::Registers::PageResult] a page result.
|
43
50
|
# @return [Ecoportal::API::V2::Registers, Ecoportal::API::V2::Registers::SearchResults]
|
44
|
-
def search(register_id, options = {})
|
51
|
+
def search(register_id, options = {}) # rubocop:disable Metrics/AbcSize
|
45
52
|
only_first = options.delete(:only_first)
|
46
53
|
options = build_options(options)
|
47
54
|
|
48
55
|
if only_first
|
49
56
|
response = client.get("/registers/#{register_id}/search", params: options)
|
50
57
|
raise "Request failed - Status #{response.status}: #{response.body}" unless response.success?
|
58
|
+
|
51
59
|
return register_search_results.new(response.body["data"])
|
52
60
|
end
|
53
61
|
|
54
62
|
cursor_id = nil
|
55
|
-
results
|
63
|
+
results = 0
|
64
|
+
total = nil
|
65
|
+
|
56
66
|
loop do
|
57
67
|
options.update(cursor_id: cursor_id) if cursor_id
|
58
68
|
response = client.get("/registers/#{register_id}/search", params: options)
|
@@ -61,12 +71,14 @@ module Ecoportal
|
|
61
71
|
data = response.body["data"]
|
62
72
|
total ||= data["total"]
|
63
73
|
if total != data["total"]
|
64
|
-
msg = "Change of total in search results.
|
74
|
+
msg = "Change of total in search results. "
|
75
|
+
msg << "Probably due to changes that affect the filter"
|
76
|
+
msg << "(register: #{register_id}):"
|
65
77
|
print_search_status(msg, total, results, cursor_id, data, options)
|
66
78
|
#total = data["total"]
|
67
79
|
end
|
68
80
|
|
69
|
-
unless total
|
81
|
+
unless total&.zero?
|
70
82
|
results += data["results"].length
|
71
83
|
print_progress(results, total)
|
72
84
|
end
|
@@ -77,12 +89,15 @@ module Ecoportal
|
|
77
89
|
end
|
78
90
|
|
79
91
|
break if total <= results
|
92
|
+
|
80
93
|
unless data["cursor_id"]
|
81
|
-
msg
|
94
|
+
msg = "Possible error... finishing search for lack of cursor_id in response:"
|
82
95
|
print_search_status(msg, total, results, cursor_id, data, options)
|
83
96
|
end
|
97
|
+
|
84
98
|
break unless (cursor_id = data["cursor_id"])
|
85
99
|
end
|
100
|
+
|
86
101
|
self
|
87
102
|
end
|
88
103
|
|
@@ -96,14 +111,14 @@ module Ecoportal
|
|
96
111
|
options.each do |key, value|
|
97
112
|
if key == :filters && value.any?
|
98
113
|
ret[key] = {filters: value}.to_json
|
99
|
-
|
100
|
-
ret[key] = value
|
114
|
+
elsif key
|
115
|
+
ret[key] = value
|
101
116
|
end
|
102
117
|
end
|
103
118
|
end
|
104
119
|
end
|
105
120
|
|
106
|
-
def print_search_status(msg, total, results, cursor_id, data, options)
|
121
|
+
def print_search_status(msg, total, results, cursor_id, data, options) # rubocop:disable Metrics/ParameterLists
|
107
122
|
msg += "\n"
|
108
123
|
msg += " • Original total: #{total}\n"
|
109
124
|
msg += " • Current total: #{data&.dig("total")}\n"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
class V2
|
4
|
+
class S3
|
5
|
+
class Data < Common::Content::DoubleModel
|
6
|
+
passthrough :s3_endpoint, :AWSAccessKeyId
|
7
|
+
passthrough :policy, :signature
|
8
|
+
passthrough :user_tmpdir
|
9
|
+
|
10
|
+
def x_amz_server_side_encryption
|
11
|
+
doc['x-amz-server-side-encryption']
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
doc[key.to_s]
|
16
|
+
end
|
17
|
+
|
18
|
+
def user_id
|
19
|
+
return unless user_tmpdir
|
20
|
+
|
21
|
+
user_tmpdir.split('uploads/').last
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
class V2
|
4
|
+
class S3
|
5
|
+
class Files
|
6
|
+
# Class service to upload multiple files in one go
|
7
|
+
class BatchUpload
|
8
|
+
include Ecoportal::API::Common::Concerns::Benchmarkable
|
9
|
+
include Ecoportal::API::Common::Concerns::Threadable
|
10
|
+
|
11
|
+
SIZE_UNITS = 1_024 # KB
|
12
|
+
MIN_UNIT = 1
|
13
|
+
|
14
|
+
attr_reader :files, :files_api
|
15
|
+
|
16
|
+
FileResult = Struct.new(:file) do
|
17
|
+
attr_accessor :poll, :s3_file_reference, :error
|
18
|
+
|
19
|
+
def error?
|
20
|
+
!error.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
def success?
|
24
|
+
poll&.success?
|
25
|
+
end
|
26
|
+
|
27
|
+
def container_id
|
28
|
+
return unless success?
|
29
|
+
poll.container_id
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param files [Array<String>] the files to be uploaded
|
34
|
+
# @param files_api [API::S3::Files] the api object.
|
35
|
+
def initialize(files, files_api:)
|
36
|
+
@files = [files].flatten.compact
|
37
|
+
@files_api = files_api
|
38
|
+
end
|
39
|
+
|
40
|
+
# Do the actual upload of the file
|
41
|
+
def upload!(benchmarking: false, threads: 1, **kargs) # rubocop:disable Metrics/AbcSize
|
42
|
+
bench = benchmarking && benchmark_enabled?
|
43
|
+
bench_mth = "#{self.class}##{__method__}"
|
44
|
+
thr_children = []
|
45
|
+
benchmarking("#{bench_mth}.#{files.count}_files", print: bench) do
|
46
|
+
with_preserved_thread_globals(report: false) do
|
47
|
+
files.each do |file|
|
48
|
+
new_thread(thr_children, max: threads) do
|
49
|
+
file_results << (result = FileResult.new(file))
|
50
|
+
|
51
|
+
s3_ref = nil
|
52
|
+
|
53
|
+
kbytes = bench ? file_size(file) : 1
|
54
|
+
|
55
|
+
benchmarking("#{bench_mth}.s3_upload (KB)", units: kbytes, print: bench) do
|
56
|
+
s3_ref = result.s3_file_reference = s3_api.upload_file(file)
|
57
|
+
end
|
58
|
+
|
59
|
+
benchmarking("##{bench_mth}.ep_poll (KB)", units: kbytes, print: bench) do
|
60
|
+
files_api.poll!(s3_ref, **kargs) do |poll|
|
61
|
+
result.poll = poll
|
62
|
+
end
|
63
|
+
end
|
64
|
+
yield(result) if block_given?
|
65
|
+
rescue *rescued_errors => err # each_file
|
66
|
+
result.error = err
|
67
|
+
yield(result) if block_given?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
thr_children.each(&:join)
|
74
|
+
file_results
|
75
|
+
ensure
|
76
|
+
puts benchmark_summary(:all) if bench
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def file_size(file, bytes: SIZE_UNITS)
|
82
|
+
return 1 unless File.exist?(file)
|
83
|
+
|
84
|
+
units = (File.size(file) / bytes).round(10)
|
85
|
+
[MIN_UNIT, units].max
|
86
|
+
end
|
87
|
+
|
88
|
+
def s3_api
|
89
|
+
files_api.s3_api
|
90
|
+
end
|
91
|
+
|
92
|
+
def file_results
|
93
|
+
@file_results ||= []
|
94
|
+
end
|
95
|
+
|
96
|
+
def rescued_errors
|
97
|
+
@rescued_errors ||= [
|
98
|
+
Ecoportal::API::V2::S3::MissingLocalFile,
|
99
|
+
Ecoportal::API::V2::S3::CredentialsGetFailed,
|
100
|
+
Ecoportal::API::V2::S3::Files::FailedPollRequest,
|
101
|
+
Ecoportal::API::V2::S3::Files::CantCheckStatus,
|
102
|
+
Ecoportal::API::Errors::TimeOut
|
103
|
+
].freeze
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
class V2
|
4
|
+
class S3
|
5
|
+
class Files
|
6
|
+
class Poll < Common::Content::DoubleModel
|
7
|
+
passthrough :poll_url
|
8
|
+
embeds_one :status, nullable: true, klass: "Ecoportal::API::V2::S3::Files::PollStatus"
|
9
|
+
|
10
|
+
def poll_id
|
11
|
+
return @poll_id if instance_variable_defined?(:@poll_id)
|
12
|
+
return @poll_id = nil unless (uri = parsed_poll_url)
|
13
|
+
|
14
|
+
@poll_id = uri.path.split('/poll/').last
|
15
|
+
end
|
16
|
+
alias_method :id, :poll_id
|
17
|
+
|
18
|
+
# The final File eP container id
|
19
|
+
def container_id
|
20
|
+
return unless status?
|
21
|
+
|
22
|
+
status.container_id
|
23
|
+
end
|
24
|
+
|
25
|
+
def status?
|
26
|
+
!doc['status'].nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def status=(value)
|
30
|
+
case value
|
31
|
+
when NilClass
|
32
|
+
doc["status"] = nil
|
33
|
+
when Ecoportal::API::V2::S3::Files::PollStatus
|
34
|
+
doc["status"] = JSON.parse(value.to_json)
|
35
|
+
when Hash
|
36
|
+
doc["status"] = value
|
37
|
+
else
|
38
|
+
# TODO
|
39
|
+
raise "Invalid set on status: Need nil, PollStatus or Hash; got #{value.class}"
|
40
|
+
end
|
41
|
+
|
42
|
+
remove_instance_variable("@status") if defined?(@status)
|
43
|
+
end
|
44
|
+
|
45
|
+
def pending?
|
46
|
+
!complete?
|
47
|
+
end
|
48
|
+
|
49
|
+
def complete?
|
50
|
+
status&.complete?
|
51
|
+
end
|
52
|
+
|
53
|
+
def success?
|
54
|
+
status&.success?
|
55
|
+
end
|
56
|
+
|
57
|
+
def failed?
|
58
|
+
status&.failed?
|
59
|
+
end
|
60
|
+
|
61
|
+
def timeout?
|
62
|
+
status&.timeout?
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def parsed_poll_url
|
68
|
+
return nil unless poll_url
|
69
|
+
|
70
|
+
require 'uri'
|
71
|
+
URI.parse(poll_url)
|
72
|
+
end
|
73
|
+
|
74
|
+
def callbacks
|
75
|
+
@callbacks ||= []
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
class V2
|
4
|
+
class S3
|
5
|
+
class Files
|
6
|
+
class PollStatus < Common::Content::DoubleModel
|
7
|
+
class FileContainer < Common::Content::DoubleModel
|
8
|
+
passkey :id
|
9
|
+
passthrough :file_container_id # same as :id :)
|
10
|
+
passthrough :name, :label
|
11
|
+
passthrough :file_size, :content_type
|
12
|
+
passthrough :tags
|
13
|
+
passboolean :archived
|
14
|
+
passthrough :active_person, :user_name
|
15
|
+
passdate :created_at, :updated_at, :file_update_at
|
16
|
+
end
|
17
|
+
|
18
|
+
# PollStatus
|
19
|
+
passthrough :status
|
20
|
+
embeds_one :file, klass: FileContainer
|
21
|
+
|
22
|
+
def container_id
|
23
|
+
return unless success?
|
24
|
+
|
25
|
+
file&.id
|
26
|
+
end
|
27
|
+
|
28
|
+
def complete?
|
29
|
+
success? || failed?
|
30
|
+
end
|
31
|
+
|
32
|
+
def timeout?
|
33
|
+
statut == 'timeout'
|
34
|
+
end
|
35
|
+
|
36
|
+
def pending?
|
37
|
+
status == "pending"
|
38
|
+
end
|
39
|
+
|
40
|
+
def success?
|
41
|
+
status == "success"
|
42
|
+
end
|
43
|
+
|
44
|
+
def failed?
|
45
|
+
status == "failed"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
class V2
|
4
|
+
# @attr_reader client [Common::Client] a `Common::Client` object that
|
5
|
+
# holds the configuration of the api connection.
|
6
|
+
class S3
|
7
|
+
class Files
|
8
|
+
class FailedPollRequest < StandardError; end
|
9
|
+
class CantCheckStatus < StandardError; end
|
10
|
+
|
11
|
+
extend Common::BaseClass
|
12
|
+
|
13
|
+
POLL_TIMEOUT = 240
|
14
|
+
DELAY_STATUS_CHECK = 5
|
15
|
+
|
16
|
+
class_resolver :poll_class, "Ecoportal::API::V2::S3::Files::Poll"
|
17
|
+
class_resolver :poll_status_class, "Ecoportal::API::V2::S3::Files::PollStatus"
|
18
|
+
|
19
|
+
attr_reader :client, :s3_api
|
20
|
+
|
21
|
+
# @param client [Common::Client] a `Common::Client` object that holds the configuration of the api connection.
|
22
|
+
# @param s3_api [V2::S3] the parent S3 api object.
|
23
|
+
# @return [Schemas] an instance object ready to make schema api requests.
|
24
|
+
def initialize(client, s3_api:)
|
25
|
+
@client = client
|
26
|
+
@s3_api = s3_api
|
27
|
+
end
|
28
|
+
|
29
|
+
# Helper to upload multiple files at once
|
30
|
+
def upload!(files, **kargs, &block)
|
31
|
+
BatchUpload.new(files, files_api: self).upload!(**kargs, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Tell ecoPortal to register the file in an actual File Container
|
35
|
+
# @note this is essential to do for the file to be attachable.
|
36
|
+
# - eP will scan the file and give create the proper data model that
|
37
|
+
# refers to the file (FileContainer).
|
38
|
+
# - FileContainers also have a file versioning sytem.
|
39
|
+
# - A file container belongs only to one single organization.
|
40
|
+
# @yield [poll] block early, when the poll is created
|
41
|
+
# @yieldparam poll [Ecoportal::API::V2::S3::Files::Poll, NilClass] the creatd poll
|
42
|
+
# @param s3_file_reference [Hash]
|
43
|
+
# @return [Ecoportal::API::V2::S3::Files::PollStatus, NilClass]
|
44
|
+
# when finalized (success, failed or timeout)
|
45
|
+
def poll!(
|
46
|
+
s3_file_reference,
|
47
|
+
check_delay: DELAY_STATUS_CHECK,
|
48
|
+
raise_timeout: false
|
49
|
+
)
|
50
|
+
raise ArgumentError, "Block required. None given" unless block_given?
|
51
|
+
|
52
|
+
poll = create_poll(s3_file_reference)
|
53
|
+
yield(poll) if block_given?
|
54
|
+
|
55
|
+
wait_for_poll_completion(poll.id, check_delay: check_delay).tap do |status|
|
56
|
+
poll.status = status
|
57
|
+
|
58
|
+
next unless raise_timeout
|
59
|
+
next if status&.complete?
|
60
|
+
|
61
|
+
msg = "File poll `#{poll.id}` not complete. "
|
62
|
+
msg << "Probably timeout after #{POLL_TIMEOUT} seconds. "
|
63
|
+
msg << "Current status: #{poll.status.status}"
|
64
|
+
|
65
|
+
raise Ecoportal::API::Errors::TimeOut, msg
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Create Poll Request to ecoPortal so it registers the file
|
70
|
+
# in an actual File Container
|
71
|
+
# @param s3_file_reference [Hash]
|
72
|
+
# @return [Ecoportal::API::V2::S3::Files::Poll, NilClass]
|
73
|
+
def create_poll(s3_file_reference)
|
74
|
+
response = client.post("/s3/files", data: s3_file_reference)
|
75
|
+
body = body_data(response.body)
|
76
|
+
|
77
|
+
return poll_class.new(body) if response.success?
|
78
|
+
|
79
|
+
msg = "Could not create poll request for\n"
|
80
|
+
msg << JSON.pretty_generate(s3_file_reference)
|
81
|
+
msg << "\n - Error #{response.status}: #{body}"
|
82
|
+
raise FailedPollRequest, msg
|
83
|
+
end
|
84
|
+
|
85
|
+
# Check progress on the File Poll status
|
86
|
+
# @param poll_id [String]
|
87
|
+
def poll_status(poll_id)
|
88
|
+
response = client.get("/s3/files/poll/#{poll_id}")
|
89
|
+
body = body_data(response.body)
|
90
|
+
|
91
|
+
return poll_status_class.new(body) if response.success?
|
92
|
+
|
93
|
+
msg = "Could not get status for poll '#{poll_id}' "
|
94
|
+
msg << "- Error #{response.status}: #{body}"
|
95
|
+
raise CantCheckStatus, msg
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def body_data(body)
|
101
|
+
return body unless body.is_a?(Hash)
|
102
|
+
return body unless body.key?("data")
|
103
|
+
|
104
|
+
body["data"]
|
105
|
+
end
|
106
|
+
|
107
|
+
# @note timeout library is evil. So we make poor-man timeout.
|
108
|
+
# https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
|
109
|
+
def wait_for_poll_completion(poll_id, check_delay: DELAY_STATUS_CHECK)
|
110
|
+
before = Time.now
|
111
|
+
|
112
|
+
loop do
|
113
|
+
status = poll_status(poll_id)
|
114
|
+
break status if status&.complete?
|
115
|
+
|
116
|
+
if Time.now >= before + POLL_TIMEOUT
|
117
|
+
status&.status = 'timeout'
|
118
|
+
break status
|
119
|
+
end
|
120
|
+
|
121
|
+
sleep(check_delay)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
require 'ecoportal/api/v2/s3/files/poll'
|
131
|
+
require 'ecoportal/api/v2/s3/files/poll_status'
|
132
|
+
require 'ecoportal/api/v2/s3/files/batch_upload'
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module Ecoportal
|
2
|
+
module API
|
3
|
+
class V2
|
4
|
+
class S3
|
5
|
+
class Upload
|
6
|
+
class MissingFile < ArgumentError; end
|
7
|
+
|
8
|
+
DEFAULT_MIME_TYPE = 'application/octet-stream'.freeze
|
9
|
+
EXPECTED_CODE = "204".freeze # No Content
|
10
|
+
MAX_RETRIES = 5
|
11
|
+
|
12
|
+
attr_reader :filename
|
13
|
+
|
14
|
+
def initialize(credentials, file:)
|
15
|
+
msg = "Expecting #{credentials_class}. Given: #{credentials.class}"
|
16
|
+
raise ArgumentError, msg unless credentials.is_a?(credentials_class)
|
17
|
+
|
18
|
+
@credentials = credentials
|
19
|
+
@filename = file
|
20
|
+
end
|
21
|
+
|
22
|
+
def upload!
|
23
|
+
require_deps!
|
24
|
+
|
25
|
+
@response = Net::HTTP.start(
|
26
|
+
uri.hostname,
|
27
|
+
uri.port,
|
28
|
+
use_ssl: true
|
29
|
+
) do |https|
|
30
|
+
https.max_retries = self.class::MAX_RETRIES
|
31
|
+
https.request(request)
|
32
|
+
end
|
33
|
+
|
34
|
+
return unless success?
|
35
|
+
|
36
|
+
yield(s3_file_reference) if block_given?
|
37
|
+
s3_file_reference
|
38
|
+
end
|
39
|
+
|
40
|
+
def failed?
|
41
|
+
return false unless response
|
42
|
+
|
43
|
+
response.code != EXPECTED_CODE
|
44
|
+
end
|
45
|
+
|
46
|
+
def success?
|
47
|
+
return false unless response
|
48
|
+
|
49
|
+
response.code == EXPECTED_CODE
|
50
|
+
end
|
51
|
+
|
52
|
+
def s3_file_reference
|
53
|
+
return unless success?
|
54
|
+
|
55
|
+
@s3_file_reference ||= {
|
56
|
+
'filename' => file_basename,
|
57
|
+
'filetype' => mime_type,
|
58
|
+
'filesize' => file_size,
|
59
|
+
'path' => s3_file_path
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
attr_reader :credentials
|
66
|
+
attr_reader :response
|
67
|
+
|
68
|
+
def request
|
69
|
+
Net::HTTP::Post.new(uri).tap do |req|
|
70
|
+
req.set_form form_data, 'multipart/form-data'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def uri
|
75
|
+
@uri ||= URI(credentials[:s3_endpoint])
|
76
|
+
end
|
77
|
+
|
78
|
+
def form_data
|
79
|
+
[
|
80
|
+
['key', s3_file_path],
|
81
|
+
*aws_params,
|
82
|
+
*file_params
|
83
|
+
]
|
84
|
+
end
|
85
|
+
|
86
|
+
def s3_file_path
|
87
|
+
"#{user_tmpdir}/#{file_basename}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def user_tmpdir
|
91
|
+
credentials[:user_tmpdir]
|
92
|
+
end
|
93
|
+
|
94
|
+
def aws_params
|
95
|
+
params = %i[AWSAccessKeyId policy signature x-amz-server-side-encryption]
|
96
|
+
params.map do |key|
|
97
|
+
[key.to_s, credentials[key]]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def file_params
|
102
|
+
[
|
103
|
+
['content-type', mime_type],
|
104
|
+
['file', io_stream]
|
105
|
+
]
|
106
|
+
end
|
107
|
+
|
108
|
+
def mime_type
|
109
|
+
@mime_type ||= lib_mime_type || DEFAULT_MIME_TYPE
|
110
|
+
end
|
111
|
+
|
112
|
+
def lib_mime_type
|
113
|
+
MIME::Types.type_for(file_extension).first&.content_type
|
114
|
+
end
|
115
|
+
|
116
|
+
def io_stream
|
117
|
+
require_file!
|
118
|
+
File.open(file_fullname)
|
119
|
+
end
|
120
|
+
|
121
|
+
def credentials_class
|
122
|
+
Ecoportal::API::V2::S3::Data
|
123
|
+
end
|
124
|
+
|
125
|
+
def file_size
|
126
|
+
@file_size ||= File.size(file_fullname)
|
127
|
+
end
|
128
|
+
|
129
|
+
def file_extension
|
130
|
+
@file_extension ||= File.extname(filename)[1..]
|
131
|
+
end
|
132
|
+
|
133
|
+
def file_basename
|
134
|
+
@file_basename ||= File.basename(filename)
|
135
|
+
end
|
136
|
+
|
137
|
+
def file_fullname
|
138
|
+
@file_fullname ||= File.expand_path(filename)
|
139
|
+
end
|
140
|
+
|
141
|
+
def require_file!
|
142
|
+
msg = "File '#{file_basename}' doesn't exist"
|
143
|
+
raise MissingFile, msg unless File.exist?(file_fullname)
|
144
|
+
end
|
145
|
+
|
146
|
+
def require_deps!
|
147
|
+
require 'net/http'
|
148
|
+
require 'mime/types'
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|