node文件资源管理器的解压缩从零实现

 更新时间:2023年12月21日 15:19:03   作者:寒露  
这篇文章主要为大家介绍了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) =&gt; {
    setTimeout(resolve, time)
  })
}

const encoder = new TextEncoder()

async function* makeIterator() {
  let length = 0
  while (length &gt; 60 * 10) {
    await sleep(1000)
    yield encoder.encode(`&lt;p&gt;${length} ${new Date().toLocaleString()}&lt;/p&gt;`)

    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

yangWs29/share-explorer

以上就是node文件资源管理器的解压缩从零实现的详细内容,更多关于node文件资源解压缩的资料请关注脚本之家其它相关文章!

相关文章

最新评论