ProjectDeleter.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. const { db, ObjectId } = require('../../infrastructure/mongojs')
  2. const { callbackify } = require('util')
  3. const { Project } = require('../../models/Project')
  4. const { DeletedProject } = require('../../models/DeletedProject')
  5. const Errors = require('../Errors/Errors')
  6. const logger = require('logger-sharelatex')
  7. const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
  8. const TagsHandler = require('../Tags/TagsHandler')
  9. const ProjectHelper = require('./ProjectHelper')
  10. const ProjectDetailsHandler = require('./ProjectDetailsHandler')
  11. const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
  12. const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
  13. const DocstoreManager = require('../Docstore/DocstoreManager')
  14. const EditorRealTimeController = require('../Editor/EditorRealTimeController')
  15. const HistoryManager = require('../History/HistoryManager')
  16. const FilestoreHandler = require('../FileStore/FileStoreHandler')
  17. const moment = require('moment')
  18. const { promiseMapWithLimit } = require('../../util/promises')
  19. const EXPIRE_PROJECTS_AFTER_DAYS = 90
  20. module.exports = {
  21. markAsDeletedByExternalSource: callbackify(markAsDeletedByExternalSource),
  22. unmarkAsDeletedByExternalSource: callbackify(unmarkAsDeletedByExternalSource),
  23. deleteUsersProjects: callbackify(deleteUsersProjects),
  24. expireDeletedProjectsAfterDuration: callbackify(
  25. expireDeletedProjectsAfterDuration
  26. ),
  27. restoreProject: callbackify(restoreProject),
  28. archiveProject: callbackify(archiveProject),
  29. unarchiveProject: callbackify(unarchiveProject),
  30. trashProject: callbackify(trashProject),
  31. untrashProject: callbackify(untrashProject),
  32. deleteProject: callbackify(deleteProject),
  33. undeleteProject: callbackify(undeleteProject),
  34. expireDeletedProject: callbackify(expireDeletedProject),
  35. promises: {
  36. archiveProject,
  37. unarchiveProject,
  38. trashProject,
  39. untrashProject,
  40. deleteProject,
  41. undeleteProject,
  42. expireDeletedProject,
  43. markAsDeletedByExternalSource,
  44. unmarkAsDeletedByExternalSource,
  45. deleteUsersProjects,
  46. expireDeletedProjectsAfterDuration,
  47. restoreProject
  48. }
  49. }
  50. async function markAsDeletedByExternalSource(projectId) {
  51. logger.log(
  52. { project_id: projectId },
  53. 'marking project as deleted by external data source'
  54. )
  55. await Project.update(
  56. { _id: projectId },
  57. { deletedByExternalDataSource: true }
  58. ).exec()
  59. EditorRealTimeController.emitToRoom(
  60. projectId,
  61. 'projectRenamedOrDeletedByExternalSource'
  62. )
  63. }
  64. async function unmarkAsDeletedByExternalSource(projectId) {
  65. await Project.update(
  66. { _id: projectId },
  67. { deletedByExternalDataSource: false }
  68. ).exec()
  69. }
  70. async function deleteUsersProjects(userId) {
  71. const projects = await Project.find({ owner_ref: userId }).exec()
  72. await promiseMapWithLimit(5, projects, project => deleteProject(project._id))
  73. await CollaboratorsHandler.promises.removeUserFromAllProjects(userId)
  74. }
  75. async function expireDeletedProjectsAfterDuration() {
  76. const deletedProjects = await DeletedProject.find({
  77. 'deleterData.deletedAt': {
  78. $lt: new Date(moment().subtract(EXPIRE_PROJECTS_AFTER_DAYS, 'days'))
  79. },
  80. project: { $ne: null }
  81. })
  82. for (const deletedProject of deletedProjects) {
  83. await expireDeletedProject(deletedProject.deleterData.deletedProjectId)
  84. }
  85. }
  86. async function restoreProject(projectId) {
  87. await Project.update(
  88. { _id: projectId },
  89. { $unset: { archived: true } }
  90. ).exec()
  91. }
  92. async function archiveProject(projectId, userId) {
  93. try {
  94. let project = await Project.findOne({ _id: projectId }).exec()
  95. if (!project) {
  96. throw new Errors.NotFoundError('project not found')
  97. }
  98. const archived = ProjectHelper.calculateArchivedArray(
  99. project,
  100. userId,
  101. 'ARCHIVE'
  102. )
  103. await Project.update(
  104. { _id: projectId },
  105. { $set: { archived: archived }, $pull: { trashed: ObjectId(userId) } }
  106. )
  107. } catch (err) {
  108. logger.warn({ err }, 'problem archiving project')
  109. throw err
  110. }
  111. }
  112. async function unarchiveProject(projectId, userId) {
  113. try {
  114. let project = await Project.findOne({ _id: projectId }).exec()
  115. if (!project) {
  116. throw new Errors.NotFoundError('project not found')
  117. }
  118. const archived = ProjectHelper.calculateArchivedArray(
  119. project,
  120. userId,
  121. 'UNARCHIVE'
  122. )
  123. await Project.update({ _id: projectId }, { $set: { archived: archived } })
  124. } catch (err) {
  125. logger.warn({ err }, 'problem unarchiving project')
  126. throw err
  127. }
  128. }
  129. async function trashProject(projectId, userId) {
  130. try {
  131. let project = await Project.findOne({ _id: projectId }).exec()
  132. if (!project) {
  133. throw new Errors.NotFoundError('project not found')
  134. }
  135. const archived = ProjectHelper.calculateArchivedArray(
  136. project,
  137. userId,
  138. 'UNARCHIVE'
  139. )
  140. await Project.update(
  141. { _id: projectId },
  142. {
  143. $addToSet: { trashed: ObjectId(userId) },
  144. $set: { archived: archived }
  145. }
  146. )
  147. } catch (err) {
  148. logger.warn({ err }, 'problem trashing project')
  149. throw err
  150. }
  151. }
  152. async function untrashProject(projectId, userId) {
  153. try {
  154. let project = await Project.findOne({ _id: projectId }).exec()
  155. if (!project) {
  156. throw new Errors.NotFoundError('project not found')
  157. }
  158. await Project.update(
  159. { _id: projectId },
  160. { $pull: { trashed: ObjectId(userId) } }
  161. )
  162. } catch (err) {
  163. logger.warn({ err }, 'problem untrashing project')
  164. throw err
  165. }
  166. }
  167. async function deleteProject(projectId, options = {}) {
  168. try {
  169. const project = await Project.findOne({ _id: projectId }).exec()
  170. if (!project) {
  171. throw new Errors.NotFoundError('project not found')
  172. }
  173. const deleterData = {
  174. deletedAt: new Date(),
  175. deleterId:
  176. options.deleterUser != null ? options.deleterUser._id : undefined,
  177. deleterIpAddress: options.ipAddress,
  178. deletedProjectId: project._id,
  179. deletedProjectOwnerId: project.owner_ref,
  180. deletedProjectCollaboratorIds: project.collaberator_refs,
  181. deletedProjectReadOnlyIds: project.readOnly_refs,
  182. deletedProjectReadWriteTokenAccessIds:
  183. project.tokenAccessReadAndWrite_refs,
  184. deletedProjectOverleafId: project.overleaf
  185. ? project.overleaf.id
  186. : undefined,
  187. deletedProjectOverleafHistoryId:
  188. project.overleaf && project.overleaf.history
  189. ? project.overleaf.history.id
  190. : undefined,
  191. deletedProjectReadOnlyTokenAccessIds: project.tokenAccessReadOnly_refs,
  192. deletedProjectReadWriteToken: project.tokens.readAndWrite,
  193. deletedProjectReadOnlyToken: project.tokens.readOnly,
  194. deletedProjectLastUpdatedAt: project.lastUpdated
  195. }
  196. Object.keys(deleterData).forEach(
  197. key => (deleterData[key] === undefined ? delete deleterData[key] : '')
  198. )
  199. await DeletedProject.update(
  200. { 'deleterData.deletedProjectId': projectId },
  201. { project, deleterData },
  202. { upsert: true }
  203. )
  204. await DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete(
  205. projectId
  206. )
  207. const memberIds = await CollaboratorsGetter.promises.getMemberIds(projectId)
  208. // fire these jobs in the background
  209. for (const memberId of memberIds) {
  210. TagsHandler.promises
  211. .removeProjectFromAllTags(memberId, projectId)
  212. .catch(err => {
  213. logger.err(
  214. { err, memberId, projectId },
  215. 'failed to remove project from tags'
  216. )
  217. })
  218. }
  219. await Project.remove({ _id: projectId }).exec()
  220. } catch (err) {
  221. logger.warn({ err }, 'problem deleting project')
  222. throw err
  223. }
  224. logger.log({ project_id: projectId }, 'successfully deleted project')
  225. }
  226. async function undeleteProject(projectId) {
  227. let deletedProject = await DeletedProject.findOne({
  228. 'deleterData.deletedProjectId': projectId
  229. }).exec()
  230. if (!deletedProject) {
  231. throw new Errors.NotFoundError('project_not_found')
  232. }
  233. if (!deletedProject.project) {
  234. throw new Errors.NotFoundError('project_too_old_to_restore')
  235. }
  236. let restored = new Project(deletedProject.project)
  237. // if we're undeleting, we want the document to show up
  238. restored.name = await ProjectDetailsHandler.promises.generateUniqueName(
  239. deletedProject.deleterData.deletedProjectOwnerId,
  240. restored.name + ' (Restored)'
  241. )
  242. restored.archived = undefined
  243. // we can't use Mongoose to re-insert the project, as it won't
  244. // create a new document with an _id already specified. We need to
  245. // insert it directly into the collection
  246. // db.projects.insert doesn't work with promisify
  247. await new Promise((resolve, reject) => {
  248. db.projects.insert(restored, err => {
  249. if (err) {
  250. reject(err)
  251. } else {
  252. resolve()
  253. }
  254. })
  255. })
  256. await DeletedProject.deleteOne({ _id: deletedProject._id }).exec()
  257. }
  258. async function expireDeletedProject(projectId) {
  259. try {
  260. const deletedProject = await DeletedProject.findOne({
  261. 'deleterData.deletedProjectId': projectId
  262. }).exec()
  263. if (!deletedProject) {
  264. throw new Errors.NotFoundError(
  265. `No deleted project found for project id ${projectId}`
  266. )
  267. }
  268. if (!deletedProject.project) {
  269. logger.warn(
  270. { projectId },
  271. `Attempted to expire already-expired deletedProject`
  272. )
  273. return
  274. }
  275. await DocstoreManager.promises.destroyProject(deletedProject.project._id)
  276. await HistoryManager.promises.deleteProject(deletedProject.project._id)
  277. await FilestoreHandler.promises.deleteProject(deletedProject.project._id)
  278. await DeletedProject.update(
  279. {
  280. _id: deletedProject._id
  281. },
  282. {
  283. $set: {
  284. 'deleterData.deleterIpAddress': null,
  285. project: null
  286. }
  287. }
  288. ).exec()
  289. } catch (error) {
  290. logger.warn({ projectId, error }, 'error expiring deleted project')
  291. throw error
  292. }
  293. }