mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Merge pull request #28672 from overleaf/em-reapply-restore-optimization
Reapply file restore optimization GitOrigin-RevId: c4479141d890e3e0af746f406a0186bf21f3d918
This commit is contained in:
@@ -294,6 +294,8 @@ class Change {
|
|||||||
if (this.v2DocVersions) {
|
if (this.v2DocVersions) {
|
||||||
snapshot.updateV2DocVersions(this.v2DocVersions)
|
snapshot.updateV2DocVersions(this.v2DocVersions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
snapshot.setTimestamp(this.timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ class Snapshot {
|
|||||||
return new Snapshot(
|
return new Snapshot(
|
||||||
FileMap.fromRaw(raw.files),
|
FileMap.fromRaw(raw.files),
|
||||||
raw.projectVersion,
|
raw.projectVersion,
|
||||||
V2DocVersions.fromRaw(raw.v2DocVersions)
|
V2DocVersions.fromRaw(raw.v2DocVersions),
|
||||||
|
raw.timestamp ? new Date(raw.timestamp) : undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,8 +46,15 @@ class Snapshot {
|
|||||||
const raw = {
|
const raw = {
|
||||||
files: this.fileMap.toRaw(),
|
files: this.fileMap.toRaw(),
|
||||||
}
|
}
|
||||||
if (this.projectVersion) raw.projectVersion = this.projectVersion
|
if (this.projectVersion) {
|
||||||
if (this.v2DocVersions) raw.v2DocVersions = this.v2DocVersions.toRaw()
|
raw.projectVersion = this.projectVersion
|
||||||
|
}
|
||||||
|
if (this.v2DocVersions) {
|
||||||
|
raw.v2DocVersions = this.v2DocVersions.toRaw()
|
||||||
|
}
|
||||||
|
if (this.timestamp != null) {
|
||||||
|
raw.timestamp = this.timestamp.toISOString()
|
||||||
|
}
|
||||||
return raw
|
return raw
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,13 +62,15 @@ class Snapshot {
|
|||||||
* @param {FileMap} [fileMap]
|
* @param {FileMap} [fileMap]
|
||||||
* @param {string} [projectVersion]
|
* @param {string} [projectVersion]
|
||||||
* @param {V2DocVersions} [v2DocVersions]
|
* @param {V2DocVersions} [v2DocVersions]
|
||||||
|
* @param {Date} [timestamp]
|
||||||
*/
|
*/
|
||||||
constructor(fileMap, projectVersion, v2DocVersions) {
|
constructor(fileMap, projectVersion, v2DocVersions, timestamp) {
|
||||||
assert.maybe.instance(fileMap, FileMap, 'bad fileMap')
|
assert.maybe.instance(fileMap, FileMap, 'bad fileMap')
|
||||||
|
|
||||||
this.fileMap = fileMap || new FileMap({})
|
this.fileMap = fileMap || new FileMap({})
|
||||||
this.projectVersion = projectVersion
|
this.projectVersion = projectVersion
|
||||||
this.v2DocVersions = v2DocVersions
|
this.v2DocVersions = v2DocVersions
|
||||||
|
this.timestamp = timestamp ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,6 +119,17 @@ class Snapshot {
|
|||||||
v2DocVersions.applyTo(this)
|
v2DocVersions.applyTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTimestamp() {
|
||||||
|
return this.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Date} timestamp
|
||||||
|
*/
|
||||||
|
setTimestamp(timestamp) {
|
||||||
|
this.timestamp = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The underlying file map.
|
* The underlying file map.
|
||||||
* @return {FileMap}
|
* @return {FileMap}
|
||||||
@@ -268,6 +289,7 @@ class Snapshot {
|
|||||||
files: rawFiles,
|
files: rawFiles,
|
||||||
projectVersion,
|
projectVersion,
|
||||||
v2DocVersions: rawV2DocVersions,
|
v2DocVersions: rawV2DocVersions,
|
||||||
|
timestamp: this.getTimestamp() ?? undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export type RawSnapshot = {
|
|||||||
files: RawFileMap
|
files: RawFileMap
|
||||||
projectVersion?: string
|
projectVersion?: string
|
||||||
v2DocVersions?: RawV2DocVersions | null
|
v2DocVersions?: RawV2DocVersions | null
|
||||||
|
timestamp?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RawHistory = {
|
export type RawHistory = {
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const { expect } = require('chai')
|
const { expect } = require('chai')
|
||||||
const core = require('..')
|
const {
|
||||||
const Change = core.Change
|
Change,
|
||||||
const File = core.File
|
File,
|
||||||
const Operation = core.Operation
|
Operation,
|
||||||
|
AddFileOperation,
|
||||||
|
Snapshot,
|
||||||
|
Origin,
|
||||||
|
RestoreFileOrigin,
|
||||||
|
} = require('..')
|
||||||
|
|
||||||
describe('Change', function () {
|
describe('Change', function () {
|
||||||
describe('findBlobHashes', function () {
|
describe('findBlobHashes', function () {
|
||||||
@@ -37,9 +42,9 @@ describe('Change', function () {
|
|||||||
|
|
||||||
describe('RestoreFileOrigin', function () {
|
describe('RestoreFileOrigin', function () {
|
||||||
it('should convert to and from raw', function () {
|
it('should convert to and from raw', function () {
|
||||||
const origin = new core.RestoreFileOrigin(1, 'path', new Date())
|
const origin = new RestoreFileOrigin(1, 'path', new Date())
|
||||||
const raw = origin.toRaw()
|
const raw = origin.toRaw()
|
||||||
const newOrigin = core.Origin.fromRaw(raw)
|
const newOrigin = Origin.fromRaw(raw)
|
||||||
expect(newOrigin).to.eql(origin)
|
expect(newOrigin).to.eql(origin)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -56,7 +61,19 @@ describe('Change', function () {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(change.getOrigin()).to.be.an.instanceof(core.RestoreFileOrigin)
|
expect(change.getOrigin()).to.be.an.instanceof(RestoreFileOrigin)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('applyTo', function () {
|
||||||
|
it('sets the timestamp on the snapshot', function () {
|
||||||
|
const snapshot = new Snapshot()
|
||||||
|
snapshot.addFile('main.tex', File.fromString(''))
|
||||||
|
const operation = new AddFileOperation('main.tex', File.fromString(''))
|
||||||
|
const now = new Date()
|
||||||
|
const change = new Change([operation], now)
|
||||||
|
change.applyTo(snapshot)
|
||||||
|
expect(snapshot.getTimestamp().toISOString()).to.equal(now.toISOString())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const withTmpDir = require('./with_tmp_dir')
|
|||||||
const StreamSizeLimit = require('./stream_size_limit')
|
const StreamSizeLimit = require('./stream_size_limit')
|
||||||
const { getProjectBlobsBatch } = require('../../storage/lib/blob_store')
|
const { getProjectBlobsBatch } = require('../../storage/lib/blob_store')
|
||||||
const assert = require('../../storage/lib/assert')
|
const assert = require('../../storage/lib/assert')
|
||||||
|
const { getChunkMetadataForVersion } = require('../../storage/lib/chunk_store')
|
||||||
|
|
||||||
const pipeline = promisify(Stream.pipeline)
|
const pipeline = promisify(Stream.pipeline)
|
||||||
|
|
||||||
@@ -396,7 +397,28 @@ async function getSnapshotAtVersion(projectId, version) {
|
|||||||
chunk.getChanges(),
|
chunk.getChanges(),
|
||||||
chunk.getEndVersion() - version
|
chunk.getEndVersion() - version
|
||||||
)
|
)
|
||||||
snapshot.applyAll(changes)
|
|
||||||
|
if (changes.length > 0) {
|
||||||
|
snapshot.applyAll(changes)
|
||||||
|
} else {
|
||||||
|
// There are no changes in this chunk; we need to look at the previous chunk
|
||||||
|
// to get the snapshot's timestamp
|
||||||
|
let chunkMetadata
|
||||||
|
try {
|
||||||
|
chunkMetadata = await getChunkMetadataForVersion(projectId, version)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Chunk.VersionNotFoundError) {
|
||||||
|
// The snapshot is the first snapshot of the first chunk, so we can't
|
||||||
|
// find a timestamp. This shouldn't happen often. Ignore the error and
|
||||||
|
// leave the timestamp empty.
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.setTimestamp(chunkMetadata.endTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
return snapshot
|
return snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ describe('persistChanges', function () {
|
|||||||
content: '',
|
content: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
timestamp: thirdChange.getTimestamp().toISOString(),
|
||||||
})
|
})
|
||||||
const history = new History(snapshot, [thirdChange])
|
const history = new History(snapshot, [thirdChange])
|
||||||
const currentChunk = new Chunk(history, 2)
|
const currentChunk = new Chunk(history, 2)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import mongodb from 'mongodb-legacy'
|
import mongodb from 'mongodb-legacy'
|
||||||
import nock from 'nock'
|
import nock from 'nock'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
|
import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
|
||||||
import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
|
import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
|
||||||
const { ObjectId } = mongodb
|
const { ObjectId } = mongodb
|
||||||
@@ -50,6 +51,12 @@ describe('LatestSnapshot', function () {
|
|||||||
.get(`/api/projects/${this.historyId}/latest/history`)
|
.get(`/api/projects/${this.historyId}/latest/history`)
|
||||||
.replyWithFile(200, fixture('chunks/0-3.json'))
|
.replyWithFile(200, fixture('chunks/0-3.json'))
|
||||||
|
|
||||||
|
const fixtureData = JSON.parse(
|
||||||
|
readFileSync(fixture('chunks/0-3.json'), 'utf8')
|
||||||
|
)
|
||||||
|
const changes = fixtureData.chunk.history.changes
|
||||||
|
const lastTimestamp = changes[changes.length - 1].timestamp
|
||||||
|
|
||||||
ProjectHistoryClient.getLatestSnapshot(this.projectId, (error, body) => {
|
ProjectHistoryClient.getLatestSnapshot(this.projectId, (error, body) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error
|
throw error
|
||||||
@@ -69,6 +76,7 @@ describe('LatestSnapshot', function () {
|
|||||||
operations: [{ textOperation: [26, '\n\nFour five six'] }],
|
operations: [{ textOperation: [26, '\n\nFour five six'] }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
timestamp: lastTimestamp,
|
||||||
},
|
},
|
||||||
version: 3,
|
version: 3,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import EditorController from '../Editor/EditorController.js'
|
|||||||
import Errors from '../Errors/Errors.js'
|
import Errors from '../Errors/Errors.js'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { callbackifyAll } from '@overleaf/promise-utils'
|
import { callbackifyAll } from '@overleaf/promise-utils'
|
||||||
import { fetchJson } from '@overleaf/fetch-utils'
|
|
||||||
import ProjectLocator from '../Project/ProjectLocator.js'
|
import ProjectLocator from '../Project/ProjectLocator.js'
|
||||||
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.js'
|
import DocumentUpdaterHandler from '../DocumentUpdater/DocumentUpdaterHandler.js'
|
||||||
import ChatApiHandler from '../Chat/ChatApiHandler.js'
|
import ChatApiHandler from '../Chat/ChatApiHandler.js'
|
||||||
@@ -69,6 +68,13 @@ const RestoreManager = {
|
|||||||
)
|
)
|
||||||
const snapshot = Snapshot.fromRaw(snapshotRaw)
|
const snapshot = Snapshot.fromRaw(snapshotRaw)
|
||||||
|
|
||||||
|
const origin = options.origin ?? {
|
||||||
|
kind: 'file-restore',
|
||||||
|
path: pathname,
|
||||||
|
version,
|
||||||
|
timestamp: snapshot.getTimestamp()?.toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
return await RestoreManager._revertSingleFile(
|
return await RestoreManager._revertSingleFile(
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -76,7 +82,7 @@ const RestoreManager = {
|
|||||||
pathname,
|
pathname,
|
||||||
threadIds,
|
threadIds,
|
||||||
snapshot,
|
snapshot,
|
||||||
options
|
{ origin }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -131,17 +137,9 @@ const RestoreManager = {
|
|||||||
})
|
})
|
||||||
.catch(() => null)
|
.catch(() => null)
|
||||||
|
|
||||||
const updates = await RestoreManager._getUpdatesFromHistory(
|
const snapshotFile = projectSnapshotAtVersion.getFile(pathname)
|
||||||
projectId,
|
if (!snapshotFile) {
|
||||||
version
|
throw new OError('file not found in snapshot', { pathname })
|
||||||
)
|
|
||||||
const updateAtVersion = updates.find(update => update.toV === version)
|
|
||||||
|
|
||||||
const origin = options.origin || {
|
|
||||||
kind: 'file-restore',
|
|
||||||
path: pathname,
|
|
||||||
version,
|
|
||||||
timestamp: new Date(updateAtVersion.meta.end_ts).toISOString(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const importInfo = await FileSystemImportManager.promises.importFile(
|
const importInfo = await FileSystemImportManager.promises.importFile(
|
||||||
@@ -162,7 +160,7 @@ const RestoreManager = {
|
|||||||
projectId,
|
projectId,
|
||||||
file.element._id,
|
file.element._id,
|
||||||
file.type,
|
file.type,
|
||||||
origin,
|
options.origin,
|
||||||
userId
|
userId
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -177,25 +175,24 @@ const RestoreManager = {
|
|||||||
threadIds.delete(file.element._id.toString())
|
threadIds.delete(file.element._id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshotFile = projectSnapshotAtVersion.getFile(pathname)
|
|
||||||
if (!snapshotFile) {
|
|
||||||
throw new OError('file not found in snapshot', { pathname })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for metadata indicating a linked file.
|
// Look for metadata indicating a linked file.
|
||||||
const fileMetadata = snapshotFile.getMetadata()
|
const fileMetadata = snapshotFile.getMetadata()
|
||||||
const isFileMetadata = fileMetadata && 'provider' in fileMetadata
|
const isFileMetadata = fileMetadata && 'provider' in fileMetadata
|
||||||
|
|
||||||
logger.debug({ fileMetadata }, 'metadata from history')
|
logger.debug({ fileMetadata }, 'metadata from history')
|
||||||
|
|
||||||
if (importInfo.type === 'file' || isFileMetadata) {
|
if (
|
||||||
|
!snapshotFile.isEditable() ||
|
||||||
|
importInfo.type === 'file' ||
|
||||||
|
isFileMetadata
|
||||||
|
) {
|
||||||
const newFile = await EditorController.promises.upsertFile(
|
const newFile = await EditorController.promises.upsertFile(
|
||||||
projectId,
|
projectId,
|
||||||
parentFolderId,
|
parentFolderId,
|
||||||
basename,
|
basename,
|
||||||
fsPath,
|
fsPath,
|
||||||
fileMetadata,
|
fileMetadata,
|
||||||
origin,
|
options.origin,
|
||||||
userId
|
userId
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -296,13 +293,17 @@ const RestoreManager = {
|
|||||||
newCommentThreadData
|
newCommentThreadData
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const lines = snapshotFile
|
||||||
|
.getContent({ filterTrackedDeletes: true })
|
||||||
|
.split('\n')
|
||||||
|
|
||||||
const { _id } = await EditorController.promises.addDocWithRanges(
|
const { _id } = await EditorController.promises.addDocWithRanges(
|
||||||
projectId,
|
projectId,
|
||||||
parentFolderId,
|
parentFolderId,
|
||||||
basename,
|
basename,
|
||||||
importInfo.lines,
|
lines,
|
||||||
newRanges,
|
newRanges,
|
||||||
origin,
|
options.origin,
|
||||||
userId
|
userId
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -362,31 +363,21 @@ const RestoreManager = {
|
|||||||
throw new OError('project does not have ranges support', { projectId })
|
throw new OError('project does not have ranges support', { projectId })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get project paths at version
|
|
||||||
const pathsAtPastVersion = await RestoreManager._getProjectPathsAtVersion(
|
|
||||||
projectId,
|
|
||||||
version
|
|
||||||
)
|
|
||||||
|
|
||||||
const updates = await RestoreManager._getUpdatesFromHistory(
|
|
||||||
projectId,
|
|
||||||
version
|
|
||||||
)
|
|
||||||
const updateAtVersion = updates.find(update => update.toV === version)
|
|
||||||
|
|
||||||
const origin = {
|
|
||||||
kind: 'project-restore',
|
|
||||||
version,
|
|
||||||
timestamp: new Date(updateAtVersion.meta.end_ts).toISOString(),
|
|
||||||
}
|
|
||||||
const threadIds = await getCommentThreadIds(projectId)
|
|
||||||
|
|
||||||
const snapshotRaw = await HistoryManager.promises.getContentAtVersion(
|
const snapshotRaw = await HistoryManager.promises.getContentAtVersion(
|
||||||
projectId,
|
projectId,
|
||||||
version
|
version
|
||||||
)
|
)
|
||||||
const snapshot = Snapshot.fromRaw(snapshotRaw)
|
const snapshot = Snapshot.fromRaw(snapshotRaw)
|
||||||
|
|
||||||
|
const pathsAtPastVersion = snapshot.getFilePathnames()
|
||||||
|
|
||||||
|
const origin = {
|
||||||
|
kind: 'project-restore',
|
||||||
|
version,
|
||||||
|
timestamp: snapshot.getTimestamp()?.toISOString(),
|
||||||
|
}
|
||||||
|
const threadIds = await getCommentThreadIds(projectId)
|
||||||
|
|
||||||
const reverted = []
|
const reverted = []
|
||||||
for (const pathname of pathsAtPastVersion) {
|
for (const pathname of pathsAtPastVersion) {
|
||||||
const res = await RestoreManager._revertSingleFile(
|
const res = await RestoreManager._revertSingleFile(
|
||||||
@@ -437,18 +428,6 @@ const RestoreManager = {
|
|||||||
}/project/${projectId}/version/${version}/${encodeURIComponent(pathname)}`
|
}/project/${projectId}/version/${version}/${encodeURIComponent(pathname)}`
|
||||||
return await FileWriter.promises.writeUrlToDisk(projectId, url)
|
return await FileWriter.promises.writeUrlToDisk(projectId, url)
|
||||||
},
|
},
|
||||||
|
|
||||||
async _getUpdatesFromHistory(projectId, version) {
|
|
||||||
const url = `${Settings.apis.project_history.url}/project/${projectId}/updates?before=${version}&min_count=1`
|
|
||||||
const res = await fetchJson(url)
|
|
||||||
return res.updates
|
|
||||||
},
|
|
||||||
|
|
||||||
async _getProjectPathsAtVersion(projectId, version) {
|
|
||||||
const url = `${Settings.apis.project_history.url}/project/${projectId}/paths/version/${version}`
|
|
||||||
const res = await fetchJson(url)
|
|
||||||
return res.paths
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { ...callbackifyAll(RestoreManager), promises: RestoreManager }
|
export default { ...callbackifyAll(RestoreManager), promises: RestoreManager }
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ describe('RestoreManager', function () {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -161,12 +162,22 @@ describe('RestoreManager', function () {
|
|||||||
getFile: pathname => ({
|
getFile: pathname => ({
|
||||||
getStringLength: sinon.stub().returns(100),
|
getStringLength: sinon.stub().returns(100),
|
||||||
getByteLength: sinon.stub().returns(100),
|
getByteLength: sinon.stub().returns(100),
|
||||||
getContent: sinon.stub().returns('file content'),
|
getContent: sinon.stub().returns('foo\nbar\nbaz'),
|
||||||
isEditable: sinon.stub().returns(true),
|
isEditable: sinon.stub().returns(true),
|
||||||
getMetadata: sinon
|
getMetadata: sinon
|
||||||
.stub()
|
.stub()
|
||||||
.returns(snapshotData?.files?.[pathname]?.metadata),
|
.returns(snapshotData?.files?.[pathname]?.metadata),
|
||||||
}),
|
}),
|
||||||
|
getFilePathnames: sinon
|
||||||
|
.stub()
|
||||||
|
.returns(Object.keys(snapshotData.files || {})),
|
||||||
|
getTimestamp: sinon
|
||||||
|
.stub()
|
||||||
|
.returns(
|
||||||
|
snapshotData?.timestamp
|
||||||
|
? new Date(snapshotData.timestamp)
|
||||||
|
: new Date()
|
||||||
|
),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
getDocUpdaterCompatibleRanges: (ctx.getDocUpdaterCompatibleRanges = sinon
|
getDocUpdaterCompatibleRanges: (ctx.getDocUpdaterCompatibleRanges = sinon
|
||||||
@@ -189,9 +200,7 @@ describe('RestoreManager', function () {
|
|||||||
meta: { end_ts: new Date('2024-01-01T00:00:00.000Z') },
|
meta: { end_ts: new Date('2024-01-01T00:00:00.000Z') },
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
ctx.RestoreManager.promises._getProjectPathsAtVersion = sinon
|
|
||||||
.stub()
|
|
||||||
.resolves([])
|
|
||||||
ctx.RestoreManager.promises._writeFileVersionToDisk = sinon
|
ctx.RestoreManager.promises._writeFileVersionToDisk = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.resolves('/tmp/mock-file-path')
|
.resolves('/tmp/mock-file-path')
|
||||||
@@ -699,9 +708,17 @@ describe('RestoreManager', function () {
|
|||||||
{
|
{
|
||||||
getFile: sinon.stub().returns({
|
getFile: sinon.stub().returns({
|
||||||
getMetadata: sinon.stub().returns(undefined),
|
getMetadata: sinon.stub().returns(undefined),
|
||||||
getContent: sinon.stub().returns('file content'),
|
getContent: sinon.stub().returns('foo\nbar\nbaz'),
|
||||||
isEditable: sinon.stub().returns(true),
|
isEditable: sinon.stub().returns(true),
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
origin: {
|
||||||
|
kind: 'file-restore',
|
||||||
|
path: 'foo.tex',
|
||||||
|
timestamp: new Date(ctx.endTs).toISOString(),
|
||||||
|
version: 42,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -770,9 +787,17 @@ describe('RestoreManager', function () {
|
|||||||
{
|
{
|
||||||
getFile: sinon.stub().returns({
|
getFile: sinon.stub().returns({
|
||||||
getMetadata: sinon.stub().returns(undefined),
|
getMetadata: sinon.stub().returns(undefined),
|
||||||
getContent: sinon.stub().returns('file content'),
|
getContent: sinon.stub().returns('foo\nbar\nbaz'),
|
||||||
isEditable: sinon.stub().returns(true),
|
isEditable: sinon.stub().returns(true),
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
origin: {
|
||||||
|
kind: 'file-restore',
|
||||||
|
path: 'foo.tex',
|
||||||
|
timestamp: new Date(ctx.endTs).toISOString(),
|
||||||
|
version: 42,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1128,15 +1153,41 @@ describe('RestoreManager', function () {
|
|||||||
|
|
||||||
describe('for a project with overlap in current files and old files', function () {
|
describe('for a project with overlap in current files and old files', function () {
|
||||||
beforeEach(async function (ctx) {
|
beforeEach(async function (ctx) {
|
||||||
|
ctx.HistoryManager.promises.getContentAtVersion = sinon
|
||||||
|
.stub()
|
||||||
|
.resolves({
|
||||||
|
files: {
|
||||||
|
'main.tex': {
|
||||||
|
hash: 'abcdef1234567890abcdef1234567890abcdef12',
|
||||||
|
stringLength: 100,
|
||||||
|
metadata: {
|
||||||
|
editorId: 'test-editor',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'figures/image.png': {
|
||||||
|
hash: 'abcdef1234567890abcdef1234567890abcdef12',
|
||||||
|
stringLength: 100,
|
||||||
|
metadata: {
|
||||||
|
provider: 'bar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'since-deleted.tex': {
|
||||||
|
hash: 'abcdef1234567890abcdef1234567890abcdef12',
|
||||||
|
stringLength: 100,
|
||||||
|
metadata: {
|
||||||
|
editorId: 'test-editor',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
ctx.ProjectEntityHandler.promises.getAllEntities = sinon
|
ctx.ProjectEntityHandler.promises.getAllEntities = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.resolves({
|
.resolves({
|
||||||
docs: [{ path: '/main.tex' }, { path: '/new-file.tex' }],
|
docs: [{ path: '/main.tex' }, { path: '/new-file.tex' }],
|
||||||
files: [{ path: '/figures/image.png' }],
|
files: [{ path: '/figures/image.png' }],
|
||||||
})
|
})
|
||||||
ctx.RestoreManager.promises._getProjectPathsAtVersion = sinon
|
|
||||||
.stub()
|
|
||||||
.resolves(['main.tex', 'figures/image.png', 'since-deleted.tex'])
|
|
||||||
|
|
||||||
await ctx.RestoreManager.promises.revertProject(
|
await ctx.RestoreManager.promises.revertProject(
|
||||||
ctx.user_id,
|
ctx.user_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user