タイトルが長いですが、タイトルと同じ流れの処理について紹介します。

フォームからデータを受け取って、それをゴニョゴニョして、そのデータをダウンロードしたりクラウドにアップロードしたい時、あると思うのです。

今回は、フォームから受け取るデータがzipファイルであり、ゴニョゴニョは画像処理で、アップロード先がS3です。

・フォームから受け取ったzipの処理方法
・zip内の画像ファイルをそれぞれ画像編集する方法
・画像編集したデータをS3へアップロードする方法

上記について参考になれば幸いです。

本記事での利用環境
Rails 6.0
ruby 2.6.5
開発環境 AWS Cloud9

本記事では、画像処理用にgem ‘rmagick’を使用します。
RMagickの導入方法については、下記で紹介しています。

最終的なコード

※簡素化するために実際のコードを一部書き換えています。ご自身の環境に合わせてroutes.rbを編集して、controller名、アクション名を書き換えてください。

View

<%= form_with url: image_convert_upload_path do |form| %>
  <%= form.file_field :image_file %>
  <%= form.text_field :s3_path %>
  <%= form.submit "アップロード", data: { disable_with: '処理中...' },accept: ".zip", class: "" %>
<% end %>

Controller

require 'zip'
require 'fileutils'
require 'aws-sdk-s3'

def upload_form
  
end

def image_convert_upload
 #pathを設定しておく
  zip_path = params[:image_file].path
  tmp_path = "#{Rails.root}/tmp/#{Time.zone.now.strftime('%Y%m%d%H%M%S')}/"
  s3_path = params[:s3_path]
  
  Zip::File.open(zip_path) do |zip|
      
      zip.each do |entry|
      ext = File.extname(entry.name)
      next if ext.blank? || File.basename(entry.name).count(".") > 1
      
      # zipファイルの中に階層がある場合、ディレクトリを作成する
      dir_name = File.dirname(entry.name)
      dir_path = tmp_path + dir_name
      FileUtils.mkdir_p(dir_path)
      
      # テンポラリファイルを作成
      Tempfile.open([File.basename(entry.to_s), ext]) do |file|
        begin
          # 一時ファイルを作成
          entry.extract(file.path) { true }
          
          image = Magick::ImageList.new(file.path).first
          width = image.columns
          height = image.rows
          new_image = image.roll(width*3/4, height*3/4)
          new_image.write(tmp_path + entry.name)
          
          new_image_file = File.new(tmp_path + entry.name) 
          
          bucket_name = 'バケット名'
          object_key = s3_path + entry.name
          data = new_image_file.read
          region = 'リージョン'
          s3_client = Aws::S3::Client.new(region: region)
        
          if object_uploaded?(s3_client, bucket_name, object_key, data)
            puts "Object '#{object_key}' uploaded to bucket '#{bucket_name}'."
          else
            puts "Object '#{object_key}' not uploaded to bucket '#{bucket_name}'."
          end
        ensure
          # 終わったらclose
          file.close!
        end
      end
    end

    # tmpディレクトリに作成したディレクトリを丸ごと削除
    FileUtils.rm_r(tmp_path)
  end
 
  redirect_to upload_form_path
end


def object_uploaded?(s3_client, bucket_name, object_key, data)
  response = s3_client.put_object(
    bucket: bucket_name,
    key: object_key,
    body: data,
    acl: 'public-read'
  )
  if response.etag
    return true
  else
    return false
  end
rescue StandardError => e
  puts "Error uploading object: #{e.message}"
  return false
end

上記のコードを動かすためには、下記のgemを追加してbundle installしておく必要があります。

gem 'rmagick'
gem 'aws-sdk-s3'
gem 'rubyzip' 

‘aws-sdk-s3’を追加することで、下記のコマンドでAWS S3へアクセスする際の設定を行うことができるはずです。(筆者の環境ではできました)

ターミナル

aws configure

ターミナル(上記のコマンドで下記項目をそれぞれ設定できます。)

AWS Access Key ID [****]: 
AWS Secret Access Key [****]: 
Default region name [****]: 
Default output format [None]: 

「Default output format」以外は、全てご自身の環境に合わせて設定してください。
※S3へアップロードするには、IAM作成、アクセス権限設定、認証情報取得、バケット作成、バケットポリシー設定etcが必要です。本記事では割愛します。

これからコードの補足説明します。

viewのフォームに関して

補足コメント記載しました。

<%= form_with url: image_convert_upload_path do |form| %>

 #ファイル受け取る
  <%= form.file_field :image_file %>
 #S3へアップロードする際にpathを追加したい場合のため、任意項目です
  <%= form.text_field :s3_path %> 

 #data: { disable_with: '処理中...' }とすることで、アップロード中に再度クリックできないようにしています。
 #accept: ".zip"とすることで、zipのみ受け取るように指定しています。不要であれば外してください。
  <%= form.submit "アップロード", data: { disable_with: '処理中...' },accept: ".zip", class: "" %>
<% end %>

各path設定について


def image_convert_upload
 #pathを設定しておく
 #Viewのフォームから受け取った:image_fileのパスです。
  zip_path = params[:image_file].path
 #tmpディレクトリへ一時的にデータを保存するためのパスです。作成するディレクトリ名は、他と重複しないように日付時間とします。
  tmp_path = "#{Rails.root}/tmp/#{Time.zone.now.strftime('%Y%m%d%H%M%S')}/"
 #Viewのフォームから受け取った:s3_pathのパスです。paramsで受け取ります。
  s3_path = params[:s3_path]

  # 〜省略〜
end

受け取ったzipファイルの展開について

def image_convert_upload
 # 〜省略〜  
  Zip::File.open(zip_path) do |zip|
      
   #zip内にあるファイルを一つずつ取り出す処理を開始します。
      zip.each do |entry|
   
   #ファイルの拡張子をextに代入
      ext = File.extname(entry.name)
   #ゴミデータを省くための処理です。拡張子がない、もしくはファイル名に.が1個よりも多い場合はスキップさせます。
      next if ext.blank? || File.basename(entry.name).count(".") > 1
      
      # 取り出したファイルの階層を把握します。
      dir_name = File.dirname(entry.name)
      # ディレクトリを事前に作っておく必要があるため、tempディレクトリへzipファイル内の階層と同じディレクトリを作成します。
      dir_path = tmp_path + dir_name
      FileUtils.mkdir_p(dir_path)
      
      # 〜省略〜
      end
    # tmpディレクトリに作成したディレクトリを丸ごと削除
    FileUtils.rm_r(tmp_path)
  end
 
  redirect_to upload_form_path
end

‘rubyzip’の使い方については、下記確認しましょう。

基本的には下記でファイル名を全て取り出すことができます。

  Zip::File.open(zip_path) do |zip|  
    zip.each do |entry|
      puts entry.name
    end
  end

File.~については、下記リンク先も参照しましょう。

entryname = "aaa/bbb/ccc/abc.jpg"

puts File.extname(entryname)
=> .jpg
puts File.basename(entryname)
=> abc.jpg
puts File.dirname(entryname)
=> aaa/bbb/ccc

FileUtils.~については下記参照。

“FileUtils.mkdir_p”は、ディレクトリがなければ作ってくれます。
“FileUtils.rm_r”は、ディレクトリを丸ごと削除してくれます。
便利ですよね。

Zip内のファイルを読み込んで画像処理を行う

def image_convert_upload
 # 〜省略〜  
  Zip::File.open(zip_path) do |zip|
      
      # 〜省略〜

      # テンポラリファイルを作成
      Tempfile.open([File.basename(entry.to_s), ext]) do |file|
        begin
          # 一時ファイルを作成
          entry.extract(file.path) { true }
          
          #rmagickで画像処理するために画像ファイルを読み込みます
          image = Magick::ImageList.new(file.path).first
          #画像の横幅を取得したり
          width = image.columns
          #画像の縦幅を取得したりできます。
          height = image.rows
          #画像の左上を切り取る場合だと下記で処理できます。
          new_image = img.crop(0, 0, width/2, height/2)

          #画像処理が終わったら、tempディレクトリへ作成したディレクトリへ書き出します。
          new_image.write(tmp_path + entry.name)
          
          #書き出した画像データをnew_image_fileとして扱えるようにします。
          new_image_file = File.new(tmp_path + entry.name) 
          
          # 〜省略〜
        ensure
          # 終わったらclose
          file.close!
        end
      end
    end
  # 〜省略〜
  end
  # 〜省略〜
end

def object_uploaded?(s3_client, bucket_name, object_key, data)
  # 〜省略〜
end

14〜20行目で画像処理を行なっています。
zipで読み込んだファイルをどうやってrmagickで読み込めるようにするのか、かなりハマってしまいましたが、テンポラリファイルを作成→一時ファイルを作成でデータとして読み込めるようになります。

画像処理に関しては、rmagickさん、けっこうなんでもできます。
下記参照してみてください。

S3へアップロードする

下記参照しておくと良いかもしれません。

def image_convert_upload
 # 〜省略〜
  Zip::File.open(zip_path) do |zip|
      
      zip.each do |entry|
      # 〜省略〜

      # テンポラリファイルを作成
      Tempfile.open([File.basename(entry.to_s), ext]) do |file|
        begin

          # 〜省略〜
          
          new_image_file = File.new(tmp_path + entry.name) 
          
          bucket_name = 'バケット名'
          object_key = s3_path + entry.name
          data = new_image_file.read
          region = 'リージョン'
          s3_client = Aws::S3::Client.new(region: region)
        
          if object_uploaded?(s3_client, bucket_name, object_key, data)
            puts "Object '#{object_key}' uploaded to bucket '#{bucket_name}'."
          else
            puts "Object '#{object_key}' not uploaded to bucket '#{bucket_name}'."
          end
        ensure
          # 終わったらclose
          file.close!
        end
      end
    end
    # tmpディレクトリに作成したディレクトリを丸ごと削除
    FileUtils.rm_r(tmp_path)
  end
  # 〜省略〜
end


def object_uploaded?(s3_client, bucket_name, object_key, data)
  response = s3_client.put_object(
    bucket: bucket_name,
    key: object_key,
    body: data,
    acl: 'public-read'
  )
  if response.etag
    return true
  else
    return false
  end
rescue StandardError => e
  puts "Error uploading object: #{e.message}"
  return false
end

18行目がポイントです。
zipファイルで読み込んだデータを、画像処理して、s3へdataとして渡します。
ここまで来るのにどれだけハマったか、、思い返すと泣きたくなります笑

17行目では、object_keyとして、s3_path + entry.nameとしております。
s3_pathは、フォームで入力した文字列です。
バケット内でファイル保存先のパスを指定したい場合に活用できます。必要なければs3_pathを省いてもOKです。

34行目では、tempディレクトリに作成したディレクトリを丸ごと削除しておりますが、確認したい場合はコメントアウトすればOKです。

45行目では、アップロード時のaclの設定ができます。本記事ではpublic-readですが、private等も設定可能です。ご自身の環境に合わせてください。
S3のaclの設定については下記記事参照ください。

無事できましたかね、、?

コードの書き方が助長かもしれませんが、とりあえず想定した動きをしてくれています。
画像520枚、合計140MBのzipファイルで試してみると、90秒程で処理完了しました。

以上、ご参考になれば幸いです。