granite_assets.repositories
Local Nginx Repository
Local filesystem asset repository served by Nginx (or any static HTTP server).
Design constraints
Nginx itself is responsible for serving files; this library only writes/reads the filesystem.
Public assets are placed under
<storage_path>/<public_prefix>/and served at<base_url>/<public_prefix>/.Private assets are placed under
<storage_path>/<private_prefix>/.Signed URLs for private assets are supported when
LocalNginxAssetRepositoryConfig.secure_link_secretis set. The token algorithm matches Nginx’sngx_http_secure_link_modulewith thesecure_link_md5directive. Nginx validates tokens server-side without any round-trip to the application.When
secure_link_secretis not set, private asset URL methods raiseAssetAccessNotSupportedErrorso that callers know they must route access through their own application layer.
Signed URL algorithm (compatible with ngx_http_secure_link_module)
Given:
expires— Unix timestamp (int) when the URL expires.uri— Full URI path component of the asset URL, e.g./assets/private/reports/q1.pdf.
secret— Shared secret string (secure_link_secret).
The token is computed as:
raw = f"{expires}{uri} {secret}".encode("utf-8")
token = base64.urlsafe_b64encode(md5(raw).digest()).rstrip(b"=").decode()
The resulting URL is:
{base_url}/{private_prefix}/{key}?md5={token}&expires={expires}
The Nginx directive that validates this token is:
secure_link $arg_md5,$arg_expires;
secure_link_md5 "$secure_link_expires$uri YOUR_SECRET_HERE";
- class granite_assets.repositories.local_nginx.LocalNginxAssetRepository(config: LocalNginxAssetRepositoryConfig)[source]
Bases:
objectAsset repository backed by the local filesystem.
Files are organised under two sub-directories:
<storage_path>/<public_prefix>/– publicly served assets.<storage_path>/<private_prefix>/– private assets (Nginx-protected).
Public URLs are constructed by joining
base_url, the relevant prefix, and the logical key.Example:
config = LocalNginxAssetRepositoryConfig( storage_path="/var/www/assets", base_url="https://cdn.example.com/assets", ) repo = LocalNginxAssetRepository(config)
- __init__(config: LocalNginxAssetRepositoryConfig) None[source]
- save(request: AssetSaveRequest) AssetSaveResult[source]
Write the asset to disk.
- Raises:
AssetError – If the file already exists and overwrite is False.
- delete(key: str) None[source]
Remove the asset file from disk.
Tries both visibility prefixes so the caller does not need to know where the file is stored.
- copy(source_key: str, dest_key: str, *, overwrite: bool = True) None[source]
Copy a file on disk using shutil (server-side, no re-upload).
- move(source_key: str, dest_key: str, *, overwrite: bool = True) None[source]
Move (rename) a file on disk.
- get_descriptor(key: str) AssetDescriptor[source]
Return file metadata without reading the file body.
- build_public_url(key: str) AssetAccessUrl[source]
Return a permanent public URL.
Only valid for assets stored with
AssetVisibility.PUBLIC.- Raises:
AssetAccessNotSupportedError – If the asset is private (no signed URL support in this backend).
- build_download_url(key: str, ttl_seconds: int | None = None) AssetAccessUrl[source]
Return a download URL for the asset.
Public assets → permanent URL (no token required).
Private assets with ``secure_link_secret`` configured → signed URL valid for ttl_seconds seconds (default:
config.secure_link_ttl_seconds).Private assets without ``secure_link_secret`` → raises
AssetAccessNotSupportedError. The caller must proxy downloads through the application layer.
- Parameters:
key – Logical key of the asset (no leading slash).
ttl_seconds – Override the default signed-URL TTL for this call only.
- Returns:
AssetAccessUrlwithexpires_at=Nonefor public assets or a UTC datetime for signed private URLs.- Raises:
AssetAccessNotSupportedError – For private assets when no secret is configured, or when the asset is not found.
- build_upload_url(key: str, content_type: str, ttl_seconds: int | None = None, *, visibility: AssetVisibility = AssetVisibility.PRIVATE) UploadUrlResult[source]
Return a tus upload-creation URL pointing to the configured tusd server.
The returned
UploadUrlResultcarriesmethod="POST"and the required tus headers. The upload flow is:Client sends
POST {url}with the headers fromresult.headersandContent-Length: 0(tus creation request).tusd calls your pre-create hook to validate
upload-token.Client sends
PATCH {location}chunks until the upload is complete.tusd calls your post-finish hook to move the file into
{storage_path}/{visibility_prefix}/{key}.
Upload-Metadata fields embedded in the request:
asset-key— logical key for the asset.content-type— MIME type.visibility—"public"or"private".upload-expires— Unix timestamp when the token expires.upload-token— HMAC-SHA256 (hex) of"{expires}:{key}:{visibility}:{content_type}"signed withupload_secret.
- Parameters:
key – Logical key for the asset (no leading slash).
content_type – MIME type of the file to be uploaded.
ttl_seconds – Override the default
upload_ttl_secondsfor this call only. Governs how long the token is valid.visibility – Target visibility for the asset (keyword-only). Defaults to
AssetVisibility.PRIVATE.
- Returns:
UploadUrlResultwithmethod="POST"and tus-specific headers.- Raises:
AssetAccessNotSupportedError – If
tusd_urlorupload_secretis not configured.
- resolve_access(key: str, ttl_seconds: int | None = None) AssetAccessUrl[source]
Return the best available URL for the asset.
Public assets → permanent public URL.
Private assets with ``secure_link_secret`` configured → signed URL (same as
build_download_url()).Private assets without ``secure_link_secret`` → raises
AssetAccessNotSupportedError.
- Parameters:
key – Logical key of the asset (no leading slash).
ttl_seconds – TTL override forwarded to the signed-URL builder.
- Raises:
AssetNotFoundError – If the key does not exist under any visibility prefix.
AssetAccessNotSupportedError – If the asset is private and
secure_link_secretis not set.
S3 Repository
S3 asset repository backed by AWS S3 (or any S3-compatible store).
Design decisions
Presigned PUT is used for upload URLs instead of presigned POST. POST allows richer server-side validation (file-size limits, content-type enforcement) but requires a multipart form submission which complicates client-side HTTP libraries. PUT is a plain binary body, trivially consumed by
fetch,axios,requests,curl, and native mobile SDKs.boto3 is loaded lazily so that importing
granite_assetsin a project without thes3extra does not raiseImportErrorat module level. Only instantiatingS3AssetRepositorytriggers the import.Object keys in S3 are prefixed with
config.key_prefixwhen non-empty. The logical key exposed to callers never includes this prefix; the mapping is transparent.Public vs private is implemented via S3 object ACLs when the bucket allows it, or purely by policy. To keep the library simple we set
ACL='public-read'for public objects and no ACL for private objects. Callers must ensure their bucket policy is compatible. If you rely on a bucket policy instead of ACLs, setpublic_base_urland manage ACLs externally.
- class granite_assets.repositories.s3.S3AssetRepository(config: S3AssetRepositoryConfig)[source]
Bases:
objectAsset repository backed by AWS S3.
Instantiation is cheap; the boto3 session is created once and reused.
Example:
config = S3AssetRepositoryConfig( bucket="my-assets", region="eu-west-1", public_base_url="https://cdn.example.com", presign_ttl_seconds=3600, ) repo = S3AssetRepository(config)
- __init__(config: S3AssetRepositoryConfig) None[source]
- save(request: AssetSaveRequest) AssetSaveResult[source]
Upload an asset to S3.
Sets
ACL='public-read'for PUBLIC assets. Metadata and checksum are forwarded as S3 object metadata.
- delete(key: str) None[source]
Delete an S3 object.
- Raises:
AssetNotFoundError – If the key does not exist.
- copy(source_key: str, dest_key: str, *, overwrite: bool = True) None[source]
Server-side S3 copy (no data transfer to/from this process).
- move(source_key: str, dest_key: str, *, overwrite: bool = True) None[source]
Copy then delete (S3 has no native move operation).
- get_descriptor(key: str) AssetDescriptor[source]
Return S3 object metadata via
head_object.
- build_public_url(key: str) AssetAccessUrl[source]
Return the permanent public URL for a PUBLIC asset.
If
public_base_urlis configured, uses that as base (CDN URL). Otherwise builds a standard virtual-hosted S3 URL.- Raises:
AssetAccessNotSupportedError – If called for a PRIVATE asset key that is known to be private (best-effort; requires a head_object call not performed here for performance).
- build_download_url(key: str, ttl_seconds: int | None = None) AssetAccessUrl[source]
Generate a download URL for a private asset.
Priority order:
cf_key_id+cf_private_keyset andcf_signing_method=URL→ CloudFront signed URL (canned policy, query-param credentials).cf_key_id+cf_private_keyset andcf_signing_method=COOKIE→ plain CloudFront URL (no signature). The browser must already hold the signed cookies obtained viabuild_signed_cookies().cf_unsigned_urls=True+public_base_urlset → plain CloudFront URL (permanent, no signature).Fallback → S3 presigned URL (time-limited, exposes S3 domain).
- build_path_signed_url(key: str, *, path_pattern: str | None = None, ttl_seconds: int | None = None) AssetAccessUrl[source]
Generate a CloudFront URL with custom-policy signing for key.
Unlike the canned-policy URL returned by
build_download_url(), the custom policy can authorise a wildcard path so that a single set of query-param credentials is valid for every file under a directory. This is the recommended approach for HLS/DASH video streaming where the player autonomously fetches dozens of segment files.- Parameters:
key – Logical key of the file whose URL is returned (e.g.
"private/videos/uuid/master.m3u8"). Must not start with/.path_pattern – CloudFront resource pattern to embed in the policy. Accepts a trailing
*wildcard (e.g."private/videos/uuid/*"). Defaults to the directory of key +/*.ttl_seconds – URL lifetime in seconds (default: configured TTL).
- Returns:
AssetAccessUrlwhoseurlcarries?Policy=…&Signature=…&Key-Pair-Id=…query params.- Raises:
AssetConfigurationError – if CloudFront signing is not configured.
- build_folder_signed_url(key: str, *, entry_filename: str, ttl_seconds: int | None = None) AssetAccessUrl[source]
Generate a CloudFront URL with wildcard custom-policy for the folder of key, pointing to entry_filename within that folder.
This is the recommended method for composite assets such as HLS/DASH video streams. The source asset (e.g. a transcoded
.mp4) and the player entry point (e.g.master.m3u8) live in the same S3 folder. A single set of credentials authorises the manifest and all segment files that the player fetches from relative paths.Usage example:
# key layout in S3: # assets/<uuid>/<uuid>.mp4 ← original source # assets/<uuid>/master.m3u8 ← HLS manifest # assets/<uuid>/1080p/index.m3u8 # assets/<uuid>/1080p/seg000.ts … seg009.ts url = repo.build_folder_signed_url( "assets/<uuid>/<uuid>.mp4", entry_filename="master.m3u8", ttl_seconds=7200, ) # Returns: # https://<cf>/assets/<uuid>/master.m3u8 # ?Policy=<wildcard over assets/<uuid>/*> # &Signature=...&Key-Pair-Id=...
Pass
url.urldirectly to HLS.js as the source — the query-string credentials are inherited by all relative segment requests.- Parameters:
key – Logical key of any file that belongs to the target folder. Used solely to derive the folder path; the URL will not point to this file. Example:
"assets/<uuid>/<uuid>.mp4".entry_filename – Filename of the player entry point within the same folder. Example:
"master.m3u8".ttl_seconds – Lifetime of the signing credentials in seconds (default: configured
presign_ttl_seconds). A new URL with a new expiry is generated on every call; credentials are not cached.
- Returns:
AssetAccessUrlwhoseurlishttps://<cf>/<folder>/<entry_filename>?Policy=…&Signature=…&Key-Pair-Id=…and whoseexpires_atreflects the policy expiry.- Raises:
AssetConfigurationError – CloudFront signing is not configured (
cf_key_idorcf_private_keynot set).AssetError – key has no directory component (it is a root-level key with no
/separator).
- build_signed_cookies(key_pattern: str, ttl_seconds: int | None = None) CfSignedCookies[source]
Generate CloudFront signed-cookie values for key_pattern.
Call this once per session (or per resource group) and set the returned values as
HttpOnly; Secure; SameSite=Nonecookies on the response. The browser will then include them automatically on every CloudFront request that matches the policy path.- Parameters:
key_pattern – Logical key pattern (may include a trailing
*wildcard) relative to the configured key prefix. Example:"private/videos/uuid/*".ttl_seconds – Cookie lifetime in seconds (default: configured TTL).
- Returns:
CfSignedCookieswithpolicy,signature, andkey_pair_idvalues ready to set as cookies.- Raises:
AssetConfigurationError – if CloudFront signing is not configured.
- build_upload_url(key: str, content_type: str, ttl_seconds: int | None = None) UploadUrlResult[source]
Generate a presigned PUT URL for client-side upload.
The client must send the file as an HTTP PUT with the
Content-Typeheader set to exactly the value provided here. No other headers are required by default.Example (using
requests):result = repo.build_upload_url("images/photo.jpg", "image/jpeg") with open("photo.jpg", "rb") as f: requests.put(result.url, data=f, headers=result.headers)