CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-rails--activestorage

JavaScript library for direct file uploads to cloud storage services in Rails applications

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

direct-upload.mddocs/

Direct Upload

Core functionality for programmatic file uploads with full control over the upload process, progress tracking, and event handling.

Capabilities

DirectUpload Class

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"
});

DirectUpload Delegate

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);

Blob Attributes

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
      })
    });
  }
});

Upload Process Flow

The DirectUpload class orchestrates a three-stage upload process:

  1. File Checksum Computation

    • Calculates MD5 hash of file content in chunks
    • Uses Web Workers when available for better performance
    • Handles large files without blocking the UI
  2. Blob Record Creation

    • Sends file metadata to Rails backend
    • Creates database record with filename, size, checksum
    • Receives signed URL for direct storage upload
  3. Direct Storage Upload

    • Uploads file directly to configured storage service (S3, GCS, etc.)
    • Uses signed URL and headers from blob creation
    • Bypasses Rails application server for better performance

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`);
  }
});

Advanced Usage Patterns

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

docs

direct-upload.md

form-integration.md

index.md

upload-controllers.md

utilities.md

tile.json