spaces-storage

SKILL.md

Spaces Object Storage Skill

Overview

DigitalOcean Spaces provides S3-compatible object storage. Use it for:

  • File uploads (images, documents, media)
  • Static asset hosting
  • Application backups
  • Data lake storage

Setup

Create Space

doctl spaces create my-space --region nyc3

CDN (Built-in)

Enable CDN for faster global access to your assets.

# CDN endpoint
https://my-space.nyc3.cdn.digitaloceanspaces.com/file.jpg

# Direct endpoint
https://my-space.nyc3.digitaloceanspaces.com/file.jpg

SDK Integration

Node.js (@aws-sdk/client-s3)

import {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectCommand,
  ListObjectsV2Command,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({
  endpoint: 'https://nyc3.digitaloceanspaces.com',
  region: 'nyc3',
  credentials: {
    accessKeyId: process.env.DIGITALOCEAN_SPACES_ACCESS_KEY!,
    secretAccessKey: process.env.DIGITALOCEAN_SPACES_SECRET_KEY!,
  },
});

const BUCKET = 'my-space';

// Upload file
async function uploadFile(key: string, body: Buffer, contentType: string) {
  await s3.send(
    new PutObjectCommand({
      Bucket: BUCKET,
      Key: key,
      Body: body,
      ContentType: contentType,
      ACL: 'public-read', // or "private"
    })
  );

  return `https://${BUCKET}.nyc3.cdn.digitaloceanspaces.com/${key}`;
}

// Download file
async function downloadFile(key: string) {
  const response = await s3.send(
    new GetObjectCommand({
      Bucket: BUCKET,
      Key: key,
    })
  );

  return response.Body;
}

// Generate presigned URL (for private uploads)
async function getPresignedUploadUrl(key: string, expiresIn = 3600) {
  const command = new PutObjectCommand({
    Bucket: BUCKET,
    Key: key,
  });

  return getSignedUrl(s3, command, { expiresIn });
}

// Generate presigned download URL (for private files)
async function getPresignedDownloadUrl(key: string, expiresIn = 3600) {
  const command = new GetObjectCommand({
    Bucket: BUCKET,
    Key: key,
  });

  return getSignedUrl(s3, command, { expiresIn });
}

// List files
async function listFiles(prefix = '') {
  const response = await s3.send(
    new ListObjectsV2Command({
      Bucket: BUCKET,
      Prefix: prefix,
    })
  );

  return response.Contents || [];
}

// Delete file
async function deleteFile(key: string) {
  await s3.send(
    new DeleteObjectCommand({
      Bucket: BUCKET,
      Key: key,
    })
  );
}

Python (boto3)

import boto3
from botocore.config import Config

session = boto3.session.Session()
s3 = session.client(
    's3',
    region_name='nyc3',
    endpoint_url='https://nyc3.digitaloceanspaces.com',
    aws_access_key_id=os.environ['DIGITALOCEAN_SPACES_ACCESS_KEY'],
    aws_secret_access_key=os.environ['DIGITALOCEAN_SPACES_SECRET_KEY']
)

BUCKET = 'my-space'

# Upload file
def upload_file(key: str, file_path: str, content_type: str = None):
    extra_args = {'ACL': 'public-read'}
    if content_type:
        extra_args['ContentType'] = content_type

    s3.upload_file(file_path, BUCKET, key, ExtraArgs=extra_args)
    return f'https://{BUCKET}.nyc3.cdn.digitaloceanspaces.com/{key}'

# Upload from memory
def upload_bytes(key: str, data: bytes, content_type: str):
    s3.put_object(
        Bucket=BUCKET,
        Key=key,
        Body=data,
        ContentType=content_type,
        ACL='public-read'
    )
    return f'https://{BUCKET}.nyc3.cdn.digitaloceanspaces.com/{key}'

# Download file
def download_file(key: str, file_path: str):
    s3.download_file(BUCKET, key, file_path)

# Generate presigned URL
def get_presigned_url(key: str, expires_in: int = 3600):
    return s3.generate_presigned_url(
        'get_object',
        Params={'Bucket': BUCKET, 'Key': key},
        ExpiresIn=expires_in
    )

# List files
def list_files(prefix: str = ''):
    response = s3.list_objects_v2(Bucket=BUCKET, Prefix=prefix)
    return response.get('Contents', [])

# Delete file
def delete_file(key: str):
    s3.delete_object(Bucket=BUCKET, Key=key)

Next.js Integration

API Route for File Upload

// app/api/upload/route.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { nanoid } from 'nanoid';

const s3 = new S3Client({
  endpoint: 'https://nyc3.digitaloceanspaces.com',
  region: 'nyc3',
  credentials: {
    accessKeyId: process.env.DIGITALOCEAN_SPACES_ACCESS_KEY!,
    secretAccessKey: process.env.DIGITALOCEAN_SPACES_SECRET_KEY!,
  },
});

export async function POST(request: Request) {
  const { filename, contentType } = await request.json();

  const key = `uploads/${nanoid()}-${filename}`;

  const command = new PutObjectCommand({
    Bucket: process.env.DIGITALOCEAN_SPACES_BUCKET!,
    Key: key,
    ContentType: contentType,
    ACL: 'public-read',
  });

  const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
  const publicUrl = `https://${process.env.DIGITALOCEAN_SPACES_BUCKET}.nyc3.cdn.digitaloceanspaces.com/${key}`;

  return Response.json({ uploadUrl, publicUrl, key });
}

Client Upload Component

"use client";

import { useState } from "react";

export function FileUpload({ onUpload }: { onUpload: (url: string) => void }) {
  const [uploading, setUploading] = useState(false);

  async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;

    setUploading(true);

    try {
      // Get presigned URL
      const response = await fetch("/api/upload", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          filename: file.name,
          contentType: file.type,
        }),
      });

      const { uploadUrl, publicUrl } = await response.json();

      // Upload directly to Spaces
      await fetch(uploadUrl, {
        method: "PUT",
        headers: { "Content-Type": file.type },
        body: file,
      });

      onUpload(publicUrl);
    } finally {
      setUploading(false);
    }
  }

  return (
    <input
      type="file"
      onChange={handleUpload}
      disabled={uploading}
    />
  );
}

FastAPI Integration

from fastapi import FastAPI, UploadFile
import boto3
from nanoid import generate

app = FastAPI()

s3 = boto3.client(
    's3',
    region_name='nyc3',
    endpoint_url='https://nyc3.digitaloceanspaces.com',
    aws_access_key_id=os.environ['DIGITALOCEAN_SPACES_ACCESS_KEY'],
    aws_secret_access_key=os.environ['DIGITALOCEAN_SPACES_SECRET_KEY']
)

BUCKET = os.environ['DIGITALOCEAN_SPACES_BUCKET']

@app.post("/upload")
async def upload_file(file: UploadFile):
    key = f"uploads/{generate()}-{file.filename}"

    s3.upload_fileobj(
        file.file,
        BUCKET,
        key,
        ExtraArgs={
            'ACL': 'public-read',
            'ContentType': file.content_type
        }
    )

    return {
        "url": f"https://{BUCKET}.nyc3.cdn.digitaloceanspaces.com/{key}",
        "key": key
    }

Environment Variables

# .env
DIGITALOCEAN_SPACES_ACCESS_KEY=your_access_key
DIGITALOCEAN_SPACES_SECRET_KEY=your_secret_key
DIGITALOCEAN_SPACES_BUCKET=my-space
DIGITALOCEAN_SPACES_REGION=nyc3
DIGITALOCEAN_SPACES_ENDPOINT=https://nyc3.digitaloceanspaces.com

CORS Configuration

Configure CORS via the DigitalOcean console or API:

{
  "CORSRules": [
    {
      "AllowedOrigins": ["https://myapp.com"],
      "AllowedMethods": ["GET", "PUT", "POST"],
      "AllowedHeaders": ["*"],
      "MaxAgeSeconds": 3000
    }
  ]
}
Weekly Installs
4
GitHub Stars
3
First Seen
Feb 11, 2026
Installed on
opencode4
gemini-cli4
claude-code4
github-copilot4
codex4
amp4