require 'tempfile' require 'fileutils' # Finish generation before reporting readiness; downloads never hold the full file in RAM. class CountsExportDownload EXPIRY = 10.minutes def self.directory = Rails.root.join('tmp', 'counts_exports') def self.verifier = Rails.application.message_verifier('counts_export_download') def self.prepare(exporter, owner) FileUtils.mkdir_p(directory, mode: 0o700) Dir.glob(directory.join('counts-*.export')).each do |path| File.unlink(path) if File.mtime(path) < Time.current - EXPIRY rescue Errno::ENOENT # Another request may have removed an expired download. end file = Tempfile.create(['counts-', '.part'], directory) complete = false path = file.path begin file.binmode exporter.body.each { file.write(_1) } file.close finished_path = path.sub(/\.part\z/, '.export') File.rename(path, finished_path) path = finished_path token = verifier.generate({ 'file' => File.basename(path), 'resource' => exporter.resource, 'format' => exporter.format, 'owner' => owner }, expires_in: EXPIRY, purpose: 'counts_export') complete = true token ensure file.close unless file.closed? File.unlink(path) if !complete && File.exist?(path) end end def self.find(token, owner) metadata = verifier.verified(token, purpose: 'counts_export') return unless metadata.is_a?(Hash) && metadata['owner'] == owner return unless metadata['file']&.match?(/\Acounts-[a-zA-Z0-9_-]+\.export\z/) return unless CountsResourceDefinition::DEFINITIONS.key?(metadata['resource']) return unless ['json', 'csv'].include?(metadata['format']) path = directory.join(metadata['file']) return unless path.file? metadata.merge('path' => path.to_s) end end