code: /* eslint-disable
      camelcase,
      handle-callback-err,
      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
   */
  let UrlCache
  const db = require('./db')
  const dbQueue = require('./DbQueue')
  const UrlFetcher = require('./UrlFetcher')
  const Settings = require('settings-sharelatex')
  const crypto = require('crypto')
  const fs = require('fs')
  const logger = require('logger-sharelatex')
  const async = require('async')
  
  module.exports = UrlCache = {
    downloadUrlToFile(project_id, url, destPath, lastModified, callback) {
      if (callback == null) {
        callback = function (error) {}
      }
      return UrlCache._ensureUrlIsInCache(
        project_id,
        url,
        lastModified,
        (error, pathToCachedUrl) => {
          if (error != null) {
            return callback(error)
          }
          return UrlCache._copyFile(pathToCachedUrl, destPath, function (error) {
            if (error != null) {
              return UrlCache._clearUrlDetails(project_id, url, () =>
                callback(error)
              )
            } else {
              return callback(error)
            }
          })
        }
      )
    },
  
    clearProject(project_id, callback) {
      if (callback == null) {
        callback = function (error) {}
      }
      return UrlCache._findAllUrlsInProject(project_id, function (error, urls) {
        logger.log(
          { project_id, url_count: urls.length },
          'clearing project URLs'
        )
        if (error != null) {
          return callback(error)
        }
        const jobs = Array.from(urls || []).map((url) =>
          ((url) => (callback) =>
            UrlCache._clearUrlFromCache(project_id, url, function (error) {
              if (error != null) {
                logger.error(
                  { err: error, project_id, url },
                  'error clearing project URL'
                )
              }
              return callback()
            }))(url)
        )
        return async.series(jobs, callback)
      })
    },
  
    _ensureUrlIsInCache(project_id, url, lastModified, callback) {
      if (callback == null) {
        callback = function (error, pathOnDisk) {}
      }
      if (lastModified != null) {
        // MYSQL only stores dates to an accuracy of a second but the incoming lastModified might have milliseconds.
        // So round down to seconds
        lastModified = new Date(Math.floor(lastModified.getTime() / 1000) * 1000)
      }
      return UrlCache._doesUrlNeedDownloading(
        project_id,
        url,
        lastModified,
        (error, needsDownloading) => {
          if (error != null) {
            return callback(error)
          }
          if (needsDownloading) {
            logger.log({ url, lastModified }, 'downloading URL')
            return UrlFetcher.pipeUrlToFileWithRetry(
              url,
              UrlCache._cacheFilePathForUrl(project_id, url),
              (error) => {
                if (error != null) {
                  return callback(error)
                }
                return UrlCache._updateOrCreateUrlDetails(
                  project_id,
                  url,
                  lastModified,
                  (error) => {
                    if (error != null) {
                      return callback(error)
                    }
                    return callback(
                      null,
                      UrlCache._cacheFilePathForUrl(project_id, url)
                    )
                  }
                )
              }
            )
          } else {
            logger.log({ url, lastModified }, 'URL is up to date in cache')
            return callback(null, UrlCache._cacheFilePathForUrl(project_id, url))
          }
        }
      )
    },
  
    _doesUrlNeedDownloading(project_id, url, lastModified, callback) {
      if (callback == null) {
        callback = function (error, needsDownloading) {}
      }
      if (lastModified == null) {
        return callback(null, true)
      }
      return UrlCache._findUrlDetails(project_id, url, function (
        error,
        urlDetails
      ) {
        if (error != null) {
          return callback(error)
        }
        if (
          urlDetails == null ||
          urlDetails.lastModified == null ||
          urlDetails.lastModified.getTime() < lastModified.getTime()
        ) {
          return callback(null, true)
        } else {
          return callback(null, false)
        }
      })
    },
  
    _cacheFileNameForUrl(project_id, url) {
      return project_id + ':' + crypto.createHash('md5').update(url).digest('hex')
    },
  
    _cacheFilePathForUrl(project_id, url) {
      return `${Settings.path.clsiCacheDir}/${UrlCache._cacheFileNameForUrl(
        project_id,
        url
      )}`
    },
  
    _copyFile(from, to, _callback) {
      if (_callback == null) {
        _callback = function (error) {}
      }
      const callbackOnce = function (error) {
        if (error != null) {
          logger.error({ err: error, from, to }, 'error copying file from cache')
        }
        _callback(error)
        return (_callback = function () {})
      }
      const writeStream = fs.createWriteStream(to)
      const readStream = fs.createReadStream(from)
      writeStream.on('error', callbackOnce)
      readStream.on('error', callbackOnce)
      writeStream.on('close', callbackOnce)
      return writeStream.on('open', () => readStream.pipe(writeStream))
    },
  
    _clearUrlFromCache(project_id, url, callback) {
      if (callback == null) {
        callback = function (error) {}
      }
      return UrlCache._clearUrlDetails(project_id, url, function (error) {
        if (error != null) {
          return callback(error)
        }
        return UrlCache._deleteUrlCacheFromDisk(project_id, url, function (
          error
        ) {
          if (error != null) {
            return callback(error)
          }
          return callback(null)
        })
      })
    },
  
    _deleteUrlCacheFromDisk(project_id, url, callback) {
      if (callback == null) {
        callback = function (error) {}
      }
      return fs.unlink(UrlCache._cacheFilePathForUrl(project_id, url), function (
        error
      ) {
        if (error != null && error.code !== 'ENOENT') {
          // no error if the file isn't present
          return callback(error)
        } else {
          return callback()
        }
      })
    },
  
    _findUrlDetails(project_id, url, callback) {
      if (callback == null) {
        callback = function (error, urlDetails) {}
      }
      const job = (cb) =>
        db.UrlCache.findOne({ where: { url, project_id } })
          .then((urlDetails) => cb(null, urlDetails))
          .error(cb)
      return dbQueue.queue.push(job, callback)
    },
  
    _updateOrCreateUrlDetails(project_id, url, lastModified, callback) {
      if (callback == null) {
        callback = function (error) {}
      }
      const job = (cb) =>
        db.UrlCache.findOrCreate({ where: { url, project_id } })
          .spread((urlDetails, created) =>
            urlDetails
              .update({ lastModified })
              .then(() => cb())
              .error(cb)
          )
          .error(cb)
      return dbQueue.queue.push(job, callback)
    },
  
    _clearUrlDetails(project_id, url, callback) {
      if (callback == null) {
        callback = function (error) {}
      }
      const job = (cb) =>
        db.UrlCache.destroy({ where: { url, project_id } })
          .then(() => cb(null))
          .error(cb)
      return dbQueue.queue.push(job, callback)
    },
  
    _findAllUrlsInProject(project_id, callback) {
      if (callback == null) {
        callback = function (error, urls) {}
      }
      const job = (cb) =>
        db.UrlCache.findAll({ where: { project_id } })
          .then((urlEntries) =>
            cb(
              null,
              urlEntries.map((entry) => entry.url)
            )
          )
          .error(cb)
      return dbQueue.queue.push(job, callback)
    }
  }
  