send_data not working as expected in rails 7 turbo_stream in POST request

141 Views Asked by At

I'm uploading a file to my application an trying to modify it, then return it back but for some reason this code does not work, i get the blob in the reposnse but it's not downloading in the browser

PS: it works when add data-trubo="false" to my form, but i want to use turbo streams in this example

This is my controller

class HomeController < ApplicationController
  require 'rubyXL/convenience_methods'

  def index
  end


  def generate
    workbook = RubyXL::Parser.parse(params[:file])

    worksheet = workbook[0]

    worksheet = workbook.add_worksheet('testSheet')

    send_data workbook.stream.read, :disposition => 'attachment', :type => 'application/xlsx', :filename => "FileName.xlsx"
  end
end

This is my view

<%= form_with(url: generate_path) do |form| %>
  <div style="margin: 10px">
    <%= form.file_field :file %>
  </div>
  <div style="margin: 10px">
    <%= form.submit "Generate", style: "background: red; cursor: pointer"  %>
  </div>
<% end %>

This are the logs

Started POST "/generate" for ::1 at 2024-01-10 11:39:18 +0100
11:39:18 web.1  | Processing by HomeController#generate as TURBO_STREAM
11:39:18 web.1  |   Parameters: {"authenticity_token"=>"[FILTERED]", "file"=>#<ActionDispatch::Http::UploadedFile:0x000000010602ce00 @tempfile=#<Tempfile:/var/folders/4r/nc3z1vfd13n1pfbmpz3jhhcm0000gn/T/RackMultipart20240110-2055-i3c224.xlsx>, @content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", @original_filename="FileName.xlsx", @headers="Content-Disposition: form-data; name=\"file\"; filename=\"FileName.xlsx\"\r\nContent-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n">, "commit"=>"Generate"}
11:39:18 web.1  |   Rendering text template
11:39:18 web.1  |   Rendered text template (Duration: 0.0ms | Allocations: 4)
11:39:18 web.1  | Sent data FileName.xlsx (0.3ms)
11:39:18 web.1  | Completed 200 OK in 22ms (Views: 0.2ms | ActiveRecord: 0.0ms | Allocations: 31116)```
1

There are 1 best solutions below

1
maikhel On

It is possible by modifying turbo stream and adding Stimulus Controller:

  1. Add handling turbo_stream format in your controller action, store @data in variable
    (...)
    @data = workbook.stream.read
    respond_to do |format|
      format.html { send_data @data, :disposition => 'attachment', :type => 'application/xlsx', :filename => "FileName.xlsx" }
      format.turbo_stream
    end
  1. Add generate.turbo_stream.erb - encode data with Base64
<%= turbo_frame_tag "file-creator" do %>
  <div data-controller="generators" data-file-content="<%= Base64.encode64(@data) %>"></div>
<% end %>

  1. Add Stimulus Controller - actually 'rewrite' the file to blob and pass it to download with file-saver saveAS method
// app/javascript/controllers/generators_controller.js

import { Controller } from "@hotwired/stimulus"
import { SaveAs } from 'file-saver'

export default class extends Controller {
  connect() {
    var dataContent = this.element.dataset.fileContent;
    var byteCharacters = atob(dataContent);
    var byteNumbers = new Array(byteCharacters.length);
    for (var i = 0; i < byteCharacters.length; i++) {
      byteNumbers[i] = byteCharacters.charCodeAt(i);
    }
    var byteArray = new Uint8Array(byteNumbers);

    var blob = new Blob([byteArray], { type: "application/xlsx;charset=utf-8" });
   
    saveAs(blob, "test.xlsx");
  }
}
  1. Register new Stimulus Controller in app/javascript/controllers/index.js
import GeneratorsController from "./generators_controller"
application.register("generators", GeneratorsController)

I'd suggest using this approach only for small files as it will cause performance issues for large ones. So, for bigger files I'd recommend:

  1. save the generated file somewhere and get url to the file
  2. pass url to generate.turbo_stream.erb
  3. use saveAs in Stimulus Controller with saveAs(fileUrl, filename)