| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725 |
- const _ = require('lodash')
- const OError = require('@overleaf/o-error')
- const async = require('async')
- const logger = require('logger-sharelatex')
- const Settings = require('settings-sharelatex')
- const Path = require('path')
- const fs = require('fs')
- const { Doc } = require('../../models/Doc')
- const DocstoreManager = require('../Docstore/DocstoreManager')
- const DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
- const Errors = require('../Errors/Errors')
- const FileStoreHandler = require('../FileStore/FileStoreHandler')
- const LockManager = require('../../infrastructure/LockManager')
- const { Project } = require('../../models/Project')
- const ProjectEntityHandler = require('./ProjectEntityHandler')
- const ProjectGetter = require('./ProjectGetter')
- const ProjectLocator = require('./ProjectLocator')
- const ProjectUpdateHandler = require('./ProjectUpdateHandler')
- const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler')
- const SafePath = require('./SafePath')
- const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
- const FileWriter = require('../../infrastructure/FileWriter')
- const EditorRealTimeController = require('../Editor/EditorRealTimeController')
- const { promisifyAll } = require('../../util/promises')
- const LOCK_NAMESPACE = 'sequentialProjectStructureUpdateLock'
- const VALID_ROOT_DOC_EXTENSIONS = Settings.validRootDocExtensions
- const VALID_ROOT_DOC_REGEXP = new RegExp(
- `^\\.(${VALID_ROOT_DOC_EXTENSIONS.join('|')})$`,
- 'i'
- )
- function wrapWithLock(methodWithoutLock) {
- // This lock is used to make sure that the project structure updates are made
- // sequentially. In particular the updates must be made in mongo and sent to
- // the doc-updater in the same order.
- if (typeof methodWithoutLock === 'function') {
- const methodWithLock = (projectId, ...rest) => {
- const adjustedLength = Math.max(rest.length, 1)
- const args = rest.slice(0, adjustedLength - 1)
- const callback = rest[adjustedLength - 1]
- LockManager.runWithLock(
- LOCK_NAMESPACE,
- projectId,
- cb => methodWithoutLock(projectId, ...args, cb),
- callback
- )
- }
- methodWithLock.withoutLock = methodWithoutLock
- return methodWithLock
- } else {
- // handle case with separate setup and locked stages
- const wrapWithSetup = methodWithoutLock.beforeLock // a function to set things up before the lock
- const mainTask = methodWithoutLock.withLock // function to execute inside the lock
- const methodWithLock = wrapWithSetup((projectId, ...rest) => {
- const adjustedLength = Math.max(rest.length, 1)
- const args = rest.slice(0, adjustedLength - 1)
- const callback = rest[adjustedLength - 1]
- LockManager.runWithLock(
- LOCK_NAMESPACE,
- projectId,
- cb => mainTask(projectId, ...args, cb),
- callback
- )
- })
- methodWithLock.withoutLock = wrapWithSetup(mainTask)
- methodWithLock.beforeLock = methodWithoutLock.beforeLock
- methodWithLock.mainTask = methodWithoutLock.withLock
- return methodWithLock
- }
- }
- function getDocContext(projectId, docId, callback) {
- ProjectGetter.getProject(
- projectId,
- { name: true, rootFolder: true },
- (err, project) => {
- if (err) {
- return callback(
- OError.tag(err, 'error fetching project', {
- projectId,
- })
- )
- }
- if (!project) {
- return callback(new Errors.NotFoundError('project not found'))
- }
- ProjectLocator.findElement(
- { project, element_id: docId, type: 'docs' },
- (err, doc, path) => {
- if (err && err instanceof Errors.NotFoundError) {
- // (Soft-)Deleted docs are removed from the file-tree (rootFolder).
- // docstore can tell whether it exists and is (soft)-deleted.
- DocstoreManager.isDocDeleted(
- projectId,
- docId,
- (err, isDeletedDoc) => {
- if (err && err instanceof Errors.NotFoundError) {
- logger.warn(
- { projectId, docId },
- 'doc not found while updating doc lines'
- )
- callback(err)
- } else if (err) {
- callback(
- OError.tag(
- err,
- 'error checking deletion status with docstore',
- { projectId, docId }
- )
- )
- } else {
- if (!isDeletedDoc) {
- // NOTE: This can happen while we delete a doc:
- // 1. web will update the projects entry
- // 2. web triggers flushes to tpds/doc-updater
- // 3. web triggers (soft)-delete in docstore
- // Specifically when an update comes in after 1
- // and before 3 completes.
- logger.info(
- { projectId, docId },
- 'updating doc that is in process of getting soft-deleted'
- )
- }
- callback(null, {
- projectName: project.name,
- isDeletedDoc: true,
- path: null,
- })
- }
- }
- )
- } else if (err) {
- callback(
- OError.tag(err, 'error finding doc in rootFolder', {
- docId,
- projectId,
- })
- )
- } else {
- callback(null, {
- projectName: project.name,
- isDeletedDoc: false,
- path: path.fileSystem,
- })
- }
- }
- )
- }
- )
- }
- const ProjectEntityUpdateHandler = {
- updateDocLines(
- projectId,
- docId,
- lines,
- version,
- ranges,
- lastUpdatedAt,
- lastUpdatedBy,
- callback
- ) {
- getDocContext(projectId, docId, (err, ctx) => {
- if (err && err instanceof Errors.NotFoundError) {
- // Do not allow an update to a doc which has never exist on this project
- logger.warn(
- { docId, projectId },
- 'project or doc not found while updating doc lines'
- )
- return callback(err)
- }
- if (err) {
- return callback(err)
- }
- const { projectName, isDeletedDoc, path } = ctx
- logger.log({ projectId, docId }, 'telling docstore manager to update doc')
- DocstoreManager.updateDoc(
- projectId,
- docId,
- lines,
- version,
- ranges,
- (err, modified, rev) => {
- if (err != null) {
- OError.tag(err, 'error sending doc to docstore', {
- docId,
- projectId,
- })
- return callback(err)
- }
- logger.log(
- { projectId, docId, modified },
- 'finished updating doc lines'
- )
- // path will only be present if the doc is not deleted
- if (!modified || isDeletedDoc) {
- return callback()
- }
- // Don't need to block for marking as updated
- ProjectUpdateHandler.markAsUpdated(
- projectId,
- lastUpdatedAt,
- lastUpdatedBy
- )
- TpdsUpdateSender.addDoc(
- {
- project_id: projectId,
- path,
- doc_id: docId,
- project_name: projectName,
- rev,
- },
- callback
- )
- }
- )
- })
- },
- setRootDoc(projectId, newRootDocID, callback) {
- logger.log({ projectId, rootDocId: newRootDocID }, 'setting root doc')
- if (projectId == null || newRootDocID == null) {
- return callback(
- new Errors.InvalidError('missing arguments (project or doc)')
- )
- }
- ProjectEntityHandler.getDocPathByProjectIdAndDocId(
- projectId,
- newRootDocID,
- (err, docPath) => {
- if (err != null) {
- return callback(err)
- }
- if (ProjectEntityUpdateHandler.isPathValidForRootDoc(docPath)) {
- Project.updateOne(
- { _id: projectId },
- { rootDoc_id: newRootDocID },
- {},
- callback
- )
- } else {
- callback(
- new Errors.UnsupportedFileTypeError(
- 'invalid file extension for root doc'
- )
- )
- }
- }
- )
- },
- unsetRootDoc(projectId, callback) {
- logger.log({ projectId }, 'removing root doc')
- Project.updateOne(
- { _id: projectId },
- { $unset: { rootDoc_id: true } },
- {},
- callback
- )
- },
- _addDocAndSendToTpds(projectId, folderId, doc, callback) {
- ProjectEntityMongoUpdateHandler.addDoc(
- projectId,
- folderId,
- doc,
- (err, result, project) => {
- if (err != null) {
- OError.tag(err, 'error adding file with project', {
- projectId,
- folderId,
- doc_name: doc != null ? doc.name : undefined,
- doc_id: doc != null ? doc._id : undefined,
- })
- return callback(err)
- }
- TpdsUpdateSender.addDoc(
- {
- project_id: projectId,
- doc_id: doc != null ? doc._id : undefined,
- path: result && result.path && result.path.fileSystem,
- project_name: project.name,
- rev: 0,
- },
- err => {
- if (err != null) {
- return callback(err)
- }
- callback(null, result, project)
- }
- )
- }
- )
- },
- addDoc(projectId, folderId, docName, docLines, userId, callback) {
- ProjectEntityUpdateHandler.addDocWithRanges(
- projectId,
- folderId,
- docName,
- docLines,
- {},
- userId,
- callback
- )
- },
- addDocWithRanges: wrapWithLock({
- beforeLock(next) {
- return function (
- projectId,
- folderId,
- docName,
- docLines,
- ranges,
- userId,
- callback
- ) {
- if (!SafePath.isCleanFilename(docName)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- // Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
- // which hasn't been created in docstore.
- const doc = new Doc({ name: docName })
- DocstoreManager.updateDoc(
- projectId.toString(),
- doc._id.toString(),
- docLines,
- 0,
- ranges,
- (err, modified, rev) => {
- if (err != null) {
- return callback(err)
- }
- next(
- projectId,
- folderId,
- doc,
- docName,
- docLines,
- ranges,
- userId,
- callback
- )
- }
- )
- }
- },
- withLock(
- projectId,
- folderId,
- doc,
- docName,
- docLines,
- ranges,
- userId,
- callback
- ) {
- ProjectEntityUpdateHandler._addDocAndSendToTpds(
- projectId,
- folderId,
- doc,
- (err, result, project) => {
- if (err != null) {
- return callback(err)
- }
- const docPath = result && result.path && result.path.fileSystem
- const projectHistoryId =
- project.overleaf &&
- project.overleaf.history &&
- project.overleaf.history.id
- const newDocs = [
- {
- doc,
- path: docPath,
- docLines: docLines.join('\n'),
- },
- ]
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- { newDocs, newProject: project },
- error => {
- if (error != null) {
- return callback(error)
- }
- callback(null, doc, folderId)
- }
- )
- }
- )
- },
- }),
- _uploadFile(projectId, folderId, fileName, fsPath, linkedFileData, callback) {
- if (!SafePath.isCleanFilename(fileName)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- const fileArgs = {
- name: fileName,
- linkedFileData,
- }
- FileStoreHandler.uploadFileFromDisk(
- projectId,
- fileArgs,
- fsPath,
- (err, fileStoreUrl, fileRef) => {
- if (err != null) {
- OError.tag(err, 'error uploading image to s3', {
- projectId,
- folderId,
- file_name: fileName,
- fileRef,
- })
- return callback(err)
- }
- callback(null, fileStoreUrl, fileRef)
- }
- )
- },
- _addFileAndSendToTpds(projectId, folderId, fileRef, callback) {
- ProjectEntityMongoUpdateHandler.addFile(
- projectId,
- folderId,
- fileRef,
- (err, result, project) => {
- if (err != null) {
- OError.tag(err, 'error adding file with project', {
- projectId,
- folderId,
- file_name: fileRef.name,
- fileRef,
- })
- return callback(err)
- }
- TpdsUpdateSender.addFile(
- {
- project_id: projectId,
- file_id: fileRef._id,
- path: result && result.path && result.path.fileSystem,
- project_name: project.name,
- rev: fileRef.rev,
- },
- err => {
- if (err != null) {
- return callback(err)
- }
- callback(null, result, project)
- }
- )
- }
- )
- },
- addFile: wrapWithLock({
- beforeLock(next) {
- return function (
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- callback
- ) {
- if (!SafePath.isCleanFilename(fileName)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- ProjectEntityUpdateHandler._uploadFile(
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- (error, fileStoreUrl, fileRef) => {
- if (error != null) {
- return callback(error)
- }
- next(
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- fileRef,
- fileStoreUrl,
- callback
- )
- }
- )
- }
- },
- withLock(
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- fileRef,
- fileStoreUrl,
- callback
- ) {
- ProjectEntityUpdateHandler._addFileAndSendToTpds(
- projectId,
- folderId,
- fileRef,
- (err, result, project) => {
- if (err != null) {
- return callback(err)
- }
- const projectHistoryId =
- project.overleaf &&
- project.overleaf.history &&
- project.overleaf.history.id
- const newFiles = [
- {
- file: fileRef,
- path: result && result.path && result.path.fileSystem,
- url: fileStoreUrl,
- },
- ]
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- { newFiles, newProject: project },
- error => {
- if (error != null) {
- return callback(error)
- }
- ProjectUpdateHandler.markAsUpdated(projectId, new Date(), userId)
- callback(null, fileRef, folderId)
- }
- )
- }
- )
- },
- }),
- replaceFile: wrapWithLock({
- beforeLock(next) {
- return function (
- projectId,
- fileId,
- fsPath,
- linkedFileData,
- userId,
- callback
- ) {
- // create a new file
- const fileArgs = {
- name: 'dummy-upload-filename',
- linkedFileData,
- }
- FileStoreHandler.uploadFileFromDisk(
- projectId,
- fileArgs,
- fsPath,
- (err, fileStoreUrl, fileRef) => {
- if (err != null) {
- return callback(err)
- }
- next(
- projectId,
- fileId,
- fsPath,
- linkedFileData,
- userId,
- fileRef,
- fileStoreUrl,
- callback
- )
- }
- )
- }
- },
- withLock(
- projectId,
- fileId,
- fsPath,
- linkedFileData,
- userId,
- newFileRef,
- fileStoreUrl,
- callback
- ) {
- ProjectEntityMongoUpdateHandler.replaceFileWithNew(
- projectId,
- fileId,
- newFileRef,
- (err, oldFileRef, project, path, newProject) => {
- if (err != null) {
- return callback(err)
- }
- const oldFiles = [
- {
- file: oldFileRef,
- path: path.fileSystem,
- },
- ]
- const newFiles = [
- {
- file: newFileRef,
- path: path.fileSystem,
- url: fileStoreUrl,
- },
- ]
- const projectHistoryId =
- project.overleaf &&
- project.overleaf.history &&
- project.overleaf.history.id
- // Increment the rev for an in-place update (with the same path) so the third-party-datastore
- // knows this is a new file.
- // Ideally we would get this from ProjectEntityMongoUpdateHandler.replaceFileWithNew
- // but it returns the original oldFileRef (after incrementing the rev value in mongo),
- // so we add 1 to the rev from that. This isn't atomic and relies on the lock
- // but it is acceptable for now.
- TpdsUpdateSender.addFile(
- {
- project_id: project._id,
- file_id: newFileRef._id,
- path: path.fileSystem,
- rev: oldFileRef.rev + 1,
- project_name: project.name,
- },
- err => {
- if (err != null) {
- return callback(err)
- }
- ProjectUpdateHandler.markAsUpdated(projectId, new Date(), userId)
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- { oldFiles, newFiles, newProject },
- callback
- )
- }
- )
- }
- )
- },
- }),
- upsertDoc: wrapWithLock(function (
- projectId,
- folderId,
- docName,
- docLines,
- source,
- userId,
- callback
- ) {
- if (!SafePath.isCleanFilename(docName)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- ProjectLocator.findElement(
- { project_id: projectId, element_id: folderId, type: 'folder' },
- (error, folder, folderPath) => {
- if (error != null) {
- return callback(error)
- }
- if (folder == null) {
- return callback(new Error("Couldn't find folder"))
- }
- const existingDoc = folder.docs.find(({ name }) => name === docName)
- const existingFile = folder.fileRefs.find(
- ({ name }) => name === docName
- )
- if (existingFile) {
- const doc = new Doc({ name: docName })
- const filePath = `${folderPath.fileSystem}/${existingFile.name}`
- DocstoreManager.updateDoc(
- projectId.toString(),
- doc._id.toString(),
- docLines,
- 0,
- {},
- (err, modified, rev) => {
- if (err != null) {
- return callback(err)
- }
- ProjectEntityMongoUpdateHandler.replaceFileWithDoc(
- projectId,
- existingFile._id,
- doc,
- (err, project) => {
- if (err) {
- return callback(err)
- }
- TpdsUpdateSender.addDoc(
- {
- project_id: projectId,
- doc_id: doc._id,
- path: filePath,
- project_name: project.name,
- rev: existingFile.rev + 1,
- },
- err => {
- if (err) {
- return callback(err)
- }
- const projectHistoryId =
- project.overleaf &&
- project.overleaf.history &&
- project.overleaf.history.id
- const newDocs = [
- {
- doc,
- path: filePath,
- docLines: docLines.join('\n'),
- },
- ]
- const oldFiles = [
- {
- file: existingFile,
- path: filePath,
- },
- ]
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- { oldFiles, newDocs, newProject: project },
- error => {
- if (error != null) {
- return callback(error)
- }
- EditorRealTimeController.emitToRoom(
- projectId,
- 'removeEntity',
- existingFile._id,
- 'convertFileToDoc'
- )
- callback(null, doc, true)
- }
- )
- }
- )
- }
- )
- }
- )
- } else if (existingDoc) {
- DocumentUpdaterHandler.setDocument(
- projectId,
- existingDoc._id,
- userId,
- docLines,
- source,
- err => {
- if (err != null) {
- return callback(err)
- }
- logger.log(
- { projectId, docId: existingDoc._id },
- 'notifying users that the document has been updated'
- )
- DocumentUpdaterHandler.flushDocToMongo(
- projectId,
- existingDoc._id,
- err => {
- if (err != null) {
- return callback(err)
- }
- callback(null, existingDoc, existingDoc == null)
- }
- )
- }
- )
- } else {
- ProjectEntityUpdateHandler.addDocWithRanges.withoutLock(
- projectId,
- folderId,
- docName,
- docLines,
- {},
- userId,
- (err, doc) => {
- if (err != null) {
- return callback(err)
- }
- callback(null, doc, existingDoc == null)
- }
- )
- }
- }
- )
- }),
- upsertFile: wrapWithLock({
- beforeLock(next) {
- return function (
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- callback
- ) {
- if (!SafePath.isCleanFilename(fileName)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- // create a new file
- const fileArgs = {
- name: fileName,
- linkedFileData,
- }
- FileStoreHandler.uploadFileFromDisk(
- projectId,
- fileArgs,
- fsPath,
- (err, fileStoreUrl, fileRef) => {
- if (err != null) {
- return callback(err)
- }
- next(
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- fileRef,
- fileStoreUrl,
- callback
- )
- }
- )
- }
- },
- withLock(
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- newFileRef,
- fileStoreUrl,
- callback
- ) {
- ProjectLocator.findElement(
- { project_id: projectId, element_id: folderId, type: 'folder' },
- (error, folder) => {
- if (error != null) {
- return callback(error)
- }
- if (folder == null) {
- return callback(new Error("Couldn't find folder"))
- }
- const existingFile = folder.fileRefs.find(
- ({ name }) => name === fileName
- )
- const existingDoc = folder.docs.find(({ name }) => name === fileName)
- if (existingDoc) {
- ProjectLocator.findElement(
- {
- project_id: projectId,
- element_id: existingDoc._id,
- type: 'doc',
- },
- (err, doc, path) => {
- if (err) {
- return callback(new Error('coudnt find existing file'))
- }
- ProjectEntityMongoUpdateHandler.replaceDocWithFile(
- projectId,
- existingDoc._id,
- newFileRef,
- (err, project) => {
- if (err) {
- return callback(err)
- }
- const projectHistoryId =
- project.overleaf &&
- project.overleaf.history &&
- project.overleaf.history.id
- TpdsUpdateSender.addFile(
- {
- project_id: project._id,
- file_id: newFileRef._id,
- path: path.fileSystem,
- rev: newFileRef.rev,
- project_name: project.name,
- },
- err => {
- if (err) {
- return callback(err)
- }
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- {
- oldDocs: [
- { doc: existingDoc, path: path.fileSystem },
- ],
- newFiles: [
- {
- file: newFileRef,
- path: path.fileSystem,
- url: fileStoreUrl,
- },
- ],
- newProject: project,
- },
- err => {
- if (err) {
- return callback(err)
- }
- EditorRealTimeController.emitToRoom(
- projectId,
- 'removeEntity',
- existingDoc._id,
- 'convertDocToFile'
- )
- callback(null, newFileRef, true, existingFile)
- }
- )
- }
- )
- }
- )
- }
- )
- } else if (existingFile) {
- // this calls directly into the replaceFile main task (without the beforeLock part)
- return ProjectEntityUpdateHandler.replaceFile.mainTask(
- projectId,
- existingFile._id,
- fsPath,
- linkedFileData,
- userId,
- newFileRef,
- fileStoreUrl,
- err => {
- if (err != null) {
- return callback(err)
- }
- callback(null, newFileRef, existingFile == null, existingFile)
- }
- )
- } else {
- // this calls directly into the addFile main task (without the beforeLock part)
- ProjectEntityUpdateHandler.addFile.mainTask(
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- newFileRef,
- fileStoreUrl,
- err => {
- if (err != null) {
- return callback(err)
- }
- callback(null, newFileRef, existingFile == null, existingFile)
- }
- )
- }
- }
- )
- },
- }),
- upsertDocWithPath: wrapWithLock(function (
- projectId,
- elementPath,
- docLines,
- source,
- userId,
- callback
- ) {
- if (!SafePath.isCleanPath(elementPath)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- const docName = Path.basename(elementPath)
- const folderPath = Path.dirname(elementPath)
- ProjectEntityUpdateHandler.mkdirp.withoutLock(
- projectId,
- folderPath,
- (err, newFolders, folder) => {
- if (err != null) {
- return callback(err)
- }
- ProjectEntityUpdateHandler.upsertDoc.withoutLock(
- projectId,
- folder._id,
- docName,
- docLines,
- source,
- userId,
- (err, doc, isNewDoc) => {
- if (err != null) {
- return callback(err)
- }
- callback(null, doc, isNewDoc, newFolders, folder)
- }
- )
- }
- )
- }),
- upsertFileWithPath: wrapWithLock({
- beforeLock(next) {
- return function (
- projectId,
- elementPath,
- fsPath,
- linkedFileData,
- userId,
- callback
- ) {
- if (!SafePath.isCleanPath(elementPath)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- const fileName = Path.basename(elementPath)
- const folderPath = Path.dirname(elementPath)
- // create a new file
- const fileArgs = {
- name: fileName,
- linkedFileData,
- }
- FileStoreHandler.uploadFileFromDisk(
- projectId,
- fileArgs,
- fsPath,
- (err, fileStoreUrl, fileRef) => {
- if (err != null) {
- return callback(err)
- }
- next(
- projectId,
- folderPath,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- fileRef,
- fileStoreUrl,
- callback
- )
- }
- )
- }
- },
- withLock(
- projectId,
- folderPath,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- fileRef,
- fileStoreUrl,
- callback
- ) {
- ProjectEntityUpdateHandler.mkdirp.withoutLock(
- projectId,
- folderPath,
- (err, newFolders, folder) => {
- if (err != null) {
- return callback(err)
- }
- // this calls directly into the upsertFile main task (without the beforeLock part)
- ProjectEntityUpdateHandler.upsertFile.mainTask(
- projectId,
- folder._id,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- fileRef,
- fileStoreUrl,
- (err, newFile, isNewFile, existingFile) => {
- if (err != null) {
- return callback(err)
- }
- callback(
- null,
- newFile,
- isNewFile,
- existingFile,
- newFolders,
- folder
- )
- }
- )
- }
- )
- },
- }),
- deleteEntity: wrapWithLock(function (
- projectId,
- entityId,
- entityType,
- userId,
- callback
- ) {
- logger.log({ entityId, entityType, projectId }, 'deleting project entity')
- if (entityType == null) {
- logger.warn({ err: 'No entityType set', projectId, entityId })
- return callback(new Error('No entityType set'))
- }
- entityType = entityType.toLowerCase()
- ProjectEntityMongoUpdateHandler.deleteEntity(
- projectId,
- entityId,
- entityType,
- (error, entity, path, projectBeforeDeletion, newProject) => {
- if (error != null) {
- return callback(error)
- }
- ProjectEntityUpdateHandler._cleanUpEntity(
- projectBeforeDeletion,
- newProject,
- entity,
- entityType,
- path.fileSystem,
- userId,
- error => {
- if (error != null) {
- return callback(error)
- }
- TpdsUpdateSender.deleteEntity(
- {
- project_id: projectId,
- path: path.fileSystem,
- project_name: projectBeforeDeletion.name,
- },
- error => {
- if (error != null) {
- return callback(error)
- }
- callback(null, entityId)
- }
- )
- }
- )
- }
- )
- }),
- deleteEntityWithPath: wrapWithLock((projectId, path, userId, callback) =>
- ProjectLocator.findElementByPath(
- { project_id: projectId, path },
- (err, element, type) => {
- if (err != null) {
- return callback(err)
- }
- if (element == null) {
- return callback(new Errors.NotFoundError('project not found'))
- }
- ProjectEntityUpdateHandler.deleteEntity.withoutLock(
- projectId,
- element._id,
- type,
- userId,
- callback
- )
- }
- )
- ),
- mkdirp: wrapWithLock(function (projectId, path, callback) {
- for (const folder of path.split('/')) {
- if (folder.length > 0 && !SafePath.isCleanFilename(folder)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- }
- ProjectEntityMongoUpdateHandler.mkdirp(
- projectId,
- path,
- { exactCaseMatch: false },
- callback
- )
- }),
- mkdirpWithExactCase: wrapWithLock(function (projectId, path, callback) {
- for (const folder of path.split('/')) {
- if (folder.length > 0 && !SafePath.isCleanFilename(folder)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- }
- ProjectEntityMongoUpdateHandler.mkdirp(
- projectId,
- path,
- { exactCaseMatch: true },
- callback
- )
- }),
- addFolder: wrapWithLock(function (
- projectId,
- parentFolderId,
- folderName,
- callback
- ) {
- if (!SafePath.isCleanFilename(folderName)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- ProjectEntityMongoUpdateHandler.addFolder(
- projectId,
- parentFolderId,
- folderName,
- callback
- )
- }),
- moveEntity: wrapWithLock(function (
- projectId,
- entityId,
- destFolderId,
- entityType,
- userId,
- callback
- ) {
- logger.log(
- { entityType, entityId, projectId, destFolderId },
- 'moving entity'
- )
- if (entityType == null) {
- logger.warn({ err: 'No entityType set', projectId, entityId })
- return callback(new Error('No entityType set'))
- }
- entityType = entityType.toLowerCase()
- ProjectEntityMongoUpdateHandler.moveEntity(
- projectId,
- entityId,
- destFolderId,
- entityType,
- (err, project, startPath, endPath, rev, changes) => {
- if (err != null) {
- return callback(err)
- }
- const projectHistoryId =
- project.overleaf &&
- project.overleaf.history &&
- project.overleaf.history.id
- // do not wait
- TpdsUpdateSender.promises
- .moveEntity({
- project_id: projectId,
- project_name: project.name,
- startPath,
- endPath,
- rev,
- })
- .catch(err => {
- logger.error({ err }, 'error sending tpds update')
- })
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- changes,
- callback
- )
- }
- )
- }),
- renameEntity: wrapWithLock(function (
- projectId,
- entityId,
- entityType,
- newName,
- userId,
- callback
- ) {
- if (!SafePath.isCleanFilename(newName)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- logger.log({ entityId, projectId }, `renaming ${entityType}`)
- if (entityType == null) {
- logger.warn({ err: 'No entityType set', projectId, entityId })
- return callback(new Error('No entityType set'))
- }
- entityType = entityType.toLowerCase()
- ProjectEntityMongoUpdateHandler.renameEntity(
- projectId,
- entityId,
- entityType,
- newName,
- (err, project, startPath, endPath, rev, changes) => {
- if (err != null) {
- return callback(err)
- }
- const projectHistoryId =
- project.overleaf &&
- project.overleaf.history &&
- project.overleaf.history.id
- // do not wait
- TpdsUpdateSender.promises
- .moveEntity({
- project_id: projectId,
- project_name: project.name,
- startPath,
- endPath,
- rev,
- })
- .catch(err => {
- logger.error({ err }, 'error sending tpds update')
- })
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- changes,
- callback
- )
- }
- )
- }),
- // This doesn't directly update project structure but we need to take the lock
- // to prevent anything else being queued before the resync update
- resyncProjectHistory: wrapWithLock((projectId, callback) =>
- ProjectGetter.getProject(
- projectId,
- { rootFolder: true, overleaf: true },
- (error, project) => {
- if (error != null) {
- return callback(error)
- }
- const projectHistoryId =
- project &&
- project.overleaf &&
- project.overleaf.history &&
- project.overleaf.history.id
- if (projectHistoryId == null) {
- error = new Errors.ProjectHistoryDisabledError(
- `project history not enabled for ${projectId}`
- )
- return callback(error)
- }
- ProjectEntityHandler.getAllEntitiesFromProject(
- project,
- (error, docs, files) => {
- if (error != null) {
- return callback(error)
- }
- docs = _.map(docs, doc => ({
- doc: doc.doc._id,
- path: doc.path,
- }))
- files = _.map(files, file => ({
- file: file.file._id,
- path: file.path,
- url: FileStoreHandler._buildUrl(projectId, file.file._id),
- }))
- DocumentUpdaterHandler.resyncProjectHistory(
- projectId,
- projectHistoryId,
- docs,
- files,
- callback
- )
- }
- )
- }
- )
- ),
- isPathValidForRootDoc(docPath) {
- const docExtension = Path.extname(docPath)
- return VALID_ROOT_DOC_REGEXP.test(docExtension)
- },
- _cleanUpEntity(
- project,
- newProject,
- entity,
- entityType,
- path,
- userId,
- callback
- ) {
- ProjectEntityUpdateHandler._updateProjectStructureWithDeletedEntity(
- project,
- newProject,
- entity,
- entityType,
- path,
- userId,
- error => {
- if (error != null) {
- return callback(error)
- }
- if (entityType.indexOf('file') !== -1) {
- ProjectEntityUpdateHandler._cleanUpFile(
- project,
- entity,
- path,
- userId,
- callback
- )
- } else if (entityType.indexOf('doc') !== -1) {
- ProjectEntityUpdateHandler._cleanUpDoc(
- project,
- entity,
- path,
- userId,
- callback
- )
- } else if (entityType.indexOf('folder') !== -1) {
- ProjectEntityUpdateHandler._cleanUpFolder(
- project,
- entity,
- path,
- userId,
- callback
- )
- } else {
- callback()
- }
- }
- )
- },
- // Note: the _cleanUpEntity code and _updateProjectStructureWithDeletedEntity
- // methods both need to recursively iterate over the entities in folder.
- // These are currently using separate implementations of the recursion. In
- // future, these could be simplified using a common project entity iterator.
- _updateProjectStructureWithDeletedEntity(
- project,
- newProject,
- entity,
- entityType,
- entityPath,
- userId,
- callback
- ) {
- // compute the changes to the project structure
- let changes
- if (entityType.indexOf('file') !== -1) {
- changes = { oldFiles: [{ file: entity, path: entityPath }] }
- } else if (entityType.indexOf('doc') !== -1) {
- changes = { oldDocs: [{ doc: entity, path: entityPath }] }
- } else if (entityType.indexOf('folder') !== -1) {
- changes = { oldDocs: [], oldFiles: [] }
- const _recurseFolder = (folder, folderPath) => {
- for (const doc of folder.docs) {
- changes.oldDocs.push({ doc, path: Path.join(folderPath, doc.name) })
- }
- for (const file of folder.fileRefs) {
- changes.oldFiles.push({
- file,
- path: Path.join(folderPath, file.name),
- })
- }
- for (const childFolder of folder.folders) {
- _recurseFolder(childFolder, Path.join(folderPath, childFolder.name))
- }
- }
- _recurseFolder(entity, entityPath)
- }
- // now send the project structure changes to the docupdater
- changes.newProject = newProject
- const projectId = project._id.toString()
- const projectHistoryId =
- project.overleaf &&
- project.overleaf.history &&
- project.overleaf.history.id
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- changes,
- callback
- )
- },
- _cleanUpDoc(project, doc, path, userId, callback) {
- const projectId = project._id.toString()
- const docId = doc._id.toString()
- const unsetRootDocIfRequired = callback => {
- if (
- project.rootDoc_id != null &&
- project.rootDoc_id.toString() === docId
- ) {
- ProjectEntityUpdateHandler.unsetRootDoc(projectId, callback)
- } else {
- callback()
- }
- }
- unsetRootDocIfRequired(error => {
- if (error != null) {
- return callback(error)
- }
- const { name } = doc
- const deletedAt = new Date()
- DocstoreManager.deleteDoc(projectId, docId, name, deletedAt, error => {
- if (error) {
- return callback(error)
- }
- DocumentUpdaterHandler.deleteDoc(projectId, docId, callback)
- })
- })
- },
- _cleanUpFile(project, file, path, userId, callback) {
- ProjectEntityMongoUpdateHandler._insertDeletedFileReference(
- project._id,
- file,
- callback
- )
- },
- _cleanUpFolder(project, folder, folderPath, userId, callback) {
- const jobs = []
- folder.docs.forEach(doc => {
- const docPath = Path.join(folderPath, doc.name)
- jobs.push(callback =>
- ProjectEntityUpdateHandler._cleanUpDoc(
- project,
- doc,
- docPath,
- userId,
- callback
- )
- )
- })
- folder.fileRefs.forEach(file => {
- const filePath = Path.join(folderPath, file.name)
- jobs.push(callback =>
- ProjectEntityUpdateHandler._cleanUpFile(
- project,
- file,
- filePath,
- userId,
- callback
- )
- )
- })
- folder.folders.forEach(childFolder => {
- folderPath = Path.join(folderPath, childFolder.name)
- jobs.push(callback =>
- ProjectEntityUpdateHandler._cleanUpFolder(
- project,
- childFolder,
- folderPath,
- userId,
- callback
- )
- )
- })
- async.series(jobs, callback)
- },
- convertDocToFile: wrapWithLock({
- beforeLock(next) {
- return function (projectId, docId, userId, callback) {
- DocumentUpdaterHandler.flushDocToMongo(projectId, docId, err => {
- if (err) {
- return callback(err)
- }
- ProjectLocator.findElement(
- { project_id: projectId, element_id: docId, type: 'doc' },
- (err, doc, path) => {
- const docPath = path.fileSystem
- if (err) {
- return callback(err)
- }
- DocstoreManager.getDoc(
- projectId,
- docId,
- (err, docLines, rev, version, ranges) => {
- if (err) {
- return callback(err)
- }
- if (!_.isEmpty(ranges)) {
- return callback(new Errors.DocHasRangesError({}))
- }
- DocumentUpdaterHandler.deleteDoc(projectId, docId, err => {
- if (err) {
- return callback(err)
- }
- FileWriter.writeLinesToDisk(
- projectId,
- docLines,
- (err, fsPath) => {
- if (err) {
- return callback(err)
- }
- FileStoreHandler.uploadFileFromDisk(
- projectId,
- { name: doc.name, rev: rev + 1 },
- fsPath,
- (err, fileStoreUrl, fileRef) => {
- if (err) {
- return callback(err)
- }
- fs.unlink(fsPath, err => {
- if (err) {
- logger.warn(
- { err, path: fsPath },
- 'failed to clean up temporary file'
- )
- }
- next(
- projectId,
- doc,
- docPath,
- fileRef,
- fileStoreUrl,
- userId,
- callback
- )
- })
- }
- )
- }
- )
- })
- }
- )
- }
- )
- })
- }
- },
- withLock(projectId, doc, path, fileRef, fileStoreUrl, userId, callback) {
- ProjectEntityMongoUpdateHandler.replaceDocWithFile(
- projectId,
- doc._id,
- fileRef,
- (err, project) => {
- if (err) {
- return callback(err)
- }
- const projectHistoryId =
- project.overleaf &&
- project.overleaf.history &&
- project.overleaf.history.id
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- {
- oldDocs: [{ doc, path }],
- newFiles: [{ file: fileRef, path, url: fileStoreUrl }],
- newProject: project,
- },
- err => {
- if (err) {
- return callback(err)
- }
- ProjectLocator.findElement(
- {
- project_id: projectId,
- element_id: fileRef._id,
- type: 'file',
- },
- (err, element, path, folder) => {
- if (err) {
- return callback(err)
- }
- EditorRealTimeController.emitToRoom(
- projectId,
- 'removeEntity',
- doc._id,
- 'convertDocToFile'
- )
- EditorRealTimeController.emitToRoom(
- projectId,
- 'reciveNewFile',
- folder._id,
- fileRef,
- 'convertDocToFile',
- null,
- userId
- )
- callback(null, fileRef)
- }
- )
- }
- )
- }
- )
- },
- }),
- }
- module.exports = ProjectEntityUpdateHandler
- module.exports.promises = promisifyAll(ProjectEntityUpdateHandler, {
- without: ['isPathValidForRootDoc'],
- multiResult: {
- _addDocAndSendToTpds: ['result', 'project'],
- addDoc: ['doc', 'folderId'],
- addDocWithRanges: ['doc', 'folderId'],
- _uploadFile: ['fileStoreUrl', 'fileRef'],
- _addFileAndSendToTpds: ['result', 'project'],
- addFile: ['fileRef', 'folderId'],
- upsertDoc: ['doc', 'isNew'],
- upsertFile: ['fileRef', 'isNew', 'oldFileRef'],
- upsertDocWithPath: ['doc', 'isNew', 'newFolders', 'folder'],
- upsertFileWithPath: ['fileRef', 'isNew', 'oldFile', 'newFolders', 'folder'],
- mkdirp: ['newFolders', 'folder'],
- mkdirpWithExactCase: ['newFolders', 'folder'],
- addFolder: ['folder', 'parentFolderId'],
- },
- })
|