(
fileBlob: Blob,
filename: string,
native: PluginNative<typeof import("../native")> | null,
uploadRequest: UploadRequest
)
| 97 | } |
| 98 | |
| 99 | export async function uploadToS3( |
| 100 | fileBlob: Blob, |
| 101 | filename: string, |
| 102 | native: PluginNative<typeof import("../native")> | null, |
| 103 | uploadRequest: UploadRequest |
| 104 | ): Promise<string> { |
| 105 | const { |
| 106 | s3Endpoint, |
| 107 | s3Bucket, |
| 108 | s3Region = "auto", |
| 109 | s3AccessKeyId, |
| 110 | s3SecretAccessKey, |
| 111 | s3SessionToken, |
| 112 | s3PublicUrl, |
| 113 | s3Prefix, |
| 114 | s3ForcePathStyle = true |
| 115 | } = settings.store as S3Store; |
| 116 | |
| 117 | if (!s3Endpoint || !s3Bucket || !s3Region || !s3AccessKeyId || !s3SecretAccessKey) { |
| 118 | throw new Error("S3 endpoint, bucket, region, access key ID, and secret key are required"); |
| 119 | } |
| 120 | |
| 121 | const endpoint = new URL(s3Endpoint); |
| 122 | if (!/^https?:$/.test(endpoint.protocol)) { |
| 123 | throw new Error("S3 endpoint must be a valid HTTP(S) URL"); |
| 124 | } |
| 125 | |
| 126 | const extension = filename.includes(".") ? filename.slice(filename.lastIndexOf(".")) : ""; |
| 127 | const prefixSegments = (s3Prefix || "").split("/").filter(Boolean); |
| 128 | const objectName = `${Date.now()}-${makeRandomHex(8)}${extension}`; |
| 129 | const objectKeyRaw = [...prefixSegments, objectName].join("/"); |
| 130 | const objectKeyPath = objectKeyRaw.split("/").filter(Boolean).map(encodeRfc3986).join("/"); |
| 131 | |
| 132 | const host = s3ForcePathStyle ? endpoint.host : `${s3Bucket}.${endpoint.host}`; |
| 133 | const canonicalUri = s3ForcePathStyle |
| 134 | ? buildS3Path(endpoint.pathname, s3Bucket, objectKeyRaw) |
| 135 | : buildS3Path(endpoint.pathname, objectKeyRaw); |
| 136 | |
| 137 | const uploadUrl = new URL(endpoint.toString()); |
| 138 | uploadUrl.host = host; |
| 139 | uploadUrl.pathname = canonicalUri; |
| 140 | uploadUrl.search = ""; |
| 141 | |
| 142 | const now = new Date(); |
| 143 | const { amzDate, dateStamp } = getTimestampParts(now); |
| 144 | const payloadHash = await sha256Hex(await fileBlob.arrayBuffer()); |
| 145 | |
| 146 | const canonicalHeadersMap: Record<string, string> = { |
| 147 | "content-type": fileBlob.type || "application/octet-stream", |
| 148 | "host": host, |
| 149 | "x-amz-content-sha256": payloadHash, |
| 150 | "x-amz-date": amzDate |
| 151 | }; |
| 152 | |
| 153 | if (s3SessionToken) { |
| 154 | canonicalHeadersMap["x-amz-security-token"] = s3SessionToken; |
| 155 | } |
| 156 |
no test coverage detected