Merge pull request #2724 from overleaf/ns-doc-over-file

allow upload of doc over existing file

GitOrigin-RevId: 13578bf4ab6d54686077402488399db0379cc761
This commit is contained in:
Alasdair Smith 2020-04-27 14:00:56 +01:00 committed by Copybot
parent be900488a9
commit 3a1ab63cce
4 changed files with 237 additions and 13 deletions

View file

@ -43,6 +43,7 @@ module.exports = {
'newProject'
]),
replaceDocWithFile: callbackify(replaceDocWithFile),
replaceFileWithDoc: callbackify(replaceFileWithDoc),
mkdirp: callbackifyMultiResult(wrapWithLock(mkdirp), [
'newFolders',
'folder'
@ -78,6 +79,7 @@ module.exports = {
addFolder: wrapWithLock(addFolder),
replaceFileWithNew: wrapWithLock(replaceFileWithNew),
replaceDocWithFile: wrapWithLock(replaceDocWithFile),
replaceFileWithDoc: wrapWithLock(replaceFileWithDoc),
mkdirp: wrapWithLock(mkdirp),
moveEntity: wrapWithLock(moveEntity),
deleteEntity: wrapWithLock(deleteEntity),
@ -210,6 +212,33 @@ async function replaceDocWithFile(projectId, docId, fileRef) {
return newProject
}
async function replaceFileWithDoc(projectId, fileId, newDoc) {
const project = await ProjectGetter.promises.getProjectWithoutLock(
projectId,
{ rootFolder: true, name: true, overleaf: true }
)
const { path } = await ProjectLocator.promises.findElement({
project,
element_id: fileId,
type: 'file'
})
const folderMongoPath = _getParentMongoPath(path.mongo)
const newProject = await Project.findOneAndUpdate(
{ _id: project._id },
{
$pull: {
[`${folderMongoPath}.fileRefs`]: { _id: fileId }
},
$push: {
[`${folderMongoPath}.docs`]: newDoc
},
$inc: { version: 1 }
},
{ new: true }
).exec()
return newProject
}
async function mkdirp(projectId, path, options = {}) {
// defaults to case insensitive paths, use options {exactCaseMatch:true}
// to make matching case-sensitive

View file

@ -754,21 +754,92 @@ const ProjectEntityUpdateHandler = {
}
ProjectLocator.findElement(
{ project_id: projectId, element_id: folderId, type: 'folder' },
(error, folder) => {
(error, folder, path) => {
if (error != null) {
return callback(error)
}
if (folder == null) {
return callback(new Error("Couldn't find folder"))
}
let existingDoc = null
for (let doc of folder.docs) {
if (doc.name === docName) {
existingDoc = doc
break
}
}
if (existingDoc != null) {
const existingDoc = folder.docs.find(({ name }) => name === docName)
const existingFile = folder.fileRefs.find(
({ name }) => name === docName
)
if (existingFile) {
const doc = new Doc({ name: docName })
DocstoreManager.updateDoc(
projectId.toString(),
doc._id.toString(),
docLines,
0,
{},
(err, modified, rev) => {
if (err != null) {
return callback(err)
}
ProjectEntityMongoUpdateHandler.replaceFileWithDoc(
projectId,
existingFile._id,
doc,
(err, project) => {
if (err) {
return callback(err)
}
TpdsUpdateSender.addDoc(
{
project_id: projectId,
doc_id: doc._id,
path: path.fileSystem,
project_name: project.name,
rev: existingFile.rev + 1
},
err => {
if (err) {
return callback(err)
}
const docPath = path.fileSystem
const projectHistoryId =
project.overleaf &&
project.overleaf.history &&
project.overleaf.history.id
const newDocs = [
{
doc,
path: docPath,
docLines: docLines.join('\n')
}
]
const oldFiles = [
{
file: existingFile,
path: Path.join(path.fileSystem, existingFile.name)
}
]
DocumentUpdaterHandler.updateProjectStructure(
projectId,
projectHistoryId,
userId,
{ oldFiles, newDocs, newProject: project },
error => {
if (error != null) {
return callback(error)
}
EditorRealTimeController.emitToRoom(
projectId,
'removeEntity',
existingFile._id,
'convertFileToDoc'
)
callback(null, doc, true)
}
)
}
)
}
)
}
)
} else if (existingDoc) {
DocumentUpdaterHandler.setDocument(
projectId,
existingDoc._id,

View file

@ -1113,4 +1113,27 @@ describe('ProjectEntityMongoUpdateHandler', function() {
this.ProjectMock.verify()
})
})
describe('replaceFileWithDoc', function() {
it('should simultaneously remove the file and add the doc', async function() {
this.ProjectMock.expects('findOneAndUpdate')
.withArgs(
{ _id: this.project._id },
{
$pull: { 'rootFolder.0.fileRefs': { _id: this.file._id } },
$push: { 'rootFolder.0.docs': this.doc },
$inc: { version: 1 }
},
{ new: true }
)
.chain('exec')
.resolves(this.project)
await this.subject.promises.replaceFileWithDoc(
this.project._id,
this.file._id,
this.doc
)
this.ProjectMock.verify()
})
})
})

View file

@ -126,7 +126,8 @@ describe('ProjectEntityUpdateHandler', function() {
moveEntity: sinon.stub(),
renameEntity: sinon.stub(),
deleteEntity: sinon.stub(),
replaceDocWithFile: sinon.stub()
replaceDocWithFile: sinon.stub(),
replaceFileWithDoc: sinon.stub()
}
this.TpdsUpdateSender = {
addFile: sinon.stub().yields(),
@ -865,7 +866,12 @@ describe('ProjectEntityUpdateHandler', function() {
describe('updating an existing doc', function() {
beforeEach(function() {
this.existingDoc = { _id: docId, name: this.docName }
this.folder = { _id: folderId, docs: [this.existingDoc] }
this.existingFile = { _id: fileId, name: this.fileName }
this.folder = {
_id: folderId,
docs: [this.existingDoc],
fileRefs: [this.existingFile]
}
this.ProjectLocator.findElement.yields(null, this.folder)
this.DocumentUpdaterHandler.setDocument.yields()
@ -915,7 +921,7 @@ describe('ProjectEntityUpdateHandler', function() {
describe('creating a new doc', function() {
beforeEach(function() {
this.folder = { _id: folderId, docs: [] }
this.folder = { _id: folderId, docs: [], fileRefs: [] }
this.newDoc = { _id: docId }
this.ProjectLocator.findElement.yields(null, this.folder)
this.ProjectEntityUpdateHandler.addDocWithRanges = {
@ -963,7 +969,7 @@ describe('ProjectEntityUpdateHandler', function() {
describe('upserting a new doc with an invalid name', function() {
beforeEach(function() {
this.folder = { _id: folderId, docs: [] }
this.folder = { _id: folderId, docs: [], fileRefs: [] }
this.newDoc = { _id: docId }
this.ProjectLocator.findElement.yields(null, this.folder)
this.ProjectEntityUpdateHandler.addDocWithRanges = {
@ -986,6 +992,101 @@ describe('ProjectEntityUpdateHandler', function() {
this.callback.calledWithMatch(errorMatcher).should.equal(true)
})
})
describe('upserting a doc on top of a file', function() {
beforeEach(function() {
this.newProject = {
name: 'new project',
overleaf: { history: { id: projectHistoryId } }
}
this.existingFile = { _id: fileId, name: 'foo.tex', rev: 12 }
this.folder = { _id: folderId, docs: [], fileRefs: [this.existingFile] }
this.newDoc = { _id: docId }
this.docLines = ['line one', 'line two']
this.path = 'path/to/file'
this.ProjectLocator.findElement.yields(null, this.folder, {
fileSystem: this.path
})
this.DocstoreManager.updateDoc.yields()
this.ProjectEntityMongoUpdateHandler.replaceFileWithDoc.yields(
null,
this.newProject
)
this.TpdsUpdateSender.addDoc.yields()
this.ProjectEntityUpdateHandler.upsertDoc(
projectId,
folderId,
'foo.tex',
this.docLines,
this.source,
userId,
this.callback
)
})
it('notifies docstore of the new doc', function() {
expect(this.DocstoreManager.updateDoc).to.have.been.calledWith(
projectId,
this.newDoc._id,
this.docLines
)
})
it('adds the new doc and removes the file in one go', function() {
expect(
this.ProjectEntityMongoUpdateHandler.replaceFileWithDoc
).to.have.been.calledWithMatch(
projectId,
this.existingFile._id,
this.newDoc
)
})
it('sends the doc to TPDS', function() {
expect(this.TpdsUpdateSender.addDoc).to.have.been.calledWith({
project_id: projectId,
doc_id: this.newDoc._id,
path: this.path,
project_name: this.newProject.name,
rev: this.existingFile.rev + 1
})
})
it('sends the updates to the doc updater', function() {
const oldFiles = [
{
file: this.existingFile,
path: `${this.path}/foo.tex`
}
]
const newDocs = [
{
doc: sinon.match(this.newDoc),
path: this.path,
docLines: this.docLines.join('\n')
}
]
expect(
this.DocumentUpdaterHandler.updateProjectStructure
).to.have.been.calledWith(projectId, projectHistoryId, userId, {
oldFiles,
newDocs,
newProject: this.newProject
})
})
it('should notify everyone of the file deletion', function() {
expect(
this.EditorRealTimeController.emitToRoom
).to.have.been.calledWith(
projectId,
'removeEntity',
this.existingFile._id,
'convertFileToDoc'
)
})
})
})
describe('upsertFile', function() {