( filePath: string, layout: PartLayout, options: UploadArchiveOptions, )
| 142 | } |
| 143 | |
| 144 | async function uploadOnePart( |
| 145 | filePath: string, |
| 146 | layout: PartLayout, |
| 147 | options: UploadArchiveOptions, |
| 148 | ): Promise<CompletedUploadPart> { |
| 149 | const { part, start } = layout; |
| 150 | const timeoutMs = options.timeoutMs ?? DEFAULT_PART_TIMEOUT_MS; |
| 151 | const controller = new AbortController(); |
| 152 | const timer = setTimeout(() => { |
| 153 | controller.abort(); |
| 154 | }, timeoutMs); |
| 155 | const stream = createReadStream(filePath, { start, end: start + part.size - 1 }); |
| 156 | try { |
| 157 | const res = await fetch(part.url, { |
| 158 | method: part.method, |
| 159 | body: Readable.toWeb(stream), |
| 160 | headers: { 'Content-Length': String(part.size) }, |
| 161 | duplex: 'half', |
| 162 | signal: controller.signal, |
| 163 | } as RequestInit); |
| 164 | if (!res.ok) { |
| 165 | const text = await res.text().catch(() => ''); |
| 166 | throw new UploadPartHttpError(part.partNumber, res.status, text); |
| 167 | } |
| 168 | const etag = res.headers.get('etag'); |
| 169 | if (etag === null || etag.length === 0) { |
| 170 | throw new Error(`Failed to upload part ${part.partNumber}: missing ETag in response.`); |
| 171 | } |
| 172 | return { partNumber: part.partNumber, etag }; |
| 173 | } catch (error) { |
| 174 | stream.destroy(); |
| 175 | if (error instanceof Error && error.name === 'AbortError') { |
| 176 | throw new Error(`Failed to upload part ${part.partNumber}: upload timed out.`, { cause: error }); |
| 177 | } |
| 178 | throw error; |
| 179 | } finally { |
| 180 | clearTimeout(timer); |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | class UploadPartHttpError extends Error { |
| 185 | constructor( |
no test coverage detected