解壓縮
這里使用較為常用的 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.
開發(fā)
預(yù)下載 mac 與 linux 版本的 7z 二進(jìn)制文件,放置于 explorer-manage/src/7zip/linux 與 /mac 目錄內(nèi)。可前往 7z 官方進(jìn)行下載,下載鏈接。
也可以使用 7zip-bin 這個依賴,內(nèi)部包含所有環(huán)境可運行的二進(jìn)制文件。由于項目是由鏡像進(jìn)行運行,使用全環(huán)境的包會加大鏡像的體積。所以這里單獨下載特定環(huán)境的下二進(jìn)制文件,可能版本會比較舊,最近更新為 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:查看當(dāng)前壓縮包內(nèi)容
explorer 客戶端展示
大致設(shè)計為彈窗模式,提供一個解壓縮位置,默認(rèn)當(dāng)前壓縮包位置。再提供一個密碼輸入欄,用于帶密碼的壓縮包解壓。
解壓縮一個超大包時,可能會超過 http 的請求超時時間,瀏覽器會主動關(guān)閉這次請求。導(dǎo)致壓縮包沒有解壓縮完畢,請求就已經(jīng)關(guān)閉了。雖然 node 還在后臺進(jìn)行解壓縮。但是客戶端無法知道是否解壓縮完畢。
可通過延長 http 的請求超時時間。也可使用 stream 逐步輸出內(nèi)容的方式避免超時,客戶端部分可以實時看到當(dāng)前解壓縮的進(jìn)度。類似像 AI 機(jī)器人提問時,文字逐字出現(xiàn)的效果。
查看壓縮包內(nèi)容
直接使用 server action 調(diào)用 node7zListAction 方法即可
解壓縮
使用 node-7z 的輸出流逐步輸出到瀏覽器
封裝一個 post api 接口。
- 監(jiān)聽 node-7z 返回的數(shù)據(jù)流
.on('data')事件。 - 對數(shù)據(jù)流做
encoder.encode(JSON.stringify(value) + ‘, ’)格式化操作。方便客戶端讀取數(shù)據(jù)流。 - 每秒往客戶端輸出一個時間戳避免請求超時
stream.push({ loading: Date.now() }) - 10 分鐘后關(guān)閉 2 的定時輸出,讓其自然超時。
- 客戶端通過 fetch 獲取數(shù)據(jù)流,具體可以看 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="壓縮包內(nèi)容">
{!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
測試用逐字輸出
每秒往客戶端輸出當(dāng)前時間。持續(xù) 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' },
})
}
效果

