Skip to content

Commit

Permalink
Merge pull request #1 from codante-io/save-avatars-from-github
Browse files Browse the repository at this point in the history
Add new route for uploading avatar images
  • Loading branch information
robertotcestari authored Aug 8, 2024
2 parents 04116ca + b16c253 commit 63d4043
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { bearerAuth } from 'hono/bearer-auth';
import screenshot from './routes/screenshot';
import vimeoVideo from './routes/vimeo-video';
import uploadImage from './routes/upload-image';
import uploadAvatarImage from './routes/upload-avatar-image';

const app = new Hono();

Expand All @@ -11,6 +12,7 @@ app.use('*', bearerAuth({ token: process.env.TOKEN! }));
app.route('/screenshot', screenshot);
// app.route('/vimeo-video', vimeoVideo);
app.route('/upload-image', uploadImage);
app.route('/upload-avatar-image', uploadAvatarImage);

export default {
port: 3012,
Expand Down
4 changes: 3 additions & 1 deletion src/lib/hono-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export function imageRequestValidator() {
const parsed = imageRequestSchema.safeParse(value);
if (!parsed.success) {
return c.json(
{ message: 'Invalid Data. You need to pass `submission_image`' },
{
message: parsed.error.errors,
},
400
);
}
Expand Down
63 changes: 50 additions & 13 deletions src/lib/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,63 @@ import {
S3Client,
} from '@aws-sdk/client-s3';
import { fromEnv } from '@aws-sdk/credential-provider-env';
import { nanoid } from 'nanoid';

export type imgMimes =
| 'image/jpeg'
| 'image/jpg'
| 'image/png'
| 'image/webp'
| 'image/avif';

interface UploadParams {
path: string;
buffer: Buffer;
contentType?: imgMimes;
}

export class S3 {
private client: S3Client;
private bucketName: string;
private assetsBaseUrl: string;

constructor() {
const bucketName = process.env.AWS_BUCKET_NAME;
const assetsBaseUrl = process.env.CODANTE_ASSETS_BASE_URL;

if (!bucketName || !assetsBaseUrl) {
throw new Error(
'AWS_BUCKET_NAME and CODANTE_ASSETS_BASE_URL must be defined.'
);
}

this.client = new S3Client({
credentials: fromEnv(),
region: process.env.AWS_REGION ?? 'sa-east-1',
});

this.bucketName = bucketName;
this.assetsBaseUrl = assetsBaseUrl;
}

// this method uploads the webp image to the S3 bucket and returns the URL
async uploadImage(
imagePath: null | string = null,
buffer: Buffer
): Promise<{ imageUrl: string }> {
// Add .webp extension to the file name if it doesn't have it
if (imagePath && !imagePath.endsWith('.webp')) imagePath += '.webp';
async uploadImage({
path,
buffer,
contentType = 'image/webp',
}: UploadParams): Promise<{ imageUrl: string }> {
// check if the path is valid
this.checkImagePath(path);

const params = {
Bucket: process.env.AWS_BUCKET_NAME ?? '',
Key: imagePath ? `${imagePath}` : `${nanoid()}.webp`,
Key: path,
Body: buffer,
ContentType: 'image/webp',
ContentType: contentType,
};

try {
await this.client.send(new PutObjectCommand(params));

// get the URL of the uploaded image
const imageUrl = `${process.env.CODANTE_ASSETS_BASE_URL}/${params.Key}`;
return { imageUrl };
return { imageUrl: `${this.assetsBaseUrl}/${path}` };
} catch (e: any) {
e.message = `S3 upload error: ${e.message}`;
throw e;
Expand Down Expand Up @@ -89,4 +113,17 @@ export class S3 {
return imageUrl; // Return the original if not a valid URL
}
}

private checkImagePath(path: string): void {
if (!path) {
throw new Error('Image path is required');
}

const validExtensions = /\.(avif|webp|jpg|jpeg|png)$/i;
if (!validExtensions.test(path)) {
throw new Error(
'Invalid image path. Must end with a valid image extension (avif, webp, jpg, jpeg, png, gif).'
);
}
}
}
10 changes: 8 additions & 2 deletions src/routes/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ app.post('/', screenshotValidator(), async (c) => {
try {
const screenshotBuffer = await getBufferFromPageScreenshot(url);
const s3 = new S3();
const { imageUrl } = await s3.uploadImage(fileName, screenshotBuffer);
const { imageUrl } = await s3.uploadImage({
path: fileName,
buffer: screenshotBuffer,
});

return c.json({ message: 'Screenshot uploaded to S3', imageUrl });
} catch (e: any) {
Expand All @@ -28,7 +31,10 @@ app.put('/', screenshotPutValidator(), async (c) => {
try {
const screenshotBuffer = await getBufferFromPageScreenshot(url);
const s3 = new S3();
const { imageUrl } = await s3.uploadImage(fileName, screenshotBuffer);
const { imageUrl } = await s3.uploadImage({
path: fileName,
buffer: screenshotBuffer,
});

// delete the old image
await s3.deleteImage(oldFilename);
Expand Down
64 changes: 64 additions & 0 deletions src/routes/upload-avatar-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Hono } from 'hono';
import sharp from 'sharp';
import { S3 } from '../lib/s3';

const app = new Hono();

app.post('/', async (c) => {
// get json data
const json = await c.req.json();
const avatarUrl = json.avatar_url;
const email = json.email;

if (!avatarUrl || !email) {
return c.json(
{ message: 'Invalid Data. You need to pass `avatar_url` and `user_id`' },
400
);
}

const encodedEmail = Buffer.from(email).toString('base64');

const smImgPath = `user-avatars/${encodedEmail}.avif`;
const lgImgPath = `user-avatars/${encodedEmail}-lg.avif`;

const res = await fetch(avatarUrl);
const buffer = Buffer.from(await res.arrayBuffer());

// resize image
const lgImgBuffer = await sharp(buffer)
.resize(800, 800, { fit: 'cover', withoutEnlargement: true })
.avif({ quality: 80 })
.toBuffer();

// resize image
const smImgBuffer = await sharp(buffer)
.resize(200, 200, { fit: 'cover', withoutEnlargement: true })
.avif({ quality: 80 })
.toBuffer();

try {
const s3 = new S3();
const { imageUrl: lgImageUrl } = await s3.uploadImage({
path: lgImgPath,
buffer: lgImgBuffer,
contentType: 'image/avif',
});

const { imageUrl: smImageUrl } = await s3.uploadImage({
path: smImgPath,
buffer: smImgBuffer,
contentType: 'image/avif',
});

return c.json({
message: 'Screenshot uploaded to S3',
lgImageUrl,
smImageUrl,
});
} catch (e: any) {
return c.json({ message: e.message }, 500);
}
});

export default app;
9 changes: 5 additions & 4 deletions src/routes/upload-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ app.post('/', imageRequestValidator(), async (c) => {

try {
const s3 = new S3();
const { imageUrl } = await s3.uploadImage(
submission_path,
resizedImageBuffer
);
const { imageUrl } = await s3.uploadImage({
path: submission_path,
buffer: resizedImageBuffer,
contentType: 'image/webp',
});

return c.json({ message: 'Screenshot uploaded to S3', imageUrl });
} catch (e: any) {
Expand Down

0 comments on commit 63d4043

Please sign in to comment.