Services

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

#uploads#storage#azure#files#signing

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 error

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).

azure_container_url(account: str, account_key: str, container: str, permissions: str, seconds: int) → stror an error

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.

azure_sas_token(account: str, account_key: str, container: str, blob: str, permissions: str, seconds: int, start: str) → stror an error

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.

Get it

$ quillo install azure_sas               # azure_sas.qwer into this folder$ quillo install azure_sas --example     # and its example, azure_sas.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/azure_sas.qwer$ curl -O https://quillolang.org/plugins/files/azure_sas.qwe

Then, in your program, all of it or only what you call:

main.qwe
use azure_sas.qweruse azure_blob_url, azure_container_url from azure_sas      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
hmac = "0.12"sha2 = "0.10"base64 = "0.22"

Example

azure_sas.qwe
// 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_sas.qwer
//! 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)}
Google Cloud Storage links 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.uploadsstoragegcpfilessigning6 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.filesstorageoauthuploads2 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.uploadsstorages3r2files8 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.filesstorageuploadsdedupeintegrity6 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.imagesuploadscdnwebhookssigning3 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.filesgoogleuploadsservice-account3 views · 7 downloads
Thread
87Useful? Like it, or add to its thread below.

Thread 0 posts

On Azure Blob SAS links: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.