Class: Ecoportal::API::GraphQL::FileUpload::Client
- Inherits:
-
Object
- Object
- Ecoportal::API::GraphQL::FileUpload::Client
- Includes:
- Concerns::Threadable
- Defined in:
- lib/ecoportal/api/graphql/file_upload/client.rb
Overview
Uploads local files to the org file manager, natively.
★ Rewritten 2026-07-30 from a CAPTURED web upload (see the har-scrub skill). The
real flow is three steps, all GraphQL except the storage POST:
1. `FileSignature` (GraphQL) -> presigned policy + credentials
2. multipart POST (S3) -> 204; fields, in order: key, AWSAccessKeyId, policy,
signature, content-type,
x-amz-server-side-encryption, file
3. `uploadFile` (GraphQL) -> the FileContainer; its `id` is what page mutations want
The previous implementation was a port of ecoportal-api-v2's REST S3 code and did
POST /api/v2/<org>/s3/files + polling. The platform does not use those endpoints
for this flow — no register-over-REST, no poll. That is also why no
X-ECOPORTAL-API-KEY is needed here: every ecoPortal call is GraphQL, on the session
token this client already holds.
Single file:
id = api.file_upload.upload('/path/report.pdf')
page.components.get_by_name('Report').file_container_ids = [id]
Many files, concurrently, with per-file error isolation (nothing raises out):
results = api.file_upload.upload_all(paths, threads: 4) do |r|
puts r.success? ? "#{r.file} -> #{r.container_id}" : "#{r.file} FAILED: #{r.error}"
end
results.select(&:error?)
Instrumentation / middleware — hooks fire per stage, per file:
client = api.file_upload
client.on(:signature) { |creds| logger.info "presigned #{creds.endpoint}" }
client.on(:storage) { |key, response| logger.info "S3 #{response.code} #{key}" }
client.on(:register) { |payload| logger.info "container #{payload.item&.id}" }
Defined Under Namespace
Classes: Error, MissingLocalFile, RegistrationFailed, Result, StorageUploadFailed
Constant Summary collapse
- STAGES =
%i[signature storage register].freeze
- DEFAULT_ENCRYPTION =
Fallback only — the real value is a condition inside the returned policy.
'AES256'.freeze
- DEFAULT_MIME =
'application/octet-stream'.freeze
- MAX_THREADS =
8
Instance Method Summary collapse
-
#initialize(graphql_client) ⇒ Client
constructor
A new instance of Client.
-
#on(stage, &block) ⇒ Object
Register a stage hook.
-
#refresh_credentials! ⇒ Object
Force the next upload to presign again (e.g. after a policy expiry).
-
#upload(file_path, **kargs) ⇒ String
The file container id.
-
#upload_all(file_paths, threads: 4, **kargs, &block) ⇒ Array<Result>
Uploads many files with bounded concurrency.
Constructor Details
#initialize(graphql_client) ⇒ Client
Returns a new instance of Client.
80 81 82 83 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 80 def initialize(graphql_client) @graphql = graphql_client @hooks = {} end |
Instance Method Details
#on(stage, &block) ⇒ Object
Register a stage hook. Called for every file, in that file's own thread — keep it thread-safe (or wrap it in your own mutex).
88 89 90 91 92 93 94 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 88 def on(stage, &block) stage = stage.to_sym raise ArgumentError, "unknown stage #{stage.inspect}; expected one of #{STAGES.join(', ')}" unless STAGES.include?(stage) mutex(:hooks).synchronize { (@hooks[stage] ||= []) << block } self end |
#refresh_credentials! ⇒ Object
Force the next upload to presign again (e.g. after a policy expiry).
135 136 137 138 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 135 def refresh_credentials! mutex(:credentials).synchronize { @credentials = nil } self end |
#upload(file_path, **kargs) ⇒ String
Returns the file container id.
98 99 100 101 102 103 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 98 def upload(file_path, **kargs) result = upload_one(file_path, **kargs) raise result.error if result.error? result.container_id end |
#upload_all(file_paths, threads: 4, **kargs, &block) ⇒ Array<Result>
Uploads many files with bounded concurrency. Never raises for a single file: each Result carries its own error, and the block is called as each finishes.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 110 def upload_all(file_paths, threads: 4, **kargs, &block) files = Array(file_paths).flatten.compact max = threads.to_i.clamp(1, MAX_THREADS) results = [] spawned = [] # Presign ONCE for the batch (the policy is time-boxed but reusable) and warm it # here so N threads don't race for the first one. credentials with_preserved_thread_globals do files.each do |file| new_thread(spawned, max: max) do result = upload_one(file, **kargs) mutex(:results).synchronize { results << result } block&.call(result) end end end spawned.each(&:join) results end |