feat(cms): recover drafts and failed imports with verified UI workflows
This commit is contained in:
1 parent
410a1e466c
commit
88790d1a0d
84 files changed
+3952
-288
No files matched your search
@@ -1,35 +1,63 @@
|
||||
import type {
|
||||
AssetAvailability,
|
||||
SourceAssetAttempt,
|
||||
SourceAssetCheck,
|
||||
SourceAssetItem,
|
||||
} from "@/lib/furni/source-assets";
|
||||
import type { CloneSource } from "./clone-sources";
|
||||
import { browserHeaders } from "./import/core/browser-headers";
|
||||
|
||||
const cache = new Map<string, { expires: number; state: AssetAvailability }>();
|
||||
export async function probeAsset(url: string): Promise<AssetAvailability> {
|
||||
const cache = new Map<
|
||||
string,
|
||||
{ expires: number; attempt: SourceAssetAttempt }
|
||||
>();
|
||||
/** Only expose the address path; credentials, signed queries and fragments stay server-side. */
|
||||
export function diagnosticAssetUrl(raw: string): string {
|
||||
const url = new URL(raw);
|
||||
return `${url.protocol}//${url.host}${url.pathname}`;
|
||||
}
|
||||
async function probeAssetAttempt(url: string): Promise<SourceAssetAttempt> {
|
||||
const cached = cache.get(url);
|
||||
if (cached && cached.expires > Date.now()) return cached.state;
|
||||
let state: AssetAvailability = "unknown";
|
||||
if (cached && cached.expires > Date.now()) return cached.attempt;
|
||||
const safeUrl = diagnosticAssetUrl(url);
|
||||
const attempt: SourceAssetAttempt = {
|
||||
url: safeUrl,
|
||||
file: new URL(url).pathname.split("/").pop() || "",
|
||||
state: "unknown",
|
||||
reason: "network",
|
||||
};
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "HEAD",
|
||||
signal: AbortSignal.timeout(6000),
|
||||
headers: browserHeaders(),
|
||||
});
|
||||
attempt.status = response.status;
|
||||
if (
|
||||
response.ok &&
|
||||
!response.headers.get("content-type")?.includes("text/html")
|
||||
)
|
||||
state = "available";
|
||||
else if (response.status === 404 || response.status === 410)
|
||||
state = "missing";
|
||||
} catch {
|
||||
/* A network problem is not proof the asset is absent. */
|
||||
) {
|
||||
attempt.state = "available";
|
||||
attempt.reason = "available";
|
||||
} else if (response.ok) attempt.reason = "invalidResponse";
|
||||
else {
|
||||
attempt.reason = "http";
|
||||
if (response.status === 404 || response.status === 410)
|
||||
attempt.state = "missing";
|
||||
}
|
||||
} catch (error) {
|
||||
attempt.reason =
|
||||
error instanceof Error &&
|
||||
(error.name === "TimeoutError" || error.name === "AbortError")
|
||||
? "timeout"
|
||||
: "network";
|
||||
}
|
||||
if (cache.size >= 2000) cache.clear();
|
||||
cache.set(url, { expires: Date.now() + 60000, state });
|
||||
return state;
|
||||
cache.set(url, { expires: Date.now() + 60000, attempt });
|
||||
return attempt;
|
||||
}
|
||||
export async function probeAsset(url: string): Promise<AssetAvailability> {
|
||||
return (await probeAssetAttempt(url)).state;
|
||||
}
|
||||
export function furnitureAssetUrls(
|
||||
item: SourceAssetItem,
|
||||
@@ -51,19 +79,6 @@ export function furnitureAssetUrls(
|
||||
);
|
||||
return urls;
|
||||
}
|
||||
async function available(
|
||||
item: SourceAssetItem,
|
||||
source?: CloneSource | null,
|
||||
): Promise<AssetAvailability> {
|
||||
const states = await Promise.all(
|
||||
furnitureAssetUrls(item, source).map(probeAsset),
|
||||
);
|
||||
return states.includes("available")
|
||||
? "available"
|
||||
: states.every((state) => state === "missing")
|
||||
? "missing"
|
||||
: "unknown";
|
||||
}
|
||||
export async function inspectSourceAssets(
|
||||
item: SourceAssetItem,
|
||||
official: SourceAssetItem[],
|
||||
@@ -76,12 +91,20 @@ export async function inspectSourceAssets(
|
||||
...item,
|
||||
revision: item.revision || canonical?.revision || 0,
|
||||
};
|
||||
const state = await available(effective, source);
|
||||
const attempts = await Promise.all(
|
||||
furnitureAssetUrls(effective, source).map(probeAssetAttempt),
|
||||
);
|
||||
const state = attempts.some((attempt) => attempt.state === "available")
|
||||
? "available"
|
||||
: attempts.every((attempt) => attempt.state === "missing")
|
||||
? "missing"
|
||||
: "unknown";
|
||||
|
||||
return {
|
||||
classname: item.classname,
|
||||
revision: effective.revision,
|
||||
state,
|
||||
alternatives: [],
|
||||
attempts,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user