目录
文件常识
文件权限
文件标识
文件描述符
常用 API
一些实践
过滤项目中适当的文件
文件拷贝
文件API
Buffer 使用
Stream 使用
文件上传
小文件上传" >小文件上传
大文件上传" >大文件上传
总结
首页 web前端 js教程 深入聊聊Node中的File模块

深入聊聊Node中的File模块

Apr 24, 2023 pm 05:49 PM
前端 node.js

深入聊聊Node中的File模块

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

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

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

文件常识

文件权限

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

深入聊聊Node中的File模块

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

文件权限分为读、写、执行,分别于数字表示为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 写入新的文件

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 < 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 进行大文件分割

    Untitled 7.png

  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(&#39;Error reading file&#39;));
        };
        fileReader.onload = (e: any) => {
            spark.append(e.target.result);
            cursor += e.target.result.byteLength;
            if (cursor < file.size) loadNext();
            else resolve(spark.end());
            
        };
        const loadNext = () => {
            const fileSlice = file.slice(cursor, cursor + chunkSize);
            fileReader.readAsArrayBuffer(fileSlice);
        }
        loadNext();
    });
    登录后复制

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

    总结

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

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

    以上是深入聊聊Node中的File模块的详细内容。更多信息请关注PHP中文网其他相关文章!

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

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

PHP与Vue:完美搭档的前端开发利器 PHP与Vue:完美搭档的前端开发利器 Mar 16, 2024 pm 12:09 PM

PHP与Vue:完美搭档的前端开发利器在当今互联网高速发展的时代,前端开发变得愈发重要。随着用户对网站和应用的体验要求越来越高,前端开发人员需要使用更加高效和灵活的工具来创建响应式和交互式的界面。PHP和Vue.js作为前端开发领域的两个重要技术,搭配起来可以称得上是完美的利器。本文将探讨PHP和Vue的结合,以及详细的代码示例,帮助读者更好地理解和应用这两

如何使用 Go 语言进行前端开发? 如何使用 Go 语言进行前端开发? Jun 10, 2023 pm 05:00 PM

随着互联网技术的发展,前端开发变得日益重要。尤其是移动端设备的普及,更需要高效、稳定、安全又易维护的前端开发技术。而作为一门快速发展的编程语言,Go语言已经被越来越多的开发者所使用。那么,使用Go语言进行前端开发行得通吗?接下来,本文将为你详细说明如何使用Go语言进行前端开发。先来看看为什么使用Go语言进行前端开发。很多人认为Go语言是一门

前端面试官常问的问题 前端面试官常问的问题 Mar 19, 2024 pm 02:24 PM

在前端开发面试中,常见问题涵盖广泛,包括HTML/CSS基础、JavaScript基础、框架和库、项目经验、算法和数据结构、性能优化、跨域请求、前端工程化、设计模式以及新技术和趋势。面试官的问题旨在评估候选人的技术技能、项目经验以及对行业趋势的理解。因此,应试者应充分准备这些方面,以展现自己的能力和专业知识。

Django是前端还是后端?一探究竟! Django是前端还是后端?一探究竟! Jan 19, 2024 am 08:37 AM

Django是一个Python编写的web应用框架,它强调快速开发和干净方法。尽管Django是一个web框架,但是要回答Django是前端还是后端这个问题,需要深入理解前后端的概念。前端是指用户直接和交互的界面,后端是指服务器端的程序,他们通过HTTP协议进行数据的交互。在前端和后端分离的情况下,前后端程序可以独立开发,分别实现业务逻辑和交互效果,数据的交

golang可以做前端吗 golang可以做前端吗 Jun 06, 2023 am 09:19 AM

golang可以做前端,Golang是一种通用性很强的编程语言,可以用于开发不同类型的应用程序,包括前端应用程序,通过使用Golang来编写前端,可以摆脱JavaScript等语言引起的一系列问题,例如类型安全性差、性能低下,以及代码难以维护等问题。

C#开发经验分享:前端与后端协同开发技巧 C#开发经验分享:前端与后端协同开发技巧 Nov 23, 2023 am 10:13 AM

作为一名C#开发者,我们的开发工作通常包括前端和后端的开发,而随着技术的发展和项目的复杂性提高,前端与后端协同开发也变得越来越重要和复杂。本文将分享一些前端与后端协同开发的技巧,以帮助C#开发者更高效地完成开发工作。确定好接口规范前后端的协同开发离不开API接口的交互。要保证前后端协同开发顺利进行,最重要的是定义好接口规范。接口规范涉及到接口的命

Go语言前端技术探秘:前端开发新视野 Go语言前端技术探秘:前端开发新视野 Mar 28, 2024 pm 01:06 PM

Go语言作为一种快速、高效的编程语言,在后端开发领域广受欢迎。然而,很少有人将Go语言与前端开发联系起来。事实上,使用Go语言进行前端开发不仅可以提高效率,还能为开发者带来全新的视野。本文将探讨使用Go语言进行前端开发的可能性,并提供具体的代码示例,帮助读者更好地了解这一领域。在传统的前端开发中,通常会使用JavaScript、HTML和CSS来构建用户界面

前端怎么实现即时通讯 前端怎么实现即时通讯 Oct 09, 2023 pm 02:47 PM

实现即时通讯的方法有WebSocket、Long Polling、Server-Sent Events、WebRTC等等。详细介绍:1、WebSocket,它可以在客户端和服务器之间建立持久连接,实现实时的双向通信,前端可以使用 WebSocket API来创建WebSocket连接,并通过发送和接收消息来实现即时通讯;2、Long Polling,是一种模拟实时通信的技术等等

See all articles