Files

172 lines
5.4 KiB
Ruby
Raw Permalink Normal View History

2023-09-01 01:27:13 +03:00
# frozen_string_literal: true
module Templates
module CreateAttachments
PDF_CONTENT_TYPE = 'application/pdf'
2025-08-12 10:32:50 +03:00
ZIP_CONTENT_TYPE = 'application/zip'
2025-08-13 11:18:49 +03:00
X_ZIP_CONTENT_TYPE = 'application/x-zip-compressed'
2025-08-12 10:32:50 +03:00
JSON_CONTENT_TYPE = 'application/json'
DOCUMENT_EXTENSIONS = %w[.docx .doc .xlsx .xls .odt .rtf].freeze
DOCUMENT_CONTENT_TYPES = %w[
application/vnd.openxmlformats-officedocument.wordprocessingml.document
application/msword
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
application/vnd.ms-excel
application/vnd.oasis.opendocument.text
application/rtf
].freeze
2023-10-22 01:54:20 +03:00
ANNOTATIONS_SIZE_LIMIT = 6.megabytes
2026-02-19 14:08:27 +02:00
MAX_ZIP_SIZE = 100.megabytes
2023-09-05 20:33:40 +03:00
InvalidFileType = Class.new(StandardError)
2024-01-14 23:22:41 +02:00
PdfEncrypted = Class.new(StandardError)
2023-09-01 01:27:13 +03:00
module_function
2026-02-12 09:22:08 +02:00
def call(template, params, extract_fields: false, dynamic: false)
documents = []
dynamic_documents = []
extract_zip_files(params[:files].presence || params[:file]).each do |file|
docs, dynamic_docs = handle_file_types(template, file, params, extract_fields:, dynamic:)
documents.push(*docs)
dynamic_documents.push(*dynamic_docs)
2024-04-06 21:44:44 +03:00
end
2026-02-12 09:22:08 +02:00
[documents, dynamic_documents]
2024-04-06 21:44:44 +03:00
end
2024-01-14 23:22:41 +02:00
2026-04-20 15:43:41 +03:00
def handle_pdf_or_image(template, file, document_data = nil, params = {}, extract_fields: false, metadata: {})
2026-08-15 19:07:43 +03:00
return handle_pdf_or_image_v2(template, file, document_data, params, extract_fields:, metadata:) if v2?
2024-04-06 21:44:44 +03:00
document_data ||= file.read
2024-01-14 23:22:41 +02:00
2024-04-06 21:44:44 +03:00
if file.content_type == PDF_CONTENT_TYPE
document_data = maybe_decrypt_pdf_or_raise(document_data, params)
2023-09-01 01:27:13 +03:00
2024-04-06 21:44:44 +03:00
annotations =
document_data.size < ANNOTATIONS_SIZE_LIMIT ? Templates::BuildAnnotations.call(document_data) : []
end
2023-09-01 01:27:13 +03:00
2026-08-15 19:07:43 +03:00
document = create_document(template, file, document_data, metadata, annotations)
Templates::ProcessDocument.call(document, document_data, extract_fields:)
end
def handle_pdf_or_image_v2(template, file, document_data = nil, params = {}, extract_fields: false, metadata: {})
document_data ||= file.read
unless file.content_type == PDF_CONTENT_TYPE
document = create_document(template, file, document_data, metadata)
return Templates::ProcessDocument.call(document, document_data, extract_fields:)
end
doc = Pdfium::Document.open_bytes(document_data, params[:password])
document_data = decrypt_document(doc) if doc.encrypted?
annotations =
document_data.size < ANNOTATIONS_SIZE_LIMIT ? Templates::BuildPdfiumAnnotations.call(doc) : []
document = create_document(template, file, document_data, metadata, annotations)
Templates::ProcessDocument.call(document, document_data, extract_fields:, doc:)
rescue Pdfium::PasswordError
raise PdfEncrypted
ensure
doc&.close
end
def maybe_decrypt_pdf_or_raise(data, params)
if data.size < ANNOTATIONS_SIZE_LIMIT && PdfUtils.encrypted?(data)
PdfUtils.decrypt(data, params[:password])
else
data
end
rescue Pdfium::PasswordError
raise PdfEncrypted
end
def decrypt_document(doc)
io = StringIO.new
doc.save(io, flags: Pdfium::FPDF_REMOVE_SECURITY)
io.tap(&:rewind).read
end
def create_document(template, file, document_data, metadata, annotations = nil)
2024-04-06 21:44:44 +03:00
sha256 = Base64.urlsafe_encode64(Digest::SHA256.digest(document_data))
2024-04-06 15:44:50 +03:00
2024-04-06 21:44:44 +03:00
blob = ActiveStorage::Blob.create_and_upload!(
io: StringIO.new(document_data),
filename: file.original_filename,
metadata: {
2026-04-20 15:43:41 +03:00
**metadata,
2024-04-06 21:44:44 +03:00
identified: file.content_type == PDF_CONTENT_TYPE,
analyzed: file.content_type == PDF_CONTENT_TYPE,
pdf: { annotations: }.compact_blank, sha256:
}.compact_blank,
content_type: file.content_type
)
2024-04-06 15:44:50 +03:00
2026-08-15 19:07:43 +03:00
template.documents.create!(blob:)
2024-01-14 23:22:41 +02:00
end
2025-08-12 10:32:50 +03:00
def extract_zip_files(files)
extracted_files = []
Array.wrap(files).each do |file|
2025-08-13 11:18:49 +03:00
if file.content_type == ZIP_CONTENT_TYPE || file.content_type == X_ZIP_CONTENT_TYPE
2026-02-19 14:08:27 +02:00
total_size = 0
2025-08-12 10:32:50 +03:00
Zip::File.open(file.tempfile).each do |entry|
next if entry.directory?
2026-02-19 14:08:27 +02:00
total_size += entry.size
raise InvalidFileType, 'zip_too_large' if total_size > MAX_ZIP_SIZE
2025-08-12 10:32:50 +03:00
tempfile = Tempfile.new(entry.name)
tempfile.binmode
entry.get_input_stream { |in_stream| IO.copy_stream(in_stream, tempfile) }
tempfile.rewind
type = Marcel::MimeType.for(tempfile, name: entry.name)
next if type.exclude?('image') &&
type != PDF_CONTENT_TYPE &&
type != JSON_CONTENT_TYPE &&
DOCUMENT_CONTENT_TYPES.exclude?(type)
extracted_files << ActionDispatch::Http::UploadedFile.new(
filename: File.basename(entry.name),
type:,
tempfile:
)
end
else
extracted_files << file
end
end
extracted_files
end
2026-02-12 09:22:08 +02:00
def handle_file_types(template, file, params, extract_fields:, dynamic: false)
2024-07-20 21:17:47 +03:00
if file.content_type.include?('image') || file.content_type == PDF_CONTENT_TYPE
2026-02-12 09:22:08 +02:00
return [handle_pdf_or_image(template, file, file.read, params, extract_fields:), []]
2024-07-20 21:17:47 +03:00
end
2026-02-12 09:22:08 +02:00
raise InvalidFileType, "#{file.content_type}/#{dynamic}"
2023-09-05 20:33:40 +03:00
end
2026-08-15 19:07:43 +03:00
def v2?
true
end
2023-09-01 01:27:13 +03:00
end
end