fix large core update snapshots
This commit is contained in:
parent
7f86055fe4
commit
782b93b3c1
1
TODO.md
1
TODO.md
@ -80,6 +80,7 @@ This file tracks larger Lumi work that cannot safely be completed in one pass. K
|
|||||||
|
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
|
- 2026-06-17: Fixed core update snapshots for large ZIP-origin installs by replacing the full-install ZIP backup with a filesystem snapshot directory, avoiding the 2 GiB ZIP limit for large preserved files such as local AI models.
|
||||||
- 2026-06-17: Completed `/admin/updates` UX pass: viewport-fixed dismissible notifications, auto-dismiss for non-critical results, async core/plugin check actions without page refresh or scroll jumps, in-place update-card data refresh, loading states, and collapsed advanced Manual ZIP fallback below repo update containers.
|
- 2026-06-17: Completed `/admin/updates` UX pass: viewport-fixed dismissible notifications, auto-dismiss for non-critical results, async core/plugin check actions without page refresh or scroll jumps, in-place update-card data refresh, loading states, and collapsed advanced Manual ZIP fallback below repo update containers.
|
||||||
- 2026-06-17: Completed homepage hero reliability pass: server-side hero validation before save, admin-visible validation errors, home-page fallback message for broken legacy heroes, automatic YouTube/Twitch/Discord embed derivation, correct Twitch parent host at render time, and image/embed conflict handling.
|
- 2026-06-17: Completed homepage hero reliability pass: server-side hero validation before save, admin-visible validation errors, home-page fallback message for broken legacy heroes, automatic YouTube/Twitch/Discord embed derivation, correct Twitch parent host at render time, and image/embed conflict handling.
|
||||||
- 2026-06-17: Completed homepage hero builder UX pass: friendlier labels, contextual help text, normal URL paste support, automatic embed filling, per-row readiness messages, and test preview support.
|
- 2026-06-17: Completed homepage hero builder UX pass: friendlier labels, contextual help text, normal URL paste support, automatic embed filling, per-row readiness messages, and test preview support.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.12",
|
"adm-zip": "^0.5.12",
|
||||||
"better-sqlite3": "^11.5.0",
|
"better-sqlite3": "^11.5.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -51,6 +51,13 @@ const GENERATED_RELATIVE_PATHS = new Set([
|
|||||||
"temp",
|
"temp",
|
||||||
"data/update-cache"
|
"data/update-cache"
|
||||||
]);
|
]);
|
||||||
|
const SNAPSHOT_EXCLUDE_RELATIVE_PATHS = new Set([
|
||||||
|
".git",
|
||||||
|
"node_modules",
|
||||||
|
"data/update-cache",
|
||||||
|
"data/snapshots"
|
||||||
|
]);
|
||||||
|
const HARDLINK_SNAPSHOT_MIN_BYTES = 64 * 1024 * 1024;
|
||||||
|
|
||||||
function ensureSnapshotsDir() {
|
function ensureSnapshotsDir() {
|
||||||
fs.mkdirSync(snapshotsDir, { recursive: true });
|
fs.mkdirSync(snapshotsDir, { recursive: true });
|
||||||
@ -96,12 +103,12 @@ async function createSnapshot({ type, pluginId, metadata = {} }) {
|
|||||||
|
|
||||||
let pluginExisted = false;
|
let pluginExisted = false;
|
||||||
let pluginZip = null;
|
let pluginZip = null;
|
||||||
let fullZip = null;
|
let fullPath = null;
|
||||||
if (type === "bot") {
|
if (type === "bot") {
|
||||||
const coreZip = path.join(snapshotPath, "core.zip");
|
const coreZip = path.join(snapshotPath, "core.zip");
|
||||||
zipCore(coreZip);
|
zipCore(coreZip);
|
||||||
fullZip = path.join(snapshotPath, "full.zip");
|
fullPath = path.join(snapshotPath, "full");
|
||||||
zipFullInstall(fullZip);
|
snapshotFullInstall(fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "plugin" && pluginId) {
|
if (type === "plugin" && pluginId) {
|
||||||
@ -116,7 +123,7 @@ async function createSnapshot({ type, pluginId, metadata = {} }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { id, type, pluginId, pluginExisted, pluginZip, fullZip, snapshotPath, metadata };
|
return { id, type, pluginId, pluginExisted, pluginZip, fullPath, snapshotPath, metadata };
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalizeSnapshot(snapshot) {
|
function finalizeSnapshot(snapshot) {
|
||||||
@ -289,15 +296,6 @@ function zipCore(destination) {
|
|||||||
zip.writeZip(destination);
|
zip.writeZip(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
function zipFullInstall(destination) {
|
|
||||||
if (!AdmZip) {
|
|
||||||
throw new Error("adm-zip is not installed. Run npm install.");
|
|
||||||
}
|
|
||||||
const zip = new AdmZip();
|
|
||||||
addFolder(zip, repoRoot, repoRoot, new Set([".git", "node_modules", "data/update-cache", "data/snapshots"]));
|
|
||||||
zip.writeZip(destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
function zipFolder(source, destination, options) {
|
function zipFolder(source, destination, options) {
|
||||||
if (!AdmZip) {
|
if (!AdmZip) {
|
||||||
throw new Error("adm-zip is not installed. Run npm install.");
|
throw new Error("adm-zip is not installed. Run npm install.");
|
||||||
@ -308,6 +306,11 @@ function zipFolder(source, destination, options) {
|
|||||||
zip.writeZip(destination);
|
zip.writeZip(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapshotFullInstall(destination) {
|
||||||
|
fs.mkdirSync(destination, { recursive: true });
|
||||||
|
copySnapshotDirectory(repoRoot, destination, SNAPSHOT_EXCLUDE_RELATIVE_PATHS, { base: repoRoot });
|
||||||
|
}
|
||||||
|
|
||||||
function addFolder(zip, folderPath, basePath, ignore) {
|
function addFolder(zip, folderPath, basePath, ignore) {
|
||||||
const entries = fs.readdirSync(folderPath, { withFileTypes: true });
|
const entries = fs.readdirSync(folderPath, { withFileTypes: true });
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@ -374,6 +377,39 @@ function copyDirectory(source, target, ignore, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copySnapshotDirectory(source, target, ignore, options = {}) {
|
||||||
|
const base = options.base || source;
|
||||||
|
const entries = fs.readdirSync(source, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const srcPath = path.join(source, entry.name);
|
||||||
|
const relPath = normalizeRelative(path.relative(base, srcPath));
|
||||||
|
if (isRelativePathIgnored(relPath, ignore)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const destPath = path.join(target, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
fs.mkdirSync(destPath, { recursive: true });
|
||||||
|
copySnapshotDirectory(srcPath, destPath, ignore, { base });
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
||||||
|
copySnapshotFile(srcPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copySnapshotFile(source, target) {
|
||||||
|
const stat = fs.statSync(source);
|
||||||
|
if (stat.size >= HARDLINK_SNAPSHOT_MIN_BYTES) {
|
||||||
|
try {
|
||||||
|
fs.linkSync(source, target);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// Fall back to copying when hard links are unavailable across devices.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.copyFileSync(source, target);
|
||||||
|
}
|
||||||
|
|
||||||
function removeGeneratedPaths() {
|
function removeGeneratedPaths() {
|
||||||
for (const relativePath of GENERATED_RELATIVE_PATHS) {
|
for (const relativePath of GENERATED_RELATIVE_PATHS) {
|
||||||
const target = path.join(repoRoot, relativePath);
|
const target = path.join(repoRoot, relativePath);
|
||||||
@ -593,6 +629,13 @@ function restoreSnapshot(id, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (entry.type === "bot") {
|
if (entry.type === "bot") {
|
||||||
|
const fullPath = path.join(entry.path, "full");
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
applyCoreFilesFromDirectory(fullPath);
|
||||||
|
restoreDatabase(entry.path);
|
||||||
|
markSnapshotRolledBack(id);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
const fullZip = path.join(entry.path, "full.zip");
|
const fullZip = path.join(entry.path, "full.zip");
|
||||||
const coreZip = path.join(entry.path, "core.zip");
|
const coreZip = path.join(entry.path, "core.zip");
|
||||||
const restoreZip = fs.existsSync(fullZip) ? fullZip : coreZip;
|
const restoreZip = fs.existsSync(fullZip) ? fullZip : coreZip;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user