87 lines
2.1 KiB
TypeScript
87 lines
2.1 KiB
TypeScript
import "server-only";
|
|
|
|
type UrlAccessOptions = {
|
|
timeoutMs?: number;
|
|
};
|
|
|
|
const DEFAULT_TIMEOUT_MS = 3500;
|
|
|
|
function withTimeout(timeoutMs: number) {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
return {
|
|
signal: controller.signal,
|
|
clear: () => clearTimeout(timer),
|
|
};
|
|
}
|
|
|
|
export async function isRemoteUrlReachable(
|
|
url: string,
|
|
options: UrlAccessOptions = {}
|
|
): Promise<boolean> {
|
|
const target = String(url || "").trim();
|
|
if (!/^https?:\/\//i.test(target)) return false;
|
|
const timeoutMs = Math.max(800, Number(options.timeoutMs || DEFAULT_TIMEOUT_MS));
|
|
|
|
const headCtx = withTimeout(timeoutMs);
|
|
try {
|
|
const headResponse = await fetch(target, {
|
|
method: "HEAD",
|
|
redirect: "follow",
|
|
cache: "no-store",
|
|
signal: headCtx.signal,
|
|
});
|
|
if (headResponse.ok) return true;
|
|
if (![403, 405, 429, 500, 501, 502, 503, 504].includes(headResponse.status)) {
|
|
return false;
|
|
}
|
|
} catch {}
|
|
finally {
|
|
headCtx.clear();
|
|
}
|
|
|
|
const getCtx = withTimeout(timeoutMs);
|
|
try {
|
|
const getResponse = await fetch(target, {
|
|
method: "GET",
|
|
headers: { Range: "bytes=0-0" },
|
|
redirect: "follow",
|
|
cache: "no-store",
|
|
signal: getCtx.signal,
|
|
});
|
|
return getResponse.ok || getResponse.status === 206;
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
getCtx.clear();
|
|
}
|
|
}
|
|
|
|
export async function filterReachableRemoteUrls(
|
|
urls: string[],
|
|
options: UrlAccessOptions = {}
|
|
): Promise<{ kept: string[]; dropped: string[] }> {
|
|
const unique = Array.from(
|
|
new Set(
|
|
(Array.isArray(urls) ? urls : [])
|
|
.map((item) => String(item || "").trim())
|
|
.filter((item) => /^https?:\/\//i.test(item))
|
|
)
|
|
);
|
|
if (unique.length === 0) {
|
|
return { kept: [], dropped: [] };
|
|
}
|
|
|
|
const checks = await Promise.all(
|
|
unique.map(async (url) => ({
|
|
url,
|
|
ok: await isRemoteUrlReachable(url, options),
|
|
}))
|
|
);
|
|
|
|
return {
|
|
kept: checks.filter((item) => item.ok).map((item) => item.url),
|
|
dropped: checks.filter((item) => !item.ok).map((item) => item.url),
|
|
};
|
|
}
|