| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- 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))
- })
|