在聊 Stream/Buffer 的时候,我们已经开始使用require("fs")引入文件模块做一些操作了文件模块是对底层文件操作的封装,例如文件读写/打开关闭/删除添加等等文件模块最大的特点就是所有的方法都提供的同步
在聊 Stream/Buffer 的时候,我们已经开始使用require("fs")
引入文件模块做一些操作了
文件模块是对底层文件操作的封装,例如文件读写/打开关闭/删除添加等等
文件模块最大的特点就是所有的方法都提供的同步和异步两个版本,具有 sync 后缀的方法都是同步方法,没有的都是异步方法
因为需要对文件进行操作,所以需要设置对应的权限。【相关教程推荐:nodejs视频教程、编程教学】
主要分为三种角色,文件所有者、文件所属组、其他用户
文件权限分为读、写、执行,分别于数字表示为4/2/1,没有权限的时候表示为0
如果取消了执行权限指,文件夹内任何文件都无法访问,也无法 cd 到文件夹
使用 linux 命令ll
能够查看目录中文件/文件夹的权限
第一位 d 代表文件夹,- 表示文件,后面就是文件的权限 // TODO: @表示什么
在 node 中,标识位代表着对文件的操作方式,可读/可写/即可读又可写等等,可以进行排列组合
在之前的内容中讲过,操作系统会为每个打开的文件分配一个叫做文件描述符的数值标识,使用这些数值来追踪特定的文件。
文件描述符一般从3开始,0/1/2分别代表标准输入/标准输出/错误输出
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中
可以使用 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 讲过,需要一点一点来,否则在大文件时内存吃不消。
使用 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();
});
});
};
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 来处理每个切片的上传进度
// 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" }])
);
切片合并
当我们所有的切片上传成功之后,我们依旧希望是按着原始文件作为保存的,所以需要对切片进行合并操作
// 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);
}
};
上传文件校验
当我们上传一个文件的时候,先去判断在服务器上是否存在该文件,如果存在则直接不做上传操作,否则按上述逻辑进行上传
// 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 < 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中的File模块的详细内容,更多请关注编程网其它相关文章!
--结束END--
本文标题: 深入聊聊Node中的File模块
本文链接: https://www.lsjlt.com/news/207865.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
下载Word文档到电脑,方便收藏和打印~
2023-05-25
2023-05-25
2023-05-25
2023-05-25
2023-05-25
2023-05-24
2023-05-24
2023-05-24
2023-05-24
2023-05-24
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0