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.
★ Extended 2026-09 with #upload_image/#upload_all_images — steps 1-2 above (the
presign + S3 POST) are IDENTICAL for an Image Gallery image; only step 3 differs
(uploadImage -> a TempImage, not uploadFile -> a FileContainer). Confirmed
against the live ecoPortal server source (read-only, /tmp/work/ecoPortal) that an
Image Gallery image is genuinely NOT a FileContainer — see
Mutation::Image::Upload's and Base::Page::DataField::ImageGallery's own headers
for the full evidence trail. #presign_and_store! is the shared internal path both
#upload_one and #upload_one_image call; #run_batch is the shared batch-with-
progress-callback loop both #upload_all and #upload_all_images call.
Single file, FileContainer (File-type fields):
id = api.file_upload.upload('/path/report.pdf')
page.components.get_by_name('Report').file_container_ids = [id]
Single file, TempImage (Image Gallery fields):
source_id = api.file_upload.upload_image('/path/photo.jpg')
page.components.get_by_name('Site Photos').add_source_images([{source_id: source_id, file_name: 'photo.jpg'}])
Many files, concurrently, with per-file error isolation (nothing raises out) — same shape for both flows:
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, for EITHER flow (the
stage names are shared: :register fires once whether the registration mutation was
uploadFile or uploadImage):
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 "registered #{payload.item&.id}" }
Defined Under Namespace
Classes: Error, ImageResult, 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
- DEFAULT_IMAGE_TYPE =
'image_gallery'.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.
-
#upload_all_images(file_paths, threads: 4, **kargs, &block) ⇒ Array<ImageResult>
Sibling to #upload_all for the image-upload pipeline — identical concurrency, isolation and progress-callback semantics.
-
#upload_image(file_path, **kargs) ⇒ String
The new
TempImageid (thesourceIdan Image GalleryImageInputwrite needs — seeBase::Page::DataField::ImageGallery# add_source_images).
Constructor Details
#initialize(graphql_client) ⇒ Client
120 121 122 123 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 120 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). Fires identically for #upload and #upload_image — the STAGE names are shared between both flows.
129 130 131 132 133 134 135 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 129 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).
175 176 177 178 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 175 def refresh_credentials! mutex(:credentials).synchronize { @credentials = nil } self end |
#upload(file_path, **kargs) ⇒ String
Returns the file container id.
139 140 141 142 143 144 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 139 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.
163 164 165 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 163 def upload_all(file_paths, threads: 4, **kargs, &block) run_batch(file_paths, threads: threads, on_result: block) { |file| upload_one(file, **kargs) } end |
#upload_all_images(file_paths, threads: 4, **kargs, &block) ⇒ Array<ImageResult>
Sibling to #upload_all for the image-upload pipeline — identical concurrency, isolation and progress-callback semantics.
170 171 172 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 170 def upload_all_images(file_paths, threads: 4, **kargs, &block) run_batch(file_paths, threads: threads, on_result: block) { |file| upload_one_image(file, **kargs) } end |
#upload_image(file_path, **kargs) ⇒ String
Returns the new TempImage id (the sourceId an Image Gallery
ImageInput write needs — see Base::Page::DataField::ImageGallery# add_source_images).
151 152 153 154 155 156 |
# File 'lib/ecoportal/api/graphql/file_upload/client.rb', line 151 def upload_image(file_path, **kargs) result = upload_one_image(file_path, **kargs) raise result.error if result.error? result.temp_image_id end |