From eeec852b3bc9ca95e4e959cca5871a546ea248db Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 7 Aug 2026 21:45:56 +0300 Subject: [PATCH] adjust preview cache --- .../preview_document_page_controller.rb | 67 ++++++++++++++----- lib/pdfium.rb | 38 +++++++++++ lib/templates/process_document.rb | 4 +- 3 files changed, 90 insertions(+), 19 deletions(-) diff --git a/app/controllers/preview_document_page_controller.rb b/app/controllers/preview_document_page_controller.rb index 5f5a8554..05b848da 100644 --- a/app/controllers/preview_document_page_controller.rb +++ b/app/controllers/preview_document_page_controller.rb @@ -5,6 +5,10 @@ class PreviewDocumentPageController < ActionController::API FORMAT = Templates::ProcessDocument::FORMAT + TMPFILE_PREFIX = 'attachment-' + TMPFILE_TTL = 5.minutes + TMPFILE_MAX_TOTAL_SIZE = Docuseal.multitenant? ? 400.megabytes : 1.gigabyte + def show result_data = ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment) @@ -30,34 +34,63 @@ class PreviewDocumentPageController < ActionController::API allow_other_host: true end - file_path = - if attachment.service.name == :disk - ActiveStorage::Blob.service.path_for(attachment.key) - else - find_or_create_document_tempfile_path(attachment) - end - preview_image = - Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i) + open_attachment_io(attachment) do |io| + Templates::ProcessDocument.generate_pdf_preview_from_io(attachment, io, params[:id].to_i) + end redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time), allow_other_host: true end - def find_or_create_document_tempfile_path(attachment) - file_path = "#{Dir.tmpdir}/attachment-#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}" + def open_attachment_io(attachment, &) + return File.open(ActiveStorage::Blob.service.path_for(attachment.key), 'rb', &) if attachment.service.name == :disk - File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f| - f.flock(File::LOCK_EX) + file_name = "#{TMPFILE_PREFIX}#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}" + + file_path = File.join(Dir.tmpdir, file_name) + + File.open(file_path, File::RDWR | File::CREAT, 0o644) do |file| + file.flock(File::LOCK_EX) # rubocop:disable Style/ZeroLengthPredicate - if f.size.zero? - f.binmode + if file.size.zero? + cleanup_stale_tempfiles - f.write(attachment.download) + file.binmode + + file.write(attachment.download) + else + FileUtils.touch(file_path) end # rubocop:enable Style/ZeroLengthPredicate - end - file_path + file.flock(File::LOCK_UN) + + yield file + end + end + + def cleanup_stale_tempfiles + entries = + Dir.glob(File.join(Dir.tmpdir, "#{TMPFILE_PREFIX}*")).filter_map do |path| + stat = File.stat(path) + + [path, stat.mtime, stat.size] + rescue Errno::ENOENT + nil + end + + total_size = entries.sum(&:last) + stale_time = TMPFILE_TTL.ago + + entries.sort_by(&:second).each do |path, mtime, size| + break if mtime > stale_time && total_size <= TMPFILE_MAX_TOTAL_SIZE + + File.unlink(path) + + total_size -= size + rescue Errno::ENOENT + nil + end end end diff --git a/lib/pdfium.rb b/lib/pdfium.rb index 06d22475..e7c5ec40 100644 --- a/lib/pdfium.rb +++ b/lib/pdfium.rb @@ -98,6 +98,7 @@ class Pdfium attach_function :FPDF_LoadDocument, %i[string FPDF_STRING], :FPDF_DOCUMENT attach_function :FPDF_LoadMemDocument, %i[pointer int FPDF_STRING], :FPDF_DOCUMENT + attach_function :FPDF_LoadCustomDocument, %i[pointer FPDF_STRING], :FPDF_DOCUMENT attach_function :FPDF_CloseDocument, [:FPDF_DOCUMENT], :void attach_function :FPDF_GetPageCount, [:FPDF_DOCUMENT], :int attach_function :FPDF_GetLastError, [], :ulong @@ -463,6 +464,43 @@ class Pdfium end end + def self.open_io(io, password = nil) + io.binmode + + get_block = FFI::Function.new(:int, %i[pointer ulong pointer ulong]) do |_param, position, out, size| + io.seek(position) + + bytes = io.read(size).to_s + + out.put_bytes(0, bytes) + + bytes.bytesize == size ? 1 : 0 + end + + file_access = Pdfium::FPDF_FILEACCESS.new + file_access[:m_FileLen] = io.size + file_access[:m_GetBlock] = get_block + file_access[:m_Param] = FFI::Pointer::NULL + + doc_ptr = Pdfium.FPDF_LoadCustomDocument(file_access, password) + + if doc_ptr.null? + Pdfium.check_last_error('Failed to load document from IO') + + raise PdfiumError, 'Failed to load document from IO, pointer is NULL.' + end + + doc = new(doc_ptr, [file_access, get_block, io]) + + return doc unless block_given? + + begin + yield doc + ensure + doc.close + end + end + def closed? @closed end diff --git a/lib/templates/process_document.rb b/lib/templates/process_document.rb index 9db405f6..4e69a4c1 100644 --- a/lib/templates/process_document.rb +++ b/lib/templates/process_document.rb @@ -192,8 +192,8 @@ module Templates end end - def generate_pdf_preview_from_file(attachment, file_path, page_number) - doc = Pdfium::Document.open_file(file_path) + def generate_pdf_preview_from_io(attachment, io, page_number) + doc = Pdfium::Document.open_io(io) doc_page = doc.get_page(page_number)