Skip to content

Store and read back

Plain S3, plus one question: where does the object stand? First source the Quickstart’s first block to set HOST, AUTH and BUCKET.

Only the tenant that created it can use it.

10-bucket.sh
# Create the bucket. 200 means it exists and it is yours.
curl -sS -m 60 -X PUT -H "$AUTH" \
-o /dev/null -w 'bucket %{http_code}\n' "$HOST/$BUCKET"

An ordinary PUT, encrypted at rest by default. You get back exactly what you stored.

11-put.sh
# Store a local file as report.bin. Bring your own bytes, or make 64 KiB.
[ -f local.bin ] || head -c 65536 /dev/urandom > local.bin
curl -sS -m 120 -X PUT -H "$AUTH" --data-binary @local.bin \
-o /dev/null -w 'put %{http_code}\n' "$HOST/$BUCKET/report.bin"
12-get.sh
# Read it back and compare it to what you sent.
curl -sS -m 120 -H "$AUTH" -o roundtrip.bin "$HOST/$BUCKET/report.bin"
cmp local.bin roundtrip.bin && echo byte-equal

HEAD adds two headers:

  • x-amz-meta-permafrost-sync-state: PENDING, then SYNCED once on Walrus; HELD or LOCAL for held and local objects (Placement).
  • x-amz-meta-permafrost-blob-id: present once on Walrus. Absent isn’t an error.

Poll HEAD until it lands:

13-head.sh
# HEAD carries Permafrost's own metadata. The sync state is always there. The
# blob id appears only once the object is certified, so its absence is
# "not yet certified", not an error.
HEADERS=$(curl -sS -m 60 -I -H "$AUTH" "$HOST/$BUCKET/report.bin")
printf '%s\n' "$HEADERS" | grep -i '^x-amz-meta-permafrost-'
WAIT_SECS=${BLOBID_WAIT_SECS:-300}
echo "waiting up to ${WAIT_SECS}s for the blob id"
WAITED=0
BLOB_ID=""
while [ "$WAITED" -lt "$WAIT_SECS" ]; do
HEADERS=$(curl -sS -m 60 -I -H "$AUTH" "$HOST/$BUCKET/report.bin")
BLOB_ID=$(printf '%s\n' "$HEADERS" \
| grep -i '^x-amz-meta-permafrost-blob-id:' | cut -d' ' -f2- | tr -d '\r' || true)
if [ -n "$BLOB_ID" ]; then break; fi
sleep 5
WAITED=$((WAITED + 5))
done
echo "blob id: ${BLOB_ID:-not yet certified} (after ${WAITED}s)"

A SYNCED object has a receipt on Sui (Verify a file).

Standard S3 multipart. Reads back whole, or by range with 206.

20-multipart.sh
# A large object goes up in parts. Every part but the last must be at least
# 5 MiB - that is S3's minimum - so the last part is deliberately smaller.
KEY=${KEY:-big.bin}
PARTS=${PARTS:-3}
PART_MIB=${PART_MIB:-5}
UPLOAD_ID=$(curl -sS -m 60 -X POST -H "$AUTH" "$HOST/$BUCKET/$KEY?uploads" \
| sed -n 's:.*<UploadId>\(.*\)</UploadId>.*:\1:p')
echo "upload id: $UPLOAD_ID"
# Upload each part, keeping the ETag the server answers with.
: > all.bin
XML="<CompleteMultipartUpload>"
i=1
while [ "$i" -le "$PARTS" ]; do
if [ "$i" -eq "$PARTS" ]; then MIB=1; else MIB=$PART_MIB; fi
head -c "$((MIB * 1048576))" /dev/urandom > "part$i.bin"
cat "part$i.bin" >> all.bin
ETAG=$(curl -sS -m 300 -X PUT -H "$AUTH" --data-binary @"part$i.bin" -D - -o /dev/null \
"$HOST/$BUCKET/$KEY?partNumber=$i&uploadId=$UPLOAD_ID" \
| grep -i '^etag:' | cut -d' ' -f2- | tr -d '\r')
XML="$XML<Part><PartNumber>$i</PartNumber><ETag>$ETAG</ETag></Part>"
echo "part $i/$PARTS ($MIB MiB) $ETAG"
i=$((i + 1))
done
XML="$XML</CompleteMultipartUpload>"
# Completing is the slow call: the server assembles and registers the object.
printf '%s' "$XML" > complete.xml
curl -sS -m 600 -X POST -H "$AUTH" -H 'Content-Type: application/xml' \
--data-binary @complete.xml \
-o complete-response.xml -w 'complete %{http_code}\n' \
"$HOST/$BUCKET/$KEY?uploadId=$UPLOAD_ID"
curl -sS -m 600 -H "$AUTH" -o whole.bin "$HOST/$BUCKET/$KEY"
cmp all.bin whole.bin && echo "byte-equal over $(wc -c < all.bin | tr -d ' ') bytes"
# 16 bytes at the 1 MiB mark. 206 means the range was served, not the object.
curl -sS -m 120 -H "$AUTH" -H 'Range: bytes=1048576-1048591' \
-o slice.bin -w 'range %{http_code}\n' "$HOST/$BUCKET/$KEY"
curl -sS -m 120 -X DELETE -H "$AUTH" \
-o /dev/null -w 'delete %{http_code}\n' "$HOST/$BUCKET/$KEY"
  • Neither header above appears. Don’t wait on them.
  • Every part but the last must meet S3’s minimum part size, and no part may be over the deployment’s limit on one sealed PUT. SDK defaults (typically 5–8 MiB parts) are far below it.
14-delete.sh
# Delete it. Expect 204, then 404 on the read path.
curl -sS -m 60 -X DELETE -H "$AUTH" \
-o /dev/null -w 'delete %{http_code}\n' "$HOST/$BUCKET/report.bin"
curl -sS -m 60 -H "$AUTH" \
-o /dev/null -w 'get after delete %{http_code}\n' "$HOST/$BUCKET/report.bin"

204, then 404 on read and no answer from the keyless verify route.

Permafrost runs on Sui testnet and Walrus testnet. Everything here describes a shipped testnet instance, not a production service.