mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 15:33:31 +00:00
Keep verified FFmpeg downloads available in Recordly 2.0 part 8
This commit is contained in:
commit
375e8dcff6
2 files changed
+193
-4
No files matched your search
@@ -2,10 +2,12 @@ import { createHash } from "node:crypto";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
APPROVED_FFMPEG_BINARIES,
|
||||
assertRedistributableFfmpegOutput,
|
||||
ensureRedistributableFfmpeg,
|
||||
verifyFfmpegDistribution,
|
||||
} from "../scripts/ffmpeg-distribution.mjs";
|
||||
|
||||
@@ -29,6 +31,8 @@ function fixture(overrides = {}) {
|
||||
return binary;
|
||||
}
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
for (const directory of fixtures.splice(0)) rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -98,3 +102,146 @@ describe("FFmpeg redistribution checks", () => {
|
||||
).toThrow("pinned redistributable sources");
|
||||
});
|
||||
});
|
||||
|
||||
const directRelease = "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.1.1";
|
||||
const invalidArtifact = gzipSync(Buffer.from("unapproved executable fixture"));
|
||||
function installTarget(platform = "linux", arch = "x64") {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "recordly-ffmpeg-download-"));
|
||||
fixtures.push(root);
|
||||
return ensureRedistributableFfmpeg({ root, platform, arch });
|
||||
}
|
||||
function artifactResponse() {
|
||||
return new Response(invalidArtifact);
|
||||
}
|
||||
|
||||
describe("FFmpeg release transport", () => {
|
||||
it("keeps a successful direct download and still rejects unapproved bytes", async () => {
|
||||
const network = vi.fn().mockResolvedValue(artifactResponse());
|
||||
vi.stubGlobal("fetch", network);
|
||||
await expect(installTarget()).rejects.toThrow("pinned SHA-256 check");
|
||||
expect(network).toHaveBeenCalledOnce();
|
||||
expect(network.mock.calls[0][0]).toBe(`${directRelease}/ffmpeg-linux-x64.gz`);
|
||||
expect(network.mock.calls[0][1]).toEqual({
|
||||
signal: expect.any(AbortSignal),
|
||||
credentials: "omit",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
500, 502, 503, 504,
|
||||
])("falls back once for retryable HTTP %i without accepting changed bytes", async (status) => {
|
||||
const failed = new Response("temporarily unavailable", { status });
|
||||
const cancel = vi.spyOn(failed.body, "cancel");
|
||||
const network = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(failed)
|
||||
.mockResolvedValueOnce(artifactResponse());
|
||||
vi.stubGlobal("fetch", network);
|
||||
await expect(installTarget()).rejects.toThrow("pinned SHA-256 check");
|
||||
expect(cancel).toHaveBeenCalledOnce();
|
||||
expect(network).toHaveBeenCalledTimes(2);
|
||||
expect(network.mock.calls[1]).toEqual([
|
||||
"https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/316528626",
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
credentials: "omit",
|
||||
headers: { Accept: "application/octet-stream" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["linux", "arm64", 316528458],
|
||||
["win32", "x64", 316528798],
|
||||
])("uses the same pinned release asset for %s/%s", async (platform, arch, assetId) => {
|
||||
const network = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(new Response(null, { status: 504 }))
|
||||
.mockResolvedValueOnce(artifactResponse());
|
||||
vi.stubGlobal("fetch", network);
|
||||
await expect(installTarget(platform, arch)).rejects.toThrow("pinned SHA-256 check");
|
||||
expect(network.mock.calls.map(([url]) => url)).toEqual([
|
||||
`${directRelease}/ffmpeg-${platform}-${arch}.gz`,
|
||||
`https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/${assetId}`,
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
400, 401, 403, 404, 429,
|
||||
])("fails closed without fallback for permanent HTTP %i", async (status) => {
|
||||
const network = vi.fn().mockResolvedValue(new Response(null, { status }));
|
||||
vi.stubGlobal("fetch", network);
|
||||
await expect(installTarget()).rejects.toThrow(`download failed (${status})`);
|
||||
expect(network).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("recovers a network exception through the exact same-provider asset API", async () => {
|
||||
const network = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new TypeError("fetch failed"))
|
||||
.mockResolvedValueOnce(artifactResponse());
|
||||
vi.stubGlobal("fetch", network);
|
||||
await expect(installTarget()).rejects.toThrow("pinned SHA-256 check");
|
||||
expect(network).toHaveBeenCalledTimes(2);
|
||||
expect(network.mock.calls[1][0]).toBe(
|
||||
"https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/316528626",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not retry programming failures as network outages", async () => {
|
||||
const network = vi.fn().mockRejectedValue(new Error("unexpected failure"));
|
||||
vi.stubGlobal("fetch", network);
|
||||
await expect(installTarget()).rejects.toThrow("unexpected failure");
|
||||
expect(network).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("stops after a failed API fallback rather than looping", async () => {
|
||||
const network = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(new Response(null, { status: 504 }))
|
||||
.mockResolvedValueOnce(new Response(null, { status: 503 }));
|
||||
vi.stubGlobal("fetch", network);
|
||||
await expect(installTarget()).rejects.toThrow("download failed (503)");
|
||||
expect(network).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("bounds both stalled requests with real abort signals", async () => {
|
||||
const realTimeout = AbortSignal.timeout.bind(AbortSignal);
|
||||
const deadlines = vi.spyOn(AbortSignal, "timeout").mockImplementation(() => realTimeout(1));
|
||||
const network = vi.fn(
|
||||
(_url, { signal }) =>
|
||||
new Promise((_resolve, reject) => {
|
||||
if (signal.aborted) reject(signal.reason);
|
||||
else
|
||||
signal.addEventListener("abort", () => reject(signal.reason), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal("fetch", network);
|
||||
await expect(installTarget()).rejects.toMatchObject({ name: "TimeoutError" });
|
||||
expect(network).toHaveBeenCalledTimes(2);
|
||||
expect(deadlines.mock.calls).toEqual([[180000], [180000]]);
|
||||
expect(network.mock.calls[0][1].signal).not.toBe(network.mock.calls[1][1].signal);
|
||||
});
|
||||
|
||||
it("includes response-body stalls in the same two-request bound", async () => {
|
||||
const realTimeout = AbortSignal.timeout.bind(AbortSignal);
|
||||
const deadlines = vi.spyOn(AbortSignal, "timeout").mockImplementation(() => realTimeout(1));
|
||||
const network = vi.fn(async (_url, { signal }) => ({
|
||||
ok: true,
|
||||
arrayBuffer: () =>
|
||||
new Promise((_resolve, reject) => {
|
||||
if (signal.aborted) reject(signal.reason);
|
||||
else
|
||||
signal.addEventListener("abort", () => reject(signal.reason), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
}));
|
||||
vi.stubGlobal("fetch", network);
|
||||
await expect(installTarget()).rejects.toMatchObject({ name: "TimeoutError" });
|
||||
expect(network).toHaveBeenCalledTimes(2);
|
||||
expect(deadlines.mock.calls).toEqual([[180000], [180000]]);
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,16 @@ import { machoContentHash } from "./macho-content-hash.mjs";
|
||||
import { peContentHash } from "./pe-content-hash.mjs";
|
||||
|
||||
const upstreamRelease = "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.1.1";
|
||||
// Exact assets from that same pinned release. The API route avoids release-page outages.
|
||||
const upstreamAssetIds = Object.freeze({
|
||||
"ffmpeg-linux-arm64.gz": 316528458,
|
||||
"ffmpeg-linux-x64.gz": 316528626,
|
||||
"ffmpeg-win32-x64.gz": 316528798,
|
||||
"ffprobe-win32-x64.gz": 316529830,
|
||||
"linux-arm64.README": 316529906,
|
||||
"linux-x64.README": 316529927,
|
||||
"win32-x64.README": 316529965,
|
||||
});
|
||||
// Verified raw release digests. The upstream darwin-arm64 artifact enables nonfree.
|
||||
export const APPROVED_FFMPEG_BINARIES = Object.freeze({
|
||||
"win32-x64": "04e1307997530f9cf2fe35cba2ca7e8875ca91da02f89d6c7243df819c94ad00",
|
||||
@@ -151,9 +161,41 @@ export function verifyFfprobeDistribution(
|
||||
}
|
||||
|
||||
async function download(url) {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(180000) });
|
||||
if (!response.ok) throw new Error(`FFmpeg download failed (${response.status}): ${url}`);
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
const assetName = url.startsWith(`${upstreamRelease}/`)
|
||||
? url.slice(upstreamRelease.length + 1)
|
||||
: undefined;
|
||||
const assetId = upstreamAssetIds[assetName];
|
||||
if (!Number.isSafeInteger(assetId)) throw new Error("Unapproved FFmpeg asset URL.");
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(
|
||||
attempt === 0
|
||||
? url
|
||||
: `https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/${assetId}`,
|
||||
{
|
||||
signal: AbortSignal.timeout(180000),
|
||||
credentials: "omit",
|
||||
...(attempt === 0 ? {} : { headers: { Accept: "application/octet-stream" } }),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel().catch(() => {
|
||||
// Preserve the HTTP failure if releasing its body also fails.
|
||||
});
|
||||
throw new Error(`FFmpeg download failed (${response.status}): ${url}`);
|
||||
}
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
} catch (error) {
|
||||
const retryable =
|
||||
response && !response.ok
|
||||
? response.status >= 500 && response.status <= 599
|
||||
: error instanceof TypeError ||
|
||||
error?.name === "TimeoutError" ||
|
||||
error?.name === "AbortError";
|
||||
if (attempt !== 0 || !retryable) throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureWindowsFfprobe(root) {
|
||||
|
||||
Reference in new issue
Block a user