大規模ファイル転送システムの技術的課題
20GB超の大容量ファイルをHTTPプロトコル経由で安定して転送するには、単純なストリーム送信ではタイムアウトやメモリ枯渇のリスクが伴います。特に、フォルダ階層の維持、クライアント側の暗号化(SM4/AES)、ブラウザ再起動やネットワーク切断からの自動再開、そしてInternet Explorer 9を含むレガシー環境との互換性を満たす実装は、アーキテクチャの設計段階から慎重な検討を要求します。
クライアント側の分塊処理と暗号化戦略
大規模ファイルを扱う場合、フロントエンドではファイルを所定のサイズに分割し、各チャンクに一意の識別子と順序番号を付与して送信します。暗号化処理はクライアント側で実施することで、サーバーの負荷を軽減し、中間者攻撃からの保護を強化できます。レガシーブラウザ対策としては、ES2015以降の構文をトランスパイルし、Blob APIやFetch APIのフォールバック実装を準備することが不可欠です。
フロントエンド実装例(TypeScript / 非同期処理最適化)
const CHUNK_THRESHOLD = 2 * 1024 * 1024; // 2MB
const MAX_CONCURRENT_REQUESTS = 3;
async function initializeChunkedTransfer(file, cryptoKey) {
const fileHash = await generateFileHash(file);
const chunkCount = Math.ceil(file.size / CHUNK_THRESHOLD);
const chunkPromises = [];
for (let i = 0; i < chunkCount; i++) {
const start = i * CHUNK_THRESHOLD;
const end = Math.min(start + CHUNK_THRESHOLD, file.size);
const chunkBlob = file.slice(start, end);
chunkPromises.push(encryptAndSendChunk({
blob: chunkBlob,
index: i,
total: chunkCount,
identifier: fileHash,
cryptoKey: cryptoKey,
fileName: file.name,
filePath: file.webkitRelativePath || file.name
}));
}
await executeWithConcurrency(chunkPromises, MAX_CONCURRENT_REQUESTS);
}
async function encryptAndSendChunk(params) {
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encryptedBuffer = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
params.cryptoKey,
params.blob
);
const formData = new FormData();
formData.append("part", new Blob([encryptedBuffer]), "part_" + params.index);
formData.append("metadata", JSON.stringify({
hash: params.identifier,
sequence: params.index,
totalParts: params.total,
originalName: params.fileName,
directoryPath: params.filePath,
iv: Array.from(iv).map(b => b.toString(16)).join("")
}));
try {
const response = await fetch("/api/v1/upload/chunk", {
method: "POST",
body: formData
});
if (!response.ok) throw new Error("Chunk upload failed");
} catch (err) {
console.warn("Retry scheduled for chunk:", params.index);
}
}
サーバーサイドの整合性検証とマージ処理
Java(Spring Boot)側では、各チャンクを受信し、一時的なワークディレクトリに格納します。すべてのチャンクが到着し、合計サイズとハッシュ値が一致した時点で、スレッドセーフなロック機構を用いてファイルをマージし、最終的なストレージパスへ移動させます。フォルダ構造の復元は、メタデータに含まれる相対パス情報を基に行います。
バックエンド実装例(Spring Boot / Java 17)
@RestController
@RequestMapping("/api/v1/upload")
@RequiredArgsConstructor
public class TransferController {
private static final Path TEMP_WORKSPACE = Paths.get("var/upload-staging");
private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
@PostMapping("/chunk")
public ResponseEntity<Map<String, Object>> handleChunk(
@RequestParam("part") InputStream chunkStream,
@RequestParam("metadata") String metaPayload) {
try {
TransferMetadata meta = JsonUtils.parse(metaPayload, TransferMetadata.class);
validateChunkIntegrity(meta);
Path targetChunkPath = TEMP_WORKSPACE
.resolve(meta.getHash())
.resolve("part_" + meta.getSequence());
Files.createDirectories(targetChunkPath.getParent());
Files.copy(chunkStream, targetChunkPath, StandardCopyOption.REPLACE_EXISTING);
if (isAllChunksReceived(meta.getHash(), meta.getTotalParts())) {
consolidateAndDecrypt(meta);
}
return ResponseEntity.ok(Map.of("status", "accepted", "sequence", meta.getSequence()));
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
private void consolidateAndDecrypt(TransferMetadata meta) throws IOException {
Path finalDir = Paths.get("var/archive")
.resolve(YearMonth.now().toString())
.resolve(meta.getDirectoryPath());
Files.createDirectories(finalDir);
Path finalFile = finalDir.resolve(meta.getOriginalName());
try (OutputStream out = Files.newOutputStream(finalFile, StandardOpenOption.CREATE)) {
for (int i = 0; i < meta.getTotalParts(); i++) {
Path chunkPath = TEMP_WORKSPACE.resolve(meta.getHash()).resolve("part_" + i);
decryptStream(Files.newInputStream(chunkPath), out, meta.getIvForPart(i));
Files.deleteIfExists(chunkPath);
}
}
Files.deleteIfExists(TEMP_WORKSPACE.resolve(meta.getHash()));
}
}
ストレージパス設計と状態管理
ファイルの永続化パスは、日付ベースのディレクトリ構造と一意の識別子を組み合わせることで、ディスクのフラグメンテーションを抑制し、バックアップポリシーの実施を容易にします。例:var/archive/2024-05/12/<unique-id>/<original-filename>
断点續伝(レジューム)機能を実現するには、クライアント側のIndexedDBまたはLocalStorageに、ファイルハッシュ、送信済みチャンク番号、ネットワーク状態を記録します。セッションが復元された際、ローカルストレージとの比較により未送信部分のみを特定し、HTTPリクエストを再構築します。IE9環境ではIndexedDBが非対応であるため、LocalStorageへのJSONシリアライズと、XMLHttpRequest Level 1 ベースの送信ロジックへのダウングレード処理が必須となります。
暗号化キーの管理については、クライアント生成の一時セッションキーと、サーバー保持のマスターキーを用いた鍵交換プロトコルを採用し、各チャンクごとに独立した初期化ベクトル(IV)を付与することで、パターン解析を回避します。これにより、20GB規模のファイルを分割・暗号化・送信・再構築する一連のパイプラインが、リソース制約のある環境でも安定して動作する基盤が構築できます。