fix: exports path

This commit is contained in:
diced
2024-09-12 16:17:04 -07:00
parent 641ec235d9
commit bd774b8ffb
3 changed files with 106 additions and 110 deletions

View File

@@ -142,6 +142,21 @@ model User {
tags Tag[]
oauthProviders OAuthProvider[]
IncompleteFile IncompleteFile[]
exports Export[]
}
model Export {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
completed Boolean @default(false)
path String
files Int
size String
User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
userId String
}
model UserQuota {

View File

@@ -1,8 +1,10 @@
import { Response } from '@/lib/api/response';
import { ActionIcon, Button, Paper, ScrollArea, Table, Title } from '@mantine/core';
import { bytes } from '@/lib/bytes';
import { ActionIcon, Button, Group, Paper, ScrollArea, Table, Title } from '@mantine/core';
import { modals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import { IconPlus, IconTrashFilled } from '@tabler/icons-react';
import { IconDownload, IconPlus, IconTrashFilled } from '@tabler/icons-react';
import Link from 'next/link';
import useSWR from 'swr';
export default function SettingsExports() {
@@ -35,13 +37,12 @@ export default function SettingsExports() {
});
};
const handleDelete = async (name: string) => {
await fetch(`/api/user/export?name=${name}`, {
const handleDelete = async (id: string) => {
await fetch(`/api/user/export?id=${id}`, {
method: 'DELETE',
});
showNotification({
title: 'Export deleted',
message: 'Export has been deleted',
color: 'red',
});
@@ -65,54 +66,42 @@ export default function SettingsExports() {
</Button>
<Title order={4} mt='sm'>
Completed Exports
Exports
</Title>
<ScrollArea.Autosize mah={500} type='auto'>
<Table highlightOnHover stickyHeader>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>ID</Table.Th>
<Table.Th>Started On</Table.Th>
<Table.Th>Files</Table.Th>
<Table.Th>Size</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{isLoading && <Table.Tr>Loading...</Table.Tr>}
{data?.complete.map((file) => (
<Table.Tr key={file.name}>
<Table.Td>{file.name}</Table.Td>
<Table.Td>{new Date(file.date).toLocaleString()}</Table.Td>
<Table.Td>{file.files}</Table.Td>
{data?.map((exportDb) => (
<Table.Tr key={exportDb.id}>
<Table.Td>{exportDb.id}</Table.Td>
<Table.Td>{new Date(exportDb.createdAt).toLocaleString()}</Table.Td>
<Table.Td>{exportDb.files}</Table.Td>
<Table.Td>{exportDb.completed ? bytes(Number(exportDb.size)) : ''}</Table.Td>
<Table.Td>
<ActionIcon onClick={() => handleDelete(file.name)}>
<Group>
<ActionIcon onClick={() => handleDelete(exportDb.id)}>
<IconTrashFilled size='1rem' />
</ActionIcon>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</ScrollArea.Autosize>
<Title order={4} mt='sm'>
Running Exports
</Title>
<ScrollArea.Autosize mah={500} type='auto'>
<Table highlightOnHover stickyHeader>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Started On</Table.Th>
<Table.Th>Files</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{isLoading && <Table.Tr>Loading...</Table.Tr>}
{data?.running.map((file) => (
<Table.Tr key={file.name}>
<Table.Td>{file.name}</Table.Td>
<Table.Td>{new Date(file.date).toLocaleString()}</Table.Td>
<Table.Td>{file.files}</Table.Td>
<ActionIcon
component={Link}
target='_blank'
href={`/api/user/export?id=${exportDb.id}`}
disabled={!exportDb.completed}
>
<IconDownload size='1rem' />
</ActionIcon>
</Group>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>

View File

@@ -3,25 +3,20 @@ import { datasource } from '@/lib/datasource';
import { prisma } from '@/lib/db';
import { log } from '@/lib/logger';
import { userMiddleware } from '@/server/middleware/user';
import { Export } from '@prisma/client';
import fastifyPlugin from 'fastify-plugin';
import { Zip, ZipPassThrough } from 'fflate';
import { createWriteStream } from 'fs';
import { readdir, rename, rm } from 'fs/promises';
import { rm, stat } from 'fs/promises';
import { join } from 'path';
export type ApiUserExportResponse = {
running?: boolean;
deleted?: boolean;
} & {
[key in 'running' | 'complete']: {
date: number;
files: number;
name: string;
}[];
};
} & Export[];
type Query = {
name?: string;
id?: string;
};
export const PATH = '/api/user/export';
@@ -31,57 +26,40 @@ const logger = log('api').c('user').c('export');
export default fastifyPlugin(
(server, _, done) => {
server.get<{ Querystring: Query }>(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
const tmpFiles = await readdir(config.core.tempDirectory);
const userExports = tmpFiles
.filter((file) => file.startsWith(`zexport_${req.user.id}`) && file.endsWith('.zip'))
.map((file) => file.split('_'))
.filter((file) => file.length === 5);
const exports = await prisma.export.findMany({
where: { userId: req.user.id },
});
const incompleteExports = userExports
.filter((file) => file[file.length - 1] === 'incomplete.zip')
.map((file) => ({
date: Number(file[2]),
files: Number(file[3]),
name: file.join('_'),
}));
const completeExports = userExports
.filter((file) => file[file.length - 1] === 'complete.zip')
.map((file) => ({
date: Number(file[2]),
files: Number(file[3]),
name: file.join('_'),
}));
if (req.query.name) {
const file = completeExports.find((file) => file.name === req.query.name);
if (req.query.id) {
const file = exports.find((x) => x.id === req.query.id);
if (!file) return res.notFound();
const path = join(config.core.tempDirectory, file.name);
if (!file.completed) return res.badRequest('Export is not completed');
const path = join(config.core.tempDirectory, file.path);
return res.sendFile(path);
}
return res.send({
running: incompleteExports,
complete: completeExports,
});
return res.send(exports);
});
server.delete<{ Querystring: Query }>(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
if (!req.query.name) return res.badRequest('No name provided');
if (!req.query.id) return res.badRequest('No id provided');
const tmpFiles = await readdir(config.core.tempDirectory);
const userExports = tmpFiles
.filter((file) => file.startsWith(`zexport_${req.user.id}`) && file.endsWith('.zip'))
.map((file) => file.split('_'))
.filter((file) => file.length === 5 && file[file.length - 1] === 'complete.zip')
.map((file) => file.join('_'));
const exportDb = await prisma.export.findFirst({
where: {
userId: req.user.id,
id: req.query.id,
},
});
if (!exportDb) return res.notFound();
if (!userExports.includes(req.query.name)) return res.notFound();
const path = join(config.core.tempDirectory, exportDb.path);
const path = join(config.core.tempDirectory, req.query.name);
await rm(path);
await prisma.export.delete({ where: { id: req.query.id } });
logger.info(`deleted export ${req.query.name}`);
logger.info(`deleted export ${exportDb.id}: ${exportDb.path}`);
return res.send({ deleted: true });
});
@@ -93,11 +71,20 @@ export default fastifyPlugin(
if (!files.length) return res.badRequest('No files to export');
const exportFileName = `zexport_${req.user.id}_${Date.now()}_${files.length}_incomplete.zip`;
const exportFileName = `zexport_${req.user.id}_${Date.now()}_${files.length}.zip`;
const exportPath = join(config.core.tempDirectory, exportFileName);
logger.debug(`exporting ${req.user.id}`, { exportPath, files: files.length });
const exportDb = await prisma.export.create({
data: {
userId: req.user.id,
path: exportFileName,
files: files.length,
size: '0',
},
});
const writeStream = createWriteStream(exportPath);
const zip = new Zip();
@@ -150,6 +137,8 @@ export default fastifyPlugin(
logger.debug('error while writing to zip', { err });
logger.error(`export for ${req.user.id} failed`);
await prisma.export.delete({ where: { id: exportDb.id } });
return;
}
@@ -157,14 +146,17 @@ export default fastifyPlugin(
if (!final) return;
const newExportName = `zexport_${req.user.id}_${Date.now()}_${files.length}_complete.zip`;
const path = join(config.core.tempDirectory, newExportName);
writeStream.end();
logger.debug('exported', { path, bytes: data.length });
logger.info(`export for ${req.user.id} finished at ${path}`);
logger.debug('exported', { path: exportPath, bytes: data.length });
logger.info(`export for ${req.user.id} finished at ${exportPath}`);
await rename(exportPath, path);
await prisma.export.update({
where: { id: exportDb.id },
data: {
completed: true,
size: (await stat(exportPath)).size.toString(),
},
});
};
for (let i = 0; i !== files.length; ++i) {