Skip to main content
Glama
storage.ts4.04 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { CopyObjectCommand, GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/cloudfront-signer'; import { Upload } from '@aws-sdk/lib-storage'; import { getSignedUrl as s3GetSignedUrl } from '@aws-sdk/s3-request-presigner'; import { concatUrls } from '@medplum/core'; import type { Binary } from '@medplum/fhirtypes'; import type { Readable } from 'node:stream'; import { getConfig } from '../../config/loader'; import { BaseBinaryStorage } from '../../storage/base'; import type { BinarySource } from '../../storage/types'; /** * The S3Storage class stores binary data in an AWS S3 bucket. * Files are stored in bucket/binary/binary.id/binary.meta.versionId. */ export class S3Storage extends BaseBinaryStorage { private readonly client: S3Client; readonly bucket: string; constructor(bucket: string) { super(); this.client = new S3Client({ region: getConfig().awsRegion }); this.bucket = bucket; } /** * Writes a file to S3. * * Early implementations used the simple "PutObjectCommand" to write the blob to S3. * However, PutObjectCommand does not support streaming. * * We now use the `@aws-sdk/lib-storage` package. * * Learn more: * https://github.com/aws/aws-sdk-js-v3/blob/main/UPGRADING.md#s3-multipart-upload * https://github.com/aws/aws-sdk-js-v3/tree/main/lib/lib-storage * * Be mindful of Cache-Control settings. * * Because we use signed URLs intended for one hour use, * we set "max-age" to 1 hour = 3600 seconds. * * But we want CloudFront to cache the response for 1 day, * so we set "s-maxage" to 1 day = 86400 seconds. * * Learn more: * https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html * @param key - The S3 key. * @param contentType - Optional binary content type. * @param stream - The Node.js stream of readable content. */ async writeFile(key: string, contentType: string | undefined, stream: BinarySource): Promise<void> { const upload = new Upload({ params: { Bucket: this.bucket, Key: key, CacheControl: 'max-age=3600, s-maxage=86400', ContentType: contentType ?? 'application/octet-stream', Body: stream, }, client: this.client, queueSize: 3, }); await upload.done(); } async readFile(key: string): Promise<Readable> { const output = await this.client.send( new GetObjectCommand({ Bucket: this.bucket, Key: key, }) ); return output.Body as Readable; } async copyFile(sourceKey: string, destinationKey: string): Promise<void> { await this.client.send( new CopyObjectCommand({ CopySource: `${this.bucket}/${sourceKey}`, Bucket: this.bucket, Key: destinationKey, }) ); } /** * Returns a presigned URL for the Binary resource content. * * Reference: * https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_cloudfront_signer.html * * @param binary - Binary resource. * @returns Presigned URL to access the binary data. */ async getPresignedUrl(binary: Binary): Promise<string> { const config = getConfig(); if (!config.signingKey || !config.signingKeyId) { const Key = this.getKey(binary); return s3GetSignedUrl(this.client, new GetObjectCommand({ Bucket: this.bucket, Key }), { expiresIn: 3600 }); } const storageBaseUrl = config.storageBaseUrl; const unsignedUrl = concatUrls(storageBaseUrl, `${binary.id}/${binary.meta?.versionId}`); const dateLessThan = new Date(); dateLessThan.setHours(dateLessThan.getHours() + 1); return getSignedUrl({ url: unsignedUrl, keyPairId: config.signingKeyId, dateLessThan: dateLessThan.toISOString(), privateKey: config.signingKey, passphrase: config.signingKeyPassphrase, }); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server