(ゲスト投稿:Eshop Guide 社 CTO、Dave Crowder 著書)
本記事では、Ruby、Heroku、SFTP To Goを使って、Shopifyのファイルインポートプロセスの自動化を実現するために必要な手順についてお話します。
今や誰でも、オンラインショップですら自分で簡単にファイル同期を行うことができますが、我々プログラマーに任せておいた方が良いこともまだあります。
最近、消費者に直接商品を販売するビジネスの多くが、Shopifyを使用しています。Shopifyは使いやすさ、デザインの美しさの面、また在庫管理から注文管理、決済処理まで、ほぼすべての管理を代行してくれます。
また、外部プラットフォームとの連携によって、ストアを目立たせ、トラフィックを獲得することができます。
Shopifyでは、顧客に[購入]ボタン以上のものを提供し、最初から最後までストアを通じてブランドを表現できるようにするために、新しいコンセプトや技術的スキルに対応し続ける方法が提供されています。画像、仕様書、マニュアル、製品の特徴を示す視覚的な手がかりなどの追加情報によって、製品を充実させるチャンスがありますよ。
Shopify内では、このようなドキュメントの保存場所はストアファイル が最適です。( Shopify の管理画面 からサインインして閲覧することもできます)
これは、Shopifyのテンプレート言語であるLiquid を介してショップテーマから簡単にアクセスすることで、ShopifyのグローバルCDN (コンテンツデリバリーネットワーク) のスピードと安定性のメリットを受けています。実は、2021年6月のShopify APIのアップデート まで、ストアのファイルの管理は面倒な手作業でしたが、アップデートによってストアファイルは GraphQL API を使ってプログラムで作成できるようになりました。ただ、これには後ほど紹介するような制約があります。
実現したかったこと 私たちの顧客であるショップオーナーは、Shopifyストアに簡単な同期オプションやファイルのアップロードを追加する必要がありました。
ShopifyのGraphQLでこのようなオプションが提供されていることは間違いありませんが、このプラットフォームは開発者向けに構築されているため、エンドユーザーやIT担当者には少し使いづらいかもしれません。一方、FTPは古くから存在するプロトコルで、大多数の IT 管理者のツールベルトとして知られており、タスクの自動化やFTP サーバーとの間でファイルをプッシュするなどのプロセスで構成されています。
そこで、PIM (製品情報管理システム)などからアイコン、画像、ユーザー マニュアルなどをエクスポートし、Shopifyストアからアクセスできる便利な方法を提供することにしました。
SFTP To Goを選んだ理由 2015年に初めてRailsアプリケーションを書いて以来、Heroku はコードを本番環境にデプロイする最もシンプルな方法であり、インフラについてほとんど考える必要がないため、筆者にとって本当に重要なこと、つまりコードによる価値の提供により集中できると感じています。
また、FTPインスタンスを設定して、その状態やストレージの監視や、アプリの環境変数へのFTP URLの追加などを気にしないといけない代わりに、HerokuはSFTP To Goのようなサービスのアドオンによって簡単に拡張もできます。数回のクリックだけで、完全に管理され、すぐに使えるFTPサーバーをアプリケーションに追加することができるのです!
では、ファイルはどのようにしてShopifyに取り込まれるのか ShopifyにはFiles API を使ってファイルをアップロードする方法は2通りあります。実際、どちらも2段階のプロセスとなります。この例で使わなかった方は、stagedUpload Target を作成し、そのURLにファイルをアップロードする必要があります。もう1つの方はもう少しシンプルで、オリジナルソース(外部の公開 URL)を指定し、ShopifyがそのURL からファイルを読み出す(フェッチ)ようにします。この方法では、アップロードプロセスを行う必要はありませんが、FTPサーバーに置かれたファイルを提供するのに、Webサーバーが必要です。
幸いなことに、SFTP To Goは、/publicという特別なフォルダから公開されたファイルもサポートしています。プロセスを簡単に表すと、下の画像ようなプロセスとなります。
さて、いよいよコードについて解説していきます。
FileOrganizerService これは、その名が示すように、全部の糸を一緒に引っ張るものです。また、Cron To Go という Heroku のアドオンを使って、1日に1回、またはその他のスケジュールで実行するようにスケジュールされた 「rake」 タスクから呼び出されるファイルのインポート処理のエントリポイントにもなります。
まず、重複したアップロードを避けるために、Shopifyストアに既存のファイル のリストを取得します。次に、SftpFileLoaderService を呼び出して、FTPサーバー上の指定されたフォルダからファイル名を全て読み込みます。最後に、そのファイル名を使って、FileImporterServiceを使ってShopifyに新しいファイルエントリを作成します。
module FileServices
## reads existing filenames from Shopify to avoid duplicates
# then starts reading filenames from FTP and creates new file entries in Shopify
class FileOrganizerService < ApplicationService
attr_reader :shop
def initialize(shop)
@shop = shop
end
def call
# get an array of the existing filenames in the Shopify store to avoid duplicate uploads
existing_filenames = query_existing_files
# get all filenames that are not yet existing in the shopify files
file_names = FileServices::SftpFileLoaderService.new(ENV.fetch('SFTPTOGO_URL'), existing_filenames).call
file_names.each do |file_name|
# Create a new file entry
FileServices::FileImporterService.new(shop).call(file_name)
rescue StandardError => e
# report any errors to everyone's favourite error tracker
Honeybadger.notify("ERROR Uploading #{file_name}: #{e.message}")
end
end
private
def query_existing_files
existing_filenames = []
shop.with_shopify_session do
file_result = ShopifyAPI::GraphQL.client.query(file_search_query)
existing_filenames << query_result_to_array(file_result&.data&.files&.edges)
# if more than one page
while file_result&.data&.files&.page_info&.has_next_page
cursor = file_result&.data&.files&.edges&.last&.cursor
file_result = ShopifyAPI::GraphQL.client.query(file_search_query, variables: { cursor: cursor })
existing_filenames << query_result_to_array(file_result&.data&.files&.edges)
end
end
existing_filenames.flatten
end
def file_search_query
ShopifyAPI::GraphQL.client.parse <<-'GRAPHQL'
query ($cursor: String){
files(first:250, after: $cursor){
pageInfo {
hasNextPage
}
edges{
cursor
node{... on GenericFile {
url
id
}}
node{... on MediaImage {
id
}}
}
}
}
GRAPHQL
end
private
def query_result_to_array(edges)
return unless edges.present?
edges.map do |edge|
if edge&.node&.__typename == 'GenericFile'
File.basename(URI.parse(edge&.node&.url).path) if edge&.node&.url.present?
else
edge&.node&.id
end
end
end
end
end
FileOrganizerService.rb SftpFileLoaderService このサービスは、SFTPサーバーへの接続を処理します。HerokuアプリケーションにSFTP To Goをインストールすると、SFTPTOGO_URL という環境変数が自動的に作成されます。この URL を使って、このサービスを呼び出します: ENV.fetch('SFTPTOGO_URL')
ENV.fetch('SFTPTOGO_URL')
もう一つの ENV変数 FTP_FILE_DIR
は、新しいファイルを探すためのディレクトリを指定します。筆者たちの場合、これは公開フォルダのサブフォルダで、HTTPSで利用可能です(これは、プロセスの後半で Shopifyがそのファイルをピックアップするときに違いが出ます)。
最終的にこの処理は、指定されたディレクトリにある、Shopify にまだないファイル名のリストを返します。
module FileServices
class SftpFileLoaderService < ApplicationService
attr_reader :sftp_uri, :filename, :existing_filenames
def initialize(sftp_uri, existing_filenames)
@sftp_uri = URI(sftp_uri)
@existing_filenames = existing_filenames
end
def call
load_files
end
private
def sftp
# establish connection to SFTP Server
@sftp ||= Net::SFTP.start(@sftp_uri.host, @sftp_uri.user, password: @sftp_uri.password)
end
# fetches all filenames from the configured directory, that are not in the existing_filenames list
def load_files
file_names = []
dir = ENV.fetch('FTP_FILE_DIR')
sftp.dir.foreach(dir) do |entry|
# skip if file already exists on server
next if existing_filenames.include? entry.name
file_names << entry.name
rescue Net::SFTP::StatusException => e
Rails.logger.error "Error while downloading data: #{e.description}"
end
file_names
end
end
end
SftpFileLoaderService.rb FileImporterService この処理は、FileImporterServiceを実行する前に返された各ファイル名に対して行われます。
ここで覚えておくべきポイントは、「full_url」です。これが「URL-Escaped」ファイル名に加えて、ENV変数 SFTPTOGO_PUBLIC_URL
で満たされます。こうすることで、ファイルは一般に公開され、Shopifyでダウンロードできるようになります。
module FileServices
class FileImporterService < ApplicationService
attr_reader :shop
class FileCreateError < StandardError
end
def initialize(shop)
@shop = shop
end
def call(file_name)
shop.with_shopify_session do
execute_query(file_name)
end
true
end
private
def execute_query(file_name)
full_url = "#{ENV.fetch('SFTPTOGO_PUBLIC_URL')}#{ERB::Util.url_encode(file_name)}"
Rails.logger.info "Creating #{file_name}"
result = ShopifyAPI::GraphQL.client.query(file_create_query, variables: {
"files": {
"originalSource": full_url
}
})
error = result&.errors&.details || result.to_h['data']['fileCreate']['userErrors'].first
raise FileCreateError, error['message'] if error.present?
end
def file_create_query
ShopifyAPI::GraphQL.client.parse <<-'GRAPHQL'
mutation ($files: [FileCreateInput!]!) {
fileCreate(files: $files) {
files {
fileStatus
}
userErrors {
field
message
}
}
}
GRAPHQL
end
end
end
FileImporterService.rb これらはShopifyストアにファイルをインポートするときの簡単なプロセスです。マーチャントはファイルをFTPディレクトリに置くだけで、あとは何も要りません。SFTP To GoにWebフック通知があるので、スケジューラーの代わりに、ファイルがアップロードされたらすぐに同期したり、Shopify からファイルを外すことができるでしょう。
著者について
Dave Crowder, Eshop Guide CTO Daveは、Eshop Guide の創業メンバー兼CTOです。ドイツを拠点に小規模なスタートアップから大企業まで 、さまざまなクライアントにShopifyに関するプロジェクトやサービスを提供しています。Eshop Guideは Shopifyの公開アプリ でも活躍しており、これまでlexoffice やsevDesk などのドイツでの会計システムや、idealo などの価格比較ポータルサイト向けの統合アプリを複数開発しています。
革新的な企業であるEshop Guideは、代理店として働くことの意味について、独自の視点を持っていると自負しています。従業員のストレスを軽減し、身体的・メンタル的にも健康であることを最優先させることで、日々の業務に取り組むことで、最終的に生産性が上がると考えています。
また、彼自身も瞑想を日課としており、瞑想を通じて意識を高めることに力を入れています。これがEshop Guideの職場環境にも反映されています。