diff --git a/services/history-v1/api/controllers/projects.js b/services/history-v1/api/controllers/projects.js index bf621aab72..3e00035d39 100644 --- a/services/history-v1/api/controllers/projects.js +++ b/services/history-v1/api/controllers/projects.js @@ -50,6 +50,15 @@ async function getLatestContent(req, res, next) { res.json(snapshot.toRaw()) } +async function getContentAtVersion(req, res, next) { + const projectId = req.swagger.params.project_id.value + const version = req.swagger.params.version.value + const blobStore = new BlobStore(projectId) + const snapshot = await getSnapshotAtVersion(projectId, version) + await snapshot.loadFiles('eager', blobStore) + res.json(snapshot.toRaw()) +} + async function getLatestHashedContent(req, res, next) { const projectId = req.swagger.params.project_id.value const blobStore = new HashCheckBlobStore(new BlobStore(projectId)) @@ -227,6 +236,7 @@ async function getSnapshotAtVersion(projectId, version) { module.exports = { initializeProject: expressify(initializeProject), getLatestContent: expressify(getLatestContent), + getContentAtVersion: expressify(getContentAtVersion), getLatestHashedContent: expressify(getLatestHashedContent), getLatestPersistedHistory: expressify(getLatestHistory), getLatestHistory: expressify(getLatestHistory), diff --git a/services/history-v1/api/swagger/projects.js b/services/history-v1/api/swagger/projects.js index 964b32cefe..f4b8ed7352 100644 --- a/services/history-v1/api/swagger/projects.js +++ b/services/history-v1/api/swagger/projects.js @@ -309,6 +309,44 @@ exports.paths = { }, }, }, + '/projects/{project_id}/versions/{version}/content': { + get: { + 'x-swagger-router-controller': 'projects', + operationId: 'getContentAtVersion', + tags: ['Project'], + description: 'Get full content at the given version', + parameters: [ + { + name: 'project_id', + in: 'path', + description: 'project id', + required: true, + type: 'string', + }, + { + name: 'version', + in: 'path', + description: 'numeric version', + required: true, + type: 'number', + }, + ], + responses: { + 200: { + description: 'Success', + schema: { + $ref: '#/definitions/Snapshot', + }, + }, + 404: { + description: 'Not Found', + schema: { + $ref: '#/definitions/Error', + }, + }, + }, + }, + }, '/projects/{project_id}/timestamp/{timestamp}/history': { get: { 'x-swagger-router-controller': 'projects', diff --git a/services/web/app/src/Features/History/HistoryManager.js b/services/web/app/src/Features/History/HistoryManager.js index b5bf7fb1c8..a0ad348b9d 100644 --- a/services/web/app/src/Features/History/HistoryManager.js +++ b/services/web/app/src/Features/History/HistoryManager.js @@ -141,6 +141,42 @@ async function getCurrentContent(projectId) { } } +/** + * Warning: Don't use this method for large projects. It will eagerly load all + * the history data and apply all operations. + * @param {string} projectId + * @param {number} version + * + * @returns Promise + */ +async function getContentAtVersion(projectId, version) { + const project = await ProjectGetter.promises.getProject(projectId, { + overleaf: true, + }) + const historyId = project?.overleaf?.history?.id + if (!historyId) { + throw new OError('project does not have a history id', { projectId }) + } + try { + return await fetchJson( + `${settings.apis.v1_history.url}/projects/${historyId}/versions/${version}/content`, + { + method: 'GET', + basicAuth: { + user: settings.apis.v1_history.user, + password: settings.apis.v1_history.pass, + }, + } + ) + } catch (err) { + throw OError.tag( + err, + 'failed to load project history snapshot at version', + { historyId, version } + ) + } +} + async function injectUserDetails(data) { // data can be either: // { @@ -235,5 +271,6 @@ module.exports = { injectUserDetails, deleteProjectHistory, getCurrentContent, + getContentAtVersion, }, }