ProjectManager.js 11 KB

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