feat(POSTV1-ASSET-ALL-16): 完整还原归档文字模板素材
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
realpathSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
@@ -15,6 +16,8 @@ import {
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
|
||||
|
||||
import { compileTextTemplateAssets } from "./text-template-assets.mjs";
|
||||
|
||||
export const P0A_RUNTIME_ASSET_ROOT_REF = "p0a_runtime_assets";
|
||||
export const RUNTIME_ASSET_MANIFEST_SCHEMA = "DadaRuntimeAssets/v1";
|
||||
|
||||
@@ -64,6 +67,8 @@ function derivedCounts(entries) {
|
||||
dynamic_images: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^DYN\d{3}-/.test(entry.assetId)).length,
|
||||
font_panel_items: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^FONT\d{3}$/.test(entry.assetId)).length,
|
||||
static_stickers: entries.filter((entry) => entry.resourceVersion === "p0a-static-v1" && /^STK\d{3,4}$/.test(entry.assetId)).length,
|
||||
text_fonts: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^TEXT-FONT-/.test(entry.assetId)).length,
|
||||
text_images: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^TEXT-IMAGE-/.test(entry.assetId)).length,
|
||||
text_previews: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^TEXT-PREVIEW-/.test(entry.assetId)).length,
|
||||
};
|
||||
}
|
||||
@@ -131,7 +136,42 @@ function sameFile(left, right) {
|
||||
return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
|
||||
}
|
||||
|
||||
export function deployRuntimeAssetPlan({ assetRoot, manifest, resources }) {
|
||||
function installRuntimeAsset(resource, targetPath, expectedSha) {
|
||||
const generatedBytes = Buffer.isBuffer(resource.sourceBytes) ? resource.sourceBytes : undefined;
|
||||
if (generatedBytes) writeFileSync(targetPath, generatedBytes, { flag: "wx" });
|
||||
else {
|
||||
try {
|
||||
linkSync(resource.sourcePath, targetPath);
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "EXDEV") {
|
||||
throw new Error("asset_hardlink_volume_mismatch");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (fileSha256(targetPath) !== expectedSha) throw new Error(generatedBytes ? "asset_generated_write_invalid" : "asset_hardlink_verification_failed");
|
||||
if (!generatedBytes && !sameFile(resource.sourcePath, targetPath)) throw new Error("asset_hardlink_verification_failed");
|
||||
}
|
||||
|
||||
function replaceManagedRuntimeAsset(resource, targetPath, expectedSha) {
|
||||
const nextPath = `${targetPath}.dada-next`;
|
||||
const previousPath = `${targetPath}.dada-previous`;
|
||||
if (existsSync(nextPath) || existsSync(previousPath)) throw new Error("asset_update_staging_conflict");
|
||||
installRuntimeAsset(resource, nextPath, expectedSha);
|
||||
renameSync(targetPath, previousPath);
|
||||
try {
|
||||
renameSync(nextPath, targetPath);
|
||||
if (fileSha256(targetPath) !== expectedSha) throw new Error("asset_update_verification_failed");
|
||||
rmSync(previousPath, { force: true });
|
||||
} catch (error) {
|
||||
if (existsSync(targetPath)) renameSync(targetPath, nextPath);
|
||||
renameSync(previousPath, targetPath);
|
||||
rmSync(nextPath, { force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function deployRuntimeAssetPlan({ allowManagedUpdate = false, assetRoot, manifest, resources }) {
|
||||
if (!isAbsolute(assetRoot)) throw new Error("asset_root_must_be_absolute");
|
||||
const normalizedManifest = createRuntimeAssetManifest({
|
||||
counts: manifest.counts,
|
||||
@@ -141,32 +181,35 @@ export function deployRuntimeAssetPlan({ assetRoot, manifest, resources }) {
|
||||
const entries = new Map(normalizedManifest.entries.map((entry) => [`${entry.resourceVersion}\u0000${entry.assetId}`, entry]));
|
||||
if (resources.length !== entries.size) throw new Error("asset_resource_plan_incomplete");
|
||||
mkdirSync(assetRoot, { recursive: true });
|
||||
const previousManifestPath = join(assetRoot, "manifest.json");
|
||||
const previousEntries = allowManagedUpdate && existsSync(previousManifestPath)
|
||||
? new Map(readRuntimeAssetManifest(previousManifestPath).entries.map((entry) => [entry.relativePath, entry]))
|
||||
: new Map();
|
||||
for (const resource of resources) {
|
||||
const key = `${resource.entry.resourceVersion}\u0000${resource.entry.assetId}`;
|
||||
const entry = entries.get(key);
|
||||
if (!entry || JSON.stringify(entry) !== JSON.stringify({ ...resource.entry, sha256: resource.entry.sha256.toLowerCase() })) {
|
||||
throw new Error("asset_resource_plan_mismatch");
|
||||
}
|
||||
if (!existsSync(resource.sourcePath) || !statSync(resource.sourcePath).isFile() || lstatSync(resource.sourcePath).isSymbolicLink()) {
|
||||
const generatedBytes = Buffer.isBuffer(resource.sourceBytes) ? resource.sourceBytes : undefined;
|
||||
if (!generatedBytes && (!existsSync(resource.sourcePath) || !statSync(resource.sourcePath).isFile() || lstatSync(resource.sourcePath).isSymbolicLink())) {
|
||||
throw new Error("asset_source_invalid");
|
||||
}
|
||||
if (fileSha256(resource.sourcePath) !== entry.sha256) throw new Error("asset_source_hash_invalid");
|
||||
if ((generatedBytes ? sha256(generatedBytes) : fileSha256(resource.sourcePath)) !== entry.sha256) throw new Error("asset_source_hash_invalid");
|
||||
const targetPath = targetWithinRoot(assetRoot, entry.relativePath);
|
||||
mkdirSync(dirname(targetPath), { recursive: true });
|
||||
if (existsSync(targetPath)) {
|
||||
if (fileSha256(targetPath) !== entry.sha256) throw new Error("asset_target_conflict");
|
||||
if (!sameFile(resource.sourcePath, targetPath)) throw new Error("asset_target_not_hardlink");
|
||||
const targetSha = fileSha256(targetPath);
|
||||
if (targetSha !== entry.sha256) {
|
||||
const previous = previousEntries.get(entry.relativePath);
|
||||
if (!previous || targetSha !== previous.sha256) throw new Error("asset_target_conflict");
|
||||
replaceManagedRuntimeAsset(resource, targetPath, entry.sha256);
|
||||
continue;
|
||||
}
|
||||
if (!generatedBytes && !sameFile(resource.sourcePath, targetPath)) throw new Error("asset_target_not_hardlink");
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
linkSync(resource.sourcePath, targetPath);
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "EXDEV") {
|
||||
throw new Error("asset_hardlink_volume_mismatch");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (!sameFile(resource.sourcePath, targetPath)) throw new Error("asset_hardlink_verification_failed");
|
||||
installRuntimeAsset(resource, targetPath, entry.sha256);
|
||||
}
|
||||
writeRuntimeAssetManifest(join(assetRoot, "manifest.json"), normalizedManifest);
|
||||
return { linked_files: resources.length, manifest: normalizedManifest, status: "ready" };
|
||||
@@ -197,6 +240,17 @@ function entryFor(sourcePath, assetId, resourceVersion, relativePath, mimeType)
|
||||
};
|
||||
}
|
||||
|
||||
function entryForBytes(sourceBytes, assetId, resourceVersion, relativePath, mimeType) {
|
||||
return {
|
||||
assetId,
|
||||
mimeType,
|
||||
relativePath,
|
||||
resourceVersion,
|
||||
rootRef: P0A_RUNTIME_ASSET_ROOT_REF,
|
||||
sha256: sha256(sourceBytes),
|
||||
};
|
||||
}
|
||||
|
||||
function safeRuntimeComponent(value) {
|
||||
const normalized = value.normalize("NFKD").replaceAll(/[^A-Za-z0-9_-]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
||||
if (normalized === value) return normalized;
|
||||
@@ -325,6 +379,14 @@ export async function buildP0aRuntimeAssetPlan({ replicationRoot }) {
|
||||
for (const templateId of registry.P0A_TEXT_TEMPLATE_IDS) {
|
||||
const templateDirectory = textTemplateDirectory(textRoot, templateId);
|
||||
const metadata = JSON.parse(readFileSync(join(templateDirectory, "metadata.json"), "utf8"));
|
||||
const compiled = compileTextTemplateAssets({ templateDirectory, templateId });
|
||||
for (const resource of compiled.resources) {
|
||||
const relativePath = `${registry.P0A_COMPLEX_RELEASE_VERSION}/${resource.assetId}${resource.extension}`;
|
||||
const entry = resource.sourceBytes
|
||||
? entryForBytes(resource.sourceBytes, resource.assetId, registry.P0A_COMPLEX_RELEASE_VERSION, relativePath, resource.mimeType)
|
||||
: entryFor(resource.sourcePath, resource.assetId, registry.P0A_COMPLEX_RELEASE_VERSION, relativePath, resource.mimeType);
|
||||
addResource({ entry, ...(resource.sourceBytes ? { sourceBytes: resource.sourceBytes } : { sourcePath: resource.sourcePath }) });
|
||||
}
|
||||
const previewReference = metadata.files?.preview;
|
||||
if (typeof previewReference !== "string" || previewReference.length === 0) continue;
|
||||
const sourcePath = join(templateDirectory, ...previewReference.split("/"));
|
||||
|
||||
Reference in New Issue
Block a user