93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
|
import { BaseService } from "./baseService.server";
|
|
import { env } from "~/env.server";
|
|
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
|
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
import { customAlphabet } from "nanoid";
|
|
import { errAsync, fromPromise } from "neverthrow";
|
|
|
|
const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 24);
|
|
const objectStoreClient =
|
|
env.ARTIFACTS_OBJECT_STORE_ACCESS_KEY_ID &&
|
|
env.ARTIFACTS_OBJECT_STORE_SECRET_ACCESS_KEY &&
|
|
env.ARTIFACTS_OBJECT_STORE_BASE_URL
|
|
? new S3Client({
|
|
credentials: {
|
|
accessKeyId: env.ARTIFACTS_OBJECT_STORE_ACCESS_KEY_ID,
|
|
secretAccessKey: env.ARTIFACTS_OBJECT_STORE_SECRET_ACCESS_KEY,
|
|
},
|
|
region: env.ARTIFACTS_OBJECT_STORE_REGION,
|
|
endpoint: env.ARTIFACTS_OBJECT_STORE_BASE_URL,
|
|
forcePathStyle: true,
|
|
})
|
|
: new S3Client();
|
|
|
|
const artifactKeyPrefixByType = {
|
|
deployment_context: "deployments",
|
|
} as const;
|
|
const artifactBytesSizeLimitByType = {
|
|
deployment_context: 100 * 1024 * 1024, // 100MB
|
|
} as const;
|
|
|
|
export class ArtifactsService extends BaseService {
|
|
private readonly bucket = env.ARTIFACTS_OBJECT_STORE_BUCKET;
|
|
|
|
public createArtifact(
|
|
type: "deployment_context",
|
|
authenticatedEnv: AuthenticatedEnvironment,
|
|
contentLength?: number
|
|
) {
|
|
const limit = artifactBytesSizeLimitByType[type];
|
|
|
|
// this is just a validation using client-side data
|
|
// the actual limit will be enforced by S3
|
|
if (contentLength && contentLength > limit) {
|
|
return errAsync({
|
|
type: "artifact_size_exceeds_limit" as const,
|
|
contentLength,
|
|
sizeLimit: limit,
|
|
});
|
|
}
|
|
|
|
const uniqueId = nanoid();
|
|
const key = `${artifactKeyPrefixByType[type]}/${authenticatedEnv.project.externalRef}/${authenticatedEnv.slug}/${uniqueId}.tar.gz`;
|
|
|
|
return this.createPresignedPost(key, limit, contentLength).map((result) => ({
|
|
artifactKey: key,
|
|
uploadUrl: result.url,
|
|
uploadFields: result.fields,
|
|
expiresAt: result.expiresAt,
|
|
}));
|
|
}
|
|
|
|
private createPresignedPost(key: string, sizeLimit: number, contentLength?: number) {
|
|
if (!this.bucket) {
|
|
return errAsync({
|
|
type: "artifacts_bucket_not_configured" as const,
|
|
});
|
|
}
|
|
|
|
const ttlSeconds = 300; // 5 minutes
|
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
|
|
|
|
return fromPromise(
|
|
createPresignedPost(objectStoreClient, {
|
|
Bucket: this.bucket,
|
|
Key: key,
|
|
Conditions: [["content-length-range", 0, sizeLimit]],
|
|
Fields: {
|
|
"Content-Type": "application/gzip",
|
|
},
|
|
Expires: ttlSeconds,
|
|
}),
|
|
(error) => ({
|
|
type: "failed_to_create_presigned_post" as const,
|
|
cause: error,
|
|
})
|
|
).map((result) => ({
|
|
...result,
|
|
expiresAt,
|
|
}));
|
|
}
|
|
}
|