mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-26 18:51:50 +02:00
* [misc] freeze time before any other unit test setup steps Freezing it after other work (notably sandboxed-module imports) will result in flaky tests. * [web] update the projects lastUpdated timestamp when changing file-tree GitOrigin-RevId: b82b2ff74dc31886f3c4bd300375117eead6e0cd
200 lines
5.2 KiB
JavaScript
200 lines
5.2 KiB
JavaScript
const { callbackify } = require('util')
|
|
const _ = require('lodash')
|
|
const fsPromises = require('fs/promises')
|
|
const fs = require('fs')
|
|
const logger = require('@overleaf/logger')
|
|
const EditorController = require('../Editor/EditorController')
|
|
const FileTypeManager = require('../Uploads/FileTypeManager')
|
|
const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
|
|
const crypto = require('crypto')
|
|
const Settings = require('@overleaf/settings')
|
|
const { pipeline } = require('stream/promises')
|
|
|
|
async function mergeUpdate(userId, projectId, path, updateRequest, source) {
|
|
const fsPath = await writeUpdateToDisk(projectId, updateRequest)
|
|
|
|
try {
|
|
// note: important to await here so file reading finishes before cleaning up below
|
|
return await _mergeUpdate(userId, projectId, path, fsPath, source)
|
|
} finally {
|
|
// note: not awaited or thrown
|
|
fsPromises.unlink(fsPath).catch(err => {
|
|
logger.err({ err, projectId, fsPath }, 'error deleting file')
|
|
})
|
|
}
|
|
}
|
|
|
|
async function writeUpdateToDisk(projectId, updateStream) {
|
|
const fsPath = `${
|
|
Settings.path.dumpFolder
|
|
}/${projectId}_${crypto.randomUUID()}`
|
|
const writeStream = fs.createWriteStream(fsPath)
|
|
try {
|
|
await pipeline(updateStream, writeStream)
|
|
} catch (err) {
|
|
try {
|
|
await fsPromises.unlink(fsPath)
|
|
} catch (err) {
|
|
logger.error({ err, projectId, fsPath }, 'error deleting file')
|
|
}
|
|
throw err
|
|
}
|
|
return fsPath
|
|
}
|
|
|
|
async function _findExistingFileType(projectId, path) {
|
|
const { docs, files } =
|
|
await ProjectEntityHandler.promises.getAllEntities(projectId)
|
|
if (_.some(docs, d => d.path === path)) {
|
|
return 'doc'
|
|
}
|
|
if (_.some(files, f => f.path === path)) {
|
|
return 'file'
|
|
}
|
|
return null
|
|
}
|
|
|
|
async function _determineFileType(projectId, path, fsPath) {
|
|
// check if there is an existing file with the same path (we either need
|
|
// to overwrite it or delete it)
|
|
const existingFileType = await _findExistingFileType(projectId, path)
|
|
|
|
// determine whether the update should create a doc or binary file
|
|
const { binary, encoding } = await FileTypeManager.promises.getType(
|
|
path,
|
|
fsPath,
|
|
existingFileType
|
|
)
|
|
|
|
// If we receive a non-utf8 encoding, we won't be able to keep things in
|
|
// sync, so we'll treat non-utf8 files as binary
|
|
const isBinary = binary || encoding !== 'utf-8'
|
|
|
|
// Existing | Update | Resulting file type
|
|
// ---------|-----------|--------------------
|
|
// file | isBinary | file
|
|
// file | !isBinary | file
|
|
// doc | isBinary | file
|
|
// doc | !isBinary | doc
|
|
// null | isBinary | file
|
|
// null | !isBinary | doc
|
|
|
|
// if a binary file already exists, always keep it as a binary file
|
|
// even if the update looks like a text file
|
|
if (existingFileType === 'file') {
|
|
return 'file'
|
|
} else {
|
|
return isBinary ? 'file' : 'doc'
|
|
}
|
|
}
|
|
|
|
async function _mergeUpdate(userId, projectId, path, fsPath, source) {
|
|
const fileType = await _determineFileType(projectId, path, fsPath)
|
|
|
|
if (fileType === 'file') {
|
|
const { file, folder } = await _processFile(
|
|
projectId,
|
|
fsPath,
|
|
path,
|
|
source,
|
|
userId
|
|
)
|
|
return {
|
|
projectId,
|
|
entityType: 'file',
|
|
entityId: file._id,
|
|
rev: file.rev,
|
|
folderId: folder._id,
|
|
}
|
|
} else if (fileType === 'doc') {
|
|
const { doc, folder } = await _processDoc(
|
|
projectId,
|
|
userId,
|
|
fsPath,
|
|
path,
|
|
source
|
|
)
|
|
return {
|
|
projectId,
|
|
entityType: 'doc',
|
|
entityId: doc._id,
|
|
rev: doc.rev,
|
|
folderId: folder._id,
|
|
}
|
|
} else {
|
|
throw new Error('unrecognized file')
|
|
}
|
|
}
|
|
|
|
async function deleteUpdate(userId, projectId, path, source) {
|
|
try {
|
|
return await EditorController.promises.deleteEntityWithPath(
|
|
projectId,
|
|
path,
|
|
source,
|
|
userId
|
|
)
|
|
} catch (err) {
|
|
logger.warn(
|
|
{ err, userId, projectId, path, source },
|
|
'failed to delete entity'
|
|
)
|
|
}
|
|
}
|
|
|
|
async function _processDoc(projectId, userId, fsPath, path, source) {
|
|
const docLines = await _readFileIntoTextArray(fsPath)
|
|
logger.debug({ docLines }, 'processing doc update from tpds')
|
|
const doc = await EditorController.promises.upsertDocWithPath(
|
|
projectId,
|
|
path,
|
|
docLines,
|
|
source,
|
|
userId
|
|
)
|
|
return doc
|
|
}
|
|
|
|
async function _processFile(projectId, fsPath, path, source, userId) {
|
|
const { file, folder } = await EditorController.promises.upsertFileWithPath(
|
|
projectId,
|
|
path,
|
|
fsPath,
|
|
null,
|
|
source,
|
|
userId
|
|
)
|
|
return { file, folder }
|
|
}
|
|
|
|
async function _readFileIntoTextArray(path) {
|
|
let content = await fsPromises.readFile(path, 'utf8')
|
|
if (content == null) {
|
|
content = ''
|
|
}
|
|
const lines = content.split(/\r\n|\n|\r/)
|
|
return lines
|
|
}
|
|
|
|
async function createFolder(projectId, path, userId) {
|
|
const { lastFolder: folder } = await EditorController.promises.mkdirp(
|
|
projectId,
|
|
path,
|
|
userId
|
|
)
|
|
return folder
|
|
}
|
|
|
|
module.exports = {
|
|
mergeUpdate: callbackify(mergeUpdate),
|
|
_mergeUpdate: callbackify(_mergeUpdate),
|
|
deleteUpdate: callbackify(deleteUpdate),
|
|
createFolder: callbackify(createFolder),
|
|
promises: {
|
|
mergeUpdate,
|
|
_mergeUpdate, // called by GitBridgeHandler
|
|
deleteUpdate,
|
|
createFolder,
|
|
},
|
|
}
|