Skip to content

Instantly share code, notes, and snippets.

@sephraim
Last active May 10, 2023 18:43
Show Gist options
  • Save sephraim/da8973f8b31cbe0eb4c043103d68e08f to your computer and use it in GitHub Desktop.
Save sephraim/da8973f8b31cbe0eb4c043103d68e08f to your computer and use it in GitHub Desktop.
[Ruby SFTP]
# frozen_string_literal: true
require 'net/sftp'
module Integrations
module Google
# Upload feed file(s) & fileset descriptor(s) to the Google Partner SFTP dropbox
#
# @see Google Dropbox SFTP setup: https://support.google.com/youtube/answer/3071034
#
# @example
# # Upload a single file & double-check it exists
# Integrations::Google::Dropbox.upload('path/to/feed.json')
# Integrations::Google::Dropbox.exist?('path/to/feed.json')
#
# # Upload multiple files & double-check they exist
# Integrations::Google::Dropbox.upload(['path/to/feed.filesetdesc.json', 'path/to/feed.json'])
# Integrations::Google::Dropbox.exist?(['path/to/feed.filesetdesc.json', 'path/to/feed.json'])
class Dropbox
# @type [String] Google Partner SFTP dropbox host
HOST = 'partnerupload.google.com'
private_constant :HOST
# @type [String] Google Partner SFTP dropbox port
PORT = '19321'
private_constant :PORT
# @type [String] Google Partner SFTP dropbox username
USERNAME = Rails.configuration.google_booking_sftp_username
private_constant :USERNAME
# @type [String] Google Partner SFTP dropbox id_rsa private key
PRIVATE_KEY = Rails.configuration.google_booking_sftp_private_key.gsub('\\n', "\n")
private_constant :PRIVATE_KEY
class << self
# Upload feed file(s) & fileset descriptor(s) to the Google Partner Generic SFTP dropbox
#
# @raise [Net::SFTP::StatusException] if the upload fails
# @raise [Net::SSH::AuthenticationFailed] if authentication fails
# @raise [ArgumentError] if local file is missing
#
# @param filepaths [String, Array<String>] feed filepath(s)
# @param debug [Boolean] whether to enable verbose logging; useful for troubleshooting connection issues
# @return [true] true if the upload was successful, else raise an exception
def upload(filepaths, debug: false) # rubocop:disable Metrics/MethodLength
filepaths = Array.wrap(filepaths)
Rails.logger.info(
"Uploading #{filepaths.count} file(s) to the Google Partner SFTP dropbox."
)
successful_uploads = 0
sftp_connect(debug: debug) do |dropbox|
filepaths.each do |filepath|
dropbox.upload!(filepath) do |event, _uploader, *args|
upload_status(event: event, args: args)
successful_uploads += 1 if event == :close
end
end
end
Rails.logger.info(
"Successfully uploaded #{successful_uploads} file(s) to the Google Partner SFTP dropbox."
)
true
end
# Check if the given feed file(s) & fileset descriptor(s) have been uploaded to
# the Google Partner Generic SFTP dropbox
#
# @param filepaths [String, Array<String>] feed filepath(s)
# @param debug [Boolean] whether to enable verbose logging; useful for troubleshooting connection issues
# @return [Boolean] true if files exist in the dropbox, else false
def exist?(filepaths, debug: false)
filepaths = Array.wrap(filepaths)
files_to_check = filepaths.map { |f| File.basename(f) }
sftp_connect(debug: debug) do |dropbox|
existing_files = dropbox.dir.entries('.').map(&:name)
return (files_to_check - existing_files).empty?
end
end
private
# Establish an SFTP connection to the Google Partner Generic SFTP dropbox, perform the given block,
# close the connection, and delete the temporary private key file.
#
# @private
#
# @yield [dropbox] Gives the SFTP connection to the block
# @yieldparam dropbox [Net::SFTP] the SFTP connection
# @yieldreturn [void]
#
# @param debug [Boolean] whether to enable verbose logging; useful for troubleshooting connection issues
# @return [void]
def sftp_connect(debug: false)
verbose_level = debug ? :debug : false
Net::SFTP.start(HOST, USERNAME, port: PORT, keys: [tmp_private_key_file.path], auth_methods: ['publickey'],
non_interactive: true, verbose: verbose_level) do |dropbox|
yield dropbox if block_given?
end
ensure
tmp_private_key_file.close! # close/delete temp file after connection closed
@tmp_private_key_file = nil
end
# Log the status of the upload
#
# @private
#
# @param event [Symbol] the event that triggered the callback
# @param args [Array] the arguments passed to the callback; see Net::SFTP::Operations::Upload
# @return [void]
def upload_status(event:, args:) # rubocop:disable Metrics/MethodLength
status = case event
when :open
# args[0] : file metadata
"Starting upload: #{args[0].local} -> #{args[0].remote} (#{args[0].size} bytes}"
when :put
# args[0] : file metadata
# args[1] : byte offset in remote file
# args[2] : data being written (as string)
"Writing #{args[2].length} bytes to #{args[0].remote} starting at #{args[1]}"
when :close
# args[0] : file metadata
"Finished with #{args[0].remote}"
when :mkdir
# args[0] : remote path name
"Creating directory #{args[0]}"
end
Rails.logger.info status
end
# Create a temporary id_rsa private key file to use for SFTP connection
#
# @note Both the private key file and its associated instance variable must be manually deleted
# upon closing the SFTP connection.
#
# @private
#
# @return [Tempfile]
def tmp_private_key_file
@tmp_private_key_file ||= begin
file = Tempfile.new('id_rsa')
file.write(PRIVATE_KEY)
file.close
file
end
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment