クリエイターズネットワーク フリーナンス engineering

フリーナンスにおけるフロントエンドでのファイルフォーマットチェック

クリエイターズネットワーク フリーナンス engineering

こんにちは! GMO ペパボのグループ会社である GMO クリエイターズネットワークのよしだです。

今回は、私達の代表的なサービスであるフリーナンスにおけるフロントエンドでのファイルフォーマットチェックについて紹介したいと思います。

はじめに

フリーナンスとは、「フリーランス・個人事業主を支えるお金と保険のサービス」のことです。

フリーナンスで提供している請求書買取サービス「即日払い」では、請求書が実在することを証明する証憑(エビデンス)を提出していただいています。 先月のアップデートで提出可能なファイル形式に動画が追加され、下記の通りとなりました。

  • pdf
  • jpeg
  • jpg
  • png
  • gif
  • mp4
  • mov

ファイルアップロード時のファイルフォーマットチェックはバックエンドで実施していますが、フロントエンドでも実施しています。 フロントエンドでも実施することにより、ユーザー体験の強化やトラフィックの節約に繋がります。

ファイルフォーマットチェックの仕方

ファイルフォーマットチェックはマジックナンバー(フォーマット識別子)を使って実施しています。 ファイルの先頭 1024 バイトを取得し、16 進数に変換して判定します。

// ファイルのヘッダー1024バイトを取得(より正確な判定のため)
const arr = (new Uint8Array((e.target as FileReader).result as ArrayBuffer)).subarray(0, 1024;
let header = '';
for (let i = 0; i < arr.length; i += 1) {
  header += arr[i].toString(16).padStart(2, '0');
}

画像であれば先頭 8 バイトで判定可能ですが、動画で厳密な判定を行うために 1024 バイト取得しています。 また、拡張子とマジックナンバーの関係は下記の通りになります。

拡張子 マジックナンバー(16 進数)
PNG 89 50 4E 47
GIF 47 49 46 38
JPEG FF D8 FF + (E0/E1/E2/E3/E8)
PDF 25 50 44 46
MP4 66 74 79 70 +
(ブランド)
6D 70 34 31 /
6D 70 34 32 /
69 73 6F 6D /
69 73 6F 32
MOV 66 74 79 70 +
(ブランド)
71 74 20 20 /
6D 6F 6F 76

ブランドとは、MP4 や MOV の場合についているもので、そのファイルが準拠している仕様や互換性を表す識別子のことです。

上記を元にマジックナンバーからファイルフォーマットを判定し、拡張子と突合したものがこちらです。

let type = '';

if (header.startsWith('25504446')) {
  // PDFの判定
  type = 'application/pdf';
} else if (header.startsWith('89504e47')) {
  // PNGの判定
  type = 'image/png';
} else if (header.startsWith('47494638')) {
  // GIFの判定
  type = 'image/gif';
} else if (header.startsWith('ffd8ff')) {
  // JPEGの判定(JFIF または Exif マーカー)
  // より厳密なJPEGマーカーの確認
  const jpegMarkers = ['e0', 'e1', 'e2', 'e3', 'e8'];
  const marker = header.substring(6, 8);
  if (jpegMarkers.includes(marker)) {
    type = 'image/jpeg';
  }
} else {
  // MP4/MOVの判定
  // ftypマーカーの検索(MP4とMOVの両方で使用)
  const ftypIndex = header.indexOf('66747970');
  if (ftypIndex !== -1) {
    // ftypの後の4バイトでブランドを確認
    const brand = header.substring(ftypIndex + 8, ftypIndex + 16);

    // MP4のメジャーブランド
    const mp4Brands = ['6D703431', '6D703432', '69736F6D', '69736F32'];
    // MOVのメジャーブランド
    const movBrands = ['71742020', '6D6F6F76'];

    if (mp4Brands.includes(brand)) {
      type = 'video/mp4';
    } else if (movBrands.includes(brand)) {
      type = 'video/quicktime';
    }
  }
}

// ファイル拡張子の確認(追加の検証)
const extension = file.name.split('.').pop()?.toLowerCase();
const isValidExtension = (fileType: string, ext: string): boolean => {
  const validExtensions = {
    'application/pdf': ['pdf'],
    'image/png': ['png'],
    'image/gif': ['gif'],
    'image/jpeg': ['jpg', 'jpeg'],
    'video/mp4': ['mp4'],
    'video/quicktime': ['mov']
  };
  return validExtensions[fileType as keyof typeof validExtensions]?.includes(ext) ?? false;
};

// マジックナンバーと拡張子の両方が一致する場合のみ有効と判定
const isValid = type !== '' && extension && isValidExtension(type, extension);

動画の場合、66747970が先頭にあるとは限らないため、indexOfを使っています。

ファイルフォーマットチェックで弾かれた場合、下記の表示となります。

エラー画面

まとめ

マジックナンバーを使用したファイルフォーマットチェックをすることで、厳密な判定ができます。 動画の場合、先頭にマジックナンバーが存在する保障がないため、余分に 1024 バイト読み込む必要がありました。

また、フロントエンドでファイルフォーマットチェックを実施することで、以下のメリットがあります:

  • ユーザーは無効なファイルを即座に認識できる
  • サーバーへの不要なアップロードを防ぐことができる
  • ネットワークリソースの効率的な利用が可能

今後も、ユーザー体験の向上とシステムの効率化を目指して、フロントエンドでの各種バリデーションを強化していきたいと考えています。

GMO クリエイターズネットワークでは、プロダクト開発職種の採用をおこなっています!

フリーナンスでは今、プロダクト開発組織を立ち上げていこうとしている最中です。

ソフトウェアエンジニアとして面白い部分は、フロントエンド〜バックエンド〜インフラを横断的に扱う開発スタイルで開発している点です。 Go のコードを書いたり、Terraform を書いたりしながら開発を進めていることが多いです。

現在は Go や React を中心としたシステムへのリアーキテクチャを検討しています。 バックエンドアーキテクチャの設計やフレームワークの選定、検証とやれることは非常に多いです。

そんな技術課題の解消やユーザに価値を提供出来る仕組みづくりを私達と一緒に進めていってくださる方々を募集中です!以下の職種を積極採用しています!

また、カジュアル面談も大歓迎ですのでお気軽に申し込んでいただけると嬉しいです!