URL Generation Overview
Public URL
Public access Permanent link
Signed URL
Time-limited access Enhanced security
Auto Generation
Auto after saveToDisk url, signedUrl properties
Storage Manager
getUrl() getSignedUrl()
UploadedFile URL Properties
url Property (Public URL)
Public URL is automatically generated after callingsaveToDisk().
class FileModel extends BaseModelClass {
@upload()
async upload(): Promise<{
url: string;
signedUrl: string;
}> {
const { bufferedFiles } = Sonamu.getContext();
const file = bufferedFiles?.[0];
if (!file) throw new Error("File is required");
// Save file
await file.saveToDisk("fs", `uploads/${Date.now()}.${file.extname}`);
// URLs auto-generated
const publicUrl = file.url!; // Public URL
const signedUrl = file.signedUrl!; // Signed URL
return {
url: publicUrl,
signedUrl,
};
}
}
url and signedUrl are only available after calling saveToDisk().Generating URLs with Storage Manager
getUrl() - Public URL
import { Sonamu } from "sonamu";
class FileModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async getFileUrl(fileId: number): Promise<{ url: string }> {
const rdb = this.getPuri("r");
const file = await rdb.table("files").where("id", fileId).first();
if (!file) {
throw new Error("File not found");
}
// Generate URL with Storage Manager
const disk = Sonamu.storage.use(file.disk_name);
const url = await disk.getUrl(file.key);
return { url };
}
}
getSignedUrl() - Signed URL
class FileModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async getDownloadUrl(
fileId: number,
expiresIn: number = 3600, // 1 hour
): Promise<{
url: string;
expiresAt: Date;
}> {
const rdb = this.getPuri("r");
const file = await rdb.table("files").where("id", fileId).first();
if (!file) {
throw new Error("File not found");
}
// Generate Signed URL
const disk = Sonamu.storage.use(file.disk_name);
const url = await disk.getSignedUrl(file.key, expiresIn);
const expiresAt = new Date(Date.now() + expiresIn * 1000);
return {
url,
expiresAt,
};
}
}
URL Formats by Storage
Local Storage
// Local storage URL
// http://localhost:3000/uploads/1736234567.jpg
AWS S3
// S3 Public URL
// https://my-bucket.s3.us-east-1.amazonaws.com/uploads/1736234567.jpg
// S3 Signed URL
// https://my-bucket.s3.us-east-1.amazonaws.com/uploads/1736234567.jpg?
// X-Amz-Algorithm=AWS4-HMAC-SHA256&
// X-Amz-Credential=...&
// X-Amz-Date=...&
// X-Amz-Expires=3600&
// X-Amz-Signature=...&
// X-Amz-SignedHeaders=host
Google Cloud Storage
// GCS Public URL
// https://storage.googleapis.com/my-bucket/uploads/1736234567.jpg
// GCS Signed URL
// https://storage.googleapis.com/my-bucket/uploads/1736234567.jpg?
// GoogleAccessId=...&
// Expires=...&
// Signature=...
Practical Examples
File Download URL
class FileModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async getDownloadUrl(fileId: number): Promise<{
url: string;
filename: string;
expiresAt: Date;
}> {
const context = Sonamu.getContext();
const rdb = this.getPuri("r");
const file = await rdb.table("files").where("id", fileId).first();
if (!file) {
throw new Error("File not found");
}
// Download log
const wdb = this.getPuri("w");
await wdb.table("download_logs").insert({
file_id: fileId,
user_id: context.user?.id,
ip_address: context.request.ip,
user_agent: context.request.headers["user-agent"],
downloaded_at: new Date(),
});
// Generate Signed URL (1 hour)
const disk = Sonamu.storage.use(file.disk_name);
const url = await disk.getSignedUrl(file.key, 3600);
const expiresAt = new Date(Date.now() + 3600 * 1000);
return {
url,
filename: file.filename,
expiresAt,
};
}
}
Image URLs by Size
class ImageModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async getImageUrls(imageId: number): Promise<{
original: string;
thumbnail: string;
medium: string;
}> {
const rdb = this.getPuri("r");
const image = await rdb.table("images").where("id", imageId).first();
if (!image) {
throw new Error("Image not found");
}
const disk = Sonamu.storage.use(image.disk_name);
return {
original: await disk.getUrl(image.original_key),
thumbnail: await disk.getUrl(image.thumbnail_key),
medium: await disk.getUrl(image.medium_key),
};
}
}
Permission-Based URL Generation
class DocumentModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async getDocumentUrl(documentId: number): Promise<{
url: string;
type: "public" | "signed";
}> {
const context = Sonamu.getContext();
const rdb = this.getPuri("r");
const document = await rdb.table("documents").where("id", documentId).first();
if (!document) {
throw new Error("Document not found");
}
const disk = Sonamu.storage.use(document.disk_name);
// Public documents get Public URL
if (document.is_public) {
const url = await disk.getUrl(document.key);
return {
url,
type: "public",
};
}
// Private documents get Signed URL
if (!context.user) {
throw new Error("Authentication required");
}
// Check permission
if (document.user_id !== context.user.id && context.user.role !== "admin") {
throw new Error("Access denied");
}
const url = await disk.getSignedUrl(document.key, 3600);
return {
url,
type: "signed",
};
}
}
Batch URL Generation
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async getBatchUrls(params: { fileIds: number[] }): Promise<{
urls: Record<
number,
{
url: string;
signedUrl: string;
}
>;
}> {
const { fileIds } = params;
if (fileIds.length > 100) {
throw new Error("Maximum 100 files at once");
}
const rdb = this.getPuri("r");
const files = await rdb.table("files").whereIn("id", fileIds).select("*");
const urls: Record<
number,
{
url: string;
signedUrl: string;
}
> = {};
for (const file of files) {
const disk = Sonamu.storage.use(file.disk_name);
urls[file.id] = {
url: await disk.getUrl(file.key),
signedUrl: await disk.getSignedUrl(file.key, 3600),
};
}
return { urls };
}
}
URL Caching
Redis Caching
import Redis from "ioredis";
class FileModel extends BaseModelClass {
private redis = new Redis(process.env.REDIS_URL);
@api({ httpMethod: "GET" })
async getSignedUrlCached(fileId: number): Promise<{
url: string;
expiresAt: Date;
}> {
// Check cache
const cacheKey = `signed-url:${fileId}`;
const cached = await this.redis.get(cacheKey);
if (cached) {
const data = JSON.parse(cached);
return data;
}
// Query file info
const rdb = this.getPuri("r");
const file = await rdb.table("files").where("id", fileId).first();
if (!file) {
throw new Error("File not found");
}
// Generate Signed URL
const disk = Sonamu.storage.use(file.disk_name);
const expiresIn = 3600; // 1 hour
const url = await disk.getSignedUrl(file.key, expiresIn);
const expiresAt = new Date(Date.now() + expiresIn * 1000);
const result = { url, expiresAt };
// Save to cache (shorter than expiration time)
await this.redis.setex(
cacheKey,
expiresIn - 60, // 1 minute buffer
JSON.stringify(result),
);
return result;
}
}
CDN URL
CloudFront (AWS)
class FileModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async getCDNUrl(fileId: number): Promise<{ url: string }> {
const rdb = this.getPuri("r");
const file = await rdb.table("files").where("id", fileId).first();
if (!file) {
throw new Error("File not found");
}
// Generate CloudFront URL
const cloudFrontDomain = process.env.CLOUDFRONT_DOMAIN;
if (cloudFrontDomain) {
const url = `https://${cloudFrontDomain}/${file.key}`;
return { url };
}
// Regular URL if no CloudFront
const disk = Sonamu.storage.use(file.disk_name);
const url = await disk.getUrl(file.key);
return { url };
}
}
Cloud CDN (GCP)
class FileModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async getCDNUrl(fileId: number): Promise<{ url: string }> {
const rdb = this.getPuri("r");
const file = await rdb.table("files").where("id", fileId).first();
if (!file) {
throw new Error("File not found");
}
// Generate Cloud CDN URL
const cdnDomain = process.env.CLOUD_CDN_DOMAIN;
if (cdnDomain) {
const url = `https://${cdnDomain}/${file.key}`;
return { url };
}
// Regular URL if no CDN
const disk = Sonamu.storage.use(file.disk_name);
const url = await disk.getUrl(file.key);
return { url };
}
}
Presigned Upload URL (Client Direct Upload)
Client Direct Upload
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async getUploadUrl(params: { filename: string; contentType: string }): Promise<{
uploadUrl: string;
key: string;
}> {
const { filename, contentType } = params;
// Generate safe key
const ext = filename.split(".").pop();
const key = `uploads/${Date.now()}.${ext}`;
// Generate Presigned Upload URL (5 minutes)
const disk = Sonamu.storage.use();
const uploadUrl = await disk.getSignedUrl(key, 300);
return {
uploadUrl,
key,
};
}
@api({ httpMethod: "POST" })
async confirmUpload(params: {
key: string;
filename: string;
contentType: string;
size: number;
}): Promise<{
fileId: number;
url: string;
}> {
const { key, filename, contentType, size } = params;
// Check file existence
const disk = Sonamu.storage.use();
const exists = await disk.exists(key);
if (!exists) {
throw new Error("File not uploaded");
}
// Save metadata to DB
const wdb = this.getPuri("w");
const [record] = await wdb
.table("files")
.insert({
key,
filename,
mime_type: contentType,
size,
url: await disk.getUrl(key),
})
.returning({ id: "id" });
return {
fileId: record.id,
url: await disk.getUrl(key),
};
}
}
URL Validity Check
Check URL Expiration Time
class FileModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async checkUrlValidity(url: string): Promise<{
valid: boolean;
expiresAt?: Date;
}> {
// Extract expiration time from Signed URL
const urlObj = new URL(url);
const expires = urlObj.searchParams.get("Expires") || urlObj.searchParams.get("X-Amz-Date");
if (!expires) {
// Public URL (no expiration)
return { valid: true };
}
const expiresAt = new Date(parseInt(expires) * 1000);
const valid = expiresAt > new Date();
return {
valid,
expiresAt,
};
}
}
Cautions
Cautions when generating URLs: 1. Set expiration time for Signed URLs 2. Use Public URLs
carefully as they are permanent 3. Consider cache invalidation when using CDN 4. Recommend URL
logging for usage tracking 5. Use Signed URLs for sensitive files
Next Steps
File Upload Setup
@upload decorator
UploadedFile Class
File info access
Saving Files
Storage Manager
@api Decorator
API basic usage