Skip to content

Commit

Permalink
Add cleanup route
Browse files Browse the repository at this point in the history
  • Loading branch information
jake-walker committed Jan 29, 2024
1 parent 48a4149 commit 4dde154
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 6 deletions.
29 changes: 29 additions & 0 deletions worker/src/cleanup.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { drizzle } from 'drizzle-orm/d1';
import * as models from './models';
import cleanup from './cleanup';
import { createShortUrl } from './controller';
import { appEnv } from './index.spec';

test('cleanup', async () => {
const { DB } = appEnv;
const d = drizzle(DB, { schema: models });

const validDate = new Date();
validDate.setMonth(validDate.getMonth() + 1);

const expiredShortLink = await createShortUrl(d, 'https://example.com', new Date(2000, 1, 1).getTime());
const validShortLink = await createShortUrl(d, 'https://example.com', validDate.getTime());
const noExpiryShortLink = await createShortUrl(d, 'https://example.com', null);

const deleted = await cleanup(d, {
accessKeyId: appEnv.S3_ACCESS_KEY_ID,
secretAccessKey: appEnv.S3_SECRET_ACCESS_KEY,
bucket: appEnv.S3_BUCKET,
endpointUrl: appEnv.S3_ENDPOINT_URL,
region: appEnv.S3_REGION,
});

expect(deleted).toContain(expiredShortLink.id);
expect(deleted).not.toContain(validShortLink.id);
expect(deleted).not.toContain(noExpiryShortLink.id);
});
38 changes: 38 additions & 0 deletions worker/src/cleanup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { type DrizzleD1Database } from 'drizzle-orm/d1';
import { eq, lt } from 'drizzle-orm';
import * as models from './models';
import { deleteObject, type S3Configuration } from './s3';

export default async function cleanup(
db: DrizzleD1Database<typeof models>,
s3Config: S3Configuration,
): Promise<string[]> {
const deleted: string[] = [];
const toCleanUp = await db.query.shortLinks.findMany({
where: lt(models.shortLinks.expiresAt, new Date()),
});

await Promise.all(toCleanUp.map(async (shortLink) => {
switch (shortLink.type) {
case 'url':
await db.delete(models.shortLinkUrls).where(eq(models.shortLinkUrls.id, shortLink.id));
await db.delete(models.shortLinks).where(eq(models.shortLinks.id, shortLink.id));
break;
case 'paste':
await db.delete(models.shortLinkPastes).where(eq(models.shortLinkPastes.id, shortLink.id));
await db.delete(models.shortLinks).where(eq(models.shortLinks.id, shortLink.id));
break;
case 'upload':
await db.delete(models.shortLinkUploads)
.where(eq(models.shortLinkUploads.id, shortLink.id));
await db.delete(models.shortLinks).where(eq(models.shortLinks.id, shortLink.id));
await deleteObject(s3Config, shortLink.id);
break;
default:
throw new Error(`Unexpected short link type ${shortLink.type}`);
}
deleted.push(shortLink.id);
}));

return deleted;
}
17 changes: 16 additions & 1 deletion worker/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { S3Configuration, putObject } from './s3';
import { sha256 } from './controller';

const { bindings } = await getBindingsProxy();
const appEnv: Bindings = {
// eslint-disable-next-line import/prefer-default-export
export const appEnv: Bindings = {
DB: bindings.DB as D1Database,
VH7_ENV: 'testing',
S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID || 'minioadmin',
S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY || 'minioadmin',
S3_REGION: process.env.S3_REGION || 'eu-west-1',
S3_ENDPOINT_URL: process.env.S3_ENDPOINT_URL || 'http://localhost:9000',
S3_BUCKET: process.env.S3_BUCKET || 'vh7-uploads',
VH7_ADMIN_TOKEN: 'keyboardcat',
};

beforeAll(async () => {
Expand Down Expand Up @@ -234,4 +236,17 @@ describe('API', () => {
}, appEnv);
expect(res.status).toBe(404);
});

test('cleanup requires auth', async () => {
const noAuthRes = await app.request('http://vh7.uk/api/cleanup', {}, appEnv);
expect([401, 403]).toContain(noAuthRes.status);

const authRes = await app.request('http://vh7.uk/api/cleanup', {
headers: {
Authorization: `Bearer ${appEnv.VH7_ADMIN_TOKEN}`,
},
}, appEnv);
expect(authRes.status).toBe(200);
expectTypeOf(await authRes.json()).toBeArray();
});
});
29 changes: 28 additions & 1 deletion worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { checkDirectUserAgent, getFrontendUrl, isValidId } from './helpers';
import * as models from './models';
import { S3Configuration, getObject } from './s3';
import cleanup from './cleanup';

export type Bindings = {
DB: D1Database,
Expand All @@ -16,7 +17,8 @@ export type Bindings = {
S3_SECRET_ACCESS_KEY: string,
S3_REGION: string,
S3_ENDPOINT_URL: string,
S3_BUCKET: string
S3_BUCKET: string,
VH7_ADMIN_TOKEN: string
};

type Env = {
Expand Down Expand Up @@ -130,6 +132,31 @@ app.get('/api/info/:id', withDb, async (c) => {
}, 404);
});

app.get('/api/cleanup', withDb, async (c) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '') || '';

if (token !== c.env.VH7_ADMIN_TOKEN) {
return c.text('Invalid or non-existant admin token', 403);
}

if (c.var.db === undefined) {
return c.status(500);
}

const s3Config: S3Configuration = {
accessKeyId: c.env.S3_ACCESS_KEY_ID,
secretAccessKey: c.env.S3_SECRET_ACCESS_KEY,
bucket: c.env.S3_BUCKET,
endpointUrl: c.env.S3_ENDPOINT_URL,
region: c.env.S3_REGION,
};

const deleted = await cleanup(c.var.db, s3Config);
return c.json({
deleted,
});
});

app.get('/:id', withDb, async (c) => {
let id: string | undefined = c.req.param('id');
const frontendUrl = getFrontendUrl(c.env.VH7_ENV);
Expand Down
5 changes: 5 additions & 0 deletions worker/src/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,8 @@ export async function putObject(config: S3Configuration, filename: string, file:

return req;
}

export async function deleteObject(config: S3Configuration, filename: string) {
const req = makeRequest(config, `/${filename}`, { method: 'DELETE' });
return req;
}
9 changes: 5 additions & 4 deletions worker/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
globals: true
}
})
globals: true,
fileParallelism: false,
},
});
1 change: 1 addition & 0 deletions worker/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ S3_REGION = "eu-west-1"
S3_ENDPOINT_URL = "http://localhost:9000"
S3_BUCKET = "vh7-uploads"
VH7_ENV = "development"
VH7_ADMIN_TOKEN = "keyboardcat"

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
Expand Down

0 comments on commit 4dde154

Please sign in to comment.