diff --git a/.gitignore b/.gitignore
index 21e258e..2b54b9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,5 +41,5 @@ yarn-error.log*
# zipline
config.toml
-uploads/
+uploads*/
dist/
\ No newline at end of file
diff --git a/src/components/pages/Manage/index.tsx b/src/components/pages/Manage/index.tsx
index 157572d..4eab265 100644
--- a/src/components/pages/Manage/index.tsx
+++ b/src/components/pages/Manage/index.tsx
@@ -309,6 +309,58 @@ 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: ,
+ });
+ } else {
+ updateNotification({
+ id: 'clear-uploads',
+ title: 'Successfully cleared uploads',
+ message: '',
+ color: 'green',
+ icon: ,
+ });
+ }
+ };
+
const handleOauthUnlink = async (provider) => {
const res = await useFetch('/api/auth/oauth', 'DELETE', {
provider,
@@ -525,6 +577,9 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
}>
Force Update Stats
+ }>
+ Delete all uploads
+
)}
diff --git a/src/lib/datasources/Datasource.ts b/src/lib/datasources/Datasource.ts
index 28b22bd..34a6f94 100644
--- a/src/lib/datasources/Datasource.ts
+++ b/src/lib/datasources/Datasource.ts
@@ -5,6 +5,7 @@ export abstract class Datasource {
public abstract save(file: string, data: Buffer): Promise;
public abstract delete(file: string): Promise;
+ public abstract clear(): Promise;
public abstract size(file: string): Promise;
public abstract get(file: string): Readable | Promise;
public abstract fullSize(): Promise;
diff --git a/src/lib/datasources/Local.ts b/src/lib/datasources/Local.ts
index ed2678a..e69578b 100644
--- a/src/lib/datasources/Local.ts
+++ b/src/lib/datasources/Local.ts
@@ -18,6 +18,13 @@ export class Local extends Datasource {
await rm(join(process.cwd(), this.path, file));
}
+ public async clear(): Promise {
+ const files = await readdir(join(process.cwd(), this.path));
+
+ for (let i = 0; i !== files.length; ++i) {
+ await rm(join(process.cwd(), this.path, files[i]));
+ }
+ }
public get(file: string): ReadStream {
const full = join(process.cwd(), this.path, file);
if (!existsSync(full)) return null;
diff --git a/src/lib/datasources/S3.ts b/src/lib/datasources/S3.ts
index 4fdb7fb..9e0797d 100644
--- a/src/lib/datasources/S3.ts
+++ b/src/lib/datasources/S3.ts
@@ -28,6 +28,18 @@ export class S3 extends Datasource {
await this.s3.removeObject(this.config.bucket, file);
}
+ public async clear(): Promise {
+ const objects = this.s3.listObjectsV2(this.config.bucket, '', true);
+ const files = [];
+
+ objects.on('data', (item) => files.push(item.name));
+ objects.on('end', async () => {
+ this.s3.removeObjects(this.config.bucket, files, (err) => {
+ if (err) throw err;
+ });
+ });
+ }
+
public get(file: string): Promise {
return new Promise((res, rej) => {
this.s3.getObject(this.config.bucket, file, (err, stream) => {
diff --git a/src/lib/datasources/Supabase.ts b/src/lib/datasources/Supabase.ts
index 9851e60..5c5e517 100644
--- a/src/lib/datasources/Supabase.ts
+++ b/src/lib/datasources/Supabase.ts
@@ -37,6 +37,41 @@ export class Supabase extends Datasource {
});
}
+ public async clear(): Promise {
+ try {
+ const resp = await fetch(`${this.config.url}/storage/v1/object/list/${this.config.bucket}`, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${this.config.key}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ prefix: '',
+ }),
+ });
+ const objs = await resp.json();
+ if (objs.error) throw new Error(`${objs.error}: ${objs.message}`);
+
+ const res = await fetch(`${this.config.url}/storage/v1/object/${this.config.bucket}`, {
+ method: 'DELETE',
+ headers: {
+ Authorization: `Bearer ${this.config.key}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ prefixes: objs.map((x: { name: string }) => x.name),
+ }),
+ });
+
+ const j = await res.json();
+ if (j.error) throw new Error(`${j.error}: ${j.message}`);
+
+ return;
+ } catch (e) {
+ this.logger.error(e);
+ }
+ }
+
public async get(file: string): Promise {
// get a readable stream from the request
const r = await fetch(`${this.config.url}/storage/v1/object/${this.config.bucket}/${file}`, {
diff --git a/src/pages/api/admin/clear.ts b/src/pages/api/admin/clear.ts
new file mode 100644
index 0000000..ce46c45
--- /dev/null
+++ b/src/pages/api/admin/clear.ts
@@ -0,0 +1,30 @@
+import datasource from 'lib/datasource';
+import Logger from 'lib/logger';
+import prisma from 'lib/prisma';
+import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
+
+const logger = Logger.get('admin');
+
+async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
+ try {
+ const { count } = await prisma.image.deleteMany({});
+ logger.info(`User ${user.username} (${user.id}) cleared the database of ${count} images`);
+
+ if (req.body.datasource) {
+ await datasource.clear();
+ logger.info(`User ${user.username} (${user.id}) cleared storage`);
+ }
+ } catch (e) {
+ logger.error(`User ${user.username} (${user.id}) failed to clear the database or storage`);
+ logger.error(e);
+ return res.badRequest(`failed to clear the database or storage: ${e}`);
+ }
+
+ return res.json({ message: 'cleared storage' });
+}
+
+export default withZipline(handler, {
+ methods: ['POST'],
+ user: true,
+ administrator: true,
+});