| 191 | const ASCII_ONLY_RE = /^[\x20-\x7e]*$/ |
| 192 | |
| 193 | async function uploadObject(action: string, input: UploadInput): Promise<void> { |
| 194 | const objectNameBytes = Buffer.byteLength(input.objectName, 'utf8') |
| 195 | if (objectNameBytes < 1 || objectNameBytes > MAX_OBJECT_NAME_BYTES) { |
| 196 | throw new Error( |
| 197 | `GCS object name is ${objectNameBytes} bytes, must be 1-${MAX_OBJECT_NAME_BYTES} bytes (UTF-8)` |
| 198 | ) |
| 199 | } |
| 200 | let metadataBytes = 0 |
| 201 | for (const [key, value] of Object.entries(input.metadata)) { |
| 202 | if (!ASCII_ONLY_RE.test(key) || !ASCII_ONLY_RE.test(value)) { |
| 203 | throw new Error(`GCS custom metadata key/value must be ASCII printable: ${key}`) |
| 204 | } |
| 205 | metadataBytes += Buffer.byteLength(key, 'utf8') + Buffer.byteLength(value, 'utf8') |
| 206 | } |
| 207 | if (metadataBytes > MAX_CUSTOM_METADATA_BYTES) { |
| 208 | throw new Error( |
| 209 | `GCS custom metadata is ${metadataBytes} bytes, exceeds the ${MAX_CUSTOM_METADATA_BYTES}-byte per-object limit` |
| 210 | ) |
| 211 | } |
| 212 | const url = `${GCS_HOST}/upload/storage/v1/b/${encodeURIComponent(input.bucket)}/o?uploadType=media&name=${encodeURIComponent(input.objectName)}` |
| 213 | await fetchWithRetry({ |
| 214 | action, |
| 215 | bucket: input.bucket, |
| 216 | url, |
| 217 | method: 'POST', |
| 218 | buildHeaders: async () => { |
| 219 | const token = await getAccessToken(input.jwt) |
| 220 | const headers: Record<string, string> = { |
| 221 | Authorization: `Bearer ${token}`, |
| 222 | 'Content-Type': input.contentType, |
| 223 | 'User-Agent': USER_AGENT, |
| 224 | } |
| 225 | for (const [key, value] of Object.entries(input.metadata)) { |
| 226 | headers[`x-goog-meta-${key}`] = value |
| 227 | } |
| 228 | return headers |
| 229 | }, |
| 230 | body: input.body, |
| 231 | signal: input.signal, |
| 232 | }) |
| 233 | } |
| 234 | |
| 235 | async function deleteObject(input: { |
| 236 | bucket: string |