深入聊聊Node中的File模块

青灯夜游

青灯夜游

2023-04-24

2058人浏览

转载

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

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

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

文件常识

文件权限

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

Untitled.png

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

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

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

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

Untitled 1.png

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

文件标识

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

Untitled 2.png

文件描述符

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

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

常用 API

Untitled 3.png

Untitled 4.png

Untitled 5.png

一些实践

过滤项目中适当的文件

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 写入新的文件

面向设计的AXUI前端框架表单
面向设计的AXUI前端框架表单

面向设计的AXUI前端框架表单是一款包含表格、列表、弹窗等的AXUI前端框架表单下载。

下载
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")
);

大文件上传

Untitled 6.png

主要步骤

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

具体实现

  1. 前端切片

    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 <p>file 是一种特殊的 <a href="https://www.php.cn/link/49f85a9ed090b20c8bed85a5923c669f" target="_blank" title="https://zhuanlan.zhihu.com/p/161000123" ref="nofollow noopener noreferrer">Blob 对象</a>,可以使用 slice 进行大文件分割</p><p><img src="https://img.php.cn/upload/article/000/000/024/cd7593e00297c761390c5eeded4dbb07-7.png?x-oss-process=image/resize,p_40" alt="Untitled 7.png" loading="lazy"></p>
  2. 上传切片

    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 来处理每个切片的上传进度

    // Client
    const 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}`);
      },
    });
    
    // Server
    router.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" }])
    );
  3. 切片合并

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

    // Client
    const uploadSlice = async (sliceList: ISlice[]) => {
    		// ...和上述 uploadSlice 一致
    		mergeSlice();
    };
    
    const mergeSlice = () => {
        request.post("/mergeSlice", {
            size: SLICE_FILE_SIZE,
            name: fileList[0].name,
        });
    };
    
    // Server
    router.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);
        }
    };
  4. 上传文件校验

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

    // Client
    const 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);
    };
    
    // Server
    router.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 calculateMD5 = (file: any) => new Promise((resolve, reject) => {
        const chunkSize = SLICE_FILE_SIZE
        const fileReader = new FileReader();
        const spark = new SparkMD5.ArrayBuffer();
        let cursor = 0;
        fileReader.onerror = () => {
            reject(new Error('Error reading file'));
        };
        fileReader.onload = (e: any) => {
            spark.append(e.target.result);
            cursor += e.target.result.byteLength;
            if (cursor  {
            const fileSlice = file.slice(cursor, cursor + chunkSize);
            fileReader.readAsArrayBuffer(fileSlice);
        }
        loadNext();
    });

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

总结

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

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

相关文章

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

前端 node.js

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
火山引擎API Key获取教程
火山引擎API Key获取教程

火山引擎API Key适合需要调用火山引擎云服务、AI模型、火山方舟接口或其他开放能力的开发者参考。本专题整理控制台入口、账号认证、服务开通、API Key创建、密钥复制保存、权限检查、调用测试和Key无效等常见问题排查。

2026.08.04

1

10

火山引擎API接入教程
火山引擎API接入教程

火山引擎API接入适合需要在应用、脚本、后台服务或AI工具中调用火山引擎能力的开发者参考。本专题整理控制台入口、服务开通、API Key获取、接口地址配置、请求参数填写、调用测试、权限设置、额度查询和常见接口报错排查。

2026.08.04

3

10

火山引擎DeepSeek API调用教程
火山引擎DeepSeek API调用教程

火山引擎DeepSeek API适合需要在应用、脚本、智能体或AI编程工具中调用DeepSeek模型的开发者参考。本专题整理火山引擎控制台入口、模型服务开通、API Key获取、Base URL配置、模型名称填写、调用测试、额度查询和常见接口报错排查。

2026.08.04

2

10

火山引擎控制台操作教程
火山引擎控制台操作教程

火山引擎控制台中常用功能包括API密钥管理、模型调用配置、云资源查看、账单明细、用量统计和权限分配。本专题整理控制台基础操作、服务开通流程、Key创建与保存、费用消耗查看、子账号权限设置和调用失败排查,方便开发者完成日常管理。

2026.08.04

3

10

PDF与PPT格式转换操作方法及在线转换技巧
PDF与PPT格式转换操作方法及在线转换技巧

本专题聚焦 PDF 与 PPT 文件格式转换需求,整理 PDF 转 PPT 在线转换方法、PPT 批量转换 PDF 操作步骤、转换后格式错乱处理以及文档版式检查技巧。通过详细教程帮助用户掌握 PDF、PPT 双向转换方法,解决演示文稿制作、文件整理和办公格式转换中的常见问题,提高办公效率。

2026.07.31

112

6

PDF合并文件操作方法与在线批量合并技巧
PDF合并文件操作方法与在线批量合并技巧

本专题聚焦 PDF 文件合并与文档整理需求,整理多个 PDF 合并成一个文件、图片批量转换 PDF、合同附件合并发送以及在线 PDF 合并操作方法等实用教程。通过详细步骤介绍 PDF 合并流程、文件顺序检查技巧和免费在线合并方案,帮助用户快速整理零散文档,提高办公文件处理效率。

2026.07.31

88

8

PDF转Word在线转换与文档编辑处理方法
PDF转Word在线转换与文档编辑处理方法

本专题聚焦 PDF 转 Word 文件转换与办公文档处理需求,整理 PDF 在线转换成 Word、PDF 转可编辑 Word、PDF 文件格式转换操作步骤以及转换后版式错乱、图片无法编辑等常见问题解决方法。通过详细教程帮助用户快速掌握 PDF 转 Word 技巧,提高办公文件处理效率。

2026.07.31

87

5

CodeIgniter下载教程
CodeIgniter下载教程

本合集由PHP中文网精心整理,为您提供CodeIgniter下载教程与官方正版下载安装指南。内容涵盖CI3/CI4官方获取渠道、Composer依赖安装及环境配置全流程。助您安全、高效地搭建轻量级PHP框架,轻松开启Web应用开发之旅。

2026.07.30

116

10

CodeIgniter数据库配置指南
CodeIgniter数据库配置指南

PHP中文网为您提供CodeIgniter数据库配置指南合集。本专题全面解析CI框架数据配置方法,涵盖配置文件修改、多环境连接、动态切换数据库及常见连接错误排查等实战技巧。内容详实易懂,助您轻松掌握CodeIgniter数据库配置,快速解决开发难题,提升项目构建效率。

2026.07.30

45

15

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
WebStorm 官方调试文档
WebStorm 官方调试文档

共0课时 | 0人学习

RabbitMQ 教程手册
RabbitMQ 教程手册

共0课时 | 0人学习

Node.js 教程
Node.js 教程

共57课时 | 23万人学习