or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

direct-upload.mdform-integration.mdindex.mdupload-controllers.mdutilities.md

direct-upload.mddocs/

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

```