nodejs實現(xiàn)文件或文件夾上傳功能的代碼示例
背景
在平常的工作中,經(jīng)常會遇到需要將本地項目文件同步到遠端服務器的情況,所以每次遇到都需要考慮如何將文件上傳到服務器上。我使用的系統(tǒng)不一樣,就需要考慮使用不同的上傳方式。
雖然現(xiàn)在有很多文件上傳的工具,不過有些庫依賴系統(tǒng),如rsync或scp這種庫一般在linux環(huán)境中使用,有些庫需要依賴安裝客戶端和服務端,如ftp。作為一個開發(fā)人員,要有折騰的精神,所以我決定寫一個基于web的文件上傳工具,方便不同系統(tǒng)環(huán)境中可以輕松地實現(xiàn)文件上傳。
把這個工具封裝成一個npm庫,并通過執(zhí)行一個簡單的命令來實現(xiàn)將本地文件上傳到遠端服務器的功能。
http上傳文件實現(xiàn)原理
使用原生http庫是實現(xiàn)文件上傳最簡單直接的方式,大概分為以下幾個步驟:
- 創(chuàng)建http服務器,監(jiān)聽客戶端請求;
- 解析http請求,獲取上傳文件的相關信息,如文件名、文件大小等;
- 創(chuàng)建一個可寫流,將接收到的文件數(shù)據(jù)寫入到服務器的臨時文件中;
- 監(jiān)聽文件寫入完成事件,將臨時文件移動到指定的目錄,完成上傳;
本文主要使用兩種方式實現(xiàn)上傳,一種是multipart/form-data類型,這種方式支持攜帶文件元數(shù)據(jù)。另一種是application/octet-stream,只傳輸純粹的二進制數(shù)據(jù),不對數(shù)據(jù)進行處理,這兩種都可以實現(xiàn)文件上傳。
使用表單實現(xiàn)上傳
當我們使用POST方式的表單向服務器發(fā)送HTTP請求時,表單的參數(shù)會被寫入到HTTP請求的BODY中,根據(jù)不同的contentType,body中存放的數(shù)據(jù)格式也不相同。
本文主要使用了multipar
text/plain
這種請求體類型是最簡單的一種,傳輸純文本數(shù)據(jù),請求體中的數(shù)據(jù)以純文本形式進行編碼,沒有鍵值對的結構。適用于簡單的文本數(shù)據(jù)傳輸,如純文本文件或簡單的文本消息。如以下代碼,請求到服務器端時,請求體內(nèi)是一段文本。
fetch('http://127.0.0.1:3000/submit', {
headers: {
content-type: 'text/plain',
body: '我只是一段文本'
}
})application/x-www-form-urlencoded
這種請求體類型是最常見的,用于傳輸表單數(shù)據(jù)。請求體中的數(shù)據(jù)以鍵值對的形式進行編碼,每個鍵值對之間使用"&"符號分隔,鍵和值之間使用"="符號分隔。適用于傳輸簡單的表單數(shù)據(jù),如登錄表單、搜索表單等。
fetch('http://127.0.0.1:3000/submit', {
headers: {
content-type: 'application/x-www-form-urlencoded',
body: 'username=admin&password=123456'
}
})multipart/form-data
這種請求體類型用于上傳文件或傳輸復雜的表單數(shù)據(jù)。請求體中的數(shù)據(jù)以多個部分(part)的形式進行編碼,每個部分之間使用boundary進行分隔。每個部分由一個頭部和一個數(shù)據(jù)部分組成,頭部包含了部分的元數(shù)據(jù)信息,數(shù)據(jù)部分則包含了具體的數(shù)據(jù)。適用于上傳文件或包含文件的表單數(shù)據(jù)。
在發(fā)起請求時,content-Type中會創(chuàng)建一個隨機數(shù)字串作為內(nèi)容之間的分隔符,以下為一個請求頭示例:
Content-Type: multipart/form-data; boundary=--------------------------585592033508456553585598
請求體內(nèi)容類似以下方式:
----------------------585592033508456553585598 Conent-Disposition: form-data; name="username" admin ----------------------585592033508456553585598 Conent-Disposition: form-data; name="password" 123456 ----------------------585592033508456553585598 Conent-Disposition: form-data; name=""; filename="hw.txt" Content-type: text/plain hello world! ----------------------585592033508456553585598 Conent-Disposition: form-data; name=""; filename="avatar.png" Content-type: image/png ?PNG [圖片數(shù)據(jù)]
每個片段都是以Content-Type中的隨機串做分隔,每個片段都可以指定自己不同的文件類型。 比如上面的示例中就包含了文本文件,圖片,表單字段。
使用數(shù)據(jù)流上傳
發(fā)送請求時,將Content-Type設置成”application/octet-stream“,這是一種通用的二進制數(shù)據(jù)傳輸格式,它不對數(shù)據(jù)進行特殊處理或解析。通常用于傳輸不可見字符、二進制文件或未知類型的數(shù)據(jù)。進行傳輸時,數(shù)據(jù)會被視為純粹的二進制流,沒有特定的結構或格式。這種方式不支持在請求體中攜帶文件的元數(shù)據(jù),僅僅是將文件內(nèi)容作為二進制數(shù)據(jù)傳輸。
實現(xiàn)功能
文件上傳npm庫,可以在服務器端使用nodejs快速啟動一個web服務,將本地文件上傳到服務器,可以跨平臺使用。命令行參數(shù)格式與 scp 上傳文件的方式一樣。不過未實現(xiàn)將服務器文件同步到本地的能力。。。
啟動web服務器
使用以下命令快速創(chuàng)建一個web服務, ip 監(jiān)聽的地址默認為'0.0.0.0', port 監(jiān)聽的端口號,默認為3000.
dwsync server [ip] [port]
上傳文件
使用以下命令上傳文件到服務器。 -t : 設置連接服務器的類型, http | ftp ,默認值: http ; 本地路徑 : 要上傳的本地文件路徑; 用戶名 : 連接ftp服務時需要; 域名 : 服務器地址; 端口 : 服務器端口; 服務器路徑 : 要保存的服務器路徑,連接服務器類型是http服務時,傳入服務器絕對路徑;連接服務器類型是ftp時,傳入為相對路徑;
dwsync [-t http|ftp] <本地路徑> 用戶名@域名:端口:<服務器路徑>
連接服務器類型為ftp時,執(zhí)行命令后,會顯示輸入密碼框: 如下圖所示:

實現(xiàn)方式
主要包含三部分內(nèi)容,在服務器端啟動一個web服務器,用來接收請求并處理。 在客戶端執(zhí)行命令請求指定服務器接口,將本地文件傳輸?shù)椒掌魃稀?如果服務器端有ftp服務器,那么可以直接在本地連接ftp服務器上傳文件。
http服務器
服務器端是運行在服務器上的一個web服務,這個web服務提供三個接口能力。
創(chuàng)建目錄
判斷當前服務器上是否存在當前目錄,如果不存在些創(chuàng)建目錄,并通知客戶端。創(chuàng)建成功或者目錄已存在,則發(fā)送響應”1“,如果創(chuàng)建目錄失敗,則返回”0“,并中止上傳。 以下為代碼實現(xiàn):
function handleCreateDir(req, res) {
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
try {
// 由于在請求時,設置了Content-Type為application/json,所以這里需要解析一下JSON格式。
const filepath = JSON.parse(body);
if (fs.existsSync(filepath)) {
logger.info(`Directory already exists: ${filepath}`);
res.end('1');
return;
}
fs.mkdir(filepath, (err) => {
if (err) {
logger.error('Error create dir:', err);
res.statusCode = 500;
res.end('0');
} else {
logger.info(`Directory created: ${filepath}`);
res.end('1');
}
});
} catch (error) {
logger.error('Error create dir:', error);
res.statusCode = 500;
res.end('0');
}
});
}上傳文件
需要兩個參數(shù),remoteFilePath用來告訴服務器我要保存到服務器的路徑。file是上傳的文件內(nèi)容。
上傳文件使用了第三方庫 formidable 中的IncomingForm類來解析傳入的請求并獲取上傳的文件。
啟動web服務時,會設置一個臨時目錄,用來保存上傳到服務器的文件,上傳完成后,會根據(jù)傳入的服務器路徑字段,將文件移動到對應服務器指定文件夾下。
具體實現(xiàn)代碼如下:
function handleFileUpload(req, res) {
const form = new IncomingForm({ allowEmptyFiles: true, minFileSize: 0 });
form.uploadDir = cacheDir;
form.parse(req, (err, fields, files) => {
let { remoteFilePath } = fields;
remoteFilePath =
remoteFilePath && remoteFilePath.length > 0
? remoteFilePath[0]
: remoteFilePath;
if (err) {
logger.error('Error parsing form:', err);
res.statusCode = 500;
res.end('Internal Server Error');
} else {
let uploadedFile;
if (files.file && Array.isArray(files.file)) {
uploadedFile = files.file[0];
} else {
logger.error('no file');
res.statusCode = 500;
res.end('Internal Server Error');
return;
}
fs.rename(uploadedFile.filepath, remoteFilePath, (err) => {
if (err) {
logger.error('Error renaming file:', err);
res.statusCode = 500;
res.end('Internal Server Error');
} else {
logger.info(`File saved: ${remoteFilePath}`);
res.end('Upload complete');
}
});
}
});
}上傳大文件
一般情況下,web服務對上傳的文件大小是有限制的,所以一些特別大的文件我們就需要做分片上傳,在這個工具中,對大文件的處理我使用了上傳二進制的方式。
上傳二進制數(shù)據(jù)時,會把文件信息封裝到header頭里,在header里設置了 fileSize是文件的總字節(jié)數(shù),startByte是本次上傳的起始下標,endByte是本次上傳的結束下標。 使用fs.createWriteStream創(chuàng)建一個追加的寫入流,將數(shù)據(jù)塊寫入指定的RemoteFilePath。監(jiān)聽請求對象上的數(shù)據(jù)事件并將接收到的數(shù)據(jù)寫入文件。 當請求數(shù)據(jù)獲取完畢后,判斷是否文件的總字節(jié)流與本次的結束下標是否一致,如果一致則發(fā)送文件上傳成功的響應,否則發(fā)送分片上傳成功響應。
代碼實現(xiàn):
function handleChunkFileUpload(req, res) {
const remoteFilePath = decodeURIComponent(req.headers['remote-file-path']);
const fileDir = path.dirname(remoteFilePath);
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir);
}
const fileSize = parseInt(req.headers['file-size']);
const startByte = parseInt(req.headers['start-byte']);
const endByte = parseInt(req.headers['end-byte']);
const writableStream = fs.createWriteStream(remoteFilePath, {
flags: 'a',
start: startByte,
});
req.on('data', (data) => {
writableStream.write(data);
});
req.on('end', () => {
writableStream.end();
if (endByte === fileSize - 1) {
logger.info(`File saved: ${remoteFilePath}`);
res.end('File uploaded successfully');
} else {
res.end('Chunk uploaded successfully');
}
});
}http上傳客戶端實現(xiàn)
http客戶端上傳使用第三方庫axios實現(xiàn)上傳功能,使用form-data庫創(chuàng)建表單對象。
要上傳文件到服務器,我們需要知道以下信息:
- 要上傳到的服務器地址
- 本地文件路徑
- 要上傳的服務器路徑
遍歷本地文件
首先我們使用 fs.statSync 方法檢查本地路徑是目錄還是文件。如果是一個文件,則請求上傳文件接口。如果是目錄則會遍歷文件夾內(nèi)所有的文件,遞歸調用upload方法處理。
// 遞歸上傳文件或文件夾
async upload(filePath, remoteFilePath) {
const stats = fs.statSync(filePath);
if (stats.isFile()) {
//...
} else if (stats.isDirectory()) {
//...
}
}當上傳的是文件并且文件大小超過200MB時,會使用大文件上傳接口通過上傳二進制數(shù)據(jù)流的方式分片上傳文件。如果文件小于200MB,則使用form-data方式上傳文件到服務器。
// 遞歸上傳文件或文件夾
async upload(filePath, remoteFilePath) {
const stats = fs.statSync(filePath);
if (stats.isFile()) {
if (stats.size > this.maxChunkSize) {
await this.uploadChunkFile(filePath, remoteFilePath, stats.size);
} else {
await this.uploadFile(filePath, remoteFilePath);
}
} else if (stats.isDirectory()) {
//...
}
}本地路徑是目錄時,首先會請求服務器端的 http://127.0.0.1:3000/mkdir 方法,確保當前傳入的 remoteFilePath 路徑已經(jīng)在服務器上創(chuàng)建。接口返回 1 之后,遍歷當前本地目錄,生成對應的服務器文件路徑,遞歸調用自身,繼續(xù)下次遍歷。
try {
// 在服務器創(chuàng)建目錄
const response = await axios.post(
`${this.protocol}://${this.host}:${this.port}/mkdir`,
remoteFilePath,
{
headers: { 'Content-Type': 'application/json' },
}
);
// 創(chuàng)建成功
if (response.data === 1) {
// 讀取本地文件列表
const list = fs.readdirSync(filePath);
for (let index = 0; index < list.length; index++) {
const file = list[index];
const childFilePath = path.join(filePath, file);
// 拼接服務器路徑
const childRemoteFilePath = path.join(remoteFilePath, file);
// 遞歸調用
await this.upload(childFilePath, childRemoteFilePath);
}
} else {
console.error(`創(chuàng)建遠程文件夾失敗: ${remoteFilePath}`);
return;
}
} catch (error) {
console.error(`創(chuàng)建遠程文件夾失敗: ${remoteFilePath}`);
console.error(error.message);
return;
}文件上傳功能
文件上傳使用第三方庫 form-data 創(chuàng)建表單數(shù)據(jù)對象,并將服務器地址 remoteFilePath 當前參數(shù)傳遞到服務器。
async uploadFile(filePath, remoteFilePath) {
const formData = new FormData();
formData.append('remoteFilePath', remoteFilePath);
formData.append('file', fs.createReadStream(filePath));
const progressBar = new ProgressBar(
`Uploading ${filePath} [:bar] :percent`,
{
total: fs.statSync(filePath).size,
width: 40,
complete: '=',
incomplete: ' ',
}
);
const response = await axios.post(
`${this.protocol}://${this.host}:${this.port}/upload`,
formData,
{
headers: formData.getHeaders(),
onUploadProgress: (progressEvent) => {
progressBar.tick(progressEvent.loaded);
},
}
);
console.log(`Upload complete: ${response.data}`);
}大文件上傳
大文件上傳使用二進制數(shù)據(jù)流發(fā)送到服務器端,使用文件大小除以最大限制值獲取需要發(fā)起請求的數(shù)量,依次發(fā)送到服務器端。
const totalChunks = Math.ceil(fileSize / this.maxChunkSize);
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
await this.uploadChunk(filePath, remoteFilePath, fileSize, chunkIndex, progressBar);
}每次上傳發(fā)起請求都需要這四個字段: Remote-File-Path :服務器端保存路徑 File-Size :文件總字節(jié)大小 Start-Byte :本次請求起始字節(jié)下標 End-Byte :本次請求結束字節(jié)下標 參數(shù)通過自定義header提交到服務器端。
async uploadChunk(filePath, remoteFilePath, fileSize, chunkIndex, progressBar) {
// 計算本次上傳起始位置
const startByte = chunkIndex * this.maxChunkSize;
// 計算本次上傳結束位置,如果超過文件長度,則使用文件長度-1
const endByte = Math.min((chunkIndex + 1) * this.maxChunkSize - 1, fileSize - 1);
const headers = {
'Content-Type': 'application/octet-stream',
'Remote-File-Path': encodeURIComponent(remoteFilePath),
'File-Size': fileSize,
'Start-Byte': startByte,
'End-Byte': endByte,
};
// 創(chuàng)建二進制數(shù)據(jù)流
const chunkStream = fs.createReadStream(filePath, {
start: startByte,
end: endByte,
});
// 發(fā)起請求
await axios.post(
`${this.protocol}://${this.host}:${this.port}/uploadchunk`,
chunkStream,
{
headers,
onUploadProgress: (progressEvent) => {
const curr = startByte + progressEvent.loaded;
progressBar.tick(curr);
},
});
}使用 form-data 表單也可以實現(xiàn)大文件上傳, form-data 類型每個片段都可以自定類型。實現(xiàn)方式?jīng)]什么區(qū)別。
ftp客戶端實現(xiàn)
這個使用第三方庫 ftp 實現(xiàn)上傳到ftp服務器,ftp同樣使用遞歸遍歷本地上傳路徑的方式,依次將文件上傳到遠端服務器。
創(chuàng)建ftp連接
根據(jù)ftp服務器的地址及用戶名、密碼信息創(chuàng)建ftp連接。ftp連接服務器成功后,調用上傳方法。
// 創(chuàng)建FTP客戶端并連接到服務器
const client = new ftp();
client.connect({
host: this.host,
port: this.port,
user: this.user,
password: this.pwd,
});
client.on('ready', async () => {
console.log('已連接到FTP服務器...');
await this.uploadToFTP(this.localPath, this.remotePath, client);
// 斷開連接
client.end();
});
client.on('progress', (totalBytes, transferredBytes, progress) => {
console.log(`上傳進度: ${progress}%`);
});
client.on('error', (err) => {
console.error(`FTP連接錯誤: ${err}`);
});遞歸遍歷路徑
遞歸遍歷路徑,如果當前路徑是文件,調用上傳方法。如果當前路徑是目錄,則遞歸調用。
// 遞歸上傳文件或文件夾
async uploadToFTP(filePath, remoteFilePath, client) {
const stats = fs.statSync(filePath);
if (stats.isFile()) {
try {
await this.uploadFile(filePath, remoteFilePath, client);
console.log(`上傳文件成功: ${filePath}`);
} catch (error) {
console.error(`文件上傳失敗: ${filePath}`);
console.error(error);
}
} else if (stats.isDirectory()) {
try {
await this.createDir(remoteFilePath, client);
} catch (error) {
console.error(`創(chuàng)建遠程文件夾失敗: ${remoteFilePath}`);
console.error(error);
return;
}
const list = fs.readdirSync(filePath);
for (let index = 0; index < list.length; index++) {
const file = list[index];
const childFilePath = path.join(filePath, file);
const childRemoteFilePath = path.join(remoteFilePath, file);
await this.uploadToFTP(childFilePath, childRemoteFilePath, client);
}
}
}上傳文件
這個上傳很簡單,調用封裝好的put方法,將文件發(fā)送到ftp服務器。
uploadFile(filePath, remoteFilePath, client) {
return new Promise((resolve, reject) => {
client.put(filePath, remoteFilePath, (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}上傳進度展示
上傳進度使用第三方庫 progress 在命令行界面顯示上傳進度及文件,可以方便的跟蹤上傳情況。使用axios發(fā)送http請求時,可以使用 onUploadProgress 方法獲取進度并使用 progress 展示出來。 使用如下代碼創(chuàng)建一個進度實例對象,第一個參數(shù)是輸出的結構,第二個參數(shù)是參數(shù)配置。
const progressBar = new ProgressBar(
`Uploading ${filePath} [:bar] :percent`,
{
total: fs.statSync(filePath).size,
width: 40,
complete: '=',
incomplete: ' ',
}
);在axios中展示使用:
// 發(fā)起請求
await axios.post(
'http://127.0.0.1/uploadchunk',
chunkStream,
{
headers,
onUploadProgress: (progressEvent) => {
progressBar.tick(progressEvent.loaded);
},
});
}結語
本文基本上實現(xiàn)了一個跨平臺的命令行文件上傳工具的功能,只需要使用命令即可實現(xiàn)本地文件上傳至服務器。
以上就是nodejs實現(xiàn)文件或文件夾上傳功能的代碼示例的詳細內(nèi)容,更多關于nodejs實現(xiàn)文件或文件夾上傳的資料請關注腳本之家其它相關文章!
相關文章
Node.js + express實現(xiàn)上傳大文件的方法分析【圖片、文本文件】
這篇文章主要介紹了Node.js + express實現(xiàn)上傳大文件的方法,結合實例形式分析了Node.js + express針對圖片、文本文件上傳操作實現(xiàn)方法及相關操作注意事項,需要的朋友可以參考下2019-03-03
node.js中實現(xiàn)同步操作的3種實現(xiàn)方法
這篇文章主要介紹了node.js中實現(xiàn)同步操作的3種實現(xiàn)方法,本文用實例講解一些需要同步操作的情況下,如何編程實現(xiàn),需要的朋友可以參考下2014-12-12
node.js中的http.response.end方法使用說明
這篇文章主要介紹了node.js中的http.response.end方法使用說明,本文介紹了http.response.end的方法說明、語法、接收參數(shù)、使用實例和實現(xiàn)源碼,需要的朋友可以參考下2014-12-12

