Node.js 小知識(shí) — 實(shí)現(xiàn)圖片上傳寫入磁盤的接口
一:開(kāi)啟 Node.js 服務(wù)
開(kāi)啟一個(gè) Node.js 服務(wù),指定路由 /upload/image 收到請(qǐng)求后調(diào)用 uploadImageHandler 方法,傳入 Request 對(duì)象。
- const http = require('http');
- const formidable = require('formidable');
- const fs = require('fs');
- const fsPromises = fs.promises;
- const path = require('path');
- const PORT = process.env.PORT || 3000;
- const server = http.createServer(async (req, res) => {
- if (req.url === '/upload/image' && req.method.toLocaleLowerCase() === 'post') {
- uploadImageHandler(req, res);
- } else {
- res.setHeader('statusCode', 404);
- res.end('Not found!')
- }
- });
- server.listen(PORT, () => {
- console.log(`server is listening at ${server.address().port}`);
- });
二:處理圖片對(duì)象
formidable 是一個(gè)用來(lái)處理上傳文件、圖片等數(shù)據(jù)的 NPM 模塊,form.parse 是一個(gè) callback 轉(zhuǎn)化為 Promise 便于處理。
Tips:拼接路徑時(shí)使用 path 模塊的 join 方法,它會(huì)將我們傳入的多個(gè)路徑參數(shù)拼接起來(lái),因?yàn)?Linux、Windows 等不同的系統(tǒng)使用的符號(hào)是不同的,該方法會(huì)根據(jù)系統(tǒng)自行轉(zhuǎn)換處理。
- const uploadImageHandler = async (req, res) => {
- const form = new formidable.IncomingForm({ multiples: true });
- form.encoding = 'utf-8';
- form.maxFieldsSize = 1024 * 5;
- form.keepExtensions = true;
- try {
- const { file } = await new Promise((resolve, reject) => {
- form.parse(req, (err, fields, file) => {
- if (err) {
- return reject(err);
- }
- return resolve({ fields, file });
- });
- });
- const { name: filename, path: sourcePath } = file.img;
- const destPath = path.join(__dirname, filename);
- console.log(`sourcePath: ${sourcePath}. destPath: ${destPath}`);
- await mv(sourcePath, destPath);
- console.log(`File ${filename} write success.`);
- res.writeHead(200, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ code: 'SUCCESS', message: `Upload success.`}));
- } catch (err) {
- console.error(`Move file failed with message: ${err.message}`);
- res.writeHead(200, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ code: 'ERROR', message: `${err.message}`}));
- }
- }
三:實(shí)現(xiàn) mv 方法
fs.rename 重命名文件
將上傳的圖片寫入本地目標(biāo)路徑一種簡(jiǎn)單的方法是使用 fs 模塊的 rename(sourcePath, destPath) 方法,該方法會(huì)異步的對(duì) sourcePath 文件做重命名操作,使用如下所示:
- const mv = async (sourcePath, destPath) => {
- return fsPromises.rename(sourcePath, destPath);
- };
cross-device link not permitted
在使用 fs.rename() 時(shí)還要注意 cross-device link not permitted 錯(cuò)誤,參考 rename(2) — Linux manual page:
**EXDEV **oldpath and newpath are not on the same mounted filesystem. (Linux permits a filesystem to be mounted at multiple points, but rename() does not work across different mount points, even if the same filesystem is mounted on both.)
oldPath 和 newPath 不在同一掛載的文件系統(tǒng)上。(Linux 允許一個(gè)文件系統(tǒng)掛載到多個(gè)點(diǎn),但是 rename() 無(wú)法跨不同的掛載點(diǎn)進(jìn)行工作,即使相同的文件系統(tǒng)被掛載在兩個(gè)掛載點(diǎn)上。)
在 Windows 系統(tǒng)同樣會(huì)遇到此問(wèn)題,參考 http://errorco.de/win32/winerror-h/error_not_same_device/0x80070011/
winerror.h 0x80070011 #define ERROR_NOT_SAME_DEVICE The system cannot move the file to a different disk drive.(系統(tǒng)無(wú)法移動(dòng)文件到不同的磁盤驅(qū)動(dòng)器。)
此處在 Windows 做下復(fù)現(xiàn),因?yàn)樵谑褂?formidable 上傳文件時(shí)默認(rèn)的目錄是操作系統(tǒng)的默認(rèn)目錄 os.tmpdir(),在我的電腦上對(duì)應(yīng)的是 C 盤下,當(dāng)我使用 fs.rename() 將其重名為 F 盤時(shí),就出現(xiàn)了以下報(bào)錯(cuò):
- C:\Users\ADMINI~1\AppData\Local\Temp\upload_3cc33e9403930347b89ea47e4045b940 F:\study\test\202366
- [Error: EXDEV: cross-device link not permitted, rename 'C:\Users\ADMINI~1\AppData\Local\Temp\upload_3cc33e9403930347b89ea47e4045b940' -> 'F:\study\test\202366'] {
- errno: -4037,
- code: 'EXDEV',
- syscall: 'rename',
- path: 'C:\\Users\\ADMINI~1\\AppData\\Local\\Temp\\upload_3cc33e9403930347b89ea47e4045b940',
- dest: 'F:\\study\\test\\202366'
- }
設(shè)置源路徑與目標(biāo)路徑在同一磁盤分區(qū)
設(shè)置上傳文件中間件的臨時(shí)路徑為最終寫入文件的磁盤分區(qū),例如我們?cè)?Windows 測(cè)試時(shí)將圖片保存在 F 盤下,所以設(shè)置 formidable 的 form 對(duì)象的 uploadDir 屬性為 F 盤,如下所示:
- const form = new formidable.IncomingForm({ multiples: true });
- form.uploadDir = 'F:\\'
- form.parse(req, (err, fields, file) => {
- ...
- });
這種方式有一定局限性,如果寫入的位置位于不同的磁盤空間該怎么辦呢?
可以看下下面的這種方式。
讀取-寫入-刪除臨時(shí)文件
一種可行的辦法是讀取臨時(shí)文件寫入到新的位置,最后在刪除臨時(shí)文件。所以下述代碼創(chuàng)建了可讀流與可寫流對(duì)象,使用 pipe 以管道的方式將數(shù)據(jù)寫入新的位置,最后調(diào)用 fs 模塊的 unlink 方法刪除臨時(shí)文件。
- const mv = async (sourcePath, destPath) => {
- try {
- await fsPromises.rename(sourcePath, destPath);
- } catch (error) {
- if (error.code === 'EXDEV') {
- const readStream = fs.createReadStream(sourcePath);
- const writeStream = fs.createWriteStream(destPath);
- return new Promise((resolve, reject) => {
- readStream.pipe(writeStream);
- readStream.on('end', onClose);
- readStream.on('error', onError);
- async function onClose() {
- await fsPromises.unlink(sourcePath);
- resolve();
- }
- function onError(err) {
- console.error(`File write failed with message: ${err.message}`);
- writeStream.close();
- reject(err)
- }
- })
- }
- throw error;
- }
- }
四:測(cè)試
方式一:終端調(diào)用
- curl --location --request POST 'localhost:3000/upload/image' \
- --form 'img=@/Users/Downloads/五月君.jpeg'
方式二:POSTMAN 調(diào)用
Reference
- https://github.com/andrewrk/node-mv/blob/master/index.js
- https://stackoverflow.com/questions/43206198/what-does-the-exdev-cross-device-link-not-permitted-error-mean/43206506#43206506
- https://nodejs.org/api/fs.html#fs_fs_rename_oldpath_newpath_callback
本文轉(zhuǎn)載自微信公眾號(hào)「 Nodejs技術(shù)棧 」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系 Nodejs技術(shù)棧公眾號(hào)。