diff --git a/server/aws.ts b/server/aws.ts index 183a1f52f..8bf292027 100644 --- a/server/aws.ts +++ b/server/aws.ts @@ -1,11 +1,34 @@ -export const awsConfig = { - region: process.env.AWS_REGION, +import { S3 } from '@aws-sdk/client-s3'; +import type { S3ClientConfig } from '@aws-sdk/client-s3'; + +export enum SupportedProviders { + AWS = 'aws', + CLOUDFLARE = 'cloudflare', + LOCAL = 'local', +} + +export interface S3Config extends S3ClientConfig { + provider: SupportedProviders; + region?: string; + credentials?: { + accessKeyId: string; + secretAccessKey: string; + }; + endpoint?: string; +} + +export const s3Config: S3Config = { + provider: process.env.S3_PROVIDER as SupportedProviders || SupportedProviders.AWS, + region: process.env.S3_REGION || undefined, credentials: { - accessKeyId: process.env.AWS_KEY!, - secretAccessKey: process.env.AWS_SECRET!, + accessKeyId: process.env.S3_KEY!, + secretAccessKey: process.env.S3_SECRET!, }, + endpoint: process.env.S3_ENDPOINT || undefined, }; -export const awsParams = { - Bucket: process.env.AWS_S3_BUCKET!, +export const s3BucketParams = { + Bucket: process.env.S3_BUCKET!, }; + +export const s3 = new S3(s3Config); diff --git a/server/cards.ts b/server/cards.ts index 28731a49d..be78d35d8 100644 --- a/server/cards.ts +++ b/server/cards.ts @@ -1,21 +1,19 @@ import './setup.ts'; -import { S3 } from '@aws-sdk/client-s3'; import * as Sentry from '@sentry/node'; import Pageres from 'pageres'; import type { Screenshot } from 'pageres'; import { ulid } from 'ulid'; -import { awsConfig, awsParams } from './aws.ts'; +import { s3Config, s3BucketParams } from './aws.ts'; import dbConnection from './db.ts'; import type { Database } from './db.ts'; import isHighLoadTime from './overload.js'; +import { s3 } from '~/server/aws.ts'; import jwt from '~/server/jwt.ts'; import { getLocaleUrls } from '~/src/domain.ts'; -const s3 = new S3(awsConfig); - const urlBases: Record = {}; for (const [locale, url] of Object.entries(getLocaleUrls(process.env.NUXT_PUBLIC_DOMAIN_BASE))) { @@ -109,12 +107,12 @@ const shoot = async (db: Database, mode: 'light' | 'dark'): Promise => { Body: buffer, ContentType: 'image/png', ACL: 'public-read', - ...awsParams, + ...s3BucketParams, }); await db.get(` UPDATE profiles - SET ${mode === 'dark' ? 'cardDark' : 'card'}='https://${awsParams.Bucket}.s3.${awsConfig.region}.amazonaws.com/${key}' + SET ${mode === 'dark' ? 'cardDark' : 'card'}='https://${s3BucketParams.Bucket}.s3.${s3Config.region}.amazonaws.com/${key}' WHERE id='${id}'`); } }; diff --git a/server/cleanupImages.ts b/server/cleanupImages.ts index 0f52d1a6b..a9d6fe02c 100644 --- a/server/cleanupImages.ts +++ b/server/cleanupImages.ts @@ -1,9 +1,8 @@ import './setup.ts'; -import { S3 } from '@aws-sdk/client-s3'; import type { ListObjectsV2Output, ObjectIdentifier, _Object } from '@aws-sdk/client-s3'; -import { awsConfig, awsParams } from './aws.ts'; +import { s3, s3BucketParams } from './aws.ts'; import dbConnection from './db.ts'; const FRESH_CUTOFF = 6 * 60 * 60 * 1000; // 6 hours @@ -108,7 +107,6 @@ async function cleanup(): Promise { validateIds('Cards', cards, 50_000); console.log('--- Cleaning up S3: Images ---'); - const s3 = new S3(awsConfig); let overall = 0; let fresh = 0; let removed = 0; @@ -124,7 +122,7 @@ async function cleanup(): Promise { Prefix: 'images/', MaxKeys: chunkSize, ContinuationToken: continuationToken, - ...awsParams, + ...s3BucketParams, }); if (!objects.Contents) { break; @@ -177,7 +175,7 @@ async function cleanup(): Promise { Delete: { Objects: toRemove, }, - ...awsParams, + ...s3BucketParams, }); } @@ -195,7 +193,7 @@ async function cleanup(): Promise { Prefix: 'card/', MaxKeys: chunkSize, ContinuationToken: continuationToken, - ...awsParams, + ...s3BucketParams, }); if (!objects.Contents) { break; @@ -231,7 +229,7 @@ async function cleanup(): Promise { Delete: { Objects: toRemove, }, - ...awsParams, + ...s3BucketParams, }); } diff --git a/server/express/images.ts b/server/express/images.ts index f6daee271..928a6fb3e 100644 --- a/server/express/images.ts +++ b/server/express/images.ts @@ -1,7 +1,6 @@ import fs from 'fs'; import path from 'path'; -import { S3 } from '@aws-sdk/client-s3'; import { loadImage, createCanvas } from 'canvas'; import type { Canvas } from 'canvas'; import { Router } from 'express'; @@ -12,7 +11,7 @@ import { ulid } from 'ulid'; import { handleErrorAsync } from '../../src/helpers.ts'; import { auditLog } from '../audit.ts'; -import { awsConfig, awsParams } from '../aws.ts'; +import { s3, s3BucketParams } from '../aws.ts'; import { rootDir } from '../paths.ts'; import { getLocale } from '~/server/data.ts'; @@ -67,8 +66,6 @@ router.post('/images/upload', handleErrorAsync(async (req, res) => { return res.status(400); } - const s3 = new S3(awsConfig); - const requestedSizes = req.query.sizes === undefined || req.query.sizes === 'all' ? null : (req.query.sizes as string).split(','); @@ -103,7 +100,7 @@ router.post('/images/upload', handleErrorAsync(async (req, res) => { Body: canvas.toBuffer('image/png'), ContentType: 'image/png', ACL: 'public-read', - ...awsParams, + ...s3BucketParams, }); } diff --git a/server/express/profile.ts b/server/express/profile.ts index fb60584f4..6ab768c1b 100644 --- a/server/express/profile.ts +++ b/server/express/profile.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import zlib from 'node:zlib'; -import { S3, NoSuchKey } from '@aws-sdk/client-s3'; +import { NoSuchKey } from '@aws-sdk/client-s3'; import * as Sentry from '@sentry/node'; import { Router } from 'express'; import type { Request, Response } from 'express'; @@ -36,7 +36,7 @@ import { socialProviders } from '../../src/socialProviders.ts'; import { colours, styles } from '../../src/styling.ts'; import { auditLog } from '../audit.ts'; import avatar from '../avatar.ts'; -import { awsConfig, awsParams } from '../aws.ts'; +import { s3, s3BucketParams } from '../aws.ts'; import crypto from '../crypto.ts'; import type { Database } from '../db.ts'; import { rootDir } from '../paths.ts'; @@ -1123,13 +1123,12 @@ router.get('/profile/export', handleErrorAsync(async (req: Request, res: Respons profiles[profile.locale] = exportProfile; } - const s3 = new S3(awsConfig); const images: ProfileExportData['images'] = {}; for (const id of customFlagIds) { try { const data = await s3.getObject({ Key: `images/${id}-flag.png`, - ...awsParams, + ...s3BucketParams, }); images[id] = { flag: await data.Body!.transformToString('base64'), @@ -1189,13 +1188,12 @@ router.post('/profile/import', handleErrorAsync(async (req, res) => { const { profiles, images } = JSON.parse(Buffer.from(payload, 'base64').toString('utf-8')) as ProfileExportData; - const s3 = new S3(awsConfig); for (const [id, sizes] of Object.entries(images)) { for (const [size, content] of Object.entries(sizes)) { try { await s3.headObject({ Key: `images/${id}-${size}.png`, - ...awsParams, + ...s3BucketParams, }); continue; } catch (error) { @@ -1209,7 +1207,7 @@ router.post('/profile/import', handleErrorAsync(async (req, res) => { Body: Buffer.from(content, 'base64'), ContentType: 'image/png', ACL: 'public-read', - ...awsParams, + ...s3BucketParams, }); } } diff --git a/server/express/pronounce.ts b/server/express/pronounce.ts index 00c08402d..237fc9ced 100644 --- a/server/express/pronounce.ts +++ b/server/express/pronounce.ts @@ -1,5 +1,6 @@ import { Polly } from '@aws-sdk/client-polly'; -import { S3, NoSuchKey } from '@aws-sdk/client-s3'; +import { NoSuchKey } from '@aws-sdk/client-s3'; +import type { S3 } from '@aws-sdk/client-s3'; import type { NodeJsClient } from '@smithy/types'; import { Router } from 'express'; import { getH3Event } from 'h3-express'; @@ -8,7 +9,7 @@ import sha1 from 'sha1'; import type { PronunciationVoiceConfig, AwsPollyPronunciationVoiceConfig, NarakeetPronunciationVoiceConfig } from '../../locale/config.ts'; import { convertPronunciationStringToSsml, convertPronunciationStringToNarakeetFormat, handleErrorAsync } from '../../src/helpers.ts'; -import { awsConfig, awsParams } from '../aws.ts'; +import { s3, s3Config, s3BucketParams } from '../aws.ts'; import { getLocale, loadConfig } from '~/server/data.ts'; @@ -26,7 +27,8 @@ const providers: Record = { return convertPronunciationStringToSsml(text); }, async generate(textTokenised: string, voice: AwsPollyPronunciationVoiceConfig): Promise<[Uint8Array, string]> { - const polly = new Polly(awsConfig) as NodeJsClient; + // @ts-expect-error Type Conflict + const polly = new Polly(s3Config) as NodeJsClient; const pollyResponse = await polly.synthesizeSpeech({ TextType: 'ssml', @@ -87,14 +89,12 @@ router.get('/pronounce/:voice/:pronunciation', handleErrorAsync(async (req, res) return res.status(404).json({ error: 'Not found' }); } - const s3 = new S3(awsConfig) as NodeJsClient; - const provider = providers[(voice.provider || 'aws_polly') as ProviderKey]; const tokenised = provider.tokenised(text); const key = `pronunciation/${config.locale}-${req.params.voice}/${sha1(tokenised)}.mp3`; try { - const s3Response = await s3.getObject({ Key: key, ...awsParams }); + const s3Response = await (s3 as NodeJsClient).getObject({ Key: key, ...s3BucketParams }); res.set('content-type', s3Response.ContentType); return s3Response.Body!.pipe(res); } catch (error) { @@ -108,7 +108,7 @@ router.get('/pronounce/:voice/:pronunciation', handleErrorAsync(async (req, res) Key: key, Body: buffer, ContentType: contentType, - ...awsParams, + ...s3BucketParams, }); res.set('content-type', contentType); diff --git a/server/imageCopy.ts b/server/imageCopy.ts index 3d65782a3..28b182959 100644 --- a/server/imageCopy.ts +++ b/server/imageCopy.ts @@ -1,11 +1,8 @@ -import { S3 } from '@aws-sdk/client-s3'; import { loadImage, createCanvas } from 'canvas'; -import { awsConfig, awsParams } from '~/server/aws.ts'; +import { s3, s3BucketParams } from '~/server/aws.ts'; import { newDate, sha256 } from '~/src/helpers.ts'; -const s3 = new S3(awsConfig); - const convertToPng = async (original: Buffer): Promise => { const image = await loadImage(original); const canvas = createCanvas(image.width, image.height); @@ -24,7 +21,7 @@ export default async (prefix: string, url: string, ttlDays: number | null = null const key = `${prefix}/${await sha256(url)}.${isSvg ? 'svg' : 'png'}`; try { - const metadata = await s3.headObject({ Key: key, ...awsParams }); + const metadata = await s3.headObject({ Key: key, ...s3BucketParams }); if ( ttlDays !== null && @@ -49,7 +46,7 @@ export default async (prefix: string, url: string, ttlDays: number | null = null ? 'image/svg+xml' : 'image/png', ACL: 'public-read', - ...awsParams, + ...s3BucketParams, }); return `${process.env.NUXT_PUBLIC_CLOUDFRONT}/${key}`;