mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-14 21:32:31 +00:00
Preserve legacy settings across same-profile upgrades
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
export type PinnedReleaseArtifactFile = {
|
||||
name: "main.js" | "manifest.json" | "styles.css";
|
||||
url: string;
|
||||
sha256: string;
|
||||
};
|
||||
|
||||
export type PinnedPluginRelease = {
|
||||
pluginId: string;
|
||||
version: string;
|
||||
files: readonly PinnedReleaseArtifactFile[];
|
||||
};
|
||||
|
||||
export type EnsurePinnedReleaseArtifactOptions = {
|
||||
artifactRoot?: string;
|
||||
fetchImplementation?: typeof fetch;
|
||||
};
|
||||
|
||||
export const UPGRADE_SOURCE_RELEASE: PinnedPluginRelease = {
|
||||
pluginId: "obsidian-livesync",
|
||||
version: "0.25.83",
|
||||
files: [
|
||||
{
|
||||
name: "main.js",
|
||||
url: "https://github.com/vrtmrz/obsidian-livesync/releases/download/0.25.83/main.js",
|
||||
sha256: "5e57f990635ab0cf2ff3879f3c6cb91ddfdbc146958d33d1e5d21f1869dff6a4",
|
||||
},
|
||||
{
|
||||
name: "manifest.json",
|
||||
url: "https://github.com/vrtmrz/obsidian-livesync/releases/download/0.25.83/manifest.json",
|
||||
sha256: "4944f5665c94bcbb58db0e3708ec2bd8ee36118791271c01d085668876dc8ba6",
|
||||
},
|
||||
{
|
||||
name: "styles.css",
|
||||
url: "https://github.com/vrtmrz/obsidian-livesync/releases/download/0.25.83/styles.css",
|
||||
sha256: "37d31798186d7e97ea979e6d2aae8021ea1ac1df2c3b9d2b03dce269959c27f3",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function digest(content: Uint8Array<ArrayBuffer>): string {
|
||||
return createHash("sha256").update(content).digest("hex");
|
||||
}
|
||||
|
||||
function assertDigest(file: PinnedReleaseArtifactFile, content: Uint8Array<ArrayBuffer>): void {
|
||||
const actual = digest(content);
|
||||
if (actual !== file.sha256) {
|
||||
throw new Error(
|
||||
`Release artefact checksum mismatch for ${file.name}. Expected ${file.sha256}, received ${actual}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function readCachedFile(
|
||||
path: string,
|
||||
file: PinnedReleaseArtifactFile
|
||||
): Promise<Uint8Array<ArrayBuffer> | undefined> {
|
||||
try {
|
||||
const content = new Uint8Array(await readFile(path));
|
||||
assertDigest(file, content);
|
||||
return content;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadVerifiedFile(
|
||||
root: string,
|
||||
file: PinnedReleaseArtifactFile,
|
||||
fetchImplementation: typeof fetch
|
||||
): Promise<Uint8Array<ArrayBuffer>> {
|
||||
const path = join(root, file.name);
|
||||
const cached = await readCachedFile(path, file);
|
||||
if (cached) return cached;
|
||||
|
||||
const response = await fetchImplementation(file.url, { redirect: "follow" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not download ${file.url}. HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
const content = new Uint8Array(await response.arrayBuffer());
|
||||
assertDigest(file, content);
|
||||
|
||||
const temporaryPath = `${path}.download-${process.pid}-${Date.now()}`;
|
||||
try {
|
||||
await writeFile(temporaryPath, content, { flag: "wx" });
|
||||
await rename(temporaryPath, path);
|
||||
} finally {
|
||||
await rm(temporaryPath, { force: true });
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialise one immutable published plug-in release in the ignored E2E cache.
|
||||
*
|
||||
* Existing files are always verified before use. A mismatched cache is left in
|
||||
* place for inspection and must be removed explicitly by the operator.
|
||||
*/
|
||||
export async function ensurePinnedReleaseArtifact(
|
||||
release: PinnedPluginRelease = UPGRADE_SOURCE_RELEASE,
|
||||
options: EnsurePinnedReleaseArtifactOptions = {}
|
||||
): Promise<string> {
|
||||
const root = resolve(
|
||||
options.artifactRoot ??
|
||||
process.env.E2E_LIVESYNC_SOURCE_ARTIFACT_ROOT?.trim() ??
|
||||
join("_testdata", "releases", release.pluginId, release.version)
|
||||
);
|
||||
await mkdir(root, { recursive: true });
|
||||
|
||||
const fetched = new Map<string, Uint8Array<ArrayBuffer>>();
|
||||
for (const file of release.files) {
|
||||
fetched.set(file.name, await downloadVerifiedFile(root, file, options.fetchImplementation ?? fetch));
|
||||
}
|
||||
|
||||
const manifestBytes = fetched.get("manifest.json");
|
||||
if (!manifestBytes) throw new Error("The pinned release does not define manifest.json.");
|
||||
const manifest = JSON.parse(new TextDecoder().decode(manifestBytes)) as { id?: unknown; version?: unknown };
|
||||
if (manifest.id !== release.pluginId || manifest.version !== release.version) {
|
||||
throw new Error(
|
||||
`Release manifest identity mismatch. Expected ${release.pluginId}@${release.version}, received ${String(manifest.id)}@${String(manifest.version)}.`
|
||||
);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
Reference in New Issue
Block a user