API Reference/Storage API

Presigned URLs

Secure file access with presigned URLs for private buckets

Presigned URLs

Presigned URLs allow secure access to private S3 files without exposing credentials. They're essential for serving files from private buckets.

Download URLs

Basic Presigned URL

// Generate URL valid for 1 hour (default)
const url = await storage.download.presignedUrl('private/document.pdf')

// Custom expiration (in seconds)
const url = await storage.download.presignedUrl('private/image.jpg', 3600) // 1 hour
const url = await storage.download.presignedUrl('private/video.mp4', 86400) // 24 hours

Direct File URLs

For public buckets, get direct URLs:

const publicUrl = await storage.download.url('public/image.jpg')
// Returns: https://bucket.s3.amazonaws.com/public/image.jpg

Upload URLs

Single Upload URL

const uploadUrl = await storage.upload.presignedUrl({
  key: 'uploads/new-file.jpg',
  contentType: 'image/jpeg',
  expiresIn: 300, // 5 minutes
  maxFileSize: 5 * 1024 * 1024 // 5MB
})

console.log(uploadUrl.url)        // Presigned URL for PUT request
console.log(uploadUrl.fields)     // Form fields for multipart upload

Batch Upload URLs

const requests = [
  { key: 'file1.jpg', contentType: 'image/jpeg' },
  { key: 'file2.pdf', contentType: 'application/pdf' },
  { key: 'file3.mp4', contentType: 'video/mp4' }
]

const urls = await storage.upload.presignedBatch(requests)

urls.forEach((result, index) => {
  if (result.success) {
    console.log(`Upload URL for ${requests[index].key}:`, result.url)
  } else {
    console.log(`Failed to generate URL:`, result.error)
  }
})

Frontend Usage Examples

Direct File Access

// API Route (app/api/files/[key]/route.ts)
import { storage } from '@/lib/upload'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(
  request: NextRequest,
  { params }: { params: { key: string } }
) {
  try {
    const url = await storage.download.presignedUrl(params.key, 3600)
    return NextResponse.redirect(url)
  } catch (error) {
    return NextResponse.json(
      { error: 'File not found' },
      { status: 404 }
    )
  }
}

File Viewer Component

'use client'

import { useState, useEffect } from 'react'

interface FileViewerProps {
  fileKey: string
}

export function FileViewer({ fileKey }: FileViewerProps) {
  const [url, setUrl] = useState<string>()
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    async function getUrl() {
      try {
        const response = await fetch('/api/presigned', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ 
            operation: 'get-download-url', 
            key: fileKey 
          })
        })
        
        const data = await response.json()
        if (data.success) {
          setUrl(data.url)
        }
      } finally {
        setLoading(false)
      }
    }

    getUrl()
  }, [fileKey])

  if (loading) return <div>Loading...</div>
  if (!url) return <div>Failed to load file</div>

  return (
    <div>
      <img src={url} alt="File preview" />
      <a href={url} download>Download File</a>
    </div>
  )
}

API Route for Presigned URLs

// app/api/presigned/route.ts
import { storage } from '@/lib/upload'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  try {
    const { operation, key, expiresIn = 3600 } = await request.json()

    switch (operation) {
      case 'get-download-url':
        const downloadUrl = await storage.download.presignedUrl(key, expiresIn)
        return NextResponse.json({
          success: true,
          url: downloadUrl,
          expiresIn
        })

      case 'get-upload-url':
        const uploadUrl = await storage.upload.presignedUrl({
          key,
          expiresIn,
          contentType: 'application/octet-stream'
        })
        return NextResponse.json({
          success: true,
          ...uploadUrl
        })

      default:
        return NextResponse.json(
          { success: false, error: 'Unknown operation' },
          { status: 400 }
        )
    }
  } catch (error) {
    return NextResponse.json(
      { success: false, error: error.message },
      { status: 500 }
    )
  }
}

Security Considerations

Expiration Times

Choose appropriate expiration times:

// Short-lived for sensitive files
const sensitiveUrl = await storage.download.presignedUrl('private/sensitive.pdf', 300) // 5 minutes

// Medium-lived for user content
const userUrl = await storage.download.presignedUrl('user/profile.jpg', 3600) // 1 hour

// Longer-lived for public content
const publicUrl = await storage.download.presignedUrl('public/banner.jpg', 86400) // 24 hours

Access Control

Implement proper access control before generating URLs:

// API Route with authentication
export async function POST(request: NextRequest) {
  // Verify user authentication
  const user = await getAuthenticatedUser(request)
  if (!user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { key } = await request.json()

  // Check if user can access this file
  if (!canUserAccessFile(user.id, key)) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
  }

  // Generate presigned URL
  const url = await storage.download.presignedUrl(key, 3600)
  return NextResponse.json({ url })
}

Type Definitions

interface PresignedUrlOptions {
  key: string
  contentType?: string
  expiresIn?: number
  maxFileSize?: number
  metadata?: Record<string, string>
}

interface PresignedUrlResult {
  url: string
  fields?: Record<string, string>
  expiresAt: Date
}