node文件资源管理器的解压缩从零实现
作者:寒露
解压缩
这里使用较为常用的 7z 来处理压缩包,它可以解开常见的压缩包格式
Unpacking only: APFS, AR, ARJ, CAB, CHM, CPIO, CramFS, DMG, EXT, FAT, GPT, HFS, IHEX, ISO, LZH, LZMA, MBR, MSI, NSIS, NTFS, QCOW2, RAR, RPM, SquashFS, UDF, UEFI, VDI, VHD, VHDX, VMDK, XAR and Z.
开发
预下载 mac 与 linux 版本的 7z 二进制文件,放置于 explorer-manage/src/7zip/linux 与 /mac 目录内。可前往 7z 官方进行下载,下载链接。
也可以使用 7zip-bin 这个依赖,内部包含所有环境可运行的二进制文件。由于项目是由镜像进行运行,使用全环境的包会加大镜像的体积。
所以这里单独下载特定环境的下二进制文件,可能版本会比较旧,最近更新为 2022/5/16 。目前最新的 2023/06/20@23.01 版本。
使用 node-7z 这个依赖处理 7z 的输入输出
安装依赖
pnpm i node-7z
运行文件
// https://laysent.com/til/2019-12-02_7zip-bin-in-alpine-docker // https://www.npmjs.com/package/node-7z // https://www.7-zip.org/download.html // import sevenBin from '7zip-bin' import node7z from 'node-7z' import { parseFilePath } from './parse-path.mjs' import path from 'path' import { dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { formatPath } from '../../lib/format-path.mjs' const __dirname = dirname(fileURLToPath(import.meta.url)) /** * @type {import('node-7z').SevenZipOptions} */ const base_option = { $bin: process.platform === 'darwin' ? path.join(__dirname, './mac/7zz') : path.join(__dirname, './linux/7zzs'), recursive: true, exclude: ['!__MACOSX/*', '!.DS_store'], latestTimeStamp: false, } /** * @param path {string} * @param out_path {string|undefined} * @param pwd {string | number | undefined} * @returns {import('node-7z').ZipStream} */ export const node7zaUnpackAction = (path, out_path = '', pwd = 'pwd') => { const join_path = formatPath(path) const { file_dir_path } = parseFilePath(join_path) return node7z.extractFull(join_path, formatPath(out_path) || `${file_dir_path}/`, { ...base_option, password: pwd, }) } /** * @param path {string} * @param pwd {string | number | undefined} * @returns {import('node-7z').ZipStream} */ export const node7zListAction = (path, pwd = 'pwd') => { const join_path = formatPath(path) return node7z.list(join_path, { ...base_option, password: pwd }) }
简单封装下 node7zaUnpackAction 与 node7zListAction 方法
- node7zaUnpackAction:解压缩方法
- node7zListAction:查看当前压缩包内容
explorer 客户端展示
大致设计为弹窗模式,提供一个解压缩位置,默认当前压缩包位置。再提供一个密码输入栏,用于带密码的压缩包解压。
解压缩一个超大包时,可能会超过 http 的请求超时时间,浏览器会主动关闭这次请求。导致压缩包没有解压缩完毕,请求就已经关闭了。虽然 node 还在后台进行解压缩。但是客户端无法知道是否解压缩完毕。
可通过延长 http 的请求超时时间。也可使用 stream 逐步输出内容的方式避免超时,客户端部分可以实时看到当前解压缩的进度。类似像 AI 机器人提问时,文字逐字出现的效果。
查看压缩包内容
直接使用 server action 调用 node7zListAction 方法即可
解压缩
使用 node-7z 的输出流逐步输出到浏览器
封装一个 post api 接口。
- 监听 node-7z 返回的数据流
.on('data')
事件。 - 对数据流做
encoder.encode(JSON.stringify(value) + ‘, ’)
格式化操作。方便客户端读取数据流。 - 每秒往客户端输出一个时间戳避免请求超时
stream.push({ loading: Date.now() })
- 10 分钟后关闭 2 的定时输出,让其自然超时。
- 客户端通过 fetch 获取数据流,具体可以看 unpack 方法
接口 api
import { NextRequest, NextResponse } from 'next/server' import { node7zaUnpackAction } from '@/explorer-manager/src/7zip/7zip.mjs' import { nodeStreamToIterator } from '@/explorer-manager/src/main.mjs' const encoder = new TextEncoder() const iteratorToStream = (iterator: AsyncGenerator) => { return new ReadableStream({ async pull(controller) { const { value, done } = await iterator.next() if (done) { controller.close() } else { controller.enqueue(encoder.encode(JSON.stringify(value) + ', ')) } }, }) } export const POST = async (req: NextRequest) => { const { path, out_path, pwd } = await req.json() try { const stream = node7zaUnpackAction(path, out_path, pwd) stream.on('data', (item) => { console.log('data', item.file) }) const interval = setInterval(() => { console.log('interval', stream.info) stream.push({ loading: Date.now() }) }, 1000) const timeout = setTimeout( () => { clearInterval(interval) }, 60 * 10 * 1000, ) stream.on('end', () => { console.log('end', stream.info) stream.push({ done: JSON.stringify(Object.fromEntries(stream.info), null, 2), }) clearTimeout(timeout) clearInterval(interval) stream.push(null) }) return new NextResponse(iteratorToStream(nodeStreamToIterator(stream)), { headers: { 'Content-Type': 'application/octet-stream', }, }) } catch (e) { return NextResponse.json({ ret: -1, err_msg: e }) } }
客户端弹窗组件
'use client' import React, { useState } from 'react' import { Card, Modal, Space, Table } from 'antd' import UnpackForm from '@/components/unpack-modal/unpack-form' import { isEmpty } from 'lodash' import { useRequest } from 'ahooks' import Bit from '@/components/bit' import DateFormat from '@/components/date-format' import { UnpackItemType } from '@/explorer-manager/src/7zip/types' import { useUnpackPathDispatch, useUnpackPathStore } from '@/components/unpack-modal/unpack-path-context' import { useUpdateReaddirList } from '@/app/path/readdir-context' import { unpackListAction } from '@/components/unpack-modal/action' let pack_list_path = '' const UnpackModal: React.FC = () => { const unpack_path = useUnpackPathStore() const changeUnpackPath = useUnpackPathDispatch() const [unpack_list, changeUnpackList] = useState<UnpackItemType['list']>([]) const { update } = useUpdateReaddirList() const packList = useRequest( async (form_val) => { pack_list_path = unpack_path const { pwd } = await form_val return unpackListAction(unpack_path, pwd) }, { manual: true, }, ) const unpack = useRequest( async (form_val) => { pack_list_path = unpack_path unpack_list.length = 0 const { out_path, pwd } = await form_val const res = await fetch('/path/api/unpack', { method: 'post', body: JSON.stringify({ path: unpack_path, out_path, pwd: pwd }), }) if (res.body) { const reader = res.body.getReader() const decode = new TextDecoder() while (1) { const { done, value } = await reader.read() const decode_value = decode .decode(value) .split(', ') .filter((text) => Boolean(String(text).trim())) .map((value) => { try { return value ? JSON.parse(value) : { value } } catch (e) { return { value } } }) .filter((item) => !item.loading) .reverse() !isEmpty(decode_value) && changeUnpackList((unpack_list) => decode_value.concat(unpack_list)) if (done) { break } } } return Promise.resolve().then(update) }, { manual: true, }, ) return ( <Modal title="解压缩" open={!isEmpty(unpack_path)} width={1000} onCancel={() => changeUnpackPath('')} footer={false} destroyOnClose={true} > <UnpackForm packList={packList} unpack={unpack} /> <Space direction="vertical" style={{ width: '100%' }}> {pack_list_path === unpack_path && !isEmpty(unpack_list) && ( <Card title="unpack" bodyStyle={{ maxHeight: '300px', overflowY: 'scroll', paddingTop: 20, overscrollBehavior: 'contain', }} > {unpack_list.map(({ file, done }) => ( <pre key={file || done}>{file || done}</pre> ))} </Card> )} {pack_list_path === unpack_path && !isEmpty(packList.data) && ( <Card title="压缩包内容"> {!isEmpty(packList.data?.data) && ( <Table scroll={{ x: true }} rowKey={({ file }) => file} columns={[ { key: 'file', dataIndex: 'file', title: 'file' }, { key: 'size', dataIndex: 'size', title: 'size', width: 100, render: (size) => { return <Bit>{size}</Bit> }, }, { key: 'sizeCompressed', dataIndex: 'sizeCompressed', title: 'sizeCompressed', width: 150, render: (size) => { return <Bit>{size}</Bit> }, }, { key: 'datetime', dataIndex: 'datetime', title: 'datetime', width: 180, render: (date) => <DateFormat>{new Date(date).getTime()}</DateFormat>, }, ]} dataSource={packList.data?.data} /> )} {packList.data?.message && <p>{packList.data?.message}</p>} </Card> )} </Space> </Modal> ) } export default UnpackModal
测试用逐字输出
每秒往客户端输出当前时间。持续 10 分钟。
import { iteratorToStream, nodeStreamToIterator } from '@/explorer-manager/src/main.mjs' function sleep(time: number) { return new Promise((resolve) => { setTimeout(resolve, time) }) } const encoder = new TextEncoder() async function* makeIterator() { let length = 0 while (length > 60 * 10) { await sleep(1000) yield encoder.encode(`<p>${length} ${new Date().toLocaleString()}</p>`) length += 1 } } export async function POST() { return new Response(iteratorToStream(nodeStreamToIterator(makeIterator())), { headers: { 'Content-Type': 'application/octet-stream' }, }) } export async function GET() { return new Response(iteratorToStream(nodeStreamToIterator(makeIterator())), { headers: { 'Content-Type': 'html' }, }) }
效果
git-repo
以上就是node文件资源管理器的解压缩从零实现的详细内容,更多关于node文件资源解压缩的资料请关注脚本之家其它相关文章!