| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- const fs = require('fs')
- const Path = require('path')
- const { callbackify } = require('util')
- const EditorController = require('../Editor/EditorController')
- const Errors = require('../Errors/Errors')
- const FileTypeManager = require('./FileTypeManager')
- const SafePath = require('../Project/SafePath')
- const logger = require('logger-sharelatex')
- module.exports = {
- addEntity: callbackify(addEntity),
- importDir: callbackify(importDir),
- promises: {
- addEntity,
- importDir,
- },
- }
- async function addDoc(userId, projectId, folderId, name, lines, replace) {
- if (replace) {
- const doc = await EditorController.promises.upsertDoc(
- projectId,
- folderId,
- name,
- lines,
- 'upload',
- userId
- )
- return doc
- } else {
- const doc = await EditorController.promises.addDoc(
- projectId,
- folderId,
- name,
- lines,
- 'upload',
- userId
- )
- return doc
- }
- }
- async function addFile(userId, projectId, folderId, name, path, replace) {
- if (replace) {
- const file = await EditorController.promises.upsertFile(
- projectId,
- folderId,
- name,
- path,
- null,
- 'upload',
- userId
- )
- return file
- } else {
- const file = await EditorController.promises.addFile(
- projectId,
- folderId,
- name,
- path,
- null,
- 'upload',
- userId
- )
- return file
- }
- }
- async function addFolder(userId, projectId, folderId, name, path, replace) {
- const newFolder = await EditorController.promises.addFolder(
- projectId,
- folderId,
- name,
- 'upload',
- userId
- )
- await addFolderContents(userId, projectId, newFolder._id, path, replace)
- return newFolder
- }
- async function addFolderContents(
- userId,
- projectId,
- parentFolderId,
- folderPath,
- replace
- ) {
- if (!(await _isSafeOnFileSystem(folderPath))) {
- logger.log(
- { userId, projectId, parentFolderId, folderPath },
- 'add folder contents is from symlink, stopping insert'
- )
- throw new Error('path is symlink')
- }
- const entries = (await fs.promises.readdir(folderPath)) || []
- for (const entry of entries) {
- if (await FileTypeManager.promises.shouldIgnore(entry)) {
- continue
- }
- await addEntity(
- userId,
- projectId,
- parentFolderId,
- entry,
- `${folderPath}/${entry}`,
- replace
- )
- }
- }
- async function addEntity(userId, projectId, folderId, name, fsPath, replace) {
- if (!(await _isSafeOnFileSystem(fsPath))) {
- logger.log(
- { userId, projectId, folderId, fsPath },
- 'add entry is from symlink, stopping insert'
- )
- throw new Error('path is symlink')
- }
- if (await FileTypeManager.promises.isDirectory(fsPath)) {
- const newFolder = await addFolder(
- userId,
- projectId,
- folderId,
- name,
- fsPath,
- replace
- )
- return newFolder
- }
- // Here, we cheat a little bit and provide the project path relative to the
- // folder, not the root of the project. This is because we don't know for sure
- // at this point what the final path of the folder will be. The project path
- // is still important for importFile() to be able to figure out if the file is
- // a binary file or an editable document.
- const projectPath = Path.join('/', name)
- const importInfo = await importFile(fsPath, projectPath)
- switch (importInfo.type) {
- case 'file': {
- const entity = await addFile(
- userId,
- projectId,
- folderId,
- name,
- importInfo.fsPath,
- replace
- )
- if (entity != null) {
- entity.type = 'file'
- }
- return entity
- }
- case 'doc': {
- const entity = await addDoc(
- userId,
- projectId,
- folderId,
- name,
- importInfo.lines,
- replace
- )
- if (entity != null) {
- entity.type = 'doc'
- }
- return entity
- }
- default: {
- throw new Error(`unknown import type: ${importInfo.type}`)
- }
- }
- }
- async function _isSafeOnFileSystem(path) {
- // Use lstat() to ensure we don't follow symlinks. Symlinks from an
- // untrusted source are dangerous.
- const stat = await fs.promises.lstat(path)
- return stat.isFile() || stat.isDirectory()
- }
- async function importFile(fsPath, projectPath) {
- const stat = await fs.promises.lstat(fsPath)
- if (!stat.isFile()) {
- throw new Error(`can't import ${fsPath}: not a regular file`)
- }
- _validateProjectPath(projectPath)
- const filename = Path.basename(projectPath)
- const { binary, encoding } = await FileTypeManager.promises.getType(
- filename,
- fsPath
- )
- if (binary) {
- return new FileImport(projectPath, fsPath)
- } else {
- const content = await fs.promises.readFile(fsPath, encoding)
- // Handle Unix, DOS and classic Mac newlines
- const lines = content.split(/\r\n|\n|\r/)
- return new DocImport(projectPath, lines)
- }
- }
- async function importDir(dirPath) {
- const stat = await fs.promises.lstat(dirPath)
- if (!stat.isDirectory()) {
- throw new Error(`can't import ${dirPath}: not a directory`)
- }
- const entries = []
- for await (const filePath of _walkDir(dirPath)) {
- const projectPath = Path.join('/', Path.relative(dirPath, filePath))
- const importInfo = await importFile(filePath, projectPath)
- entries.push(importInfo)
- }
- return entries
- }
- function _validateProjectPath(path) {
- if (!SafePath.isAllowedLength(path) || !SafePath.isCleanPath(path)) {
- throw new Errors.InvalidNameError(`Invalid path: ${path}`)
- }
- }
- async function* _walkDir(dirPath) {
- const entries = await fs.promises.readdir(dirPath)
- for (const entry of entries) {
- const entryPath = Path.join(dirPath, entry)
- if (await FileTypeManager.promises.shouldIgnore(entryPath)) {
- continue
- }
- // Use lstat() to ensure we don't follow symlinks. Symlinks from an
- // untrusted source are dangerous.
- const stat = await fs.promises.lstat(entryPath)
- if (stat.isFile()) {
- yield entryPath
- } else if (stat.isDirectory()) {
- yield* _walkDir(entryPath)
- }
- }
- }
- class FileImport {
- constructor(projectPath, fsPath) {
- this.type = 'file'
- this.projectPath = projectPath
- this.fsPath = fsPath
- }
- }
- class DocImport {
- constructor(projectPath, lines) {
- this.type = 'doc'
- this.projectPath = projectPath
- this.lines = lines
- }
- }
|