[web] support for reverting binary files (#18033)

* [web] revert binary file

* use addEntityWithName if file was deleted

* todo comments

* only show Revert file in ui even if deleted

* use _revertBinaryFile function

* emit new ids when reverting

* format:fix

* await emitToRoom calls

* use EditorController.upsertFile

* remove _revertBinaryFile function

* binary file check

* mock importFile method in tests

* move findElementByPath stub

* debug ci error

* resolve with empty object as file

* fix tests

* remove await before expect()

* format:fix

* test when binary file exists and when it does not

* use "file-revert" for source

* [web] revert existing file without ranges support (#18107)

* [web] revert existing file without ranges support

* ignore document_updated_externally if file-revert

* fix test

GitOrigin-RevId: a5e0c83a7635bc7d934dec9debe916bdd4beb51e
This commit is contained in:
Domagoj Kriskovic 2024-05-29 12:07:40 +02:00 committed by Copybot
parent 3b2e60ece7
commit 218a4538c1
5 changed files with 106 additions and 40 deletions

View file

@ -8,6 +8,7 @@ const moment = require('moment')
const { callbackifyAll } = require('@overleaf/promise-utils') const { callbackifyAll } = require('@overleaf/promise-utils')
const { fetchJson } = require('@overleaf/fetch-utils') const { fetchJson } = require('@overleaf/fetch-utils')
const ProjectLocator = require('../Project/ProjectLocator') const ProjectLocator = require('../Project/ProjectLocator')
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
const RestoreManager = { const RestoreManager = {
async restoreFileFromV2(userId, projectId, version, pathname) { async restoreFileFromV2(userId, projectId, version, pathname) {
@ -42,6 +43,7 @@ const RestoreManager = {
}, },
async revertFile(userId, projectId, version, pathname) { async revertFile(userId, projectId, version, pathname) {
const source = 'file-revert'
const fsPath = await RestoreManager._writeFileVersionToDisk( const fsPath = await RestoreManager._writeFileVersionToDisk(
projectId, projectId,
version, version,
@ -50,34 +52,53 @@ const RestoreManager = {
const basename = Path.basename(pathname) const basename = Path.basename(pathname)
let dirname = Path.dirname(pathname) let dirname = Path.dirname(pathname)
if (dirname === '.') { if (dirname === '.') {
// no directory // root directory
dirname = '' dirname = '/'
} }
const parentFolderId = await RestoreManager._findOrCreateFolder( const parentFolderId = await RestoreManager._findOrCreateFolder(
projectId, projectId,
dirname dirname
) )
let fileExists = true const file = await ProjectLocator.promises
try { .findElementByPath({
// TODO: Is there a better way of doing this? project_id: projectId,
await ProjectLocator.promises.findElementByPath({
projectId,
path: pathname, path: pathname,
}) })
} catch (error) { .catch(() => null)
fileExists = false
}
if (fileExists) {
throw new Errors.InvalidError('File already exists')
}
const importInfo = await FileSystemImportManager.promises.importFile( const importInfo = await FileSystemImportManager.promises.importFile(
fsPath, fsPath,
pathname pathname
) )
if (importInfo.type !== 'doc') { if (importInfo.type === 'file') {
// TODO: Handle binary files const newFile = await EditorController.promises.upsertFile(
throw new Errors.InvalidError('File is not editable') projectId,
parentFolderId,
basename,
fsPath,
file?.element?.linkedFileData,
source,
userId
)
return {
_id: newFile._id,
type: importInfo.type,
}
}
if (file) {
await DocumentUpdaterHandler.promises.setDocument(
projectId,
file.element._id,
userId,
importInfo.lines,
source
)
return {
_id: file.element._id,
type: importInfo.type,
}
} }
const ranges = await RestoreManager._getRangesFromHistory( const ranges = await RestoreManager._getRangesFromHistory(

View file

@ -12,7 +12,7 @@ export interface Meta {
start_ts: number start_ts: number
end_ts: number end_ts: number
type?: 'external' // TODO type?: 'external' // TODO
source?: 'git-bridge' // TODO source?: 'git-bridge' | 'file-revert' // TODO
origin?: { origin?: {
kind: kind:
| 'dropbox' | 'dropbox'

View file

@ -283,6 +283,9 @@ export const EditorManagerProvider: FC = ({ children }) => {
) { ) {
return return
} }
if (update.meta.source === 'file-revert') {
return
}
showGenericMessageModal( showGenericMessageModal(
t('document_updated_externally'), t('document_updated_externally'),
t('document_updated_externally_detail') t('document_updated_externally_detail')

View file

@ -442,6 +442,9 @@ export default EditorManager = (function () {
) { ) {
return return
} }
if (update?.meta?.source === 'file-revert') {
return
}
return this.ide.showGenericMessageModal( return this.ide.showGenericMessageModal(
'Document Updated Externally', 'Document Updated Externally',
'This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history.' 'This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history.'

View file

@ -23,6 +23,8 @@ describe('RestoreManager', function () {
promises: {}, promises: {},
}), }),
'../Project/ProjectLocator': (this.ProjectLocator = { promises: {} }), '../Project/ProjectLocator': (this.ProjectLocator = { promises: {} }),
'../DocumentUpdater/DocumentUpdaterHandler':
(this.DocumentUpdaterHandler = { promises: {} }),
}, },
}) })
this.user_id = 'mock-user-id' this.user_id = 'mock-user-id'
@ -217,41 +219,78 @@ describe('RestoreManager', function () {
describe('with an existing file in the current project', function () { describe('with an existing file in the current project', function () {
beforeEach(function () { beforeEach(function () {
this.pathname = 'foo.tex' this.pathname = 'foo.tex'
this.ProjectLocator.promises.findElementByPath = sinon.stub().resolves() this.FileSystemImportManager.promises.importFile = sinon
.stub()
.resolves({ type: 'doc' })
this.ProjectLocator.promises.findElementByPath = sinon
.stub()
.resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
this.FileSystemImportManager.promises.importFile = sinon
.stub()
.resolves({ type: 'doc', lines: ['foo', 'bar', 'baz'] })
this.DocumentUpdaterHandler.promises.setDocument = sinon
.stub()
.resolves()
}) })
it('should reject', function () { it('should call setDocument in document updater and revert file', async function () {
expect( const revertRes = await this.RestoreManager.promises.revertFile(
this.RestoreManager.promises.revertFile(
this.user_id, this.user_id,
this.project_id, this.project_id,
this.version, this.version,
this.pathname this.pathname
) )
expect(
this.DocumentUpdaterHandler.promises.setDocument
).to.have.been.calledWith(
this.project_id,
'mock-file-id',
this.user_id,
['foo', 'bar', 'baz'],
'file-revert'
) )
.to.eventually.be.rejectedWith('File already exists') expect(revertRes).to.deep.equal({ _id: 'mock-file-id', type: 'doc' })
.and.be.instanceOf(Errors.InvalidError)
}) })
}) })
describe('when reverting a binary file', function () { describe('when reverting a binary file', function () {
beforeEach(function () { beforeEach(async function () {
this.pathname = 'foo.png' this.pathname = 'foo.png'
this.FileSystemImportManager.promises.importFile = sinon this.FileSystemImportManager.promises.importFile = sinon
.stub() .stub()
.resolves({ type: 'binary' }) .resolves({ type: 'file' })
this.EditorController.promises.upsertFile = sinon
.stub()
.resolves({ _id: 'mock-file-id', type: 'file' })
}) })
it('should reject', function () {
expect( it('should return the created entity if file exists', async function () {
this.RestoreManager.promises.revertFile( this.ProjectLocator.promises.findElementByPath = sinon
.stub()
.resolves({ type: 'file' })
const revertRes = await this.RestoreManager.promises.revertFile(
this.user_id, this.user_id,
this.project_id, this.project_id,
this.version, this.version,
this.pathname this.pathname
) )
expect(revertRes).to.deep.equal({ _id: 'mock-file-id', type: 'file' })
})
it('should return the created entity if file does not exists', async function () {
this.ProjectLocator.promises.findElementByPath = sinon.stub().rejects()
const revertRes = await this.RestoreManager.promises.revertFile(
this.user_id,
this.project_id,
this.version,
this.pathname
) )
.to.eventually.be.rejectedWith('File is not editable')
.and.be.instanceOf(Errors.InvalidError) expect(revertRes).to.deep.equal({ _id: 'mock-file-id', type: 'file' })
}) })
}) })