Azure Blob SAS links
Azure Blob Storage SAS links signed from the account key: browsers upload and download blobs directly, with the permissions and lifetime you choose.
Version 1.0.0 · 3 functions · 3 crates
Functions
What use azure_sas.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use azure_blob_url, azure_container_url from azure_sas (Quillo 0.6).
azure_blob_url(account: str, account_key: str, container: str, blob: str, permissions: str, seconds: int) → stror an errorA link to one blob, signed with the storage account's key (base64, from the portal's Access keys), for seconds (up to a year). permissions are letters: "r" read, "w" write, "c" create, "d" delete, "a" add (so "r" to download, "cw" to upload with a PUT and x-ms-blob-type: BlockBlob).
azure_container_url(account: str, account_key: str, container: str, permissions: str, seconds: int) → stror an errorA link to a whole container, for listing (l) or uploading many blobs: append /<blob name> before the ? for each one, or &restype=container&comp=list to list.
azure_sas_token(account: str, account_key: str, container: str, blob: str, permissions: str, seconds: int, start: str) → stror an errorOnly the SAS query string (sv=...&sig=...), for another host such as Azurite or a custom domain; blob "" signs the whole container. start is "" for now, or a Unix time in seconds (as text) to get the same token each time.
Get it
$ quillo install azure_sas # azure_sas.qwer into this folder$ quillo install azure_sas --example # and its example, azure_sas.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/azure_sas.qwer$ curl -O https://quillolang.org/plugins/files/azure_sas.qweThen, in your program, all of it or only what you call:
use azure_sas.qweruse azure_blob_url, azure_container_url from azure_sas // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
hmac = "0.12"sha2 = "0.10"base64 = "0.22"Example
// Invoices downloaded straight from Azure Blob Storage, and uploads that skip the server.//// AZURE_ACCOUNT=myshop AZURE_KEY=<key from Access keys> quillo run azure_sas.qwe then open http://localhost:8080/invoice/1042// (the default key is Azurite's well-known development key)use "azure_sas.qwer"let ACCOUNT = env("AZURE_ACCOUNT", "devstoreaccount1")let KEY = env("AZURE_KEY", "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==")route GET /invoice/:id(int) = redirect(azure_blob_url(ACCOUNT, KEY, "invoices", `2026/invoice-${id}.pdf`, "r", 300)?)route GET /upload-link { let name = `uploads/${uuid()}.jpg` // The page PUTs the file to it with the header x-ms-blob-type: BlockBlob. { name, put: azure_blob_url(ACCOUNT, KEY, "photos", name, "cw", 600)? }}route GET /token { let t = azure_sas_token(ACCOUNT, KEY, "invoices", "2026/invoice-1042.pdf", "rw", 3600, "1772355600")? { token: t, same_again: t == azure_sas_token(ACCOUNT, KEY, "invoices", "2026/invoice-1042.pdf", "wr", 3600, "1772355600")?, wrong: azure_sas_token(ACCOUNT, KEY, "invoices", "", "rz", 60, "").error }}$ quillo run azure_sas.qwe$ curl 'localhost:8080/upload-link'$ curl 'localhost:8080/token'Source
//! Azure Blob Storage SAS links from the account key: browsers upload and download blobs directly, with the permissions and lifetime you choose, signed with no SDK and no network call.//! version: 1.0.0//!//! [dependencies]//! hmac = "0.12"//! sha2 = "0.10"//! base64 = "0.22"use base64::engine::general_purpose::STANDARD;use base64::Engine;use hmac::{Hmac, Mac};use sha2::Sha256;const VERSION: &str = "2022-11-02";fn enc(s: &str, keep_slash: bool) -> String { let mut out = String::new(); for b in s.bytes() { if b.is_ascii_alphanumeric() || b"-_.~".contains(&b) || (keep_slash && b == b'/') { out.push(b as char); } else { out.push_str(&format!("%{:02X}", b)); } } out}fn civil(z: i64) -> (i64, i64, i64) { let z = z + 719468; let era = z.div_euclid(146097); let doe = z - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; (yoe + era * 400 + if m <= 2 { 1 } else { 0 }, m, d)}fn iso(secs: i64) -> String { let (y, m, d) = civil(secs.div_euclid(86400)); let r = secs.rem_euclid(86400); format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, r / 3600, r % 3600 / 60, r % 60)}fn now() -> i64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0)}/// The permission letters in the order Azure wants them, or an error naming a wrong one.fn perms(p: &str, resource: &str) -> Result<String, String> { let order = if resource == "c" { "racwdxltfmei" } else { "racwdxtmei" }; for ch in p.chars() { if !order.contains(ch) { return Err(format!("`{}` is not a permission here (use letters of {})", ch, order)); } } let out: String = order.chars().filter(|c| p.contains(*c)).collect(); if out.is_empty() { return Err("give at least one permission: r read, w write, c create, d delete, l list...".into()); } Ok(out)}/// The SAS query string for a blob (`blob` given) or a whole container (`blob` ""), valid from a minute ago for `seconds`.fn sas(account: &str, key: &str, container: &str, blob: &str, permissions: &str, seconds: i64, start: i64) -> Result<String, String> { if account.is_empty() || container.is_empty() { return Err("a SAS needs the account and the container".into()); } let key = STANDARD.decode(key.trim()).map_err(|_| "the account key is not base64 (it is the key the portal shows under Access keys)".to_string())?; let sr = if blob.is_empty() { "c" } else { "b" }; let sp = perms(permissions, sr)?; let (st, se) = (iso(start - 60), iso(start + seconds.clamp(1, 365 * 86400))); let resource = if blob.is_empty() { format!("/blob/{}/{}", account, container) } else { format!("/blob/{}/{}/{}", account, container, blob) }; // Permissions, start, expiry, resource, identifier, IP, protocol, version, resource kind, // snapshot time, encryption scope, and the five response headers: as version 2020-12-06 and later sign them. let to_sign = [sp.as_str(), &st, &se, &resource, "", "", "https", VERSION, sr, "", "", "", "", "", "", ""].join("\n"); let mut mac = Hmac::<Sha256>::new_from_slice(&key).map_err(|e| e.to_string())?; mac.update(to_sign.as_bytes()); let sig = STANDARD.encode(mac.finalize().into_bytes()); Ok(format!("sv={}&st={}&se={}&sr={}&sp={}&spr=https&sig={}", VERSION, enc(&st, false), enc(&se, false), sr, sp, enc(&sig, false)))}/// A link to one blob, signed with the storage account's key (base64, from the portal's Access keys), for `seconds` (up to a year)./// `permissions` are letters: "r" read, "w" write, "c" create, "d" delete, "a" add (so "r" to download, "cw" to upload with a PUT and `x-ms-blob-type: BlockBlob`).pub fn azure_blob_url(account: &str, account_key: &str, container: &str, blob: &str, permissions: &str, seconds: i64) -> Result<String, String> { if blob.is_empty() { return Err("name the blob, or use azure_container_url".into()); } let q = sas(account, account_key, container, blob, permissions, seconds, now())?; Ok(format!("https://{}.blob.core.windows.net/{}/{}?{}", account, enc(container, false), enc(blob, true), q))}/// A link to a whole container, for listing (`l`) or uploading many blobs: append `/<blob name>` before the `?` for each one, or `&restype=container&comp=list` to list.pub fn azure_container_url(account: &str, account_key: &str, container: &str, permissions: &str, seconds: i64) -> Result<String, String> { let q = sas(account, account_key, container, "", permissions, seconds, now())?; Ok(format!("https://{}.blob.core.windows.net/{}?{}", account, enc(container, false), q))}/// Only the SAS query string (`sv=...&sig=...`), for another host such as Azurite or a custom domain; `blob` "" signs the whole container./// `start` is "" for now, or a Unix time in seconds (as text) to get the same token each time.pub fn azure_sas_token(account: &str, account_key: &str, container: &str, blob: &str, permissions: &str, seconds: i64, start: &str) -> Result<String, String> { let t = if start.is_empty() { now() } else { start.trim().parse::<i64>().map_err(|_| format!("`{}` is not a Unix time", start))? }; sas(account, account_key, container, blob, permissions, seconds, t)}Related
gcs_sign.qwer · 1.0.0Google Cloud Storage V4 signed URLs from a service-account key: browsers upload and download directly, signed without a network call.6 views · 8 downloadsDropbox dropbox.qwer · 1.0.0Dropbox over its HTTP API: uploads in one request or by upload sessions that recover a lost answer, downloads, folders listed with cursors, shared links made or found again, a refresh token traded for short-lived tokens, and 429s waited out as Retry-After says.2 views · 10 downloadsS3 links s3.qwer · 1.0.0Signed upload and download links for S3, R2, MinIO and the like: browsers move the bytes, not your server.8 views · 9 downloadsContent-addressed files blob_store.qwer · 1.0.0Uploads kept in a folder by their SHA-256, as Git keeps objects: the same file stored once, names pointing at contents, atomic writes, every file verified against its hash, and contents nothing names swept away.6 views · 7 downloadsCloudinary cloudinary.qwer · 1.0.0Cloudinary without an SDK: signed uploads from a URL or a file, the signature a browser needs for a direct upload, delivery URLs with transformations and URL signatures, webhook and upload signatures checked, and assets listed by cursor.3 views · 9 downloadsGoogle Drive google_drive.qwer · 1.0.0Google Drive's v3 API: files listed with quoted queries and page tokens, uploads by multipart or a resumable session that recovers a lost piece, downloads and Google Doc exports, sharing, and service-account JWT sign-in.3 views · 7 downloadsThread 0 posts
On Azure Blob SAS links: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.