Skip to content

Commit

Permalink
feat: implement no-content verification strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
0-vortex committed Apr 9, 2023
1 parent feac006 commit 6efe0e6
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 81 deletions.
84 changes: 16 additions & 68 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@resvg/resvg-js": "^2.4.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"fastify": "^4.15.0",
"nestjs-pino": "^3.1.2",
"octokit": "^2.0.14",
"pino-http": "^8.3.3",
Expand All @@ -81,13 +82,12 @@
"@open-sauced/conventional-commit": "^1.0.1",
"@swc/core": "^1.3.32",
"@types/eslint": "^8.21.0",
"@types/express": "^4.17.17",
"@types/jest": "29.5.0",
"@types/node": "18.15.11",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.13",
"@types/source-map-support": "^0.5.6",
"@types/supertest": "^2.0.12",
"@types/validator": "^13.7.11",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"eslint": "^8.37.0",
Expand Down
4 changes: 2 additions & 2 deletions src/s3-file-storage/s3-file-storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ export class S3FileStorageService {

return response.LastModified ?? null;
} catch (error) {
console.error(error);

if (error instanceof Error) {
if (error.name === "NotFound") {
return null;
Expand All @@ -77,6 +75,7 @@ export class S3FileStorageService {
fileContent: Buffer | Readable,
hash: string,
contentType: string,
metadata?: Record<string, string>,
): Promise<void> {
await this.s3Client.send(
new PutObjectCommand({
Expand All @@ -85,6 +84,7 @@ export class S3FileStorageService {
Body: fileContent,
ContentType: contentType,
ACL: "public-read",
Metadata: metadata,
}),
);
}
Expand Down
37 changes: 32 additions & 5 deletions src/social-card/social-card.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Controller, Get, Header, Param, Redirect, StreamableFile } from "@nestjs/common";
import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger";
import { Controller, Get, Head, Header, HttpStatus, Param, Redirect, Res, StreamableFile } from "@nestjs/common";
import {
ApiNoContentResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation, ApiResponse,
ApiTags,
} from "@nestjs/swagger";
import { FastifyReply } from "fastify";

import { SocialCardService } from "./social-card.service";

Expand All @@ -10,20 +17,40 @@ export class SocialCardController {
private readonly socialCardService: SocialCardService,
) {}

@Head("/:username")
@ApiNoContentResponse({ description: "User social card image is up to date", status: HttpStatus.NO_CONTENT })
@ApiResponse({ description: "User social card image needs regeneration", status: HttpStatus.NOT_MODIFIED })
@ApiNotFoundResponse({ description: "User social card image not found", status: HttpStatus.NOT_FOUND })
async checkUserSocialCard (
@Param("username") username: string,
@Res() res: FastifyReply,
): Promise<void> {
const { fileUrl, hasFile, needsUpdate, lastModified } = await this.socialCardService.checkRequiresUpdate(username);

return res
.headers({
"x-amz-meta-last-modified": lastModified?.toISOString() ?? "",
"x-amz-meta-location": fileUrl,
})
.status(hasFile ? needsUpdate ? HttpStatus.NOT_MODIFIED : HttpStatus.NO_CONTENT : HttpStatus.NOT_FOUND)
.send();
}

@Get("/:username")
@ApiOperation({
operationId: "generateUserSocialCard",
summary: "Gets latest cache aware social card link for :username or generates a new one",
})
@Header("Content-Type", "image/png")
@ApiOkResponse({ type: StreamableFile })
@ApiOkResponse({ type: StreamableFile, description: "Social card image" })
@ApiNotFoundResponse({ description: "User not found" })
@Redirect()
async generateUserSocialCard (
@Param("username") username: string,
): Promise<{ url: string }> {
@Res() res: FastifyReply,
): Promise<void> {
const url = await this.socialCardService.getUserCard(username);

return { url };
return res.status(302).redirect(url);
}
}
40 changes: 36 additions & 4 deletions src/social-card/social-card.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import { GithubService } from "../github/github.service";
import { S3FileStorageService } from "../s3-file-storage/s3-file-storage.service";
import tailwindConfig from "./templates/tailwind.config";

interface RequiresUpdateMeta {
fileUrl: string,
hasFile: boolean;
needsUpdate: boolean;
lastModified: Date | null,
}

@Injectable()
export class SocialCardService {
private readonly logger = new Logger(this.constructor.name);
Expand Down Expand Up @@ -67,6 +74,32 @@ export class SocialCardService {
};
}

async checkRequiresUpdate (username: string): Promise<RequiresUpdateMeta> {
const hash = `users/${String(username)}.png`;
const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`;
const hasFile = await this.s3FileStorageService.fileExists(hash);
const today3daysAgo = new Date((new Date).setDate((new Date).getDate() - 0.001));
const returnVal: RequiresUpdateMeta = {
fileUrl,
hasFile,
needsUpdate: true,
lastModified: null,
};

if (hasFile) {
const lastModified = await this.s3FileStorageService.getFileLastModified(hash);

returnVal.lastModified = lastModified;

if (lastModified && lastModified > today3daysAgo) {
this.logger.debug(`User ${username} exists in S3 with lastModified: ${lastModified.toISOString()} less than 3 days ago, redirecting to ${fileUrl}`);
returnVal.needsUpdate = true;
}
}

return returnVal;
}

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

Expand All @@ -81,15 +114,14 @@ export class SocialCardService {
const hash = `users/${String(id)}.png`;
const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`;
const hasFile = await this.s3FileStorageService.fileExists(hash);
const today = (new Date);
const today3daysAgo = new Date((new Date).setDate(today.getDate() - 3));
const today3daysAgo = new Date((new Date).setDate((new Date).getDate() - 0.001));

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`);
this.logger.debug(`User ${username} exists in S3 with lastModified: ${lastModified.toISOString()} less than 3 days ago, redirecting`);
return fileUrl;
}
}
Expand Down Expand Up @@ -118,7 +150,7 @@ export class SocialCardService {

const pngBuffer = pngData.asPng();

await this.s3FileStorageService.uploadFile(pngBuffer, hash, "image/png");
await this.s3FileStorageService.uploadFile(pngBuffer, hash, "image/png", { "x-amz-meta-user-id": String(id) });

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

Expand Down

0 comments on commit 6efe0e6

Please sign in to comment.