Files
overleaf-cep/services/clsi/app/js/UrlCache.js
Andrew Rumble cd7da983d1 Merge pull request #30232 from overleaf/ar/convert-clsi-to-es-modules
[clsi] convert to ES modules

GitOrigin-RevId: fb7fa52cc8f678ee31be352e62a5dff95e88008b
2026-01-22 09:06:23 +00:00

134 lines
3.3 KiB
JavaScript

/* eslint-disable
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import UrlFetcher from './UrlFetcher.js'
import Settings from '@overleaf/settings'
import fs from 'node:fs'
import Path from 'node:path'
import { callbackify } from 'node:util'
import Metrics from '@overleaf/metrics'
const PENDING_DOWNLOADS = new Map()
function getProjectDir(projectId) {
return Path.join(Settings.path.clsiCacheDir, projectId)
}
function getCachePath(projectId, url, lastModified) {
// The url is a filestore URL.
// It is sufficient to look at the path and mtime for uniqueness.
const mtime = (lastModified && lastModified.getTime()) || 0
const key = new URL(url).pathname.replace(/\//g, '-') + '-' + mtime
return Path.join(getProjectDir(projectId), key)
}
async function clearProject(projectId, options) {
const timer = new Metrics.Timer('url_cache', {
status: options?.reason || 'unknown',
path: 'delete',
})
await fs.promises.rm(getProjectDir(projectId), {
force: true,
recursive: true,
})
timer.done()
}
async function createProjectDir(projectId) {
await fs.promises.mkdir(getProjectDir(projectId), { recursive: true })
}
async function downloadUrlToFile(
projectId,
url,
fallbackURL,
destPath,
lastModified
) {
const cachePath = getCachePath(projectId, url, lastModified)
try {
const timer = new Metrics.Timer('url_cache', {
status: 'cache-hit',
path: 'copy',
})
try {
await fs.promises.copyFile(cachePath, destPath)
} catch (err) {
if (err.code === 'ENOENT' && fallbackURL) {
const fallbackPath = getCachePath(projectId, fallbackURL, lastModified)
await fs.promises.copyFile(fallbackPath, destPath)
} else {
throw err
}
}
// the metric is only updated if the file is present in the cache
timer.done()
return
} catch (e) {
if (e.code !== 'ENOENT') {
throw e
}
}
// time the download
{
const timer = new Metrics.Timer('url_cache', {
status: 'cache-miss',
path: 'download',
})
try {
await download(url, fallbackURL, cachePath)
} finally {
timer.done()
}
}
// time the file copy
{
const timer = new Metrics.Timer('url_cache', {
status: 'cache-miss',
path: 'copy',
})
await fs.promises.copyFile(cachePath, destPath)
timer.done()
}
}
async function download(url, fallbackURL, cachePath) {
let pending = PENDING_DOWNLOADS.get(cachePath)
if (pending) {
return pending
}
pending = UrlFetcher.promises.pipeUrlToFileWithRetry(
url,
fallbackURL,
cachePath
)
PENDING_DOWNLOADS.set(cachePath, pending)
try {
await pending
} finally {
PENDING_DOWNLOADS.delete(cachePath)
}
}
export default {
clearProject: callbackify(clearProject),
createProjectDir: callbackify(createProjectDir),
downloadUrlToFile: callbackify(downloadUrlToFile),
promises: {
clearProject,
createProjectDir,
downloadUrlToFile,
},
}