使用 async 命令同步文件。实现备份或移动文件。
开发
dockerfile
安装 async 命令
FROM node:20.9.0-alpine AS base
# 添加源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
...
RUN apk add --no-cache rsync
explorer-manage
使用 node 的 spawn 模块执行 rsync 命令。
import { spawn } from 'child_process'
import { formatPath } from '../format-path.mjs'
/**
*
* @param {string} source
* @param {string} destination
* @param {string[]} opt
* @param {boolean} test
* @returns {ChildProcessWithoutNullStreams}
*/
export const rsync = (source, destination, opt = [], test = false) => {
if (test) {
opt.push('-n')
}
console.log('rsync', [...opt, source, destination].join(' '))
return spawn('rsync', [...opt, source, destination])
}
/**
*
* @param {string} source
* @param {string} destination
* @param {boolean} has_delete_source
* @param {boolean} test
* @returns {ChildProcessWithoutNullStreams}
*/
export const rsyncMovePath = (source, destination, has_delete_source = false, test = false) => {
const opt = ['-a', '--progress']
has_delete_source && opt.push('--remove-source-files')
return rsync(formatPath(source), formatPath(destination), opt, test)
}
新增两个方法,rsync 与 rsyncMovePath 方法
- rsync:为简单的命令行封装
- rsyncMovePath:添加一个当同步完成后,删除原目录的参数 --remove-source-files。
explorer
创建一个流式数据 route api 接口
import { NextRequest, NextResponse } from 'next/server'
import { rsyncMovePath } from '@/explorer-manager/src/rsync/index.mjs'
export const POST = async (req: NextRequest) => {
const { path, out_path, rsync_delete_source } = await req.json()
try {
const stream = rsyncMovePath(path, out_path, rsync_delete_source)
return new NextResponse(stream.stdout, {
headers: {
'Content-Type': 'application/octet-stream',
},
})
} catch (e: any) {
return NextResponse.json({ err_msg: e.message }, { status: 500 })
}
}
客户端部分在原”移动“的弹窗上新增一个移动方式的 Select 菜单。提供两个选项
- 移动
- rsync 同步。当选择 rsync 同步时,出现一个”删除源文件“的开关
<Form.Item<FieldType> label="移动方式" name="move_type" rules={[{ required: true, message: '请选择移动方式' }]}>
<Select
options={[
{ label: '移动', value: 'move' },
{ label: 'rsync 同步', value: 'rsync' },
]}
/>
</Form.Item>
提交表单时,判断移动方式类型,选择不同的两种移动方式。
有时候目录文件比较大,rsync 同步使用流式回传当前同步状态信息。
...
import { moveAction } from '@/components/move-modal/action'
const MoveModal: React.FC = () => {
const move_path = useMovePathStore()
const changeMovePath = useMovePathDispatch()
const [chunk, changeChunk] = useState<string[]>([])
const { update } = useUpdateReaddirList()
const { message } = App.useApp()
const rsyncMove = useRequest(
async (values: FieldType) => {
const { path, new_path, last, rsync_delete_source } = values
const res = await fetch('/path/api/rsync-move', {
method: 'post',
body: JSON.stringify({
path: path,
out_path: [new_path, last].join('/'),
rsync_delete_source: rsync_delete_source,
}),
})
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)
!isEmpty(decode_value) && changeChunk((chunk) => chunk.concat(decode_value).reverse())
if (done) {
break
}
}
}
return Promise.resolve().then(update)
},
{
manual: true,
},
)
return (
<Modal
title="移动"
open={!isEmpty(move_path)}
width={1000}
onCancel={() => changeMovePath('')}
footer={false}
destroyOnClose={true}
>
<MoveForm
onFinish={(values) => {
const { move_type, path, new_path, last } = values
const thenAction = () => {
changeMovePath('')
update()
message.success('移动成功').then()
}
if (move_type === 'move') {
return moveAction(path, [new_path, last].join('/')).then(thenAction)
} else if (move_type === 'rsync') {
rsyncMove.run(values)
}
}}
/>
{!isEmpty(chunk) && (
<Card>
<pre>{chunk.join('')}</pre>
</Card>
)}
</Modal>
)
}
export default MoveModal
效果
截屏2023-12-22 20.34.11.png