From 8e510d2d68ca59c5976a1f74db28b062d5e24ecf Mon Sep 17 00:00:00 2001 From: Steve Van Hooser Date: Sat, 18 Apr 2026 13:38:43 -0400 Subject: [PATCH 1/3] Default putFiles to curl so uploads match downloads Pre-signed PUT uploads going through MATLAB's native HTTP client sometimes store objects in S3 with headers that don't match what curl sends, producing inconsistent Content-Encoding/Content-Type metadata on the objects and flaky downloads later. Flip the default to curl for consistency with ndi.cloud.api.files.getFile. --- src/ndi/+ndi/+cloud/+api/+files/putFiles.m | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ndi/+ndi/+cloud/+api/+files/putFiles.m b/src/ndi/+ndi/+cloud/+api/+files/putFiles.m index fe40e9f79..4f9105d9f 100644 --- a/src/ndi/+ndi/+cloud/+api/+files/putFiles.m +++ b/src/ndi/+ndi/+cloud/+api/+files/putFiles.m @@ -14,8 +14,10 @@ % Name-Value Pairs: % 'useCurl' (logical) - If true, the function will use a system call % to the `curl` command-line tool to perform the -% upload. This can be a robust fallback if the native -% MATLAB HTTP client fails. Defaults to false. +% upload. Defaults to true so every upload path +% stores objects in S3 with consistent headers +% (the MATLAB HTTP client can tag objects +% differently, producing flaky downloads). % % Outputs: % b - True if the upload succeeded (HTTP 200), false otherwise. @@ -39,7 +41,7 @@ arguments preSignedURL (1,1) string filePath (1,1) string {mustBeFile} - options.useCurl (1,1) logical = false + options.useCurl (1,1) logical = true end % 1. Create an instance of the implementation class, passing the options. api_call = ndi.cloud.api.implementation.files.PutFiles(... @@ -51,4 +53,3 @@ [b, answer, apiResponse, apiURL] = api_call.execute(); end - From 8a295109ff9a330d19bb9cd7c83fe2e94005c248 Mon Sep 17 00:00:00 2001 From: Steve Van Hooser Date: Sat, 18 Apr 2026 13:39:01 -0400 Subject: [PATCH 2/3] Default PutFiles to curl and pin upload headers Align the implementation class with the wrapper's new curl-by-default behavior. Explicitly set Content-Type: application/octet-stream and Accept-Encoding: identity on the PUT so the metadata stored on the S3 object is consistent across clients. Add -f so stale signed URLs (403) or missing objects (404) fail loudly instead of being reported as a successful 200 OK upload. --- .../+api/+implementation/+files/PutFiles.m | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/ndi/+ndi/+cloud/+api/+implementation/+files/PutFiles.m b/src/ndi/+ndi/+cloud/+api/+implementation/+files/PutFiles.m index ef5c4b487..af21029eb 100644 --- a/src/ndi/+ndi/+cloud/+api/+implementation/+files/PutFiles.m +++ b/src/ndi/+ndi/+cloud/+api/+implementation/+files/PutFiles.m @@ -16,7 +16,7 @@ arguments args.preSignedURL (1,1) string args.filePath (1,1) string {mustBeFile} - args.useCurl (1,1) logical = false + args.useCurl (1,1) logical = true end this.preSignedURL = args.preSignedURL; this.filePath = args.filePath; @@ -62,9 +62,16 @@ % Implementation using a system call to curl b = false; apiURL = this.preSignedURL; % Return the URL as a string - - command = sprintf('curl -X PUT --upload-file "%s" "%s"', this.filePath, this.preSignedURL); - + + % -f so HTTP errors (403/404 on a stale signed URL, etc.) surface + % as a non-zero exit. Pin Content-Type to application/octet-stream + % and Accept-Encoding to identity so the object metadata stored in + % S3 is predictable regardless of the client's environment. + command = sprintf(['curl -fsSL -X PUT --upload-file "%s" ' ... + '-H "Content-Type: application/octet-stream" ' ... + '-H "Accept-Encoding: identity" ' ... + '"%s"'], this.filePath, this.preSignedURL); + [status, result] = system(command); b = (status == 0); @@ -75,4 +82,3 @@ end end end - From 39ac4cabe12f67a1ee47f53ea01fc2a61143e51c Mon Sep 17 00:00:00 2001 From: Steve Van Hooser Date: Sat, 18 Apr 2026 13:39:30 -0400 Subject: [PATCH 3/3] Forward useCurl through uploadSingleFile's bulk branch The non-bulk branch already forwarded options.useCurl to putFiles, but the bulk-upload branch called putFiles with no options, so it always took the (previously false) default. With the default now true this is cosmetic, but making the forward explicit keeps the caller's choice honored if useCurl is ever set to false here. --- src/ndi/+ndi/+cloud/uploadSingleFile.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ndi/+ndi/+cloud/uploadSingleFile.m b/src/ndi/+ndi/+cloud/uploadSingleFile.m index f52f0edb3..3af119d6f 100644 --- a/src/ndi/+ndi/+cloud/uploadSingleFile.m +++ b/src/ndi/+ndi/+cloud/uploadSingleFile.m @@ -40,7 +40,7 @@ error(['Could not get file collection upload URL: ' url_or_error.message]); end - [b_put, put_or_error] = ndi.cloud.api.files.putFiles(url_or_error, zip_file); + [b_put, put_or_error] = ndi.cloud.api.files.putFiles(url_or_error, zip_file, 'useCurl', options.useCurl); if ~b_put error(['Could not upload zip file: ' put_or_error.message]); end