URL Generation Overview
Public URL
Public accessPermanent link
Signed URL
Time-limited accessEnhanced security
Auto Generation
Auto after saveToDiskurl, signedUrl properties
Storage Manager
getUrl()getSignedUrl()
UploadedFile URL Properties
url Property (Public URL)
Public URL is automatically generated after callingsaveToDisk().
Copy
class FileModel extends BaseModelClass {
@api({ httpMethod: "POST" })
@upload({ mode: "single" })
async upload(params: {
file: UploadedFile;
}): Promise<{
url: string;
signedUrl: string;
}> {
const { file } = params;
// Save file
await file.saveToDisk(`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
Copy
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
Copy
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
Copy
// Local storage URL
// http://localhost:3000/uploads/1736234567.jpg
AWS S3
Copy
// 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
Copy
// 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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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)
Copy
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)
Copy
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
Copy
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
Copy
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:
- Set expiration time for Signed URLs
- Use Public URLs carefully as they are permanent
- Consider cache invalidation when using CDN
- Recommend URL logging for usage tracking
- Use Signed URLs for sensitive files