class PostImportGoogleSheetsFetcher HOST = 'docs.google.com'.freeze PUBLICATION_ID = /\A[A-Za-z0-9_-]+\z/ GID = /\A\d+\z/ RATE_LIMIT = 10 RATE_WINDOW = 1.minute def self.fetch! raw_url, rate_key: new(raw_url, rate_key:).fetch! end def initialize raw_url, rate_key: @raw_url = raw_url.to_s @rate_key = rate_key.to_s end def fetch! publication_id, gid = parse! throttle! csv_url = "https://#{ HOST }/spreadsheets/d/e/#{ publication_id }/pub?gid=#{ gid }&single=true&output=csv" response = Preview::HttpFetcher.fetch(csv_url, max_bytes: PostImportSourceParser::MAX_BYTES, allowed_hosts: [HOST]) raise ArgumentError, 'スプレッドシートを CSV として取得できませんでした.' unless csv?(response) response.body rescue Preview::UrlSafety::UnsafeUrl, Preview::HttpFetcher::FetchFailed => e raise ArgumentError, "スプレッドシートを取得できませんでした.ウェブ公開 URL を確認するか、ファイル保存又はコピー貼付けを使用してください: #{ e.message }" end private def parse! uri = URI.parse(@raw_url) unless uri.scheme == 'https' && uri.host&.downcase == HOST && uri.query.blank? raise ArgumentError, '公開 Google スプレッドシート URL を指定してください.' end match = uri.path.match(%r{\A/spreadsheets/d/e/([^/]+)/pubhtml\z}) gid = URI.decode_www_form(uri.fragment.to_s).to_h['gid'] unless match && PUBLICATION_ID.match?(match[1]) && GID.match?(gid.to_s) raise ArgumentError, '公開 Google スプレッドシート URL の publication_id 又は gid が不正です.' end [match[1], gid] rescue URI::InvalidURIError raise ArgumentError, '公開 Google スプレッドシート URL が不正です.' end def throttle! key = "post-import-google-sheets:#{ @rate_key }" count = Rails.cache.increment(key, 1, expires_in: RATE_WINDOW, initial: 0) raise ArgumentError, 'スプレッドシート取得の回数が多すぎます.しばらくしてから再試行してください.' if count > RATE_LIMIT end def csv? response type = response.content_type.to_s.downcase type.include?('text/csv') || response.body.valid_encoding? end end