Skip to content

Commit

Permalink
feat: implement digital ocean spaces s3 client storage and cache imag…
Browse files Browse the repository at this point in the history
…es 3 days
  • Loading branch information
0-vortex committed Apr 5, 2023
1 parent 3d9f0c9 commit 792992e
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 18 deletions.
10 changes: 5 additions & 5 deletions src/config/digital-ocean.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { registerAs } from "@nestjs/config";

const DigitalOceanConfig = registerAs("digitalOcean", () => ({
endpoint: String(process.env.DIGITAL_OCEAN_ENDPOINT ?? "https://sfo3.digitaloceanspaces.com"),
region: String(process.env.DIGITAL_OCEAN_REGION ?? "us-east-1"),
accessKeyId: String(process.env.DIGITAL_OCEAN_ACCESS_KEY_ID ?? ""),
secretAccessKey: String(process.env.DIGITAL_OCEAN_SECRET_ACCESS_KEY ?? ""),
bucketName: String(process.env.DIGITAL_OCEAN_BUCKET_NAME ?? "opengraph-dev"),
endpoint: String(process.env.DO_SPACES_ENDPOINT ?? "https://sfo3.digitaloceanspaces.com"),
region: String(process.env.DO_SPACES_REGION ?? "us-east-1"),
accessKeyId: String(process.env.DO_SPACES_ACCESS_KEY_ID ?? ""),
secretAccessKey: String(process.env.DO_SPACES_SECRET_ACCESS_KEY ?? ""),
bucketName: String(process.env.DO_SPACES_BUCKET_NAME ?? "opengraph-dev"),
}));

export default DigitalOceanConfig;
1 change: 1 addition & 0 deletions src/github/gql/get-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const getUser = (username: string, dateSince: string) => ({
query ($username: String!, $dateSince: DateTime) {
user(login: $username) {
id
databaseId
avatarUrl
bio
bioHTML
Expand Down
13 changes: 8 additions & 5 deletions src/s3-file-storage/s3-file-storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import DigitalOceanConfig from "../config/digital-ocean.config";

@Injectable()
export class S3FileStorageService {
private s3Client: S3Client;
private readonly s3Client: S3Client;

constructor (
@Inject(DigitalOceanConfig.KEY)
Expand All @@ -25,6 +25,10 @@ export class S3FileStorageService {
});
}

generateFileUrl (hash: string): string {
return `https://${this.config.bucketName}.${this.config.endpoint.replace(/https?:\/\//, "")}/${hash}`;
}

async fileExists (hash: string): Promise<boolean> {
try {
await this.s3Client.send(
Expand All @@ -35,10 +39,8 @@ export class S3FileStorageService {
);
return true;
} catch (error) {
console.error(error);

if (error instanceof Error) {
if (error.name === "ResourceNotFoundException") {
if (error.name === "NotFound") {
return false;
}
}
Expand All @@ -61,7 +63,7 @@ export class S3FileStorageService {
console.error(error);

if (error instanceof Error) {
if (error.name === "ResourceNotFoundException") {
if (error.name === "NotFound") {
return null;
}
}
Expand All @@ -81,6 +83,7 @@ export class S3FileStorageService {
Key: hash,
Body: fileContent,
ContentType: contentType,
ACL: "public-read",
}),
);
}
Expand Down
9 changes: 5 additions & 4 deletions src/social-card/social-card.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, Header, Param, StreamableFile } from "@nestjs/common";
import { Controller, Get, Header, Param, Redirect, StreamableFile } from "@nestjs/common";
import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger";

import { SocialCardService } from "./social-card.service";
Expand All @@ -18,11 +18,12 @@ export class SocialCardController {
@Header("Content-Type", "image/png")
@ApiOkResponse({ type: StreamableFile })
@ApiNotFoundResponse({ description: "User not found" })
@Redirect()
async generateUserSocialCard (
@Param("username") username: string,
): Promise<StreamableFile> {
const image = await this.socialCardService.getUserCard(username);
): Promise<{ url: string }> {
const url = await this.socialCardService.getUserCard(username);

return new StreamableFile(image);
return { url };
}
}
31 changes: 27 additions & 4 deletions src/social-card/social-card.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from "@nestjs/common";
import { Injectable, Logger } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { Resvg } from "@resvg/resvg-js";
import { Repository, Language, User } from "@octokit/graphql-schema";
Expand All @@ -12,13 +12,16 @@ import { S3FileStorageService } from "../s3-file-storage/s3-file-storage.service

@Injectable()
export class SocialCardService {
private readonly logger = new Logger(this.constructor.name);

constructor (
private readonly httpService: HttpService,
private readonly githubService: GithubService,
private readonly s3FileStorageService: S3FileStorageService,
) {}

async getUserData (username: string): Promise<{
id: User["databaseId"],
name: User["name"],
langs: (Language & {
size: number,
Expand Down Expand Up @@ -54,6 +57,7 @@ export class SocialCardService {
});

return {
id: user.databaseId,
name: user.name,
langs: Array.from(Object.values(langs)),
langTotal,
Expand All @@ -62,7 +66,7 @@ export class SocialCardService {
};
}

async getUserCard (username: string): Promise<Buffer> {
async getUserCard (username: string): Promise<string> {
const { remaining } = await this.githubService.rateLimit();

if (remaining < 1000) {
Expand All @@ -72,7 +76,22 @@ export class SocialCardService {
const { html } = await import("satori-html");
const satori = (await import("satori")).default;

const { name, avatarUrl, repos, langs, langTotal } = await this.getUserData(username);
const { id, name, avatarUrl, repos, langs, langTotal } = await this.getUserData(username);
const hash = `users/${String(id)}.png`;
const fileUrl = this.s3FileStorageService.generateFileUrl(hash);
const hasFile = await this.s3FileStorageService.fileExists(hash);
const today = (new Date);
const today3daysAgo = new Date((new Date).setDate(today.getDate() - 3));

if (hasFile) {
// route to s3
const lastModified = await this.s3FileStorageService.getFileLastModified(hash);

if (lastModified && lastModified > today3daysAgo) {
this.logger.debug(`User ${username} exists in S3 with lastModified: ${lastModified.toISOString()} higher than 3 days ago, redirecting`);
return fileUrl;
}
}

const template = html(userProfileCard(avatarUrl, name!, userLangs(langs, langTotal), userProfileRepos(repos)));

Expand All @@ -97,6 +116,10 @@ export class SocialCardService {

const pngBuffer = pngData.asPng();

return pngBuffer;
await this.s3FileStorageService.uploadFile(pngBuffer, hash, "image/png");

this.logger.debug(`User ${username} did not exist in S3, generated image and uploaded to S3, redirecting`);

return fileUrl;
}
}

0 comments on commit 792992e

Please sign in to comment.