Efficiently export files to Microsoft Azure using cURL
To export a file to Azure Blob Storage with cURL, send an authenticated PUT with
x-ms-blob-type: BlockBlob. The example below signs that request with an existing storage account
key, refuses to replace an existing blob, and downloads the result to compare its bytes with the
local file.
Check the prerequisites
Use a Linux shell with Node.js and cURL installed. This example was tested with Node.js 26.12.0 and cURL 8.23.0; use those versions for the same reproduction. Node runs the TypeScript helper without a compiler or package installation. The Node.js download page and cURL releases provide the tools.
Check both executables before saving the program:
node --version && curl --version
You also need:
- An existing storage account and private container reachable through the public Azure endpoint
https://ACCOUNT.blob.core.windows.net. For a new project, use a supported general-purpose v2 account. Account and container provisioning are outside this walkthrough. - Permission to use Shared Key on that account, and its Base64 account key supplied through your organization’s secret-management process. Have it available in a protected file readable only by you. The program reads that file through standard input; keep it outside your project and version control.
- A regular local file from zero through 8 MiB. Keep it unchanged until verification finishes. This small-file example buffers the input and downloaded bytes in memory.
Use printable ASCII characters in the complete local path. For the remote blob name, use slash-separated segments containing letters, digits, periods, underscores, and hyphens, beginning each segment with a letter, digit, underscore, or hyphen. The helper deliberately rejects other names rather than guessing URL escaping. An empty file is valid.
Sign the request
Shared Key uses an HMAC-SHA256 signature over the HTTP method, selected request headers, and resource
path. The account key is Base64-decoded before signing. Header order matters, and a zero-byte upload
uses an empty content-length field in the signature even though its HTTP Content-Length is 0.
These details follow Microsoft’s Shared Key specification.
An account key grants broad access. Microsoft recommends Microsoft Entra ID with managed identities where possible; this recipe is specifically for an environment already authorized to use Shared Key. A bearer token or SAS token cannot be substituted for the account key in this program.
Save this complete program as azure-upload.mts. The .mts extension makes its module format
explicit, including inside an existing CommonJS project.
import { spawnSync } from 'node:child_process'
import { createHash, createHmac } from 'node:crypto'
import { readFileSync, statSync } from 'node:fs'
import { resolve } from 'node:path'
const maxBytes = 8 * 1024 * 1024
function main(): void {
const [input, account, container, blob, ...extra] = process.argv.slice(2)
if (!input || !account || !container || !blob || extra.length > 0) {
throw new Error('Usage: node azure-upload.mts FILE ACCOUNT CONTAINER BLOB < KEY_FILE')
}
const file = resolve(input)
if (!/^[\x20-\x7e]+$/.test(file)) throw new Error('Use a printable ASCII local path.')
if (!/^[a-z0-9]{3,24}$/.test(account)) throw new Error('Invalid account name.')
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(container) || container.length < 3 || container.length > 63) {
throw new Error('Invalid container name.')
}
if (blob.length > 1024 || !blob.split('/').every((part) => /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(part))) {
throw new Error('Use simple slash-separated blob-name segments.')
}
const info = statSync(file)
if (!info.isFile() || info.size > maxBytes) throw new Error('Use a regular file of at most 8 MiB.')
const original = readFileSync(file)
if (original.length > maxBytes) throw new Error('The input grew beyond 8 MiB.')
const encodedKey = readFileSync(0, 'utf8').trim()
if (!/^[A-Za-z0-9+/]{86}==$/.test(encodedKey)) throw new Error('Expected a Base64 512-bit account key.')
const key = Buffer.from(encodedKey, 'base64')
const url = `https://${account}.blob.core.windows.net/${container}/${blob}`
function request(method: 'PUT' | 'GET'): Buffer {
const uploading = method === 'PUT'
const length = uploading ? original.length : 0
const contentType = uploading ? 'application/octet-stream' : ''
const azureHeaders = [
...(uploading ? ['x-ms-blob-type:BlockBlob'] : []),
`x-ms-date:${new Date().toUTCString()}`,
'x-ms-version:2023-11-03',
]
const fields = [method, '', '', length === 0 ? '' : String(length), '', contentType,
'', '', '', uploading ? '*' : '', '', '']
const stringToSign = `${fields.join('\n')}\n${azureHeaders.join('\n')}\n/${account}/${container}/${blob}`
const signature = createHmac('sha256', key).update(stringToSign).digest('base64')
const headers = [
...azureHeaders,
`Authorization: SharedKey ${account}:${signature}`,
...(uploading ? [`Content-Length: ${length}`, `Content-Type: ${contentType}`, 'If-None-Match: *'] : []),
]
const result = spawnSync('curl', [
'--disable', '--globoff', '--silent', '--show-error', '--fail', '--proto', '=https',
'--request', method, '--header', '@-',
'--write-out', '%{stderr}HTTP:%{http_code}',
...(uploading ? ['--upload-file', file] : []),
'--output', uploading ? '/dev/null' : '-', url,
], { input: `${headers.join('\n')}\n`, maxBuffer: maxBytes + 65536 })
const status = /HTTP:(\d{3})$/.exec(result.stderr?.toString() ?? '')?.[1] ?? 'unknown'
if (result.status !== 0 || status !== (uploading ? '201' : '200')) {
console.error(`${method} failed: HTTP ${status}; cURL exit ${result.status ?? 'unknown'}.`)
throw new Error('Request failed.')
}
return result.stdout
}
request('PUT')
const downloaded = request('GET')
if (!original.equals(downloaded)) throw new Error('Downloaded bytes differ from the input.')
const sha256 = createHash('sha256').update(original).digest('hex')
console.log(`Verified ${original.length} bytes; SHA-256 ${sha256}`)
}
try {
main()
} catch {
console.error('Export failed. Check the input, account key, destination, and connection.')
process.exitCode = 1
}
The helper passes the authorization header to cURL through standard input with --header @-,
keeping the key and signature out of command arguments. It also disables .curlrc loading so a
saved cURL configuration cannot add retries or change output handling. Avoid shell tracing and
cURL trace/verbose options when handling credentials. See the cURL manual
for these options.
Upload and check the bytes
Replace the account, container, destination blob name, and key-file path below. The container must
already exist; choose a destination that does not. Run this command from the directory containing
azure-upload.mts and your input file:
node azure-upload.mts ./localfile.bin youraccount yourcontainer exports/localfile.bin < /secure/path/azure-account.key
A successful Put Blob
returns HTTP 201. The program then requires HTTP 200 from a separate authenticated download
and compares the complete response with the original input. Only after both checks pass does it
print Verified, the byte count, and the SHA-256 digest. A successful upload alone does not produce
that message. The blob remains in your private container; this program does not delete it.
Handle a refusal or uncertain result
The If-None-Match: * condition makes the upload create-only. Repeating the command with an existing destination
must fail and leave its bytes intact. Azure can report 409 with BlobAlreadyExists or a 412
condition failure; do not diagnose every conflict as an authentication problem. Microsoft lists
Blob Storage error codes.
The program reports only the HTTP status and cURL exit code, without printing provider error bodies.
A 403 needs an access check: confirm the account/key pairing, whether Shared Key is permitted,
and whether your network can access this account. Check the local file and key-file path if the
program fails before reporting an HTTP status. No automatic retries are enabled.
A broken connection can leave the write’s outcome uncertain: Azure may have stored the blob before the response was lost. A failed verification download also leaves the uploaded blob in place. Inspect that exact destination through your normal authenticated storage tooling and compare its downloaded bytes before choosing to keep it or remove it. Repeating this create-only command cannot confirm an existing blob’s contents and will not replace it. Do not delete an existing destination merely to make a retry succeed.
Choose another tool for larger transfers
The 8 MiB ceiling belongs to this memory-buffered example, not Azure’s service limit. It uses one
Put Blob request rather than staging and committing blocks. For larger files, resumability, or
batch automation, use a storage client that manages those transfers; this example makes no
multi-block or throughput guarantee. The Azure Blob client libraries
are a useful next step when you outgrow a small REST export.
