質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.37%

[PHP]ファイルアップロード時に同名ファイルが存在する場合の処理について

解決済

回答 4

投稿

  • 評価
  • クリップ 4
  • VIEW 7,777

score 153

PHP勉強中の者なのですが、ファイルアップロード時に同名ファイルが存在する場合の処理について考えています。
自分なりに調べた結果、「同じ名前のファイルが存在するときは番号(連番)を付ける」と書いてあるサイトを見つけたのですが、書いてあるコードが難しく、理解することができませんでした。その時のコードはこちらになります。

<?php
$filepath = unique_filename(dirname(__FILE__). '/' . 'test.txt');
file_put_contents($filepath, "sample");
echo $filepath;


function unique_filename($org_path, $num=0){

    if( $num > 0){
        $info = pathinfo($org_path);
        $path = $info['dirname'] . "/" . $info['filename'] . "_" . $num;
        if(isset($info['extension'])) $path .= "." . $info['extension'];
    } else {
        $path = $org_path;
    }

    if(file_exists($path)){
        $num++;
        return unique_filename($org_path, $num);
    } else {
        return $path;
    }
}


僕の作成した、ファイルアップロード時の処理については以下になります。

<form method="post" enctype="multipart/form-data">
  <div><input type="file"name="new_img"></div>
  <div><input type="submit" value="送信"></div>
</form>
$tmp_file = $_FILES['new_img']['tmp_name']; 
$file_name = 'img/' . $_FILES['new_img']['name']; 
if (is_uploaded_file($tmp_file) === TRUE){
    if ( move_uploaded_file($tmp_file,$file_name ) === TRUE){//同名ファイルがアップロードされた場合の処理
        if (file_exists($faile_name)) {
          $error_msg = '同名のファイルがアップロードされています。';
          }
      //MIMEタイプ指定
      $finfo = finfo_open(FILEINFO_MIME_TYPE);
      $mime_type = finfo_file($finfo, $file_name);
      finfo_close($finfo);         

      if(strpos($mime_type,'jpeg')||strpos($mime_type,'jpg')||strpos($mime_type,'png') === FALSE){
         unset($file_name);
         $error_msg[] = 'ファイル形式が異なります。画像ファイルはJPEG又はPNGのみ利用可能です。';
      }
  } else {
     $err_msg[] = 'ファイルがアップロードできません';
        }

} else {
    $err_msg[] = 'ファイルが選択されていません';
}


某サイトのコードの、unique_filename関数の中身に関してはどこでどのような処理が行われてどうなっているかが、全くと言っていい程分からない状態です...
僕のコードに何か付け加える形でできるのならば、そうしたいと考えております。
どなたか簡単にご説明くださる方いましたら、教えて下さい...

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 4

+3

アップロードされたファイルのファイル名をファイルに使用すること自体を避けてください。
$_FILES['new_img']['name'] から取得されるファイル名は PHP のバージョンによっては脆弱性の原因になる[1]ほか、最新の PHP においてもファイル名自体が空になることがあるため安全ではありません。

同じファイル名のファイルがアップロードされることを考慮しなければならないのも、アップロードされたファイル名を使用していることが原因です。同時アクセスを考慮すると、アプローチとしては連番をつけるのではなく次のようなものが挙げられます[2]。

  • ファイルのハッシュ値を取得してファイル名とする
  • 乱数を使用してファイル名とする

[1] PHPにおけるファイルアップロードの脆弱性CVE-2011-2202 | 徳丸浩の日記
[2] ファイルアップロードの例外処理はこれぐらいしないと気が済まない - Qiita

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/08/11 10:21

    ご回答ありがとうございます。
    [2]のサイトを参照させて頂いたのですが、重複アップロードを防止する方法については僕の知識では現状難しかったので、取り敢えず連番を付けるという方法を取らせていただきたいと思います。
    もう一点、「アップロードされたファイルのファイル名をファイルに使用すること自体を避けてください。」ここの部分なのですが、具体的にはどこの部分を修正すれば良いでしょうか?
    勉強不足で申し訳ないですが、ご返信いただけたら幸いです。

    キャンセル

  • 2017/08/14 01:07

    回答でも示しましたが、以下の2通りです。
    ・ファイルのハッシュ値を取得してファイル名
    ・乱数を使用してファイル名
    move_uploaded_file にユーザーから渡されたファイル名を使用するよりかは、完全な連番(1234.jpg → 1234 番目にアップロードされたファイル)の方が安全です。

    キャンセル

+3

unique_filename関数には、TOCTOUと言われるタイプの問題があります。
これは、同じファイル名で同時にアップロードが行われた場合に、タイミングによっては、ファイル名の衝突が起きてしまうというものです。具体的には下記のような状況です。

元々 a.png は存在しない

タイミング  ユーザ A                          ユーザ B
1          a.png をアップロード
2
3          if(file_exists('a.png')) ...          a.png をアップロード
4            上記は false になる
5          return 'a.png';                       if(file_exists('a.png')) ...
6          ...                                     上記は false になる
7          a.pngでファイル作成           return 'a.png';
8                                                 ...
9                                                 a.pngでファイル作成

すなわち、同時に同名のファイルがアップロードされた場合、まだその名前のファイルはないので、ファイル名チェックとファイル作成のタイミングのずれにより、両方の処理で同じファイル名が使われてしまう可能性があります。

これを避けるには、ファイルの存在確認を行うと同時に、もしファイルがなければファイルを作成する、という処理を行います。PHPのfopenは 'x' というオプションがあり、これを行うことができます。

簡単なサンプルを作ってみました。再帰処理にするまでもない処理なのでループにしています。

function unique_filename($org_path) {
  $info = pathinfo($org_path);
  $path = $org_path;
  $num = 0;
  do {
    $fp = @fopen($path, 'x'); // 'x'の指定により、元々$pathが存在する場合はエラーになる
    if ($fp) break;
    $num++;
    $path = $info['dirname'] . "/" . $info['filename'] . "_" . $num;
    if(isset($info['extension'])) $path .= "." . $info['extension'];
  } while ($num < 100);  // 過度に同名のファイルがある状況はエラーにしているが適宜調整のこと
  if ($fp === FALSE)
    die('ファイルが作成できません');
  fclose($fp);  // このタイミングで $pathというファイルは存在したままだが
  return $path; // アップロード処理で上書きしてしまう(いったん消してはいけない)
}

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/08/24 12:57

    ご回答ありがとうございます。
    後でじっくり確認させていただき、もし、またご質問があったらお聞きさせて頂くかもしれません!w

    キャンセル

checkベストアンサー

+2

私もPHP勉強中です。共に勉強頑張りましょう!

unique_filename関数についての説明ですが、
すでに存在するファイル名の場合はファイル名の後に「_$num」をくっつけて返すという処理になっています。

コードを読んでいきましょう。
1行目のunique_filename($org_path, $num=0)ですが

まず、unique_filename関数は$org_path$numという引数を取ることがわかります。$numは省略可能な引数で、省略した場合は0が渡されます。
例では$org_pathdirname(__FILE__). '/' . 'test.txt')が渡されていて、$numは省略されていますね。
ちなみに、__FILE__はPHPで定義されている特殊な定数で、呼び出したファイルのPathが入っています。
そしてdirname関数はファイルの親ディレクトリを返すので、ファイルが/foo/bar/sample.phpだった場合は__FILE__/foo/bar/sample.phpで、dirname(__FILE__)/foo/barになります。

進みます。

    if( $num > 0){
        $info = pathinfo($org_path);
        $path = $info['dirname'] . "/" . $info['filename'] . "_" . $num;
        if(isset($info['extension'])) $path .= "." . $info['extension'];
    } 

最初のif文は、今の段階ではあまり意味をもちませんのでとりあえず無視してelseへ進みます。

    else {
        $path = $org_path;
    }


引数の$org_path$pathに代入しているだけですね。
次へ進みましょう。

    if(file_exists($path)){
        $num++;
        return unique_filename($org_path, $num);
    } else {
        return $path;
    }

この部分が、この関数のキモです。

ここで何をやっているかというと、if文でファイルがすでに存在しているかどうかを調べます。
存在していない場合は、最初に与えられたpathをそのまま返します。
存在している場合は$numを+1します。
そして+1した$numを引数にしたunique_filenameを再び実行します。

つまり、引数を変えてunique_filename関数の最初に戻ります。

    if( $num > 0){
        $info = pathinfo($org_path);
        $path = $info['dirname'] . "/" . $info['filename'] . "_" . $num;
        if(isset($info['extension'])) $path .= "." . $info['extension'];
    } 


ここで最初のif文が意味を持ち始めます。
今回の$numは1を与えられているので、$num > 0はtrueとなり、if内のコードが実行されます。

pathinfo関数は第二引数を省略した場合はpathを分解した次のような連想配列を返します。

Array
(
    'dirname' => /foo/bar
    'basename' => test.txt
    'extension' => txt
    'filename' => test
)

つまり$path = $info['dirname'] . "/" . $info['filename'] . "_" . $num;の部分は、親ディレクトリ+'/'+ファイル名(test) + '_' + $num(1)$pathに代入しているということですね。
最後に、ファイルに拡張子があれば追加で拡張子も$pathに連結させています。

そして関数の最後に再び同じファイル名が存在するかどうかを調べて、存在しなければtest_1.txtをreturnし、存在すれば再びunique_filename関数を呼び出し、test_2.txtを返す・・・
という処理を、ファイル名が被らなくなるまで繰り返し続けるということですね。

このような自分の中で自分を呼び出すような関数を再帰処理と言います。
少し不思議な感じがしますが、とても便利な考え方なので、勉強してみるといいと思います。

newyeeさんのコードに組み込むとすれば、どこかでunique_filename関数を定義した上で4行目を

if ( move_uploaded_file($tmp_file,unique_filename($filename)) === TRUE){


とすれば良いかと思います。
ただ、その場合は

if (file_exists($faile_name)) {
    $error_msg = '同名のファイルがアップロードされています。'       
}


は不要になるかと思います。

長くなってしまって申し訳ありません、私の説明で理解できたら幸いです。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/08/11 13:23

    if($num > 0)がFalseである場合はelse句の$path = $org_path;が実行されるので、$pathが未定義になることはありません!

    なので、最初のifが飛ばされた場合、次に実行されるのは
    else {
    $path = $org_path;
    }
    の部分になります。

    キャンセル

  • 2017/08/11 13:35

    なるほどです...ようやく理解できました!
    if( $num > 0){ ←ここでtrueの場合のみ「if」文が実行される訳じゃないですもんね...
    「false」だから else {$path = $org_path;}ここに飛んで、次の「if(file_exists($path)){」←ここの部分に飛ぶんですよね...
    理解できて頭スッキリしました!
    長々とお付き合いただき、ご丁寧に教えて下さり本当にありがとうございました!

    キャンセル

  • 2017/08/11 13:38

    いえいえ、お役に立てて光栄です。
    これからも頑張ってください!

    キャンセル

+1

あなたのコードにunique_filename()を組み込むだけであれば
$file_name = unique_filename('img/' . $_FILES['new_img']['name']);
とすれば、同名のファイルがある場合は連番を付与してくれます。

unique_filename()関数は再帰を使っているので、一見分かりにくく見えますが、分かってしまえば何の事はないです。
前半部分はパスの組み立て、
後半部分はファイルの存在確認をしていると考えて下さい。

後半部分で再帰処理が使われていますが、
「ファイルが存在している場合、引数$numの値を1つ増加させてunique_filename関数を再実行する」
「ファイルが存在しない場合は、前半で組み立てたパスを返却する」
という仕組みで、ユニークなファイル名になるまでインクリメントします。

再帰処理についてはググれば色々出てくると思いますが、teratailの中だと
https://teratail.com/questions/52947
などが参考になると思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/08/10 01:09

    ありがとうございます。
    再起処理について調べてみようと思います。
    URL参考にさせて頂きますね!

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.37%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る