Skip to content

fix: add sql dump validation and reset the wrong dump when decompression fails #3342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -208,7 +210,6 @@ export default defineNuxtModule<ModuleOptions>({
// `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
Expand All @@ -229,9 +230,79 @@ export default defineNuxtModule<ModuleOptions>({
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<string> {
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<string>) {
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<string, string[]> = {}
const collectionChecksum: Record<string, string> = {}
Expand Down
15 changes: 14 additions & 1 deletion src/runtime/internal/database.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ async function loadCollectionDatabase<T>(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()
Expand Down Expand Up @@ -119,7 +120,19 @@ async function loadCollectionDatabase<T>(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') {
Expand Down