0
# Direct Upload
1
2
Core functionality for programmatic file uploads with full control over the upload process, progress tracking, and event handling.
3
4
## Capabilities
5
6
### DirectUpload Class
7
8
Main class for performing direct file uploads to cloud storage services through the Rails Active Storage backend.
9
10
```javascript { .api }
11
/**
12
* Main class for performing direct file uploads to cloud storage
13
* Handles the complete upload workflow: checksum, blob creation, and storage upload
14
*/
15
class DirectUpload {
16
/**
17
* Creates a new DirectUpload instance
18
* @param file - File object to upload
19
* @param url - Rails direct upload endpoint URL
20
* @param delegate - Optional delegate object for upload event callbacks
21
* @param customHeaders - Optional custom HTTP headers for requests
22
*/
23
constructor(
24
file: File,
25
url: string,
26
delegate?: DirectUploadDelegate,
27
customHeaders?: Record<string, string>
28
);
29
30
/**
31
* Starts the upload process
32
* @param callback - Called when upload completes or fails
33
*/
34
create(callback: (error: string | null, blob?: BlobAttributes) => void): void;
35
36
/** Unique identifier for this upload instance */
37
readonly id: number;
38
39
/** File being uploaded */
40
readonly file: File;
41
42
/** Direct upload endpoint URL */
43
readonly url: string;
44
45
/** Optional delegate for upload callbacks */
46
readonly delegate?: DirectUploadDelegate;
47
48
/** Custom headers for HTTP requests */
49
readonly customHeaders: Record<string, string>;
50
}
51
```
52
53
**Usage Examples:**
54
55
```javascript
56
import { DirectUpload } from "@rails/activestorage";
57
58
// Basic upload
59
const file = document.querySelector("input[type=file]").files[0];
60
const upload = new DirectUpload(file, "/rails/active_storage/direct_uploads");
61
62
upload.create((error, blob) => {
63
if (error) {
64
console.error("Upload failed:", error);
65
} else {
66
console.log("Upload successful:", blob);
67
// Use blob.signed_id in form submission
68
}
69
});
70
71
// Upload with delegate callbacks
72
const upload = new DirectUpload(file, "/rails/active_storage/direct_uploads", {
73
directUploadWillCreateBlobWithXHR(xhr) {
74
console.log("Creating blob record...");
75
// Modify request if needed
76
xhr.setRequestHeader("X-Custom-Header", "value");
77
},
78
79
directUploadWillStoreFileWithXHR(xhr) {
80
console.log("Uploading to storage...");
81
// Track upload progress
82
xhr.upload.addEventListener("progress", (event) => {
83
const percent = (event.loaded / event.total) * 100;
84
console.log(`Upload progress: ${percent}%`);
85
});
86
}
87
});
88
89
// Upload with custom headers
90
const upload = new DirectUpload(file, url, null, {
91
"X-API-Key": "your-api-key",
92
"X-Client-Version": "1.0.0"
93
});
94
```
95
96
### DirectUpload Delegate
97
98
Interface for receiving callbacks during the upload process.
99
100
```javascript { .api }
101
interface DirectUploadDelegate {
102
/**
103
* Called before making request to create blob record on Rails backend
104
* Use to modify the XMLHttpRequest before it's sent
105
* @param xhr - XMLHttpRequest instance for blob creation
106
*/
107
directUploadWillCreateBlobWithXHR?(xhr: XMLHttpRequest): void;
108
109
/**
110
* Called before uploading file to storage service
111
* Use to modify the XMLHttpRequest or track upload progress
112
* @param xhr - XMLHttpRequest instance for file upload
113
*/
114
directUploadWillStoreFileWithXHR?(xhr: XMLHttpRequest): void;
115
}
116
```
117
118
**Delegate Usage Examples:**
119
120
```javascript
121
const delegate = {
122
directUploadWillCreateBlobWithXHR(xhr) {
123
// Add authentication
124
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
125
126
// Add request tracking
127
xhr.addEventListener("loadstart", () => {
128
console.log("Starting blob creation...");
129
});
130
},
131
132
directUploadWillStoreFileWithXHR(xhr) {
133
// Track upload progress
134
xhr.upload.addEventListener("progress", (event) => {
135
if (event.lengthComputable) {
136
const percentComplete = (event.loaded / event.total) * 100;
137
updateProgressBar(percentComplete);
138
}
139
});
140
141
// Handle upload events
142
xhr.upload.addEventListener("load", () => {
143
console.log("File upload completed");
144
});
145
146
xhr.upload.addEventListener("error", () => {
147
console.error("File upload failed");
148
});
149
}
150
};
151
152
const upload = new DirectUpload(file, url, delegate);
153
```
154
155
### Blob Attributes
156
157
Data structure returned after successful upload containing blob metadata and storage information.
158
159
```javascript { .api }
160
interface BlobAttributes {
161
/** Signed ID for referencing the blob in forms */
162
signed_id: string;
163
164
/** Unique storage key for the blob */
165
key: string;
166
167
/** Original filename */
168
filename: string;
169
170
/** MIME content type */
171
content_type: string;
172
173
/** File size in bytes */
174
byte_size: number;
175
176
/** MD5 checksum of the file content */
177
checksum: string;
178
}
179
```
180
181
**Using Blob Attributes:**
182
183
```javascript
184
upload.create((error, blob) => {
185
if (!error && blob) {
186
// Add to form as hidden input
187
const hiddenInput = document.createElement("input");
188
hiddenInput.type = "hidden";
189
hiddenInput.name = "post[attachment]";
190
hiddenInput.value = blob.signed_id;
191
form.appendChild(hiddenInput);
192
193
// Display file info
194
console.log(`Uploaded: ${blob.filename} (${blob.byte_size} bytes)`);
195
196
// Use in API calls
197
fetch("/api/posts", {
198
method: "POST",
199
headers: { "Content-Type": "application/json" },
200
body: JSON.stringify({
201
title: "My Post",
202
attachment_id: blob.signed_id
203
})
204
});
205
}
206
});
207
```
208
209
### Upload Process Flow
210
211
The DirectUpload class orchestrates a three-stage upload process:
212
213
1. **File Checksum Computation**
214
- Calculates MD5 hash of file content in chunks
215
- Uses Web Workers when available for better performance
216
- Handles large files without blocking the UI
217
218
2. **Blob Record Creation**
219
- Sends file metadata to Rails backend
220
- Creates database record with filename, size, checksum
221
- Receives signed URL for direct storage upload
222
223
3. **Direct Storage Upload**
224
- Uploads file directly to configured storage service (S3, GCS, etc.)
225
- Uses signed URL and headers from blob creation
226
- Bypasses Rails application server for better performance
227
228
**Error Handling:**
229
230
```javascript
231
upload.create((error, blob) => {
232
if (error) {
233
// Handle different error types
234
if (error.includes("Status: 422")) {
235
console.error("Validation error:", error);
236
showError("File type not allowed or file too large");
237
} else if (error.includes("Status: 403")) {
238
console.error("Authentication error:", error);
239
showError("Not authorized to upload files");
240
} else {
241
console.error("Upload error:", error);
242
showError("Upload failed. Please try again.");
243
}
244
} else {
245
console.log("Upload successful:", blob);
246
showSuccess(`${blob.filename} uploaded successfully`);
247
}
248
});
249
```
250
251
### Advanced Usage Patterns
252
253
**Multiple File Uploads:**
254
255
```javascript
256
async function uploadFiles(files) {
257
const uploads = Array.from(files).map(file => {
258
return new Promise((resolve, reject) => {
259
const upload = new DirectUpload(file, "/rails/active_storage/direct_uploads");
260
upload.create((error, blob) => {
261
if (error) reject(error);
262
else resolve(blob);
263
});
264
});
265
});
266
267
try {
268
const blobs = await Promise.all(uploads);
269
console.log("All uploads completed:", blobs);
270
return blobs;
271
} catch (error) {
272
console.error("Some uploads failed:", error);
273
throw error;
274
}
275
}
276
```
277
278
**Upload with Progress Tracking:**
279
280
```javascript
281
class ProgressTracker {
282
constructor(file, url) {
283
this.file = file;
284
this.upload = new DirectUpload(file, url, this);
285
this.progress = 0;
286
}
287
288
start() {
289
return new Promise((resolve, reject) => {
290
this.upload.create((error, blob) => {
291
if (error) reject(error);
292
else resolve(blob);
293
});
294
});
295
}
296
297
directUploadWillStoreFileWithXHR(xhr) {
298
xhr.upload.addEventListener("progress", (event) => {
299
if (event.lengthComputable) {
300
this.progress = (event.loaded / event.total) * 100;
301
this.onProgress?.(this.progress);
302
}
303
});
304
}
305
306
onProgress(percent) {
307
console.log(`${this.file.name}: ${Math.round(percent)}%`);
308
}
309
}
310
311
// Usage
312
const tracker = new ProgressTracker(file, "/rails/active_storage/direct_uploads");
313
tracker.onProgress = (percent) => updateProgressBar(file.name, percent);
314
315
try {
316
const blob = await tracker.start();
317
console.log("Upload completed:", blob);
318
} catch (error) {
319
console.error("Upload failed:", error);
320
}
321
```