From 59723b3a5cab3037ef51a7511c3883c6751f6238 Mon Sep 17 00:00:00 2001 From: royorange Date: Tue, 6 May 2025 19:05:04 +0800 Subject: [PATCH 1/2] Fix SQL dump validation logic in module.ts --- src/module.ts | 75 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/src/module.ts b/src/module.ts index 0e784f304..393fb7e5e 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,4 +1,6 @@ -import { stat } from 'node:fs/promises' +import { stat, readFile } from 'node:fs/promises' +import zlib from 'node:zlib' +import { promisify } from 'node:util' import { defineNuxtModule, createResolver, @@ -208,7 +210,6 @@ export default defineNuxtModule({ // `modules:done` is triggered for all environments nuxt.hook('modules:done', async () => { const fest = await processCollectionItems(nuxt, manifest.collections, options) - // Update manifest manifest.checksumStructure = fest.checksumStructure manifest.checksum = fest.checksum @@ -229,9 +230,79 @@ export default defineNuxtModule({ await setupPreview(options, nuxt, resolver, manifest) } }) + + nuxt.hook('nitro:build:public-assets', async () => { + try { + const fest = await processCollectionItems(nuxt, manifest.collections, options) + // validate content + for (const collection of manifest.collections) { + if (!collection.private) { + // load the compressed dump + const route = `/__nuxt_content/${collection.name}/sql_dump` + const outputPath = `.output/public${route}` + try { + const stats = await stat(outputPath) + let path = outputPath + if (stats.isDirectory()) { + path = join(outputPath, 'index.html') + } + // decompress content and validate + await validateContent(path, fest.dump[collection.name]) + } + catch (error) { + console.error(`Failed to read file ${outputPath}: ${error}`) + throw error + } + } + } + } + catch (error) { + logger.error('Build process terminated due to validation failure:', error) + process.exit(1) + } + }) }, }) +const gunzip = promisify(zlib.gunzip) +// decompress content +async function decompressContent(content: string): Promise { + const buffer = Buffer.from(content, 'base64') + const decompressed = await gunzip(buffer) + return decompressed.toString('utf-8') +} + +// validate content is same as the original +async function validateContent(dumpPath: string, dump: Array) { + try { + // read the compressed content + const compressedContent = await readFile(dumpPath, 'utf-8') + + // decompress the content + const decompressedContent = await decompressContent(compressedContent) + + // join the dump array into a single string + const dumpSQLContent = dump.join('\n') + if (dumpSQLContent === decompressedContent) { + logger.success(`Content of ${dumpPath} checks out OK`) + } + else { + logger.error(`Content of ${dumpPath} does not match`) + throw new Error(`Validation failed for ${dumpPath}: Content does not match`) + } + } + catch (error: unknown) { + if (error instanceof Error) { + logger.error(`Error validating content of ${dumpPath}:`, error.message, 'please check your config or server/middleware') + throw error + } + else { + logger.error(`Error validating content of ${dumpPath}:`, error) + throw new Error(`Unknown error occurred during validation of ${dumpPath}`) + } + } +} + async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollection[], options: ModuleOptions) { const collectionDump: Record = {} const collectionChecksum: Record = {} From 1280e3077ce554b7515c03b7cab8faccfbb19c21 Mon Sep 17 00:00:00 2001 From: royorange Date: Fri, 9 May 2025 17:59:09 +0800 Subject: [PATCH 2/2] Add storage reset logic when sql decompress failed in database.client.ts --- src/runtime/internal/database.client.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/runtime/internal/database.client.ts b/src/runtime/internal/database.client.ts index 62ec962e4..839c2c51b 100644 --- a/src/runtime/internal/database.client.ts +++ b/src/runtime/internal/database.client.ts @@ -86,6 +86,7 @@ async function loadCollectionDatabase(collection: T) { const checksumId = `checksum_${collection}` const dumpId = `collection_${collection}` let checksumState = 'matched' + try { const dbChecksum = db.exec({ sql: `SELECT * FROM ${tables.info} where id = '${checksumId}'`, rowMode: 'object', returnValue: 'resultRows' }) .shift() @@ -119,7 +120,19 @@ async function loadCollectionDatabase(collection: T) { } } - const dump = await decompressSQLDump(compressedDump!) + let dump + try { + dump = await decompressSQLDump(compressedDump!) + } + catch (error) { + console.error('Error decompress SQLDump', error) + // reset the local cache + if (!import.meta.dev) { + window.localStorage.removeItem(`content_${checksumId}`) + window.localStorage.removeItem(`content_${dumpId}`) + } + throw error + } await db.exec({ sql: `DROP TABLE IF EXISTS ${tables[String(collection)]}` }) if (checksumState === 'mismatch') {