> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# URL Generation

> Public URL and Signed URL

Learn how to generate URLs for accessing files after saving.

## URL Generation Overview

<CardGroup cols={2}>
  <Card title="Public URL" icon="globe">
    Public access Permanent link
  </Card>

  <Card title="Signed URL" icon="lock">
    Time-limited access Enhanced security
  </Card>

  <Card title="Auto Generation" icon="wand-magic-sparkles">
    Auto after saveToDisk url, signedUrl properties
  </Card>

  <Card title="Storage Manager" icon="database">
    getUrl() getSignedUrl()
  </Card>
</CardGroup>

## UploadedFile URL Properties

### url Property (Public URL)

Public URL is automatically generated after calling `saveToDisk()`.

```typescript theme={null}
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,
    };
  }
}
```

<Info>`url` and `signedUrl` are only available after calling `saveToDisk()`.</Info>

## Generating URLs with Storage Manager

### getUrl() - Public URL

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
// Local storage URL
// http://localhost:3000/uploads/1736234567.jpg
```

Local storage generates Public URL based on app URL.

### AWS S3

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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)

```typescript theme={null}
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)

```typescript theme={null}
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

```typescript theme={null}
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");

    return {
      fileId: record.id,
      url: await disk.getUrl(key),
    };
  }
}
```

## URL Validity Check

### Check URL Expiration Time

```typescript theme={null}
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

<Warning>
  **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
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="File Upload Setup" icon="gear" href="/en/api-development/file-upload/setup">
    @upload decorator
  </Card>

  <Card title="UploadedFile Class" icon="file" href="/en/api-development/file-upload/uploaded-file-class">
    File info access
  </Card>

  <Card title="Saving Files" icon="floppy-disk" href="/en/api-development/file-upload/saving-files">
    Storage Manager
  </Card>

  <Card title="@api Decorator" icon="at" href="/en/api-development/creating-apis/api-decorator">
    API basic usage
  </Card>
</CardGroup>
