feat: clearing orphaned files (#303)

This commit is contained in:
Jayvin Hernandez 2023-02-25 20:35:08 -08:00 committed by GitHub
parent 2d69cd580a
commit 95a1c7f92c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 103 additions and 53 deletions

View file

@ -0,0 +1,88 @@
import { Button, Checkbox, Group, Modal, Text, Title } from '@mantine/core';
import { closeAllModals, openConfirmModal } from '@mantine/modals';
import { showNotification, updateNotification } from '@mantine/notifications';
import { CheckIcon, CrossIcon } from 'components/icons';
import useFetch from 'hooks/useFetch';
export default function ClearStorage({ open, setOpen, check, setCheck }) {
const handleDelete = async (datasource: boolean, orphaned?: boolean) => {
showNotification({
id: 'clear-uploads',
title: 'Clearing...',
message: '',
loading: true,
autoClose: false,
});
const res = await useFetch('/api/admin/clear', 'POST', { datasource, orphaned });
if (res.error) {
updateNotification({
id: 'clear-uploads',
title: 'Error while clearing uploads',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
} else {
updateNotification({
id: 'clear-uploads',
title: 'Successfully cleared uploads',
message: '',
color: 'green',
icon: <CheckIcon />,
});
}
};
return (
<Modal
opened={open}
onClose={() => setOpen(false)}
title={<Title size='sm'>Are you sure you want to clear all uploads in the database?</Title>}
>
<Checkbox
id='orphanedFiles'
label='Clear only orphaned files?'
description='Orphaned files are not owned by anyone. They can&#39;t be seen the dashboard by anyone.'
checked={check}
onChange={(e) => setCheck(e.currentTarget.checked)}
/>
<Group position='right' mt='md'>
<Button
onClick={() => {
setOpen(() => false);
}}
>
No
</Button>
<Button
onClick={() => {
setOpen(false);
openConfirmModal({
title: 'Do you want to clear storage too?',
labels: { confirm: 'Yes', cancel: check ? 'Ok' : 'No' },
children: check && (
<Text size='sm' color='gray'>
Due to clearing orphaned files, storage clearing will be unavailable.
</Text>
),
confirmProps: { disabled: check },
onConfirm: () => {
closeAllModals();
handleDelete(true);
},
onCancel: () => {
closeAllModals();
handleDelete(false, check);
},
onClose: () => setCheck(false),
});
}}
>
Yes
</Button>
</Group>
</Modal>
);
}

View file

@ -42,6 +42,7 @@ import { bytesToHuman } from 'lib/utils/bytes';
import { capitalize } from 'lib/utils/client'; import { capitalize } from 'lib/utils/client';
import { useEffect, useReducer, useState } from 'react'; import { useEffect, useReducer, useState } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import ClearStorage from './ClearStorage';
import Flameshot from './Flameshot'; import Flameshot from './Flameshot';
import ShareX from './ShareX'; import ShareX from './ShareX';
import { TotpModal } from './TotpModal'; import { TotpModal } from './TotpModal';
@ -76,10 +77,12 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
const [totpOpen, setTotpOpen] = useState(false); const [totpOpen, setTotpOpen] = useState(false);
const [shareXOpen, setShareXOpen] = useState(false); const [shareXOpen, setShareXOpen] = useState(false);
const [flameshotOpen, setFlameshotOpen] = useState(false); const [flameshotOpen, setFlameshotOpen] = useState(false);
const [clrStorOpen, setClrStorOpen] = useState(false);
const [exports, setExports] = useState([]); const [exports, setExports] = useState([]);
const [file, setFile] = useState<File>(null); const [file, setFile] = useState<File>(null);
const [fileDataURL, setFileDataURL] = useState(user.avatar ?? null); const [fileDataURL, setFileDataURL] = useState(user.avatar ?? null);
const [totpEnabled, setTotpEnabled] = useState(!!user.totpSecret); const [totpEnabled, setTotpEnabled] = useState(!!user.totpSecret);
const [checked, setCheck] = useState(false);
const getDataURL = (f: File): Promise<string> => { const getDataURL = (f: File): Promise<string> => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
@ -312,58 +315,6 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
} }
}; };
const openClearData = () => {
modals.openConfirmModal({
title: 'Are you sure you want to clear all uploads in the database?',
closeOnConfirm: false,
labels: { confirm: 'Yes', cancel: 'No' },
onConfirm: () => {
modals.openConfirmModal({
title: 'Do you want to clear storage too?',
labels: { confirm: 'Yes', cancel: 'No' },
onConfirm: () => {
handleClearData(true);
modals.closeAll();
},
onCancel: () => {
handleClearData(false);
modals.closeAll();
},
});
},
});
};
const handleClearData = async (datasource?: boolean) => {
showNotification({
id: 'clear-uploads',
title: 'Clearing...',
message: '',
loading: true,
autoClose: false,
});
const res = await useFetch('/api/admin/clear', 'POST', { datasource });
if (res.error) {
updateNotification({
id: 'clear-uploads',
title: 'Error while clearing uploads',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
} else {
updateNotification({
id: 'clear-uploads',
title: 'Successfully cleared uploads',
message: '',
color: 'green',
icon: <CheckIcon />,
});
}
};
const handleOauthUnlink = async (provider) => { const handleOauthUnlink = async (provider) => {
const res = await useFetch('/api/auth/oauth', 'DELETE', { const res = await useFetch('/api/auth/oauth', 'DELETE', {
provider, provider,
@ -598,7 +549,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
<Button size='md' onClick={forceUpdateStats} color='red' rightIcon={<RefreshIcon />}> <Button size='md' onClick={forceUpdateStats} color='red' rightIcon={<RefreshIcon />}>
Force Update Stats Force Update Stats
</Button> </Button>
<Button size='md' onClick={openClearData} color='red' rightIcon={<TrashIcon />}> <Button size='md' onClick={() => setClrStorOpen(true)} color='red' rightIcon={<TrashIcon />}>
Delete all uploads Delete all uploads
</Button> </Button>
</Group> </Group>
@ -617,6 +568,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
<ShareX user={user} open={shareXOpen} setOpen={setShareXOpen} /> <ShareX user={user} open={shareXOpen} setOpen={setShareXOpen} />
<Flameshot user={user} open={flameshotOpen} setOpen={setFlameshotOpen} /> <Flameshot user={user} open={flameshotOpen} setOpen={setFlameshotOpen} />
<ClearStorage open={clrStorOpen} setOpen={setClrStorOpen} check={checked} setCheck={setCheck} />
</> </>
); );
} }

View file

@ -7,6 +7,16 @@ const logger = Logger.get('admin');
async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
try { try {
const { datasource, orphaned } = req.body;
if (orphaned) {
const { count } = await prisma.file.deleteMany({
where: {
userId: null,
},
});
logger.info(`User ${user.username} (${user.id}) cleared the database of ${count} orphaned files`);
return res.json({ message: 'cleared storage (orphaned only)' });
}
const { count } = await prisma.file.deleteMany({}); const { count } = await prisma.file.deleteMany({});
logger.info(`User ${user.username} (${user.id}) cleared the database of ${count} files`); logger.info(`User ${user.username} (${user.id}) cleared the database of ${count} files`);