Preserve folder structure when uploading folders (#16502)

GitOrigin-RevId: 791233ce1e68920a4f2d7042ed4c60ca3f4be8fb
This commit is contained in:
Alf Eaton 2024-02-15 09:48:48 +00:00 committed by Copybot
parent 3f29aa2195
commit f4b176c93d
15 changed files with 473 additions and 215 deletions

View file

@ -1,16 +1,3 @@
/* eslint-disable
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let ProjectUploadController
const logger = require('@overleaf/logger')
const metrics = require('@overleaf/metrics')
const fs = require('fs')
@ -18,13 +5,16 @@ const Path = require('path')
const FileSystemImportManager = require('./FileSystemImportManager')
const ProjectUploadManager = require('./ProjectUploadManager')
const SessionManager = require('../Authentication/SessionManager')
const EditorController = require('../Editor/EditorController')
const ProjectLocator = require('../Project/ProjectLocator')
const Settings = require('@overleaf/settings')
const { InvalidZipFileError } = require('./ArchiveErrors')
const multer = require('multer')
const _ = require('lodash')
const { defaultsDeep } = require('lodash')
const { expressify } = require('@overleaf/promise-utils')
const upload = multer(
_.defaultsDeep(
defaultsDeep(
{
dest: Settings.path.uploadFolder,
limits: {
@ -35,120 +25,141 @@ const upload = multer(
)
)
module.exports = ProjectUploadController = {
uploadProject(req, res, next) {
const timer = new metrics.Timer('project-upload')
const userId = SessionManager.getLoggedInUserId(req.session)
const { path } = req.file
const name = Path.basename(req.body.name, '.zip')
return ProjectUploadManager.createProjectFromZipArchive(
userId,
name,
path,
function (error, project) {
fs.unlink(path, function () {})
timer.done()
if (error != null) {
logger.error(
{ err: error, filePath: path, fileName: name },
'error uploading project'
)
if (error instanceof InvalidZipFileError) {
return res.status(422).json({
success: false,
error: req.i18n.translate(error.message),
})
} else {
return res.status(500).json({
success: false,
error: req.i18n.translate('upload_failed'),
})
}
function uploadProject(req, res, next) {
const timer = new metrics.Timer('project-upload')
const userId = SessionManager.getLoggedInUserId(req.session)
const { path } = req.file
const name = Path.basename(req.body.name, '.zip')
return ProjectUploadManager.createProjectFromZipArchive(
userId,
name,
path,
function (error, project) {
fs.unlink(path, function () {})
timer.done()
if (error != null) {
logger.error(
{ err: error, filePath: path, fileName: name },
'error uploading project'
)
if (error instanceof InvalidZipFileError) {
return res.status(422).json({
success: false,
error: req.i18n.translate(error.message),
})
} else {
return res.json({ success: true, project_id: project._id })
}
}
)
},
uploadFile(req, res, next) {
const timer = new metrics.Timer('file-upload')
const name = req.body.name
const path = req.file != null ? req.file.path : undefined
const projectId = req.params.Project_id
const { folder_id: folderId } = req.query
if (name == null || name.length === 0 || name.length > 150) {
return res.status(422).json({
success: false,
error: 'invalid_filename',
})
}
const userId = SessionManager.getLoggedInUserId(req.session)
return FileSystemImportManager.addEntity(
userId,
projectId,
folderId,
name,
path,
true,
function (error, entity) {
fs.unlink(path, function () {})
timer.done()
if (error != null) {
if (error.name === 'InvalidNameError') {
return res.status(422).json({
success: false,
error: 'invalid_filename',
})
} else if (error.message === 'project_has_too_many_files') {
return res.status(422).json({
success: false,
error: 'project_has_too_many_files',
})
} else if (error.message === 'folder_not_found') {
return res.status(422).json({
success: false,
error: 'folder_not_found',
})
} else {
logger.error(
{
err: error,
projectId,
filePath: path,
fileName: name,
folderId,
},
'error uploading file'
)
return res.status(422).json({ success: false })
}
} else {
return res.json({
success: true,
entity_id: entity != null ? entity._id : undefined,
entity_type: entity != null ? entity.type : undefined,
return res.status(500).json({
success: false,
error: req.i18n.translate('upload_failed'),
})
}
} else {
return res.json({ success: true, project_id: project._id })
}
)
},
multerMiddleware(req, res, next) {
if (upload == null) {
return res
.status(500)
.json({ success: false, error: req.i18n.translate('upload_failed') })
}
return upload.single('qqfile')(req, res, function (err) {
if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
return res
.status(422)
.json({ success: false, error: req.i18n.translate('file_too_large') })
}
return next(err)
})
},
)
}
async function uploadFile(req, res, next) {
const timer = new metrics.Timer('file-upload')
const name = req.body.name
const path = req.file?.path
const projectId = req.params.Project_id
let { folder_id: folderId } = req.query
if (name == null || name.length === 0 || name.length > 150) {
return res.status(422).json({
success: false,
error: 'invalid_filename',
})
}
// preserve the directory structure from an uploaded folder
const { relativePath } = req.body
// NOTE: Uppy sends a "null" string for `relativePath` when the file is not nested in a folder
if (relativePath && relativePath !== 'null') {
const { path } = await ProjectLocator.promises.findElement({
project_id: projectId,
element_id: folderId,
type: 'folder',
})
const { lastFolder } = await EditorController.promises.mkdirp(
projectId,
Path.dirname(Path.join('/', path.fileSystem, relativePath))
)
folderId = lastFolder._id
}
const userId = SessionManager.getLoggedInUserId(req.session)
return FileSystemImportManager.addEntity(
userId,
projectId,
folderId,
name,
path,
true,
function (error, entity) {
fs.unlink(path, function () {})
timer.done()
if (error != null) {
if (error.name === 'InvalidNameError') {
return res.status(422).json({
success: false,
error: 'invalid_filename',
})
} else if (error.message === 'project_has_too_many_files') {
return res.status(422).json({
success: false,
error: 'project_has_too_many_files',
})
} else if (error.message === 'folder_not_found') {
return res.status(422).json({
success: false,
error: 'folder_not_found',
})
} else {
logger.error(
{
err: error,
projectId,
filePath: path,
fileName: name,
folderId,
},
'error uploading file'
)
return res.status(422).json({ success: false })
}
} else {
return res.json({
success: true,
entity_id: entity?._id,
entity_type: entity?.type,
})
}
}
)
}
function multerMiddleware(req, res, next) {
if (upload == null) {
return res
.status(500)
.json({ success: false, error: req.i18n.translate('upload_failed') })
}
return upload.single('qqfile')(req, res, function (err) {
if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
return res
.status(422)
.json({ success: false, error: req.i18n.translate('file_too_large') })
}
return next(err)
})
}
module.exports = {
uploadProject,
uploadFile: expressify(uploadFile),
multerMiddleware,
}

View file

@ -403,6 +403,7 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
textExtensions: Settings.textExtensions,
editableFilenames: Settings.editableFilenames,
validRootDocExtensions: Settings.validRootDocExtensions,
fileIgnorePattern: Settings.fileIgnorePattern,
sentryAllowedOriginRegex: Settings.sentry.allowedOriginRegex,
sentryDsn: Settings.sentry.publicDSN,
sentryEnvironment: Settings.sentry.environment,

View file

@ -2,4 +2,6 @@ window.i18n = { currentLangCode: 'en' }
window.ExposedSettings = {
appName: 'Overleaf',
validRootDocExtensions: ['tex', 'Rtex', 'ltx', 'Rnw'],
fileIgnorePattern:
'**/{{__MACOSX,.git,.texpadtmp,.R}{,/**},.!(latexmkrc),*.{dvi,aux,log,toc,out,pdfsync,synctex,synctex(busy),fdb_latexmk,fls,nlo,ind,glo,gls,glg,bbl,blg,doc,docx,gz,swp}}',
} as typeof window.ExposedSettings

View file

@ -284,6 +284,8 @@
"display_deleted_user": "",
"do_this_later": "",
"do_you_want_to_change_your_primary_email_address_to": "",
"do_you_want_to_overwrite_it": "",
"do_you_want_to_overwrite_it_plural": "",
"do_you_want_to_overwrite_them": "",
"document_too_long": "",
"document_too_long_detail": "",
@ -851,6 +853,7 @@
"overleaf_labs": "",
"overview": "",
"overwrite": "",
"overwriting_the_original_folder": "",
"owned_by_x": "",
"owner": "",
"page_current": "",
@ -1281,6 +1284,8 @@
"thanks_for_subscribing_you_help_sl": "",
"thanks_settings_updated": "",
"the_following_files_already_exist_in_this_project": "",
"the_following_folder_already_exists_in_this_project": "",
"the_following_folder_already_exists_in_this_project_plural": "",
"the_target_folder_could_not_be_found": "",
"the_width_you_choose_here_is_based_on_the_width_of_the_text_in_your_document": "",
"their_projects_will_be_transferred_to_another_user": "",

View file

@ -0,0 +1,135 @@
import { FileTreeEntity } from '../../../../../../types/file-tree-entity'
import { useTranslation } from 'react-i18next'
import { useProjectContext } from '@/shared/context/project-context'
import { useCallback } from 'react'
import { syncDelete } from '@/features/file-tree/util/sync-mutation'
import { Button } from 'react-bootstrap'
export function UploadConflicts({
cancel,
conflicts,
folderConflicts,
handleOverwrite,
setError,
}: {
cancel: () => void
conflicts: FileTreeEntity[]
folderConflicts: FileTreeEntity[]
handleOverwrite: () => void
setError: (error: string) => void
}) {
const { t } = useTranslation()
// ensure that no uploads happen while there are folder conflicts
if (folderConflicts.length > 0) {
return (
<FolderUploadConflicts
cancel={cancel}
folderConflicts={folderConflicts}
handleOverwrite={handleOverwrite}
setError={setError}
/>
)
}
return (
<div className="small modal-new-file--body-conflict">
{conflicts.length > 0 && (
<>
<p className="text-center mb-0">
{t('the_following_files_already_exist_in_this_project')}
</p>
<ul className="text-center list-unstyled row-spaced-small mt-1">
{conflicts.map((conflict, index) => (
<li key={index}>
<strong>{conflict.name}</strong>
</li>
))}
</ul>
</>
)}
<p className="text-center row-spaced-small">
{t('do_you_want_to_overwrite_them')}
</p>
<p className="text-center">
<Button bsStyle={null} className="btn-secondary" onClick={cancel}>
{t('cancel')}
</Button>
&nbsp;
<Button bsStyle="danger" onClick={handleOverwrite}>
{t('overwrite')}
</Button>
</p>
</div>
)
}
function FolderUploadConflicts({
cancel,
handleOverwrite,
folderConflicts,
setError,
}: {
cancel: () => void
handleOverwrite: () => void
folderConflicts: FileTreeEntity[]
setError: (error: string) => void
}) {
const { t } = useTranslation()
const { _id: projectId } = useProjectContext()
const deleteAndRetry = useCallback(async () => {
// TODO: confirm deletion?
try {
await Promise.all(
folderConflicts.map(
entity => syncDelete(projectId, 'folder', entity._id) // TODO: might be a file!
)
)
handleOverwrite()
} catch (error: any) {
setError(error.message)
}
}, [setError, folderConflicts, handleOverwrite, projectId])
return (
<div className="small modal-new-file--body-conflict">
<p className="text-center mb-0">
{t('the_following_folder_already_exists_in_this_project', {
count: folderConflicts.length,
})}
</p>
<ul className="text-center list-unstyled row-spaced-small mt-1">
{folderConflicts.map((entity, index) => (
<li key={index}>
<strong>{entity.name}</strong>
</li>
))}
</ul>
<p className="text-center row-spaced-small">
{t('overwriting_the_original_folder')}
<br />
{t('do_you_want_to_overwrite_it', {
count: folderConflicts.length,
})}
</p>
<p className="text-center">
<Button bsStyle={null} className="btn-secondary" onClick={cancel}>
{t('cancel')}
</Button>
&nbsp;
<Button bsStyle="danger" onClick={deleteAndRetry}>
{t('overwrite')}
</Button>
</p>
</div>
)
}

View file

@ -1,37 +1,80 @@
import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import Uppy from '@uppy/core'
import XHRUpload from '@uppy/xhr-upload'
import { Dashboard } from '@uppy/react'
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
import { useProjectContext } from '../../../../../shared/context/project-context'
import * as eventTracking from '../../../../../infrastructure/event-tracking'
import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css'
import { refreshProjectMetadata } from '../../../util/api'
import ErrorMessage from '../error-message'
import { debugConsole } from '@/utils/debugging'
import { isAcceptableFile } from '@/features/file-tree/util/is-acceptable-file'
import { findByNameInFolder } from '@/features/file-tree/util/is-name-unique-in-folder'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { FileTreeEntity } from '../../../../../../../types/file-tree-entity'
import { UploadConflicts } from '@/features/file-tree/components/file-tree-create/file-tree-upload-conflicts'
export default function FileTreeUploadDoc() {
const { parentFolderId, cancel, isDuplicate, droppedFiles, setDroppedFiles } =
const { parentFolderId, cancel, droppedFiles, setDroppedFiles } =
useFileTreeActionable()
const { fileTreeData } = useFileTreeData()
const { _id: projectId } = useProjectContext()
const [error, setError] = useState<string>()
const [conflicts, setConflicts] = useState<any[]>([])
const [conflicts, setConflicts] = useState<FileTreeEntity[]>([])
const [folderConflicts, setFolderConflicts] = useState<FileTreeEntity[]>([])
const [overwrite, setOverwrite] = useState(false)
const maxNumberOfFiles = 40
const maxFileSize = window.ExposedSettings.maxUploadSize
// calculate conflicts
const buildConflicts = (files: Record<string, any>) =>
Object.values(files).filter(file =>
isDuplicate(file.meta.targetFolderId ?? parentFolderId, file.meta.name)
)
const buildConflicts = (files: Record<string, any>) => {
const conflicts = new Set<FileTreeEntity>()
for (const file of Object.values(files)) {
const { name, relativePath } = file.meta
if (!relativePath) {
const targetFolderId = file.meta.targetFolderId ?? parentFolderId
const duplicate = findByNameInFolder(fileTreeData, targetFolderId, name)
if (duplicate) {
conflicts.add(duplicate)
}
}
}
return [...conflicts]
}
const buildFolderConflicts = (files: Record<string, any>) => {
const conflicts = new Set<FileTreeEntity>()
for (const file of Object.values(files)) {
const { relativePath } = file.meta
if (relativePath) {
const [rootName] = relativePath.replace(/^\//, '').split('/')
if (!conflicts.has(rootName)) {
const targetFolderId = file.meta.targetFolderId ?? parentFolderId
const duplicateEntity = findByNameInFolder(
fileTreeData,
targetFolderId,
rootName
)
if (duplicateEntity) {
conflicts.add(duplicateEntity)
}
}
}
}
return [...conflicts]
}
const buildEndpoint = (projectId: string, targetFolderId: string) => {
let endpoint = `/project/${projectId}/upload`
@ -43,6 +86,11 @@ export default function FileTreeUploadDoc() {
return endpoint
}
const overwriteRef = useRef(overwrite)
useEffect(() => {
overwriteRef.current = overwrite
}, [overwrite])
// initialise the Uppy object
const [uppy] = useState(() => {
const endpoint = buildEndpoint(projectId, parentFolderId)
@ -55,24 +103,21 @@ export default function FileTreeUploadDoc() {
maxNumberOfFiles,
maxFileSize: maxFileSize || null,
},
onBeforeUpload: files => {
let result = true
setOverwrite(overwrite => {
if (!overwrite) {
setConflicts(() => {
const conflicts = buildConflicts(files)
result = conflicts.length === 0
return conflicts
})
}
return overwrite
})
return result
onBeforeFileAdded(file) {
if (!isAcceptableFile(file)) {
return false
}
},
onBeforeUpload(files) {
if (overwriteRef.current) {
return true
} else {
const conflicts = buildConflicts(files)
const folderConflicts = buildFolderConflicts(files)
setConflicts(conflicts)
setFolderConflicts(folderConflicts)
return conflicts.length === 0 && folderConflicts.length === 0
}
},
autoProceed: true,
})
@ -135,6 +180,7 @@ export default function FileTreeUploadDoc() {
source: 'Local',
isRemote: false,
meta: {
relativePath: file.relativePath,
targetFolderId: droppedFiles.targetFolderId,
},
})
@ -163,7 +209,8 @@ export default function FileTreeUploadDoc() {
}, [uppy])
// whether to show a message about conflicting files
const showConflicts = !overwrite && conflicts.length > 0
const showConflicts =
!overwrite && (conflicts.length > 0 || folderConflicts.length > 0)
return (
<>
@ -175,7 +222,9 @@ export default function FileTreeUploadDoc() {
<UploadConflicts
cancel={cancel}
conflicts={conflicts}
folderConflicts={folderConflicts}
handleOverwrite={handleOverwrite}
setError={setError}
/>
) : (
<Dashboard
@ -186,16 +235,19 @@ export default function FileTreeUploadDoc() {
width="100%"
showLinkToFileUploadResult={false}
proudlyDisplayPoweredByUppy={false}
// allow files or folders to be selected
fileManagerSelectionType="both"
locale={{
strings: {
// Text to show on the droppable area.
// `%{browse}` is replaced with a link that opens the system file selection dialog.
// TODO: 'drag_here' or 'drop_files_here_to_upload'?
// dropHereOr: `${t('drag_here')} ${t('or')} %{browse}`,
dropPasteFiles: `Drag here, paste an image or file, or %{browseFiles}`,
dropPasteBoth: `Drop or paste your files, folder, or images here. %{browseFiles} or %{browseFolders} from your computer.`,
// Used as the label for the link that opens the system file selection dialog.
// browseFiles: t('select_from_your_computer')
browseFiles: 'select from your computer',
browseFiles: 'Select files',
browseFolders: 'select a folder',
},
}}
/>
@ -227,45 +279,3 @@ function UploadErrorMessage({
return <ErrorMessage error={error} />
}
}
function UploadConflicts({
cancel,
conflicts,
handleOverwrite,
}: {
cancel: () => void
conflicts: any[]
handleOverwrite: () => void
}) {
const { t } = useTranslation()
return (
<div className="small modal-new-file--body-conflict">
<p className="text-center mb-0">
{t('the_following_files_already_exist_in_this_project')}
</p>
<ul className="text-center list-unstyled row-spaced-small mt-1">
{conflicts.map((conflict, index) => (
<li key={index}>
<strong>{conflict.meta.name}</strong>
</li>
))}
</ul>
<p className="text-center row-spaced-small">
{t('do_you_want_to_overwrite_them')}
</p>
<p className="text-center">
<Button bsStyle={null} className="btn-secondary" onClick={cancel}>
{t('cancel')}
</Button>
&nbsp;
<Button bsStyle="danger" onClick={handleOverwrite}>
{t('overwrite')}
</Button>
</p>
</div>
)
}

View file

@ -15,6 +15,7 @@ import { useFileTreeActionable } from './file-tree-actionable'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
import { useEditorContext } from '@/shared/context/editor-context'
import { isAcceptableFile } from '@/features/file-tree/util/is-acceptable-file'
const DRAGGABLE_TYPE = 'ENTITY'
export const FileTreeDraggableProvider: FC<{
@ -120,10 +121,12 @@ export function useDroppable(targetEntityId: string) {
}
// native file(s) dragged in from outside
getDroppedFiles(item as unknown as DataTransfer).then(files => {
setDroppedFiles({ files, targetFolderId: targetEntityId })
startUploadingDocOrFile()
})
getDroppedFiles(item as unknown as DataTransfer)
.then(files => files.filter(isAcceptableFile))
.then(files => {
setDroppedFiles({ files, targetFolderId: targetEntityId })
startUploadingDocOrFile()
})
},
collect(monitor) {
return {

View file

@ -0,0 +1,9 @@
import { Minimatch } from 'minimatch'
const fileIgnoreMatcher = new Minimatch(
window.ExposedSettings.fileIgnorePattern,
{ nocase: true, dot: true }
)
export const isAcceptableFile = (file: { name: string }) =>
!fileIgnoreMatcher.match(file.name)

View file

@ -1,13 +0,0 @@
import { findInTree } from '../util/find-in-tree'
export function isNameUniqueInFolder(tree, parentFolderId, name) {
if (tree._id !== parentFolderId) {
tree = findInTree(tree, parentFolderId).entity
}
if (tree.docs.some(entity => entity.name === name)) return false
if (tree.fileRefs.some(entity => entity.name === name)) return false
if (tree.folders.some(entity => entity.name === name)) return false
return true
}

View file

@ -0,0 +1,28 @@
import { findInTree } from '../util/find-in-tree'
import { Folder } from '../../../../../types/folder'
import { Doc } from '../../../../../types/doc'
import { FileRef } from '../../../../../types/file-ref'
export function isNameUniqueInFolder(
tree: Folder,
parentFolderId: string,
name: string
): boolean {
return !findByNameInFolder(tree, parentFolderId, name)
}
export function findByNameInFolder(
tree: Folder,
parentFolderId: string,
name: string
): Doc | FileRef | Folder | undefined {
if (tree._id !== parentFolderId) {
tree = findInTree(tree, parentFolderId).entity
}
return (
tree.docs.find(entity => entity.name === name) ||
tree.fileRefs.find(entity => entity.name === name) ||
tree.folders.find(entity => entity.name === name)
)
}

View file

@ -194,6 +194,8 @@ const initialize = () => {
],
editableFilenames: ['latexmkrc', '.latexmkrc', 'makefile', 'gnumakefile'],
validRootDocExtensions: ['tex', 'Rtex', 'ltx', 'Rnw'],
fileIgnorePattern:
'**/{{__MACOSX,.git,.texpadtmp,.R}{,/**},.!(latexmkrc),*.{dvi,aux,log,toc,out,pdfsync,synctex,synctex(busy),fdb_latexmk,fls,nlo,ind,glo,gls,glg,bbl,blg,doc,docx,gz,swp}}',
projectUploadTimeout: 12000,
}

View file

@ -534,4 +534,8 @@
font-size: inherit;
}
}
.uppy-Dashboard-AddFiles-title {
width: 26em; // sized to create a wrap between the sentences
}
}

View file

@ -421,6 +421,8 @@
"do_not_link_accounts": "Dont link accounts",
"do_this_later": "Ill do this later",
"do_you_want_to_change_your_primary_email_address_to": "Do you want to change your primary email address to <b>__email__</b>?",
"do_you_want_to_overwrite_it": "Do you want to overwrite it?",
"do_you_want_to_overwrite_it_plural": "Do you want to overwrite them?",
"do_you_want_to_overwrite_them": "Do you want to overwrite them?",
"document_history": "Document history",
"document_too_long": "Document Too Long",
@ -1283,6 +1285,7 @@
"overleaf_labs": "Overleaf Labs",
"overview": "Overview",
"overwrite": "Overwrite",
"overwriting_the_original_folder": "Overwriting the original folder will delete it and all the files it contains.",
"owned_by_x": "owned by __x__",
"owner": "Owner",
"page_current": "Page __page__, Current Page",
@ -1882,6 +1885,8 @@
"the_easy_online_collab_latex_editor": "The easy to use, online, collaborative LaTeX editor",
"the_file_supplied_is_of_an_unsupported_type ": "The link to open this content on Overleaf pointed to the wrong kind of file. Valid file types are .tex documents and .zip files. If this keeps happening for links on a particular site, please report this to them.",
"the_following_files_already_exist_in_this_project": "The following files already exist in this project:",
"the_following_folder_already_exists_in_this_project": "The following folder already exists in this project:",
"the_following_folder_already_exists_in_this_project_plural": "The following folders already exist in this project:",
"the_project_that_contains_this_file_is_not_shared_with_you": "The project that contains this file is not shared with you",
"the_requested_conversion_job_was_not_found": "The link to open this content on Overleaf specified a conversion job that could not be found. Its possible that the job has expired and needs to be run again. If this keeps happening for links on a particular site, please report this to them.",
"the_requested_publisher_was_not_found": "The link to open this content on Overleaf specified a publisher that could not be found. If this keeps happening for links on a particular site, please report this to them.",

View file

@ -40,6 +40,12 @@ describe('ProjectUploadController', function () {
this.SessionManager = {
getLoggedInUserId: sinon.stub().returns(this.user_id),
}
this.ProjectLocator = {
promises: {},
}
this.EditorController = {
promises: {},
}
return (this.ProjectUploadController = SandboxedModule.require(modulePath, {
requires: {
@ -50,6 +56,8 @@ describe('ProjectUploadController', function () {
'@overleaf/metrics': this.metrics,
'../Authentication/SessionManager': this.SessionManager,
'./ArchiveErrors': ArchiveErrors,
'../Project/ProjectLocator': this.ProjectLocator,
'../Editor/EditorController': this.EditorController,
fs: (this.fs = {}),
},
}))
@ -224,6 +232,53 @@ describe('ProjectUploadController', function () {
})
})
describe('with folder structure', function () {
beforeEach(function (done) {
this.entity = {
_id: '1234',
type: 'file',
}
this.FileSystemImportManager.addEntity = sinon
.stub()
.callsArgWith(6, null, this.entity)
this.ProjectLocator.promises.findElement = sinon.stub().resolves({
path: { fileSystem: '/test' },
})
this.EditorController.promises.mkdirp = sinon.stub().resolves({
lastFolder: { _id: 'folder-id' },
})
this.req.body.relativePath = 'foo/bar/' + this.name
this.res.json = data => {
expect(data.success).to.be.true
done()
}
this.ProjectUploadController.uploadFile(this.req, this.res)
})
it('should insert the file', function () {
this.ProjectLocator.promises.findElement.should.be.calledOnceWithExactly(
{
project_id: this.project_id,
element_id: this.folder_id,
type: 'folder',
}
)
this.EditorController.promises.mkdirp.should.be.calledWith(
this.project_id,
'/test/foo/bar'
)
this.FileSystemImportManager.addEntity.should.be.calledOnceWith(
this.user_id,
this.project_id,
'folder-id',
this.name,
this.path
)
})
})
describe('when FileSystemImportManager.addEntity returns a generic error', function () {
beforeEach(function () {
this.FileSystemImportManager.addEntity = sinon

View file

@ -42,6 +42,7 @@ export type ExposedSettings = {
textExtensions: string[]
editableFilenames: string[]
validRootDocExtensions: string[]
fileIgnorePattern: string
templateLinks?: TemplateLink[]
labsEnabled: boolean
}