ksaito blog

GCSのsigned policy documentsでフォルダごとアップロードする

webアプリケーションでファイルアップロード機能をよく提供すると思います。 ほんの数ファイルのアップロードであれば、署名付きURLを使うことが多いですが、フォルダのようにファイル数が読めないような場合は署名付きポリシードキュメントを使うと便利です。 署名付きURLの特徴 ここではフォルダアップロードする際に困る点だけ説明します。 その他の特徴については公式ドキュメント1を参照してください。 署名付きURLは主に次のようなフォーマットになっており、パスにオブジェクト名まで含まれています。 https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Signature=... これはつまり、アップロードできるオブジェクト名が1つに限定されてしまうということです。 複数ファイルをアップロードしたい場合、ファイルごとに発行する必要があり、計算量はO(n)となってしまいます。 署名付きポリシードキュメントの特徴 前述の計算量がO(n)がネックになるケースでは、署名付きポリシードキュメントを使用することで解決できます。 https://cloud.google.com/storage/docs/authentication/signatures?hl=ja#policy-document ポリシードキュメントには次のような特徴があります。 アップロード先のバケットを指定できる ファイル名のプレフィックスを限定できる 特定のフォルダ以下に対しての操作を許可できる それ以外のフォルダにはアップロードできない Content-TypeやContent-Lengthなどの条件を指定できる POSTのみ可能でGETやDELETEは不可 特にフォルダ配下に対してアップロードする権限を付与できるので、フォルダ内のファイルを全てアップロードするようなケースでは、署名の発行の計算量がO(1)となり、大幅に効率化できます。 実装してみる apiにrails、フロントエンドにreactを使用したアプリケーションで、GCSの署名付きポリシードキュメントを発行する例を示します。 まずapiは、google-cloud-storage gemを使用して、署名付きポリシードキュメントを発行し、返します。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 require 'bundler/inline' gemfile do source 'https://rubygems.org' gem "google-cloud-storage" end class Storage # @param prefix [String] The prefix for the objects to be uploaded # @param max_file_size [Integer] The maximum allowed file size for uploads # @return [Hash] A hash containing the URL and form fields for the signed policy document def self.generate_policy_document_for_prefix(prefix:, max_file_size: 100 * 1024 * 1024) post_object = bucket.generate_signed_post_policy_v4( "#{prefix}${filename}", expires: 3600, conditions: [ [ "starts-with", "$key", prefix ], [ "content-length-range", 0, max_file_size ] ] ) { url: post_object.url, fields: post_object.fields } end private def self.bucket @bucket ||= storage_client.bucket({bucket_name}) end def self.storage_client @storage_client ||= begin Google::Cloud::Storage.new( credentials: load_service_account_key ) end end def self.load_service_account_key keyfile_path = Rails.root.join({service_account.json}).to_s JSON.parse(File.read(keyfile_path)) end end 次にフロントエンドで、発行されたポリシードキュメントを使用して、フォルダ内のファイルをアップロードします。 ...

2025-11-15 · 5 min · ksaito

ファイル名を判定する際は正規化に気をつける

ブラウザからアップロードされたファイルを、とある命名規則に沿っているか判定する処理を実装していました。 しかし、MacとWindowsで一貫した挙動にならず、悩まされた結果、unicode正規化の問題であることがわかりました。 この記事では、その問題と解決策について共有します。 ハマった事象 正規表現でファイル名の判定を書いていたのですが、簡単のために比較表現で説明します。 下記のような判定をしていたのですが、後者の判定結果がfalseを返してしまいました。 "ペ" == "ペ" => true "ペ" == "ペ" => false 原因 MacのFinderでは、ファイル名に含まれる文字がunicode正規化のNFD(Normalization Form D)形式で保存されます。 しかし、エディタに入力した文字はNFC(Normalization Form C)形式であるため、同じ見た目の文字列でもバイト列が異なり、不一致となってしまいます。 false判定になった文字列で、それぞれの文字コードを調べると以下のようになります。 "\\u%04x" % "ペ".ord => "\\u30da" "\\u%04x" % "ペ".ord => "\\u30d8" このようにコードポイントが異なるため、等価ではないと判断されてしまいます。 後者の(\u30d8)は、Finder上でフォルダを作成したときに生成される"ペ"の文字コードで、これは基底文字"ヘ"(\u30d8)と結合文字"゜"(\u309c)の組み合わせで表現されています。 解決策 このようにコードポイントの違いで比較結果が期待通りにならない場合、文字列を正規化した上で比較を行うことで、解決できます。 pe = "ペ" "\\u%04x" % pe.ord => "\\u30d8" "ペ" == pe => false "ペ" == pe.unicode_normalize(:nfc) => true このようにRubyで用意されているunicode_normalizeメソッドを使用して、NFC形式に正規化することで、期待通りの比較が可能になります。 MacのファイルシステムではNFD形式で保存されるため、ファイル名を扱う際にはNFC形式に正規化してから比較や処理を行うことをお勧めします。 unicode正規化とは Unicode正規化wikiに詳しく記載されていますが、NFDとNFCの違いを簡単に説明します。 まず、合成と分解という概念があって、NFDは視覚的・意味的に等価な文字列に分解し結合文字として扱います。 一方、NFCはNFDのように分解したあと視覚的・意味的に等価な文字列を合成し合成文字として扱います。 このように視覚的には違いがわからなくても、“ヘ”+"゜“の結合文字と"ペ"の合成文字は異なるコードポイントとして扱われるため、比較結果が異なってしまいます。

2025-10-28 · 1 min · ksaito

rspec-mocksメモ

モックに使えるメソッドのメモ。 この記事ではスタブ、ダブル、モックなどのワードを厳密に区別しません。 ここに書いている内容はほとんど公式ドキュメントからの抜粋です。 https://rspec.info/features/3-12/rspec-mocks/ double 1 dbl = double("Hoge") doubleメソッドでダブルオブジェクトを作成できます。 これだけではメソッド呼び出しに対応できないので、receiveメソッドで振る舞いを定義します。 1 2 3 dbl = double("Hoge") allow(dbl).to receive(:foo).and_return("bar")` dbl.foo # => "bar" doubleは実オブジェクトに存在しないメソッドも定義できます。 これを避けたい場合は、次に上げるinstance_doubleが使えます。 instance_double 1 2 3 4 5 6 dbl = instance_double("String") allow(dbl).to receive(:length).and_return(5) dbl.length # => 5 # Stringオブジェクトに存在しないので、エラーになる allow(dbl).to receive(:hoge).and_return(5) 実オブジェクトの一部の振る舞いを差し替えたいことが多いと思うので、基本的にはinstance_doubleを使うのが良いでしょう。 クラスメソッドに対しては、class_doubleがあります。 ...

2025-10-21 · 2 min · ksaito

RSpecのdefine_negated_matcherでカスタムマッチャーの否定形を定義する

RSpecのdefine_negated_matcherを使うと、カスタムマッチャーの否定形を簡単に定義できます。 not_toが使えないような複合条件のケースを作成する際に便利です。 このようなケースを想定しています。 1 2 3 4 5 6 RSpec.describe [1, 2, 3] do it '' do lists = subject.dup expect { lists.delete(2) }.to change { lists }.not_to include(2) end end 実行すると、次のエラーが発生します。 NotImplementedError: `expect { }.not_to change { }.to()` is not supported このようなケースでは次のように書けば変わらんだろうというツッコミはさておき、直接的に検証したい場合は、define_negated_matcherを使って否定形のマッチャーを定義することで解決できます。 1 expect { lists.delete(2) }.to change { lists }.to contain_exactly(1, 3) 使い方 1 2 3 4 5 6 7 8 9 RSpec::Matchers.define_negated_matcher :an_array_excluding, :include RSpec.describe [1, 2, 3] do it '' do lists = subject.dup expect { lists.delete(2) }.to change { lists }.to an_array_excluding(2) expect { lists.delete(2) }.to change { lists }.to contain_exactly(1, 3) end end これだけでOKです。 listsは変更されて、2が含まれなくなることを検証できます。

2025-10-20 · 1 min · ksaito

RSpecで配列を検証するときのマッチャーの使い分け

毎回調べているのでいい加減整理しておきたい。 サマリ 先に結果を書いておく。 配列の要素が一致し、かつ、並び順は問わない: contain_exactly match_arrayでも同じ 配列の要素が一致し、かつ、並び順も一致する: eq 配列の要素が部分的に一致する: include contain_exactly / match_array 配列の要素が一致し、かつ、並び順は問わない 1 2 3 4 5 6 7 8 9 RSpec.describe [1, 2, 3] do it '' do is_expected.to contain_exactly(1, 2, 3) is_expected.to contain_exactly(3, 2, 1) is_expected.not_to contain_exactly(1) is_expected.to match_array([3, 2, 1]) end end eq 配列の要素が一致し、かつ、並び順も一致する ...

2025-10-19 · 1 min · ksaito

carrierwave with fog-googleで静的ファイルを扱ってみる

carrierwaveとfog-googleでGCSに静的ファイルをアップロードしたり、ローカルと本番でどう切り替えていくかについて、試してみたことを書き連ねていきます。 ※特に目新しいことはしておらず、既に出回っている記事以上のことは書いていないと思います ローカルストレージでcarrierwaveを試してみる carrierwaveのREADME の手順に沿って、環境構築していきます。 uploaderクラスをこれで生成します。 1 bin/rails g uploader Avatar Userテーブルにnameとavatarを保存するカラムを用意します。 1 2 bin/rails g model User name:string avatar:string bin/rails db:migrate 次に公式に従って以下を定義します。 1 2 3 class User < ApplicationRecord mount_uploader :avatar, AvatarUploader end ここまで公式の手順に従ってやってきました。 これでuserテーブルにavatarのパスを保存できる準備が整います。 rails consoleで試してみます。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 bin/rails c u = User.new u.name = 'tanaka' file = File.open('/Users/tanaka/Desktop/hoge.png') u.avatar = file u.save! u.avatar => #<AvatarUploader:0x00000001105974d0 @cache_id=nil, @cache_storage=#<CarrierWave::Storage::File:0x00000001106db170 @cache_called=nil, @uploader=#<AvatarUploader:0x00000001105974d0 ...>>, @deduplication_index=nil, @file= #<CarrierWave::SanitizedFile:0x00000001102dadc8 @content=nil, @content_type=nil, @declared_content_type=nil, @file="/Users/tanaka/work/project/examples/carrierwave-cdn-gcs-signedurl/public/uploads/user/avatar/1/hoge.png", @original_filename=nil>, @filename="hoge.png", @identifier="hoge.png", @model=#<User:0x000000010f1af698 id: 1, name: "tanaka", avatar: "hoge.png", created_at: Sun, 26 Nov 2023 03:16:54.005319000 UTC +00:00, updated_at: Sun, 26 Nov 2023 03:16:54.005319000 UTC +00:00>, @mounted_as=:avatar, @original_filename=nil, @staged=false, @storage=#<CarrierWave::Storage::File:0x0000000110678688 @cache_called=nil, @uploader=#<AvatarUploader:0x00000001105974d0 ...>>, @versions={}> u.avatar.url => "/uploads/user/avatar/1/hoge.png" u.avatar.curent_path => "/Users/tanaka/work/project/examples/carrierwave-cdn-gcs-signedurl/public/uploads/user/avatar/1/hoge.png" こんな感じでavatar.{url,current_path}でpathを引っ張ってくることができます。 ...

2023-11-27 · 2 min · ksaito

CloudLoadBalancingでバックエンドバケットにルーティングするときはpathとバケット内の階層を一致させよう

GoogleCloudLoadBalancing(以下、GCLB)のurl-mapを指定して、バックエンドバケットにルーティングするときに地味にハマったエラーとその解消法について書きます。 環境 1 2 3 4 5 6 7 GoogleCloud # terraformで構築するため一応バージョンのせておく terraform --version v1.3.5 hashicorp/google v4.74.0 結論 GCLBを経由してGCSのオブジェクト参照するときは、オブジェクトのpathとurl-mapで指定するpathは一致させる必要があります。 解説 今回使用するterraformのコードの一部です。他にもgcsなどのリソースも作ってますが、解説する内容とは関係ないので省略してます。 デフォルトのルートとは別にpaths = ["/test/*"]でバックエンドバケットにルーティングするように設定されています。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 resource "google_compute_url_map" "default" { name = "${local.project}-${local.prefix}" default_service = google_compute_backend_bucket.backend_bucket.id host_rule { hosts = ["*"] path_matcher = "path-matcher-2" } path_matcher { name = "path-matcher-2" default_service = google_compute_backend_bucket.backend_bucket.id path_rule { paths = ["/test/*"] service = google_compute_backend_bucket.backend_bucket.id } } } 上記のコードで構築した、GCLBの設定は以下のようになっています。 ...

2023-11-12 · 1 min · ksaito

Railsのmigrationとschema_versionsの関係について

Railsのmigrationのバージョン管理がどうやって行われていて、エラーが発生したときにどう対処すればいいか、調べてみたのでまとめていきます。 すでにRailsに関する記事はweb上に山程あって、この記事に目新しい内容は書かれないですが個人の学習記録として残してます。 新しい技術をちゃんとキャッチアップするには、アウトプットが重要なので。 環境 1 2 3 ruby 3.0.6 rails 7.0.6 postgres 15 バージョン管理について railsのmigrationファイルは、/app/db/migrate/に<日時create_model.rb>のような形式でファイルが作られます。 bin/rails db:migrateを実行したときに、どこまでマイグレーションが実行されているかの管理をデータベースのschema_migrationsのほうで記録して、管理しています。 ですので、schema_migrationsに記録されているバージョンと db/migrate/ に存在するmigrationファイルの整合性が合わなくなると、マイグレーションコマンドを実行するときに予期せぬエラーが発生します。 試してみる 以下のコマンドでモデル(必要ないけど)とマイグレーションファイルを作ります。 1 bin/rails g model user そうすると、 20230806103311_create_users.rbが生成されます。 この状態で実行すると、dbのschema_versionsに以下のようにバージョンが記録されます。 1 2 3 version ---------------- 20230806103311 この状態でマイグレーションファイルを削除するなりして、 bin/rails db:rollbackを実行すると、 1 2 3 4 5 6 7 rails aborted! ActiveRecord::UnknownMigrationVersionError: No migration with version number 20230806103311. Tasks: TOP => db:rollback (See full trace by running task with --trace) マイグレーションバージョンに記録されたファイルがありませんとエラーを吐きます。 ...

2023-08-07 · 2 min · ksaito