ProjectDeleter.js 9.9 KB

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