feat: export folder as zip file

This commit is contained in:
diced
2025-11-14 23:48:50 -08:00
parent d9df04bac5
commit 1f1bcd3a47
3 changed files with 77 additions and 0 deletions

View File

@@ -16,6 +16,7 @@ import {
IconShare,
IconShareOff,
IconTrashFilled,
IconZip,
} from '@tabler/icons-react';
import ViewFilesModal from '../ViewFilesModal';
import EditFolderNameModal from '../EditFolderNameModal';
@@ -169,6 +170,14 @@ export default function FolderTableView() {
<IconPencil size='1rem' />
</ActionIcon>
</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'>
<ActionIcon
color='red'

View 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 },
);