Merge pull request #24775 from overleaf/em-bypass-project-history

Call history-v1 directly for latest history and changes

GitOrigin-RevId: 39c32dd50ff7875f82bbb2716da753a9c3e6e81d
This commit is contained in:
Eric Mc Sween
2025-04-10 08:52:41 -04:00
committed by Copybot
parent 42aea53307
commit dd526693f5
3 changed files with 104 additions and 47 deletions

View File

@@ -457,6 +457,19 @@ async function _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res) {
}
}
async function getLatestHistory(req, res, next) {
const projectId = req.params.project_id
const history = await HistoryManager.promises.getLatestHistory(projectId)
res.json(history)
}
async function getChanges(req, res, next) {
const projectId = req.params.project_id
const since = req.query.since
const changes = await HistoryManager.promises.getChanges(projectId, { since })
res.json(changes)
}
function isPrematureClose(err) {
return (
err instanceof Error &&
@@ -480,6 +493,8 @@ module.exports = {
createLabel: expressify(createLabel),
deleteLabel: expressify(deleteLabel),
downloadZipOfVersion: expressify(downloadZipOfVersion),
getLatestHistory: expressify(getLatestHistory),
getChanges: expressify(getChanges),
_displayNameForUser,
promises: {
_pipeHistoryZipToResponse,

View File

@@ -21,6 +21,12 @@ const projectKey = require('./project_key')
const GLOBAL_BLOBS = new Set() // CHANGE FROM SOURCE: only store hashes.
const HISTORY_V1_URL = settings.apis.v1_history.url
const HISTORY_V1_BASIC_AUTH = {
user: settings.apis.v1_history.user,
password: settings.apis.v1_history.pass,
}
function makeGlobalKey(hash) {
return `${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash.slice(4)}`
}
@@ -144,16 +150,10 @@ async function _deleteProjectInProjectHistory(projectId) {
async function _deleteProjectInFullProjectHistory(historyId) {
try {
await fetchNothing(
`${settings.apis.v1_history.url}/projects/${historyId}`,
{
method: 'DELETE',
basicAuth: {
user: settings.apis.v1_history.user,
password: settings.apis.v1_history.pass,
},
}
)
await fetchNothing(`${HISTORY_V1_URL}/projects/${historyId}`, {
method: 'DELETE',
basicAuth: HISTORY_V1_BASIC_AUTH,
})
} catch (err) {
throw OError.tag(err, 'failed to clear project history', { historyId })
}
@@ -162,29 +162,23 @@ async function _deleteProjectInFullProjectHistory(historyId) {
async function uploadBlobFromDisk(historyId, hash, byteLength, fsPath) {
const outStream = fs.createReadStream(fsPath)
const url = `${settings.apis.v1_history.url}/projects/${historyId}/blobs/${hash}`
const url = `${HISTORY_V1_URL}/projects/${historyId}/blobs/${hash}`
await fetchNothing(url, {
method: 'PUT',
body: outStream,
headers: { 'Content-Length': byteLength }, // add the content length to work around problems with chunked encoding in node 18
signal: AbortSignal.timeout(60 * 1000),
basicAuth: {
user: settings.apis.v1_history.user,
password: settings.apis.v1_history.pass,
},
basicAuth: HISTORY_V1_BASIC_AUTH,
})
}
async function copyBlob(sourceHistoryId, targetHistoryId, hash) {
const url = `${settings.apis.v1_history.url}/projects/${targetHistoryId}/blobs/${hash}`
const url = `${HISTORY_V1_URL}/projects/${targetHistoryId}/blobs/${hash}`
await fetchNothing(
`${url}?${new URLSearchParams({ copyFrom: sourceHistoryId })}`,
{
method: 'POST',
basicAuth: {
user: settings.apis.v1_history.user,
password: settings.apis.v1_history.pass,
},
basicAuth: HISTORY_V1_BASIC_AUTH,
}
)
}
@@ -200,7 +194,7 @@ async function requestBlobWithFallback(
'overleaf.history.id': true,
})
// Talk to history-v1 directly to avoid streaming via project-history.
let url = new URL(settings.apis.v1_history.url)
let url = new URL(HISTORY_V1_URL)
url.pathname += `/projects/${project.overleaf.history.id}/blobs/${hash}`
const opts = { method, headers: { Range: range } }
@@ -255,22 +249,14 @@ async function requestBlobWithFallback(
* @returns Promise<object>
*/
async function getCurrentContent(projectId) {
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 })
}
const historyId = await getHistoryId(projectId)
try {
return await fetchJson(
`${settings.apis.v1_history.url}/projects/${historyId}/latest/content`,
`${HISTORY_V1_URL}/projects/${historyId}/latest/content`,
{
method: 'GET',
basicAuth: {
user: settings.apis.v1_history.user,
password: settings.apis.v1_history.pass,
},
basicAuth: HISTORY_V1_BASIC_AUTH,
}
)
} catch (err) {
@@ -287,22 +273,14 @@ async function getCurrentContent(projectId) {
* @returns Promise<object>
*/
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 })
}
const historyId = await getHistoryId(projectId)
try {
return await fetchJson(
`${settings.apis.v1_history.url}/projects/${historyId}/versions/${version}/content`,
`${HISTORY_V1_URL}/projects/${historyId}/versions/${version}/content`,
{
method: 'GET',
basicAuth: {
user: settings.apis.v1_history.user,
password: settings.apis.v1_history.pass,
},
basicAuth: HISTORY_V1_BASIC_AUTH,
}
)
} catch (err) {
@@ -314,6 +292,53 @@ async function getContentAtVersion(projectId, version) {
}
}
/**
* Get the latest chunk from history
*
* @param {string} projectId
*/
async function getLatestHistory(projectId) {
const historyId = await getHistoryId(projectId)
return await fetchJson(
`${HISTORY_V1_URL}/projects/${historyId}/latest/history`,
{
basicAuth: HISTORY_V1_BASIC_AUTH,
}
)
}
/**
* Get history changes since a given version
*
* @param {string} projectId
* @param {object} opts
* @param {number} opts.since - The start version of changes to get
*/
async function getChanges(projectId, opts = {}) {
const historyId = await getHistoryId(projectId)
const url = new URL(`${HISTORY_V1_URL}/projects/${historyId}/changes`)
if (opts.since) {
url.searchParams.set('since', opts.since)
}
return await fetchJson(url, {
basicAuth: HISTORY_V1_BASIC_AUTH,
})
}
async function getHistoryId(projectId) {
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 })
}
return historyId
}
async function injectUserDetails(data) {
// data can be either:
// {
@@ -404,6 +429,8 @@ module.exports = {
uploadBlobFromDisk: callbackify(uploadBlobFromDisk),
copyBlob: callbackify(copyBlob),
requestBlobWithFallback: callbackify(requestBlobWithFallback),
getLatestHistory: callbackify(getLatestHistory),
getChanges: callbackify(getChanges),
promises: {
loadGlobalBlobs,
initializeProject,
@@ -417,5 +444,7 @@ module.exports = {
uploadBlobFromDisk,
copyBlob,
requestBlobWithFallback,
getLatestHistory,
getChanges,
},
}

View File

@@ -151,15 +151,28 @@ function apply(webRouter, privateApiRouter) {
webRouter.get(
'/project/:project_id/latest/history',
validate({
params: Joi.object({
project_id: Joi.objectId().required(),
}),
}),
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.proxyToHistoryApi
HistoryController.getLatestHistory
)
webRouter.get(
'/project/:project_id/changes',
validate({
params: Joi.object({
project_id: Joi.objectId().required(),
}),
query: Joi.object({
since: Joi.number().integer().min(0).optional(),
}),
}),
AuthorizationMiddleware.blockRestrictedUserFromProject,
AuthorizationMiddleware.ensureUserCanReadProject,
HistoryController.proxyToHistoryApi
HistoryController.getChanges
)
}