module Sdk4me class Attachments S3_PROVIDER = 's3'.freeze FILENAME_TEMPLATE = '${filename}'.freeze def initialize(client, path) @client = client @path = path end # Upload attachments and replace the data inline with the uploaded # attachments info. # # To upload field attachments: # * data[:note_attachments] = ['/tmp/test.doc', '/tmp/test.log'] # # To upload inline images: # * data[:note] containing text referring to inline images in # data[:note_attachments] by their array index, with the index being # zero-based. Text can only refer to inline images in its own # attachments collection. For example: # # data = { # note: "Hello [note_attachments: 0] and [note_attachments: 1]", # note_attachments: ['/tmp/jip.png', '/tmp/janneke.png'], # ... # } # # After calling this method the data that will be posted to update the # 4me record would look similar to: # # data = { # note: "Hello ![](storage/abc/adjhajdhjaadf.png) and ![](storage/abc/fskdhakjfkjdssdf.png])", # note_attachments: [ # { key: 'storage/abc/fskdhakjfkjdssdf.png', filesize: 12345, inline: true }, # { key: 'storage/abc/fskdhakjfkjdssdf.png'], filesize: 98765, inline: true } # ], # ... # } def upload_attachments!(data) # Field attachments field_attachments = [] data.each do |field, value| next unless field.to_s.end_with?('_attachments') next unless value.is_a?(Enumerable) && value.any? value.map! { |attachment| upload_attachment(attachment) }.compact! field_attachments << field if value.any? end # Rich text inline attachments field_attachments.each do |field_attachment| field = field_attachment.to_s.sub(/_attachments$/, '') value = data[field.to_sym] || data[field] next unless value.is_a?(String) value.gsub!(/\[#{field_attachment}:\s?(\d+)\]/) do |match| idx = Regexp.last_match(1).to_i attachment = data[field_attachment][idx] if attachment attachment[:inline] = true "![](#{attachment[:key]})" # magic markdown for inline attachments else match end end end end private def raise_error(message) @client.logger.error { message } raise Sdk4me::UploadFailed, message end def storage @storage ||= @client.get('/attachments/storage').json.with_indifferent_access end # Upload a single attachment and return the data that should be submitted # back to 4me. Returns nil and provides an error in case the attachment # upload failed. def upload_attachment(attachment) return nil unless attachment provider = storage[:provider] raise 'No provider found' unless provider # attachment is already a file or we need to open the file from disk unless attachment.respond_to?(:path) && attachment.respond_to?(:read) raise "file does not exist: #{attachment}" unless File.exist?(attachment) attachment = File.open(attachment, 'rb') end key_template = storage[provider][:key] key = key_template.sub(FILENAME_TEMPLATE, File.basename(attachment.path)) key = if provider == S3_PROVIDER upload_to_s3(key, attachment) else upload_to_4me_local(key, attachment) end # return the values for the attachments param { key: key, filesize: File.size(attachment.path) } rescue StandardError => e raise_error("Attachment upload failed: #{e.message}") end # Upload the file to AWS S3 storage def upload_to_s3(key, attachment) uri = storage[:upload_uri] response = send_file(uri, storage[:s3].merge({ file: attachment })) # this is a bit of a hack, but Amazon S3 returns only XML :( xml = response.body || '' error = xml[%r{.*(.*).*}, 1] raise "AWS S3 upload to #{uri} for #{key} failed: #{error}" if error xml[%r{(.*)}, 1] end # Upload the file directly to 4me local storage def upload_to_4me_local(key, attachment) uri = storage[:upload_uri] response = send_file(uri, storage[:local].merge({ file: attachment }), @client.send(:expand_header)) raise "4me upload to #{uri} for #{key} failed: #{response.message}" unless response.valid? JSON.parse(response.body)['key'] end def send_file(uri, params, basic_auth_header = {}) params = { 'Content-Type': MIME::Types.type_for(params[:key])[0] || MIME::Types['application/octet-stream'][0] }.merge(params) data, header = Sdk4me::Multipart::Post.prepare_query(params) ssl, domain, port, path = @client.send(:ssl_domain_port_path, uri) request = Net::HTTP::Post.new(path, basic_auth_header.merge(header)) request.body = data @client.send(:_send, request, domain, port, ssl) end end end