ファイルアップローダー
Index
― 目次 ―「ファイルアップローダー」の仕様について
「ファイルアップローダー」には、以下のような機能を実装します。
仕様について- 「ファイルアップローダー」のディレクトリにBasic認証を設置
「ファイルアップローダー」にアクセスする際、セキュリティ面を考慮し、SSLで暗号化された領域内にBasic認証を設置。
ユーザー名とパスワードを入力した場合のみアクセス可能とする。 - 「ファイルアップローダー」内にメッセージ機能を実装
アップロードしたファイルに関する補足説明を登録できるように「メッセージ入力欄」を設置。 - ファイル一覧に表示される内容
ファイル名、ダウンロードボタン、削除ボタンの3つを表示。
ファイル名には、アップロード日時が分かるように「ファイル名の先頭に年・月・日・時間・分・秒のタイムスタンプを自動的に付与」。
表示順序は「新しいファイル順」とする。 - アップロードの方法
「参照ボタンよりファイルを選択してアップロードする形」ではなく、「ファイルをドラッグ & ドロップしてアップロードする形」とする。
※PCでの利用が前提 - 検証ブラウザ
Google Chrome(※2024.9.17時点での最新バージョン:128.0.6613.138)
ファイル構成
「ファイルアップローダー」のファイル構成は、以下のような感じです。
▼ファイル構成のイメージ画像
uploaderディレクトリ内に「ファイルアップローダーに必要なファイル一式」を格納します。
- .htaccessファイル
Basic認証に必要なファイル(※セキュリティ面を考慮し、.htpasswdファイルは、ブラウザからアクセスできる領域とは別の領域に設置) - uploadディレクトリ
アップロードファイルの格納ディレクトリ(※このディレクトリ内にメッセージ文保存用の「message.txt」も格納) - index.html
ブラウザ表示用のhtmlファイル(※URL/uploader/にアクセス → ファイルアップローダーのページ) - uploader.css
UI(ユーザーインターフェース)編集用のCSSファイル - ajax.js
Ajax通信用のJavaScripファイル - uploader.php
サーバー側の処理を行うphpファイル(※設定を変更したい場合はこちらを編集)
HTML・CSS・JavaScript(Ajax)・PHPなどの参考ソース
「ファイルアップローダー」に必要なHTML・CSS・JavaScript(Ajax)・PHPなどについて説明していきます。
前提として、uploaderディレクトリ以下は、Basic認証でアクセスを制限してください。
Basic認証については、参考できるWebサイトが多数あり、Webサーバーを利用されている方向けに「指定ディレクトリ以下へのアクセス制限機能」が付加されている場合もありますので、本記事内での説明は割愛します。
セキュリティ面を考慮して「SSLで暗号化された領域内にBasic認証を設置」「.htpasswdファイルは、ブラウザからアクセスできる領域とは別の領域に設置」「1つのユーザー名とパスワードを複数名で使いまわすのではなく、各自に発行」するようにしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
<!DOCTYPE html> <html lang="ja"? <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>ファイルアップローダー</title> <link rel="stylesheet" href="/uploader/uploader.css" media="all"> <script type="text/javascript" src="ajax.js"></script> <script type="text/javascript"> window.onload = function(){ // テキストの読み込み sendData = createQueryParam( "flg", 1 ); ajaxNetConnect( "./uploads.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( "./uploads.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( "./uploads.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( "./uploads.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 class='base_style'>"; str += "<tbody>"; str += "<tr>"; str += "<th class='w70'>ファイル名</th>"; str += "<th class='w15'>ダウンロード</th>"; str += "<th class='w15'>削除</th>"; str += "</tr>"; for( var i=0; i < fileNames.length; i++ ){ // ファイル名からフォルダ名を除去 var buff = fileNames[i].split( "/" ); var fileName = buff[2]; str += "<tr class='display_none'>"; str += "<td class='left'>"+fileName+"</td>"; str += "<td><a href='"+fileNames[i]+"' download>ダウンロードする</a></td>"; str += "<td><a href='#' onClick='fileDeleteStart(\""+fileNames[i]+"\");'>削除する</a></td>"; str += "</tr>"; } str += "</tbody>"; str += "</table>"; obj.innerHTML = str; } else { str = "<p style='color: #ff0000; margin-bottom: 30px;'>「メッセージ保存用のテキストファイル」をアップロードしてください</p>"; obj.innerHTML = str; } } // ファイル削除 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( "./uploads.php", sendData, "POST", 1, "file_delete_complete" ); } } // ファイル削除完了 function file_delete_complete(){ if( ajaxRecvData == "file_delete_success" ){ alert( "ファイルを削除しました" ); // ファイル一覧取得通信開始 getFileList(); } else { alert( "削除ファイルが見つかりませんでした" ); } } </script> </head> <body> <h1 class="ttl">ファイルアップローダー</h1> <h2 class="sub_ttl">[メッセージ]</h2> <textarea id="save_text" name="save_text" class="ta_style"></textarea> <input type="button" id="button" name="button" onClick="textSave()" value="メッセージを保存する" class="save_button"> <h2 class="sub_ttl">[ファイル一覧]</h2><p class="annotation"><span class="asterisk">※</span>アップロードされたファイルの先頭には「アップロード日時(年・月・日・時間・分・秒)」が自動的に付与されます</p> <div id="file_list" name="file_list"></div> <h2 class="sub_ttl">[ファイルアップロード]</h2><p class="annotation"><span class="asterisk">※</span>ファイルを1点ずつアップロードしてください(ファイル点数が多い場合、全ファイルをフォルダ内に格納し「ZIP圧縮したファイル」をアップロードしてください)</p> <div id="file_upload" name="file_upload" class="dd_style" ondragover="f_dragover(event);" ondrop="f_drop(event);">こちらに「ファイルをドラッグ & ドロップ」してください</div> </body> </html> |
6行目が外部CSSファイルへのパス指定(※UI編集用のCSSファイル)。
7行目が外部JavaScriptファイルのパス指定(※Ajax通信用のJavaScripファイル)。
8~126行目がJavaScriptの記述です。
こちらには「メッセージ用テキストの処理」「アップロードしたファイルに関する処理」を記述しています。
サーバーリクエストの際「flg=0」というようにパラメータを付与し、サーバーサイドでフラグの値を見て「0=読込」「1=保存」というように処理を分岐させています(※処理を分岐させることでファイル数を減らし、エコな作りにしています)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
@charset "utf-8"; /*********************************************************** File Uploader UI ***********************************************************/ /*----------------------------------------------- Text Link -----------------------------------------------*/ /* Base */ a:link { color: #323232; text-decoration: underline; } a:visited { color: #323232; } a:hover { color: #f01e69; text-decoration: underline; } /*----------------------------------------------- border-boxを全ての要素に適用 ※横幅・高さのサイズ調整 -----------------------------------------------*/ *, *:before, *:after { -webkit-box-sizing: border-box; box-sizing: border-box; } /*----------------------------------------------- Title -----------------------------------------------*/ /* Page Title */ .ttl { font-size: 16px; font-weight: normal; color: #ffffff; background-color: #000000; margin: 0px 0px 35px 0px; padding: 15px; width: 100%; } /*----------------------------------------------- Sub Title -----------------------------------------------*/ .sub_ttl { font-size: 16px; font-weight: normal; margin: 0px 0px 5px 0px; } /*----------------------------------------------- Annotation -----------------------------------------------*/ .annotation { font-size: 13px; font-weight: normal; line-height: 1.0em; margin: 0px 0px 10px 0px; padding: 0px; } .asterisk { color: #ff0000; } /*----------------------------------------------- Table -----------------------------------------------*/ /* table幅の指定:外枠あり・隣接するセルのボーダーを重ねて表示 */ table.base_style { margin: 0px 0px 30px 0px; padding: 0px; width: 100%; border-collapse: collapse; border: none; } /* th base */ table.base_style th { padding: 13px; font-weight: normal; text-align: center; border: 1px solid #c3c3c3; background-color: #eaeaea; } /* tr td base */ table.base_style tr td { padding: 13px; text-align: center; border: 1px solid #c3c3c3; } /* tr td custom:align left */ table tr td.left { text-align: left; } /* th custom:width 70% */ table.base_style th.w70 { width: 70%; } /* th custom:width 15% */ table.base_style th.w15 { width: 15%; } /* message.txtの非表示 */ tr.display_none:nth-child(2) { display: none; } /*----------------------------------------------- Textarea & Drag & Drop Area -----------------------------------------------*/ /* Textarea */ .ta_style { font-size: 15px; color: #323232; letter-spacing: 0.07em; line-height: 1.6em; margin: 0px; padding: 13px; border: 1px solid #c3c3c3; width: 100%; height: 200px; } /* Drag & Drop Area */ .dd_style { font-size: 15px; color: #323232; letter-spacing: 0.07em; line-height: 1.6em; margin: 0px; padding: 13px; border: 1px solid #c3c3c3; width: 100%; height: 200px; } /*----------------------------------------------- Button -----------------------------------------------*/ input.save_button { font-size: 15px; color: #ffffff; background-color: #000000; border: none; margin: 0px 0px 30px 0px; padding: 10px 30px 10px 30px; } |
上記のようなCSSを適応することで、最後に掲載している『「ファイルアップローダー」の画面イメージ』のようなUIに仕上がります。
※フォントに関しては、好みのWebフォントを使用したり、お好きなfont-familyを適応してください。
27~35行目が「border-box」プロパティに関する記述です。
テキストエリアなどにボーダーを適応すると「テキストエリアのサイズ=横幅(高さ)」ではなくなりますので(※ボーダー幅の分サイズが合わなくなる)、「サイズが合わないことにより横スクロールが発生する」原因となります。そのため「border-boxプロパティを全ての要素に適用し、横幅・高さのサイズ調整」を行います。
82~93行目が「隣接するセルのボーダーを重ねて表示」を指定。
これは、私が以前よりよく利用している手法で、合わせて「th・tr・tdのボーダー・横幅・表示位置などを調整」してやることで、table関連のタグを制御することができます。
126~129行目は「message.txt」の表示に関連してきます。
後程説明する「uploader.php」の記述を見ていただくと分かるかと思いますが、現状だと「メッセージ文の内容を保存するmessage.txtもファイル一覧に表示される」ようになっています。「message.txtファイルをダウンロード・削除できる」といったメリットもありますが、削除すると「メッセージ保存用のテキストファイル」をアップロードしてくださいとエラーが出力されるようにしていますので、CSSの類似クラスで非表示にしています。この場合「trに適応したクラスdisplay_noneに対して、nth-child(2)という類似クラスを使用」しています。これは「trの2つ目=テーブルの2行目」を意味しますので、「1つ目に表示されるファイル名(message.txt)を非表示にする」ということになります(※こちらに関しては、PHP側をカスタマイズしたり、CSS側をカスタマイズして削除ボタンだけ非表示にするなど、お好きに編集してみてください)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
//*************************************************************************************** // Ajax //*************************************************************************************** var xmlhttp; // XMLHttpオブジェクト var ajaxRecvData; // 受信データ var HTTP_GET = "GET"; var HTTP_POST = "POST"; var HTTP_RECV = 1; //----------------------------------------------------------- // getXMLHttp : 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 : クエリパラメータ作成 //----------------------------------------------------------- function createQueryParam() { var param = new Object(); var key; var array = new Array(); if( arguments.length % 2 != 0 ){ 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 : データ送受信 //----------------------------------------------------------- function ajaxNetConnect( url, sendData, sendKind, sendFlg, funcName ) { if( xmlhttp == null ){ getXMLHttp(); if( xmlhttp == null ){ alert( "エラー:XMLHttpオブジェクトがありません" ); return; } } // 通信種別によって処理を変更 if( sendKind == HTTP_GET ){ if( sendData != null ) url += "?"+sendData+""; } xmlhttp.open( sendKind, url, true ); if( sendKind == HTTP_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通信 //----------------------------------------------------------- function ajaxSendFormData( url, sendData, funcName ) { if( xmlhttp == null ){ getXMLHttp(); 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通信を行うためのスクリプトです
31~57行目がクエリパラメータの文字列を作成するメソッド。
※パラメータ数→通信種別により変化→引数が可変長引数(arguments変数)になる
59~111行目がAjax通信を実行するメソッド(※jQueryではなく、素のJavaScriptでAjax通信を処理)。
※第5引数funcNameでメソッド名を渡す→サーバーからのデータ受信後evalメソッドを実行→指定した引数(文字列)のメソッドを実行できる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
<?php $response = "responce_no_data"; // メッセージの保存先とファイル名 $textFileName = "./upload/message.txt"; // ファイルのアップロード先 $folderName = "./upload"; $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']; // 「".date("Y.m.d-H:i:s")."_」でファイル名の先頭にアップロード日時を追加 $copyFolder = "".$folderName."/".date("Y.m.d-H:i:s")."_".$fileName.""; move_uploaded_file( $_FILES['file']['tmp_name'], $copyFolder ); } break; case 3: // アップロードファイル一覧の取得 $result = glob( "".$folderName."/*" ); rsort($result); // ファイルを降順(アップロード日時が新しい順)で表示 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; |
3~4行目が「メッセージの保存先とファイル名」を指定。
5~6行目が「ファイルのアップロード先」を指定。
30-31行目が「ファイル名の先頭にアップロード日時を追加」(※「年.月.日-時間:分:秒_ファイル名.拡張子」となります)。
※ファイル名の先頭にアップロード日時のタイムスタンプを自動的に付与することで「アップロード日時をファイル単体で知ることができる(わざわざ管理画面を見なくても済みます)」「同名ファイルアップロード時の上書きを防ぐことができる」などのメリットがあります。
38行目が「ファイル一覧を降順(アップロード日時が新しい順)で表示」。
※ファイル名の先頭にアップロード日時のタイムスタンプを自動的に付与し、rsort()関数でファイル一覧を降順で表示することで、「アップロード日時が新しい順」に並びます。
※タイムスタンプがズレる場合、PHPのデフォルトタイムゾーンをご確認ください
→ php.iniを編集
1 2 |
; タイムゾーンの設定 date.timezone = Asia/Tokyo |
「ファイルアップローダー」の画面イメージ
「ファイルアップローダー」の完成イメージは、以下のような感じです。
▼ファイルアップローダーの画面イメージ
[仕様についての補足]
●アップロードできるファイルの種類について
現状、アップロードできるファイルの種類は指定していません。
「アップロードされたファイルをプレビューなどでブラウザ表示させること」が前提ではなく、「PCの指定フォルダにダウンロードさせること」が前提のためです。ブラウザでプレビュー表示させると、スクリプトファイルなどはブラウザで実行できてしまいますので、予期せぬ事態を防ぐためダウンロードと削除機能をメインとし、アップロードできるファイルの種類は制限していません。
●ドラッグ & ドロップ時の複数ファイル一括アップロードについて
現状、複数ファイル一括アップロードには対応していません。
画像などを大量に一括アップロードされるとファイル一覧が見ずらくなり管理も大変になります。そのため、ファイル点数が多い場合、全ファイルをフォルダ内に格納し「ZIP圧縮したファイル」をアップロードすることを推奨しています(※ファイルアップローダーの画面イメージより、「ファイルアップロード」下の注釈をご参照ください)。
この辺りは、おいおい改修していこうと思います。
今回は「ファイルアップローダー」についてご紹介しました。
制作時の参考になりましたら幸いです。
もう少し手を加えたい箇所もありますが、順次改修していこうと思っています(※記事内でも説明しておりますが、当ファイルアップローダーを参考にされる場合、セキュリティ面に配慮した上でご活用ください)。