Initial commit
This commit is contained in:
393
references/direct-upload-complete-workflow.md
Normal file
393
references/direct-upload-complete-workflow.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# Direct Creator Upload - Complete Workflow
|
||||
|
||||
Complete architecture and implementation guide for user-uploaded images.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌──────────┐
|
||||
│ Browser │ │ Backend │ │Cloudflare│
|
||||
│ (User) │ │ API │ │ Images │
|
||||
└────┬────┘ └────┬────┘ └─────┬────┘
|
||||
│ │ │
|
||||
│ 1. Request upload URL │ │
|
||||
├─────────────────────────────>│ │
|
||||
│ POST /api/upload-url │ │
|
||||
│ { userId: "123" } │ │
|
||||
│ │ │
|
||||
│ │ 2. Generate upload URL │
|
||||
│ ├──────────────────────────────>│
|
||||
│ │ POST /direct_upload │
|
||||
│ │ { requireSignedURLs, metadata }│
|
||||
│ │ │
|
||||
│ │ 3. Return uploadURL + ID │
|
||||
│ │<──────────────────────────────┤
|
||||
│ │ { uploadURL, id } │
|
||||
│ │ │
|
||||
│ 4. Return uploadURL │ │
|
||||
│<─────────────────────────────┤ │
|
||||
│ { uploadURL, imageId } │ │
|
||||
│ │ │
|
||||
│ 5. Upload file directly │ │
|
||||
├──────────────────────────────────────────────────────────────>│
|
||||
│ POST uploadURL │ │
|
||||
│ FormData: { file } │ │
|
||||
│ │ │
|
||||
│ 6. Success response │ │
|
||||
│<──────────────────────────────────────────────────────────────┤
|
||||
│ { success: true } │ │
|
||||
│ │ │
|
||||
│ 7. (Optional) Webhook │ │
|
||||
│ │<──────────────────────────────┤
|
||||
│ │ POST /webhook │
|
||||
│ │ { imageId, status } │
|
||||
│ │ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Backend - Generate Upload URL
|
||||
|
||||
**Endpoint**: `POST /api/upload-url`
|
||||
|
||||
```typescript
|
||||
// backend.ts
|
||||
interface Env {
|
||||
IMAGES_ACCOUNT_ID: string;
|
||||
IMAGES_API_TOKEN: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// Parse request
|
||||
const body = await request.json<{
|
||||
userId?: string;
|
||||
requireSignedURLs?: boolean;
|
||||
}>();
|
||||
|
||||
// Generate one-time upload URL
|
||||
const response = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${env.IMAGES_ACCOUNT_ID}/images/v2/direct_upload`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${env.IMAGES_API_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requireSignedURLs: body.requireSignedURLs ?? false,
|
||||
metadata: {
|
||||
userId: body.userId || 'anonymous',
|
||||
uploadedAt: new Date().toISOString()
|
||||
},
|
||||
expiry: new Date(Date.now() + 60 * 60 * 1000).toISOString() // 1 hour
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return Response.json({
|
||||
uploadURL: result.result?.uploadURL,
|
||||
imageId: result.result?.id
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Frontend - Request Upload URL
|
||||
|
||||
```javascript
|
||||
// frontend.js
|
||||
async function requestUploadURL() {
|
||||
const response = await fetch('/api/upload-url', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId: getCurrentUserId(),
|
||||
requireSignedURLs: false
|
||||
})
|
||||
});
|
||||
|
||||
const { uploadURL, imageId } = await response.json();
|
||||
return { uploadURL, imageId };
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Frontend - Upload to Cloudflare
|
||||
|
||||
```javascript
|
||||
async function uploadImage(file) {
|
||||
// Step 1: Get upload URL
|
||||
const { uploadURL, imageId } = await requestUploadURL();
|
||||
|
||||
// Step 2: Upload directly to Cloudflare
|
||||
const formData = new FormData();
|
||||
formData.append('file', file); // MUST be named 'file'
|
||||
|
||||
const response = await fetch(uploadURL, {
|
||||
method: 'POST',
|
||||
body: formData // NO Content-Type header - browser sets multipart/form-data
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return imageId;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend HTML Example
|
||||
|
||||
```html
|
||||
<form id="upload-form">
|
||||
<input type="file" id="file-input" accept="image/*" />
|
||||
<button type="submit">Upload</button>
|
||||
<div id="status"></div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('upload-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const status = document.getElementById('status');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
status.textContent = 'Please select a file';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
status.textContent = 'Requesting upload URL...';
|
||||
|
||||
// Get upload URL from backend
|
||||
const urlResponse = await fetch('/api/upload-url', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId: 'user-123' })
|
||||
});
|
||||
|
||||
const { uploadURL, imageId } = await urlResponse.json();
|
||||
|
||||
status.textContent = 'Uploading...';
|
||||
|
||||
// Upload directly to Cloudflare
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const uploadResponse = await fetch(uploadURL, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (uploadResponse.ok) {
|
||||
status.textContent = `✓ Upload successful! Image ID: ${imageId}`;
|
||||
} else {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
status.textContent = `✗ Error: ${error.message}`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Webhook Integration
|
||||
|
||||
### Configure Webhook
|
||||
|
||||
1. Dashboard → Notifications → Destinations → Webhooks → Create
|
||||
2. Enter webhook URL: `https://your-backend.com/webhook`
|
||||
3. Notifications → All Notifications → Add → Images → Select webhook
|
||||
|
||||
### Handle Webhook
|
||||
|
||||
```typescript
|
||||
// backend-webhook.ts
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const webhook = await request.json();
|
||||
|
||||
console.log('Image upload webhook:', webhook);
|
||||
// {
|
||||
// imageId: "abc123",
|
||||
// status: "uploaded",
|
||||
// metadata: { userId: "user-123" }
|
||||
// }
|
||||
|
||||
// Update database
|
||||
await db.images.create({
|
||||
id: webhook.imageId,
|
||||
userId: webhook.metadata.userId,
|
||||
status: webhook.status,
|
||||
uploadedAt: new Date()
|
||||
});
|
||||
|
||||
return Response.json({ received: true });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Draft vs Uploaded State
|
||||
|
||||
When you generate upload URL, image record is created in **draft** state.
|
||||
|
||||
**Check status**:
|
||||
```typescript
|
||||
const response = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1/${imageId}`,
|
||||
{
|
||||
headers: { 'Authorization': `Bearer ${apiToken}` }
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.result?.draft) {
|
||||
console.log('Upload not completed yet');
|
||||
} else {
|
||||
console.log('Upload complete, image available');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Backend Errors
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const response = await fetch(directUploadURL, { ... });
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(`Cloudflare error: ${error.errors?.[0]?.message}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to generate upload URL:', error);
|
||||
return Response.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend Errors
|
||||
|
||||
```javascript
|
||||
// File size validation
|
||||
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > MAX_SIZE) {
|
||||
throw new Error('File too large (max 10MB)');
|
||||
}
|
||||
|
||||
// File type validation
|
||||
if (!file.type.startsWith('image/')) {
|
||||
throw new Error('Please select an image file');
|
||||
}
|
||||
|
||||
// Upload timeout
|
||||
const timeout = setTimeout(() => {
|
||||
throw new Error('Upload timeout (30s limit)');
|
||||
}, 28000); // 28s (before Cloudflare's 30s timeout)
|
||||
|
||||
try {
|
||||
await fetch(uploadURL, { body: formData });
|
||||
clearTimeout(timeout);
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom ID Support
|
||||
|
||||
```typescript
|
||||
// Generate upload URL with custom ID
|
||||
const response = await fetch(directUploadURL, {
|
||||
method: 'POST',
|
||||
headers: { ... },
|
||||
body: JSON.stringify({
|
||||
id: `user-${userId}-profile`, // Custom ID
|
||||
metadata: { userId }
|
||||
})
|
||||
});
|
||||
|
||||
// Access with custom ID
|
||||
const imageURL = `https://imagedelivery.net/${accountHash}/user-${userId}-profile/public`;
|
||||
```
|
||||
|
||||
**Note**: Custom IDs cannot be used with `requireSignedURLs=true`.
|
||||
|
||||
---
|
||||
|
||||
## Expiry Configuration
|
||||
|
||||
```typescript
|
||||
// Default: 30 minutes
|
||||
// Min: 2 minutes
|
||||
// Max: 6 hours
|
||||
|
||||
const expiry = new Date(Date.now() + 6 * 60 * 60 * 1000); // 6 hours
|
||||
|
||||
const response = await fetch(directUploadURL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
expiry: expiry.toISOString()
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never expose API token to browser**: Backend-only
|
||||
2. **Validate file type and size**: Frontend and backend
|
||||
3. **Rate limit upload URL generation**: Prevent abuse
|
||||
4. **Associate uploads with users**: Track in metadata
|
||||
5. **Implement webhooks**: Verify successful uploads
|
||||
6. **Set reasonable expiry**: 30min-1hr for most cases
|
||||
7. **Use signed URLs for private content**: `requireSignedURLs=true`
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Test backend endpoint
|
||||
curl -X POST http://localhost:8787/api/upload-url \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"userId":"test-user"}'
|
||||
|
||||
# Test upload (replace UPLOAD_URL with response)
|
||||
curl -X POST "UPLOAD_URL" \
|
||||
-F "file=@./test-image.jpg"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Official Documentation
|
||||
|
||||
- **Direct Creator Upload**: https://developers.cloudflare.com/images/upload-images/direct-creator-upload/
|
||||
- **Configure Webhooks**: https://developers.cloudflare.com/images/manage-images/configure-webhooks/
|
||||
Reference in New Issue
Block a user