Skip to main content
Learn how to generate URLs for accessing files after saving.

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 calling saveToDisk().
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

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
Local storage generates Public URL based on app URL.

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