( uploadId: string, tenantId?: string, )
| 97 | } |
| 98 | |
| 99 | export async function finalizeResumableUpload( |
| 100 | uploadId: string, |
| 101 | tenantId?: string, |
| 102 | ): Promise<{ artifactPath: string; tempDir: string }> { |
| 103 | const entry = requireResumableUpload(uploadId, tenantId); |
| 104 | const offset = currentResumableUploadOffset(entry); |
| 105 | if (offset !== entry.sizeBytes) { |
| 106 | throw new AppError('INVALID_ARGS', 'Upload is incomplete', { |
| 107 | uploadId, |
| 108 | offset, |
| 109 | sizeBytes: entry.sizeBytes, |
| 110 | }); |
| 111 | } |
| 112 | const actualHash = await computeFileHash(entry.payloadPath); |
| 113 | if (actualHash !== entry.sha256) { |
| 114 | cleanupResumableUpload(entry.id); |
| 115 | throw new AppError('INVALID_ARGS', 'Upload hash mismatch', { |
| 116 | uploadId, |
| 117 | expectedSha256: entry.sha256, |
| 118 | actualSha256: actualHash, |
| 119 | }); |
| 120 | } |
| 121 | |
| 122 | RESUMABLE_UPLOADS_BY_ID.delete(entry.id); |
| 123 | RESUMABLE_UPLOADS_BY_KEY.delete(entry.key); |
| 124 | clearTimeout(entry.timer); |
| 125 | |
| 126 | if (entry.artifactType === 'file') { |
| 127 | const artifactPath = path.join(entry.tempDir, entry.fileName); |
| 128 | fs.renameSync(entry.payloadPath, artifactPath); |
| 129 | return { artifactPath, tempDir: entry.tempDir }; |
| 130 | } |
| 131 | |
| 132 | const artifactPath = await extractTarInstallableArtifact({ |
| 133 | archivePath: entry.payloadPath, |
| 134 | tempDir: entry.tempDir, |
| 135 | platform: entry.platform === 'android' ? 'android' : 'ios', |
| 136 | expectedRootName: entry.fileName, |
| 137 | }); |
| 138 | fs.rmSync(entry.payloadPath, { force: true }); |
| 139 | return { artifactPath, tempDir: entry.tempDir }; |
| 140 | } |
| 141 | |
| 142 | function validateResumableUploadOptions(options: BeginResumableUploadOptions): void { |
| 143 | if (!/^[a-f0-9]{64}$/i.test(options.sha256)) { |
no test coverage detected
searching dependent graphs…