57 lines
2.9 KiB
TypeScript
57 lines
2.9 KiB
TypeScript
import { z } from "zod"
|
|
import { getClaims, getToken } from "@/lib/auth"
|
|
import { ApiClientError, requestBlob } from "./http-client"
|
|
|
|
const receiptSchema = z.object({
|
|
artifactId: z.string().uuid(),
|
|
humanDownloadOnly: z.literal(true),
|
|
downloadPath: z.string().optional(),
|
|
})
|
|
|
|
/** Only structured receipts qualify; supplied paths never become navigation targets. */
|
|
export function humanArtifactReceipt(value: unknown) {
|
|
const parsed = receiptSchema.safeParse(value)
|
|
if (!parsed.success) return null
|
|
const path = `/api/mcp/artifacts/${parsed.data.artifactId}`
|
|
if (parsed.data.downloadPath !== undefined && parsed.data.downloadPath !== path) return null
|
|
return { artifactId: parsed.data.artifactId, path }
|
|
}
|
|
|
|
export async function downloadHumanArtifact(base: string, receipt: unknown, signal?: AbortSignal) {
|
|
const artifact = humanArtifactReceipt(receipt)
|
|
if (!artifact) throw new Error("Некорректная квитанция файла")
|
|
const claims = getClaims()
|
|
if (!getToken() || !claims?.sub || !claims.apps?.includes("mm")) {
|
|
throw new ApiClientError("Войдите в портал для скачивания файла", 401)
|
|
}
|
|
const response = await requestBlob(base, artifact.path, { signal, cache: "no-store", redirect: "error" })
|
|
const blob = await response.blob()
|
|
signal?.throwIfAborted()
|
|
const disposition = response.headers.get("Content-Disposition") ?? ""
|
|
const suggested = /filename="([a-zA-Z0-9._-]{1,200})"/.exec(disposition)?.[1]
|
|
const filename = suggested && suggested !== "." && suggested !== ".." ? suggested : `mcp-${artifact.artifactId}.bin`
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement("a")
|
|
try {
|
|
link.href = url
|
|
link.download = filename
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
} finally {
|
|
link.remove()
|
|
// Let the browser begin consuming the download before releasing the in-memory file.
|
|
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
}
|
|
}
|
|
|
|
export function humanArtifactError(error: unknown): string {
|
|
if (error instanceof ApiClientError) {
|
|
if (error.status === 401) return "Войдите в портал для скачивания файла."
|
|
if (error.status === 403) return "Доступ к файлу отозван или файл принадлежит другому пользователю."
|
|
if (error.status === 404 || error.status === 410) return "Файл отсутствует или срок хранения истёк. Подготовьте новую выгрузку."
|
|
if (error.status === 409) return "Файл изменился или больше не соответствует квитанции. Подготовьте новую выгрузку."
|
|
}
|
|
// Do not render response bodies, filenames, credentials, or binary content as errors.
|
|
return "Не удалось скачать файл. Проверьте соединение и повторите попытку."
|
|
}
|