BunshipBunship
Features

Cloud Storage

Bunship uses AWS S3 compatible file storage systems

Bunship integrates with AWS S3 compatible cloud storage solutions for storing and managing various files such as user-uploaded images, documents, videos, and more. In addition to Amazon S3, Bunship also supports other storage services compatible with the S3 API, including MinIO, DigitalOcean Spaces, Cloudflare R2, and others.

Environment Configuration

To enable cloud storage functionality, configure the following environment variables in your .env file:

S3_ACCESS_KEY=<access_key_id>
S3_SECRET_KEY=<secret_key>
S3_REGION=<region, e.g., us-east-1>
S3_ENDPOINT=<endpoint_url, e.g., https://s3.amazonaws.com>
S3_BUCKET=<bucket_name>
S3_URL_BASE=<base_url_for_access>

Configuration Examples for Different Service Providers

Amazon S3

S3_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
S3_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
S3_REGION=us-east-1
S3_ENDPOINT=https://s3.amazonaws.com
S3_BUCKET=my-bucket-name
S3_URL_BASE=https://my-bucket-name.s3.amazonaws.com

MinIO

S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_REGION=us-east-1
S3_ENDPOINT=http://localhost:9000
S3_BUCKET=bunship
S3_URL_BASE=http://localhost:9000/bunship

Usage

Bunship provides an encapsulated S3 service class that can generate temporary upload URLs on the server side, allowing the frontend to upload files directly to cloud storage without going through the server.

S3 Server Configuration

1. Using Pre-signed URLs for File Upload

Bunship has implemented an S3 service class in @/lib/s3. Here's an example of server-side operations to generate pre-signed URLs:

"use server";
 
import { env } from "@/env";
import { S3Service } from "@/lib/s3";
import { unstable_noStore as noStore } from "next/cache";
 
export const getS3Sts = async (
  key: string,
  fileType: string,
  prefix = "/bunship-uploads",
) => {
  noStore();
  const s3 = new S3Service({
    endpoint: env.S3_ENDPOINT,
    region: env.S3_REGION,
    accessKeyId: env.S3_ACCESS_KEY,
    secretAccessKey: env.S3_SECRET_KEY,
    url: env.S3_URL_BASE,
    bucket: env.S3_BUCKET,
  });
  
  const res = await s3.getSts(key, {
    path: prefix!,
    acl: "public-read",
    ContentType: fileType,
  });
  
  return res;
};

This function returns:

  • putUrl: Temporary signed URL for uploading files
  • url: URL for accessing the file after upload
  • endpoint: S3 endpoint URL
  • completedUrl: Complete file access URL

2. Server-side Direct File Upload

If you need to upload files directly from the server:

"use server";
 
import { env } from "@/env";
import { S3Service } from "@/lib/s3";
import { unstable_noStore as noStore } from "next/cache";
 
export const uploadFile = async (
  key: string,
  file: Buffer,
  contentType: string
) => {
  noStore();
  const s3 = new S3Service({
    endpoint: env.S3_ENDPOINT,
    region: env.S3_REGION,
    accessKeyId: env.S3_ACCESS_KEY,
    secretAccessKey: env.S3_SECRET_KEY,
    url: env.S3_URL_BASE,
    bucket: env.S3_BUCKET,
  });
  
  const res = await s3.putItemInBucket(key, file, { ContentType: contentType });
  
  return res;
};

This function returns:

  • path: File path in the bucket
  • pathWithFilename: Complete file key name
  • filename: Filename
  • completedUrl: Complete file access URL
  • baseUrl: Base storage URL
  • mime: File MIME type
  • bucket: Bucket name

Frontend File Upload Example

Here's how to use the server-side operations to upload files from the frontend:

"use client";
 
import { useState } from "react";
import { Button } from "@bunship-ai/ui/components/button";
import { getS3Sts } from "@/actions/s3-action";
 
export function FileUploader() {
  const [isUploading, setIsUploading] = useState(false);
  const [fileUrl, setFileUrl] = useState("");
 
  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;
 
    try {
      setIsUploading(true);
 
      // 1. Get pre-signed URL
      const fileName = `${Date.now()}-${file.name}`;
      const { putUrl, completedUrl } = await getS3Sts(
        fileName,
        file.type
      );
 
      // 2. Upload file using pre-signed URL
      const uploadResponse = await fetch(putUrl, {
        method: "PUT",
        body: file,
        headers: {
          "Content-Type": file.type,
        },
      });
 
      if (uploadResponse.ok) {
        // 3. Upload successful, get file URL
        setFileUrl(completedUrl);
      } else {
        console.error("File upload failed");
      }
    } catch (error) {
      console.error("Error uploading file:", error);
    } finally {
      setIsUploading(false);
    }
  };
 
  return (
    <div className="space-y-4">
      <div>
        <input 
          type="file"
          onChange={handleUpload}
          disabled={isUploading}
        />
        {isUploading && <p>Uploading...</p>}
      </div>
      
      {fileUrl && (
        <div>
          <p>File uploaded:</p>
          <a href={fileUrl} target="_blank" rel="noopener noreferrer">
            {fileUrl}
          </a>
        </div>
      )}
    </div>
  );
}

Advanced Use Cases

Image Upload and Preview

Using avatar upload as an example, here's how to handle image upload and preview:

"use client";
 
import { useState } from "react";
import { Avatar } from "@bunship-ai/ui/components/avatar";
import { Button } from "@bunship-ai/ui/components/button";
import { getS3Sts } from "@/actions/s3-action";
 
export function AvatarUploader({ initialAvatarUrl = "" }) {
  const [avatarUrl, setAvatarUrl] = useState(initialAvatarUrl);
  const [isUploading, setIsUploading] = useState(false);
 
  const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;
    
    // Only accept image files
    if (!file.type.startsWith("image/")) {
      alert("Please select an image file");
      return;
    }
 
    setIsUploading(true);
    
    try {
      // 1. Create a local preview
      const localPreview = URL.createObjectURL(file);
      setAvatarUrl(localPreview);
      
      // 2. Upload to S3
      const fileName = `avatars/${Date.now()}-${file.name}`;
      const { putUrl, completedUrl } = await getS3Sts(
        fileName, 
        file.type,
        "/users"
      );
      
      await fetch(putUrl, {
        method: "PUT",
        body: file,
        headers: {
          "Content-Type": file.type,
        },
      });
      
      // 3. Update avatar with actual S3 URL
      setAvatarUrl(completedUrl);
      
      // 4. Logic to save user avatar URL to database could be added here
      
    } catch (error) {
      console.error("Failed to upload avatar:", error);
      // Revert to initial avatar
      setAvatarUrl(initialAvatarUrl);
    } finally {
      setIsUploading(false);
    }
  };
 
  return (
    <div className="flex flex-col items-center gap-4">
      <Avatar className="w-24 h-24">
        {avatarUrl ? (
          <img src={avatarUrl} alt="User avatar" />
        ) : (
          <div className="bg-muted flex items-center justify-center w-full h-full">
            User
          </div>
        )}
      </Avatar>
      
      <div>
        <input
          type="file"
          id="avatar-upload"
          className="hidden"
          accept="image/*"
          onChange={handleAvatarChange}
          disabled={isUploading}
        />
        <label htmlFor="avatar-upload">
          <Button as="span" variant="outline" disabled={isUploading}>
            {isUploading ? "Uploading..." : "Change Avatar"}
          </Button>
        </label>
      </div>
    </div>
  );
}

Security Considerations

The main advantages of using pre-signed URLs are:

  1. No need to expose sensitive S3 credentials to the client
  2. Precise control over permissions and expiration for each upload
  3. Server-side validation of file types, sizes, etc.

In production environments, we recommend:

  • Configuring appropriate CORS policies
  • Using temporary credentials instead of long-term ones
  • Setting reasonable expiration times for signed URLs
  • Implementing size limits for uploaded files