1. 導入
Webアプリケーションにおいて、ユーザーからのファイルアップロード機能は定番ですが、そのまま保存すると既存ファイルの上書きによるデータ消失のリスクがあります。かといって、常にランダムな文字列(UUIDなど)に変換すると、ファイル本来の管理が難しくなるケースもあります。本記事では、既存のファイル名を守りつつ、同名ファイルが存在する場合は自動で連番を付与する、堅牢で再利用性の高い実装手法を解説します。
2. 基礎知識
ファイル名の重複を判定するために、PHPには file_exists() という標準関数が用意されています。これは指定したパスにファイルまたはディレクトリが存在するかを確認する関数です。これと、ファイルパスを分解する pathinfo() を組み合わせることで、拡張子を保持したまま「ファイル名_番号.拡張子」という形式を生成できます。再帰処理を用いることで、test_1、test_2…と、空いている番号が見つかるまで自動的に探索することが可能です。
3. 実装/解決策
実装のポイントは、ディレクトリトラバーサル攻撃などのセキュリティリスクを考慮しつつ、再帰的にファイルパスを生成することです。以下のコードでは、元のファイル名が「sample.jpg」であれば「sample_1.jpg」「sample_2.jpg」…と順にチェックを行い、まだ存在しないパスが見つかった時点でそのパスを返却します。
4. サンプルプログラム
/
function getUniqueFilePath(string $originalPath): string
{
// パス情報を分解
$info = pathinfo($originalPath);
$dir = $info[‘dirname’];
$filename = $info[‘filename’];
$extension = isset($info[‘extension’]) ? ‘.’ . $info[‘extension’] : ”;
$targetPath = $originalPath;
$counter = 1;
// ファイルが存在しなくなるまでループ(再帰ではなくループ処理の方がメモリ効率が良い)
while (file_exists($targetPath)) {
// 新しいパスを生成: ディレクトリ/ファイル名_連番.拡張子
$targetPath = sprintf(‘%s/%s_%d%s’, $dir, $filename, $counter, $extension);
$counter++;
}
return $targetPath;
}
// 使用例
$saveDir = __DIR__ . ‘/uploads’;
$uploadFile = $saveDir . ‘/document.pdf’;
$safePath = getUniqueFilePath($uploadFile);
// ファイル保存処理の実行
// file_put_contents($safePath, $data);
echo “保存先パス: ” . $safePath;
?>
5. 応用・注意点
実務でこのロジックを導入する際は、以下の点に注意してください。
・再帰関数のスタックオーバーフロー回避
参考元の再帰関数も有効ですが、大量のファイルが同一ディレクトリに存在する場合、スタックの上限に達する可能性があります。上記のサンプルコードのようにwhileループで記述する方が、メモリ消費が少なく、実務では推奨されます。
・Race Condition(競合状態)の防止
高トラフィックなサイトでは、ファイルパスを決定した「直後」に別のプロセスが同じ名前で保存してしまう可能性があります。厳密さを求める場合は、flock() を使用した排他制御や、データベース側で一意制約を設けるなどの対策を併用してください。
・ファイル名のサニタイズ
ユーザーから受け取ったファイル名をそのまま使用するのは非常に危険です。basename() を使用してディレクトリ区切り文字を除去し、日本語ファイル名などの場合は必要に応じてハッシュ化や英数字への変換を行うことを強くお勧めします。