mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-27 13:53:44 +00:00
157 lines
4.4 KiB
JavaScript
157 lines
4.4 KiB
JavaScript
|
#!/usr/bin/env node
|
||
|
|
||
|
'use strict'
|
||
|
|
||
|
const commandLineArgs = require('command-line-args')
|
||
|
const { chunkStore } = require('..')
|
||
|
|
||
|
main()
|
||
|
.then(() => {
|
||
|
process.exit(0)
|
||
|
})
|
||
|
.catch(err => {
|
||
|
console.error(err)
|
||
|
process.exit(1)
|
||
|
})
|
||
|
|
||
|
async function main() {
|
||
|
const opts = commandLineArgs([
|
||
|
{ name: 'project-ids', type: String, multiple: true, defaultOption: true },
|
||
|
{ name: 'save', type: Boolean, defaultValue: false },
|
||
|
{ name: 'help', type: Boolean, defaultValue: false },
|
||
|
])
|
||
|
if (opts.help || opts['project-ids'] == null) {
|
||
|
console.log('Usage: fix_duplicate_versions [--save] PROJECT_ID...')
|
||
|
process.exit()
|
||
|
}
|
||
|
for (const projectId of opts['project-ids']) {
|
||
|
await processProject(projectId, opts.save)
|
||
|
}
|
||
|
if (!opts.save) {
|
||
|
console.log('\nThis was a dry run. Re-run with --save to persist changes.')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async function processProject(projectId, save) {
|
||
|
console.log(`Project ${projectId}:`)
|
||
|
const chunk = await chunkStore.loadLatest(projectId)
|
||
|
let numChanges = 0
|
||
|
numChanges += removeDuplicateProjectVersions(chunk)
|
||
|
numChanges += removeDuplicateDocVersions(chunk)
|
||
|
console.log(` ${numChanges > 0 ? numChanges : 'no'} changes`)
|
||
|
if (save && numChanges > 0) {
|
||
|
await replaceChunk(projectId, chunk)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function removeDuplicateProjectVersions(chunk) {
|
||
|
let numChanges = 0
|
||
|
let lastVersion = null
|
||
|
const { snapshot, changes } = chunk.history
|
||
|
if (snapshot.projectVersion != null) {
|
||
|
lastVersion = snapshot.projectVersion
|
||
|
}
|
||
|
for (const change of changes) {
|
||
|
if (change.projectVersion == null) {
|
||
|
// Not a project structure change. Ignore.
|
||
|
continue
|
||
|
}
|
||
|
if (
|
||
|
lastVersion != null &&
|
||
|
!areProjectVersionsIncreasing(lastVersion, change.projectVersion)
|
||
|
) {
|
||
|
// Duplicate. Remove all ops
|
||
|
console.log(
|
||
|
` Removing out-of-order project structure change: ${change.projectVersion} <= ${lastVersion}`
|
||
|
)
|
||
|
change.setOperations([])
|
||
|
delete change.projectVersion
|
||
|
numChanges++
|
||
|
} else {
|
||
|
lastVersion = change.projectVersion
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return numChanges
|
||
|
}
|
||
|
|
||
|
function removeDuplicateDocVersions(chunk) {
|
||
|
let numChanges = 0
|
||
|
const lastVersions = new Map()
|
||
|
const { snapshot, changes } = chunk.history
|
||
|
if (snapshot.v2DocVersions != null) {
|
||
|
for (const { pathname, v } of Object.values(snapshot.v2DocVersions.data)) {
|
||
|
lastVersions.set(pathname, v)
|
||
|
}
|
||
|
}
|
||
|
for (const change of changes) {
|
||
|
if (change.v2DocVersions == null) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Collect all docs that have problematic versions
|
||
|
const badPaths = []
|
||
|
const badDocIds = []
|
||
|
for (const [docId, { pathname, v }] of Object.entries(
|
||
|
change.v2DocVersions.data
|
||
|
)) {
|
||
|
const lastVersion = lastVersions.get(docId)
|
||
|
if (lastVersion != null && v <= lastVersion) {
|
||
|
// Duplicate. Remove ops related to that doc
|
||
|
console.log(
|
||
|
` Removing out-of-order change for doc ${docId} (${pathname}): ${v} <= ${lastVersion}`
|
||
|
)
|
||
|
badPaths.push(pathname)
|
||
|
badDocIds.push(docId)
|
||
|
numChanges++
|
||
|
} else {
|
||
|
lastVersions.set(docId, v)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Remove bad operations
|
||
|
if (badPaths.length > 0) {
|
||
|
change.setOperations(
|
||
|
change.operations.filter(
|
||
|
op => op.pathname == null || !badPaths.includes(op.pathname)
|
||
|
)
|
||
|
)
|
||
|
}
|
||
|
|
||
|
// Remove bad v2 doc versions
|
||
|
for (const docId of badDocIds) {
|
||
|
delete change.v2DocVersions.data[docId]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return numChanges
|
||
|
}
|
||
|
|
||
|
function areProjectVersionsIncreasing(v1Str, v2Str) {
|
||
|
const v1 = parseProjectVersion(v1Str)
|
||
|
const v2 = parseProjectVersion(v2Str)
|
||
|
return v2.major > v1.major || (v2.major === v1.major && v2.minor > v1.minor)
|
||
|
}
|
||
|
|
||
|
function parseProjectVersion(version) {
|
||
|
const [major, minor] = version.split('.').map(x => parseInt(x, 10))
|
||
|
if (isNaN(major) || isNaN(minor)) {
|
||
|
throw new Error(`Invalid project version: ${version}`)
|
||
|
}
|
||
|
return { major, minor }
|
||
|
}
|
||
|
|
||
|
async function replaceChunk(projectId, chunk) {
|
||
|
const endVersion = chunk.getEndVersion()
|
||
|
const oldChunkId = await chunkStore.getChunkIdForVersion(
|
||
|
projectId,
|
||
|
endVersion
|
||
|
)
|
||
|
console.log(` Replacing chunk ${oldChunkId}`)
|
||
|
// The chunks table has a unique constraint on doc_id and end_version. Because
|
||
|
// we're replacing a chunk with the same end version, we need to destroy the
|
||
|
// chunk first.
|
||
|
await chunkStore.destroy(projectId, oldChunkId)
|
||
|
await chunkStore.create(projectId, chunk)
|
||
|
}
|