Skip to content

Commit 83da58a

Browse files
fix: use raw fetch for v2/images upload to decouple from schema types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0e626f5 commit 83da58a

1 file changed

Lines changed: 79 additions & 27 deletions

File tree

src/lib/images/upload.ts

Lines changed: 79 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,40 @@ import chalk from "chalk";
1010
import cliProgress from "cli-progress";
1111
import cliSpinners from "cli-spinners";
1212
import ora, { type Ora } from "ora";
13-
import { apiClient } from "../../apiClient.ts";
13+
import { getAuthToken, loadConfig } from "../../helpers/config.ts";
1414
import { logAndQuit } from "../../helpers/errors.ts";
1515

16+
// V2 API types (hardcoded to avoid schema.ts coupling)
17+
18+
type V2CreateImageRequest = {
19+
name: string;
20+
};
21+
22+
type V2CreateImageResponse = {
23+
object: "image";
24+
id: string;
25+
upload_status: string;
26+
};
27+
28+
type V2UploadPartRequest = {
29+
part_id: number;
30+
};
31+
32+
type V2UploadPartResponse = {
33+
url: string;
34+
expires_at: string;
35+
};
36+
37+
type V2CompleteUploadRequest = {
38+
sha256: string;
39+
};
40+
41+
type V2CompleteUploadResponse = {
42+
object: "image";
43+
upload_status: string;
44+
id: string;
45+
};
46+
1647
async function readChunk(
1748
filePath: string,
1849
start: number,
@@ -75,23 +106,30 @@ const upload = new Command("upload")
75106
let progressBar: cliProgress.SingleBar | undefined;
76107

77108
try {
78-
const client = await apiClient();
109+
const config = await loadConfig();
110+
const token = await getAuthToken();
111+
const apiHeaders = {
112+
Authorization: `Bearer ${token}`,
113+
"Content-Type": "application/json",
114+
};
79115

80116
preparingSpinner = ora(`Preparing upload for ${name}...`).start();
81117

82118
// Create image via v2 API
83-
const startResponse = await client.POST("/v2/images", {
84-
body: { name },
119+
const startResponse = await fetch(`${config.api_url}/v2/images`, {
120+
method: "POST",
121+
headers: apiHeaders,
122+
body: JSON.stringify({ name } satisfies V2CreateImageRequest),
85123
});
86124

87-
if (!startResponse.response.ok || !startResponse.data) {
88-
const errorText = await startResponse.response.text().catch(() => "");
125+
if (!startResponse.ok) {
89126
throw new Error(
90-
`Failed to start upload: ${startResponse.response.status} ${startResponse.response.statusText}${errorText ? ` - ${errorText}` : ""}`,
127+
`Failed to start upload: ${startResponse.status} ${startResponse.statusText}`,
91128
);
92129
}
93130

94-
const imageId = startResponse.data.id;
131+
const startData = (await startResponse.json()) as V2CreateImageResponse;
132+
const imageId = startData.id;
95133

96134
preparingSpinner.succeed(
97135
`Started upload for image ${chalk.cyan(name)} (${chalk.blackBright(
@@ -231,16 +269,19 @@ const upload = new Command("upload")
231269
}
232270

233271
// Get presigned URL via v2 API
234-
const partResponse = await client.POST("/v2/images/{id}/parts", {
235-
params: { path: { id: imageId } },
236-
body: { part_id: part },
237-
});
272+
const partResponse = await fetch(
273+
`${config.api_url}/v2/images/${imageId}/parts`,
274+
{
275+
method: "POST",
276+
headers: apiHeaders,
277+
body: JSON.stringify({
278+
part_id: part,
279+
} satisfies V2UploadPartRequest),
280+
},
281+
);
238282

239-
if (!partResponse.response.ok || !partResponse.data) {
240-
const status = partResponse.response.status;
241-
const errorText = await partResponse.response
242-
.text()
243-
.catch(() => "");
283+
if (!partResponse.ok) {
284+
const status = partResponse.status;
244285

245286
if (
246287
status >= 400 &&
@@ -250,18 +291,20 @@ const upload = new Command("upload")
250291
) {
251292
bail(
252293
new Error(
253-
`Failed to get upload URL for part ${part}: ${status} ${partResponse.response.statusText} - ${errorText}`,
294+
`Failed to get upload URL for part ${part}: ${status} ${partResponse.statusText}`,
254295
),
255296
);
256297
return;
257298
}
258299

259300
throw new Error(
260-
`Failed to get upload URL for part ${part}: ${status} ${partResponse.response.statusText} - ${errorText}`,
301+
`Failed to get upload URL for part ${part}: ${status} ${partResponse.statusText}`,
261302
);
262303
}
263304

264-
const url = partResponse.data.url;
305+
const partData =
306+
(await partResponse.json()) as V2UploadPartResponse;
307+
const url = partData.url;
265308

266309
// Read chunk from disk with progress tracking
267310
const payload = await readChunk(
@@ -353,21 +396,30 @@ const upload = new Command("upload")
353396
const sha256Hash = hash.digest("hex");
354397

355398
// Complete upload via v2 API
356-
const completeResponse = await client.POST("/v2/images/{id}/complete", {
357-
params: { path: { id: imageId } },
358-
body: { sha256: sha256Hash },
359-
});
399+
const completeResponse = await fetch(
400+
`${config.api_url}/v2/images/${imageId}/complete`,
401+
{
402+
method: "POST",
403+
headers: apiHeaders,
404+
body: JSON.stringify({
405+
sha256: sha256Hash,
406+
} satisfies V2CompleteUploadRequest),
407+
},
408+
);
360409

361-
if (!completeResponse.response.ok || !completeResponse.data) {
410+
if (!completeResponse.ok) {
362411
throw new Error(
363-
`Failed to complete upload: ${completeResponse.response.status} ${completeResponse.response.statusText}${completeResponse.error ? ` - ${JSON.stringify(completeResponse.error)}` : ""}`,
412+
`Failed to complete upload: ${completeResponse.status} ${completeResponse.statusText}`,
364413
);
365414
}
366415

416+
const completeData =
417+
(await completeResponse.json()) as V2CompleteUploadResponse;
418+
367419
finalizingSpinner.succeed("Image uploaded and verified");
368420

369421
console.log(chalk.gray("\nNext steps:"));
370-
console.log(` sf images get ${chalk.cyan(completeResponse.data.id)}`);
422+
console.log(` sf images get ${chalk.cyan(completeData.id)}`);
371423
} catch (err) {
372424
if (spinnerTimer) {
373425
clearInterval(spinnerTimer);

0 commit comments

Comments
 (0)