JavaScript library for direct file uploads to cloud storage services in Rails applications
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Core functionality for programmatic file uploads with full control over the upload process, progress tracking, and event handling.
Main class for performing direct file uploads to cloud storage services through the Rails Active Storage backend.
/**
* Main class for performing direct file uploads to cloud storage
* Handles the complete upload workflow: checksum, blob creation, and storage upload
*/
class DirectUpload {
/**
* Creates a new DirectUpload instance
* @param file - File object to upload
* @param url - Rails direct upload endpoint URL
* @param delegate - Optional delegate object for upload event callbacks
* @param customHeaders - Optional custom HTTP headers for requests
*/
constructor(
file: File,
url: string,
delegate?: DirectUploadDelegate,
customHeaders?: Record<string, string>
);
/**
* Starts the upload process
* @param callback - Called when upload completes or fails
*/
create(callback: (error: string | null, blob?: BlobAttributes) => void): void;
/** Unique identifier for this upload instance */
readonly id: number;
/** File being uploaded */
readonly file: File;
/** Direct upload endpoint URL */
readonly url: string;
/** Optional delegate for upload callbacks */
readonly delegate?: DirectUploadDelegate;
/** Custom headers for HTTP requests */
readonly customHeaders: Record<string, string>;
}Usage Examples:
import { DirectUpload } from "@rails/activestorage";
// Basic upload
const file = document.querySelector("input[type=file]").files[0];
const upload = new DirectUpload(file, "/rails/active_storage/direct_uploads");
upload.create((error, blob) => {
if (error) {
console.error("Upload failed:", error);
} else {
console.log("Upload successful:", blob);
// Use blob.signed_id in form submission
}
});
// Upload with delegate callbacks
const upload = new DirectUpload(file, "/rails/active_storage/direct_uploads", {
directUploadWillCreateBlobWithXHR(xhr) {
console.log("Creating blob record...");
// Modify request if needed
xhr.setRequestHeader("X-Custom-Header", "value");
},
directUploadWillStoreFileWithXHR(xhr) {
console.log("Uploading to storage...");
// Track upload progress
xhr.upload.addEventListener("progress", (event) => {
const percent = (event.loaded / event.total) * 100;
console.log(`Upload progress: ${percent}%`);
});
}
});
// Upload with custom headers
const upload = new DirectUpload(file, url, null, {
"X-API-Key": "your-api-key",
"X-Client-Version": "1.0.0"
});Interface for receiving callbacks during the upload process.
interface DirectUploadDelegate {
/**
* Called before making request to create blob record on Rails backend
* Use to modify the XMLHttpRequest before it's sent
* @param xhr - XMLHttpRequest instance for blob creation
*/
directUploadWillCreateBlobWithXHR?(xhr: XMLHttpRequest): void;
/**
* Called before uploading file to storage service
* Use to modify the XMLHttpRequest or track upload progress
* @param xhr - XMLHttpRequest instance for file upload
*/
directUploadWillStoreFileWithXHR?(xhr: XMLHttpRequest): void;
}Delegate Usage Examples:
const delegate = {
directUploadWillCreateBlobWithXHR(xhr) {
// Add authentication
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
// Add request tracking
xhr.addEventListener("loadstart", () => {
console.log("Starting blob creation...");
});
},
directUploadWillStoreFileWithXHR(xhr) {
// Track upload progress
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
updateProgressBar(percentComplete);
}
});
// Handle upload events
xhr.upload.addEventListener("load", () => {
console.log("File upload completed");
});
xhr.upload.addEventListener("error", () => {
console.error("File upload failed");
});
}
};
const upload = new DirectUpload(file, url, delegate);Data structure returned after successful upload containing blob metadata and storage information.
interface BlobAttributes {
/** Signed ID for referencing the blob in forms */
signed_id: string;
/** Unique storage key for the blob */
key: string;
/** Original filename */
filename: string;
/** MIME content type */
content_type: string;
/** File size in bytes */
byte_size: number;
/** MD5 checksum of the file content */
checksum: string;
}Using Blob Attributes:
upload.create((error, blob) => {
if (!error && blob) {
// Add to form as hidden input
const hiddenInput = document.createElement("input");
hiddenInput.type = "hidden";
hiddenInput.name = "post[attachment]";
hiddenInput.value = blob.signed_id;
form.appendChild(hiddenInput);
// Display file info
console.log(`Uploaded: ${blob.filename} (${blob.byte_size} bytes)`);
// Use in API calls
fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "My Post",
attachment_id: blob.signed_id
})
});
}
});The DirectUpload class orchestrates a three-stage upload process:
File Checksum Computation
Blob Record Creation
Direct Storage Upload
Error Handling:
upload.create((error, blob) => {
if (error) {
// Handle different error types
if (error.includes("Status: 422")) {
console.error("Validation error:", error);
showError("File type not allowed or file too large");
} else if (error.includes("Status: 403")) {
console.error("Authentication error:", error);
showError("Not authorized to upload files");
} else {
console.error("Upload error:", error);
showError("Upload failed. Please try again.");
}
} else {
console.log("Upload successful:", blob);
showSuccess(`${blob.filename} uploaded successfully`);
}
});Multiple File Uploads:
async function uploadFiles(files) {
const uploads = Array.from(files).map(file => {
return new Promise((resolve, reject) => {
const upload = new DirectUpload(file, "/rails/active_storage/direct_uploads");
upload.create((error, blob) => {
if (error) reject(error);
else resolve(blob);
});
});
});
try {
const blobs = await Promise.all(uploads);
console.log("All uploads completed:", blobs);
return blobs;
} catch (error) {
console.error("Some uploads failed:", error);
throw error;
}
}Upload with Progress Tracking:
class ProgressTracker {
constructor(file, url) {
this.file = file;
this.upload = new DirectUpload(file, url, this);
this.progress = 0;
}
start() {
return new Promise((resolve, reject) => {
this.upload.create((error, blob) => {
if (error) reject(error);
else resolve(blob);
});
});
}
directUploadWillStoreFileWithXHR(xhr) {
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) {
this.progress = (event.loaded / event.total) * 100;
this.onProgress?.(this.progress);
}
});
}
onProgress(percent) {
console.log(`${this.file.name}: ${Math.round(percent)}%`);
}
}
// Usage
const tracker = new ProgressTracker(file, "/rails/active_storage/direct_uploads");
tracker.onProgress = (percent) => updateProgressBar(file.name, percent);
try {
const blob = await tracker.start();
console.log("Upload completed:", blob);
} catch (error) {
console.error("Upload failed:", error);
}Install with Tessl CLI
npx tessl i tessl/npm-rails--activestorage