93 lines
3.8 KiB
JavaScript
93 lines
3.8 KiB
JavaScript
import { constants } from 'node:fs';
|
|
import { lstat, mkdir, open, readFile, readdir } from 'node:fs/promises';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
const defaultRepositoryRoot = resolve(dirname(scriptPath), '..');
|
|
|
|
const isErrno = (error, code) =>
|
|
typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
|
|
const directoryIdentity = (stats) => ({ dev: stats.dev, ino: stats.ino });
|
|
const hasSameDirectoryIdentity = (left, right) => left.dev === right.dev && left.ino === right.ino;
|
|
|
|
const assertNonSymlinkDirectory = (stats) => {
|
|
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
throw new Error('Prepared asset path must be a non-symlink directory.');
|
|
}
|
|
};
|
|
|
|
export const assertPreparedAssetDirectoryIdentity = async (preparedAssetsPath, expectedIdentity) => {
|
|
const current = await lstat(preparedAssetsPath);
|
|
assertNonSymlinkDirectory(current);
|
|
if (!hasSameDirectoryIdentity(directoryIdentity(current), expectedIdentity)) {
|
|
throw new Error('Prepared asset directory identity changed during validation.');
|
|
}
|
|
};
|
|
|
|
const assertOpenedDirectoryIdentity = async (preparedAssetsPath, expectedIdentity) => {
|
|
const noFollowDirectoryFlags = constants.O_RDONLY | (constants.O_DIRECTORY ?? 0) | (constants.O_NOFOLLOW ?? 0);
|
|
let handle;
|
|
try {
|
|
handle = await open(preparedAssetsPath, noFollowDirectoryFlags);
|
|
} catch (error) {
|
|
if (isErrno(error, 'ELOOP')) {
|
|
throw new Error('Prepared asset path must be a non-symlink directory.');
|
|
}
|
|
if (isErrno(error, 'EINVAL') || isErrno(error, 'ENOTSUP') || isErrno(error, 'ENOSYS')) return;
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
const opened = await handle.stat();
|
|
assertNonSymlinkDirectory(opened);
|
|
if (!hasSameDirectoryIdentity(directoryIdentity(opened), expectedIdentity)) {
|
|
throw new Error('Prepared asset directory identity changed during validation.');
|
|
}
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
};
|
|
|
|
const readEmptyManifest = async (repositoryRoot) => {
|
|
const manifestPath = resolve(repositoryRoot, 'content/public/asset-manifest.json');
|
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest) ||
|
|
Object.keys(manifest).length !== 1 || !Array.isArray(manifest.assets) || manifest.assets.length !== 0) {
|
|
throw new Error('Phase 1 asset manifest must contain exactly zero assets.');
|
|
}
|
|
};
|
|
|
|
const ensureEmptyPreparedAssetDirectory = async (repositoryRoot) => {
|
|
const preparedAssetsPath = resolve(repositoryRoot, 'inputs/prepared-assets');
|
|
let initialStats;
|
|
try {
|
|
initialStats = await lstat(preparedAssetsPath);
|
|
} catch (error) {
|
|
if (!isErrno(error, 'ENOENT')) throw error;
|
|
await mkdir(preparedAssetsPath, { recursive: true });
|
|
initialStats = await lstat(preparedAssetsPath);
|
|
}
|
|
|
|
assertNonSymlinkDirectory(initialStats);
|
|
const initialIdentity = directoryIdentity(initialStats);
|
|
await assertOpenedDirectoryIdentity(preparedAssetsPath, initialIdentity);
|
|
await assertPreparedAssetDirectoryIdentity(preparedAssetsPath, initialIdentity);
|
|
const members = await readdir(preparedAssetsPath);
|
|
await assertPreparedAssetDirectoryIdentity(preparedAssetsPath, initialIdentity);
|
|
if (members.length !== 0) {
|
|
throw new Error('Prepared asset directory must be empty during Phase 1.');
|
|
}
|
|
};
|
|
|
|
export const materializeEmptyAssetSet = async (repositoryRoot) => {
|
|
const resolvedRepositoryRoot = resolve(repositoryRoot);
|
|
await readEmptyManifest(resolvedRepositoryRoot);
|
|
await ensureEmptyPreparedAssetDirectory(resolvedRepositoryRoot);
|
|
};
|
|
|
|
if (process.argv[1] !== undefined && resolve(process.argv[1]) === scriptPath) {
|
|
await materializeEmptyAssetSet(defaultRepositoryRoot);
|
|
}
|