2019-05-29 05:21:06 -04:00
|
|
|
const _ = require('underscore')
|
|
|
|
const async = require('async')
|
|
|
|
const logger = require('logger-sharelatex')
|
|
|
|
const path = require('path')
|
|
|
|
const settings = require('settings-sharelatex')
|
|
|
|
const CooldownManager = require('../Cooldown/CooldownManager')
|
|
|
|
const Errors = require('../Errors/Errors')
|
|
|
|
const { Folder } = require('../../models/Folder')
|
|
|
|
const LockManager = require('../../infrastructure/LockManager')
|
|
|
|
const { Project } = require('../../models/Project')
|
|
|
|
const ProjectEntityHandler = require('./ProjectEntityHandler')
|
|
|
|
const ProjectGetter = require('./ProjectGetter')
|
|
|
|
const ProjectLocator = require('./ProjectLocator')
|
|
|
|
const SafePath = require('./SafePath')
|
|
|
|
|
|
|
|
const LOCK_NAMESPACE = 'mongoTransaction'
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
function wrapWithLock(methodWithoutLock) {
|
2019-05-29 05:21:06 -04:00
|
|
|
// This lock is used whenever we read or write to an existing project's
|
|
|
|
// structure. Some operations to project structure cannot be done atomically
|
|
|
|
// in mongo, this lock is used to prevent reading the structure between two
|
|
|
|
// parts of a staged update.
|
2019-11-04 04:50:15 -05:00
|
|
|
function methodWithLock(projectId, ...rest) {
|
|
|
|
const adjustedLength = Math.max(rest.length, 1)
|
|
|
|
const args = rest.slice(0, adjustedLength - 1)
|
|
|
|
const callback = rest[adjustedLength - 1]
|
|
|
|
LockManager.runWithLock(
|
2019-05-29 05:21:06 -04:00
|
|
|
LOCK_NAMESPACE,
|
2019-11-04 04:50:15 -05:00
|
|
|
projectId,
|
|
|
|
cb => methodWithoutLock(projectId, ...args, cb),
|
2019-05-29 05:21:06 -04:00
|
|
|
callback
|
|
|
|
)
|
|
|
|
}
|
|
|
|
methodWithLock.withoutLock = methodWithoutLock
|
|
|
|
return methodWithLock
|
|
|
|
}
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
const ProjectEntityMongoUpdateHandler = {
|
2019-05-29 05:21:06 -04:00
|
|
|
LOCK_NAMESPACE,
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
addDoc: wrapWithLock(function(projectId, folderId, doc, callback) {
|
|
|
|
ProjectGetter.getProjectWithoutLock(
|
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
{ rootFolder: true, name: true, overleaf: true },
|
2019-11-04 04:50:15 -05:00
|
|
|
(err, project) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-11-04 04:50:15 -05:00
|
|
|
logger.warn({ projectId, err }, 'error getting project for add doc')
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
logger.log(
|
2019-11-04 04:50:15 -05:00
|
|
|
{ projectId, folderId, doc_name: doc.name },
|
2019-05-29 05:21:06 -04:00
|
|
|
'adding doc to project with project'
|
|
|
|
)
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._confirmFolder(
|
|
|
|
project,
|
|
|
|
folderId,
|
|
|
|
folderId => {
|
|
|
|
ProjectEntityMongoUpdateHandler._putElement(
|
|
|
|
project,
|
|
|
|
folderId,
|
|
|
|
doc,
|
|
|
|
'doc',
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
}),
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
addFile: wrapWithLock(function(projectId, folderId, fileRef, callback) {
|
|
|
|
ProjectGetter.getProjectWithoutLock(
|
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
{ rootFolder: true, name: true, overleaf: true },
|
2019-11-04 04:50:15 -05:00
|
|
|
(err, project) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-11-04 04:50:15 -05:00
|
|
|
logger.warn({ projectId, err }, 'error getting project for add file')
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
logger.log(
|
2019-11-04 04:50:15 -05:00
|
|
|
{ projectId: project._id, folderId, file_name: fileRef.name },
|
2019-05-29 05:21:06 -04:00
|
|
|
'adding file'
|
|
|
|
)
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._confirmFolder(
|
|
|
|
project,
|
|
|
|
folderId,
|
|
|
|
folderId =>
|
|
|
|
ProjectEntityMongoUpdateHandler._putElement(
|
|
|
|
project,
|
|
|
|
folderId,
|
|
|
|
fileRef,
|
|
|
|
'file',
|
|
|
|
callback
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}),
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
replaceFileWithNew: wrapWithLock((projectId, fileId, newFileRef, callback) =>
|
|
|
|
ProjectGetter.getProjectWithoutLock(
|
|
|
|
projectId,
|
|
|
|
{ rootFolder: true, name: true, overleaf: true },
|
|
|
|
(err, project) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectLocator.findElement(
|
|
|
|
{ project, element_id: fileId, type: 'file' },
|
|
|
|
(err, fileRef, path) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
ProjectEntityMongoUpdateHandler._insertDeletedFileReference(
|
|
|
|
projectId,
|
|
|
|
fileRef,
|
|
|
|
err => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
const conditions = { _id: project._id }
|
|
|
|
const inc = {}
|
|
|
|
// increment the project structure version as we are adding a new file here
|
|
|
|
inc['version'] = 1
|
|
|
|
const set = {}
|
|
|
|
set[`${path.mongo}._id`] = newFileRef._id
|
|
|
|
set[`${path.mongo}.created`] = new Date()
|
|
|
|
set[`${path.mongo}.linkedFileData`] = newFileRef.linkedFileData
|
|
|
|
inc[`${path.mongo}.rev`] = 1
|
|
|
|
set[`${path.mongo}.hash`] = newFileRef.hash
|
|
|
|
const update = {
|
|
|
|
$inc: inc,
|
|
|
|
$set: set
|
|
|
|
}
|
|
|
|
// Note: Mongoose uses new:true to return the modified document
|
|
|
|
// https://mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
|
|
|
|
// but Mongo uses returnNewDocument:true instead
|
|
|
|
// https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/
|
|
|
|
// We are using Mongoose here, but if we ever switch to a direct mongo call
|
|
|
|
// the next line will need to be updated.
|
|
|
|
Project.findOneAndUpdate(
|
|
|
|
conditions,
|
|
|
|
update,
|
|
|
|
{ new: true },
|
|
|
|
(err, newProject) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
callback(null, fileRef, project, path, newProject)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
),
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
mkdirp: wrapWithLock(function(projectId, path, options, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
// defaults to case insensitive paths, use options {exactCaseMatch:true}
|
|
|
|
// to make matching case-sensitive
|
|
|
|
let folders = path.split('/')
|
|
|
|
folders = _.select(folders, folder => folder.length !== 0)
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectGetter.getProjectWithOnlyFolders(projectId, (err, project) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
if (path === '/') {
|
|
|
|
logger.log(
|
|
|
|
{ projectId: project._id },
|
|
|
|
'mkdir is only trying to make path of / so sending back root folder'
|
|
|
|
)
|
|
|
|
return callback(null, [], project.rootFolder[0])
|
|
|
|
}
|
|
|
|
logger.log({ projectId: project._id, path, folders }, 'running mkdirp')
|
|
|
|
|
|
|
|
let builtUpPath = ''
|
|
|
|
const procesFolder = (previousFolders, folderName, callback) => {
|
|
|
|
let parentFolderId
|
|
|
|
previousFolders = previousFolders || []
|
|
|
|
const parentFolder = previousFolders[previousFolders.length - 1]
|
|
|
|
if (parentFolder != null) {
|
|
|
|
parentFolderId = parentFolder._id
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
builtUpPath = `${builtUpPath}/${folderName}`
|
|
|
|
ProjectLocator.findElementByPath(
|
|
|
|
{
|
|
|
|
project,
|
|
|
|
path: builtUpPath,
|
|
|
|
exactCaseMatch: options != null ? options.exactCaseMatch : undefined
|
|
|
|
},
|
|
|
|
(err, foundFolder) => {
|
|
|
|
if (err != null) {
|
|
|
|
logger.log(
|
|
|
|
{ path, projectId: project._id, folderName },
|
|
|
|
'making folder from mkdirp'
|
|
|
|
)
|
|
|
|
ProjectEntityMongoUpdateHandler.addFolder.withoutLock(
|
|
|
|
projectId,
|
|
|
|
parentFolderId,
|
|
|
|
folderName,
|
|
|
|
(err, newFolder, parentFolderId) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
newFolder.parentFolder_id = parentFolderId
|
|
|
|
previousFolders.push(newFolder)
|
|
|
|
callback(null, previousFolders)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
foundFolder.filterOut = true
|
|
|
|
previousFolders.push(foundFolder)
|
|
|
|
callback(null, previousFolders)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
|
|
|
|
async.reduce(folders, [], procesFolder, (err, folders) => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
const lastFolder = folders[folders.length - 1]
|
|
|
|
folders = _.select(folders, folder => !folder.filterOut)
|
|
|
|
callback(null, folders, lastFolder)
|
|
|
|
})
|
|
|
|
})
|
2019-05-29 05:21:06 -04:00
|
|
|
}),
|
|
|
|
|
|
|
|
moveEntity: wrapWithLock(function(
|
2019-11-04 04:50:15 -05:00
|
|
|
projectId,
|
|
|
|
entityId,
|
2019-05-29 05:21:06 -04:00
|
|
|
destFolderId,
|
|
|
|
entityType,
|
|
|
|
callback
|
|
|
|
) {
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectGetter.getProjectWithoutLock(
|
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
{ rootFolder: true, name: true, overleaf: true },
|
2019-11-04 04:50:15 -05:00
|
|
|
(err, project) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectLocator.findElement(
|
|
|
|
{ project, element_id: entityId, type: entityType },
|
|
|
|
(err, entity, entityPath) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
// Prevent top-level docs/files with reserved names (to match v1 behaviour)
|
2019-11-04 04:50:15 -05:00
|
|
|
if (
|
|
|
|
ProjectEntityMongoUpdateHandler._blockedFilename(
|
|
|
|
entityPath,
|
|
|
|
entityType
|
|
|
|
)
|
|
|
|
) {
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(
|
|
|
|
new Errors.InvalidNameError('blocked element name')
|
|
|
|
)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._checkValidMove(
|
2019-05-29 05:21:06 -04:00
|
|
|
project,
|
|
|
|
entityType,
|
|
|
|
entity,
|
|
|
|
entityPath,
|
|
|
|
destFolderId,
|
2019-11-04 04:50:15 -05:00
|
|
|
error => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityHandler.getAllEntitiesFromProject(
|
2019-05-29 05:21:06 -04:00
|
|
|
project,
|
2019-11-04 04:50:15 -05:00
|
|
|
(error, oldDocs, oldFiles) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
// For safety, insert the entity in the destination
|
|
|
|
// location first, and then remove the original. If
|
|
|
|
// there is an error the entity may appear twice. This
|
|
|
|
// will cause some breakage but is better than being
|
|
|
|
// lost, which is what happens if this is done in the
|
|
|
|
// opposite order.
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._putElement(
|
2019-05-29 05:21:06 -04:00
|
|
|
project,
|
|
|
|
destFolderId,
|
|
|
|
entity,
|
|
|
|
entityType,
|
2019-11-04 04:50:15 -05:00
|
|
|
(err, result) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
// Note: putElement always pushes onto the end of an
|
|
|
|
// array so it will never change an existing mongo
|
|
|
|
// path. Therefore it is safe to remove an element
|
|
|
|
// from the project with an existing path after
|
|
|
|
// calling putElement. But we must be sure that we
|
|
|
|
// have not moved a folder subfolder of itself (which
|
|
|
|
// is done by _checkValidMove above) because that
|
|
|
|
// would lead to it being deleted.
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._removeElementFromMongoArray(
|
2019-05-29 05:21:06 -04:00
|
|
|
Project,
|
2019-11-04 04:50:15 -05:00
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
entityPath.mongo,
|
2019-11-04 04:50:15 -05:00
|
|
|
entityId,
|
|
|
|
(err, newProject) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityHandler.getAllEntitiesFromProject(
|
2019-05-29 05:21:06 -04:00
|
|
|
newProject,
|
2019-11-04 04:50:15 -05:00
|
|
|
(err, newDocs, newFiles) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
const startPath = entityPath.fileSystem
|
|
|
|
const endPath = result.path.fileSystem
|
|
|
|
const changes = {
|
|
|
|
oldDocs,
|
|
|
|
newDocs,
|
|
|
|
oldFiles,
|
|
|
|
newFiles,
|
|
|
|
newProject
|
|
|
|
}
|
|
|
|
// check that no files have been lost (or duplicated)
|
|
|
|
if (
|
|
|
|
oldFiles.length !== newFiles.length ||
|
|
|
|
oldDocs.length !== newDocs.length
|
|
|
|
) {
|
2019-07-01 09:48:09 -04:00
|
|
|
logger.warn(
|
2019-05-29 05:21:06 -04:00
|
|
|
{
|
2019-11-04 04:50:15 -05:00
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
oldDocs: oldDocs.length,
|
|
|
|
newDocs: newDocs.length,
|
|
|
|
oldFiles: oldFiles.length,
|
|
|
|
newFiles: newFiles.length,
|
|
|
|
origProject: project,
|
|
|
|
newProject
|
|
|
|
},
|
|
|
|
"project corrupted moving files - shouldn't happen"
|
|
|
|
)
|
|
|
|
return callback(
|
|
|
|
new Error(
|
|
|
|
'unexpected change in project structure'
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
callback(
|
2019-05-29 05:21:06 -04:00
|
|
|
null,
|
|
|
|
project,
|
|
|
|
startPath,
|
|
|
|
endPath,
|
|
|
|
entity.rev,
|
|
|
|
changes,
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}),
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
deleteEntity: wrapWithLock((projectId, entityId, entityType, callback) =>
|
2019-05-29 05:21:06 -04:00
|
|
|
ProjectGetter.getProjectWithoutLock(
|
2019-11-04 04:50:15 -05:00
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
{ name: true, rootFolder: true, overleaf: true },
|
2019-11-04 04:50:15 -05:00
|
|
|
(error, project) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectLocator.findElement(
|
|
|
|
{ project, element_id: entityId, type: entityType },
|
|
|
|
(error, entity, path) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._removeElementFromMongoArray(
|
2019-05-29 05:21:06 -04:00
|
|
|
Project,
|
2019-11-04 04:50:15 -05:00
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
path.mongo,
|
2019-11-04 04:50:15 -05:00
|
|
|
entityId,
|
|
|
|
(error, newProject) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
callback(null, entity, path, project, newProject)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
),
|
|
|
|
|
|
|
|
renameEntity: wrapWithLock(
|
2019-11-04 04:50:15 -05:00
|
|
|
(projectId, entityId, entityType, newName, callback) =>
|
2019-05-29 05:21:06 -04:00
|
|
|
ProjectGetter.getProjectWithoutLock(
|
2019-11-04 04:50:15 -05:00
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
{ rootFolder: true, name: true, overleaf: true },
|
|
|
|
(error, project) => {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityHandler.getAllEntitiesFromProject(
|
2019-05-29 05:21:06 -04:00
|
|
|
project,
|
|
|
|
(error, oldDocs, oldFiles) => {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectLocator.findElement(
|
|
|
|
{ project, element_id: entityId, type: entityType },
|
2019-05-29 05:21:06 -04:00
|
|
|
(error, entity, entPath, parentFolder) => {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
const endPath = path.join(
|
|
|
|
path.dirname(entPath.fileSystem),
|
|
|
|
newName
|
|
|
|
)
|
|
|
|
// Prevent top-level docs/files with reserved names (to match v1 behaviour)
|
|
|
|
if (
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._blockedFilename(
|
|
|
|
{ fileSystem: endPath },
|
|
|
|
entityType
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
) {
|
|
|
|
return callback(
|
|
|
|
new Errors.InvalidNameError('blocked element name')
|
|
|
|
)
|
|
|
|
}
|
|
|
|
// check if the new name already exists in the current folder
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._checkValidElementName(
|
2019-05-29 05:21:06 -04:00
|
|
|
parentFolder,
|
|
|
|
newName,
|
|
|
|
error => {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
const conditions = { _id: projectId }
|
2019-05-29 05:21:06 -04:00
|
|
|
const update = { $set: {}, $inc: {} }
|
|
|
|
const namePath = entPath.mongo + '.name'
|
|
|
|
update['$set'][namePath] = newName
|
|
|
|
// we need to increment the project version number for any structure change
|
|
|
|
update['$inc']['version'] = 1
|
2019-11-04 04:50:15 -05:00
|
|
|
Project.findOneAndUpdate(
|
2019-05-29 05:21:06 -04:00
|
|
|
conditions,
|
|
|
|
update,
|
|
|
|
{ new: true },
|
2019-11-04 04:50:15 -05:00
|
|
|
(error, newProject) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityHandler.getAllEntitiesFromProject(
|
2019-05-29 05:21:06 -04:00
|
|
|
newProject,
|
|
|
|
(error, newDocs, newFiles) => {
|
|
|
|
if (error != null) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
const startPath = entPath.fileSystem
|
|
|
|
const changes = {
|
|
|
|
oldDocs,
|
|
|
|
newDocs,
|
|
|
|
oldFiles,
|
|
|
|
newFiles,
|
|
|
|
newProject
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
callback(
|
2019-05-29 05:21:06 -04:00
|
|
|
null,
|
|
|
|
project,
|
|
|
|
startPath,
|
|
|
|
endPath,
|
|
|
|
entity.rev,
|
|
|
|
changes,
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
),
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
addFolder: wrapWithLock((projectId, parentFolderId, folderName, callback) =>
|
2019-05-29 05:21:06 -04:00
|
|
|
ProjectGetter.getProjectWithoutLock(
|
2019-11-04 04:50:15 -05:00
|
|
|
projectId,
|
2019-05-29 05:21:06 -04:00
|
|
|
{ rootFolder: true, name: true, overleaf: true },
|
2019-11-04 04:50:15 -05:00
|
|
|
(err, project) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
2019-07-01 09:48:09 -04:00
|
|
|
logger.warn(
|
2019-11-04 04:50:15 -05:00
|
|
|
{ projectId, err },
|
2019-05-29 05:21:06 -04:00
|
|
|
'error getting project for add folder'
|
|
|
|
)
|
|
|
|
return callback(err)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._confirmFolder(
|
2019-05-29 05:21:06 -04:00
|
|
|
project,
|
2019-11-04 04:50:15 -05:00
|
|
|
parentFolderId,
|
|
|
|
parentFolderId => {
|
2019-05-29 05:21:06 -04:00
|
|
|
const folder = new Folder({ name: folderName })
|
|
|
|
logger.log(
|
2019-11-04 04:50:15 -05:00
|
|
|
{ project: project._id, parentFolderId, folderName },
|
2019-05-29 05:21:06 -04:00
|
|
|
'adding new folder'
|
|
|
|
)
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._putElement(
|
2019-05-29 05:21:06 -04:00
|
|
|
project,
|
2019-11-04 04:50:15 -05:00
|
|
|
parentFolderId,
|
2019-05-29 05:21:06 -04:00
|
|
|
folder,
|
|
|
|
'folder',
|
|
|
|
err => {
|
|
|
|
if (err != null) {
|
2019-07-01 09:48:09 -04:00
|
|
|
logger.warn(
|
2019-11-04 04:50:15 -05:00
|
|
|
{ err, projectId: project._id },
|
2019-05-29 05:21:06 -04:00
|
|
|
'error adding folder to project'
|
|
|
|
)
|
|
|
|
return callback(err)
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
callback(null, folder, parentFolderId)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
),
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
_removeElementFromMongoArray(model, modelId, path, elementId, callback) {
|
|
|
|
const conditions = { _id: modelId }
|
2019-05-29 05:21:06 -04:00
|
|
|
const pullUpdate = { $pull: {}, $inc: {} }
|
|
|
|
const nonArrayPath = path.slice(0, path.lastIndexOf('.'))
|
|
|
|
// remove specific element from array by id
|
2019-11-04 04:50:15 -05:00
|
|
|
pullUpdate['$pull'][nonArrayPath] = { _id: elementId }
|
2019-05-29 05:21:06 -04:00
|
|
|
// we need to increment the project version number for any structure change
|
|
|
|
pullUpdate['$inc']['version'] = 1
|
2019-11-04 04:50:15 -05:00
|
|
|
model.findOneAndUpdate(conditions, pullUpdate, { new: true }, callback)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
_countElements(project) {
|
2019-11-04 04:50:15 -05:00
|
|
|
function countFolder(folder) {
|
2019-05-29 05:21:06 -04:00
|
|
|
let total = 0
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
for (let subfolder of (folder != null ? folder.folders : undefined) ||
|
|
|
|
[]) {
|
2019-05-29 05:21:06 -04:00
|
|
|
total += countFolder(subfolder)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
2019-11-04 04:50:15 -05:00
|
|
|
folder != null &&
|
|
|
|
folder.folders != null &&
|
|
|
|
folder.folders.length > 0
|
2019-05-29 05:21:06 -04:00
|
|
|
) {
|
|
|
|
total += folder.folders.length
|
|
|
|
}
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
if (folder != null && folder.docs != null && folder.docs.length > 0) {
|
2019-05-29 05:21:06 -04:00
|
|
|
total += folder.docs.length
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
2019-11-04 04:50:15 -05:00
|
|
|
folder != null &&
|
|
|
|
folder.fileRefs != null &&
|
|
|
|
folder.fileRefs.length > 0
|
2019-05-29 05:21:06 -04:00
|
|
|
) {
|
|
|
|
total += folder.fileRefs.length
|
|
|
|
}
|
|
|
|
|
|
|
|
return total
|
|
|
|
}
|
|
|
|
|
|
|
|
return countFolder(project.rootFolder[0])
|
|
|
|
},
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
_putElement(project, folderId, element, type, callback) {
|
|
|
|
function sanitizeTypeOfElement(elementType) {
|
2019-05-29 05:21:06 -04:00
|
|
|
const lastChar = elementType.slice(-1)
|
|
|
|
if (lastChar !== 's') {
|
|
|
|
elementType += 's'
|
|
|
|
}
|
|
|
|
if (elementType === 'files') {
|
|
|
|
elementType = 'fileRefs'
|
|
|
|
}
|
|
|
|
return elementType
|
|
|
|
}
|
|
|
|
|
|
|
|
if (element == null || element._id == null) {
|
2019-07-01 09:48:09 -04:00
|
|
|
logger.warn(
|
2019-11-04 04:50:15 -05:00
|
|
|
{ projectId: project._id, folderId, element, type },
|
2019-05-29 05:21:06 -04:00
|
|
|
'failed trying to insert element as it was null'
|
|
|
|
)
|
2019-11-04 04:50:15 -05:00
|
|
|
return callback(new Error('no element passed to be inserted'))
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
type = sanitizeTypeOfElement(type)
|
|
|
|
|
|
|
|
// original check path.resolve("/", element.name) isnt "/#{element.name}" or element.name.match("/")
|
|
|
|
// check if name is allowed
|
|
|
|
if (!SafePath.isCleanFilename(element.name)) {
|
2019-07-01 09:48:09 -04:00
|
|
|
logger.warn(
|
2019-11-04 04:50:15 -05:00
|
|
|
{ projectId: project._id, folderId, element, type },
|
2019-05-29 05:21:06 -04:00
|
|
|
'failed trying to insert element as name was invalid'
|
|
|
|
)
|
2019-11-04 04:50:15 -05:00
|
|
|
return callback(new Errors.InvalidNameError('invalid element name'))
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
if (folderId == null) {
|
|
|
|
folderId = project.rootFolder[0]._id
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
if (
|
|
|
|
ProjectEntityMongoUpdateHandler._countElements(project) >
|
|
|
|
settings.maxEntitiesPerProject
|
|
|
|
) {
|
2019-05-29 05:21:06 -04:00
|
|
|
logger.warn(
|
2019-11-04 04:50:15 -05:00
|
|
|
{ projectId: project._id },
|
2019-05-29 05:21:06 -04:00
|
|
|
'project too big, stopping insertions'
|
|
|
|
)
|
|
|
|
CooldownManager.putProjectOnCooldown(project._id)
|
2019-07-01 09:54:23 -04:00
|
|
|
return callback(new Error('project_has_to_many_files'))
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectLocator.findElement(
|
|
|
|
{ project, element_id: folderId, type: 'folders' },
|
2019-05-29 05:21:06 -04:00
|
|
|
(err, folder, path) => {
|
|
|
|
if (err != null) {
|
2019-07-01 09:48:09 -04:00
|
|
|
logger.warn(
|
2019-11-04 04:50:15 -05:00
|
|
|
{ err, projectId: project._id, folderId, type, element },
|
2019-05-29 05:21:06 -04:00
|
|
|
'error finding folder for _putElement'
|
|
|
|
)
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
const newPath = {
|
|
|
|
fileSystem: `${path.fileSystem}/${element.name}`,
|
|
|
|
mongo: path.mongo
|
|
|
|
}
|
|
|
|
// check if the path would be too long
|
|
|
|
if (!SafePath.isAllowedLength(newPath.fileSystem)) {
|
|
|
|
return callback(new Errors.InvalidNameError('path too long'))
|
|
|
|
}
|
|
|
|
// Prevent top-level docs/files with reserved names (to match v1 behaviour)
|
2019-11-04 04:50:15 -05:00
|
|
|
if (ProjectEntityMongoUpdateHandler._blockedFilename(newPath, type)) {
|
2019-05-29 05:21:06 -04:00
|
|
|
return callback(new Errors.InvalidNameError('blocked element name'))
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._checkValidElementName(
|
|
|
|
folder,
|
|
|
|
element.name,
|
|
|
|
err => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
const id = element._id + ''
|
|
|
|
element._id = require('mongoose').Types.ObjectId(id)
|
|
|
|
const conditions = { _id: project._id }
|
|
|
|
const mongopath = `${path.mongo}.${type}`
|
|
|
|
const update = { $push: {}, $inc: {} }
|
|
|
|
update['$push'][mongopath] = element
|
|
|
|
// we need to increment the project version number for any structure change
|
|
|
|
update['$inc']['version'] = 1 // increment project version number
|
|
|
|
logger.log(
|
|
|
|
{
|
|
|
|
projectId: project._id,
|
|
|
|
element_id: element._id,
|
|
|
|
fileType: type,
|
|
|
|
folderId,
|
|
|
|
mongopath
|
|
|
|
},
|
|
|
|
'adding element to project'
|
|
|
|
)
|
|
|
|
// We are using Mongoose here, but if we ever switch to a direct mongo call
|
|
|
|
// the next line will need to be updated to {returnNewDocument:true}
|
|
|
|
Project.findOneAndUpdate(
|
|
|
|
conditions,
|
|
|
|
update,
|
|
|
|
{ new: true },
|
|
|
|
(err, newProject) => {
|
|
|
|
if (err != null) {
|
|
|
|
logger.warn(
|
|
|
|
{ err, projectId: project._id },
|
|
|
|
'error saving in putElement project'
|
|
|
|
)
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
callback(err, { path: newPath }, newProject)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
_blockedFilename(entityPath, entityType) {
|
|
|
|
// check if name would be blocked in v1
|
|
|
|
// javascript reserved names are forbidden for docs and files
|
|
|
|
// at the top-level (but folders with reserved names are allowed).
|
|
|
|
const isFolder = ['folder', 'folders'].includes(entityType)
|
2019-11-04 04:50:15 -05:00
|
|
|
const [dir, file] = [
|
2019-05-29 05:21:06 -04:00
|
|
|
path.dirname(entityPath.fileSystem),
|
|
|
|
path.basename(entityPath.fileSystem)
|
2019-11-04 04:50:15 -05:00
|
|
|
]
|
2019-05-29 05:21:06 -04:00
|
|
|
const isTopLevel = dir === '/'
|
|
|
|
if (isTopLevel && !isFolder && SafePath.isBlockedFilename(file)) {
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_checkValidElementName(folder, name, callback) {
|
|
|
|
// check if the name is already taken by a doc, file or
|
|
|
|
// folder. If so, return an error "file already exists".
|
|
|
|
const err = new Errors.InvalidNameError('file already exists')
|
2019-11-04 04:50:15 -05:00
|
|
|
for (let doc of (folder != null ? folder.docs : undefined) || []) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (doc.name === name) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
for (let file of (folder != null ? folder.fileRefs : undefined) || []) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (file.name === name) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
for (folder of (folder != null ? folder.folders : undefined) || []) {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (folder.name === name) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
callback()
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
_confirmFolder(project, folderId, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
logger.log(
|
2019-11-04 04:50:15 -05:00
|
|
|
{ folderId, projectId: project._id },
|
2019-05-29 05:21:06 -04:00
|
|
|
'confirming folder in project'
|
|
|
|
)
|
2019-11-04 04:50:15 -05:00
|
|
|
if (folderId + '' === 'undefined') {
|
|
|
|
callback(project.rootFolder[0]._id)
|
|
|
|
} else if (folderId !== null) {
|
|
|
|
callback(folderId)
|
2019-05-29 05:21:06 -04:00
|
|
|
} else {
|
2019-11-04 04:50:15 -05:00
|
|
|
callback(project.rootFolder[0]._id)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_checkValidMove(
|
|
|
|
project,
|
|
|
|
entityType,
|
|
|
|
entity,
|
|
|
|
entityPath,
|
|
|
|
destFolderId,
|
|
|
|
callback
|
|
|
|
) {
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectLocator.findElement(
|
2019-05-29 05:21:06 -04:00
|
|
|
{ project, element_id: destFolderId, type: 'folder' },
|
2019-11-04 04:50:15 -05:00
|
|
|
(err, destEntity, destFolderPath) => {
|
2019-05-29 05:21:06 -04:00
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
// check if there is already a doc/file/folder with the same name
|
|
|
|
// in the destination folder
|
2019-11-04 04:50:15 -05:00
|
|
|
ProjectEntityMongoUpdateHandler._checkValidElementName(
|
|
|
|
destEntity,
|
|
|
|
entity.name,
|
|
|
|
err => {
|
|
|
|
if (err != null) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
if (/folder/.test(entityType)) {
|
|
|
|
logger.log(
|
|
|
|
{
|
|
|
|
destFolderPath: destFolderPath.fileSystem,
|
|
|
|
folderPath: entityPath.fileSystem
|
|
|
|
},
|
|
|
|
'checking folder is not moving into child folder'
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
2019-11-04 04:50:15 -05:00
|
|
|
const isNestedFolder =
|
|
|
|
destFolderPath.fileSystem.slice(
|
|
|
|
0,
|
|
|
|
entityPath.fileSystem.length
|
|
|
|
) === entityPath.fileSystem
|
|
|
|
if (isNestedFolder) {
|
|
|
|
return callback(
|
|
|
|
new Errors.InvalidNameError(
|
|
|
|
'destination folder is a child folder of me'
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
callback()
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-04 04:50:15 -05:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
_insertDeletedDocReference(projectId, doc, callback) {
|
|
|
|
Project.update(
|
2019-05-29 05:21:06 -04:00
|
|
|
{
|
2019-11-04 04:50:15 -05:00
|
|
|
_id: projectId
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
$push: {
|
|
|
|
deletedDocs: {
|
|
|
|
_id: doc._id,
|
|
|
|
name: doc.name,
|
|
|
|
deletedAt: new Date()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{},
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
_insertDeletedFileReference(projectId, fileRef, callback) {
|
|
|
|
Project.update(
|
2019-05-29 05:21:06 -04:00
|
|
|
{
|
2019-11-04 04:50:15 -05:00
|
|
|
_id: projectId
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
$push: {
|
|
|
|
deletedFiles: {
|
|
|
|
_id: fileRef._id,
|
|
|
|
name: fileRef.name,
|
|
|
|
linkedFileData: fileRef.linkedFileData,
|
|
|
|
hash: fileRef.hash,
|
|
|
|
deletedAt: new Date()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{},
|
|
|
|
callback
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-04 04:50:15 -05:00
|
|
|
module.exports = ProjectEntityMongoUpdateHandler
|