Skip to content

Commit

Permalink
Switch to Cloudflare R2
Browse files Browse the repository at this point in the history
  • Loading branch information
jake-walker committed Apr 9, 2024
1 parent dd8695c commit bb3835d
Show file tree
Hide file tree
Showing 7 changed files with 27 additions and 134 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A free and open source URL shortening, file sharing and pastebin service.

VH7 is a small project offering a free URL shortening, file sharing and pastebin service. Unlike other major URL shorteners, VH7 offers shorter links (4 characters) as well as the ability to have a short link for files and code snippets under the same roof.

VH7 utilises [Cloudflare Workers](https://workers.cloudflare.com/) for hosting the API, [Cloudflare Pages](https://pages.cloudflare.com/) for hosting the frontend, [AWS DynamoDB](https://aws.amazon.com/dynamodb/) for storing data and [AWS S3](https://aws.amazon.com/s3/) for storing files. _I have chosen to use two different cloud providers to allow me to run VH7 as cheaply as I can. Cloudflare gives a very generous Workers free tier, whereas AWS DynamoDB gives a good balance between price and flexability (whereas Cloudflare Workers KV which was used in the past was slightly more difficult to work with)._
VH7 utilises [Cloudflare Workers](https://workers.cloudflare.com/) for hosting the API, [Cloudflare Pages](https://pages.cloudflare.com/) for hosting the frontend, [Cloudflare D1](https://developers.cloudflare.com/d1/) for storing data and [Cloudflare R2](https://developers.cloudflare.com/r2/) for storing files.

## Getting Started

Expand Down
5 changes: 2 additions & 3 deletions worker/src/cleanup.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
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,
bucket: R2Bucket,
): Promise<string[]> {
const deleted: string[] = [];
const toCleanUp = await db.query.shortLinks.findMany({
Expand All @@ -26,7 +25,7 @@ export default async function cleanup(
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);
await bucket.delete(shortLink.id);
break;
default:
throw new Error(`Unexpected short link type ${shortLink.type}`);
Expand Down
8 changes: 2 additions & 6 deletions worker/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { customAlphabet } from 'nanoid/async';
import { DrizzleD1Database } from 'drizzle-orm/d1';
import { eq } from 'drizzle-orm/expressions';
import * as models from './models';
import { S3Configuration, putObject } from './s3';

const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 4);

Expand Down Expand Up @@ -73,17 +72,14 @@ export async function createPaste(

export async function createUpload(
db: DrizzleD1Database<typeof models>,
bucket: R2Bucket,
file: File,
rawExpires: number | null,
s3Config: S3Configuration,
): Promise<models.ShortLink & models.ShortLinkUpload> {
const id = await generateId();
const hash = await sha256(file);

const res = await putObject(s3Config, id, file);
if (res.status !== 200) {
throw new Error(`Failed to put object (status=${res.status}, msg=${await res.text()})`);
}
await bucket.put(id, file);

const maxExpiry = new Date();
maxExpiry.setDate(maxExpiry.getDate() + 30);
Expand Down
22 changes: 5 additions & 17 deletions worker/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import { getBindingsProxy } from 'wrangler';
import { getPlatformProxy } from 'wrangler';
import { drizzle } from 'drizzle-orm/d1';
import app, { type Bindings } from './index';
import * as models from './models';
import { S3Configuration, putObject } from './s3';
import { sha256 } from './controller';

const { bindings } = await getBindingsProxy();
const { env } = await getPlatformProxy();
// eslint-disable-next-line import/prefer-default-export
export const appEnv: Bindings = {
DB: bindings.DB as D1Database,
DB: env.DB as D1Database,
UPLOADS: env.UPLOADS as R2Bucket,
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',
};

Expand Down Expand Up @@ -57,14 +52,7 @@ beforeAll(async () => {
id: 'CCCC', filename: file.name, hash: await sha256(file), size: file.size,
});

const s3Config: S3Configuration = {
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,
};
await putObject(s3Config, 'CCCC', file);
await appEnv.UPLOADS.put('CCCC', file);
});

describe('API', () => {
Expand Down
47 changes: 9 additions & 38 deletions worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,12 @@ import {
} from './controller';
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,
VH7_ENV: string,
S3_ACCESS_KEY_ID: string,
S3_SECRET_ACCESS_KEY: string,
S3_REGION: string,
S3_ENDPOINT_URL: string,
S3_BUCKET: string,
UPLOADS: R2Bucket,
VH7_ADMIN_TOKEN: string
};

Expand Down Expand Up @@ -105,15 +100,8 @@ app.post('/api/upload',
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 upload = await createUpload(c.var.db, parsed.data.file, parsed.data.expires, s3Config);
const upload = await createUpload(c.var.db, c.env.UPLOADS, parsed.data.file,
parsed.data.expires);
return c.json(upload);
});

Expand Down Expand Up @@ -149,15 +137,7 @@ app.get('/api/cleanup', withDb, async (c) => {
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);
const deleted = await cleanup(c.var.db, c.env.UPLOADS);
return c.json({
deleted,
});
Expand Down Expand Up @@ -203,27 +183,18 @@ app.get('/:id', withDb, async (c) => {
});
case 'upload':
// eslint-disable-next-line no-case-declarations
const obj = await getObject({
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,
}, shortlink.id);

if (obj.status === 404) {
return c.text('Short link not found', 404);
}
const obj = await c.env.UPLOADS.get(shortlink.id);

if (obj.status !== 200) {
return c.status(500);
if (obj === null) {
return c.text('Short link not found', 404);
}

return c.body(obj.body as any, 200, {
return c.body(obj.body, 200, {
'Content-Type': 'application/force-download',
'Content-Transfer-Encoding': 'binary',
'Content-Disposition': `attachment; filename="${shortlink.filename}"`,
'Cache-Control': 'max-age=86400',
etag: obj.httpEtag,
});
default:
return c.status(500);
Expand Down
61 changes: 0 additions & 61 deletions worker/src/s3.ts

This file was deleted.

16 changes: 8 additions & 8 deletions worker/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ workers_dev = true
main = "src/index.ts"

[vars]
S3_ACCESS_KEY_ID = "minioadmin"
S3_SECRET_ACCESS_KEY = "minioadmin"
S3_REGION = "eu-west-1"
S3_ENDPOINT_URL = "http://localhost:9000"
S3_BUCKET = "vh7-uploads"
VH7_ENV = "development"
VH7_ADMIN_TOKEN = "keyboardcat"

Expand All @@ -18,10 +13,11 @@ database_name = "vh7-development"
database_id = "87904197-0107-411a-a030-be6ed70f8ff7"
migrations_dir = "migrations"

[[r2_buckets]]
binding = "UPLOADS"
bucket_name = "vh7-uploads-development"

[env.production.vars]
S3_REGION = "eu-west-1"
S3_ENDPOINT_URL = "https://gateway.storjshare.io"
S3_BUCKET = "uploads"
VH7_ENV = "production"

[[env.production.d1_databases]]
Expand All @@ -30,6 +26,10 @@ database_name = "vh7"
database_id = "8248b645-7133-4795-a343-e9273706f77c"
migrations_dir = "migrations"

[[env.production.r2_buckets]]
binding = "UPLOADS"
bucket_name = "vh7-uploads-production"

[[env.production.routes]]
pattern = "vh7.uk/*"
zone_name = "vh7.uk"
Expand Down

0 comments on commit bb3835d

Please sign in to comment.