ファイルアップロードツールを作ってみた

#はじめに
PHPとJavaScriptを使ってブラウザ上で動作するシンプルなファイルアップロードツールを作りました。
Chromeブラウザで動作します。

#ツールイメージ
ツールのイメージはこんな感じになります。
![01_img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740996/75e52bd1-a1e0-c4d6-9725-e41cbffb55ec.png)
①テキストを保存する領域です。テキストエリアになっているので複数行保存出来ます。
②上部のテキストの内容を保存するボタンです。
③アップロードされているファイル一覧が表示される領域です。
ファイルがアップロードされている場合は、<b>ファイル名・ダウンロードボタン・削除ボタン</b>が表示されます。
④ファイルアップロード領域です。ここにファイルをドラッグ＆ドロップするとファイルがアップロードされます。

#ファイル構成
ツールのファイル構成はこのようになっています。
![02_img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/740996/4c30bb60-9150-0db1-96fd-e9894d59e090.png)

fileフォルダ・・アップロードしたファイルが格納されるフォルダ
Ajax.inc・・Ajax処理をまとめているJavaScriptライブラリファイル
index.html・・ブラウザのページファイル※このファイルにアクセスしてツールを起動します
server.php・・サーバ側の処理を行うPHPファイル
text.txt・・テキストの入力欄に入力し保存された文字を保存するテキストファイル

#クライアント側
クライアント側のソースは以下になります。

```javascript:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <title>file upload</title>
        <script type="text/javascript" src="Ajax.inc"></script>
        <script type="text/javascript">
            window.onload = function(){
                // テキスト読み込み
                sendData = createQueryParam( "flg", 1 );
                ajaxNetConnect( "./server.php", sendData, "POST", 1, "text_load_complete" );
            }
            // テキスト読み込み完了
            function text_load_complete(){
                // テキストエリアへ反映
                var dom = document.getElementById( 'save_text' );
                dom.value = ajaxRecvData;

                // ファイル一覧取得通信開始
                getFileList();
            }

            // テキスト保存実行
            function textSave(){
                var sendData;
                var text_val = document.getElementById('save_text').value;
                //console.log(dom);

                sendData = createQueryParam( "flg", 0, "text_val", text_val );
                //console.log( sendData );
                ajaxNetConnect( "./server.php", sendData, "POST", 1, "text_save_complete" );
            }
            // テキスト保存完了
            function text_save_complete(){
                //console.log( "受信データ:"+ajaxRecvData+"" );
                alert( "テキスト保存成功しました" );
            }

            // ドラッグ要素がドロップ要素に重なっている間の処理
            function f_dragover(event){
                // dragoverイベントをキャンセルして、ドロップ先の要素がドロップを受け付けるようにする
                event.preventDefault();
            }
  
            // ドロップ時の処理
            function f_drop(event){
                var file = event.dataTransfer.files[0];
                //console.log( file.name );

                // ファイル送信用AJAX
                var formData  = new FormData();
                formData.append( "flg", "2" );
                formData.append( "file", file );

                ajaxSendFormData( "./server.php", formData, "file_save_complete" );

                // エラー回避のため、ドロップ処理の最後にdropイベントをキャンセルしておく
                event.preventDefault();
            }

            // ファイル保存完了
            function file_save_complete(){
                console.log( 'file_save_complete' );
                // ファイル一覧取得通信開始
                getFileList();
            }

            // ファイル一覧取得
            function getFileList(){
                sendData = createQueryParam( "flg", 3 );
                ajaxNetConnect( "./server.php", sendData, "POST", 1, "file_list_complete" );
            }

            // ファイル一覧取得完了
            function file_list_complete(){
                //ajaxRecvData
                var obj = document.getElementById( 'file_list' );
                var str = "";
                if( ajaxRecvData != "file_not_found" ){
                    var fileNames = ajaxRecvData.split( "," );
                    str += "<table border='1'>";
                    str += "<tr>";
                    str += "<td width='500px'>ファイル名</td>";
                    str += "<td>ダウンロード</td>";
                    str += "<td>削除</td>";
                    str += "</tr>";
                    for( var i=0; i<fileNames.length; i++ ){
                        // ファイル名からフォルダ名を除く
                        var buff = fileNames[i].split( "/" );
                        var fileName = buff[2];

                        str += "<tr>";
                        str += "<td>"+fileName+"</td>";
                        str += "<td><a href='"+fileNames[i]+"' download>ダウンロード</a></td>";
                        str += "<td align='center'><a href='#' onClick='fileDeleteStart(\""+fileNames[i]+"\");'>■</a></td>";
                        str += "</tr>";
                    }

                    str += "</table>";
                    obj.innerHTML = str;

                } else {
                    str = "ファイルがありません";
                    obj.innerHTML = str;

                }
                //console.log( 'file_save_complete' );
            }

            // ファイル削除
            function fileDeleteStart( fileName ){
                var buff = fileName.split( "/" );
                var buffFileName = buff[2];
                // ファイル削除確認ダイアログ表示
                var value = window.confirm( "「"+buffFileName+"」を削除します、よろしいですか？" );
                if( value ){
                    sendData = createQueryParam( "flg", 4, "file_name", fileName );
                    ajaxNetConnect( "./server.php", sendData, "POST", 1, "file_delete_complete" );
                }
            }

            // ファイル削除完了
            function file_delete_complete(){
                if( ajaxRecvData == "file_delete_success" ){
                    alert( "ファイル削除成功" );
                    // ファイル一覧取得通信開始
                    getFileList();
                } else {
                    alert( "削除ファイルが見つかりませんでした" );
                }
            }
        </script>
</head>
<body>

    [テキスト]<br>
    <textarea id="save_text" name="save_text" rows="10" cols="100"></textarea>
    <br>

    <input type='button' id='button' name='button' onClick='textSave()' value='保存'><br><br>

    [ファイル一覧]<br>
    <div id='file_list' name='file_list'>

    </div><br>

    [ファイルアップロード]<br>
    <div id='file_upload' name='file_upload' style='width:300px; height:100px; border:1px dotted #333333;' ondragover="f_dragover(event);" ondrop="f_drop(event);">
        <p>ここにドラッグ</p>
    </div>

</body>

</html>
```
#コード解説
コードを見てもらえば分かるのですが、9割JavaScriptになっています。
また、このような小規模のシステムの時は極力ファイル数を増やしたくないので、サーバへのリクエスト時に「flg=0」といったパラメータを付与し、サーバ側ではフラグの値を見て「0:読み込み、1:保存」というように処理を分岐させるといったことをよくやります。
上記の観点でコードを見てもらえると処理が追いやすいかと思います。

続いて、Ajaxライブラリになります。

```javascript:Ajax.inc
//=======================================================================================
//
//  AJAX
//
//=======================================================================================
var xmlhttp;		// XMLHttpオブジェクト
var ajaxRecvData;	// 受信データ

var HTTP_GET = "GET";
var HTTP_POST = "POST";
var HTTP_RECV = 1;

//-----------------------------------------------------------
//     getXMLHttp : XMLHttpオブジェクト取得
//           引数 : 無し
//         戻り値 : XMLHttpオブジェクト
//-----------------------------------------------------------
function getXMLHttp()
{
	// ブラウザによって処理を変える
	if( window.XMLHttpRequest ){
		xmlhttp = new XMLHttpRequest();
	} else if( window.ActiveXObject ){
		try {
			xmlhttp = new ActiveXObject( "Msxml2.XMLHTTP" );
		} catch( e ){
			xmlhttp = new ActiveXObject( "Microsoft.XMLHTTP" );
		}
	}
	return xmlhttp;
}

//-----------------------------------------------------------
//   createQueryParam : クエリパラメータ作成
//               引数 : 名前,パラメータ(可変長引数)
//             戻り値 : 成功･･クエリパラメータ
//             戻り値 : 失敗･･false
//-----------------------------------------------------------
function createQueryParam()
{
	var param = new Object();
	var key;
	var array = new Array();

	if( arguments.length % 2 != 0 ){	// 引数が2の倍数でなければエラー
		alert( "エラー：クエリパラメータは[名前：値]のセットで指定して下さい" );
		return false;
	}

	// クエリパラメータを作成
	for( i=0; i<arguments.length; i+=2 ){
		param[arguments[i]] = arguments[i+1];
	}

	for( key in param ){
		array.push( key + "=" + encodeURIComponent(param[key]) );
	}
	array = array.join( "&" );	// クエリパラメータを&で結合する

	return array;
}

//-----------------------------------------------------------
//  ajaxNetConnect : データ送受信
//            引数 : URL,送信データ,通信種別(GET,POST)
//            引数 : 送受信フラグ,コールバック関数名
//          戻り値 : 無し
//-----------------------------------------------------------
function ajaxNetConnect( url, sendData, sendKind, sendFlg, funcName )
{
	if( xmlhttp == null ){
		getXMLHttp();	// XMLHttpオブジェクト取得
		if( xmlhttp == null ){
			alert( "エラー：XMLHttpオブジェクトがありません" );
			return;
		}
	}

	// 通信種別によって処理を変更
	if( sendKind == HTTP_GET ){	// GET通信
		if( sendData != null )	url += "?"+sendData+"";		// URLの後ろにリクエストパラメータを付与
	}

	xmlhttp.open( sendKind, url, true );	// オープン
	if( sendKind == HTTP_POST ){	// POST通信
		xmlhttp.setRequestHeader( "Content-Type", "application/x-www-form-urlencoded" );	// ヘッダーセット
		xmlhttp.send( sendData );
	}

	if( sendFlg == HTTP_RECV ){	// 受信の時のみ処理
		ajaxRecvData = null;	// 受信データ初期化
	}

	xmlhttp.onreadystatechange=function()
	{
		if( xmlhttp.readyState == 4  ){	// 準備完了
			if( xmlhttp.status == 200 ){	// 成功

				if( sendFlg == HTTP_RECV ){	// 受信の時のみ処理
					ajaxRecvData = xmlhttp.responseText;	// 受信データを保存
				}

			} else {	// 失敗

				if( sendFlg == HTTP_RECV ){	// 受信の時のみ処理
					ajaxRecvData = "error";
				}
			}

			if( funcName != null ){	// コールバック関数名がある場合のみ処理
				eval( funcName + '()' );	// コールバック関数実行
			}
		}
	};
	// GET通信の場合はここで送信
	if( sendKind == HTTP_GET )	xmlhttp.send( null );
}

//-----------------------------------------------------------
// ajaxSendFormData : フォームデータ用Ajax通信
//             引数 : URL,送信データ,コールバック関数名
//           戻り値 : 無し
//-----------------------------------------------------------
function ajaxSendFormData( url, sendData, funcName )
{
	if( xmlhttp == null ){
		getXMLHttp();	// XMLHttpオブジェクト取得
		if( xmlhttp == null ){
			alert( "エラー：XMLHttpオブジェクトがありません" );
			return;
		}
	}
	ajaxRecvData = null;	// 受信データ初期化

	xmlhttp.open( 'POST', url );
	xmlhttp.send( sendData );

	xmlhttp.onreadystatechange=function()
	{
		if( xmlhttp.readyState == 4  ){	// 準備完了
			if( xmlhttp.status == 200 ){	// 成功
				ajaxRecvData = xmlhttp.responseText;	// 受信データを保存

			} else {	// 失敗
				ajaxRecvData = "error";
			}

			if( funcName != null ){	// コールバック関数名がある場合のみ処理
				eval( funcName + '()' );	// コールバック関数実行
			}
		}
	};
}
```
#コード解説
Ajax通信を行うだけのライブラリなのですごくシンプルです。
少し特徴的な部分だけ解説します。
■<b>createQueryParam</b>メソッド
クエリパラメータの文字列を作成するメソッドなのですが、パラメータ数は通信の種類によって変化するために引数が可変長引数(arguments変数)になっています。

■<b>ajaxNetConnect</b>メソッド
Ajax通信を行っているメソッドです。
業務ではよくjQueryを使用してAjax通信処理を実行することが多いと思うのですが、素のJavaScriptでAjax通信を記述するとこんな感じになります。
第5引数の「funcName」でメソッド名を渡しているのですが、サーバからのデータ受信後にevalメソッドを実行することで、指定した引数(文字列)のメソッドを実行させることが出来ます。
こうすることで「通信A終了後はAメソッドを実行」「通信B終了後はBメソッドを実行」といった形でそれぞれ別メソッドに定義することが出来るようになります。

#サーバ側
サーバ側のソースは以下になります。

```php:server.php
<?php
$response = "responce_no_data";
$textFileName = "./file.txt";
$folderName = "./file";

$flg = $_REQUEST['flg'];

switch( $flg ){
case 0:     // テキスト保存
    $text_val = $_REQUEST['text_val'];
    $text_val = mb_convert_encoding( $text_val, "Shift-JIS", "UTF-8" );

    // テキストファイル保存
    if( file_exists($textFileName) ){
        unlink( $textFileName );
    }
    file_put_contents( $textFileName, $text_val );
    // レスポンス
    $response = "保存成功";
    break;

case 1:     // テキスト読み込み
    $response = file_get_contents( $textFileName );
    $response = mb_convert_encoding( $response, "UTF-8", "Shift-JIS" );
    break;

case 2:     // ファイル保存
    if( isset($_FILES['file']) ){
        $file = $_FILES['file'];
        $fileName = $file['name'];          // ファイル名

        $copyFolder = "".$folderName."/".$fileName."";   // コピーフォルダ名

        move_uploaded_file( $_FILES['file']['tmp_name'], $copyFolder );
    }
    break;

case 3:     // ファイル一覧取得

    $result = glob( "".$folderName."/*" );

    if( !empty($result) ){  // ファイルが存在する
        $response = "";
        for( $i=0; $i<count($result); $i++ ){
            if( $i > 0 )    $response .= ",";
            $response .= $result[$i];
        }

    } else {
        $response = "file_not_found";

    }
    break;

case 4:     // ファイル削除
    $deleteFileName = $_REQUEST['file_name'];

    if( file_exists($deleteFileName) ){
        unlink( $deleteFileName );
        $response = "file_delete_success";

    } else {
        $response = "file_not_found";
    }
    break;
}
echo $response;
```
#コード解説
サーバ側のコードはとてもシンプルになっています。
特にPHPはファイル保存や移動などが数行のコードで実現出来るのでとても便利です。
クライアント側でも書きましたが「<b>flg</b>」変数で分岐させているので処理が見通しやすくなっていると思います。

#最後に
とてもシンプルなツールですが、Ajax通信の連続実行(ページ読み込み時)やドラッグ＆ドロップのイベント処理など、業務に利用出来るような部分もあるかなと思います。
また[前回](https://qiita.com/noji505/items/b6fdb89ee93e14e20694)の内容と組み合わせると、外部からアクセス出来るツールにすることも出来るので是非チャレンジしてみて下さい。