File Uploader With NodeJs

File Uploader With NodeJs

File Uploader With NodeJs 一些紀錄與坑

前言

想要做一個可以上傳檔案到伺服器的基礎網站一些歷程記錄。

前置準備

這當中會用到 formidable模組,可以先行安裝

npm install express formidable --save

The Front End

HTML

簡單的前端使用bootstrap 就行了 在 views/ 之中

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title>File Uploader - coligo.io</title>
<link href='https://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<link href="css/styles.css" rel="stylesheet">
</head>
<body>

<div class="container">
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default">
<div class="panel-body">
<span class="glyphicon glyphicon-cloud-upload"></span>
<h2>File Uploader</h2>
<h4>coligo.io</h4>
<div class="progress">
<div class="progress-bar" role="progressbar"></div>
</div>
<button class="btn btn-lg upload-btn" type="button">Upload File</button>
</div>
</div>
</div>
</div>
</div>

<input id="upload-input" type="file" name="uploads[]" multiple="multiple"></br>

<script src="https://code.jquery.com/jquery-2.2.0.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script src="javascripts/upload.js"></script>
</body>
</html>

<input id="upload-input" type="file" name="uploads[]" multiple="multiple">

這行需要特別注意 type=”file” 將會時做出一個選擇檔案的按鈕用來上傳。

imgur

multiple="multiple 可以更改決定上船是否為單一或是多個檔案。

接著先將上面的<input id="upload-input" type="file" name="uploads[]" multiple="multiple">使用CSS隱藏

1
2
3
#upload-input {
display: none;
}

因為後面會使用端的javascirpt來操作他。

CSS

public/css/ 之中創建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
.btn:focus, .upload-btn:focus{
outline: 0 !important;
}

html,
body {
height: 100%;
background-color: #4791D2;
}

body {
text-align: center;
font-family: 'Raleway', sans-serif;
}

.row {
margin-top: 80px;
}

.upload-btn {
color: #ffffff;
background-color: #F89406;
border: none;
}

.upload-btn:hover,
.upload-btn:focus,
.upload-btn:active,
.upload-btn.active {
color: #ffffff;
background-color: #FA8900;
border: none;
}

h4 {
padding-bottom: 30px;
color: #B8BDC1;
}

.glyphicon {
font-size: 5em;
color: #9CA3A9;
}

h2 {
margin-top: 15px;
color: #68757E;
}

.panel {
padding-top: 20px;
padding-bottom: 20px;
}

#upload-input {
display: none;
}

@media (min-width: 768px) {
.main-container {
width: 100%;
}
}

@media (min-width: 992px) {
.container {
width: 450px;
}
}

javascript

前面樣式與面綁調整好之後接下來就是使用者端javascript的邏輯部分,先解決剛剛隱藏掉的upload按鈕如下:

1
2
3
$('.upload-btn').on('click', function (){
$('#upload-input').click();
});

然後因為有處理進度條的 %數 需要重置因此如下:

1
2
3
4
5
$('.upload-btn').on('click', function (){
$('#upload-input').click();
$('.progress-bar').text('0%');
$('.progress-bar').width('0%');
});

接下來是最重要的上傳邏輯部分 #uplaod-input 改變時做出將檔案使用陣列與key/value方式整理好如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$('#upload-input').on('change', function(){

var files = $(this).get(0).files;

if (files.length > 0){
// One or more files selected, process the file upload

// create a FormData object which will be sent as the data payload in the
// AJAX request
var formData = new FormData();

// loop through all the selected files
for (var i = 0; i < files.length; i++) {
var file = files[i];

// add the files to formData object for the data payload
formData.append('uploads[]', file, file.name);
}

}

});

緊接著 AJAX request 使用POST方法送到我們 /upload 的endpoint:

1
2
3
4
5
6
7
8
9
10
$.ajax({
url: '/upload',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(data){
console.log('upload successful!');
}
});

注意賈有使用Csrf做認證的話需要以下而外的東西

  1. getCsrfToken的函數:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function getCsrfToken() { 
    var metas = document.getElementsByTagName('input');

    for (i=0; i<metas.length; i++) {
    if (metas[i].getAttribute("name") == "_csrf") {
    console.log(metas[i].getAttribute("name"))
    return metas[i].getAttribute("value");
    }
    }

    return "";
    }
  2. 在headers加入認證header:
    1
    2
    3
    4
    5
    6
    7
    8
    var csrf = getCsrfToken();
    $.ajax({
    ...
    headers: {
    '_csrf':csrf,
    'X-CSRF-Token': csrf
    },
    ...

基本上這樣上傳邏輯就完成了,下面是要控制 progress-bar 的 callback:

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
xhr: function() {
// create an XMLHttpRequest
var xhr = new XMLHttpRequest();

// listen to the 'progress' event
xhr.upload.addEventListener('progress', function(evt) {

if (evt.lengthComputable) {
// calculate the percentage of upload completed
var percentComplete = evt.loaded / evt.total;
percentComplete = parseInt(percentComplete * 100);

// update the Bootstrap progress bar with the new percentage
$('.progress-bar').text(percentComplete + '%');
$('.progress-bar').width(percentComplete + '%');

// once the upload reaches 100%, set the progress bar text to done
if (percentComplete === 100) {
$('.progress-bar').html('Done');
}

}

}, false);

return xhr;
}

最後整個js檔案會像這樣裡面我自己有一些註解的debug用程式碼:

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
$('.upload-btn').on('click', function (){
$('#upload-input').click();
$('.progress-bar').text('0%');
$('.progress-bar').width('0%');
});

$('#upload-input').on('change', function(){

var files = $(this).get(0).files;

if (files.length > 0){
// create a FormData object which will be sent as the data payload in the
// AJAX request
var formData = new FormData();

// loop through all the selected files and add them to the formData object
for (var i = 0; i < files.length; i++) {
var file = files[i];

// add the files to formData object for the data payload
formData.append('uploads[]', file, file.name);
}
function getCsrfToken() {
var metas = document.getElementsByTagName('input');

for (i=0; i<metas.length; i++) {
if (metas[i].getAttribute("name") == "_csrf") {
console.log(metas[i].getAttribute("name"))
return metas[i].getAttribute("value");
}
}

return "";
}

var csrf = getCsrfToken();
// console.log(csrf)
$.ajax({
url: '/account/upload',
type: 'POST',
data: formData ,
headers: {
'_csrf':csrf,
'X-CSRF-Token': csrf
},
processData: false,
contentType: false,
success: function(data){
console.log('upload successful!\n' + data);

},
error: function (xhr, ajaxOptions, thrownError) {
//Add these parameters to display the required response
//alert(xhr.status);
//console.log(xhr.responseText);
},
xhr: function() {
// create an XMLHttpRequest
var xhr = new XMLHttpRequest();

// listen to the 'progress' event
xhr.withCredentials = true;
xhr.upload.addEventListener('progress', function(evt) {

if (evt.lengthComputable) {
// calculate the percentage of upload completed
var percentComplete = evt.loaded / evt.total;
percentComplete = parseInt(percentComplete * 100);

// update the Bootstrap progress bar with the new percentage
$('.progress-bar').text(percentComplete + '%');
$('.progress-bar').width(percentComplete + '%');

// once the upload reaches 100%, set the progress bar text to done
if (percentComplete === 100) {
$('.progress-bar').html('Done');
alert('upload successful!\n');
}

}

}, false);
//console.log(xhr.getAllResponseHeaders());
return xhr;
}
});

}
});

The Back End: Processing the Upload

後端基本上就比較簡單了,這裡就不贅述expressjs的用法直接貼上完整的server.js

很多情況後端不會這麼簡單需要自行修改。

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
var express = require('express');
var app = express();
var path = require('path');
var formidable = require('formidable');
var fs = require('fs');

app.use(express.static(path.join(__dirname, 'public')));

app.get('/', function(req, res){
res.sendFile(path.join(__dirname, 'views/index.html'));
});

app.post('/upload', function(req, res){

// create an incoming form object
var form = new formidable.IncomingForm();

// specify that we want to allow the user to upload multiple files in a single request
form.multiples = true;

// store all uploads in the /uploads directory
form.uploadDir = path.join(__dirname, '/uploads');

// every time a file has been uploaded successfully,
// rename it to it's orignal name
form.on('file', function(field, file) {
fs.rename(file.path, path.join(form.uploadDir, file.name));
});

// log any errors that occur
form.on('error', function(err) {
console.log('An error has occured: \n' + err);
});

// once all the files have been uploaded, send a response to the client
form.on('end', function() {
res.cookie('fileName',fileName)
res.end('/uploads/News/'+fileName);
});

// parse the incoming request containing the form data
form.parse(req);

});

var server = app.listen(3000, function(){
console.log('Server listening on port 3000');
});