UpdateMerger.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. const { callbackify } = require('util')
  2. const _ = require('underscore')
  3. const fsPromises = require('fs/promises')
  4. const logger = require('@overleaf/logger')
  5. const EditorController = require('../Editor/EditorController')
  6. const FileTypeManager = require('../Uploads/FileTypeManager')
  7. const FileWriter = require('../../infrastructure/FileWriter')
  8. const ProjectEntityHandler = require('../Project/ProjectEntityHandler')
  9. async function mergeUpdate(userId, projectId, path, updateRequest, source) {
  10. const fsPath = await FileWriter.promises.writeStreamToDisk(
  11. projectId,
  12. updateRequest
  13. )
  14. try {
  15. const metadata = await _mergeUpdate(userId, projectId, path, fsPath, source)
  16. return metadata
  17. } finally {
  18. try {
  19. await fsPromises.unlink(fsPath)
  20. } catch (err) {
  21. logger.err({ projectId, fsPath }, 'error deleting file')
  22. }
  23. }
  24. }
  25. async function _findExistingFileType(projectId, path) {
  26. const { docs, files } = await ProjectEntityHandler.promises.getAllEntities(
  27. projectId
  28. )
  29. if (_.some(docs, d => d.path === path)) {
  30. return 'doc'
  31. }
  32. if (_.some(files, f => f.path === path)) {
  33. return 'file'
  34. }
  35. return null
  36. }
  37. async function _determineFileType(projectId, path, fsPath) {
  38. // check if there is an existing file with the same path (we either need
  39. // to overwrite it or delete it)
  40. const existingFileType = await _findExistingFileType(projectId, path)
  41. // determine whether the update should create a doc or binary file
  42. const { binary, encoding } = await FileTypeManager.promises.getType(
  43. path,
  44. fsPath,
  45. existingFileType
  46. )
  47. // If we receive a non-utf8 encoding, we won't be able to keep things in
  48. // sync, so we'll treat non-utf8 files as binary
  49. const isBinary = binary || encoding !== 'utf-8'
  50. // Existing | Update | Resulting file type
  51. // ---------|-----------|--------------------
  52. // file | isBinary | file
  53. // file | !isBinary | file
  54. // doc | isBinary | file
  55. // doc | !isBinary | doc
  56. // null | isBinary | file
  57. // null | !isBinary | doc
  58. // if a binary file already exists, always keep it as a binary file
  59. // even if the update looks like a text file
  60. if (existingFileType === 'file') {
  61. return 'file'
  62. } else {
  63. return isBinary ? 'file' : 'doc'
  64. }
  65. }
  66. async function _mergeUpdate(userId, projectId, path, fsPath, source) {
  67. const fileType = await _determineFileType(projectId, path, fsPath)
  68. if (fileType === 'file') {
  69. const file = await _processFile(projectId, fsPath, path, source, userId)
  70. return { entityType: 'file', entityId: file._id, rev: file.rev }
  71. } else if (fileType === 'doc') {
  72. const doc = await _processDoc(projectId, userId, fsPath, path, source)
  73. // The doc entry doesn't have a rev. Since the document is set in
  74. // docupdater, it's possible that the next rev contains a merge of changes
  75. // in Dropbox and changes from docupdater.
  76. const metadata = { entityType: 'doc', entityId: doc._id, rev: doc.rev }
  77. return metadata
  78. } else {
  79. throw new Error('unrecognized file')
  80. }
  81. }
  82. async function deleteUpdate(userId, projectId, path, source) {
  83. try {
  84. await EditorController.promises.deleteEntityWithPath(
  85. projectId,
  86. path,
  87. source,
  88. userId
  89. )
  90. } catch (err) {
  91. logger.warn(
  92. { err, userId, projectId, path, source },
  93. 'failed to delete entity'
  94. )
  95. }
  96. }
  97. async function _processDoc(projectId, userId, fsPath, path, source) {
  98. const docLines = await _readFileIntoTextArray(fsPath)
  99. logger.debug({ docLines }, 'processing doc update from tpds')
  100. const doc = await EditorController.promises.upsertDocWithPath(
  101. projectId,
  102. path,
  103. docLines,
  104. source,
  105. userId
  106. )
  107. return doc
  108. }
  109. async function _processFile(projectId, fsPath, path, source, userId) {
  110. const file = await EditorController.promises.upsertFileWithPath(
  111. projectId,
  112. path,
  113. fsPath,
  114. null,
  115. source,
  116. userId
  117. )
  118. return file
  119. }
  120. async function _readFileIntoTextArray(path) {
  121. let content = await fsPromises.readFile(path, 'utf8')
  122. if (content == null) {
  123. content = ''
  124. }
  125. const lines = content.split(/\r\n|\n|\r/)
  126. return lines
  127. }
  128. module.exports = {
  129. mergeUpdate: callbackify(mergeUpdate),
  130. _mergeUpdate: callbackify(_mergeUpdate),
  131. deleteUpdate: callbackify(deleteUpdate),
  132. promises: {
  133. mergeUpdate,
  134. _mergeUpdate, // called by GitBridgeHandler
  135. deleteUpdate,
  136. },
  137. }