Skip to content

Commit

Permalink
Implement Binary Upload (Fixes #2126)
Browse files Browse the repository at this point in the history
  • Loading branch information
allentiak committed Feb 5, 2025
1 parent faea419 commit 80ae128
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 30 deletions.
54 changes: 54 additions & 0 deletions .github/scripts/write-binary-content-via-raw-data.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/bash -e

# This script creates a large binary resource (8 MiB) and verifies that its binary content
# * can be read correctly via direct binary upload, and that
# * it's properly base64-encoded.

BASE="http://localhost:8080/fhir"

# Create temporary files for original and downloaded data
TEMP_ORIGINAL=$(mktemp)
TEMP_DOWNLOAD=$(mktemp)
TEMP_JSON=$(mktemp)

# Ensure cleanup of temporary files
trap 'rm -f "$TEMP_ORIGINAL" "$TEMP_DOWNLOAD" "$TEMP_JSON"' EXIT

echo "Testing direct binary upload and download..."

# Generate 8 MiB of random binary data
dd if=/dev/urandom bs=8388608 count=1 2>/dev/null > "$TEMP_ORIGINAL"

# Create Binary resource via direct binary upload
ID=$(curl -s \
-H 'Content-Type: application/octet-stream' \
--data-binary "@$TEMP_ORIGINAL" \
"$BASE/Binary" | \
jq -r '.id')

echo "Created Binary resource via direct binary upload with ID: $ID"

# Download as JSON format to verify base64 encoding
curl -s \
-H 'Accept: application/fhir+json' \
"$BASE/Binary/$ID" > "$TEMP_JSON"

# Extract the base64 content and decode it
jq -r '.data' "$TEMP_JSON" | base64 -d > "$TEMP_DOWNLOAD"

# Compare files directly
if [ -n "$ID" ] && cmp -s "$TEMP_ORIGINAL" "$TEMP_DOWNLOAD"; then
echo "✅ Direct Binary: Successfully verified 8 MiB binary content integrity and base64 encoding"
else
echo "🆘 Direct Binary: Content verification failed"
echo "Server response (JSON):"
cat "$TEMP_JSON"
echo "Original size: $(wc -c < "$TEMP_ORIGINAL") bytes"
echo "Downloaded size: $(wc -c < "$TEMP_DOWNLOAD") bytes"
# Show first few bytes of both files in hex for debugging
echo "First 32 bytes of original:"
hexdump -C "$TEMP_ORIGINAL" | head -n 2
echo "First 32 bytes of downloaded:"
hexdump -C "$TEMP_DOWNLOAD" | head -n 2
exit 1
fi
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,9 @@ jobs:
- name: Binary Content Download - found (via XML)
run: .github/scripts/read-binary-content-via-xml-found.sh

- name: Binary Content Upload (via raw data)
run: .github/scripts/write-binary-content-via-raw-data.sh

- name: Conditional Delete - Check Referential Integrity Violated
run: .github/scripts/conditional-delete-type/check-referential-integrity-violated.sh

Expand Down
12 changes: 10 additions & 2 deletions modules/rest-api/src/blaze/rest_api/routes.clj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
{:name :auth-guard
:wrap auth-guard/wrap-auth-guard})

(def ^:private wrap-binary-data
{:name :binary-data
:wrap resource/wrap-binary-data})

(def ^:private wrap-resource
{:name :resource
:wrap resource/wrap-resource})
Expand Down Expand Up @@ -111,7 +115,9 @@
:blaze.rest-api.interaction/handler)})
(contains? interactions :create)
(assoc :post {:interaction "create"
:middleware [wrap-resource]
:middleware (if (= name "Binary")
[wrap-binary-data]
[wrap-resource])
:handler (-> interactions :create
:blaze.rest-api.interaction/handler)})
(contains? interactions :conditional-delete-type)
Expand Down Expand Up @@ -179,7 +185,9 @@
:blaze.rest-api.interaction/handler)})
(contains? interactions :update)
(assoc :put {:interaction "update"
:middleware [wrap-resource]
:middleware (if (= name "Binary")
[wrap-binary-data]
[wrap-resource])
:handler (-> interactions :update
:blaze.rest-api.interaction/handler)})
(contains? interactions :delete)
Expand Down
55 changes: 50 additions & 5 deletions modules/rest-util/src/blaze/middleware/fhir/resource.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
[blaze.anomaly :as ba :refer [if-ok when-ok]]
[blaze.async.comp :as ac]
[blaze.fhir.spec :as fhir-spec]
[blaze.fhir.spec.type :as type]
[clojure.data.xml.jvm.parse :as xml-jvm]
[clojure.data.xml.tree :as xml-tree]
[clojure.java.io :as io]
Expand All @@ -16,7 +17,10 @@
[ring.util.request :as request])
(:import
[com.ctc.wstx.api WstxInputProperties]
[java.io InputStream]
[java.io Reader]
[java.util Base64$Encoder]
[java.util Base64]
[javax.xml.stream XMLInputFactory]))

(set! *warn-on-reflection* true)
Expand Down Expand Up @@ -95,6 +99,23 @@
(assoc request :body resource))
(ba/incorrect "Missing HTTP body.")))

(defn- get-binary-data [body]
(with-open [_ (prom/timer parse-duration-seconds "binary")]
(.encodeToString ^Base64$Encoder (Base64/getEncoder) (.readAllBytes ^InputStream body))))

(defn- resource-request-binary-data [{:keys [body headers] :as request}]
(if body
;; `when-ok` is not needed here because:
;; * Binary data is not parsed nor validated, and
;; * Base64-encoding should not fail under normal circumstances.
(let [b64-encoded-data (get-binary-data body)
content-type (get headers "content-type")]
(assoc request :body
{:fhir/type :fhir/Binary
:contentType (type/code content-type)
:data (type/base64Binary b64-encoded-data)}))
(ba/incorrect "Missing HTTP body.")))

(defn- unsupported-media-type-msg [media-type]
(format "Unsupported media type `%s` expect one of `application/fhir+json` or `application/fhir+xml`."
media-type))
Expand All @@ -109,19 +130,43 @@
:http/status 415))
(if (str/blank? (slurp (:body request)))
(assoc request :body nil)
(ba/incorrect "Content-Type header expected, but is missing."))))
(ba/incorrect "Missing Content-Type header for FHIR resources."))))

(defn- binary-resource-request [request]
(if-let [content-type (request/content-type request)]
(cond
(str/starts-with? content-type "application/fhir+json") (resource-request-json request)
(str/starts-with? content-type "application/fhir+xml") (resource-request-xml request)
:else
(resource-request-binary-data request))
(ba/incorrect "Missing Content-Type header for binary resources.")))

(defn wrap-resource
"Middleware to parse a resource from the body according the content-type
header.
Updates the :body key in the request map on successful parsing and conforming
the resource to the internal format.
If the resource is successfully parsed and conformed to the internal format,
updates the :body key in the request map.
Returns an OperationOutcome in the internal format, skipping the handler, with
an appropriate error on parsing and conforming errors."
In case on errors, returns an OperationOutcome in the internal format with the
appropriate error and skips the handler."
[handler]
(fn [request]
(if-ok [request (resource-request request)]
(handler request)
ac/completed-future)))

(defn wrap-binary-data
"Middleware to parse binary data from the body according the content-type
header.
If the resource is successfully parsed and conformed to the internal format,
updates the :body key in the request map.
In case on errors, returns an OperationOutcome in the internal format with the
appropriate error and skips the handler."
[handler]
(fn [request]
(if-ok [request (binary-resource-request request)]
(handler request)
ac/completed-future)))
Loading

0 comments on commit 80ae128

Please sign in to comment.