ProjectManager.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-unused-vars,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS207: Consider shorter variations of null checks
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. let ProjectManager
  15. const RedisManager = require('./RedisManager')
  16. const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
  17. const DocumentManager = require('./DocumentManager')
  18. const HistoryManager = require('./HistoryManager')
  19. const async = require('async')
  20. const logger = require('logger-sharelatex')
  21. const Metrics = require('./Metrics')
  22. const Errors = require('./Errors')
  23. module.exports = ProjectManager = {
  24. flushProjectWithLocks(project_id, _callback) {
  25. if (_callback == null) {
  26. _callback = function (error) {}
  27. }
  28. const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
  29. const callback = function (...args) {
  30. timer.done()
  31. return _callback(...args)
  32. }
  33. return RedisManager.getDocIdsInProject(project_id, function (
  34. error,
  35. doc_ids
  36. ) {
  37. if (error != null) {
  38. return callback(error)
  39. }
  40. const jobs = []
  41. const errors = []
  42. for (const doc_id of doc_ids) {
  43. ;((doc_id) =>
  44. jobs.push((callback) =>
  45. DocumentManager.flushDocIfLoadedWithLock(
  46. project_id,
  47. doc_id,
  48. function (error) {
  49. if (error != null && error instanceof Errors.NotFoundError) {
  50. logger.warn(
  51. { err: error, project_id, doc_id },
  52. 'found deleted doc when flushing'
  53. )
  54. return callback()
  55. } else if (error != null) {
  56. logger.error(
  57. { err: error, project_id, doc_id },
  58. 'error flushing doc'
  59. )
  60. errors.push(error)
  61. return callback()
  62. } else {
  63. return callback()
  64. }
  65. }
  66. )
  67. ))(doc_id)
  68. }
  69. logger.log({ project_id, doc_ids }, 'flushing docs')
  70. return async.series(jobs, function () {
  71. if (errors.length > 0) {
  72. return callback(
  73. new Error('Errors flushing docs. See log for details')
  74. )
  75. } else {
  76. return callback(null)
  77. }
  78. })
  79. })
  80. },
  81. flushAndDeleteProjectWithLocks(project_id, options, _callback) {
  82. if (_callback == null) {
  83. _callback = function (error) {}
  84. }
  85. const timer = new Metrics.Timer(
  86. 'projectManager.flushAndDeleteProjectWithLocks'
  87. )
  88. const callback = function (...args) {
  89. timer.done()
  90. return _callback(...args)
  91. }
  92. return RedisManager.getDocIdsInProject(project_id, function (
  93. error,
  94. doc_ids
  95. ) {
  96. if (error != null) {
  97. return callback(error)
  98. }
  99. const jobs = []
  100. const errors = []
  101. for (const doc_id of doc_ids) {
  102. ;((doc_id) =>
  103. jobs.push((callback) =>
  104. DocumentManager.flushAndDeleteDocWithLock(
  105. project_id,
  106. doc_id,
  107. {},
  108. function (error) {
  109. if (error != null) {
  110. logger.error(
  111. { err: error, project_id, doc_id },
  112. 'error deleting doc'
  113. )
  114. errors.push(error)
  115. }
  116. return callback()
  117. }
  118. )
  119. ))(doc_id)
  120. }
  121. logger.log({ project_id, doc_ids }, 'deleting docs')
  122. return async.series(jobs, () =>
  123. // When deleting the project here we want to ensure that project
  124. // history is completely flushed because the project may be
  125. // deleted in web after this call completes, and so further
  126. // attempts to flush would fail after that.
  127. HistoryManager.flushProjectChanges(project_id, options, function (
  128. error
  129. ) {
  130. if (errors.length > 0) {
  131. return callback(
  132. new Error('Errors deleting docs. See log for details')
  133. )
  134. } else if (error != null) {
  135. return callback(error)
  136. } else {
  137. return callback(null)
  138. }
  139. })
  140. )
  141. })
  142. },
  143. queueFlushAndDeleteProject(project_id, callback) {
  144. if (callback == null) {
  145. callback = function (error) {}
  146. }
  147. return RedisManager.queueFlushAndDeleteProject(project_id, function (
  148. error
  149. ) {
  150. if (error != null) {
  151. logger.error(
  152. { project_id, error },
  153. 'error adding project to flush and delete queue'
  154. )
  155. return callback(error)
  156. }
  157. Metrics.inc('queued-delete')
  158. return callback()
  159. })
  160. },
  161. getProjectDocsTimestamps(project_id, callback) {
  162. if (callback == null) {
  163. callback = function (error) {}
  164. }
  165. return RedisManager.getDocIdsInProject(project_id, function (
  166. error,
  167. doc_ids
  168. ) {
  169. if (error != null) {
  170. return callback(error)
  171. }
  172. if (!(doc_ids != null ? doc_ids.length : undefined)) {
  173. return callback(null, [])
  174. }
  175. return RedisManager.getDocTimestamps(doc_ids, function (
  176. error,
  177. timestamps
  178. ) {
  179. if (error != null) {
  180. return callback(error)
  181. }
  182. return callback(null, timestamps)
  183. })
  184. })
  185. },
  186. getProjectDocsAndFlushIfOld(
  187. project_id,
  188. projectStateHash,
  189. excludeVersions,
  190. _callback
  191. ) {
  192. if (excludeVersions == null) {
  193. excludeVersions = {}
  194. }
  195. if (_callback == null) {
  196. _callback = function (error, docs) {}
  197. }
  198. const timer = new Metrics.Timer(
  199. 'projectManager.getProjectDocsAndFlushIfOld'
  200. )
  201. const callback = function (...args) {
  202. timer.done()
  203. return _callback(...args)
  204. }
  205. return RedisManager.checkOrSetProjectState(
  206. project_id,
  207. projectStateHash,
  208. function (error, projectStateChanged) {
  209. if (error != null) {
  210. logger.error(
  211. { err: error, project_id },
  212. 'error getting/setting project state in getProjectDocsAndFlushIfOld'
  213. )
  214. return callback(error)
  215. }
  216. // we can't return docs if project structure has changed
  217. if (projectStateChanged) {
  218. return callback(
  219. Errors.ProjectStateChangedError('project state changed')
  220. )
  221. }
  222. // project structure hasn't changed, return doc content from redis
  223. return RedisManager.getDocIdsInProject(project_id, function (
  224. error,
  225. doc_ids
  226. ) {
  227. if (error != null) {
  228. logger.error(
  229. { err: error, project_id },
  230. 'error getting doc ids in getProjectDocs'
  231. )
  232. return callback(error)
  233. }
  234. const jobs = []
  235. for (const doc_id of doc_ids) {
  236. ;((doc_id) =>
  237. jobs.push((
  238. cb // get the doc lines from redis
  239. ) =>
  240. DocumentManager.getDocAndFlushIfOldWithLock(
  241. project_id,
  242. doc_id,
  243. function (err, lines, version) {
  244. if (err != null) {
  245. logger.error(
  246. { err, project_id, doc_id },
  247. 'error getting project doc lines in getProjectDocsAndFlushIfOld'
  248. )
  249. return cb(err)
  250. }
  251. const doc = { _id: doc_id, lines, v: version } // create a doc object to return
  252. return cb(null, doc)
  253. }
  254. )
  255. ))(doc_id)
  256. }
  257. return async.series(jobs, function (error, docs) {
  258. if (error != null) {
  259. return callback(error)
  260. }
  261. return callback(null, docs)
  262. })
  263. })
  264. }
  265. )
  266. },
  267. clearProjectState(project_id, callback) {
  268. if (callback == null) {
  269. callback = function (error) {}
  270. }
  271. return RedisManager.clearProjectState(project_id, callback)
  272. },
  273. updateProjectWithLocks(
  274. project_id,
  275. projectHistoryId,
  276. user_id,
  277. docUpdates,
  278. fileUpdates,
  279. version,
  280. _callback
  281. ) {
  282. if (_callback == null) {
  283. _callback = function (error) {}
  284. }
  285. const timer = new Metrics.Timer('projectManager.updateProject')
  286. const callback = function (...args) {
  287. timer.done()
  288. return _callback(...args)
  289. }
  290. const project_version = version
  291. let project_subversion = 0 // project versions can have multiple operations
  292. let project_ops_length = 0
  293. const handleDocUpdate = function (projectUpdate, cb) {
  294. const doc_id = projectUpdate.id
  295. projectUpdate.version = `${project_version}.${project_subversion++}`
  296. if (projectUpdate.docLines != null) {
  297. return ProjectHistoryRedisManager.queueAddEntity(
  298. project_id,
  299. projectHistoryId,
  300. 'doc',
  301. doc_id,
  302. user_id,
  303. projectUpdate,
  304. function (error, count) {
  305. project_ops_length = count
  306. return cb(error)
  307. }
  308. )
  309. } else {
  310. return DocumentManager.renameDocWithLock(
  311. project_id,
  312. doc_id,
  313. user_id,
  314. projectUpdate,
  315. projectHistoryId,
  316. function (error, count) {
  317. project_ops_length = count
  318. return cb(error)
  319. }
  320. )
  321. }
  322. }
  323. const handleFileUpdate = function (projectUpdate, cb) {
  324. const file_id = projectUpdate.id
  325. projectUpdate.version = `${project_version}.${project_subversion++}`
  326. if (projectUpdate.url != null) {
  327. return ProjectHistoryRedisManager.queueAddEntity(
  328. project_id,
  329. projectHistoryId,
  330. 'file',
  331. file_id,
  332. user_id,
  333. projectUpdate,
  334. function (error, count) {
  335. project_ops_length = count
  336. return cb(error)
  337. }
  338. )
  339. } else {
  340. return ProjectHistoryRedisManager.queueRenameEntity(
  341. project_id,
  342. projectHistoryId,
  343. 'file',
  344. file_id,
  345. user_id,
  346. projectUpdate,
  347. function (error, count) {
  348. project_ops_length = count
  349. return cb(error)
  350. }
  351. )
  352. }
  353. }
  354. return async.eachSeries(docUpdates, handleDocUpdate, function (error) {
  355. if (error != null) {
  356. return callback(error)
  357. }
  358. return async.eachSeries(fileUpdates, handleFileUpdate, function (error) {
  359. if (error != null) {
  360. return callback(error)
  361. }
  362. if (
  363. HistoryManager.shouldFlushHistoryOps(
  364. project_ops_length,
  365. docUpdates.length + fileUpdates.length,
  366. HistoryManager.FLUSH_PROJECT_EVERY_N_OPS
  367. )
  368. ) {
  369. HistoryManager.flushProjectChangesAsync(project_id)
  370. }
  371. return callback()
  372. })
  373. })
  374. },
  375. }