Add a mediaController for s3 uploads

This commit is contained in:
Alex Gleason 2023-09-06 17:55:46 -05:00
parent 4b4bfd48c7
commit 3fc60c78d2
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
5 changed files with 91 additions and 5 deletions

View File

@ -1,4 +1,4 @@
import { dotenv, getPublicKey, nip19, secp } from '@/deps.ts'; import { dotenv, getPublicKey, nip19, secp, z } from '@/deps.ts';
/** Load environment config from `.env` */ /** Load environment config from `.env` */
await dotenv.load({ await dotenv.load({
@ -7,6 +7,16 @@ await dotenv.load({
examplePath: null, examplePath: null,
}); });
const optionalBooleanSchema = z
.enum(['true', 'false'])
.optional()
.transform((value) => value !== undefined ? value === 'true' : undefined);
const optionalNumberSchema = z
.string()
.optional()
.transform((value) => value !== undefined ? Number(value) : undefined);
/** Application-wide configuration. */ /** Application-wide configuration. */
const Conf = { const Conf = {
/** Ditto admin secret key in nip19 format. This is the way it's configured by an admin. */ /** Ditto admin secret key in nip19 format. This is the way it's configured by an admin. */
@ -58,6 +68,36 @@ const Conf = {
get adminEmail() { get adminEmail() {
return Deno.env.get('ADMIN_EMAIL') || 'webmaster@localhost'; return Deno.env.get('ADMIN_EMAIL') || 'webmaster@localhost';
}, },
/** S3 media storage configuration. */
s3: {
get endPoint() {
return Deno.env.get('S3_ENDPOINT')!;
},
get region() {
return Deno.env.get('S3_REGION')!;
},
get accessKey() {
return Deno.env.get('S3_ACCESS_KEY');
},
get secretKey() {
return Deno.env.get('S3_SECRET_KEY');
},
get bucket() {
return Deno.env.get('S3_BUCKET');
},
get pathStyle() {
return optionalBooleanSchema.parse(Deno.env.get('S3_PATH_STYLE'));
},
get port() {
return optionalNumberSchema.parse(Deno.env.get('S3_PORT'));
},
get sessionToken() {
return Deno.env.get('S3_SESSION_TOKEN');
},
get useSSL() {
return optionalBooleanSchema.parse(Deno.env.get('S3_USE_SSL'));
},
},
/** Domain of the Ditto server as a `URL` object, for easily grabbing the `hostname`, etc. */ /** Domain of the Ditto server as a `URL` object, for easily grabbing the `hostname`, etc. */
get url() { get url() {
return new URL(Conf.localDomain); return new URL(Conf.localDomain);

View File

@ -2,7 +2,7 @@ import { type AppController } from '@/app.ts';
import { type Filter, findReplyTag, z } from '@/deps.ts'; import { type Filter, findReplyTag, z } from '@/deps.ts';
import * as mixer from '@/mixer.ts'; import * as mixer from '@/mixer.ts';
import { getAuthor, getFollowedPubkeys, getFollows, syncUser } from '@/queries.ts'; import { getAuthor, getFollowedPubkeys, getFollows, syncUser } from '@/queries.ts';
import { booleanParamSchema } from '@/schema.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { toAccount, toRelationship, toStatus } from '@/transformers/nostr-to-mastoapi.ts'; import { toAccount, toRelationship, toStatus } from '@/transformers/nostr-to-mastoapi.ts';
import { isFollowing, lookupAccount, Time } from '@/utils.ts'; import { isFollowing, lookupAccount, Time } from '@/utils.ts';
@ -113,8 +113,6 @@ const accountStatusesController: AppController = async (c) => {
return paginated(c, events, statuses); return paginated(c, events, statuses);
}; };
const fileSchema = z.custom<File>((value) => value instanceof File);
const updateCredentialsSchema = z.object({ const updateCredentialsSchema = z.object({
display_name: z.string().optional(), display_name: z.string().optional(),
note: z.string().optional(), note: z.string().optional(),

View File

@ -0,0 +1,35 @@
import { AppController } from '@/app.ts';
import { Conf } from '@/config.ts';
import { S3Client, z } from '@/deps.ts';
import { fileSchema } from '@/schema.ts';
import { parseBody } from '@/utils/web.ts';
const s3 = new S3Client({ ...Conf.s3 });
const mediaBodySchema = z.object({
file: fileSchema.refine((file) => !!file.type),
thumbnail: fileSchema.optional(),
description: z.string().optional(),
focus: z.string().optional(),
});
const mediaController: AppController = async (c) => {
const result = mediaBodySchema.safeParse(await parseBody(c.req.raw));
if (!result.success) {
return c.json({ error: 'Bad request.', schema: result.error }, 422);
}
const { file } = result.data;
try {
await s3.putObject('test', file.stream());
} catch (e) {
console.error(e);
return c.json({ error: 'Failed to upload file.' }, 500);
}
return c.json({});
};
export { mediaController };

View File

@ -66,5 +66,6 @@ export {
export { DenoSqliteDialect } from 'https://gitlab.com/soapbox-pub/kysely-deno-sqlite/-/raw/v1.0.1/mod.ts'; export { DenoSqliteDialect } from 'https://gitlab.com/soapbox-pub/kysely-deno-sqlite/-/raw/v1.0.1/mod.ts';
export { default as tldts } from 'npm:tldts@^6.0.14'; export { default as tldts } from 'npm:tldts@^6.0.14';
export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts'; export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts';
export { S3Client } from 'https://deno.land/x/s3_lite_client@0.6.1/mod.ts';
export type * as TypeFest from 'npm:type-fest@^4.3.0'; export type * as TypeFest from 'npm:type-fest@^4.3.0';

View File

@ -48,4 +48,16 @@ const safeUrlSchema = z.string().max(2048).url();
/** https://github.com/colinhacks/zod/issues/1630#issuecomment-1365983831 */ /** https://github.com/colinhacks/zod/issues/1630#issuecomment-1365983831 */
const booleanParamSchema = z.enum(['true', 'false']).transform((value) => value === 'true'); const booleanParamSchema = z.enum(['true', 'false']).transform((value) => value === 'true');
export { booleanParamSchema, decode64Schema, emojiTagSchema, filteredArray, hashtagSchema, jsonSchema, safeUrlSchema }; /** Schema for `File` objects. */
const fileSchema = z.custom<File>((value) => value instanceof File);
export {
booleanParamSchema,
decode64Schema,
emojiTagSchema,
fileSchema,
filteredArray,
hashtagSchema,
jsonSchema,
safeUrlSchema,
};