mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 07:40:45 -08:00
feat: export folder as zip file
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
|||||||
IconShare,
|
IconShare,
|
||||||
IconShareOff,
|
IconShareOff,
|
||||||
IconTrashFilled,
|
IconTrashFilled,
|
||||||
|
IconZip,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import ViewFilesModal from '../ViewFilesModal';
|
import ViewFilesModal from '../ViewFilesModal';
|
||||||
import EditFolderNameModal from '../EditFolderNameModal';
|
import EditFolderNameModal from '../EditFolderNameModal';
|
||||||
@@ -169,6 +170,14 @@ export default function FolderTableView() {
|
|||||||
<IconPencil size='1rem' />
|
<IconPencil size='1rem' />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip label='Export folder as ZIP'>
|
||||||
|
<ActionIcon
|
||||||
|
color='blue'
|
||||||
|
onClick={() => window.open(`/api/user/folders/${folder.id}/export`, '_blank')}
|
||||||
|
>
|
||||||
|
<IconZip size='1rem' />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip label='Delete Folder'>
|
<Tooltip label='Delete Folder'>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
color='red'
|
color='red'
|
||||||
|
|||||||
68
src/server/routes/api/user/folders/[id]/export.ts
Normal file
68
src/server/routes/api/user/folders/[id]/export.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { datasource } from '@/lib/datasource';
|
||||||
|
import { prisma } from '@/lib/db';
|
||||||
|
import { log } from '@/lib/logger';
|
||||||
|
import { userMiddleware } from '@/server/middleware/user';
|
||||||
|
import archiver from 'archiver';
|
||||||
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
|
|
||||||
|
export type ApiUserFoldersIdExportResponse = null;
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const logger = log('api').c('user').c('folders').c('[id]').c('export');
|
||||||
|
|
||||||
|
export const PATH = '/api/user/folders/:id/export';
|
||||||
|
export default fastifyPlugin(
|
||||||
|
(server, _, done) => {
|
||||||
|
server.get<{ Params: Params }>(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const folder = await prisma.folder.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
files: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!folder) return res.notFound('Folder not found');
|
||||||
|
if (req.user.id !== folder.userId) return res.forbidden('You do not own this folder');
|
||||||
|
|
||||||
|
if (!folder.files.length) return res.badRequest("Can't export an empty folder.");
|
||||||
|
|
||||||
|
logger.info(`folder export requested: ${folder.name}`, { user: req.user.id, folder: folder.id });
|
||||||
|
|
||||||
|
res.hijack();
|
||||||
|
|
||||||
|
const zip = archiver('zip', {
|
||||||
|
zlib: { level: 9 },
|
||||||
|
});
|
||||||
|
|
||||||
|
zip.pipe(res.raw);
|
||||||
|
|
||||||
|
for (const file of folder.files) {
|
||||||
|
const stream = await datasource.get(file.name);
|
||||||
|
if (!stream) {
|
||||||
|
logger.warn('failed to get file stream for folder export', { file: file.id, folder: folder.id });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.append(stream, { name: file.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.on('error', (err) => {
|
||||||
|
logger.error('error during folder export zip creation', { folder: folder.id }).error(err as Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
zip.on('finish', () => {
|
||||||
|
logger.info(`folder export completed: ${folder.name}`, { user: req.user.id, folder: folder.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
await zip.finalize();
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
{ name: PATH },
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user