| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959 |
- const _ = require('lodash')
- const OError = require('@overleaf/o-error')
- const async = require('async')
- const logger = require('@overleaf/logger')
- const Settings = require('@overleaf/settings')
- 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('@overleaf/promise-utils')
- const { iterablePaths } = require('./IterablePath')
- 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, lockManager = LockManager) {
- // 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, folder) => {
- 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.debug(
- { projectId, docId },
- 'updating doc that is in process of getting soft-deleted'
- )
- }
- callback(null, {
- projectName: project.name,
- isDeletedDoc: true,
- path: null,
- folder: 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,
- folder,
- })
- }
- }
- )
- }
- )
- }
- 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, folder } = ctx
- logger.debug(
- { 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.debug(
- { projectId, docId, modified },
- 'finished updating doc lines'
- )
- // path will only be present if the doc is not deleted
- if (!modified || isDeletedDoc) {
- return callback(null, { rev })
- }
- // Don't need to block for marking as updated
- ProjectUpdateHandler.promises
- .markAsUpdated(projectId, lastUpdatedAt, lastUpdatedBy)
- .catch(error => {
- logger.error({ error }, 'failed to mark project as updated')
- })
- TpdsUpdateSender.addDoc(
- {
- projectId,
- path,
- docId,
- projectName,
- rev,
- folderId: folder?._id,
- },
- err => {
- if (err) {
- return callback(err)
- }
- callback(null, { rev, modified })
- }
- )
- }
- )
- })
- },
- setRootDoc(projectId, newRootDocID, callback) {
- logger.debug({ 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)) {
- // Ignore spurious floating promises warning until we promisify
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- Project.updateOne(
- { _id: projectId },
- { rootDoc_id: newRootDocID },
- {},
- callback
- )
- } else {
- callback(
- new Errors.UnsupportedFileTypeError(
- 'invalid file extension for root doc'
- )
- )
- }
- }
- )
- },
- unsetRootDoc(projectId, callback) {
- logger.debug({ projectId }, 'removing root doc')
- // Ignore spurious floating promises warning until we promisify
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- 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(
- {
- projectId,
- docId: doc != null ? doc._id : undefined,
- path: result?.path?.fileSystem,
- projectName: project.name,
- rev: 0,
- folderId,
- },
- err => {
- if (err != null) {
- return callback(err)
- }
- callback(null, result, project)
- }
- )
- }
- )
- },
- addDoc(projectId, folderId, docName, docLines, userId, source, callback) {
- ProjectEntityUpdateHandler.addDocWithRanges(
- projectId,
- folderId,
- docName,
- docLines,
- {},
- userId,
- source,
- callback
- )
- },
- addDocWithRanges: wrapWithLock({
- beforeLock(next) {
- return function (
- projectId,
- folderId,
- docName,
- docLines,
- ranges,
- userId,
- source,
- 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)
- }
- doc.rev = rev
- next(
- projectId,
- folderId,
- doc,
- docName,
- docLines,
- ranges,
- userId,
- source,
- callback
- )
- }
- )
- }
- },
- withLock(
- projectId,
- folderId,
- doc,
- docName,
- docLines,
- ranges,
- userId,
- source,
- 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'),
- ranges,
- },
- ]
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- { newDocs, newProject: project },
- source,
- error => {
- if (error != null) {
- return callback(error)
- }
- callback(null, doc, folderId || project.rootFolder[0]._id)
- }
- )
- }
- )
- },
- }),
- _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(
- {
- projectId,
- fileId: fileRef._id,
- path: result?.path?.fileSystem,
- projectName: project.name,
- rev: fileRef.rev,
- folderId,
- },
- err => {
- if (err != null) {
- return callback(err)
- }
- callback(null, result, project)
- }
- )
- }
- )
- },
- addFile: wrapWithLock({
- beforeLock(next) {
- return function (
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- source,
- 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,
- source,
- callback
- )
- }
- )
- }
- },
- withLock(
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- fileRef,
- fileStoreUrl,
- source,
- 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 },
- source,
- error => {
- if (error != null) {
- return callback(error)
- }
- ProjectUpdateHandler.promises
- .markAsUpdated(projectId, new Date(), userId)
- .catch(error => {
- logger.error({ error }, 'failed to mark project as updated')
- })
- callback(null, fileRef, folderId)
- }
- )
- }
- )
- },
- }),
- _replaceFile(
- projectId,
- fileId,
- fsPath,
- linkedFileData,
- userId,
- newFileRef,
- fileStoreUrl,
- folderId,
- source,
- callback
- ) {
- ProjectEntityMongoUpdateHandler.replaceFileWithNew(
- projectId,
- fileId,
- newFileRef,
- (err, oldFileRef, project, path, newProject, newFileRef) => {
- 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
- TpdsUpdateSender.addFile(
- {
- projectId: project._id,
- fileId: newFileRef._id,
- path: path.fileSystem,
- rev: newFileRef.rev,
- projectName: project.name,
- folderId,
- },
- err => {
- if (err != null) {
- return callback(err)
- }
- ProjectUpdateHandler.promises
- .markAsUpdated(projectId, new Date(), userId)
- .catch(error => {
- logger.error({ error }, 'failed to mark project as updated')
- })
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- { oldFiles, newFiles, newProject },
- source,
- err => {
- if (err) {
- return callback(err)
- }
- callback(null, newFileRef)
- }
- )
- }
- )
- }
- )
- },
- 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) {
- if (error instanceof Errors.NotFoundError && folder == null) {
- return callback(new Error('folder_not_found'))
- }
- 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)
- }
- doc.rev = rev
- ProjectEntityMongoUpdateHandler.replaceFileWithDoc(
- projectId,
- existingFile._id,
- doc,
- (err, project) => {
- if (err) {
- return callback(err)
- }
- TpdsUpdateSender.addDoc(
- {
- projectId,
- docId: doc._id,
- path: filePath,
- projectName: project.name,
- rev: existingFile.rev + 1,
- folderId,
- },
- 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 },
- source,
- 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, result) => {
- if (err != null) {
- return callback(err)
- }
- logger.debug(
- { projectId, docId: existingDoc._id },
- 'notifying users that the document has been updated'
- )
- // there is no need to flush the doc to mongo at this point as docupdater
- // flushes it as part of setDoc.
- //
- // combine rev from response with existing doc metadata
- callback(
- null,
- { ...existingDoc, ...result },
- existingDoc == null
- )
- }
- )
- } else {
- ProjectEntityUpdateHandler.addDocWithRanges.withoutLock(
- projectId,
- folderId,
- docName,
- docLines,
- {},
- userId,
- source,
- (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,
- source,
- 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,
- source,
- callback
- )
- }
- )
- }
- },
- withLock(
- projectId,
- folderId,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- newFileRef,
- fileStoreUrl,
- source,
- callback
- ) {
- ProjectLocator.findElement(
- { project_id: projectId, element_id: folderId, type: 'folder' },
- (error, folder) => {
- if (error != null) {
- if (error instanceof Errors.NotFoundError && folder == null) {
- return callback(new Error('folder_not_found'))
- }
- 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(
- {
- projectId: project._id,
- fileId: newFileRef._id,
- path: path.fileSystem,
- rev: newFileRef.rev,
- projectName: project.name,
- folderId,
- },
- 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,
- },
- source,
- err => {
- if (err) {
- return callback(err)
- }
- EditorRealTimeController.emitToRoom(
- projectId,
- 'removeEntity',
- existingDoc._id,
- 'convertDocToFile'
- )
- callback(null, newFileRef, true, existingFile)
- }
- )
- }
- )
- }
- )
- }
- )
- } else if (existingFile) {
- ProjectEntityUpdateHandler._replaceFile(
- projectId,
- existingFile._id,
- fsPath,
- linkedFileData,
- userId,
- newFileRef,
- fileStoreUrl,
- folderId,
- source,
- (err, newFileRef) => {
- 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,
- source,
- 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,
- source,
- 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,
- source,
- callback
- )
- }
- )
- }
- },
- withLock(
- projectId,
- folderPath,
- fileName,
- fsPath,
- linkedFileData,
- userId,
- fileRef,
- fileStoreUrl,
- source,
- 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,
- source,
- (err, newFile, isNewFile, existingFile) => {
- if (err != null) {
- return callback(err)
- }
- callback(
- null,
- newFile,
- isNewFile,
- existingFile,
- newFolders,
- folder
- )
- }
- )
- }
- )
- },
- }),
- deleteEntity: wrapWithLock(
- function (projectId, entityId, entityType, userId, source, callback) {
- logger.debug(
- { 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,
- source,
- (error, subtreeListing) => {
- if (error != null) {
- return callback(error)
- }
- const subtreeEntityIds = subtreeListing.map(entry =>
- entry.entity._id.toString()
- )
- TpdsUpdateSender.deleteEntity(
- {
- projectId,
- path: path.fileSystem,
- projectName: projectBeforeDeletion.name,
- entityId,
- entityType,
- subtreeEntityIds,
- },
- error => {
- if (error != null) {
- return callback(error)
- }
- callback(null, entityId)
- }
- )
- }
- )
- }
- )
- }
- ),
- deleteEntityWithPath: wrapWithLock(
- (projectId, path, userId, source, callback) =>
- ProjectLocator.findElementByPath(
- { project_id: projectId, path, exactCaseMatch: true },
- (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,
- source,
- 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,
- source,
- callback
- ) {
- logger.debug(
- { 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()
- DocumentUpdaterHandler.flushProjectToMongo(projectId, err => {
- if (err) {
- return callback(err)
- }
- 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
- TpdsUpdateSender.moveEntity(
- {
- projectId,
- projectName: project.name,
- startPath,
- endPath,
- rev,
- entityId,
- entityType,
- folderId: destFolderId,
- },
- err => {
- if (err) {
- logger.error({ err }, 'error sending tpds update')
- }
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- changes,
- source,
- callback
- )
- }
- )
- }
- )
- })
- }
- ),
- renameEntity: wrapWithLock(
- function (
- projectId,
- entityId,
- entityType,
- newName,
- userId,
- source,
- callback
- ) {
- if (!newName || typeof newName !== 'string') {
- const err = new OError('invalid newName value', {
- value: newName,
- type: typeof newName,
- projectId,
- entityId,
- entityType,
- userId,
- source,
- })
- logger.error({ err }, 'Invalid newName passed to renameEntity')
- return callback(err)
- }
- if (!SafePath.isCleanFilename(newName)) {
- return callback(new Errors.InvalidNameError('invalid element name'))
- }
- logger.debug({ 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()
- DocumentUpdaterHandler.flushProjectToMongo(projectId, err => {
- if (err) {
- return callback(err)
- }
- 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
- TpdsUpdateSender.moveEntity(
- {
- projectId,
- projectName: project.name,
- startPath,
- endPath,
- rev,
- entityId,
- entityType,
- folderId: null, // this means the folder has not changed
- },
- err => {
- if (err) {
- logger.error({ err }, 'error sending tpds update')
- }
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- userId,
- changes,
- source,
- 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)
- }
- let docs, files, folders
- try {
- ;({ docs, files, folders } =
- ProjectEntityHandler.getAllEntitiesFromProject(project))
- } catch (error) {
- return callback(error)
- }
- // _checkFileTree() must be passed the folders before docs and
- // files
- ProjectEntityUpdateHandler._checkFiletree(
- projectId,
- projectHistoryId,
- [...folders, ...docs, ...files],
- error => {
- if (error) {
- 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),
- _hash: file.file.hash,
- }))
- DocumentUpdaterHandler.resyncProjectHistory(
- projectId,
- projectHistoryId,
- docs,
- files,
- callback
- )
- }
- )
- }
- ),
- LockManager.withTimeout(6 * 60) // use an extended lock for the resync operations
- ),
- _checkFiletree(projectId, projectHistoryId, entities, callback) {
- const adjustPathsAfterFolderRename = (oldPath, newPath) => {
- oldPath = oldPath + '/'
- newPath = newPath + '/'
- for (const entity of entities) {
- if (entity.path.startsWith(oldPath)) {
- entity.path = newPath + entity.path.slice(oldPath.length)
- }
- }
- }
- // Data structures for recording pending renames
- const renames = []
- const paths = new Set()
- for (const entity of entities) {
- const originalName = entity.folder
- ? entity.folder.name
- : entity.doc
- ? entity.doc.name
- : entity.file.name
- let newPath = entity.path
- let newName = originalName
- // Clean the filename if necessary
- if (newName === '') {
- newName = 'untitled'
- } else {
- newName = SafePath.clean(newName)
- }
- if (newName !== originalName) {
- newPath = Path.join(
- newPath.slice(0, newPath.length - originalName.length),
- newName
- )
- }
- // Check if we've seen that path already
- if (paths.has(newPath)) {
- newPath = ProjectEntityUpdateHandler.findNextAvailablePath(
- paths,
- newPath
- )
- newName = newPath.split('/').pop()
- }
- // If we've changed the filename, schedule a rename
- if (newName !== originalName) {
- renames.push({ entity, newName, newPath })
- if (entity.folder) {
- // Here, we rely on entities being processed in the right order.
- // Parent folders need to be processed before their children. This is
- // the case only because getAllEntitiesFromProject() returns folders
- // in that order and resyncProjectHistory() calls us with the folders
- // first.
- try {
- adjustPathsAfterFolderRename(entity.path, newPath)
- } catch (error) {
- return callback(error)
- }
- }
- }
- // Remember that we've seen this path
- paths.add(newPath)
- }
- if (renames.length === 0) {
- return callback()
- }
- logger.warn(
- {
- projectId,
- renames: renames.map(rename => ({
- oldPath: rename.entity.path,
- newPath: rename.newPath,
- })),
- },
- 'found conflicts or bad filenames in filetree'
- )
- // rename the duplicate files
- const doRename = (rename, cb) => {
- const entity = rename.entity
- const entityId = entity.folder
- ? entity.folder._id
- : entity.doc
- ? entity.doc._id
- : entity.file._id
- const entityType = entity.folder ? 'folder' : entity.doc ? 'doc' : 'file'
- ProjectEntityMongoUpdateHandler.renameEntity(
- projectId,
- entityId,
- entityType,
- rename.newName,
- (err, project, startPath, endPath, rev, changes) => {
- if (err) {
- return cb(err)
- }
- // update the renamed entity for the resync
- entity.path = rename.newPath
- if (entityType === 'folder') {
- entity.folder.name = rename.newName
- } else if (entityType === 'doc') {
- entity.doc.name = rename.newName
- } else {
- entity.file.name = rename.newName
- }
- DocumentUpdaterHandler.updateProjectStructure(
- projectId,
- projectHistoryId,
- null,
- changes,
- 'automatic-fix',
- cb
- )
- }
- )
- }
- // Avoid conflicts by processing renames in the reverse order. If we have
- // the following starting situation:
- //
- // somefile.tex
- // somefile.tex
- // somefile.tex (1)
- //
- // somefile.tex would be processed first, and then somefile.tex (1),
- // yielding the following renames:
- //
- // somefile.tex -> somefile.tex (1)
- // somefile.tex (1) -> somefile.tex (2)
- //
- // When the first rename was decided, we didn't know that somefile.tex (1)
- // existed, so that created a conflict. By processing renames in the
- // reverse order, we start with the files that had the most extensive
- // information about existing files.
- renames.reverse()
- async.eachSeries(renames, doRename, callback)
- },
- findNextAvailablePath(allPaths, candidatePath) {
- const incrementReplacer = (match, p1) => {
- return ' (' + (parseInt(p1, 10) + 1) + ')'
- }
- // if the filename was invalid we should normalise it here too. Currently
- // this only handles renames in the same folder, so we will be out of luck
- // if it is the folder name which in invalid. We could handle folder
- // renames by returning the folders list from getAllEntitiesFromProject
- do {
- // does the filename look like "foo (1)" if so, increment the number in parentheses
- if (/ \(\d+\)$/.test(candidatePath)) {
- candidatePath = candidatePath.replace(/ \((\d+)\)$/, incrementReplacer)
- } else {
- // otherwise, add a ' (1)' suffix to the name
- candidatePath = candidatePath + ' (1)'
- }
- } while (allPaths.has(candidatePath)) // keep going until the name is unique
- // add the new name to the set
- allPaths.add(candidatePath)
- return candidatePath
- },
- isPathValidForRootDoc(docPath) {
- const docExtension = Path.extname(docPath)
- return VALID_ROOT_DOC_REGEXP.test(docExtension)
- },
- _cleanUpEntity(
- project,
- newProject,
- entity,
- entityType,
- path,
- userId,
- source,
- callback
- ) {
- const subtreeListing = _listSubtree(entity, entityType, path)
- ProjectEntityUpdateHandler._updateProjectStructureWithDeletedEntity(
- project,
- newProject,
- subtreeListing,
- userId,
- source,
- error => {
- if (error != null) {
- return callback(error)
- }
- const jobs = []
- for (const entry of subtreeListing) {
- if (entry.type === 'doc') {
- jobs.push(cb => {
- ProjectEntityUpdateHandler._cleanUpDoc(
- project,
- entry.entity,
- entry.path,
- userId,
- cb
- )
- })
- } else if (entry.type === 'file') {
- jobs.push(cb => {
- ProjectEntityUpdateHandler._cleanUpFile(
- project,
- entry.entity,
- entry.path,
- userId,
- cb
- )
- })
- }
- }
- async.series(jobs, err => {
- if (err) {
- return callback(err)
- }
- callback(null, subtreeListing)
- })
- }
- )
- },
- _updateProjectStructureWithDeletedEntity(
- project,
- newProject,
- subtreeListing,
- userId,
- source,
- callback
- ) {
- const changes = { oldDocs: [], oldFiles: [] }
- for (const entry of subtreeListing) {
- if (entry.type === 'doc') {
- changes.oldDocs.push({ doc: entry.entity, path: entry.path })
- } else if (entry.type === 'file') {
- changes.oldFiles.push({ file: entry.entity, path: entry.path })
- }
- }
- // 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,
- source,
- 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
- )
- },
- convertDocToFile: wrapWithLock({
- beforeLock(next) {
- return function (projectId, docId, userId, source, 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,
- source,
- callback
- )
- })
- }
- )
- }
- )
- })
- }
- )
- }
- )
- })
- }
- },
- withLock(
- projectId,
- doc,
- path,
- fileRef,
- fileStoreUrl,
- userId,
- source,
- 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,
- },
- source,
- 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)
- }
- )
- }
- )
- }
- )
- },
- }),
- }
- /**
- * List all descendants of an entity along with their type and path. Include
- * the top-level entity as well.
- */
- function _listSubtree(entity, entityType, entityPath) {
- if (entityType.indexOf('file') !== -1) {
- return [{ type: 'file', entity, path: entityPath }]
- } else if (entityType.indexOf('doc') !== -1) {
- return [{ type: 'doc', entity, path: entityPath }]
- } else if (entityType.indexOf('folder') !== -1) {
- const listing = []
- const _recurseFolder = (folder, folderPath) => {
- listing.push({ type: 'folder', entity: folder, path: folderPath })
- for (const doc of iterablePaths(folder, 'docs')) {
- listing.push({
- type: 'doc',
- entity: doc,
- path: Path.join(folderPath, doc.name),
- })
- }
- for (const file of iterablePaths(folder, 'fileRefs')) {
- listing.push({
- type: 'file',
- entity: file,
- path: Path.join(folderPath, file.name),
- })
- }
- for (const childFolder of iterablePaths(folder, 'folders')) {
- _recurseFolder(childFolder, Path.join(folderPath, childFolder.name))
- }
- }
- _recurseFolder(entity, entityPath)
- return listing
- } else {
- // This shouldn't happen, but if it does, fail silently.
- return []
- }
- }
- 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'],
- },
- })
|