如何处理file_as_string(由Prawn 生成)以便它被Carrierwave 接受?

How to handle a file_as_string (generated by Prawn) so that it is accepted by Carrierwave?

我正在使用 Prawn 从 Rails 应用程序的控制器生成 PDF,

...
respond_to do |format|
  format.pdf do
    pdf = GenerateReportPdf.new(@object, view_context)
    send_data pdf.render, filename: "Report", type: "application/pdf", disposition: "inline"
  end
end

这很好用,但我现在想将 GenerateReportPdf 移动到后台任务中,并将生成的对象传递给 Carrierwave 以直接上传到 S3。

工人长这样

def perform
  pdf           = GenerateReportPdf.new(@object)
  fileString    = ???????
  document      = Document.new(
    object_id: @object.id,
    file: fileString )
    # file is field used by Carrierwave 
end

如何处理Prawn(?????)返回的对象,以确保它是Carrierwave可以读取的格式。

fileString = pdf.render_file 'filename' 将对象写入应用程序的根目录。因为我在 Heroku 上,所以这是不可能的。

file = pdf.render returns ArgumentError: string contains null byte

fileString = StringIO.new( pdf.render_file 'filename' ) returns TypeError: no implicit conversion of nil into String

fileString = StringIO.new( pdf.render ) returns ActiveRecord::RecordInvalid: Validation failed: File You are not allowed to upload nil files, allowed types: jpg, jpeg, gif, png, pdf, doc, docx, xls, xlsx

fileString = File.open( pdf.render ) returns ArgumentError: string contains null byte

.....等等。

我错过了什么? StringIO.new( pdf.render ) 似乎应该可以,但我不清楚为什么会产生此错误。

您想创建一个临时文件(这在 Heroku 上没问题,只要您不希望它在请求中持续存在即可)。

def perform
  # Create instance of your Carrierwave Uploader
  uploader = MyUploader.new

  # Generate your PDF
  pdf = GenerateReportPdf.new(@object)

  # Create a tempfile
  tmpfile = Tempfile.new("my_filename")

  # set to binary mode to avoid UTF-8 conversion errors
  tmpfile.binmode 

  # Use render to write the file contents
  tmpfile.write pdf.render

  # Upload the tempfile with your Carrierwave uploader
  uploader.store! tmpfile

  # Close the tempfile and delete it
  tmpfile.close
  tmpfile.unlink
end

事实证明 StringIO.new( pdf.render ) 应该确实有效。

我遇到的问题是文件名设置不正确,尽管遵循了 Carrierwave 维基上的以下建议,代码中其他地方的错误意味着文件名作为空字符串返回。我忽略了这一点并假设还需要其他东西

https://github.com/carrierwaveuploader/carrierwave/wiki/How-to:-Upload-from-a-string-in-Rails-3

我的代码最终看起来像这样

def perform
  s = StringIO.new(pdf.render)

  def s.original_filename; "my file name"; end

  document  = Document.new(
    object_id: @object.id
  )

  document.file = s

  document.save!
end

这里有一种方法可以像 Andy Harvey 提到的那样使用 StringIO,但无需向 StringIO 实例的特征类添加方法。

class VirtualFile < StringIO
  attr_accessor :original_filename

  def initialize(string, original_filename)
    @original_filename = original_filename
    super(string)
  end
end

def perform
  pdf_string    = GenerateReportPdf.new(@object)
  file          = VirtualFile.new(pdf_string, 'filename.pdf')
  document      = Document.new(object_id: @object.id, file: file)
end

这花了我几天时间,关键是调用 render_file 控制文件路径,这样你就可以跟踪文件,像这样:

在我的一个模型中,例如:Policy 我有一个文档列表,这只是更新与载波相关的模型的方法,例如:PolicyDocument < ApplicationRecord mount_uploader :pdf_file, PdfDocumentUploader

def upload_pdf_document_file_to_s3_bucket(document_type, filepath)
  policy_document = self.policy_documents.where(policy_document_type: document_type)
                        .where(status: 'processing')
                        .where(pdf_file: nil).last
  policy_document.pdf_file = File.open(file_path, "r")
  policy_document.status = 's3_uploaded'
  policy_document.save(validate:false)
  policy_document
  rescue => e
    policy_document.status = 's3_uploaded_failed'
    policy_document.save(validate:false)
    Rails.logger.error "Error uploading policy documents: #{e.inspect}"
  end
end

在我的 Prawn PDF 文件生成器之一中,例如:PolicyPdfDocumentX 在这里请注意我如何渲染文件并返回文件路径,以便我可以从工作对象本身获取

  def generate_prawn_pdf_document
    Prawn::Document.new do |pdf|
      pdf.draw_text "Hello World PDF File", size: 8, at: [370, 462]
      pdf.start_new_page
      pdf.image Rails.root.join('app', 'assets', 'images', 'hello-world.png'), width: 550
    end
  end

def generate_tmp_file(filename)
   file_path = File.join(Rails.root, "tmp/pdfs", filename)
   self.generate_prawn_pdf_document.render_file(file_path)
   return filepath
end

在 "global" Worker 中创建文件并将它们上传到 s3 存储桶中,例如:PolicyDocumentGeneratorWorker

def perform(filename, document_type, policy)
 #here we create the instance of the prawn pdf generator class
 pdf_generator_class = document_type.constantize.new
 #here we are creating the file, but also `returning the filepath`
 file_path = pdf_generator_class.generate_tmp_file(filename)
 #here we are simply updating the model with the new file created
 policy.upload_pdf_document_file_to_s3_bucket(document_type, file_path)
end

最后怎么测试,运行 rails c 和:

the_policy = Policies.where....
PolicyDocumentGeneratorWorker.new.perform('report_x.pdf', 'PolicyPdfDocumentX',the_policy)

注意: 我使用元编程以防我们有多个不同的文件生成器,constantize.new 只是创建新的 prawn pdf 文档生成器instance so 类似于 PolicyPdfDocument.new 这样我们只能有一个 pdf doc generator worker class 可以处理 all 你的 prawn pdf 文档所以例如如果你需要一个新文档你可以只是 PolicyDocumentGeneratorWorker.new.perform('report_y.pdf', 'PolicyPdfDocumentY',the_policy)

:D

希望这有助于节省一些时间