深入聊聊Node中的File模块

广告:宝塔服务器面板,一键全能部署及管理,送你10850元礼包,点我领取~~~

深入聊聊Node中的File模块

在聊 Stream/Buffer 的时候,我们已经开始使用require("fs")引入文件模块做一些操作了

文件模块是对底层文件操作的封装,例如文件读写/打开关闭/删除添加等等

文件模块最大的特点就是所有的方法都提供的同步异步两个版本,具有 sync 后缀的方法都是同步方法,没有的都是异步方法

文件常识文件权限

因为需要对文件进行操作,所以需要设置对应的权限。【相关教程推荐:nodeJS视频教程、编程教学】

主要分为三种角色,文件所有者、文件所属组、其他用户

文件权限分为读、写、执行,分别于数字表示为4/2/1,没有权限的时候表示为0

如果取消了执行权限指,文件夹内任何文件都无法访问,也无法 cd 到文件夹

使用 Linux 命令ll能够查看目录中文件/文件夹的权限

第一位 d 代表文件夹,- 表示文件,后面就是文件的权限 // TODO: @表示什么

文件标识

在 Node 中,标识位代表着对文件的操作方式,可读/可写/即可读又可写等等,可以进行排列组合

文件描述符

在之前的内容中讲过,操作系统会为每个打开的文件分配一个叫做文件描述符的数值标识,使用这些数值来追踪特定的文件。

文件描述符一般从3开始,0/1/2分别代表标准输入/标准输出/错误输出

常用 API

一些实践过滤项目中适当的文件
const fs = require("fs");const path = require("path");const { promisify } = require("util");const reg = new RegExp("(.ts[x]*|.js[x]*|.json)$");const targetPath = path.resolve(__dirname, "../mini-proxy-mobx");const readDir = (targetPath, callback) => {    fs.readdir(targetPath, (err, files) => {        if (err) callback(err);        files.forEach(async (file) => {            const filePath = path.resolve(__dirname, `${targetPath}/${file}`);            const stats = await promisify(fs.stat)(filePath);            if (stats.isDirectory()) {                await readDir(filePath);            } else {                checkFile(filePath);            }        });    });};const checkFile = (file) => {    if (reg.test(file)) {        console.log(file);    }};readDir(targetPath, (err) => {    throw err;});
登录后复制文件拷贝

问题:需要将文件1中的内容拷贝到文件2中

文件API

可以使用 fs.readFile 把文件内容读取完成,再采用 fs.writeFile 写入新的文件

const fs = require("fs");const path = require("path");const sourceFile = path.resolve(__dirname, "../doc/Mobx原理及丐版实现.md");const targetFile = path.resolve(__dirname, "target.txt");fs.readFile(sourceFile, (err, data) => {    if (err) throw err;    const dataStr = data.toString();    fs.writeFile(targetFile, dataStr, (err) => {        if (err) throw err;        console.log("copy success~");        process.exit(1);    });});
登录后复制

? 这样是否存在问题,我们在 Stream 讲过,需要一点一点来,否则在大文件时内存吃不消。

Buffer 使用

使用 fs.open 方法打开文件,获得文件描述符,再调用 fs.read/fs.write 方法往特定的位置读写一定量的数据

const copyFile = (source, target, size, callback) => {    const sourceFile = path.resolve(__dirname, source);    const targetFile = path.resolve(__dirname, target);    const buf = Buffer.alloc(size);    let hasRead = 0; // 下次读取文件的位置    let hasWrite = 0; // 下次写入文件的位置    fs.open(sourceFile, "r", (err, sourceFd) => {        if (err) callback(err);        fs.open(targetFile, "w", (err, targetFd) => {            if (err) throw callback(err);            function next() {                fs.read(sourceFd, buf, 0, size, hasRead, (err, bytesRead) => {                    if (err) callback(err);                    hasRead += bytesRead;                    if (bytesRead) {                        fs.write(targetFd, buf, 0, size, hasWrite, (err, bytesWrite) => {                            if (err) callback(err);                            hasWrite += bytesWrite;                            next();                        });                        return;                    }                    fs.close(sourceFd, () => { console.log("关闭源文件"); });                    fs.close(targetFd, () => { console.log("关闭目标文件"); });                });            }            next();        });    });};
登录后复制Stream 使用
const fs = require("fs");const path = require("path");const readStream = fs.createReadStream(    path.resolve(__dirname, "../doc/Mobx原理及丐版实现.md"));const writeStream = fs.createWriteStream(path.resolve("target.txt"));readStream.pipe(writeStream);
登录后复制文件上传小文件上传
// 上传后资源的URL地址const RESOURCE_URL = `http://localhost:${PORT}`;// 存储上传文件的目录const UPLOAD_DIR = path.join(__dirname, "../public");const storage = multer.diskStorage({    destination: async function (req, file, cb) {        // 设置文件的存储目录        cb(null, UPLOAD_DIR);    },    filename: function (req, file, cb) {        // 设置文件名        cb(null, `${file.originalname}`);    },});const multerUpload = multer({ storage });router.post(    "/uploadSingle",    async (ctx, next) => {        try {            await next();            ctx.body = {                code: 1,                msg: "文件上传成功",                url: `${RESOURCE_URL}/${ctx.file.originalname}`,            };        } catch (error) {            console.log(error);            ctx.body = {                code: 0,                msg: "文件上传失败",            };        }    },    multerUpload.single("file"));
登录后复制大文件上传

主要步骤

前端接收大文件,并进行切片处理将每份切片进行上传处理后端接收到所有的切片,存储所有切片到一个文件夹中将文件夹中的切片做合并,并对切片做删除再次上传统一文件时,能够快速上传

具体实现

前端切片

const BIG_FILE_SIZE = 25 * 1024 * 1024;const SLICE_FILE_SIZE = 5 * 1024 * 1024;const uploadFile = async () => {    if (!fileList?.length) return alert("请选择文件");    const file = fileList[0];    const shouldUpload = await verifyUpload(file.name);    if (!shouldUpload) return message.success("文件已存在,上传成功");    if (file.size > BIG_FILE_SIZE) {        // big handle        getSliceList(file);    }    // // normal handle    // upload("/uploadSingle", file);};const getSliceList = (file: RcFile) => {    const sliceList: ISlice[] = [];    let curSize = 0;    let index = 0;    while (curSize < file.size) {        sliceList.push({            id: shortid.generate(),            slice: new File(                [file.slice(curSize, (curSize += SLICE_FILE_SIZE))],                `${file.name}-${index}`            ),            name: file.name,            sliceName: `${file.name}-${index}`,            progress: 0,        });        index++;    }    uploadSlice(sliceList);    setSliceList(sliceList);};
登录后复制

file 是一种特殊的 Blob 对象,可以使用 slice 进行大文件分割

上传切片

const uploadSlice = async (sliceList: ISlice[]) => {  const requestList = sliceList      .map(({ slice, sliceName, name }: ISlice, index: number) => {          const formData = new FormData();          formData.append("slice", slice);          formData.append("sliceName", sliceName);          formData.append("name", name);          return { formData, index, sliceName };      })      .map(({ formData }: { formData: FormData }, index: number) =>          request.post("/uploadBig", formData, {              onUploadProgress: (progressEvent: AxiosProgressEvent) =>                  sliceUploadProgress(progressEvent, index),          })      );  await Promise.all(requestList);};
登录后复制

根据切片构建每个切片的 formData,将二进制数据放在 slice 参数中,分别发送请求。

onUploadProgress 来处理每个切片的上传进度

// Clientconst storage = multer.diskStorage({  destination: async function (req, file, cb) {      const name = file?.originalname.split(".")?.[0];      const SLICE_DIR = path.join(UPLOAD_DIR, `${name}-slice`);      if (!fs.existsSync(SLICE_DIR)) {          await fs.mkdirSync(SLICE_DIR);      }      // 设置文件的存储目录      cb(null, SLICE_DIR);  },  filename: async function (req, file, cb) {      // 设置文件名      cb(null, `${file?.originalname}`);  },});// Serverrouter.post(    "/uploadBig",    async (ctx, next) => {        try {            await next();            const slice = ctx.files.slice[0]; // 切片文件            ctx.body = {                code: 1,                msg: "文件上传成功",                url: `${RESOURCE_URL}/${slice.originalname}`,            };        } catch (error) {            ctx.body = {                code: 0,                msg: "文件上传失败",            };        }    },    multerUpload.fields([{ name: "slice" }]));
登录后复制

切片合并

当我们所有的切片上传成功之后,我们依旧希望是按着原始文件作为保存的,所以需要对切片进行合并操作

// Clientconst uploadSlice = async (sliceList: ISlice[]) => {// ...和上述 uploadSlice 一致mergeSlice();};const mergeSlice = () => {    request.post("/mergeSlice", {        size: SLICE_FILE_SIZE,        name: fileList[0].name,    });};// Serverrouter.post("/mergeSlice", async (ctx, next) => {    try {        await next();        const { size, name } = ctx.request.body ?? {};        const sliceName = name.split(".")?.[0];        const filePath = path.join(UPLOAD_DIR, name);        const slice_dir = path.join(UPLOAD_DIR, `${sliceName}-slice`);        await mergeSlice(filePath, slice_dir, size);        ctx.body = {            code: 1,            msg: "文件合并成功",        };    } catch (error) {        ctx.body = {            code: 0,            msg: "文件合并失败",        };    }});// 通过 stream 来读写数据,将 slice 中数据读取到文件中const pipeStream = (path, writeStream) => {    return new Promise((resolve) => {        const readStream = fs.createReadStream(path);        readStream.on("end", () => {            fs.unlinkSync(path);   // 读取完成之后,删除切片文件            resolve();        });        readStream.pipe(writeStream);    });};const mergeSlice = async (filePath, sliceDir, size) => {    if (!fs.existsSync(sliceDir)) {        throw new Error("当前文件不存在");    }    const slices = await fs.readdirSync(sliceDir);    slices.sort((a, b) => a.split("-")[1] - b.split("-")[1]);    try {        const slicesPipe = slices.map((sliceName, index) => {            return pipeStream(                path.resolve(sliceDir, sliceName),                fs.createWriteStream(filePath, { start: index * size })            );        });        await Promise.all(slicesPipe);        await fs.rmdirSync(sliceDir);  // 读取完成之后,删除切片文件夹    } catch (error) {        console.log(error);    }};
登录后复制

上传文件校验

当我们上传一个文件的时候,先去判断在服务器上是否存在该文件,如果存在则直接不做上传操作,否则按上述逻辑进行上传

// Clientconst verifyUpload = async (name: string) => {    const res = await request.post("/verify", { name });    return res?.data?.data;};const uploadFile = async () => {    if (!fileList?.length) return alert("请选择文件");    const file = fileList[0];    const shouldUpload = await verifyUpload(file.name);    if (!shouldUpload) return message.success("文件已存在,上传成功");    if (file.size > BIG_FILE_SIZE) {        // big handle        getSliceList(file);    }    // // normal handle    // upload("/uploadSingle", file);};// Serverrouter.post("/verify", async (ctx, next) => {    try {        await next();        const { name } = ctx.request.body ?? {};        const filePath = path.resolve(UPLOAD_DIR, name);        if (fs.existsSync(filePath)) {            ctx.body = {                code: 1,                data: false,            };        } else {            ctx.body = {                code: 1,                data: true,            };        }    } catch (error) {        ctx.body = {            code: 0,            msg: "检测失败",        };    }});
登录后复制

上述直接使用文件名来做判断,过于绝对,对文件做了相关修改并不更改名字,就会出现问题。更应该采用的方案是根据文件相关的元数据计算出它的 hash 值来做判断。

const fs = require("fs");const path = require("path");const sourceFile = path.resolve(__dirname, "../doc/Mobx原理及丐版实现.md");const targetFile = path.resolve(__dirname, "target.txt");fs.readFile(sourceFile, (err, data) => {    if (err) throw err;    const dataStr = data.toString();    fs.writeFile(targetFile, dataStr, (err) => {        if (err) throw err;        console.log("copy success~");        process.exit(1);    });});0
登录后复制

本文所有的代码可以github上查看

总结

本文从文件常识/常用的文件 API 入手,重点讲解了 Node 中 File 的相关实践,最后使用相关内容实现了大文件上传。

更多node相关知识,请访问:nodejs 教程!

以上就是深入聊聊Node中的File模块的详细内容,更多请关注9543建站博客其它相关文章!

9543建站博客
一个专注于网站开发、微信开发的技术类纯净博客。

标签: JS

作者头像
admin创始人

肥猫,知名SEO博客站长,14年SEO经验。

上一篇:如何用html给图片加边框
下一篇:取消Uniapp微信授权

发表评论

关闭广告
关闭广告