Files
overleaf-cep/services/history-v1/storage/scripts/redact.mjs
T
Brian Gough 367e8d53b2 Merge pull request #32702 from overleaf/bg-allow-redacting-blobs
add script for redacting unwanted blobs

GitOrigin-RevId: cddbeb4d27546b7cb98634ab364cc8dad0ada76c
2026-04-10 08:03:52 +00:00

145 lines
4.1 KiB
JavaScript

import fs from 'node:fs'
import { Readable } from 'node:stream'
import { createRequire } from 'node:module'
import * as readline from 'node:readline/promises'
import commandLineArgs from 'command-line-args'
import { makeProjectKey } from '../lib/blob_store/index.js'
import { client } from '../lib/mongodb.js'
import knex from '../lib/knex.js'
import redis from '../lib/redis.js'
const require = createRequire(import.meta.url)
const config = require('config')
const persistor = require('../lib/persistor.js')
const { Errors } = require('@overleaf/object-persistor')
const optionDefinitions = [
{ name: 'historyId', alias: 'p', type: String },
{ name: 'blob', alias: 'b', type: String },
{ name: 'file', alias: 'f', type: String },
{ name: 'empty', alias: 'e', type: Boolean },
{ name: 'delete', alias: 'd', type: Boolean },
{ name: 'yes', alias: 'y', type: Boolean },
{ name: 'message', alias: 'm', type: String },
]
async function replaceBlob(historyId, blobHash, options) {
const bucket = config.get('blobStore.projectBucket')
const key = makeProjectKey(historyId, blobHash)
// 1. Check existence
let originalSize
try {
originalSize = await persistor.getObjectSize(bucket, key)
console.log(`Found blob ${blobHash} of size ${originalSize} bytes`)
} catch (err) {
if (
err instanceof Errors.NotFoundError ||
err.code === 'NoSuchKey' ||
err.name === 'NoSuchKey'
) {
throw new Error(`Blob ${blobHash} not found in project ${historyId}`)
}
throw err
}
// 2. Prepare action
let stream
let streamSize
let actionDesc
if (!options.delete) {
if (options.empty) {
stream = Readable.from([])
streamSize = 0
actionDesc = 'empty file'
} else if (options.file) {
const stat = fs.statSync(options.file)
stream = fs.createReadStream(options.file)
streamSize = stat.size
actionDesc = `file ${options.file}`
} else {
const baseMessage = options.message || 'REDACTED'
const msg = `${baseMessage} ${new Date().toISOString()}`
const buf = Buffer.from(msg, 'utf8')
stream = Readable.from([buf])
streamSize = buf.length
actionDesc = `message "${msg}"`
}
}
const actionLog = options.delete
? `Deleting blob ${blobHash} in ${historyId}`
: `Replacing blob ${blobHash} in ${historyId} with ${actionDesc} (${streamSize} bytes)`
console.log(actionLog)
if (!options.yes) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
const answer = await rl.question('Proceed (Y/N)? ')
rl.close()
if (answer.toLowerCase() !== 'y') {
console.log('Aborted.')
return
}
}
// 3. Execute action
if (options.delete) {
await persistor.deleteObject(bucket, key)
console.log('Blob deleted successfully.')
} else {
await persistor.sendStream(bucket, key, stream, {
contentType: 'application/octet-stream',
contentLength: streamSize,
})
console.log('Blob replaced successfully.')
}
}
async function main() {
const options = commandLineArgs(optionDefinitions)
if (!options.historyId) {
console.error('Error: --historyId is required.')
process.exit(1)
}
if (!options.blob) {
console.error('Error: --blob is required.')
process.exit(1)
}
const activeModes = [
options.delete ? '--delete' : null,
options.empty ? '--empty' : null,
options.file ? '--file' : null,
options.message !== undefined ? '--message' : null,
].filter(Boolean)
if (activeModes.length > 1) {
console.error(
`Error: Conflicting options provided (${activeModes.join(
', '
)}). Please select exactly one redaction mode.`
)
process.exit(1)
}
await replaceBlob(options.historyId, options.blob, options)
}
main()
.then(() => console.log('Done.'))
.catch(err => {
console.error('Error:', err.message)
process.exit(1)
})
.finally(() => {
knex.destroy().catch(err => console.error('Error closing Postgres:', err))
client.close().catch(err => console.error('Error closing MongoDB:', err))
redis
.disconnect()
.catch(err => console.error('Error disconnecting Redis:', err))
})