87 lines
2.3 KiB
TypeScript
87 lines
2.3 KiB
TypeScript
import { logUtil } from "../../utils/logger";
|
|
|
|
/**
|
|
* Publish a .gem file to RubyGems.org.
|
|
*
|
|
* Uses the multipart form upload API:
|
|
* POST https://rubygems.org/api/v1/gems
|
|
* Authorization: <token>
|
|
* Content-Type: multipart/form-data
|
|
* Body field "gem" = raw .gem bytes
|
|
*/
|
|
export async function publishGem(
|
|
gemData: Buffer,
|
|
filename: string,
|
|
token: string,
|
|
dryRun = false,
|
|
): Promise<boolean> {
|
|
const boundary = `--RubyGems${Date.now()}${Math.random().toString(36).slice(2)}`;
|
|
|
|
if (dryRun) {
|
|
logUtil.log("[rubygems] DRY RUN — skipping upload");
|
|
logUtil.log(
|
|
`[rubygems] would publish ${filename} (${gemData.length} bytes)`,
|
|
);
|
|
logUtil.log("[rubygems] POST https://rubygems.org/api/v1/gems");
|
|
logUtil.log("[rubygems] Authorization: <redacted>");
|
|
logUtil.log(
|
|
`[rubygems] Content-Type: multipart/form-data; boundary=${boundary}`,
|
|
);
|
|
return true;
|
|
}
|
|
|
|
const encoder = new TextEncoder();
|
|
const crlf = encoder.encode("\r\n");
|
|
|
|
const parts: Uint8Array[] = [];
|
|
|
|
// File field: gem=<filename>
|
|
parts.push(encoder.encode(`--${boundary}\r\n`));
|
|
parts.push(
|
|
encoder.encode(
|
|
`Content-Disposition: form-data; name="gem"; filename="${filename}"\r\n`,
|
|
),
|
|
);
|
|
parts.push(encoder.encode("Content-Type: application/octet-stream\r\n\r\n"));
|
|
parts.push(new Uint8Array(gemData));
|
|
parts.push(crlf);
|
|
|
|
// End boundary
|
|
parts.push(encoder.encode(`--${boundary}--\r\n`));
|
|
|
|
const body = new Uint8Array(parts.reduce((acc, p) => acc + p.length, 0));
|
|
let offset = 0;
|
|
for (const p of parts) {
|
|
body.set(p, offset);
|
|
offset += p.length;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch("https://rubygems.org/api/v1/gems", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: token,
|
|
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
"User-Agent": "RubyGems/3.5.0",
|
|
},
|
|
body,
|
|
});
|
|
|
|
if (res.ok) {
|
|
logUtil.log(`[rubygems] Published ${filename}`);
|
|
return true;
|
|
}
|
|
|
|
const text = await res.text();
|
|
logUtil.log(
|
|
`[rubygems] Publish failed (${res.status}): ${text.slice(0, 300)}`,
|
|
);
|
|
return false;
|
|
} catch (e) {
|
|
logUtil.log(
|
|
`[rubygems] Publish error: ${e instanceof Error ? e.message : String(e)}`,
|
|
);
|
|
return false;
|
|
}
|
|
}
|