DocstoreManager.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. const { promisify } = require('util')
  2. const { promisifyMultiResult, callbackify } = require('@overleaf/promise-utils')
  3. const request = require('request').defaults({ jar: false })
  4. const OError = require('@overleaf/o-error')
  5. const logger = require('@overleaf/logger')
  6. const settings = require('@overleaf/settings')
  7. const Errors = require('../Errors/Errors')
  8. const { fetchJson } = require('@overleaf/fetch-utils')
  9. const TIMEOUT = 30 * 1000 // request timeout
  10. function deleteDoc(projectId, docId, name, deletedAt, callback) {
  11. const url = `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}`
  12. const docMetaData = { deleted: true, deletedAt, name }
  13. const options = { url, json: docMetaData, timeout: TIMEOUT }
  14. request.patch(options, (error, res) => {
  15. if (error) {
  16. return callback(error)
  17. }
  18. if (res.statusCode >= 200 && res.statusCode < 300) {
  19. callback(null)
  20. } else if (res.statusCode === 404) {
  21. error = new Errors.NotFoundError({
  22. message: 'tried to delete doc not in docstore',
  23. info: {
  24. projectId,
  25. docId,
  26. },
  27. })
  28. callback(error) // maybe suppress the error when delete doc which is not present?
  29. } else {
  30. error = new OError(
  31. `docstore api responded with non-success code: ${res.statusCode}`,
  32. {
  33. projectId,
  34. docId,
  35. }
  36. )
  37. callback(error)
  38. }
  39. })
  40. }
  41. /**
  42. * @param {string} projectId
  43. */
  44. function getAllDocs(projectId, callback) {
  45. const url = `${settings.apis.docstore.url}/project/${projectId}/doc`
  46. request.get(
  47. {
  48. url,
  49. timeout: TIMEOUT,
  50. json: true,
  51. },
  52. (error, res, docs) => {
  53. if (error) {
  54. return callback(error)
  55. }
  56. if (res.statusCode >= 200 && res.statusCode < 300) {
  57. callback(null, docs)
  58. } else {
  59. error = new OError(
  60. `docstore api responded with non-success code: ${res.statusCode}`,
  61. { projectId }
  62. )
  63. callback(error)
  64. }
  65. }
  66. )
  67. }
  68. function getAllDeletedDocs(projectId, callback) {
  69. const url = `${settings.apis.docstore.url}/project/${projectId}/doc-deleted`
  70. request.get({ url, timeout: TIMEOUT, json: true }, (error, res, docs) => {
  71. if (error) {
  72. callback(OError.tag(error, 'could not get deleted docs from docstore'))
  73. } else if (res.statusCode === 200) {
  74. callback(null, docs)
  75. } else {
  76. callback(
  77. new OError(
  78. `docstore api responded with non-success code: ${res.statusCode}`,
  79. { projectId }
  80. )
  81. )
  82. }
  83. })
  84. }
  85. /**
  86. * @param {string} projectId
  87. */
  88. async function getCommentThreadIds(projectId) {
  89. const url = `${settings.apis.docstore.url}/project/${projectId}/comment-thread-ids`
  90. return fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
  91. }
  92. /**
  93. * @param {string} projectId
  94. */
  95. async function getTrackedChangesUserIds(projectId) {
  96. const url = `${settings.apis.docstore.url}/project/${projectId}/tracked-changes-user-ids`
  97. return fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) })
  98. }
  99. /**
  100. * @param {string} projectId
  101. * @param {Callback} callback
  102. */
  103. function getAllRanges(projectId, callback) {
  104. const url = `${settings.apis.docstore.url}/project/${projectId}/ranges`
  105. request.get(
  106. {
  107. url,
  108. timeout: TIMEOUT,
  109. json: true,
  110. },
  111. (error, res, docs) => {
  112. if (error) {
  113. return callback(error)
  114. }
  115. if (res.statusCode >= 200 && res.statusCode < 300) {
  116. callback(null, docs)
  117. } else {
  118. error = new OError(
  119. `docstore api responded with non-success code: ${res.statusCode}`,
  120. { projectId }
  121. )
  122. callback(error)
  123. }
  124. }
  125. )
  126. }
  127. function getDoc(projectId, docId, options, callback) {
  128. if (options == null) {
  129. options = {}
  130. }
  131. if (typeof options === 'function') {
  132. callback = options
  133. options = {}
  134. }
  135. const requestParams = { timeout: TIMEOUT, json: true }
  136. if (options.peek) {
  137. requestParams.url = `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}/peek`
  138. } else {
  139. requestParams.url = `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}`
  140. }
  141. if (options.include_deleted) {
  142. requestParams.qs = { include_deleted: 'true' }
  143. }
  144. request.get(requestParams, (error, res, doc) => {
  145. if (error) {
  146. return callback(error)
  147. }
  148. if (res.statusCode >= 200 && res.statusCode < 300) {
  149. logger.debug(
  150. { docId, projectId, version: doc.version, rev: doc.rev },
  151. 'got doc from docstore api'
  152. )
  153. callback(null, doc.lines, doc.rev, doc.version, doc.ranges)
  154. } else if (res.statusCode === 404) {
  155. error = new Errors.NotFoundError({
  156. message: 'doc not found in docstore',
  157. info: {
  158. projectId,
  159. docId,
  160. },
  161. })
  162. callback(error)
  163. } else {
  164. error = new OError(
  165. `docstore api responded with non-success code: ${res.statusCode}`,
  166. {
  167. projectId,
  168. docId,
  169. }
  170. )
  171. callback(error)
  172. }
  173. })
  174. }
  175. function isDocDeleted(projectId, docId, callback) {
  176. const url = `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}/deleted`
  177. request.get({ url, timeout: TIMEOUT, json: true }, (err, res, body) => {
  178. if (err) {
  179. callback(err)
  180. } else if (res.statusCode === 200) {
  181. callback(null, body.deleted)
  182. } else if (res.statusCode === 404) {
  183. callback(
  184. new Errors.NotFoundError({
  185. message: 'doc does not exist in project',
  186. info: { projectId, docId },
  187. })
  188. )
  189. } else {
  190. callback(
  191. new OError(
  192. `docstore api responded with non-success code: ${res.statusCode}`,
  193. { projectId, docId }
  194. )
  195. )
  196. }
  197. })
  198. }
  199. function updateDoc(projectId, docId, lines, version, ranges, callback) {
  200. const url = `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}`
  201. request.post(
  202. {
  203. url,
  204. timeout: TIMEOUT,
  205. json: {
  206. lines,
  207. version,
  208. ranges,
  209. },
  210. },
  211. (error, res, result) => {
  212. if (error) {
  213. return callback(error)
  214. }
  215. if (res.statusCode >= 200 && res.statusCode < 300) {
  216. logger.debug(
  217. { projectId, docId },
  218. 'update doc in docstore url finished'
  219. )
  220. callback(null, result.modified, result.rev)
  221. } else {
  222. error = new OError(
  223. `docstore api responded with non-success code: ${res.statusCode}`,
  224. { projectId, docId }
  225. )
  226. callback(error)
  227. }
  228. }
  229. )
  230. }
  231. /**
  232. * Asks docstore whether any doc in the project has ranges
  233. *
  234. * @param {string} proejctId
  235. * @param {Callback} callback
  236. */
  237. function projectHasRanges(projectId, callback) {
  238. const url = `${settings.apis.docstore.url}/project/${projectId}/has-ranges`
  239. request.get({ url, timeout: TIMEOUT, json: true }, (err, res, body) => {
  240. if (err) {
  241. return callback(err)
  242. }
  243. if (res.statusCode >= 200 && res.statusCode < 300) {
  244. callback(null, body.projectHasRanges)
  245. } else {
  246. callback(
  247. new OError(
  248. `docstore api responded with non-success code: ${res.statusCode}`,
  249. { projectId }
  250. )
  251. )
  252. }
  253. })
  254. }
  255. function archiveProject(projectId, callback) {
  256. _operateOnProject(projectId, 'archive', callback)
  257. }
  258. function unarchiveProject(projectId, callback) {
  259. _operateOnProject(projectId, 'unarchive', callback)
  260. }
  261. function destroyProject(projectId, callback) {
  262. _operateOnProject(projectId, 'destroy', callback)
  263. }
  264. function _operateOnProject(projectId, method, callback) {
  265. const url = `${settings.apis.docstore.url}/project/${projectId}/${method}`
  266. logger.debug({ projectId }, `calling ${method} for project in docstore`)
  267. // use default timeout for archiving/unarchiving/destroying
  268. request.post(url, (err, res, docs) => {
  269. if (err) {
  270. OError.tag(err, `error calling ${method} project in docstore`, {
  271. projectId,
  272. })
  273. return callback(err)
  274. }
  275. if (res.statusCode >= 200 && res.statusCode < 300) {
  276. callback()
  277. } else {
  278. const error = new Error(
  279. `docstore api responded with non-success code: ${res.statusCode}`
  280. )
  281. logger.warn(
  282. { err: error, projectId },
  283. `error calling ${method} project in docstore`
  284. )
  285. callback(error)
  286. }
  287. })
  288. }
  289. module.exports = {
  290. deleteDoc,
  291. getAllDocs,
  292. getAllDeletedDocs,
  293. getAllRanges,
  294. getDoc,
  295. getCommentThreadIds: callbackify(getCommentThreadIds),
  296. getTrackedChangesUserIds: callbackify(getTrackedChangesUserIds),
  297. isDocDeleted,
  298. updateDoc,
  299. projectHasRanges,
  300. archiveProject,
  301. unarchiveProject,
  302. destroyProject,
  303. promises: {
  304. deleteDoc: promisify(deleteDoc),
  305. getAllDocs: promisify(getAllDocs),
  306. getAllDeletedDocs: promisify(getAllDeletedDocs),
  307. getAllRanges: promisify(getAllRanges),
  308. getDoc: promisifyMultiResult(getDoc, ['lines', 'rev', 'version', 'ranges']),
  309. getCommentThreadIds,
  310. getTrackedChangesUserIds,
  311. isDocDeleted: promisify(isDocDeleted),
  312. updateDoc: promisifyMultiResult(updateDoc, ['modified', 'rev']),
  313. projectHasRanges: promisify(projectHasRanges),
  314. archiveProject: promisify(archiveProject),
  315. unarchiveProject: promisify(unarchiveProject),
  316. destroyProject: promisify(destroyProject),
  317. },
  318. }