mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-24 21:12:38 -04:00
889 lines
30 KiB
JavaScript
889 lines
30 KiB
JavaScript
|
/* eslint-disable
|
||
|
camelcase,
|
||
|
handle-callback-err,
|
||
|
max-len,
|
||
|
one-var,
|
||
|
standard/no-callback-literal,
|
||
|
*/
|
||
|
// TODO: This file was created by bulk-decaffeinate.
|
||
|
// Fix any style issues and re-enable lint.
|
||
|
/*
|
||
|
* decaffeinate suggestions:
|
||
|
* DS101: Remove unnecessary use of Array.from
|
||
|
* DS102: Remove unnecessary code created because of implicit returns
|
||
|
* DS103: Rewrite code to no longer use __guard__
|
||
|
* DS201: Simplify complex destructure assignments
|
||
|
* DS207: Consider shorter variations of null checks
|
||
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||
|
*/
|
||
|
let ProjectEntityMongoUpdateHandler, self
|
||
|
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'
|
||
|
|
||
|
const wrapWithLock = function(methodWithoutLock) {
|
||
|
// 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.
|
||
|
const methodWithLock = function(project_id, ...rest) {
|
||
|
const adjustedLength = Math.max(rest.length, 1),
|
||
|
args = rest.slice(0, adjustedLength - 1),
|
||
|
callback = rest[adjustedLength - 1]
|
||
|
return LockManager.runWithLock(
|
||
|
LOCK_NAMESPACE,
|
||
|
project_id,
|
||
|
cb => methodWithoutLock(project_id, ...Array.from(args), cb),
|
||
|
callback
|
||
|
)
|
||
|
}
|
||
|
methodWithLock.withoutLock = methodWithoutLock
|
||
|
return methodWithLock
|
||
|
}
|
||
|
|
||
|
module.exports = ProjectEntityMongoUpdateHandler = self = {
|
||
|
LOCK_NAMESPACE,
|
||
|
|
||
|
addDoc: wrapWithLock(function(project_id, folder_id, doc, callback) {
|
||
|
if (callback == null) {
|
||
|
callback = function(err, result) {}
|
||
|
}
|
||
|
return ProjectGetter.getProjectWithoutLock(
|
||
|
project_id,
|
||
|
{ rootFolder: true, name: true, overleaf: true },
|
||
|
function(err, project) {
|
||
|
if (err != null) {
|
||
|
logger.err({ project_id, err }, 'error getting project for add doc')
|
||
|
return callback(err)
|
||
|
}
|
||
|
logger.log(
|
||
|
{ project_id, folder_id, doc_name: doc.name },
|
||
|
'adding doc to project with project'
|
||
|
)
|
||
|
return self._confirmFolder(project, folder_id, folder_id => {
|
||
|
return self._putElement(project, folder_id, doc, 'doc', callback)
|
||
|
})
|
||
|
}
|
||
|
)
|
||
|
}),
|
||
|
|
||
|
addFile: wrapWithLock(function(project_id, folder_id, fileRef, callback) {
|
||
|
if (callback == null) {
|
||
|
callback = function(error, result, project) {}
|
||
|
}
|
||
|
return ProjectGetter.getProjectWithoutLock(
|
||
|
project_id,
|
||
|
{ rootFolder: true, name: true, overleaf: true },
|
||
|
function(err, project) {
|
||
|
if (err != null) {
|
||
|
logger.err({ project_id, err }, 'error getting project for add file')
|
||
|
return callback(err)
|
||
|
}
|
||
|
logger.log(
|
||
|
{ project_id: project._id, folder_id, file_name: fileRef.name },
|
||
|
'adding file'
|
||
|
)
|
||
|
return self._confirmFolder(project, folder_id, folder_id =>
|
||
|
self._putElement(project, folder_id, fileRef, 'file', callback)
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}),
|
||
|
|
||
|
replaceFileWithNew: wrapWithLock(
|
||
|
(project_id, file_id, newFileRef, callback) =>
|
||
|
ProjectGetter.getProjectWithoutLock(
|
||
|
project_id,
|
||
|
{ rootFolder: true, name: true, overleaf: true },
|
||
|
function(err, project) {
|
||
|
if (err != null) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
return ProjectLocator.findElement(
|
||
|
{ project, element_id: file_id, type: 'file' },
|
||
|
(err, fileRef, path) => {
|
||
|
if (err != null) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
return ProjectEntityMongoUpdateHandler._insertDeletedFileReference(
|
||
|
project_id,
|
||
|
fileRef,
|
||
|
function(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.
|
||
|
return Project.findOneAndUpdate(
|
||
|
conditions,
|
||
|
update,
|
||
|
{ new: true },
|
||
|
function(err, newProject) {
|
||
|
if (err != null) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
return callback(null, fileRef, project, path, newProject)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
),
|
||
|
|
||
|
mkdirp: wrapWithLock(function(project_id, path, options, callback) {
|
||
|
// 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)
|
||
|
|
||
|
return ProjectGetter.getProjectWithOnlyFolders(
|
||
|
project_id,
|
||
|
(err, project) => {
|
||
|
if (path === '/') {
|
||
|
logger.log(
|
||
|
{ project_id: project._id },
|
||
|
'mkdir is only trying to make path of / so sending back root folder'
|
||
|
)
|
||
|
return callback(null, [], project.rootFolder[0])
|
||
|
}
|
||
|
logger.log({ project_id: project._id, path, folders }, 'running mkdirp')
|
||
|
|
||
|
let builtUpPath = ''
|
||
|
const procesFolder = (previousFolders, folderName, callback) => {
|
||
|
let parentFolder_id
|
||
|
previousFolders = previousFolders || []
|
||
|
const parentFolder = previousFolders[previousFolders.length - 1]
|
||
|
if (parentFolder != null) {
|
||
|
parentFolder_id = parentFolder._id
|
||
|
}
|
||
|
builtUpPath = `${builtUpPath}/${folderName}`
|
||
|
return ProjectLocator.findElementByPath(
|
||
|
{
|
||
|
project,
|
||
|
path: builtUpPath,
|
||
|
exactCaseMatch:
|
||
|
options != null ? options.exactCaseMatch : undefined
|
||
|
},
|
||
|
(err, foundFolder) => {
|
||
|
if (foundFolder == null) {
|
||
|
logger.log(
|
||
|
{ path, project_id: project._id, folderName },
|
||
|
'making folder from mkdirp'
|
||
|
)
|
||
|
return self.addFolder.withoutLock(
|
||
|
project_id,
|
||
|
parentFolder_id,
|
||
|
folderName,
|
||
|
function(err, newFolder, parentFolder_id) {
|
||
|
if (err != null) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
newFolder.parentFolder_id = parentFolder_id
|
||
|
previousFolders.push(newFolder)
|
||
|
return callback(null, previousFolders)
|
||
|
}
|
||
|
)
|
||
|
} else {
|
||
|
foundFolder.filterOut = true
|
||
|
previousFolders.push(foundFolder)
|
||
|
return callback(null, previousFolders)
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return async.reduce(folders, [], procesFolder, function(err, folders) {
|
||
|
if (err != null) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
const lastFolder = folders[folders.length - 1]
|
||
|
folders = _.select(folders, folder => !folder.filterOut)
|
||
|
return callback(null, folders, lastFolder)
|
||
|
})
|
||
|
}
|
||
|
)
|
||
|
}),
|
||
|
|
||
|
moveEntity: wrapWithLock(function(
|
||
|
project_id,
|
||
|
entity_id,
|
||
|
destFolderId,
|
||
|
entityType,
|
||
|
callback
|
||
|
) {
|
||
|
if (callback == null) {
|
||
|
callback = function(error) {}
|
||
|
}
|
||
|
return ProjectGetter.getProjectWithoutLock(
|
||
|
project_id,
|
||
|
{ rootFolder: true, name: true, overleaf: true },
|
||
|
function(err, project) {
|
||
|
if (err != null) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
return ProjectLocator.findElement(
|
||
|
{ project, element_id: entity_id, type: entityType },
|
||
|
function(err, entity, entityPath) {
|
||
|
if (err != null) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
// Prevent top-level docs/files with reserved names (to match v1 behaviour)
|
||
|
if (self._blockedFilename(entityPath, entityType)) {
|
||
|
return callback(
|
||
|
new Errors.InvalidNameError('blocked element name')
|
||
|
)
|
||
|
}
|
||
|
return self._checkValidMove(
|
||
|
project,
|
||
|
entityType,
|
||
|
entity,
|
||
|
entityPath,
|
||
|
destFolderId,
|
||
|
function(error) {
|
||
|
if (error != null) {
|
||
|
return callback(error)
|
||
|
}
|
||
|
return ProjectEntityHandler.getAllEntitiesFromProject(
|
||
|
project,
|
||
|
function(error, oldDocs, oldFiles) {
|
||
|
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.
|
||
|
return self._putElement(
|
||
|
project,
|
||
|
destFolderId,
|
||
|
entity,
|
||
|
entityType,
|
||
|
function(err, result) {
|
||
|
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.
|
||
|
return self._removeElementFromMongoArray(
|
||
|
Project,
|
||
|
project_id,
|
||
|
entityPath.mongo,
|
||
|
entity_id,
|
||
|
function(err, newProject) {
|
||
|
if (err != null) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
return ProjectEntityHandler.getAllEntitiesFromProject(
|
||
|
newProject,
|
||
|
function(err, newDocs, newFiles) {
|
||
|
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
|
||
|
) {
|
||
|
logger.err(
|
||
|
{
|
||
|
project_id,
|
||
|
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'
|
||
|
)
|
||
|
)
|
||
|
}
|
||
|
return callback(
|
||
|
null,
|
||
|
project,
|
||
|
startPath,
|
||
|
endPath,
|
||
|
entity.rev,
|
||
|
changes,
|
||
|
callback
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}),
|
||
|
|
||
|
deleteEntity: wrapWithLock((project_id, entity_id, entityType, callback) =>
|
||
|
ProjectGetter.getProjectWithoutLock(
|
||
|
project_id,
|
||
|
{ name: true, rootFolder: true, overleaf: true },
|
||
|
function(error, project) {
|
||
|
if (error != null) {
|
||
|
return callback(error)
|
||
|
}
|
||
|
return ProjectLocator.findElement(
|
||
|
{ project, element_id: entity_id, type: entityType },
|
||
|
function(error, entity, path) {
|
||
|
if (error != null) {
|
||
|
return callback(error)
|
||
|
}
|
||
|
return self._removeElementFromMongoArray(
|
||
|
Project,
|
||
|
project_id,
|
||
|
path.mongo,
|
||
|
entity_id,
|
||
|
function(error, newProject) {
|
||
|
if (error != null) {
|
||
|
return callback(error)
|
||
|
}
|
||
|
return callback(null, entity, path, project, newProject)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
),
|
||
|
|
||
|
renameEntity: wrapWithLock(
|
||
|
(project_id, entity_id, entityType, newName, callback) =>
|
||
|
ProjectGetter.getProjectWithoutLock(
|
||
|
project_id,
|
||
|
{ rootFolder: true, name: true, overleaf: true },
|
||
|
(error, project) => {
|
||
|
if (error != null) {
|
||
|
return callback(error)
|
||
|
}
|
||
|
return ProjectEntityHandler.getAllEntitiesFromProject(
|
||
|
project,
|
||
|
(error, oldDocs, oldFiles) => {
|
||
|
if (error != null) {
|
||
|
return callback(error)
|
||
|
}
|
||
|
return ProjectLocator.findElement(
|
||
|
{ project, element_id: entity_id, type: entityType },
|
||
|
(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 (
|
||
|
self._blockedFilename({ fileSystem: endPath }, entityType)
|
||
|
) {
|
||
|
return callback(
|
||
|
new Errors.InvalidNameError('blocked element name')
|
||
|
)
|
||
|
}
|
||
|
// check if the new name already exists in the current folder
|
||
|
return self._checkValidElementName(
|
||
|
parentFolder,
|
||
|
newName,
|
||
|
error => {
|
||
|
if (error != null) {
|
||
|
return callback(error)
|
||
|
}
|
||
|
const conditions = { _id: project_id }
|
||
|
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
|
||
|
return Project.findOneAndUpdate(
|
||
|
conditions,
|
||
|
update,
|
||
|
{ new: true },
|
||
|
function(error, newProject) {
|
||
|
if (error != null) {
|
||
|
return callback(error)
|
||
|
}
|
||
|
return ProjectEntityHandler.getAllEntitiesFromProject(
|
||
|
newProject,
|
||
|
(error, newDocs, newFiles) => {
|
||
|
if (error != null) {
|
||
|
return callback(error)
|
||
|
}
|
||
|
const startPath = entPath.fileSystem
|
||
|
const changes = {
|
||
|
oldDocs,
|
||
|
newDocs,
|
||
|
oldFiles,
|
||
|
newFiles,
|
||
|
newProject
|
||
|
}
|
||
|
return callback(
|
||
|
null,
|
||
|
project,
|
||
|
startPath,
|
||
|
endPath,
|
||
|
entity.rev,
|
||
|
changes,
|
||
|
callback
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
),
|
||
|
|
||
|
addFolder: wrapWithLock((project_id, parentFolder_id, folderName, callback) =>
|
||
|
ProjectGetter.getProjectWithoutLock(
|
||
|
project_id,
|
||
|
{ rootFolder: true, name: true, overleaf: true },
|
||
|
function(err, project) {
|
||
|
if (err != null) {
|
||
|
logger.err(
|
||
|
{ project_id, err },
|
||
|
'error getting project for add folder'
|
||
|
)
|
||
|
return callback(err)
|
||
|
}
|
||
|
return self._confirmFolder(
|
||
|
project,
|
||
|
parentFolder_id,
|
||
|
parentFolder_id => {
|
||
|
const folder = new Folder({ name: folderName })
|
||
|
logger.log(
|
||
|
{ project: project._id, parentFolder_id, folderName },
|
||
|
'adding new folder'
|
||
|
)
|
||
|
return self._putElement(
|
||
|
project,
|
||
|
parentFolder_id,
|
||
|
folder,
|
||
|
'folder',
|
||
|
err => {
|
||
|
if (err != null) {
|
||
|
logger.err(
|
||
|
{ err, project_id: project._id },
|
||
|
'error adding folder to project'
|
||
|
)
|
||
|
return callback(err)
|
||
|
}
|
||
|
return callback(null, folder, parentFolder_id)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
),
|
||
|
|
||
|
_removeElementFromMongoArray(model, model_id, path, element_id, callback) {
|
||
|
if (callback == null) {
|
||
|
callback = function(err, project) {}
|
||
|
}
|
||
|
const conditions = { _id: model_id }
|
||
|
const pullUpdate = { $pull: {}, $inc: {} }
|
||
|
const nonArrayPath = path.slice(0, path.lastIndexOf('.'))
|
||
|
// remove specific element from array by id
|
||
|
pullUpdate['$pull'][nonArrayPath] = { _id: element_id }
|
||
|
// we need to increment the project version number for any structure change
|
||
|
pullUpdate['$inc']['version'] = 1
|
||
|
return model.findOneAndUpdate(
|
||
|
conditions,
|
||
|
pullUpdate,
|
||
|
{ new: true },
|
||
|
callback
|
||
|
)
|
||
|
},
|
||
|
|
||
|
_countElements(project) {
|
||
|
var countFolder = function(folder) {
|
||
|
let total = 0
|
||
|
|
||
|
for (let subfolder of Array.from(
|
||
|
(folder != null ? folder.folders : undefined) || []
|
||
|
)) {
|
||
|
total += countFolder(subfolder)
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
__guard__(folder != null ? folder.folders : undefined, x => x.length) !=
|
||
|
null
|
||
|
) {
|
||
|
total += folder.folders.length
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
__guard__(folder != null ? folder.docs : undefined, x1 => x1.length) !=
|
||
|
null
|
||
|
) {
|
||
|
total += folder.docs.length
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
__guard__(
|
||
|
folder != null ? folder.fileRefs : undefined,
|
||
|
x2 => x2.length
|
||
|
) != null
|
||
|
) {
|
||
|
total += folder.fileRefs.length
|
||
|
}
|
||
|
|
||
|
return total
|
||
|
}
|
||
|
|
||
|
return countFolder(project.rootFolder[0])
|
||
|
},
|
||
|
|
||
|
_putElement(project, folder_id, element, type, callback) {
|
||
|
let e
|
||
|
if (callback == null) {
|
||
|
callback = function(err, path, project) {}
|
||
|
}
|
||
|
const sanitizeTypeOfElement = function(elementType) {
|
||
|
const lastChar = elementType.slice(-1)
|
||
|
if (lastChar !== 's') {
|
||
|
elementType += 's'
|
||
|
}
|
||
|
if (elementType === 'files') {
|
||
|
elementType = 'fileRefs'
|
||
|
}
|
||
|
return elementType
|
||
|
}
|
||
|
|
||
|
if (element == null || element._id == null) {
|
||
|
e = new Error('no element passed to be inserted')
|
||
|
logger.err(
|
||
|
{ project_id: project._id, folder_id, element, type },
|
||
|
'failed trying to insert element as it was null'
|
||
|
)
|
||
|
return callback(e)
|
||
|
}
|
||
|
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)) {
|
||
|
e = new Errors.InvalidNameError('invalid element name')
|
||
|
logger.err(
|
||
|
{ project_id: project._id, folder_id, element, type },
|
||
|
'failed trying to insert element as name was invalid'
|
||
|
)
|
||
|
return callback(e)
|
||
|
}
|
||
|
|
||
|
if (folder_id == null) {
|
||
|
folder_id = project.rootFolder[0]._id
|
||
|
}
|
||
|
|
||
|
if (self._countElements(project) > settings.maxEntitiesPerProject) {
|
||
|
logger.warn(
|
||
|
{ project_id: project._id },
|
||
|
'project too big, stopping insertions'
|
||
|
)
|
||
|
CooldownManager.putProjectOnCooldown(project._id)
|
||
|
return callback('project_has_to_many_files')
|
||
|
}
|
||
|
|
||
|
return ProjectLocator.findElement(
|
||
|
{ project, element_id: folder_id, type: 'folders' },
|
||
|
(err, folder, path) => {
|
||
|
if (err != null) {
|
||
|
logger.err(
|
||
|
{ err, project_id: project._id, folder_id, type, element },
|
||
|
'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)
|
||
|
if (self._blockedFilename(newPath, type)) {
|
||
|
return callback(new Errors.InvalidNameError('blocked element name'))
|
||
|
}
|
||
|
return self._checkValidElementName(folder, element.name, err => {
|
||
|
if (err != null) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
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(
|
||
|
{
|
||
|
project_id: project._id,
|
||
|
element_id: element._id,
|
||
|
fileType: type,
|
||
|
folder_id,
|
||
|
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}
|
||
|
return Project.findOneAndUpdate(
|
||
|
conditions,
|
||
|
update,
|
||
|
{ new: true },
|
||
|
function(err, newProject) {
|
||
|
if (err != null) {
|
||
|
logger.err(
|
||
|
{ err, project_id: project._id },
|
||
|
'error saving in putElement project'
|
||
|
)
|
||
|
return callback(err)
|
||
|
}
|
||
|
return callback(err, { path: newPath }, newProject)
|
||
|
}
|
||
|
)
|
||
|
})
|
||
|
}
|
||
|
)
|
||
|
},
|
||
|
|
||
|
_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)
|
||
|
const [dir, file] = Array.from([
|
||
|
path.dirname(entityPath.fileSystem),
|
||
|
path.basename(entityPath.fileSystem)
|
||
|
])
|
||
|
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".
|
||
|
if (callback == null) {
|
||
|
callback = function(err) {}
|
||
|
}
|
||
|
const err = new Errors.InvalidNameError('file already exists')
|
||
|
for (let doc of Array.from(
|
||
|
(folder != null ? folder.docs : undefined) || []
|
||
|
)) {
|
||
|
if (doc.name === name) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
}
|
||
|
for (let file of Array.from(
|
||
|
(folder != null ? folder.fileRefs : undefined) || []
|
||
|
)) {
|
||
|
if (file.name === name) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
}
|
||
|
for (folder of Array.from(
|
||
|
(folder != null ? folder.folders : undefined) || []
|
||
|
)) {
|
||
|
if (folder.name === name) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
}
|
||
|
return callback()
|
||
|
},
|
||
|
|
||
|
_confirmFolder(project, folder_id, callback) {
|
||
|
logger.log(
|
||
|
{ folder_id, project_id: project._id },
|
||
|
'confirming folder in project'
|
||
|
)
|
||
|
if (folder_id + '' === 'undefined') {
|
||
|
return callback(project.rootFolder[0]._id)
|
||
|
} else if (folder_id !== null) {
|
||
|
return callback(folder_id)
|
||
|
} else {
|
||
|
return callback(project.rootFolder[0]._id)
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_checkValidMove(
|
||
|
project,
|
||
|
entityType,
|
||
|
entity,
|
||
|
entityPath,
|
||
|
destFolderId,
|
||
|
callback
|
||
|
) {
|
||
|
if (callback == null) {
|
||
|
callback = function(error) {}
|
||
|
}
|
||
|
return ProjectLocator.findElement(
|
||
|
{ project, element_id: destFolderId, type: 'folder' },
|
||
|
function(err, destEntity, destFolderPath) {
|
||
|
if (err != null) {
|
||
|
return callback(err)
|
||
|
}
|
||
|
// check if there is already a doc/file/folder with the same name
|
||
|
// in the destination folder
|
||
|
return self._checkValidElementName(destEntity, entity.name, function(
|
||
|
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'
|
||
|
)
|
||
|
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'
|
||
|
)
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
return callback()
|
||
|
})
|
||
|
}
|
||
|
)
|
||
|
},
|
||
|
|
||
|
_insertDeletedDocReference(project_id, doc, callback) {
|
||
|
if (callback == null) {
|
||
|
callback = function(error) {}
|
||
|
}
|
||
|
return Project.update(
|
||
|
{
|
||
|
_id: project_id
|
||
|
},
|
||
|
{
|
||
|
$push: {
|
||
|
deletedDocs: {
|
||
|
_id: doc._id,
|
||
|
name: doc.name,
|
||
|
deletedAt: new Date()
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
{},
|
||
|
callback
|
||
|
)
|
||
|
},
|
||
|
|
||
|
_insertDeletedFileReference(project_id, fileRef, callback) {
|
||
|
if (callback == null) {
|
||
|
callback = function(error) {}
|
||
|
}
|
||
|
return Project.update(
|
||
|
{
|
||
|
_id: project_id
|
||
|
},
|
||
|
{
|
||
|
$push: {
|
||
|
deletedFiles: {
|
||
|
_id: fileRef._id,
|
||
|
name: fileRef.name,
|
||
|
linkedFileData: fileRef.linkedFileData,
|
||
|
hash: fileRef.hash,
|
||
|
deletedAt: new Date()
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
{},
|
||
|
callback
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function __guard__(value, transform) {
|
||
|
return typeof value !== 'undefined' && value !== null
|
||
|
? transform(value)
|
||
|
: undefined
|
||
|
}
|