| name | beamdrop |
| description | Interact with a Beamdrop file storage server โ upload, download, and manage files via the S3-compatible API. Use when the user wants to store files, create buckets, generate presigned/shareable URLs, manage API keys, set up webhooks, connect via MCP, or integrate Beamdrop into their project. Covers Go SDK, HTTP API, presigned URL strategies, webhooks, MCP server, and error handling. |
| license | MIT |
| metadata | {"author":"Tachera Sasi","repository":"https://github.com/ekilie/beamdrop-skills","version":"1.0.0","keywords":"ai, agent, skill, file-storage, s3, presigned-urls"} |
| references | ["references/agent-instructions.md"] |
Beamdrop File Storage Skill
When to Use This Skill
Use this skill when the user asks to:
- Upload, download, or manage files on a Beamdrop server
- Create, list, or delete storage buckets
- Generate presigned or shareable download URLs
- Manage Beamdrop API keys (create, list, delete, scope permissions)
- Store AI-generated artifacts (code, images, documents, build outputs) on Beamdrop
- Set up Beamdrop integration in their Go, Python, JavaScript, PHP, or any project
- Compare presigned URL types or decide on a file-sharing strategy
- Set up webhooks for real-time event notifications (object, bucket, share, presign events)
- Connect AI assistants via the built-in MCP server at
/mcp
- Debug Beamdrop API errors (401, 404, 409, 429)
- Configure Beamdrop for production deployment
Prerequisites
The user needs a running Beamdrop instance with API authentication enabled:
beamdrop -dir /path/to/share -api-auth
They need these environment variables or config values:
BEAMDROP_BASE_URL โ The server URL (e.g., http://localhost:7777)
BEAMDROP_ACCESS_KEY_ID โ API access key (format: BDK_ + 16 hex chars)
BEAMDROP_SECRET_KEY โ API secret key (format: sk_ + 40 hex chars)
If the user doesn't have API keys yet, they can create them:
curl -X POST http://localhost:7777/api/v1/keys
Authentication Details
All S3 API requests at /api/v1/ use HMAC-SHA256 signing:
StringToSign = METHOD + "\n" + PATH + "\n" + RFC3339_TIMESTAMP
Signature = Base64(HMAC-SHA256(StringToSign, SecretKey))
- PATH is the URL path only โ no query string. Example:
/api/v1/buckets/my-bucket
- TIMESTAMP is RFC3339 UTC (e.g.,
2024-01-15T10:30:00Z). Must be within ยฑ15 minutes of server time.
- Base64 is standard encoding (not URL-safe)
Headers required on every request:
Authorization: Bearer BDK_xxxx:SIGNATURE
X-Beamdrop-Date: 2024-01-15T10:30:00Z
API key properties:
permissions: "read", "write", or "read,write"
bucketScope: Optional โ restricts key to a single bucket
expiresAt: Optional โ key auto-expires after this time
disabled: Can be set to temporarily disable without deleting
Go Client SDK
When generating Go code, always use the official client SDK at github.com/ekilie/beamdrop/pkg/client:
import "github.com/ekilie/beamdrop/pkg/client"
c, err := client.New(client.Config{
BaseURL: os.Getenv("BEAMDROP_BASE_URL"),
AccessKeyID: os.Getenv("BEAMDROP_ACCESS_KEY_ID"),
SecretKey: os.Getenv("BEAMDROP_SECRET_KEY"),
})
if err != nil {
log.Fatal(err)
}
Bucket Operations
buckets, err := c.ListBuckets(ctx)
created, err := c.CreateBucket(ctx, "my-bucket")
created, err := c.CreateBucketIfNotExists(ctx, "my-bucket")
exists, err := c.BucketExists(ctx, "my-bucket")
err = c.DeleteBucket(ctx, "my-bucket")
Bucket name rules: 3-63 chars, regex ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$, no IP-like names.
Object Operations
uploaded, err := c.PutObject(ctx, "bucket", "path/to/file.txt", []byte("content"))
f, _ := os.Open("large-file.bin")
defer f.Close()
uploaded, err = c.PutObjectReader(ctx, "bucket", "large-file.bin", f)
obj, err := c.GetObject(ctx, "bucket", "path/to/file.txt")
fmt.Println(string(obj.Body))
fmt.Println(obj.ContentType)
fmt.Println(obj.ETag)
meta, err := c.HeadObject(ctx, "bucket", "path/to/file.txt")
fmt.Printf("Size: %d, Type: %s\n", meta.ContentLength, meta.ContentType)
exists, err := c.ObjectExists(ctx, "bucket", "key")
err = c.DeleteObject(ctx, "bucket", "path/to/file.txt")
list, err := c.ListObjects(ctx, "bucket", client.ListObjectsOptions{
Prefix: "folder/",
Delimiter: "/",
MaxKeys: 100,
})
Object key rules: max 1024 bytes, no .., no leading /. Forward slashes create virtual directory hierarchies.
Max upload size: 5GB. Writes are atomic (crash-safe). ETag = MD5 hex hash of content.
Presigned URLs โ Choosing the Right Type
Beamdrop has two presigned URL mechanisms. Always choose deliberately โ they have different trade-offs:
Client-side HMAC presigned URLs
Generated locally using your secret key. No server API call needed. Self-contained URL.
url, err := c.PresignObjectURL("GET", "bucket", "file.txt", time.Now().Add(24*time.Hour))
Use when:
- You need zero server overhead (URL computed locally)
- Generating many links in a batch
- Embedding in automated emails or notifications
- Temporary access with no tracking needed
Limitations:
- Cannot be revoked (valid until expiry)
- No download counting or limits
- Key rotation invalidates ALL outstanding client-side URLs
- URL is long (contains bucket/key path + query params)
Server-side presigned URLs (RECOMMENDED for most use cases)
Created via API call, stored in database with a short 32-char hex token. Clean /dl/{token} URLs.
presigned, err := c.CreatePresignedURL(ctx, client.CreatePresignedURLRequest{
Bucket: "bucket",
Key: "file.txt",
Method: "GET",
ExpiresIn: int64Ptr(3600),
MaxDownloads: intPtr(10),
})
urls, err := c.ListPresignedURLs(ctx)
details, err := c.GetPresignedURL(ctx, "token")
fmt.Printf("Downloaded %d/%d times\n", details.DownloadCount, *details.MaxDownloads)
err = c.DeletePresignedURL(ctx, "token")
Use when:
- You need to revoke access after sharing
- You want download counting or limits
- You need an audit trail (createdBy, createdAt tracked)
- Link must survive API key rotation
- You want short, clean URLs for users
- Sharing sensitive files
Decision matrix:
| Scenario | Client-side | Server-side |
|---|
| Quick temporary link, no tracking | โ
| |
| Need to revoke after sharing | | โ
|
| Limit number of downloads | | โ
|
| Track download count | | โ
|
| Batch-generate 1000 links | โ
| |
| Share in download portal | | โ
|
| Must survive key rotation | | โ
|
| Sensitive files with audit trail | | โ
|
| Embed in automated emails | โ
| |
HTTP API Quick Reference
When generating code in other languages, use the HTTP API directly with HMAC signing:
Buckets
GET /api/v1/buckets โ {"buckets":[{name, createdAt}], "count":N}
PUT /api/v1/buckets/{name} โ 201 {bucket, created, location} | 409 BUCKET_EXISTS
PUT /api/v1/buckets/{name}?createIfNotExists=true โ 201 (new) or 200 {exists:true} (existed)
HEAD /api/v1/buckets/{name} โ 200 or 404
DELETE /api/v1/buckets/{name} โ 204 | 409 BUCKET_NOT_EMPTY
Objects
PUT /api/v1/buckets/{bucket}/{key} (body = raw bytes) โ 200 {bucket, key, etag, size, url}
GET /api/v1/buckets/{bucket}/{key} โ raw file (headers: Content-Type, Content-Length, ETag, Last-Modified). Supports Range header
HEAD /api/v1/buckets/{bucket}/{key} โ headers only, no body
DELETE /api/v1/buckets/{bucket}/{key} โ 204
GET /api/v1/buckets/{bucket}?list=true&prefix=X&delimiter=/&max-keys=N โ {contents, commonPrefixes, isTruncated}
Presigned URLs
POST /api/v1/presign (JSON: {bucket, key, method, expiresIn, maxDownloads}) โ 201 {token, url, ...}
GET /api/v1/presign โ {urls, count}
GET /api/v1/presign/{token} โ presigned URL details with current downloadCount
DELETE /api/v1/presign/{token} โ 200 (immediate revocation)
GET /dl/{token} โ public download (no auth needed). 404 if expired/revoked/max-reached
API Keys
POST /api/v1/keys (JSON: {name, permissions, bucketScope}) โ 201 {accessKeyId, secretKey, ...} โ secret shown ONCE
GET /api/v1/keys โ {keys, count} โ no secrets
DELETE /api/v1/keys?accessKeyId=BDK_xxxx โ 204
Error Handling
All errors return consistent JSON: {"error":{"code":"CODE","category":"CATEGORY","message":"...","details":{}}}
Handle these errors in generated code:
| HTTP | Code | What to Do |
|---|
| 400 | INVALID_BUCKET_NAME | Fix bucket name: 3-63 chars, lowercase a-z0-9 + hyphens/dots, start/end with letter/digit |
| 400 | INVALID_OBJECT_KEY | Fix key: no .., no leading /, max 1024 bytes |
| 401 | UNAUTHORIZED | Check: (1) API key exists and not disabled, (2) timestamp within ยฑ15 min, (3) signing path matches request path |
| 403 | PERMISSION_DENIED | API key lacks required permission or bucket scope doesn't match |
| 404 | BUCKET_NOT_FOUND | Create bucket first with CreateBucketIfNotExists |
| 404 | OBJECT_NOT_FOUND | Object doesn't exist โ check key spelling, verify bucket |
| 409 | BUCKET_EXISTS | Use ?createIfNotExists=true to avoid this |
| 409 | BUCKET_NOT_EMPTY | Delete all objects before deleting bucket |
| 413 | FILE_TOO_LARGE | File exceeds 5GB limit โ split or compress |
| 423 | OBJECT_LOCKED | Another operation holds the lock โ retry after brief delay (lock timeout is 30s) |
| 429 | RATE_LIMIT_EXCEEDED | Retry after Retry-After header seconds. Response has X-Retryable: true. General: 100/min, upload: 10/min |
| 507 | STORAGE_FULL | Server reached -max-storage limit โ cannot upload |
Go client error handling pattern:
result, err := c.GetObject(ctx, "bucket", "key")
if err != nil {
var apiErr *client.APIError
if errors.As(err, &apiErr) {
switch apiErr.Code {
case "OBJECT_NOT_FOUND":
case "RATE_LIMIT_EXCEEDED":
time.Sleep(time.Duration(apiErr.RetryAfter) * time.Second)
}
}
}
Common Workflows
Store AI-generated artifacts
c.CreateBucketIfNotExists(ctx, "ai-artifacts")
key := fmt.Sprintf("generations/%s/%s", sessionID, "output.json")
c.PutObject(ctx, "ai-artifacts", key, resultBytes)
presigned, _ := c.CreatePresignedURL(ctx, client.CreatePresignedURLRequest{
Bucket: "ai-artifacts",
Key: key,
Method: "GET",
ExpiresIn: int64Ptr(86400),
})
fmt.Println("Download:", presigned.URL)
Upload and share with download limits
c.CreateBucketIfNotExists(ctx, "shared")
c.PutObject(ctx, "shared", "report.pdf", pdfBytes)
presigned, _ := c.CreatePresignedURL(ctx, client.CreatePresignedURLRequest{
Bucket: "shared",
Key: "report.pdf",
Method: "GET",
ExpiresIn: int64Ptr(7 * 24 * 3600),
MaxDownloads: intPtr(5),
})
List and organize files
list, _ := c.ListObjects(ctx, "bucket", client.ListObjectsOptions{
Delimiter: "/",
})
list, _ = c.ListObjects(ctx, "bucket", client.ListObjectsOptions{
Prefix: "folder1/",
Delimiter: "/",
})
Deduplicate with ETags
meta, _ := c.HeadObject(ctx, "bucket", "file.txt")
if meta.ETag == computeMD5Hex(newContent) {
fmt.Println("Content unchanged, skipping upload")
} else {
c.PutObject(ctx, "bucket", "file.txt", newContent)
}
Scoped API keys for limited access
curl -X POST http://localhost:7777/api/v1/keys \
-H "Content-Type: application/json" \
-d '{"name":"readonly-reports","permissions":"read","bucketScope":"reports"}'
Validation Rules Summary
| Rule | Constraint |
|---|
| Bucket name length | 3-63 characters |
| Bucket name regex | ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ |
| Bucket name chars | Lowercase a-z, digits 0-9, hyphens, dots |
| Bucket name start/end | Must be letter or digit |
| IP-like bucket names | Rejected (e.g., 192.168.1.1) |
| Object key max length | 1024 bytes |
| Object key no-go patterns | Empty, contains .., starts with / |
| Max upload size | 5GB (5,242,880,000 bytes) |
| HMAC clock skew tolerance | ยฑ15 minutes |
| Object lock timeout | 30 seconds |
| Rate limit โ general | 100 req/min per IP |
| Rate limit โ upload | 10 req/min per IP |
| Rate limit โ auth | 5 req/min per IP |