FileSystemImportManager.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. const fs = require('fs')
  2. const Path = require('path')
  3. const { callbackify } = require('util')
  4. const EditorController = require('../Editor/EditorController')
  5. const Errors = require('../Errors/Errors')
  6. const FileTypeManager = require('./FileTypeManager')
  7. const SafePath = require('../Project/SafePath')
  8. const logger = require('logger-sharelatex')
  9. module.exports = {
  10. addEntity: callbackify(addEntity),
  11. importDir: callbackify(importDir),
  12. promises: {
  13. addEntity,
  14. importDir,
  15. },
  16. }
  17. async function addDoc(userId, projectId, folderId, name, lines, replace) {
  18. if (replace) {
  19. const doc = await EditorController.promises.upsertDoc(
  20. projectId,
  21. folderId,
  22. name,
  23. lines,
  24. 'upload',
  25. userId
  26. )
  27. return doc
  28. } else {
  29. const doc = await EditorController.promises.addDoc(
  30. projectId,
  31. folderId,
  32. name,
  33. lines,
  34. 'upload',
  35. userId
  36. )
  37. return doc
  38. }
  39. }
  40. async function addFile(userId, projectId, folderId, name, path, replace) {
  41. if (replace) {
  42. const file = await EditorController.promises.upsertFile(
  43. projectId,
  44. folderId,
  45. name,
  46. path,
  47. null,
  48. 'upload',
  49. userId
  50. )
  51. return file
  52. } else {
  53. const file = await EditorController.promises.addFile(
  54. projectId,
  55. folderId,
  56. name,
  57. path,
  58. null,
  59. 'upload',
  60. userId
  61. )
  62. return file
  63. }
  64. }
  65. async function addFolder(userId, projectId, folderId, name, path, replace) {
  66. const newFolder = await EditorController.promises.addFolder(
  67. projectId,
  68. folderId,
  69. name,
  70. 'upload',
  71. userId
  72. )
  73. await addFolderContents(userId, projectId, newFolder._id, path, replace)
  74. return newFolder
  75. }
  76. async function addFolderContents(
  77. userId,
  78. projectId,
  79. parentFolderId,
  80. folderPath,
  81. replace
  82. ) {
  83. if (!(await _isSafeOnFileSystem(folderPath))) {
  84. logger.log(
  85. { userId, projectId, parentFolderId, folderPath },
  86. 'add folder contents is from symlink, stopping insert'
  87. )
  88. throw new Error('path is symlink')
  89. }
  90. const entries = (await fs.promises.readdir(folderPath)) || []
  91. for (const entry of entries) {
  92. if (await FileTypeManager.promises.shouldIgnore(entry)) {
  93. continue
  94. }
  95. await addEntity(
  96. userId,
  97. projectId,
  98. parentFolderId,
  99. entry,
  100. `${folderPath}/${entry}`,
  101. replace
  102. )
  103. }
  104. }
  105. async function addEntity(userId, projectId, folderId, name, fsPath, replace) {
  106. if (!(await _isSafeOnFileSystem(fsPath))) {
  107. logger.log(
  108. { userId, projectId, folderId, fsPath },
  109. 'add entry is from symlink, stopping insert'
  110. )
  111. throw new Error('path is symlink')
  112. }
  113. if (await FileTypeManager.promises.isDirectory(fsPath)) {
  114. const newFolder = await addFolder(
  115. userId,
  116. projectId,
  117. folderId,
  118. name,
  119. fsPath,
  120. replace
  121. )
  122. return newFolder
  123. }
  124. // Here, we cheat a little bit and provide the project path relative to the
  125. // folder, not the root of the project. This is because we don't know for sure
  126. // at this point what the final path of the folder will be. The project path
  127. // is still important for importFile() to be able to figure out if the file is
  128. // a binary file or an editable document.
  129. const projectPath = Path.join('/', name)
  130. const importInfo = await importFile(fsPath, projectPath)
  131. switch (importInfo.type) {
  132. case 'file': {
  133. const entity = await addFile(
  134. userId,
  135. projectId,
  136. folderId,
  137. name,
  138. importInfo.fsPath,
  139. replace
  140. )
  141. if (entity != null) {
  142. entity.type = 'file'
  143. }
  144. return entity
  145. }
  146. case 'doc': {
  147. const entity = await addDoc(
  148. userId,
  149. projectId,
  150. folderId,
  151. name,
  152. importInfo.lines,
  153. replace
  154. )
  155. if (entity != null) {
  156. entity.type = 'doc'
  157. }
  158. return entity
  159. }
  160. default: {
  161. throw new Error(`unknown import type: ${importInfo.type}`)
  162. }
  163. }
  164. }
  165. async function _isSafeOnFileSystem(path) {
  166. // Use lstat() to ensure we don't follow symlinks. Symlinks from an
  167. // untrusted source are dangerous.
  168. const stat = await fs.promises.lstat(path)
  169. return stat.isFile() || stat.isDirectory()
  170. }
  171. async function importFile(fsPath, projectPath) {
  172. const stat = await fs.promises.lstat(fsPath)
  173. if (!stat.isFile()) {
  174. throw new Error(`can't import ${fsPath}: not a regular file`)
  175. }
  176. _validateProjectPath(projectPath)
  177. const filename = Path.basename(projectPath)
  178. const { binary, encoding } = await FileTypeManager.promises.getType(
  179. filename,
  180. fsPath
  181. )
  182. if (binary) {
  183. return new FileImport(projectPath, fsPath)
  184. } else {
  185. const content = await fs.promises.readFile(fsPath, encoding)
  186. // Handle Unix, DOS and classic Mac newlines
  187. const lines = content.split(/\r\n|\n|\r/)
  188. return new DocImport(projectPath, lines)
  189. }
  190. }
  191. async function importDir(dirPath) {
  192. const stat = await fs.promises.lstat(dirPath)
  193. if (!stat.isDirectory()) {
  194. throw new Error(`can't import ${dirPath}: not a directory`)
  195. }
  196. const entries = []
  197. for await (const filePath of _walkDir(dirPath)) {
  198. const projectPath = Path.join('/', Path.relative(dirPath, filePath))
  199. const importInfo = await importFile(filePath, projectPath)
  200. entries.push(importInfo)
  201. }
  202. return entries
  203. }
  204. function _validateProjectPath(path) {
  205. if (!SafePath.isAllowedLength(path) || !SafePath.isCleanPath(path)) {
  206. throw new Errors.InvalidNameError(`Invalid path: ${path}`)
  207. }
  208. }
  209. async function* _walkDir(dirPath) {
  210. const entries = await fs.promises.readdir(dirPath)
  211. for (const entry of entries) {
  212. const entryPath = Path.join(dirPath, entry)
  213. if (await FileTypeManager.promises.shouldIgnore(entryPath)) {
  214. continue
  215. }
  216. // Use lstat() to ensure we don't follow symlinks. Symlinks from an
  217. // untrusted source are dangerous.
  218. const stat = await fs.promises.lstat(entryPath)
  219. if (stat.isFile()) {
  220. yield entryPath
  221. } else if (stat.isDirectory()) {
  222. yield* _walkDir(entryPath)
  223. }
  224. }
  225. }
  226. class FileImport {
  227. constructor(projectPath, fsPath) {
  228. this.type = 'file'
  229. this.projectPath = projectPath
  230. this.fsPath = fsPath
  231. }
  232. }
  233. class DocImport {
  234. constructor(projectPath, lines) {
  235. this.type = 'doc'
  236. this.projectPath = projectPath
  237. this.lines = lines
  238. }
  239. }