ProjectUploadController.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import logger from '@overleaf/logger'
  2. import metrics from '@overleaf/metrics'
  3. import fs from 'node:fs'
  4. import Path from 'node:path'
  5. import FileSystemImportManager from './FileSystemImportManager.mjs'
  6. import ProjectUploadManager from './ProjectUploadManager.mjs'
  7. import SessionManager from '../Authentication/SessionManager.mjs'
  8. import EditorController from '../Editor/EditorController.mjs'
  9. import ProjectLocator from '../Project/ProjectLocator.mjs'
  10. import Settings from '@overleaf/settings'
  11. import { InvalidZipFileError } from './ArchiveErrors.mjs'
  12. import multer from 'multer'
  13. import lodash from 'lodash'
  14. import { expressify } from '@overleaf/promise-utils'
  15. import { DuplicateNameError } from '../Errors/Errors.js'
  16. const defaultsDeep = lodash.defaultsDeep
  17. const upload = multer(
  18. defaultsDeep(
  19. {
  20. dest: Settings.path.uploadFolder,
  21. limits: {
  22. fileSize: Settings.maxUploadSize,
  23. },
  24. },
  25. Settings.multerOptions
  26. )
  27. )
  28. /**
  29. * @param {any} req
  30. * @param {any} res
  31. * @param {any} next
  32. */
  33. function uploadProject(req, res, next) {
  34. const timer = new metrics.Timer('project-upload')
  35. const userId = SessionManager.getLoggedInUserId(req.session)
  36. const { path } = req.file
  37. const name = Path.basename(req.body.name, '.zip')
  38. return ProjectUploadManager.createProjectFromZipArchive(
  39. userId,
  40. name,
  41. path,
  42. function (error, project) {
  43. fs.unlink(path, function () {})
  44. timer.done()
  45. if (error != null) {
  46. logger.error(
  47. { err: error, filePath: path, fileName: name },
  48. 'error uploading project'
  49. )
  50. if (error instanceof InvalidZipFileError) {
  51. return res.status(422).json({
  52. success: false,
  53. error: req.i18n.translate(error.message),
  54. })
  55. } else {
  56. return res.status(500).json({
  57. success: false,
  58. error: req.i18n.translate('upload_failed'),
  59. })
  60. }
  61. } else {
  62. return res.json({ success: true, project_id: project._id })
  63. }
  64. }
  65. )
  66. }
  67. /**
  68. * @param {any} req
  69. * @param {any} res
  70. * @param {any} next
  71. */
  72. async function uploadFile(req, res, next) {
  73. const timer = new metrics.Timer('file-upload')
  74. const name = req.body.name
  75. const { path } = req.file
  76. const projectId = req.params.Project_id
  77. const userId = SessionManager.getLoggedInUserId(req.session)
  78. let { folder_id: folderId } = req.query
  79. if (name == null || name.length === 0 || name.length > 150) {
  80. fs.unlink(path, function () {})
  81. return res.status(422).json({
  82. success: false,
  83. error: 'invalid_filename',
  84. })
  85. }
  86. try {
  87. // preserve the directory structure from an uploaded folder
  88. const { relativePath } = req.body
  89. // NOTE: Uppy sends a "null" string for `relativePath` when the file is not nested in a folder
  90. if (relativePath && relativePath !== 'null') {
  91. const { path } = await ProjectLocator.promises.findElement({
  92. project_id: projectId,
  93. element_id: folderId,
  94. type: 'folder',
  95. })
  96. const { lastFolder } = await EditorController.promises.mkdirp(
  97. projectId,
  98. Path.dirname(Path.join('/', path.fileSystem, relativePath)),
  99. userId
  100. )
  101. folderId = lastFolder._id
  102. }
  103. } catch (error) {
  104. fs.unlink(path, function () {})
  105. throw error
  106. }
  107. return FileSystemImportManager.addEntity(
  108. userId,
  109. projectId,
  110. folderId,
  111. name,
  112. path,
  113. true,
  114. function (error, entity) {
  115. fs.unlink(path, function () {})
  116. timer.done()
  117. if (error != null) {
  118. if (error.name === 'InvalidNameError') {
  119. return res.status(422).json({
  120. success: false,
  121. error: 'invalid_filename',
  122. })
  123. } else if (error instanceof DuplicateNameError) {
  124. return res.status(422).json({
  125. success: false,
  126. error: 'duplicate_file_name',
  127. })
  128. } else if (error.message === 'project_has_too_many_files') {
  129. return res.status(422).json({
  130. success: false,
  131. error: 'project_has_too_many_files',
  132. })
  133. } else if (error.message === 'folder_not_found') {
  134. return res.status(422).json({
  135. success: false,
  136. error: 'folder_not_found',
  137. })
  138. } else {
  139. logger.error(
  140. {
  141. err: error,
  142. projectId,
  143. filePath: path,
  144. fileName: name,
  145. folderId,
  146. },
  147. 'error uploading file'
  148. )
  149. return res.status(422).json({ success: false })
  150. }
  151. } else {
  152. return res.json({
  153. success: true,
  154. entity_id: entity?._id,
  155. entity_type: entity?.type,
  156. hash: entity?.hash,
  157. })
  158. }
  159. }
  160. )
  161. }
  162. /**
  163. * @param {any} req
  164. * @param {any} res
  165. * @param {any} next
  166. */
  167. function multerMiddleware(req, res, next) {
  168. if (upload == null) {
  169. return res
  170. .status(500)
  171. .json({ success: false, error: req.i18n.translate('upload_failed') })
  172. }
  173. return upload.single('qqfile')(
  174. req,
  175. res,
  176. /** @param {any} err */ function (err) {
  177. if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
  178. return res
  179. .status(422)
  180. .json({ success: false, error: req.i18n.translate('file_too_large') })
  181. }
  182. if (err) return next(err)
  183. if (!req.file?.path) {
  184. logger.info({ req }, 'missing req.file.path on upload')
  185. return res
  186. .status(400)
  187. .json({ success: false, error: 'invalid_upload_request' })
  188. }
  189. next()
  190. }
  191. )
  192. }
  193. export default {
  194. uploadProject,
  195. uploadFile: expressify(uploadFile),
  196. multerMiddleware,
  197. }