mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #19740 from overleaf/jpa-linked-file-metadata
[misc] persist linkedFileData in full project history as file metadata GitOrigin-RevId: f3e8ba947ea34b6796e210a076a248c57188d148
This commit is contained in:
parent
e85045255c
commit
e26b6de51b
11 changed files with 691 additions and 209 deletions
|
@ -94,3 +94,9 @@ export type RawEditOperation =
|
||||||
| RawAddCommentOperation
|
| RawAddCommentOperation
|
||||||
| RawDeleteCommentOperation
|
| RawDeleteCommentOperation
|
||||||
| RawSetCommentStateOperation
|
| RawSetCommentStateOperation
|
||||||
|
|
||||||
|
export type LinkedFileData = {
|
||||||
|
importedAt: string
|
||||||
|
provider: string
|
||||||
|
[other: string]: any
|
||||||
|
}
|
||||||
|
|
|
@ -119,6 +119,7 @@ const ProjectHistoryRedisManager = {
|
||||||
ts: new Date(),
|
ts: new Date(),
|
||||||
},
|
},
|
||||||
version: projectUpdate.version,
|
version: projectUpdate.version,
|
||||||
|
metadata: projectUpdate.metadata,
|
||||||
projectHistoryId,
|
projectHistoryId,
|
||||||
}
|
}
|
||||||
if (ranges) {
|
if (ranges) {
|
||||||
|
|
|
@ -170,6 +170,53 @@ describe('ProjectHistoryRedisManager', function () {
|
||||||
.should.equal(true)
|
.should.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should queue an update with file metadata', async function () {
|
||||||
|
const metadata = {
|
||||||
|
importedAt: '2024-07-30T09:14:45.928Z',
|
||||||
|
provider: 'references-provider',
|
||||||
|
}
|
||||||
|
const projectId = 'project-id'
|
||||||
|
const fileId = 'file-id'
|
||||||
|
const url = `http://filestore/project/${projectId}/file/${fileId}`
|
||||||
|
await this.ProjectHistoryRedisManager.promises.queueAddEntity(
|
||||||
|
projectId,
|
||||||
|
this.projectHistoryId,
|
||||||
|
'file',
|
||||||
|
fileId,
|
||||||
|
this.user_id,
|
||||||
|
{
|
||||||
|
pathname: 'foo.png',
|
||||||
|
url,
|
||||||
|
version: 42,
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
this.source
|
||||||
|
)
|
||||||
|
|
||||||
|
const update = {
|
||||||
|
pathname: 'foo.png',
|
||||||
|
docLines: undefined,
|
||||||
|
url,
|
||||||
|
meta: {
|
||||||
|
user_id: this.user_id,
|
||||||
|
ts: new Date(),
|
||||||
|
source: this.source,
|
||||||
|
},
|
||||||
|
version: 42,
|
||||||
|
metadata,
|
||||||
|
projectHistoryId: this.projectHistoryId,
|
||||||
|
file: fileId,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
this.ProjectHistoryRedisManager.promises.queueOps.args[0][1]
|
||||||
|
).to.equal(JSON.stringify(update))
|
||||||
|
this.ProjectHistoryRedisManager.promises.queueOps.should.have.been.calledWithExactly(
|
||||||
|
projectId,
|
||||||
|
JSON.stringify(update)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should forward history compatible ranges if history ranges support is enabled', async function () {
|
it('should forward history compatible ranges if history ranges support is enabled', async function () {
|
||||||
this.rawUpdate.historyRangesSupport = true
|
this.rawUpdate.historyRangesSupport = true
|
||||||
this.docLines = 'the quick fox jumps over the lazy dog'
|
this.docLines = 'the quick fox jumps over the lazy dog'
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { isInsert, isDelete } from './Utils.js'
|
||||||
* @typedef {import('./types').TrackingDirective} TrackingDirective
|
* @typedef {import('./types').TrackingDirective} TrackingDirective
|
||||||
* @typedef {import('./types').TrackingType} TrackingType
|
* @typedef {import('./types').TrackingType} TrackingType
|
||||||
* @typedef {import('./types').Update} Update
|
* @typedef {import('./types').Update} Update
|
||||||
|
* @typedef {import('./types').ProjectStructureUpdate} ProjectStructureUpdate
|
||||||
*/
|
*/
|
||||||
const MAX_RESYNC_HISTORY_RECORDS = 100 // keep this many records of previous resyncs
|
const MAX_RESYNC_HISTORY_RECORDS = 100 // keep this many records of previous resyncs
|
||||||
const EXPIRE_RESYNC_HISTORY_INTERVAL_MS = 90 * 24 * 3600 * 1000 // 90 days
|
const EXPIRE_RESYNC_HISTORY_INTERVAL_MS = 90 * 24 * 3600 * 1000 // 90 days
|
||||||
|
@ -377,7 +378,7 @@ class SyncUpdateExpander {
|
||||||
constructor(projectId, snapshotFiles, origin) {
|
constructor(projectId, snapshotFiles, origin) {
|
||||||
this.projectId = projectId
|
this.projectId = projectId
|
||||||
this.files = snapshotFiles
|
this.files = snapshotFiles
|
||||||
this.expandedUpdates = []
|
this.expandedUpdates = /** @type ProjectStructureUpdate[] */ []
|
||||||
this.origin = origin
|
this.origin = origin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -471,6 +472,7 @@ class SyncUpdateExpander {
|
||||||
expectedBinaryFiles,
|
expectedBinaryFiles,
|
||||||
persistedBinaryFiles
|
persistedBinaryFiles
|
||||||
)
|
)
|
||||||
|
this.queueSetMetadataOpsForLinkedFiles(update)
|
||||||
} else if ('resyncDocContent' in update) {
|
} else if ('resyncDocContent' in update) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
{ projectId: this.projectId, update },
|
{ projectId: this.projectId, update },
|
||||||
|
@ -537,6 +539,7 @@ class SyncUpdateExpander {
|
||||||
} else {
|
} else {
|
||||||
update.file = entity.file
|
update.file = entity.file
|
||||||
update.url = entity.url
|
update.url = entity.url
|
||||||
|
update.metadata = entity.metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
this.expandedUpdates.push(update)
|
this.expandedUpdates.push(update)
|
||||||
|
@ -546,6 +549,47 @@ class SyncUpdateExpander {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queueSetMetadataOpsForLinkedFiles(update) {
|
||||||
|
const allEntities = update.resyncProjectStructure.docs.concat(
|
||||||
|
update.resyncProjectStructure.files
|
||||||
|
)
|
||||||
|
for (const file of allEntities) {
|
||||||
|
const pathname = UpdateTranslator._convertPathname(file.path)
|
||||||
|
const matchingAddFileOperation = this.expandedUpdates.some(
|
||||||
|
// Look for an addFile operation that already syncs the metadata.
|
||||||
|
u => u.pathname === pathname && u.metadata === file.metadata
|
||||||
|
)
|
||||||
|
if (matchingAddFileOperation) continue
|
||||||
|
const metaData = this.files[pathname].getMetadata()
|
||||||
|
|
||||||
|
let shouldUpdate = false
|
||||||
|
if (file.metadata) {
|
||||||
|
// check for in place update of linked-file
|
||||||
|
shouldUpdate = Object.entries(file.metadata).some(
|
||||||
|
([k, v]) => metaData[k] !== v
|
||||||
|
)
|
||||||
|
} else if (metaData.provider) {
|
||||||
|
// overwritten by non-linked-file with same hash
|
||||||
|
// or overwritten by doc
|
||||||
|
shouldUpdate = true
|
||||||
|
}
|
||||||
|
if (!shouldUpdate) continue
|
||||||
|
|
||||||
|
this.expandedUpdates.push({
|
||||||
|
pathname,
|
||||||
|
meta: {
|
||||||
|
resync: true,
|
||||||
|
origin: this.origin,
|
||||||
|
ts: update.meta.ts,
|
||||||
|
},
|
||||||
|
metadata: file.metadata || {},
|
||||||
|
})
|
||||||
|
Metrics.inc('project_history_resync_operation', 1, {
|
||||||
|
status: 'update metadata',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
queueUpdateForOutOfSyncBinaryFiles(update, expectedFiles, persistedFiles) {
|
queueUpdateForOutOfSyncBinaryFiles(update, expectedFiles, persistedFiles) {
|
||||||
// create a map to lookup persisted files by their path
|
// create a map to lookup persisted files by their path
|
||||||
const persistedFileMap = new Map(persistedFiles.map(x => [x.path, x]))
|
const persistedFileMap = new Map(persistedFiles.map(x => [x.path, x]))
|
||||||
|
@ -583,6 +627,7 @@ class SyncUpdateExpander {
|
||||||
},
|
},
|
||||||
file: entity.file,
|
file: entity.file,
|
||||||
url: entity.url,
|
url: entity.url,
|
||||||
|
metadata: entity.metadata,
|
||||||
}
|
}
|
||||||
this.expandedUpdates.push(addUpdate)
|
this.expandedUpdates.push(addUpdate)
|
||||||
Metrics.inc('project_history_resync_operation', 1, {
|
Metrics.inc('project_history_resync_operation', 1, {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { isInsert, isRetain, isDelete, isComment } from './Utils.js'
|
||||||
* @typedef {import('./types').TrackingDirective} TrackingDirective
|
* @typedef {import('./types').TrackingDirective} TrackingDirective
|
||||||
* @typedef {import('./types').TrackingProps} TrackingProps
|
* @typedef {import('./types').TrackingProps} TrackingProps
|
||||||
* @typedef {import('./types').SetCommentStateUpdate} SetCommentStateUpdate
|
* @typedef {import('./types').SetCommentStateUpdate} SetCommentStateUpdate
|
||||||
|
* @typedef {import('./types').SetFileMetadataOperation} SetFileMetadataOperation
|
||||||
* @typedef {import('./types').Update} Update
|
* @typedef {import('./types').Update} Update
|
||||||
* @typedef {import('./types').UpdateWithBlob} UpdateWithBlob
|
* @typedef {import('./types').UpdateWithBlob} UpdateWithBlob
|
||||||
*/
|
*/
|
||||||
|
@ -64,6 +65,9 @@ function _convertToChange(projectId, updateWithBlob) {
|
||||||
if (_isAddDocUpdate(update)) {
|
if (_isAddDocUpdate(update)) {
|
||||||
op.file.rangesHash = updateWithBlob.blobHashes.ranges
|
op.file.rangesHash = updateWithBlob.blobHashes.ranges
|
||||||
}
|
}
|
||||||
|
if (_isAddFileUpdate(update)) {
|
||||||
|
op.file.metadata = update.metadata
|
||||||
|
}
|
||||||
operations = [op]
|
operations = [op]
|
||||||
projectVersion = update.version
|
projectVersion = update.version
|
||||||
} else if (isTextUpdate(update)) {
|
} else if (isTextUpdate(update)) {
|
||||||
|
@ -89,6 +93,13 @@ function _convertToChange(projectId, updateWithBlob) {
|
||||||
resolved: update.resolved,
|
resolved: update.resolved,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
} else if (isSetFileMetadataOperation(update)) {
|
||||||
|
operations = [
|
||||||
|
{
|
||||||
|
pathname: _convertPathname(update.pathname),
|
||||||
|
metadata: update.metadata,
|
||||||
|
},
|
||||||
|
]
|
||||||
} else if (isDeleteCommentUpdate(update)) {
|
} else if (isDeleteCommentUpdate(update)) {
|
||||||
operations = [
|
operations = [
|
||||||
{
|
{
|
||||||
|
@ -215,6 +226,14 @@ export function isDeleteCommentUpdate(update) {
|
||||||
return 'deleteComment' in update
|
return 'deleteComment' in update
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Update} update
|
||||||
|
* @returns {update is SetFileMetadataOperation}
|
||||||
|
*/
|
||||||
|
export function isSetFileMetadataOperation(update) {
|
||||||
|
return 'metadata' in update
|
||||||
|
}
|
||||||
|
|
||||||
export function _convertPathname(pathname) {
|
export function _convertPathname(pathname) {
|
||||||
// Strip leading /
|
// Strip leading /
|
||||||
pathname = pathname.replace(/^\//, '')
|
pathname = pathname.replace(/^\//, '')
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { HistoryRanges } from '../../../document-updater/app/js/types'
|
import { HistoryRanges } from '../../../document-updater/app/js/types'
|
||||||
|
import { LinkedFileData } from 'overleaf-editor-core/lib/types'
|
||||||
|
|
||||||
export type Update =
|
export type Update =
|
||||||
| TextUpdate
|
| TextUpdate
|
||||||
|
@ -7,9 +8,16 @@ export type Update =
|
||||||
| RenameUpdate
|
| RenameUpdate
|
||||||
| DeleteCommentUpdate
|
| DeleteCommentUpdate
|
||||||
| SetCommentStateUpdate
|
| SetCommentStateUpdate
|
||||||
|
| SetFileMetadataOperation
|
||||||
| ResyncProjectStructureUpdate
|
| ResyncProjectStructureUpdate
|
||||||
| ResyncDocContentUpdate
|
| ResyncDocContentUpdate
|
||||||
|
|
||||||
|
export type ProjectStructureUpdate =
|
||||||
|
| AddDocUpdate
|
||||||
|
| AddFileUpdate
|
||||||
|
| RenameUpdate
|
||||||
|
| SetFileMetadataOperation
|
||||||
|
|
||||||
export type UpdateMeta = {
|
export type UpdateMeta = {
|
||||||
user_id: string
|
user_id: string
|
||||||
ts: number
|
ts: number
|
||||||
|
@ -38,6 +46,12 @@ export type SetCommentStateUpdate = {
|
||||||
meta: UpdateMeta
|
meta: UpdateMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SetFileMetadataOperation = {
|
||||||
|
pathname: string
|
||||||
|
meta: UpdateMeta
|
||||||
|
metadata: LinkedFileData | object
|
||||||
|
}
|
||||||
|
|
||||||
export type DeleteCommentUpdate = {
|
export type DeleteCommentUpdate = {
|
||||||
pathname: string
|
pathname: string
|
||||||
deleteComment: string
|
deleteComment: string
|
||||||
|
@ -61,6 +75,7 @@ export type AddFileUpdate = ProjectUpdateBase & {
|
||||||
pathname: string
|
pathname: string
|
||||||
file: string
|
file: string
|
||||||
url: string
|
url: string
|
||||||
|
metadata?: LinkedFileData
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RenameUpdate = ProjectUpdateBase & {
|
export type RenameUpdate = ProjectUpdateBase & {
|
||||||
|
@ -199,6 +214,8 @@ export type File = {
|
||||||
file: string
|
file: string
|
||||||
url: string
|
url: string
|
||||||
path: string
|
path: string
|
||||||
|
_hash: string
|
||||||
|
metadata?: LinkedFileData
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Entity = Doc | File
|
export type Entity = Doc | File
|
||||||
|
|
|
@ -498,10 +498,12 @@ describe('SyncManager', function () {
|
||||||
getContent: sinon.stub().returns(null),
|
getContent: sinon.stub().returns(null),
|
||||||
getHash: sinon.stub().returns(null),
|
getHash: sinon.stub().returns(null),
|
||||||
load: sinon.stub().resolves(this.loadedSnapshotDoc),
|
load: sinon.stub().resolves(this.loadedSnapshotDoc),
|
||||||
|
getMetadata: sinon.stub().returns({}),
|
||||||
},
|
},
|
||||||
'1.png': {
|
'1.png': {
|
||||||
isEditable: sinon.stub().returns(false),
|
isEditable: sinon.stub().returns(false),
|
||||||
data: { hash: this.persistedFile._hash },
|
data: { hash: this.persistedFile._hash },
|
||||||
|
getMetadata: sinon.stub().returns({}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
this.UpdateTranslator._convertPathname
|
this.UpdateTranslator._convertPathname
|
||||||
|
@ -600,7 +602,7 @@ describe('SyncManager', function () {
|
||||||
expect(this.extendLock).to.have.been.called
|
expect(this.extendLock).to.have.been.called
|
||||||
})
|
})
|
||||||
|
|
||||||
it('queues file additions for missing files', async function () {
|
it('queues file additions for missing regular files', async function () {
|
||||||
const newFile = {
|
const newFile = {
|
||||||
path: '2.png',
|
path: '2.png',
|
||||||
file: {},
|
file: {},
|
||||||
|
@ -625,6 +627,50 @@ describe('SyncManager', function () {
|
||||||
pathname: newFile.path,
|
pathname: newFile.path,
|
||||||
file: newFile.file,
|
file: newFile.file,
|
||||||
url: newFile.url,
|
url: newFile.url,
|
||||||
|
metadata: undefined,
|
||||||
|
meta: {
|
||||||
|
resync: true,
|
||||||
|
ts: TIMESTAMP,
|
||||||
|
origin: { kind: 'history-resync' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(this.extendLock).to.have.been.called
|
||||||
|
})
|
||||||
|
|
||||||
|
it('queues file additions for missing linked files', async function () {
|
||||||
|
const newFile = {
|
||||||
|
path: '2.png',
|
||||||
|
file: {},
|
||||||
|
url: 'filestore/2.png',
|
||||||
|
metadata: {
|
||||||
|
importedAt: '2024-07-30T09:14:45.928Z',
|
||||||
|
provider: 'references-provider',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const updates = [
|
||||||
|
resyncProjectStructureUpdate(
|
||||||
|
[this.persistedDoc],
|
||||||
|
[this.persistedFile, newFile]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
const expandedUpdates =
|
||||||
|
await this.SyncManager.promises.expandSyncUpdates(
|
||||||
|
this.projectId,
|
||||||
|
this.historyId,
|
||||||
|
updates,
|
||||||
|
this.extendLock
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(expandedUpdates).to.deep.equal([
|
||||||
|
{
|
||||||
|
pathname: newFile.path,
|
||||||
|
file: newFile.file,
|
||||||
|
url: newFile.url,
|
||||||
|
metadata: {
|
||||||
|
importedAt: '2024-07-30T09:14:45.928Z',
|
||||||
|
provider: 'references-provider',
|
||||||
|
},
|
||||||
meta: {
|
meta: {
|
||||||
resync: true,
|
resync: true,
|
||||||
ts: TIMESTAMP,
|
ts: TIMESTAMP,
|
||||||
|
@ -704,6 +750,185 @@ describe('SyncManager', function () {
|
||||||
pathname: fileWichWasADoc.path,
|
pathname: fileWichWasADoc.path,
|
||||||
file: fileWichWasADoc.file,
|
file: fileWichWasADoc.file,
|
||||||
url: fileWichWasADoc.url,
|
url: fileWichWasADoc.url,
|
||||||
|
metadata: undefined,
|
||||||
|
meta: {
|
||||||
|
resync: true,
|
||||||
|
ts: TIMESTAMP,
|
||||||
|
origin: { kind: 'history-resync' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(this.extendLock).to.have.been.called
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes and re-adds linked-files if their binary state differs', async function () {
|
||||||
|
const fileWhichWasADoc = {
|
||||||
|
path: this.persistedDoc.path,
|
||||||
|
url: 'filestore/references.txt',
|
||||||
|
_hash: 'other-hash',
|
||||||
|
metadata: {
|
||||||
|
importedAt: '2024-07-30T09:14:45.928Z',
|
||||||
|
provider: 'references-provider',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = [
|
||||||
|
resyncProjectStructureUpdate(
|
||||||
|
[],
|
||||||
|
[fileWhichWasADoc, this.persistedFile]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
const expandedUpdates =
|
||||||
|
await this.SyncManager.promises.expandSyncUpdates(
|
||||||
|
this.projectId,
|
||||||
|
this.historyId,
|
||||||
|
updates,
|
||||||
|
this.extendLock
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(expandedUpdates).to.deep.equal([
|
||||||
|
{
|
||||||
|
pathname: fileWhichWasADoc.path,
|
||||||
|
new_pathname: '',
|
||||||
|
meta: {
|
||||||
|
resync: true,
|
||||||
|
ts: TIMESTAMP,
|
||||||
|
origin: { kind: 'history-resync' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pathname: fileWhichWasADoc.path,
|
||||||
|
file: fileWhichWasADoc.file,
|
||||||
|
url: fileWhichWasADoc.url,
|
||||||
|
metadata: {
|
||||||
|
importedAt: '2024-07-30T09:14:45.928Z',
|
||||||
|
provider: 'references-provider',
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
resync: true,
|
||||||
|
ts: TIMESTAMP,
|
||||||
|
origin: { kind: 'history-resync' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(this.extendLock).to.have.been.called
|
||||||
|
})
|
||||||
|
|
||||||
|
it('add linked file data with same hash', async function () {
|
||||||
|
const nowLinkedFile = {
|
||||||
|
path: this.persistedFile.path,
|
||||||
|
url: 'filestore/1.png',
|
||||||
|
_hash: this.persistedFile._hash,
|
||||||
|
metadata: {
|
||||||
|
importedAt: '2024-07-30T09:14:45.928Z',
|
||||||
|
provider: 'image-provider',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = [
|
||||||
|
resyncProjectStructureUpdate([this.persistedDoc], [nowLinkedFile]),
|
||||||
|
]
|
||||||
|
const expandedUpdates =
|
||||||
|
await this.SyncManager.promises.expandSyncUpdates(
|
||||||
|
this.projectId,
|
||||||
|
this.historyId,
|
||||||
|
updates,
|
||||||
|
this.extendLock
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(expandedUpdates).to.deep.equal([
|
||||||
|
{
|
||||||
|
pathname: nowLinkedFile.path,
|
||||||
|
metadata: {
|
||||||
|
importedAt: '2024-07-30T09:14:45.928Z',
|
||||||
|
provider: 'image-provider',
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
resync: true,
|
||||||
|
ts: TIMESTAMP,
|
||||||
|
origin: { kind: 'history-resync' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(this.extendLock).to.have.been.called
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates linked file data when hash remains the same', async function () {
|
||||||
|
this.fileMap[this.persistedFile.path].getMetadata.returns({
|
||||||
|
importedAt: '2024-07-30T09:14:45.928Z',
|
||||||
|
provider: 'image-provider',
|
||||||
|
})
|
||||||
|
const updatedLinkedFile = {
|
||||||
|
path: this.persistedFile.path,
|
||||||
|
url: 'filestore/1.png',
|
||||||
|
_hash: this.persistedFile._hash,
|
||||||
|
metadata: {
|
||||||
|
importedAt: '2024-07-31T00:00:00.000Z',
|
||||||
|
provider: 'image-provider',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = [
|
||||||
|
resyncProjectStructureUpdate(
|
||||||
|
[this.persistedDoc],
|
||||||
|
[updatedLinkedFile]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
const expandedUpdates =
|
||||||
|
await this.SyncManager.promises.expandSyncUpdates(
|
||||||
|
this.projectId,
|
||||||
|
this.historyId,
|
||||||
|
updates,
|
||||||
|
this.extendLock
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(expandedUpdates).to.deep.equal([
|
||||||
|
{
|
||||||
|
pathname: updatedLinkedFile.path,
|
||||||
|
metadata: {
|
||||||
|
importedAt: '2024-07-31T00:00:00.000Z',
|
||||||
|
provider: 'image-provider',
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
resync: true,
|
||||||
|
ts: TIMESTAMP,
|
||||||
|
origin: { kind: 'history-resync' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(this.extendLock).to.have.been.called
|
||||||
|
})
|
||||||
|
|
||||||
|
it('remove linked file data', async function () {
|
||||||
|
this.fileMap[this.persistedFile.path].getMetadata.returns({
|
||||||
|
importedAt: '2024-07-30T09:14:45.928Z',
|
||||||
|
provider: 'image-provider',
|
||||||
|
})
|
||||||
|
|
||||||
|
const noLongerLinkedFile = {
|
||||||
|
path: this.persistedFile.path,
|
||||||
|
url: 'filestore/1.png',
|
||||||
|
_hash: this.persistedFile._hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = [
|
||||||
|
resyncProjectStructureUpdate(
|
||||||
|
[this.persistedDoc],
|
||||||
|
[noLongerLinkedFile]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
const expandedUpdates =
|
||||||
|
await this.SyncManager.promises.expandSyncUpdates(
|
||||||
|
this.projectId,
|
||||||
|
this.historyId,
|
||||||
|
updates,
|
||||||
|
this.extendLock
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(expandedUpdates).to.deep.equal([
|
||||||
|
{
|
||||||
|
pathname: noLongerLinkedFile.path,
|
||||||
|
metadata: {},
|
||||||
meta: {
|
meta: {
|
||||||
resync: true,
|
resync: true,
|
||||||
ts: TIMESTAMP,
|
ts: TIMESTAMP,
|
||||||
|
@ -801,6 +1026,7 @@ describe('SyncManager', function () {
|
||||||
pathname: persistedFileWithNewContent.path,
|
pathname: persistedFileWithNewContent.path,
|
||||||
file: persistedFileWithNewContent.file,
|
file: persistedFileWithNewContent.file,
|
||||||
url: persistedFileWithNewContent.url,
|
url: persistedFileWithNewContent.url,
|
||||||
|
metadata: undefined,
|
||||||
meta: {
|
meta: {
|
||||||
resync: true,
|
resync: true,
|
||||||
ts: TIMESTAMP,
|
ts: TIMESTAMP,
|
||||||
|
|
|
@ -8,6 +8,7 @@ const metrics = require('@overleaf/metrics')
|
||||||
const { promisify } = require('util')
|
const { promisify } = require('util')
|
||||||
const { promisifyMultiResult } = require('@overleaf/promise-utils')
|
const { promisifyMultiResult } = require('@overleaf/promise-utils')
|
||||||
const ProjectGetter = require('../Project/ProjectGetter')
|
const ProjectGetter = require('../Project/ProjectGetter')
|
||||||
|
const FileStoreHandler = require('../FileStore/FileStoreHandler')
|
||||||
|
|
||||||
function flushProjectToMongo(projectId, callback) {
|
function flushProjectToMongo(projectId, callback) {
|
||||||
_makeRequest(
|
_makeRequest(
|
||||||
|
@ -230,6 +231,18 @@ function resyncProjectHistory(
|
||||||
opts,
|
opts,
|
||||||
callback
|
callback
|
||||||
) {
|
) {
|
||||||
|
docs = docs.map(doc => ({
|
||||||
|
doc: doc.doc._id,
|
||||||
|
path: doc.path,
|
||||||
|
}))
|
||||||
|
files = files.map(file => ({
|
||||||
|
file: file.file._id,
|
||||||
|
path: file.path,
|
||||||
|
url: FileStoreHandler._buildUrl(projectId, file.file._id),
|
||||||
|
_hash: file.file.hash,
|
||||||
|
metadata: buildFileMetadataForHistory(file.file),
|
||||||
|
}))
|
||||||
|
|
||||||
const body = { docs, files, projectHistoryId }
|
const body = { docs, files, projectHistoryId }
|
||||||
if (opts.historyRangesMigration) {
|
if (opts.historyRangesMigration) {
|
||||||
body.historyRangesMigration = opts.historyRangesMigration
|
body.historyRangesMigration = opts.historyRangesMigration
|
||||||
|
@ -470,6 +483,7 @@ function _getUpdates(
|
||||||
historyRangesSupport,
|
historyRangesSupport,
|
||||||
url: newEntity.url,
|
url: newEntity.url,
|
||||||
hash: newEntity.file != null ? newEntity.file.hash : undefined,
|
hash: newEntity.file != null ? newEntity.file.hash : undefined,
|
||||||
|
metadata: buildFileMetadataForHistory(newEntity.file),
|
||||||
})
|
})
|
||||||
} else if (newEntity.path !== oldEntity.path) {
|
} else if (newEntity.path !== oldEntity.path) {
|
||||||
// entity renamed
|
// entity renamed
|
||||||
|
@ -485,6 +499,25 @@ function _getUpdates(
|
||||||
return { deletes, adds, renames }
|
return { deletes, adds, renames }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildFileMetadataForHistory(file) {
|
||||||
|
if (!file?.linkedFileData) return undefined
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
// Files do not have a created at timestamp in the history.
|
||||||
|
// For cloned projects, the importedAt timestamp needs to remain untouched.
|
||||||
|
// Record the timestamp in the metadata blob to keep everything self-contained.
|
||||||
|
importedAt: file.created,
|
||||||
|
...file.linkedFileData,
|
||||||
|
}
|
||||||
|
if (metadata.provider === 'project_output_file') {
|
||||||
|
// The build-id and clsi-server-id are only used for downloading file.
|
||||||
|
// Omit them from history as they are not useful in the future.
|
||||||
|
delete metadata.build_id
|
||||||
|
delete metadata.clsiServerId
|
||||||
|
}
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
flushProjectToMongo,
|
flushProjectToMongo,
|
||||||
flushMultipleProjectsToMongo,
|
flushMultipleProjectsToMongo,
|
||||||
|
|
|
@ -1435,17 +1435,6 @@ const ProjectEntityUpdateHandler = {
|
||||||
if (error) {
|
if (error) {
|
||||||
return callback(error)
|
return callback(error)
|
||||||
}
|
}
|
||||||
docs = _.map(docs, doc => ({
|
|
||||||
doc: doc.doc._id,
|
|
||||||
path: doc.path,
|
|
||||||
}))
|
|
||||||
|
|
||||||
files = _.map(files, file => ({
|
|
||||||
file: file.file._id,
|
|
||||||
path: file.path,
|
|
||||||
url: FileStoreHandler._buildUrl(projectId, file.file._id),
|
|
||||||
_hash: file.file.hash,
|
|
||||||
}))
|
|
||||||
|
|
||||||
DocumentUpdaterHandler.resyncProjectHistory(
|
DocumentUpdaterHandler.resyncProjectHistory(
|
||||||
projectId,
|
projectId,
|
||||||
|
|
|
@ -53,6 +53,11 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
done() {}
|
done() {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'../FileStore/FileStoreHandler': {
|
||||||
|
_buildUrl: sinon.stub().callsFake((projectId, fileId) => {
|
||||||
|
return `http://filestore/project/${projectId}/file/${fileId}`
|
||||||
|
}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.ProjectGetter.getProjectWithoutLock
|
this.ProjectGetter.getProjectWithoutLock
|
||||||
|
@ -1126,6 +1131,7 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
url: undefined,
|
url: undefined,
|
||||||
hash: undefined,
|
hash: undefined,
|
||||||
ranges: undefined,
|
ranges: undefined,
|
||||||
|
metadata: undefined,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1136,20 +1142,18 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
this.changes,
|
this.changes,
|
||||||
this.source,
|
this.source,
|
||||||
() => {
|
() => {
|
||||||
this.request
|
this.request.should.have.been.calledWith({
|
||||||
.calledWith({
|
url: this.url,
|
||||||
url: this.url,
|
method: 'POST',
|
||||||
method: 'POST',
|
json: {
|
||||||
json: {
|
updates,
|
||||||
updates,
|
userId: this.user_id,
|
||||||
userId: this.user_id,
|
version: this.version,
|
||||||
version: this.version,
|
projectHistoryId: this.projectHistoryId,
|
||||||
projectHistoryId: this.projectHistoryId,
|
source: this.source,
|
||||||
source: this.source,
|
},
|
||||||
},
|
timeout: 30 * 1000,
|
||||||
timeout: 30 * 1000,
|
})
|
||||||
})
|
|
||||||
.should.equal(true)
|
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1180,6 +1184,7 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
historyRangesSupport: false,
|
historyRangesSupport: false,
|
||||||
hash: '12345',
|
hash: '12345',
|
||||||
ranges: undefined,
|
ranges: undefined,
|
||||||
|
metadata: undefined,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1190,20 +1195,18 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
this.changes,
|
this.changes,
|
||||||
this.source,
|
this.source,
|
||||||
() => {
|
() => {
|
||||||
this.request
|
this.request.should.have.been.calledWith({
|
||||||
.calledWith({
|
url: this.url,
|
||||||
url: this.url,
|
method: 'POST',
|
||||||
method: 'POST',
|
json: {
|
||||||
json: {
|
updates,
|
||||||
updates,
|
userId: this.user_id,
|
||||||
userId: this.user_id,
|
version: this.version,
|
||||||
version: this.version,
|
projectHistoryId: this.projectHistoryId,
|
||||||
projectHistoryId: this.projectHistoryId,
|
source: this.source,
|
||||||
source: this.source,
|
},
|
||||||
},
|
timeout: 30 * 1000,
|
||||||
timeout: 30 * 1000,
|
})
|
||||||
})
|
|
||||||
.should.equal(true)
|
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1236,20 +1239,18 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
this.changes,
|
this.changes,
|
||||||
this.source,
|
this.source,
|
||||||
() => {
|
() => {
|
||||||
this.request
|
this.request.should.have.been.calledWith({
|
||||||
.calledWith({
|
url: this.url,
|
||||||
url: this.url,
|
method: 'POST',
|
||||||
method: 'POST',
|
json: {
|
||||||
json: {
|
updates,
|
||||||
updates,
|
userId: this.user_id,
|
||||||
userId: this.user_id,
|
version: this.version,
|
||||||
version: this.version,
|
projectHistoryId: this.projectHistoryId,
|
||||||
projectHistoryId: this.projectHistoryId,
|
source: this.source,
|
||||||
source: this.source,
|
},
|
||||||
},
|
timeout: 30 * 1000,
|
||||||
timeout: 30 * 1000,
|
})
|
||||||
})
|
|
||||||
.should.equal(true)
|
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1294,6 +1295,7 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
url: undefined,
|
url: undefined,
|
||||||
hash: undefined,
|
hash: undefined,
|
||||||
ranges: undefined,
|
ranges: undefined,
|
||||||
|
metadata: undefined,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1395,6 +1397,7 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
url: undefined,
|
url: undefined,
|
||||||
hash: undefined,
|
hash: undefined,
|
||||||
ranges: this.ranges,
|
ranges: this.ranges,
|
||||||
|
metadata: undefined,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1405,20 +1408,18 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
this.changes,
|
this.changes,
|
||||||
this.source,
|
this.source,
|
||||||
() => {
|
() => {
|
||||||
this.request
|
this.request.should.have.been.calledWith({
|
||||||
.calledWith({
|
url: this.url,
|
||||||
url: this.url,
|
method: 'POST',
|
||||||
method: 'POST',
|
json: {
|
||||||
json: {
|
updates,
|
||||||
updates,
|
userId: this.user_id,
|
||||||
userId: this.user_id,
|
version: this.version,
|
||||||
version: this.version,
|
projectHistoryId: this.projectHistoryId,
|
||||||
projectHistoryId: this.projectHistoryId,
|
source: this.source,
|
||||||
source: this.source,
|
},
|
||||||
},
|
timeout: 30 * 1000,
|
||||||
timeout: 30 * 1000,
|
})
|
||||||
})
|
|
||||||
.should.equal(true)
|
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1442,6 +1443,7 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
url: undefined,
|
url: undefined,
|
||||||
hash: undefined,
|
hash: undefined,
|
||||||
ranges: this.ranges,
|
ranges: this.ranges,
|
||||||
|
metadata: undefined,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1452,20 +1454,18 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
this.changes,
|
this.changes,
|
||||||
this.source,
|
this.source,
|
||||||
() => {
|
() => {
|
||||||
this.request
|
this.request.should.have.been.calledWith({
|
||||||
.calledWith({
|
url: this.url,
|
||||||
url: this.url,
|
method: 'POST',
|
||||||
method: 'POST',
|
json: {
|
||||||
json: {
|
updates,
|
||||||
updates,
|
userId: this.user_id,
|
||||||
userId: this.user_id,
|
version: this.version,
|
||||||
version: this.version,
|
projectHistoryId: this.projectHistoryId,
|
||||||
projectHistoryId: this.projectHistoryId,
|
source: this.source,
|
||||||
source: this.source,
|
},
|
||||||
},
|
timeout: 30 * 1000,
|
||||||
timeout: 30 * 1000,
|
})
|
||||||
})
|
|
||||||
.should.equal(true)
|
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1473,4 +1473,134 @@ describe('DocumentUpdaterHandler', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('resyncProjectHistory', function () {
|
||||||
|
it('should add docs', function (done) {
|
||||||
|
const docId1 = new ObjectId()
|
||||||
|
const docId2 = new ObjectId()
|
||||||
|
const docs = [
|
||||||
|
{ doc: { _id: docId1 }, path: 'main.tex' },
|
||||||
|
{ doc: { _id: docId2 }, path: 'references.bib' },
|
||||||
|
]
|
||||||
|
const files = []
|
||||||
|
this.request.yields(null, { statusCode: 200 })
|
||||||
|
const projectId = new ObjectId()
|
||||||
|
const projectHistoryId = 99
|
||||||
|
this.handler.resyncProjectHistory(
|
||||||
|
projectId,
|
||||||
|
projectHistoryId,
|
||||||
|
docs,
|
||||||
|
files,
|
||||||
|
{},
|
||||||
|
() => {
|
||||||
|
this.request.should.have.been.calledWith({
|
||||||
|
url: `${this.settings.apis.documentupdater.url}/project/${projectId}/history/resync`,
|
||||||
|
method: 'POST',
|
||||||
|
json: {
|
||||||
|
docs: [
|
||||||
|
{ doc: docId1, path: 'main.tex' },
|
||||||
|
{ doc: docId2, path: 'references.bib' },
|
||||||
|
],
|
||||||
|
files: [],
|
||||||
|
projectHistoryId,
|
||||||
|
},
|
||||||
|
timeout: 6 * 60 * 1000,
|
||||||
|
})
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should add files', function (done) {
|
||||||
|
const fileId1 = new ObjectId()
|
||||||
|
const fileId2 = new ObjectId()
|
||||||
|
const fileId3 = new ObjectId()
|
||||||
|
const fileCreated2 = new Date()
|
||||||
|
const fileCreated3 = new Date()
|
||||||
|
const otherProjectId = new ObjectId().toString()
|
||||||
|
const files = [
|
||||||
|
{ file: { _id: fileId1, hash: '42' }, path: '1.png' },
|
||||||
|
{
|
||||||
|
file: {
|
||||||
|
_id: fileId2,
|
||||||
|
hash: '1337',
|
||||||
|
created: fileCreated2,
|
||||||
|
linkedFileData: {
|
||||||
|
provider: 'references-provider',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
path: '1.bib',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: {
|
||||||
|
_id: fileId3,
|
||||||
|
hash: '21',
|
||||||
|
created: fileCreated3,
|
||||||
|
linkedFileData: {
|
||||||
|
provider: 'project_output_file',
|
||||||
|
build_id: '1234-abc',
|
||||||
|
clsiServerId: 'server-1',
|
||||||
|
source_project_id: otherProjectId,
|
||||||
|
source_output_file_path: 'foo/bar.txt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
path: 'bar.txt',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const docs = []
|
||||||
|
this.request.yields(null, { statusCode: 200 })
|
||||||
|
const projectId = new ObjectId()
|
||||||
|
const projectHistoryId = 99
|
||||||
|
this.handler.resyncProjectHistory(
|
||||||
|
projectId,
|
||||||
|
projectHistoryId,
|
||||||
|
docs,
|
||||||
|
files,
|
||||||
|
{},
|
||||||
|
() => {
|
||||||
|
this.request.should.have.been.calledWith({
|
||||||
|
url: `${this.settings.apis.documentupdater.url}/project/${projectId}/history/resync`,
|
||||||
|
method: 'POST',
|
||||||
|
json: {
|
||||||
|
docs: [],
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
file: fileId1,
|
||||||
|
_hash: '42',
|
||||||
|
path: '1.png',
|
||||||
|
url: `http://filestore/project/${projectId}/file/${fileId1}`,
|
||||||
|
metadata: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: fileId2,
|
||||||
|
_hash: '1337',
|
||||||
|
path: '1.bib',
|
||||||
|
url: `http://filestore/project/${projectId}/file/${fileId2}`,
|
||||||
|
metadata: {
|
||||||
|
importedAt: fileCreated2,
|
||||||
|
provider: 'references-provider',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: fileId3,
|
||||||
|
_hash: '21',
|
||||||
|
path: 'bar.txt',
|
||||||
|
url: `http://filestore/project/${projectId}/file/${fileId3}`,
|
||||||
|
metadata: {
|
||||||
|
importedAt: fileCreated3,
|
||||||
|
provider: 'project_output_file',
|
||||||
|
source_project_id: otherProjectId,
|
||||||
|
source_output_file_path: 'foo/bar.txt',
|
||||||
|
// build_id and clsiServerId are omitted
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
projectHistoryId,
|
||||||
|
},
|
||||||
|
timeout: 6 * 60 * 1000,
|
||||||
|
})
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -2012,17 +2012,15 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('a project with project-history enabled', function () {
|
describe('a project with project-history enabled', function () {
|
||||||
|
const docs = [{ doc: { _id: docId, name: 'main.tex' }, path: 'main.tex' }]
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
file: { _id: fileId, name: 'universe.png', hash: '123456' },
|
||||||
|
path: 'universe.png',
|
||||||
|
},
|
||||||
|
]
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.ProjectGetter.getProject.yields(null, this.project)
|
this.ProjectGetter.getProject.yields(null, this.project)
|
||||||
const docs = [
|
|
||||||
{ doc: { _id: docId, name: 'main.tex' }, path: 'main.tex' },
|
|
||||||
]
|
|
||||||
const files = [
|
|
||||||
{
|
|
||||||
file: { _id: fileId, name: 'universe.png', hash: '123456' },
|
|
||||||
path: 'universe.png',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const folders = []
|
const folders = []
|
||||||
this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
|
this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
|
||||||
docs,
|
docs,
|
||||||
|
@ -2051,20 +2049,6 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tells the doc updater to sync the project', function () {
|
it('tells the doc updater to sync the project', function () {
|
||||||
const docs = [
|
|
||||||
{
|
|
||||||
doc: docId,
|
|
||||||
path: 'main.tex',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const files = [
|
|
||||||
{
|
|
||||||
file: fileId,
|
|
||||||
path: 'universe.png',
|
|
||||||
url: `www.filestore.test/${projectId}/${fileId}`,
|
|
||||||
_hash: '123456',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.DocumentUpdaterHandler.resyncProjectHistory
|
this.DocumentUpdaterHandler.resyncProjectHistory
|
||||||
.calledWith(projectId, projectHistoryId, docs, files)
|
.calledWith(projectId, projectHistoryId, docs, files)
|
||||||
.should.equal(true)
|
.should.equal(true)
|
||||||
|
@ -2157,40 +2141,24 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tells the doc updater to resync the project', function () {
|
it('tells the doc updater to resync the project', function () {
|
||||||
const docs = [
|
const docs = this.docs.map(d => {
|
||||||
{ doc: 'doc1', path: 'main.tex' },
|
if (d.doc._id === 'doc3') {
|
||||||
{ doc: 'doc2', path: 'a/b/c/duplicate.tex' },
|
return Object.assign({}, d, { path: 'a/b/c/duplicate.tex (1)' })
|
||||||
{ doc: 'doc3', path: 'a/b/c/duplicate.tex (1)' },
|
}
|
||||||
{ doc: 'doc4', path: 'another dupe (22)' },
|
if (d.doc._id === 'doc5') {
|
||||||
{ doc: 'doc5', path: 'a/b/c/duplicate.tex (2)' },
|
return Object.assign({}, d, { path: 'a/b/c/duplicate.tex (2)' })
|
||||||
]
|
}
|
||||||
const urlPrefix = `www.filestore.test/${projectId}`
|
return d
|
||||||
const files = [
|
})
|
||||||
{
|
const files = this.files.map(f => {
|
||||||
file: 'file1',
|
if (f.file._id === 'file3') {
|
||||||
path: 'image.jpg',
|
return Object.assign({}, f, { path: 'duplicate.jpg (1)' })
|
||||||
url: `${urlPrefix}/file1`,
|
}
|
||||||
_hash: 'hash1',
|
if (f.file._id === 'file4') {
|
||||||
},
|
return Object.assign({}, f, { path: 'another dupe (23)' })
|
||||||
{
|
}
|
||||||
file: 'file2',
|
return f
|
||||||
path: 'duplicate.jpg',
|
})
|
||||||
url: `${urlPrefix}/file2`,
|
|
||||||
_hash: 'hash2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'file3',
|
|
||||||
path: 'duplicate.jpg (1)',
|
|
||||||
url: `${urlPrefix}/file3`,
|
|
||||||
_hash: 'hash3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'file4',
|
|
||||||
path: 'another dupe (23)',
|
|
||||||
url: `${urlPrefix}/file4`,
|
|
||||||
_hash: 'hash4',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
expect(
|
expect(
|
||||||
this.DocumentUpdaterHandler.resyncProjectHistory
|
this.DocumentUpdaterHandler.resyncProjectHistory
|
||||||
).to.have.been.calledWith(projectId, projectHistoryId, docs, files)
|
).to.have.been.calledWith(projectId, projectHistoryId, docs, files)
|
||||||
|
@ -2262,25 +2230,24 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tells the doc updater to resync the project', function () {
|
it('tells the doc updater to resync the project', function () {
|
||||||
const docs = [
|
const docs = this.docs.map(d => {
|
||||||
{ doc: 'doc1', path: 'a/b/c/_d_e_f_test.tex' },
|
if (d.doc._id === 'doc1') {
|
||||||
{ doc: 'doc2', path: 'a/untitled' },
|
return Object.assign({}, d, { path: 'a/b/c/_d_e_f_test.tex' })
|
||||||
]
|
}
|
||||||
const urlPrefix = `www.filestore.test/${projectId}`
|
if (d.doc._id === 'doc2') {
|
||||||
const files = [
|
return Object.assign({}, d, { path: 'a/untitled' })
|
||||||
{
|
}
|
||||||
file: 'file1',
|
return d
|
||||||
path: 'A_.png',
|
})
|
||||||
url: `${urlPrefix}/file1`,
|
const files = this.files.map(f => {
|
||||||
_hash: 'hash1',
|
if (f.file._id === 'file1') {
|
||||||
},
|
return Object.assign({}, f, { path: 'A_.png' })
|
||||||
{
|
}
|
||||||
file: 'file2',
|
if (f.file._id === 'file2') {
|
||||||
path: 'A_.png (1)',
|
return Object.assign({}, f, { path: 'A_.png (1)' })
|
||||||
url: `${urlPrefix}/file2`,
|
}
|
||||||
_hash: 'hash2',
|
return f
|
||||||
},
|
})
|
||||||
]
|
|
||||||
expect(
|
expect(
|
||||||
this.DocumentUpdaterHandler.resyncProjectHistory
|
this.DocumentUpdaterHandler.resyncProjectHistory
|
||||||
).to.have.been.calledWith(projectId, projectHistoryId, docs, files)
|
).to.have.been.calledWith(projectId, projectHistoryId, docs, files)
|
||||||
|
@ -2288,29 +2255,29 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('a project with a bad folder name', function () {
|
describe('a project with a bad folder name', function () {
|
||||||
|
const folders = [
|
||||||
|
{
|
||||||
|
folder: { _id: 'folder1', name: 'good' },
|
||||||
|
path: 'good',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
folder: { _id: 'folder2', name: 'bad*' },
|
||||||
|
path: 'bad*',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const docs = [
|
||||||
|
{
|
||||||
|
doc: { _id: 'doc1', name: 'doc1.tex' },
|
||||||
|
path: 'good/doc1.tex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: { _id: 'doc2', name: 'duplicate.tex' },
|
||||||
|
path: 'bad*/doc2.tex',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const files = []
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
this.ProjectGetter.getProject.yields(null, this.project)
|
this.ProjectGetter.getProject.yields(null, this.project)
|
||||||
const folders = [
|
|
||||||
{
|
|
||||||
folder: { _id: 'folder1', name: 'good' },
|
|
||||||
path: 'good',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
folder: { _id: 'folder2', name: 'bad*' },
|
|
||||||
path: 'bad*',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const docs = [
|
|
||||||
{
|
|
||||||
doc: { _id: 'doc1', name: 'doc1.tex' },
|
|
||||||
path: 'good/doc1.tex',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: { _id: 'doc2', name: 'duplicate.tex' },
|
|
||||||
path: 'bad*/doc2.tex',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const files = []
|
|
||||||
this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
|
this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
|
||||||
docs,
|
docs,
|
||||||
files,
|
files,
|
||||||
|
@ -2335,37 +2302,38 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tells the doc updater to resync the project', function () {
|
it('tells the doc updater to resync the project', function () {
|
||||||
const docs = [
|
const fixedDocs = docs.map(d => {
|
||||||
{ doc: 'doc1', path: 'good/doc1.tex' },
|
if (d.doc._id === 'doc2') {
|
||||||
{ doc: 'doc2', path: 'bad_/doc2.tex' },
|
return Object.assign({}, d, { path: 'bad_/doc2.tex' })
|
||||||
]
|
}
|
||||||
const files = []
|
return d
|
||||||
|
})
|
||||||
expect(
|
expect(
|
||||||
this.DocumentUpdaterHandler.resyncProjectHistory
|
this.DocumentUpdaterHandler.resyncProjectHistory
|
||||||
).to.have.been.calledWith(projectId, projectHistoryId, docs, files)
|
).to.have.been.calledWith(projectId, projectHistoryId, fixedDocs, files)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('a project with duplicate names between a folder and a doc', function () {
|
describe('a project with duplicate names between a folder and a doc', function () {
|
||||||
|
const folders = [
|
||||||
|
{
|
||||||
|
folder: { _id: 'folder1', name: 'chapters' },
|
||||||
|
path: 'chapters',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const docs = [
|
||||||
|
{
|
||||||
|
doc: { _id: 'doc1', name: 'chapters' },
|
||||||
|
path: 'chapters',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: { _id: 'doc2', name: 'chapter1.tex' },
|
||||||
|
path: 'chapters/chapter1.tex',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const files = []
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
this.ProjectGetter.getProject.yields(null, this.project)
|
this.ProjectGetter.getProject.yields(null, this.project)
|
||||||
const folders = [
|
|
||||||
{
|
|
||||||
folder: { _id: 'folder1', name: 'chapters' },
|
|
||||||
path: 'chapters',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const docs = [
|
|
||||||
{
|
|
||||||
doc: { _id: 'doc1', name: 'chapters' },
|
|
||||||
path: 'chapters',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc: { _id: 'doc2', name: 'chapter1.tex' },
|
|
||||||
path: 'chapters/chapter1.tex',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const files = []
|
|
||||||
this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
|
this.ProjectEntityHandler.getAllEntitiesFromProject.returns({
|
||||||
docs,
|
docs,
|
||||||
files,
|
files,
|
||||||
|
@ -2390,14 +2358,15 @@ describe('ProjectEntityUpdateHandler', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tells the doc updater to resync the project', function () {
|
it('tells the doc updater to resync the project', function () {
|
||||||
const docs = [
|
const fixedDocs = docs.map(d => {
|
||||||
{ doc: 'doc1', path: 'chapters (1)' },
|
if (d.doc._id === 'doc1') {
|
||||||
{ doc: 'doc2', path: 'chapters/chapter1.tex' },
|
return Object.assign({}, d, { path: 'chapters (1)' })
|
||||||
]
|
}
|
||||||
const files = []
|
return d
|
||||||
|
})
|
||||||
expect(
|
expect(
|
||||||
this.DocumentUpdaterHandler.resyncProjectHistory
|
this.DocumentUpdaterHandler.resyncProjectHistory
|
||||||
).to.have.been.calledWith(projectId, projectHistoryId, docs, files)
|
).to.have.been.calledWith(projectId, projectHistoryId, fixedDocs, files)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue