mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-22 12:03:25 -04:00
Merge branch 'aws-v3-migration' into 'main'
aws v3 migration See merge request PronounsPage/PronounsPage!434
This commit is contained in:
commit
3321f31917
@ -1,3 +1,5 @@
|
||||
import type { Engine, LanguageCode, VoiceId } from '@aws-sdk/client-polly';
|
||||
|
||||
type Toggable<T> = ({ enabled: true } & T) | { enabled: false } & Partial<T>;
|
||||
|
||||
export interface Config {
|
||||
@ -283,15 +285,15 @@ interface PronunciationVoiceConfig {
|
||||
/**
|
||||
* language code
|
||||
*/
|
||||
language: string;
|
||||
language: LanguageCode;
|
||||
/**
|
||||
* voice name
|
||||
*/
|
||||
voice: string;
|
||||
voice: VoiceId;
|
||||
/**
|
||||
* voice engine
|
||||
*/
|
||||
engine: 'standard' | 'neural';
|
||||
engine: Engine;
|
||||
}
|
||||
|
||||
interface SourcesConfig {
|
||||
|
@ -44,8 +44,8 @@ pronouns:
|
||||
pronunciation:
|
||||
enabled: true
|
||||
voices:
|
||||
GB:
|
||||
language: 'ko-KO'
|
||||
KO:
|
||||
language: 'ko-KR'
|
||||
voice: 'Seoyeon'
|
||||
engine: 'neural'
|
||||
|
||||
|
@ -690,7 +690,7 @@ const nuxtConfig: NuxtConfig = {
|
||||
listen(_server, { port }) {
|
||||
if (version) {
|
||||
process.stderr.write(`[${new Date().toISOString()}] ` +
|
||||
`Listening on port ${port} with version ${version}`);
|
||||
`Listening on port ${port} with version ${version}\n`);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -13,6 +13,8 @@
|
||||
"test": "node --experimental-vm-modules $(yarn bin jest)"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-polly": "^3.525.0",
|
||||
"@aws-sdk/client-s3": "^3.525.0",
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
"@nuxtjs/pwa": "3.3.5",
|
||||
"@nuxtjs/redirect-module": "^0.3.1",
|
||||
@ -23,7 +25,6 @@
|
||||
"avris-futurus": "^1.0.2",
|
||||
"avris-generator": "^0.8.2",
|
||||
"avris-sorter": "^0.0.3",
|
||||
"aws-sdk": "^2.1425.0",
|
||||
"canvas": "^2.11.2",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cookie-universal-nuxt": "^2.1.4",
|
||||
@ -93,11 +94,13 @@
|
||||
"@types/js-md5": "^0.7.2",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/luxon": "^1.27.1",
|
||||
"@types/multer": "1.4.5",
|
||||
"@types/node": "^20.11.5",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/rtlcss": "^3.1.2",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/speakeasy": "^2.0.10",
|
||||
"@types/webpack": "^4.41.38",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
|
@ -1,12 +1,12 @@
|
||||
import fetch from 'node-fetch';
|
||||
import S3 from 'aws-sdk/clients/s3.js';
|
||||
import awsConfig from './aws.js';
|
||||
import { S3 } from '@aws-sdk/client-s3';
|
||||
import { awsConfig, awsParams } from './aws.ts';
|
||||
import md5 from 'js-md5';
|
||||
import { loadImage, createCanvas } from 'canvas';
|
||||
|
||||
const s3 = new S3(awsConfig);
|
||||
|
||||
export default async (provider, url) => {
|
||||
export default async (provider: string, url: string): Promise<string | null> => {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
@ -14,7 +14,7 @@ export default async (provider, url) => {
|
||||
const key = `images-copy/${provider}/${md5(url)}.png`;
|
||||
|
||||
try {
|
||||
await s3.headObject({ Key: key }).promise();
|
||||
await s3.headObject({ Key: key, ...awsParams });
|
||||
|
||||
return `${process.env.CLOUDFRONT}/${key}`;
|
||||
} catch {
|
||||
@ -28,7 +28,8 @@ export default async (provider, url) => {
|
||||
Body: canvas.toBuffer('image/png'),
|
||||
ContentType: 'image/png',
|
||||
ACL: 'public-read',
|
||||
}).promise();
|
||||
...awsParams,
|
||||
});
|
||||
|
||||
return `${process.env.CLOUDFRONT}/${key}`;
|
||||
} catch (e) {
|
@ -1,10 +0,0 @@
|
||||
export default {
|
||||
region: process.env.AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_KEY,
|
||||
secretAccessKey: process.env.AWS_SECRET,
|
||||
},
|
||||
params: {
|
||||
Bucket: process.env.AWS_S3_BUCKET,
|
||||
},
|
||||
};
|
11
server/aws.ts
Normal file
11
server/aws.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const awsConfig = {
|
||||
region: process.env.AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_KEY!,
|
||||
secretAccessKey: process.env.AWS_SECRET!,
|
||||
},
|
||||
};
|
||||
|
||||
export const awsParams = {
|
||||
Bucket: process.env.AWS_S3_BUCKET!,
|
||||
};
|
@ -1,32 +1,34 @@
|
||||
import './setup.ts';
|
||||
|
||||
import type { Screenshot } from 'pageres';
|
||||
import Pageres from 'pageres';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import isHighLoadTime from './overload.js';
|
||||
import type { Database } from './db.ts';
|
||||
import dbConnection from './db.ts';
|
||||
import allLocales from '../locale/locales.ts';
|
||||
import { ulid } from 'ulid';
|
||||
|
||||
import awsConfig from './aws.js';
|
||||
import S3 from 'aws-sdk/clients/s3.js';
|
||||
import { awsConfig, awsParams } from './aws.ts';
|
||||
import { S3 } from '@aws-sdk/client-s3';
|
||||
|
||||
const s3 = new S3(awsConfig);
|
||||
|
||||
const urlBases = {};
|
||||
const urlBases: Record<string, string> = {};
|
||||
for (const { code, url } of allLocales) {
|
||||
urlBases[code] = `${url}/card/@`; // 'http://localhost:3000/card/@'
|
||||
}
|
||||
const domainLocaleMap = {};
|
||||
const domainLocaleMap: Record<string, string> = {};
|
||||
for (const { code, url } of allLocales) {
|
||||
domainLocaleMap[url.replace(/^https?:\/\//, '')] = code;
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||
const sleep = (ms: number): Promise<void> => new Promise((res) => setTimeout(res, ms));
|
||||
|
||||
const modes = ['light', 'dark'];
|
||||
const modes = ['light', 'dark'] as const;
|
||||
|
||||
const shoot = async (db, mode) => {
|
||||
const profiles = (await db.all(`
|
||||
const shoot = async (db: Database, mode: 'light' | 'dark'): Promise<void> => {
|
||||
const profiles = (await db.all<{ id: string, locale: string, username: string }>(`
|
||||
SELECT profiles.id, profiles.locale, users.username
|
||||
FROM profiles
|
||||
LEFT JOIN users on profiles.userId = users.id
|
||||
@ -40,7 +42,7 @@ const shoot = async (db, mode) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = {};
|
||||
const results: Record<string, Screenshot> = {};
|
||||
|
||||
try {
|
||||
const pr = new Pageres({
|
||||
@ -55,7 +57,7 @@ const shoot = async (db, mode) => {
|
||||
}
|
||||
|
||||
for (const buffer of await pr.run()) {
|
||||
const [, domain, username] = buffer.filename.match(/(.*)!card!@(.*)-1024x300\.png/);
|
||||
const [, domain, username] = buffer.filename.match(/(.*)!card!@(.*)-1024x300\.png/)!;
|
||||
const locale = domainLocaleMap[domain];
|
||||
results[`${locale}/${username.replace(/[^A-Za-z0-9.-]/g, '_')}`] = buffer;
|
||||
}
|
||||
@ -85,16 +87,17 @@ const shoot = async (db, mode) => {
|
||||
Body: buffer,
|
||||
ContentType: 'image/png',
|
||||
ACL: 'public-read',
|
||||
}).promise();
|
||||
...awsParams,
|
||||
});
|
||||
|
||||
await db.get(`
|
||||
UPDATE profiles
|
||||
SET ${mode === 'dark' ? 'cardDark' : 'card'}='https://${awsConfig.params.Bucket}.s3.${awsConfig.region}.amazonaws.com/${key}'
|
||||
SET ${mode === 'dark' ? 'cardDark' : 'card'}='https://${awsParams.Bucket}.s3.${awsConfig.region}.amazonaws.com/${key}'
|
||||
WHERE id='${id}'`);
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
(async (): Promise<void> => {
|
||||
const db = await dbConnection();
|
||||
while (true) {
|
||||
for (const mode of modes) {
|
@ -1,18 +1,19 @@
|
||||
import './setup.ts';
|
||||
|
||||
import dbConnection from './db.ts';
|
||||
import awsConfig from './aws.js';
|
||||
import S3 from 'aws-sdk/clients/s3.js';
|
||||
import { awsConfig, awsParams } from './aws.ts';
|
||||
import { S3 } from '@aws-sdk/client-s3';
|
||||
import type { ListObjectsV2Output, ObjectIdentifier, _Object } from '@aws-sdk/client-s3';
|
||||
|
||||
const execute = process.env.EXECUTE === '1';
|
||||
|
||||
console.log(execute ? 'WILL REMOVE FILES!' : 'Dry run');
|
||||
|
||||
async function cleanup() {
|
||||
async function cleanup(): Promise<void> {
|
||||
console.log('--- Fetching ids expected to stay ---');
|
||||
const db = await dbConnection();
|
||||
|
||||
const avatars = {};
|
||||
const avatars: Record<string, true> = {};
|
||||
for (const row of await db.all(`
|
||||
SELECT avatarSource
|
||||
FROM users
|
||||
@ -20,7 +21,7 @@ async function cleanup() {
|
||||
avatars[row.avatarSource.match('https://[^/]+/images/(.*)-(?:thumb|avatar).png')[1]] = true;
|
||||
}
|
||||
|
||||
const flags = {};
|
||||
const flags: Record<string, true> = {};
|
||||
for (const row of await db.all(`
|
||||
SELECT customFlags
|
||||
FROM profiles
|
||||
@ -31,7 +32,7 @@ async function cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
const sources = {};
|
||||
const sources: Record<string, true> = {};
|
||||
for (const row of await db.all(`
|
||||
SELECT images
|
||||
FROM sources
|
||||
@ -42,7 +43,7 @@ async function cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
const terms = {};
|
||||
const terms: Record<string, true> = {};
|
||||
for (const row of await db.all(`
|
||||
SELECT images
|
||||
FROM terms
|
||||
@ -53,7 +54,7 @@ async function cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
const cards = {};
|
||||
const cards: Record<string, true> = {};
|
||||
for (const row of await db.all(`
|
||||
SELECT card
|
||||
FROM profiles
|
||||
@ -90,33 +91,37 @@ async function cleanup() {
|
||||
let removedSize = 0;
|
||||
|
||||
const chunkSize = 1000;
|
||||
let marker = undefined;
|
||||
let continuationToken: string | undefined = undefined;
|
||||
while (true) {
|
||||
console.log('Making a request');
|
||||
const objects = await s3.listObjects({
|
||||
const objects: ListObjectsV2Output = await s3.listObjectsV2({
|
||||
Prefix: 'images/',
|
||||
MaxKeys: chunkSize,
|
||||
Marker: marker,
|
||||
}).promise();
|
||||
ContinuationToken: continuationToken,
|
||||
...awsParams,
|
||||
});
|
||||
if (!objects.Contents) {
|
||||
break;
|
||||
}
|
||||
continuationToken = objects.NextContinuationToken;
|
||||
|
||||
const toRemove = [];
|
||||
const remove = async (object, reason) => {
|
||||
const toRemove: ObjectIdentifier[] = [];
|
||||
const remove = async (object: _Object, reason: string): Promise<void> => {
|
||||
console.log(`REMOVING: ${object.Key} (${reason})`);
|
||||
toRemove.push({ Key: object.Key });
|
||||
toRemove.push({ Key: object.Key! });
|
||||
removed += 1;
|
||||
removedSize += object.Size;
|
||||
removedSize += object.Size!;
|
||||
};
|
||||
|
||||
for (const object of objects.Contents) {
|
||||
overall++;
|
||||
marker = object.Key;
|
||||
|
||||
if (object.LastModified > new Date() - 60 * 60 * 1000) {
|
||||
if (object.LastModified!.getTime() > new Date().getTime() - 60 * 60 * 1000) {
|
||||
fresh++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, id, size] = object.Key.match('images/(.*)-(.*).png');
|
||||
const [, id, size] = object.Key!.match('images/(.*)-(.*).png')!;
|
||||
|
||||
if (avatars[id]) {
|
||||
if (size !== 'thumb' && size !== 'avatar') {
|
||||
@ -145,7 +150,8 @@ async function cleanup() {
|
||||
Delete: {
|
||||
Objects: toRemove,
|
||||
},
|
||||
}).promise();
|
||||
...awsParams,
|
||||
});
|
||||
}
|
||||
|
||||
if (objects.Contents.length < chunkSize) {
|
||||
@ -154,35 +160,39 @@ async function cleanup() {
|
||||
}
|
||||
|
||||
console.log('--- Cards ---');
|
||||
marker = undefined;
|
||||
continuationToken = undefined;
|
||||
while (true) {
|
||||
console.log('Making a request');
|
||||
const objects = await s3.listObjects({
|
||||
const objects: ListObjectsV2Output = await s3.listObjectsV2({
|
||||
Prefix: 'card/',
|
||||
MaxKeys: chunkSize,
|
||||
Marker: marker,
|
||||
}).promise();
|
||||
ContinuationToken: continuationToken,
|
||||
...awsParams,
|
||||
});
|
||||
if (!objects.Contents) {
|
||||
break;
|
||||
}
|
||||
continuationToken = objects.NextContinuationToken;
|
||||
|
||||
const toRemove = [];
|
||||
|
||||
for (const object of objects.Contents) {
|
||||
overall++;
|
||||
marker = object.Key;
|
||||
|
||||
if (object.LastModified > new Date() - 60 * 60 * 1000) {
|
||||
if (object.LastModified!.getTime() > new Date().getTime() - 60 * 60 * 1000) {
|
||||
fresh++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const id = object.Key.endsWith('-dark.png')
|
||||
? object.Key.match('card/[^/]+/.+-([^-]+)-dark\.png')[1]
|
||||
: object.Key.match('card/[^/]+/.+-([^-]+)\.png')[1];
|
||||
const id = object.Key!.endsWith('-dark.png')
|
||||
? object.Key!.match('card/[^/]+/.+-([^-]+)-dark\.png')![1]
|
||||
: object.Key!.match('card/[^/]+/.+-([^-]+)\.png')![1];
|
||||
|
||||
if (!cards[id]) {
|
||||
console.log(`REMOVING: ${object.Key}`);
|
||||
toRemove.push({ Key: object.Key });
|
||||
toRemove.push({ Key: object.Key! });
|
||||
removed += 1;
|
||||
removedSize += object.Size;
|
||||
removedSize += object.Size!;
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,7 +202,8 @@ async function cleanup() {
|
||||
Delete: {
|
||||
Objects: toRemove,
|
||||
},
|
||||
}).promise();
|
||||
...awsParams,
|
||||
});
|
||||
}
|
||||
|
||||
if (objects.Contents.length < chunkSize) {
|
@ -33,10 +33,10 @@ import sourcesRoute from './routes/sources.js';
|
||||
import nounsRoute from './routes/nouns.js';
|
||||
import inclusiveRoute from './routes/inclusive.js';
|
||||
import termsRoute from './routes/terms.js';
|
||||
import pronounceRoute from './routes/pronounce.js';
|
||||
import pronounceRoute from './routes/pronounce.ts';
|
||||
import censusRoute from './routes/census.js';
|
||||
import namesRoute from './routes/names.js';
|
||||
import imagesRoute from './routes/images.js';
|
||||
import imagesRoute from './routes/images.ts';
|
||||
import blogRoute from './routes/blog.js';
|
||||
import calendarRoute from './routes/calendar.js';
|
||||
import translationsRoute from './routes/translations.ts';
|
||||
|
@ -2,6 +2,7 @@ import { Router } from 'express';
|
||||
import { ulid } from 'ulid';
|
||||
import multer from 'multer';
|
||||
import { loadImage, createCanvas } from 'canvas';
|
||||
import type { Canvas } from 'canvas';
|
||||
import { handleErrorAsync } from '../../src/helpers.ts';
|
||||
import sharp from 'sharp';
|
||||
import fs from 'fs';
|
||||
@ -9,19 +10,25 @@ import SQL from 'sql-template-strings';
|
||||
import path from 'path';
|
||||
import auditLog from '../audit.ts';
|
||||
|
||||
import awsConfig from '../aws.js';
|
||||
import S3 from 'aws-sdk/clients/s3.js';
|
||||
import { awsConfig, awsParams } from '../aws.ts';
|
||||
import { S3 } from '@aws-sdk/client-s3';
|
||||
|
||||
const sizes = {
|
||||
big: [1200, false],
|
||||
flag: [256, false],
|
||||
thumb: [192, true],
|
||||
avatar: [144, true],
|
||||
};
|
||||
} as const;
|
||||
|
||||
const resizeImage = (image, width, height, sx = null, sy = null) => {
|
||||
const resizeImage = (
|
||||
image: Canvas,
|
||||
width: number,
|
||||
height: number,
|
||||
sx: number | null = null,
|
||||
sy: number | null = null,
|
||||
): Canvas => {
|
||||
const canvas = createCanvas(width, height);
|
||||
if (sx === null) {
|
||||
if (sx === null || sy === null) {
|
||||
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
|
||||
} else {
|
||||
canvas.getContext('2d').drawImage(image, sx, sy, width, height, 0, 0, width, height);
|
||||
@ -30,13 +37,13 @@ const resizeImage = (image, width, height, sx = null, sy = null) => {
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const cropToSquare = (image) => {
|
||||
const cropToSquare = (image: Canvas): Canvas => {
|
||||
return image.width > image.height
|
||||
? resizeImage(image, image.height, image.height, (image.width - image.height) / 2, 0)
|
||||
: resizeImage(image, image.width, image.width, 0, (image.height - image.width) / 2);
|
||||
};
|
||||
|
||||
const scaleDownTo = (image, size) => {
|
||||
const scaleDownTo = (image: Canvas, size: number): Canvas => {
|
||||
if (image.width > image.height) {
|
||||
return image.width > size
|
||||
? resizeImage(image, size, image.height * size / image.width)
|
||||
@ -50,15 +57,15 @@ const scaleDownTo = (image, size) => {
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/images/upload', multer({ limits: { fileSize: 10 * 1024 * 1024 } }).any('images[]', 12), handleErrorAsync(async (req, res) => {
|
||||
router.post('/images/upload', multer({ limits: { fileSize: 10 * 1024 * 1024 } }).any(), handleErrorAsync(async (req, res) => {
|
||||
const s3 = new S3(awsConfig);
|
||||
|
||||
const requestedSizes = req.query.sizes === undefined || req.query.sizes === 'all'
|
||||
? null
|
||||
: req.query.sizes.split(',');
|
||||
: (req.query.sizes as string).split(',');
|
||||
|
||||
const ids = [];
|
||||
for (const file of req.files) {
|
||||
for (const file of req.files as Express.Multer.File[]) {
|
||||
const id = ulid();
|
||||
let image;
|
||||
try {
|
||||
@ -68,15 +75,10 @@ router.post('/images/upload', multer({ limits: { fileSize: 10 * 1024 * 1024 } })
|
||||
return res.status(400).json({ error: 'error.invalidImage' });
|
||||
}
|
||||
|
||||
for (const s in sizes) {
|
||||
if (!sizes.hasOwnProperty(s)) {
|
||||
for (const [name, [size, square]] of Object.entries(sizes)) {
|
||||
if (requestedSizes !== null && !requestedSizes.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
if (requestedSizes !== null && !requestedSizes.includes(s)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [size, square] = sizes[s];
|
||||
|
||||
let canvas = createCanvas(image.width, image.height);
|
||||
canvas.getContext('2d').drawImage(image, 0, 0);
|
||||
@ -88,11 +90,12 @@ router.post('/images/upload', multer({ limits: { fileSize: 10 * 1024 * 1024 } })
|
||||
canvas = scaleDownTo(canvas, size);
|
||||
|
||||
await s3.putObject({
|
||||
Key: `images/${id}-${s}.png`,
|
||||
Key: `images/${id}-${name}.png`,
|
||||
Body: canvas.toBuffer('image/png'),
|
||||
ContentType: 'image/png',
|
||||
ACL: 'public-read',
|
||||
}).promise();
|
||||
...awsParams,
|
||||
});
|
||||
}
|
||||
|
||||
ids.push(id);
|
||||
@ -122,9 +125,12 @@ router.get('/downloads', handleErrorAsync(async (req, res) => {
|
||||
return res.status(401).json({ error: 'Unauthorised' });
|
||||
}
|
||||
|
||||
const stats = {};
|
||||
const stats: Record<string, number> = {};
|
||||
|
||||
for (const { filename, c } of await req.db.all(SQL`SELECT filename, count(*) as c FROM downloads WHERE locale = ${global.config.locale} GROUP BY filename;`)) {
|
||||
const downloads = await req.db.all<{ filename: string, c: number }>(SQL`
|
||||
SELECT filename, count(*) as c FROM downloads WHERE locale = ${global.config.locale} GROUP BY filename;
|
||||
`);
|
||||
for (const { filename, c } of downloads) {
|
||||
stats[filename] = c;
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ import { normaliseUrl } from '../../src/links.js';
|
||||
import allLocales from '../../locale/locales.ts';
|
||||
import auditLog from '../audit.ts';
|
||||
import crypto from '../../src/crypto.js';
|
||||
import awsConfig from '../aws.js';
|
||||
import S3 from 'aws-sdk/clients/s3.js';
|
||||
import { awsConfig, awsParams } from '../aws.ts';
|
||||
import { S3, NoSuchKey } from '@aws-sdk/client-s3';
|
||||
import zlib from 'zlib';
|
||||
import multer from 'multer';
|
||||
|
||||
@ -981,7 +981,8 @@ router.get('/profile/export', handleErrorAsync(async (req, res) => {
|
||||
try {
|
||||
const data = await s3.getObject({
|
||||
Key: `images/${id}-flag.png`,
|
||||
}).promise();
|
||||
...awsParams,
|
||||
});
|
||||
images[id] = {
|
||||
flag: data.Body.toString('base64'),
|
||||
};
|
||||
@ -1037,16 +1038,22 @@ router.post('/profile/import', multer({ limits: { fileSize: 10 * 1024 * 1024 } }
|
||||
try {
|
||||
await s3.headObject({
|
||||
Key: `images/${id}-${size}.png`,
|
||||
}).promise();
|
||||
...awsParams,
|
||||
});
|
||||
continue;
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
if (!(error instanceof NoSuchKey)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await s3.putObject({
|
||||
Key: `images/${id}-${size}.png`,
|
||||
Body: Buffer.from(content, 'base64'),
|
||||
ContentType: 'image/png',
|
||||
ACL: 'public-read',
|
||||
}).promise();
|
||||
...awsParams,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,10 @@ import sha1 from 'sha1';
|
||||
import { convertPronunciationStringToSsml, handleErrorAsync } from '../../src/helpers.ts';
|
||||
import { Base64 } from 'js-base64';
|
||||
|
||||
import awsConfig from '../aws.js';
|
||||
import Polly from 'aws-sdk/clients/polly.js';
|
||||
import S3 from 'aws-sdk/clients/s3.js';
|
||||
import { awsConfig, awsParams } from '../aws.ts';
|
||||
import { Polly } from '@aws-sdk/client-polly';
|
||||
import { S3, NoSuchKey } from '@aws-sdk/client-s3';
|
||||
import type { NodeJsClient } from '@smithy/types';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -16,21 +17,25 @@ router.get('/pronounce/:voice/:pronunciation', handleErrorAsync(async (req, res)
|
||||
return res.status(404).json({ error: 'Not found' });
|
||||
}
|
||||
|
||||
const voice = global.config.pronunciation.voices[req.params.voice];
|
||||
const voice = global.config.pronunciation?.voices?.[req.params.voice];
|
||||
if (!voice) {
|
||||
return res.status(404).json({ error: 'Not found' });
|
||||
}
|
||||
|
||||
const s3 = new S3(awsConfig);
|
||||
const polly = new Polly(awsConfig);
|
||||
const s3 = new S3(awsConfig) as NodeJsClient<S3>;
|
||||
const polly = new Polly(awsConfig) as NodeJsClient<Polly>;
|
||||
|
||||
const ssml = convertPronunciationStringToSsml(text);
|
||||
const key = `pronunciation/${global.config.locale}-${req.params.voice}/${sha1(ssml)}.mp3`;
|
||||
|
||||
try {
|
||||
const s3getResponse = await s3.getObject({ Key: key }).promise();
|
||||
return res.set('content-type', s3getResponse.ContentType).send(s3getResponse.Body);
|
||||
} catch {
|
||||
const s3Response = await s3.getObject({ Key: key, ...awsParams });
|
||||
res.set('content-type', s3Response.ContentType);
|
||||
return s3Response.Body!.pipe(res);
|
||||
} catch (error) {
|
||||
if (!(error instanceof NoSuchKey)) {
|
||||
throw error;
|
||||
}
|
||||
const pollyResponse = await polly.synthesizeSpeech({
|
||||
TextType: 'ssml',
|
||||
Text: ssml,
|
||||
@ -38,15 +43,18 @@ router.get('/pronounce/:voice/:pronunciation', handleErrorAsync(async (req, res)
|
||||
LanguageCode: voice.language,
|
||||
VoiceId: voice.voice,
|
||||
Engine: voice.engine,
|
||||
}).promise();
|
||||
|
||||
});
|
||||
const buffer = await pollyResponse.AudioStream!.transformToByteArray();
|
||||
await s3.putObject({
|
||||
Key: key,
|
||||
Body: pollyResponse.AudioStream,
|
||||
Body: buffer,
|
||||
ContentType: pollyResponse.ContentType,
|
||||
}).promise();
|
||||
...awsParams,
|
||||
});
|
||||
|
||||
return res.set('content-type', pollyResponse.ContentType).send(pollyResponse.AudioStream);
|
||||
res.set('content-type', pollyResponse.ContentType);
|
||||
res.write(buffer);
|
||||
return res.end();
|
||||
}
|
||||
}));
|
||||
|
@ -14,7 +14,7 @@ import assert from 'assert';
|
||||
import { addMfaInfo } from './mfa.js';
|
||||
import buildLocaleList from '../../src/buildLocaleList.ts';
|
||||
import { lookupBanArchive } from '../ban.js';
|
||||
import copyAvatar from '../avatarCopy.js';
|
||||
import copyAvatar from '../avatarCopy.ts';
|
||||
import { usernameRegex, usernameUnsafeRegex } from '../../src/username.ts';
|
||||
const config = loadSuml('config');
|
||||
import auditLog from '../audit.ts';
|
||||
|
4
server/sha1.d.ts
vendored
Normal file
4
server/sha1.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module 'sha1' {
|
||||
const sha1: (message: string) => string;
|
||||
export = sha1;
|
||||
}
|
@ -258,7 +258,7 @@ export const isGranted = (user: Pick<User, 'roles'>, locale: string | null, area
|
||||
return false;
|
||||
};
|
||||
|
||||
type ErrorAsyncFunction = (req: Request, res: Response, next?: NextFunction) => Promise<Response>;
|
||||
type ErrorAsyncFunction = (req: Request, res: Response, next?: NextFunction) => Promise<unknown>;
|
||||
export const handleErrorAsync = (func: ErrorAsyncFunction) => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
func(req, res, next).catch((error: unknown) => next(error));
|
||||
|
Loading…
x
Reference in New Issue
Block a user