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

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

新規登録して質問してみよう
ただいま回答率
85.48%
ファイル

ファイルとは、文字列に基づいた名前又はパスからアクセスすることができる、任意の情報のブロック又は情報を格納するためのリソースです。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

Q&A

解決済

4回答

12416閲覧

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

newyee

総合スコア213

ファイル

ファイルとは、文字列に基づいた名前又はパスからアクセスすることができる、任意の情報のブロック又は情報を格納するためのリソースです。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

0グッド

4クリップ

投稿2017/08/09 14:05

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

php

1<?php 2$filepath = unique_filename(dirname(__FILE__). '/' . 'test.txt'); 3file_put_contents($filepath, "sample"); 4echo $filepath; 5 6 7function unique_filename($org_path, $num=0){ 8 9 if( $num > 0){ 10 $info = pathinfo($org_path); 11 $path = $info['dirname'] . "/" . $info['filename'] . "_" . $num; 12 if(isset($info['extension'])) $path .= "." . $info['extension']; 13 } else { 14 $path = $org_path; 15 } 16 17 if(file_exists($path)){ 18 $num++; 19 return unique_filename($org_path, $num); 20 } else { 21 return $path; 22 } 23}

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

html

1<form method="post" enctype="multipart/form-data"> 2 <div><input type="file"name="new_img"></div> 3 <div><input type="submit" value="送信"></div> 4</form>

php

1$tmp_file = $_FILES['new_img']['tmp_name']; 2$file_name = 'img/' . $_FILES['new_img']['name']; 3if (is_uploaded_file($tmp_file) === TRUE){ 4 if ( move_uploaded_file($tmp_file,$file_name ) === TRUE){//同名ファイルがアップロードされた場合の処理 5 if (file_exists($faile_name)) { 6 $error_msg = '同名のファイルがアップロードされています。'; 7 } 8 //MIMEタイプ指定 9 $finfo = finfo_open(FILEINFO_MIME_TYPE); 10 $mime_type = finfo_file($finfo, $file_name); 11 finfo_close($finfo); 12 13 if(strpos($mime_type,'jpeg')||strpos($mime_type,'jpg')||strpos($mime_type,'png') === FALSE){ 14 unset($file_name); 15 $error_msg[] = 'ファイル形式が異なります。画像ファイルはJPEG又はPNGのみ利用可能です。'; 16 } 17 } else { 18 $err_msg[] = 'ファイルがアップロードできません'; 19 } 20 21} else { 22 $err_msg[] = 'ファイルが選択されていません'; 23}

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

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

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' というオプションがあり、これを行うことができます。

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

PHP

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

投稿2017/08/24 03:19

ockeghem

総合スコア11701

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

newyee

2017/08/24 03:57

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

0

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

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

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

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

投稿2017/08/10 18:20

chitoku

総合スコア1610

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

newyee

2017/08/11 01:21

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

2017/08/13 16:07

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

0

ベストアンサー

私も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になります。

進みます。

PHP

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

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

PHP

1 else { 2 $path = $org_path; 3 }

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

PHP

1 if(file_exists($path)){ 2 $num++; 3 return unique_filename($org_path, $num); 4 } else { 5 return $path; 6 }

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

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

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

PHP

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

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

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

PHP

1Array 2( 3 'dirname' => /foo/bar 4 'basename' => test.txt 5 'extension' => txt 6 'filename' => test 7)

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

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

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

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

PHP

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

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

PHP

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

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

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

投稿2017/08/09 16:32

y_ahiru

総合スコア50

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

newyee

2017/08/10 16:36

大変ご丁寧なご回答ありがとうございます。 unique_filename関数について全く理解できなかった所から、かなり理解できるところまで到達だきたかなと思っております。 一点だけ、ご質問があるのですが、僕のコードに組み込んで説明して下さったこの部分、 「if ( move_uploaded_file($tmp_file,unique_filename($filename)) === TRUE){」 ここなんですが、unique_filename関数の第二引数に$numを指定していませんが、ここの部分は省略可能と説明があったのでそうだと思うのですが、$numを省略した場合、 if(file_exists($path)){ $num++; return unique_filename($org_path, $num); ここの「$num++」された値は、変数が宣言されていないのに、 「if( $num > 0){」の$numに代入できるということなのでしょうか? 説明がうまくできず申し訳ありません。。。 もし質問の意図が分からない場合がありましたら、ご指摘頂けたらと思います...
y_ahiru

2017/08/11 02:28

理解が進んだとのことで、たいへん光栄です。 それは、unique_filename関数を宣言した行の引数部分の秘密があります。 function unique_filename($org_path, $num=0) 第2引数がただの$numではなく、$num=0になっていることがわかると思います。 これは、第二引数が省略された場合は$numに0を代入してくださいという意味になります。 なので、unique_filename($filename)とuniqu_filename($filename, 0)は全く同じ動作をするということです。
newyee

2017/08/11 03:45

ご返信ありがとうございます。 なるほど!理解できました!ご丁寧にありがとうございます。 実は、もう一点どうしても分からない所がでてきてしまいまして...ごめんなさい!もう一点だけ質問させて下さい! 処理の流れとしまして、 「if ( move_uploaded_file($tmp_file,unique_filename($filename)) === TRUE){」でunique_filename関数の中身が実行されるます。 次に関数の中の実行順序なのですが、if( $num > 0){ ←一番最初ここのifは $num が0な為、実行はされず飛ばされると思います。 なので一番最初は、ここのif文が実行されると解釈しています。 if(file_exists($path)){ $num++; return unique_filename($org_path, $num); } else { return $path; } ここでなのですが、if(file_exists($path)){ ←ここの部分の「$path」なんですが、最初の「if文」が飛ばされているため「$path」の中身には何も代入されておらず空なので、ファイルが存在するかどうか確かめることは不可能なのではないかと思っています... ご回答下さった通り自分のコードに組み込んで実行してみたところ、連番が振られ成功はしたのですが、「$path」の部分がなぜなのかどうしても理解できなくて... お手数かとは思いますが、ご返信頂けたら幸いです。
y_ahiru

2017/08/11 04:23

if($num > 0)がFalseである場合はelse句の$path = $org_path;が実行されるので、$pathが未定義になることはありません! なので、最初のifが飛ばされた場合、次に実行されるのは else { $path = $org_path; } の部分になります。
newyee

2017/08/11 04:35

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

2017/08/11 04:38

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

0

あなたのコードに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/09 15:19

tsuemura

総合スコア663

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

newyee

2017/08/09 16:09

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問