spaces-storage
DigitalOcean Spaces (S3-compatible) object storage patterns
What this skill does
# 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
```bash
doctl spaces create my-space --region nyc3
```
### CDN (Built-in)
Enable CDN for faster global access to your assets.
```bash
# 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)
```typescript
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)
```python
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
```typescript
// 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
```typescript
"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
```python
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
```bash
# .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:
```json
{
"CORSRules": [
{
"AllowedOrigins": ["https://myapp.com"],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3000
}
]
}
```
Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.