mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #655 from sharelatex/sk-linked-files-output-redux
Linked files from project output
This commit is contained in:
commit
f6424ada40
21 changed files with 1159 additions and 583 deletions
|
@ -182,6 +182,14 @@ module.exports = ClsiManager =
|
|||
return callback(error) if error?
|
||||
callback(null, projectStateHash, docs)
|
||||
|
||||
getOutputFileStream: (project_id, user_id, build_id, output_file_path, callback=(err, readStream)->) ->
|
||||
url = "#{Settings.apis.clsi.url}/project/#{project_id}/user/#{user_id}/build/#{build_id}/output/#{output_file_path}"
|
||||
ClsiCookieManager.getCookieJar project_id, (err, jar)->
|
||||
return callback(err) if err?
|
||||
options = { url: url, method: "GET", timeout: 60 * 1000, jar : jar }
|
||||
readStream = request(options)
|
||||
callback(null, readStream)
|
||||
|
||||
_buildRequestFromDocupdater: (project_id, options, project, projectStateHash, docUpdaterDocs, callback = (error, request) ->) ->
|
||||
ProjectEntityHandler.getAllDocPathsFromProject project, (error, docPath) ->
|
||||
return callback(error) if error?
|
||||
|
|
|
@ -4,11 +4,27 @@ ProjectLocator = require '../Project/ProjectLocator'
|
|||
Settings = require 'settings-sharelatex'
|
||||
logger = require 'logger-sharelatex'
|
||||
_ = require 'underscore'
|
||||
LinkedFilesHandler = require './LinkedFilesHandler'
|
||||
{
|
||||
|
||||
UrlFetchFailedError,
|
||||
InvalidUrlError,
|
||||
OutputFileFetchFailedError,
|
||||
AccessDeniedError,
|
||||
BadEntityTypeError,
|
||||
BadDataError,
|
||||
ProjectNotFoundError,
|
||||
V1ProjectNotFoundError,
|
||||
SourceFileNotFoundError,
|
||||
} = require './LinkedFilesErrors'
|
||||
|
||||
|
||||
module.exports = LinkedFilesController = {
|
||||
|
||||
Agents: {
|
||||
url: require('./UrlAgent'),
|
||||
project_file: require('./ProjectFileAgent')
|
||||
project_file: require('./ProjectFileAgent'),
|
||||
project_output_file: require('./ProjectOutputFileAgent')
|
||||
}
|
||||
|
||||
_getAgent: (provider) ->
|
||||
|
@ -18,15 +34,6 @@ module.exports = LinkedFilesController = {
|
|||
return null
|
||||
LinkedFilesController.Agents[provider]
|
||||
|
||||
_getFileById: (project_id, file_id, callback=(err, file)->) ->
|
||||
ProjectLocator.findElement {
|
||||
project_id,
|
||||
element_id: file_id,
|
||||
type: 'file'
|
||||
}, (err, file, path, parentFolder) ->
|
||||
return callback(err) if err?
|
||||
callback(null, file, path, parentFolder)
|
||||
|
||||
createLinkedFile: (req, res, next) ->
|
||||
{project_id} = req.params
|
||||
{name, provider, data, parent_folder_id} = req.body
|
||||
|
@ -37,23 +44,23 @@ module.exports = LinkedFilesController = {
|
|||
if !Agent?
|
||||
return res.sendStatus(400)
|
||||
|
||||
linkedFileData = Agent.sanitizeData(data)
|
||||
linkedFileData.provider = provider
|
||||
data.provider = provider
|
||||
|
||||
if !Agent.canCreate(linkedFileData)
|
||||
return res.status(403).send('Cannot create linked file')
|
||||
|
||||
LinkedFilesController._doImport(
|
||||
req, res, next, Agent, project_id, user_id,
|
||||
parent_folder_id, name, linkedFileData
|
||||
)
|
||||
Agent.createLinkedFile project_id,
|
||||
data,
|
||||
name,
|
||||
parent_folder_id,
|
||||
user_id,
|
||||
(err, newFileId) ->
|
||||
return LinkedFilesController.handleError(err, req, res, next) if err?
|
||||
res.json(new_file_id: newFileId)
|
||||
|
||||
refreshLinkedFile: (req, res, next) ->
|
||||
{project_id, file_id} = req.params
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {project_id, file_id, user_id}, 'refresh linked file request'
|
||||
|
||||
LinkedFilesController._getFileById project_id, file_id, (err, file, path, parentFolder) ->
|
||||
LinkedFilesHandler.getFileById project_id, file_id, (err, file, path, parentFolder) ->
|
||||
return next(err) if err?
|
||||
return res.sendStatus(404) if !file?
|
||||
name = file.name
|
||||
|
@ -65,37 +72,51 @@ module.exports = LinkedFilesController = {
|
|||
Agent = LinkedFilesController._getAgent(provider)
|
||||
if !Agent?
|
||||
return res.sendStatus(400)
|
||||
LinkedFilesController._doImport(
|
||||
req, res, next, Agent, project_id, user_id,
|
||||
parent_folder_id, name, linkedFileData
|
||||
|
||||
Agent.refreshLinkedFile project_id,
|
||||
linkedFileData,
|
||||
name,
|
||||
parent_folder_id,
|
||||
user_id,
|
||||
(err, newFileId) ->
|
||||
return LinkedFilesController.handleError(err, req, res, next) if err?
|
||||
res.json(new_file_id: newFileId)
|
||||
|
||||
handleError: (error, req, res, next) ->
|
||||
if error instanceof BadDataError
|
||||
res.status(400).send("The submitted data is not valid")
|
||||
|
||||
else if error instanceof AccessDeniedError
|
||||
res.status(403).send("You do not have access to this project")
|
||||
|
||||
else if error instanceof BadDataError
|
||||
res.status(400).send("The submitted data is not valid")
|
||||
|
||||
else if error instanceof BadEntityTypeError
|
||||
res.status(400).send("The file is the wrong type")
|
||||
|
||||
else if error instanceof SourceFileNotFoundError
|
||||
res.status(404).send("Source file not found")
|
||||
|
||||
else if error instanceof ProjectNotFoundError
|
||||
res.status(404).send("Project not found")
|
||||
|
||||
else if error instanceof V1ProjectNotFoundError
|
||||
res.status(409).send("Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file")
|
||||
|
||||
else if error instanceof OutputFileFetchFailedError
|
||||
res.status(404).send("Could not get output file")
|
||||
|
||||
else if error instanceof UrlFetchFailedError
|
||||
res.status(422).send(
|
||||
"Your URL could not be reached (#{error.statusCode} status code). Please check it and try again."
|
||||
)
|
||||
|
||||
_doImport: (req, res, next, Agent, project_id, user_id, parent_folder_id, name, linkedFileData) ->
|
||||
Agent.checkAuth project_id, linkedFileData, user_id, (err, allowed) ->
|
||||
return Agent.handleError(err, req, res, next) if err?
|
||||
return res.sendStatus(403) if !allowed
|
||||
Agent.decorateLinkedFileData linkedFileData, (err, newLinkedFileData) ->
|
||||
return Agent.handleError(err) if err?
|
||||
linkedFileData = newLinkedFileData
|
||||
Agent.writeIncomingFileToDisk project_id,
|
||||
linkedFileData,
|
||||
user_id,
|
||||
(error, fsPath) ->
|
||||
if error?
|
||||
logger.error(
|
||||
{err: error, project_id, name, linkedFileData, parent_folder_id, user_id},
|
||||
'error writing linked file to disk'
|
||||
)
|
||||
return Agent.handleError(error, req, res, next)
|
||||
EditorController.upsertFile project_id,
|
||||
parent_folder_id,
|
||||
name,
|
||||
fsPath,
|
||||
linkedFileData,
|
||||
"upload",
|
||||
user_id,
|
||||
(error, file) ->
|
||||
return next(error) if error?
|
||||
res.json(new_file_id: file._id) # created
|
||||
else if error instanceof InvalidUrlError
|
||||
res.status(422).send(
|
||||
"Your URL is not valid. Please check it and try again."
|
||||
)
|
||||
|
||||
}
|
||||
else
|
||||
next(error)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
UrlFetchFailedError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'UrlFetchFailedError'
|
||||
error.__proto__ = UrlFetchFailedError.prototype
|
||||
return error
|
||||
UrlFetchFailedError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
InvalidUrlError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'InvalidUrlError'
|
||||
error.__proto__ = InvalidUrlError.prototype
|
||||
return error
|
||||
InvalidUrlError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
OutputFileFetchFailedError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'OutputFileFetchFailedError'
|
||||
error.__proto__ = OutputFileFetchFailedError.prototype
|
||||
return error
|
||||
OutputFileFetchFailedError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
AccessDeniedError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'AccessDenied'
|
||||
error.__proto__ = AccessDeniedError.prototype
|
||||
return error
|
||||
AccessDeniedError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
BadEntityTypeError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadEntityType'
|
||||
error.__proto__ = BadEntityTypeError.prototype
|
||||
return error
|
||||
BadEntityTypeError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
BadDataError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadData'
|
||||
error.__proto__ = BadDataError.prototype
|
||||
return error
|
||||
BadDataError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
ProjectNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'ProjectNotFound'
|
||||
error.__proto__ = ProjectNotFoundError.prototype
|
||||
return error
|
||||
ProjectNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
V1ProjectNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'V1ProjectNotFound'
|
||||
error.__proto__ = V1ProjectNotFoundError.prototype
|
||||
return error
|
||||
V1ProjectNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
SourceFileNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'SourceFileNotFound'
|
||||
error.__proto__ = SourceFileNotFoundError.prototype
|
||||
return error
|
||||
SourceFileNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
module.exports = {
|
||||
|
||||
UrlFetchFailedError,
|
||||
InvalidUrlError,
|
||||
OutputFileFetchFailedError,
|
||||
AccessDeniedError,
|
||||
BadEntityTypeError,
|
||||
BadDataError,
|
||||
ProjectNotFoundError,
|
||||
V1ProjectNotFoundError,
|
||||
SourceFileNotFoundError,
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
FileWriter = require '../../infrastructure/FileWriter'
|
||||
EditorController = require '../Editor/EditorController'
|
||||
ProjectLocator = require '../Project/ProjectLocator'
|
||||
Project = require("../../models/Project").Project
|
||||
ProjectGetter = require("../Project/ProjectGetter")
|
||||
_ = require 'underscore'
|
||||
{
|
||||
ProjectNotFoundError,
|
||||
V1ProjectNotFoundError,
|
||||
BadDataError
|
||||
} = require './LinkedFilesErrors'
|
||||
|
||||
|
||||
module.exports = LinkedFilesHandler =
|
||||
|
||||
getFileById: (project_id, file_id, callback=(err, file)->) ->
|
||||
ProjectLocator.findElement {
|
||||
project_id,
|
||||
element_id: file_id,
|
||||
type: 'file'
|
||||
}, (err, file, path, parentFolder) ->
|
||||
return callback(err) if err?
|
||||
callback(null, file, path, parentFolder)
|
||||
|
||||
getSourceProject: (data, callback=(err, project)->) ->
|
||||
projection = {_id: 1, name: 1}
|
||||
if data.v1_source_doc_id?
|
||||
Project.findOne {'overleaf.id': data.v1_source_doc_id}, projection, (err, project) ->
|
||||
return callback(err) if err?
|
||||
if !project?
|
||||
return callback(new V1ProjectNotFoundError())
|
||||
callback(null, project)
|
||||
else if data.source_project_id?
|
||||
ProjectGetter.getProject data.source_project_id, projection, (err, project) ->
|
||||
return callback(err) if err?
|
||||
if !project?
|
||||
return callback(new ProjectNotFoundError())
|
||||
callback(null, project)
|
||||
else
|
||||
callback(new BadDataError('neither v1 nor v2 id present'))
|
||||
|
||||
importFromStream: (
|
||||
project_id,
|
||||
readStream,
|
||||
linkedFileData,
|
||||
name,
|
||||
parent_folder_id,
|
||||
user_id,
|
||||
callback=(err, file)->
|
||||
) ->
|
||||
callback = _.once(callback)
|
||||
FileWriter.writeStreamToDisk project_id, readStream, (err, fsPath) ->
|
||||
return callback(err) if err?
|
||||
EditorController.upsertFile project_id,
|
||||
parent_folder_id,
|
||||
name,
|
||||
fsPath,
|
||||
linkedFileData,
|
||||
"upload",
|
||||
user_id,
|
||||
(err, file) =>
|
||||
return callback(err) if err?
|
||||
callback(null, file)
|
||||
|
||||
importContent: (
|
||||
project_id,
|
||||
content,
|
||||
linkedFileData,
|
||||
name,
|
||||
parent_folder_id,
|
||||
user_id,
|
||||
callback=(err, file)->
|
||||
) ->
|
||||
callback = _.once(callback)
|
||||
FileWriter.writeContentToDisk project_id, content, (err, fsPath) ->
|
||||
return callback(err) if err?
|
||||
EditorController.upsertFile project_id,
|
||||
parent_folder_id,
|
||||
name,
|
||||
fsPath,
|
||||
linkedFileData,
|
||||
"upload",
|
||||
user_id,
|
||||
(err, file) =>
|
||||
return callback(err) if err?
|
||||
callback(null, file)
|
|
@ -1,68 +1,94 @@
|
|||
FileWriter = require('../../infrastructure/FileWriter')
|
||||
AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
ProjectLocator = require('../Project/ProjectLocator')
|
||||
ProjectGetter = require('../Project/ProjectGetter')
|
||||
Project = require("../../models/Project").Project
|
||||
DocstoreManager = require('../Docstore/DocstoreManager')
|
||||
FileStoreHandler = require('../FileStore/FileStoreHandler')
|
||||
FileWriter = require('../../infrastructure/FileWriter')
|
||||
_ = require "underscore"
|
||||
Settings = require 'settings-sharelatex'
|
||||
LinkedFilesHandler = require './LinkedFilesHandler'
|
||||
{
|
||||
BadDataError,
|
||||
AccessDeniedError,
|
||||
BadEntityTypeError,
|
||||
SourceFileNotFoundError,
|
||||
ProjectNotFoundError,
|
||||
V1ProjectNotFoundError
|
||||
} = require './LinkedFilesErrors'
|
||||
|
||||
module.exports = ProjectFileAgent = {
|
||||
|
||||
AccessDeniedError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'AccessDenied'
|
||||
error.__proto__ = AccessDeniedError.prototype
|
||||
return error
|
||||
AccessDeniedError.prototype.__proto__ = Error.prototype
|
||||
createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
|
||||
if !@_canCreate(linkedFileData)
|
||||
return callback(new AccessDeniedError())
|
||||
@_go(project_id, linkedFileData, name, parent_folder_id, user_id, callback)
|
||||
|
||||
refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
|
||||
@_go project_id, linkedFileData, name, parent_folder_id, user_id, callback
|
||||
|
||||
BadEntityTypeError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadEntityType'
|
||||
error.__proto__ = BadEntityTypeError.prototype
|
||||
return error
|
||||
BadEntityTypeError.prototype.__proto__ = Error.prototype
|
||||
_prepare: (project_id, linkedFileData, user_id, callback=(err, linkedFileData)->) ->
|
||||
@_checkAuth project_id, linkedFileData, user_id, (err, allowed) =>
|
||||
return callback(err) if err?
|
||||
return callback(new AccessDeniedError()) if !allowed
|
||||
if !@_validate(linkedFileData)
|
||||
return callback(new BadDataError())
|
||||
callback(null, linkedFileData)
|
||||
|
||||
_go: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
|
||||
linkedFileData = @_sanitizeData(linkedFileData)
|
||||
@_prepare project_id, linkedFileData, user_id, (err, linkedFileData) =>
|
||||
return callback(err) if err?
|
||||
if !@_validate(linkedFileData)
|
||||
return callback(new BadDataError())
|
||||
@_getEntity linkedFileData, user_id, (err, source_project, entity, type) =>
|
||||
return callback(err) if err?
|
||||
if type == 'doc'
|
||||
DocstoreManager.getDoc source_project._id, entity._id, (err, lines) ->
|
||||
return callback(err) if err?
|
||||
LinkedFilesHandler.importContent project_id,
|
||||
lines.join('\n'),
|
||||
linkedFileData,
|
||||
name,
|
||||
parent_folder_id,
|
||||
user_id,
|
||||
(err, file) ->
|
||||
return callback(err) if err?
|
||||
callback(null, file._id) # Created
|
||||
else if type == 'file'
|
||||
FileStoreHandler.getFileStream source_project._id, entity._id, null, (err, fileStream) ->
|
||||
return callback(err) if err?
|
||||
LinkedFilesHandler.importFromStream project_id,
|
||||
fileStream,
|
||||
linkedFileData,
|
||||
name,
|
||||
parent_folder_id,
|
||||
user_id,
|
||||
(err, file) ->
|
||||
return callback(err) if err?
|
||||
callback(null, file._id) # Created
|
||||
else
|
||||
callback(new BadEntityTypeError())
|
||||
|
||||
BadDataError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadData'
|
||||
error.__proto__ = BadDataError.prototype
|
||||
return error
|
||||
BadDataError.prototype.__proto__ = Error.prototype
|
||||
_getEntity:
|
||||
(linkedFileData, current_user_id, callback = (err, entity, type) ->) ->
|
||||
callback = _.once(callback)
|
||||
{ source_entity_path } = linkedFileData
|
||||
@_getSourceProject linkedFileData, (err, project) ->
|
||||
return callback(err) if err?
|
||||
source_project_id = project._id
|
||||
ProjectLocator.findElementByPath {
|
||||
project_id: source_project_id,
|
||||
path: source_entity_path
|
||||
}, (err, entity, type) ->
|
||||
if err?
|
||||
if err.toString().match(/^not found.*/)
|
||||
err = new SourceFileNotFoundError()
|
||||
return callback(err)
|
||||
callback(null, project, entity, type)
|
||||
|
||||
|
||||
ProjectNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'ProjectNotFound'
|
||||
error.__proto__ = ProjectNotFoundError.prototype
|
||||
return error
|
||||
ProjectNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
V1ProjectNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'V1ProjectNotFound'
|
||||
error.__proto__ = V1ProjectNotFoundError.prototype
|
||||
return error
|
||||
V1ProjectNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
SourceFileNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'SourceFileNotFound'
|
||||
error.__proto__ = SourceFileNotFoundError.prototype
|
||||
return error
|
||||
SourceFileNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
module.exports = ProjectFileAgent =
|
||||
|
||||
sanitizeData: (data) ->
|
||||
_sanitizeData: (data) ->
|
||||
return _.pick(
|
||||
data,
|
||||
'provider',
|
||||
'source_project_id',
|
||||
'v1_source_doc_id',
|
||||
'source_entity_path'
|
||||
|
@ -74,34 +100,13 @@ module.exports = ProjectFileAgent =
|
|||
data.source_entity_path?
|
||||
)
|
||||
|
||||
canCreate: (data) ->
|
||||
_canCreate: (data) ->
|
||||
# Don't allow creation of linked-files with v1 doc ids
|
||||
!data.v1_source_doc_id?
|
||||
|
||||
_getSourceProject: (data, callback=(err, project)->) ->
|
||||
projection = {_id: 1, name: 1}
|
||||
if data.v1_source_doc_id?
|
||||
Project.findOne {'overleaf.id': data.v1_source_doc_id}, projection, (err, project) ->
|
||||
return callback(err) if err?
|
||||
if !project?
|
||||
return callback(new V1ProjectNotFoundError())
|
||||
callback(null, project)
|
||||
else if data.source_project_id?
|
||||
ProjectGetter.getProject data.source_project_id, projection, (err, project) ->
|
||||
return callback(err) if err?
|
||||
if !project?
|
||||
return callback(new ProjectNotFoundError())
|
||||
callback(null, project)
|
||||
else
|
||||
callback(new BadDataError('neither v1 nor v2 id present'))
|
||||
_getSourceProject: LinkedFilesHandler.getSourceProject
|
||||
|
||||
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
|
||||
callback = _.once(callback)
|
||||
@_getSourceProject data, (err, project) ->
|
||||
return callback(err) if err?
|
||||
callback(err, _.extend(data, {source_project_display_name: project.name}))
|
||||
|
||||
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
|
||||
_checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
|
||||
callback = _.once(callback)
|
||||
if !ProjectFileAgent._validate(data)
|
||||
return callback(new BadDataError())
|
||||
|
@ -110,52 +115,4 @@ module.exports = ProjectFileAgent =
|
|||
AuthorizationManager.canUserReadProject current_user_id, project._id, null, (err, canRead) ->
|
||||
return callback(err) if err?
|
||||
callback(null, canRead)
|
||||
|
||||
writeIncomingFileToDisk:
|
||||
(project_id, data, current_user_id, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
if !ProjectFileAgent._validate(data)
|
||||
return callback(new BadDataError())
|
||||
{ source_entity_path } = data
|
||||
@_getSourceProject data, (err, project) ->
|
||||
return callback(err) if err?
|
||||
source_project_id = project._id
|
||||
ProjectLocator.findElementByPath {
|
||||
project_id: source_project_id,
|
||||
path: source_entity_path
|
||||
}, (err, entity, type) ->
|
||||
if err?
|
||||
if err.toString().match(/^not found.*/)
|
||||
err = new SourceFileNotFoundError()
|
||||
return callback(err)
|
||||
ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback
|
||||
|
||||
_writeEntityToDisk: (project_id, entity_id, type, callback=(err, location)->) ->
|
||||
callback = _.once(callback)
|
||||
if type == 'doc'
|
||||
DocstoreManager.getDoc project_id, entity_id, (err, lines) ->
|
||||
return callback(err) if err?
|
||||
FileWriter.writeLinesToDisk entity_id, lines, callback
|
||||
else if type == 'file'
|
||||
FileStoreHandler.getFileStream project_id, entity_id, null, (err, fileStream) ->
|
||||
return callback(err) if err?
|
||||
FileWriter.writeStreamToDisk entity_id, fileStream, callback
|
||||
else
|
||||
callback(new BadEntityTypeError())
|
||||
|
||||
handleError: (error, req, res, next) ->
|
||||
if error instanceof AccessDeniedError
|
||||
res.status(403).send("You do not have access to this project")
|
||||
else if error instanceof BadDataError
|
||||
res.status(400).send("The submitted data is not valid")
|
||||
else if error instanceof BadEntityTypeError
|
||||
res.status(400).send("The file is the wrong type")
|
||||
else if error instanceof SourceFileNotFoundError
|
||||
res.status(404).send("Source file not found")
|
||||
else if error instanceof ProjectNotFoundError
|
||||
res.status(404).send("Project not found")
|
||||
else if error instanceof V1ProjectNotFoundError
|
||||
res.status(409).send("Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file")
|
||||
else
|
||||
next(error)
|
||||
next()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
ProjectGetter = require('../Project/ProjectGetter')
|
||||
Settings = require 'settings-sharelatex'
|
||||
CompileManager = require '../Compile/CompileManager'
|
||||
ClsiManager = require '../Compile/ClsiManager'
|
||||
ProjectFileAgent = require './ProjectFileAgent'
|
||||
_ = require "underscore"
|
||||
{
|
||||
BadDataError,
|
||||
AccessDeniedError,
|
||||
BadEntityTypeError,
|
||||
OutputFileFetchFailedError
|
||||
} = require './LinkedFilesErrors'
|
||||
LinkedFilesHandler = require './LinkedFilesHandler'
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
|
||||
module.exports = ProjectOutputFileAgent = {
|
||||
|
||||
_prepare: (project_id, linkedFileData, user_id, callback=(err, linkedFileData)->) ->
|
||||
@_checkAuth project_id, linkedFileData, user_id, (err, allowed) =>
|
||||
return callback(err) if err?
|
||||
return callback(new AccessDeniedError()) if !allowed
|
||||
if !@_validate(linkedFileData)
|
||||
return callback(new BadDataError())
|
||||
callback(null, linkedFileData)
|
||||
|
||||
createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
|
||||
if !@_canCreate(linkedFileData)
|
||||
return callback(new AccessDeniedError())
|
||||
linkedFileData = @_sanitizeData(linkedFileData)
|
||||
@_prepare project_id, linkedFileData, user_id, (err, linkedFileData) =>
|
||||
return callback(err) if err?
|
||||
@_getFileStream linkedFileData, user_id, (err, readStream) =>
|
||||
return callback(err) if err?
|
||||
readStream.on "error", callback
|
||||
readStream.on "response", (response) =>
|
||||
if 200 <= response.statusCode < 300
|
||||
readStream.resume()
|
||||
LinkedFilesHandler.importFromStream project_id,
|
||||
readStream,
|
||||
linkedFileData,
|
||||
name,
|
||||
parent_folder_id,
|
||||
user_id,
|
||||
(err, file) ->
|
||||
return callback(err) if err?
|
||||
callback(null, file._id) # Created
|
||||
else
|
||||
err = new OutputFileFetchFailedError(
|
||||
"Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}"
|
||||
)
|
||||
err.statusCode = response.statusCode
|
||||
callback(err)
|
||||
|
||||
refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
|
||||
@_prepare project_id, linkedFileData, user_id, (err, linkedFileData) =>
|
||||
return callback(err) if err?
|
||||
@_compileAndGetFileStream linkedFileData, user_id, (err, readStream, new_build_id) =>
|
||||
return callback(err) if err?
|
||||
readStream.on "error", callback
|
||||
readStream.on "response", (response) =>
|
||||
if 200 <= response.statusCode < 300
|
||||
readStream.resume()
|
||||
linkedFileData.build_id = new_build_id
|
||||
LinkedFilesHandler.importFromStream project_id,
|
||||
readStream,
|
||||
linkedFileData,
|
||||
name,
|
||||
parent_folder_id,
|
||||
user_id,
|
||||
(err, file) ->
|
||||
return callback(err) if err?
|
||||
callback(null, file._id) # Created
|
||||
else
|
||||
err = new OutputFileFetchFailedError(
|
||||
"Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}"
|
||||
)
|
||||
err.statusCode = response.statusCode
|
||||
callback(err)
|
||||
|
||||
|
||||
_sanitizeData: (data) ->
|
||||
return {
|
||||
provider: data.provider,
|
||||
source_project_id: data.source_project_id,
|
||||
source_output_file_path: data.source_output_file_path,
|
||||
build_id: data.build_id
|
||||
}
|
||||
|
||||
_canCreate: ProjectFileAgent._canCreate
|
||||
|
||||
_getSourceProject: LinkedFilesHandler.getSourceProject
|
||||
|
||||
_validate: (data) ->
|
||||
return (
|
||||
(data.source_project_id? || data.v1_source_doc_id?) &&
|
||||
data.source_output_file_path? &&
|
||||
data.build_id?
|
||||
)
|
||||
|
||||
_checkAuth: (project_id, data, current_user_id, callback = (err, allowed)->) ->
|
||||
callback = _.once(callback)
|
||||
if !@_validate(data)
|
||||
return callback(new BadDataError())
|
||||
@_getSourceProject data, (err, project) ->
|
||||
return callback(err) if err?
|
||||
AuthorizationManager.canUserReadProject current_user_id,
|
||||
project._id,
|
||||
null,
|
||||
(err, canRead) ->
|
||||
return callback(err) if err?
|
||||
callback(null, canRead)
|
||||
|
||||
_getFileStream: (linkedFileData, user_id, callback=(err, fileStream)->) ->
|
||||
callback = _.once(callback)
|
||||
{ source_output_file_path, build_id } = linkedFileData
|
||||
@_getSourceProject linkedFileData, (err, project) ->
|
||||
return callback(err) if err?
|
||||
source_project_id = project._id
|
||||
ClsiManager.getOutputFileStream source_project_id,
|
||||
user_id,
|
||||
build_id,
|
||||
source_output_file_path,
|
||||
(err, readStream) ->
|
||||
return callback(err) if err?
|
||||
readStream.pause()
|
||||
callback(null, readStream)
|
||||
|
||||
_compileAndGetFileStream: (linkedFileData, user_id, callback=(err, stream, build_id)->) ->
|
||||
callback = _.once(callback)
|
||||
{ source_output_file_path } = linkedFileData
|
||||
@_getSourceProject linkedFileData, (err, project) ->
|
||||
return callback(err) if err?
|
||||
source_project_id = project._id
|
||||
CompileManager.compile source_project_id,
|
||||
user_id,
|
||||
{},
|
||||
(err, status, outputFiles) ->
|
||||
return callback(err) if err?
|
||||
if status != 'success'
|
||||
return callback(new OutputFileFetchFailedError())
|
||||
outputFile = _.find(
|
||||
outputFiles,
|
||||
(o) => o.path == source_output_file_path
|
||||
)
|
||||
if !outputFile?
|
||||
return callback(new OutputFileFetchFailedError())
|
||||
build_id = outputFile.build
|
||||
ClsiManager.getOutputFileStream source_project_id,
|
||||
user_id,
|
||||
build_id,
|
||||
source_output_file_path,
|
||||
(err, readStream) ->
|
||||
return callback(err) if err?
|
||||
readStream.pause()
|
||||
callback(null, readStream, build_id)
|
||||
}
|
|
@ -1,67 +1,53 @@
|
|||
request = require 'request'
|
||||
FileWriter = require('../../infrastructure/FileWriter')
|
||||
_ = require "underscore"
|
||||
urlValidator = require 'valid-url'
|
||||
Settings = require 'settings-sharelatex'
|
||||
{ InvalidUrlError, UrlFetchFailedError } = require './LinkedFilesErrors'
|
||||
LinkedFilesHandler = require './LinkedFilesHandler'
|
||||
|
||||
UrlFetchFailedError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'UrlFetchFailedError'
|
||||
error.__proto__ = UrlFetchFailedError.prototype
|
||||
return error
|
||||
UrlFetchFailedError.prototype.__proto__ = Error.prototype
|
||||
|
||||
InvalidUrlError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'InvalidUrlError'
|
||||
error.__proto__ = InvalidUrlError.prototype
|
||||
return error
|
||||
InvalidUrlError.prototype.__proto__ = Error.prototype
|
||||
|
||||
module.exports = UrlAgent = {
|
||||
UrlFetchFailedError: UrlFetchFailedError
|
||||
InvalidUrlError: InvalidUrlError
|
||||
|
||||
sanitizeData: (data) ->
|
||||
createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
|
||||
linkedFileData = @._sanitizeData(linkedFileData)
|
||||
@_getUrlStream project_id, linkedFileData, user_id, (err, readStream) ->
|
||||
return callback(err) if err?
|
||||
readStream.on "error", callback
|
||||
readStream.on "response", (response) ->
|
||||
if 200 <= response.statusCode < 300
|
||||
readStream.resume()
|
||||
LinkedFilesHandler.importFromStream project_id,
|
||||
readStream,
|
||||
linkedFileData,
|
||||
name,
|
||||
parent_folder_id,
|
||||
user_id,
|
||||
(err, file) ->
|
||||
return callback(err) if err?
|
||||
callback(null, file._id) # Created
|
||||
else
|
||||
error = new UrlFetchFailedError("url fetch failed: #{linkedFileData.url}")
|
||||
error.statusCode = response.statusCode
|
||||
callback(error)
|
||||
|
||||
refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
|
||||
@createLinkedFile project_id, linkedFileData, name, parent_folder_id, user_id, callback
|
||||
|
||||
_sanitizeData: (data) ->
|
||||
return {
|
||||
provider: data.provider
|
||||
url: @._prependHttpIfNeeded(data.url)
|
||||
}
|
||||
|
||||
canCreate: (data) -> true
|
||||
|
||||
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
|
||||
return callback(null, data)
|
||||
|
||||
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
|
||||
callback(null, true)
|
||||
|
||||
writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) ->
|
||||
_getUrlStream: (project_id, data, current_user_id, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
url = data.url
|
||||
if !urlValidator.isWebUri(url)
|
||||
return callback(new InvalidUrlError("invalid url: #{url}"))
|
||||
url = UrlAgent._wrapWithProxy(url)
|
||||
url = @_wrapWithProxy(url)
|
||||
readStream = request.get(url)
|
||||
readStream.on "error", callback
|
||||
readStream.on "response", (response) ->
|
||||
if 200 <= response.statusCode < 300
|
||||
FileWriter.writeStreamToDisk project_id, readStream, callback
|
||||
else
|
||||
error = new UrlFetchFailedError("url fetch failed: #{url}")
|
||||
error.statusCode = response.statusCode
|
||||
callback(error)
|
||||
|
||||
handleError: (error, req, res, next) ->
|
||||
if error instanceof UrlFetchFailedError
|
||||
res.status(422).send(
|
||||
"Your URL could not be reached (#{error.statusCode} status code). Please check it and try again."
|
||||
)
|
||||
else if error instanceof InvalidUrlError
|
||||
res.status(422).send(
|
||||
"Your URL is not valid. Please check it and try again."
|
||||
)
|
||||
else
|
||||
next(error)
|
||||
readStream.pause()
|
||||
callback(null, readStream)
|
||||
|
||||
_prependHttpIfNeeded: (url) ->
|
||||
if !url.match('://')
|
||||
|
|
|
@ -55,7 +55,10 @@ module.exports = ProjectEntityUpdateHandler = self =
|
|||
logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
|
||||
return callback()
|
||||
# convert any invalid characters in original file to '_'
|
||||
fileRef = new File name : SafePath.clean(origonalFileRef.name)
|
||||
fileProperties = name : SafePath.clean(origonalFileRef.name)
|
||||
if origonalFileRef.linkedFileData?
|
||||
fileProperties.linkedFileData = origonalFileRef.linkedFileData
|
||||
fileRef = new File(fileProperties)
|
||||
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
|
||||
|
|
|
@ -15,11 +15,14 @@ module.exports = FileWriter =
|
|||
callback(null)
|
||||
|
||||
writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) ->
|
||||
FileWriter.writeContentToDisk(identifier, lines.join('\n'), callback)
|
||||
|
||||
writeContentToDisk: (identifier, content, callback = (error, fsPath)->) ->
|
||||
callback = _.once(callback)
|
||||
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
|
||||
FileWriter._ensureDumpFolderExists (error) ->
|
||||
return callback(error) if error?
|
||||
fs.writeFile fsPath, lines.join('\n'), (error) ->
|
||||
fs.writeFile fsPath, content, (error) ->
|
||||
return callback(error) if error?
|
||||
callback(null, fsPath)
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ div.binary-file.full-size(
|
|||
) #{translate("no_preview_available")}
|
||||
|
||||
div.binary-file-footer
|
||||
// Linked Files: URL
|
||||
div(ng-if="openFile.linkedFileData.provider == 'url'")
|
||||
p
|
||||
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
|
||||
|
@ -47,6 +48,7 @@ div.binary-file.full-size(
|
|||
|
|
||||
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
|
||||
|
||||
// Linked Files: Project File
|
||||
div(ng-if="openFile.linkedFileData.provider == 'project_file'")
|
||||
p
|
||||
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
|
||||
|
@ -54,14 +56,30 @@ div.binary-file.full-size(
|
|||
|
|
||||
a(ng-if='!openFile.linkedFileData.v1_source_doc_id'
|
||||
ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank")
|
||||
| {{ openFile.linkedFileData.source_project_display_name }}
|
||||
| Another project
|
||||
span(ng-if='openFile.linkedFileData.v1_source_doc_id')
|
||||
| {{ openFile.linkedFileData.source_project_display_name }}
|
||||
| Another project
|
||||
| /{{ openFile.linkedFileData.source_entity_path.slice(1) }},
|
||||
|
|
||||
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
|
||||
|
||||
span(ng-if="openFile.linkedFileData.provider == 'url' || openFile.linkedFileData.provider == 'project_file'")
|
||||
// Linked Files: Project Output File
|
||||
div(ng-if="openFile.linkedFileData.provider == 'project_output_file'")
|
||||
p
|
||||
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
|
||||
| Imported from the output of
|
||||
|
|
||||
a(ng-if='!openFile.linkedFileData.v1_source_doc_id'
|
||||
ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank")
|
||||
| Another project
|
||||
span(ng-if='openFile.linkedFileData.v1_source_doc_id')
|
||||
| Another project
|
||||
| : {{ openFile.linkedFileData.source_output_file_path }},
|
||||
|
|
||||
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
|
||||
|
||||
// Bottom Controls
|
||||
span(ng-if="openFile.linkedFileData.provider")
|
||||
button.btn.btn-success(
|
||||
href, ng-click="refreshFile(openFile)",
|
||||
ng-disabled="refreshing"
|
||||
|
|
|
@ -308,165 +308,10 @@ script(type='text/ng-template', id='entityListItemTemplate')
|
|||
ng-repeat="child in entity.children | orderBy:[orderByFoldersFirst, 'name']"
|
||||
)
|
||||
|
||||
script(type='text/ng-template', id='newDocModalTemplate')
|
||||
.modal-header
|
||||
h3 #{translate("new_file")}
|
||||
.modal-body
|
||||
form(novalidate, name="newDocForm")
|
||||
div.alert.alert-danger(ng-if="error")
|
||||
div(ng-switch="error")
|
||||
span(ng-switch-when="already exists") #{translate("file_already_exists")}
|
||||
span(ng-switch-default) {{error}}
|
||||
input.form-control(
|
||||
type="text",
|
||||
placeholder="File Name",
|
||||
required,
|
||||
ng-model="inputs.name",
|
||||
on-enter="create()",
|
||||
select-name-on="open",
|
||||
valid-file,
|
||||
name="name"
|
||||
)
|
||||
.text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile")
|
||||
| #{translate('files_cannot_include_invalid_characters')}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="cancel()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
ng-disabled="newDocForm.$invalid || state.inflight"
|
||||
ng-click="create()"
|
||||
)
|
||||
span(ng-hide="state.inflight") #{translate("create")}
|
||||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
// Project Linked Files Modal
|
||||
script(type='text/ng-template', id='projectLinkedFileModalTemplate')
|
||||
.modal-header
|
||||
h3 New file from Project
|
||||
|
||||
.modal-body
|
||||
div
|
||||
div.alert.alert-danger(ng-if="state.error") Error, something went wrong!
|
||||
div
|
||||
form
|
||||
.form-controls
|
||||
label(for="project-select") Select a Project
|
||||
span(ng-show="state.inFlight.projects")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-select"
|
||||
ng-model="data.selectedProjectId"
|
||||
ng-disabled="!shouldEnableProjectSelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a Project
|
||||
option(
|
||||
ng-repeat="project in data.projects"
|
||||
value="{{ project._id }}"
|
||||
) {{ project.name }}
|
||||
|
||||
br
|
||||
.form-controls
|
||||
label(for="project-entity-select") Select a File
|
||||
span(ng-show="state.inFlight.entities")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-entity-select"
|
||||
ng-model="data.selectedProjectEntity"
|
||||
ng-disabled="!shouldEnableProjectEntitySelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a File
|
||||
option(
|
||||
ng-repeat="projectEntity in data.projectEntities"
|
||||
value="{{ projectEntity.path }}"
|
||||
) {{ projectEntity.path.slice(1) }}
|
||||
br
|
||||
|
||||
.form-controls
|
||||
label(for="name") File Name In This Project
|
||||
input.form-control(
|
||||
type="text"
|
||||
placeholder="example.tex"
|
||||
required
|
||||
ng-model="data.name"
|
||||
name="name"
|
||||
)
|
||||
br
|
||||
|
||||
.modal-footer
|
||||
span(ng-show="state.inFlight.create")
|
||||
i.fa.fa-spinner.fa-spin
|
||||
|
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="cancel()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
ng-disabled="!shouldEnableCreateButton()"
|
||||
ng-click="create()"
|
||||
)
|
||||
span(ng-hide="state.inflight") #{translate("create")}
|
||||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
script(type='text/ng-template', id='linkedFileModalTemplate')
|
||||
.modal-header
|
||||
h3 New file from URL
|
||||
.modal-body
|
||||
form(novalidate, name="newLinkedFileForm")
|
||||
div.alert.alert-danger(ng-if="error")
|
||||
div(ng-switch="error")
|
||||
span(ng-switch-when="already exists") #{translate("file_already_exists")}
|
||||
span(ng-switch-default) {{error}}
|
||||
label(for="url") URL to fetch the file from
|
||||
input.form-control(
|
||||
type="text",
|
||||
placeholder="www.example.com/my_file",
|
||||
required,
|
||||
ng-model="inputs.url",
|
||||
focus-on="open",
|
||||
on-enter="create()",
|
||||
name="url"
|
||||
)
|
||||
.row-spaced
|
||||
label(for="name") File name in this project
|
||||
input.form-control(
|
||||
type="text",
|
||||
placeholder="my_file",
|
||||
required,
|
||||
ng-model="inputs.name",
|
||||
ng-change="nameChangedByUser = true"
|
||||
valid-file,
|
||||
on-enter="create()",
|
||||
name="name"
|
||||
)
|
||||
.text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile")
|
||||
| #{translate('files_cannot_include_invalid_characters')}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="cancel()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
ng-disabled="newLinkedFileForm.$invalid || state.inflight"
|
||||
ng-click="create()"
|
||||
)
|
||||
span(ng-hide="state.inflight") #{translate("create")}
|
||||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
script(type='text/ng-template', id='newFolderModalTemplate')
|
||||
.modal-header
|
||||
h3 #{translate("new_folder")}
|
||||
.modal-body
|
||||
div.alert.alert-danger(ng-if="error")
|
||||
div(ng-switch="error")
|
||||
span(ng-switch-when="already exists") #{translate("file_already_exists")}
|
||||
span(ng-switch-default) {{error}}
|
||||
form(novalidate, name="newFolderForm")
|
||||
input.form-control(
|
||||
type="text",
|
||||
|
@ -478,8 +323,12 @@ script(type='text/ng-template', id='newFolderModalTemplate')
|
|||
valid-file,
|
||||
name="name"
|
||||
)
|
||||
.text-danger.row-spaced-small(ng-show="newFolderForm.name.$error.validFile")
|
||||
| #{translate('files_cannot_include_invalid_characters')}
|
||||
div.alert.alert-danger.row-spaced-small(ng-show="newFolderForm.name.$error.validFile")
|
||||
| #{translate('files_cannot_include_invalid_characters')}
|
||||
div.alert.alert-danger.row-spaced-small(ng-if="error")
|
||||
div(ng-switch="error")
|
||||
span(ng-switch-when="already exists") #{translate("file_already_exists")}
|
||||
span(ng-switch-default) {{error}}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
|
@ -492,70 +341,7 @@ script(type='text/ng-template', id='newFolderModalTemplate')
|
|||
span(ng-hide="state.inflight") #{translate("create")}
|
||||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
script(type="text/template", id="qq-file-uploader-template")
|
||||
div.qq-uploader-selector
|
||||
div(qq-hide-dropzone="").qq-upload-drop-area-selector.qq-upload-drop-area
|
||||
span.qq-upload-drop-area-text-selector #{translate('drop_files_here_to_upload')}
|
||||
div.qq-upload-button-selector.btn.btn-primary.btn-lg
|
||||
div #{translate('upload')}
|
||||
span.or.btn-lg #{translate('or')}
|
||||
span.drag-here.btn-lg #{translate('drag_here')}
|
||||
span.qq-drop-processing-selector
|
||||
span #{translate('processing')}
|
||||
span.qq-drop-processing-spinner-selector
|
||||
ul.qq-upload-list-selector
|
||||
li
|
||||
div.qq-progress-bar-container-selector
|
||||
div(
|
||||
role="progressbar"
|
||||
aria-valuenow="0"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
class="qq-progress-bar-selector qq-progress-bar"
|
||||
)
|
||||
span.qq-upload-file-selector.qq-upload-file
|
||||
span.qq-upload-size-selector.qq-upload-size
|
||||
a(type="button").qq-btn.qq-upload-cancel-selector.qq-upload-cancel #{translate('cancel')}
|
||||
button(type="button").qq-btn.qq-upload-retry-selector.qq-upload-retry #{translate('retry')}
|
||||
span(role="status").qq-upload-status-text-selector.qq-upload-status-text
|
||||
|
||||
script(type="text/ng-template", id="uploadFileModalTemplate")
|
||||
.modal-header
|
||||
h3 #{translate("upload_files")}
|
||||
.alert.alert-warning.small.modal-alert(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
|
||||
.alert.alert-warning.small.modal-alert(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")}
|
||||
.alert.alert-warning.small.modal-alert(ng-if="notLoggedIn") #{translate("session_expired_redirecting_to_login", {seconds:"{{secondsToRedirect}}"})}
|
||||
.alert.alert-warning.small.modal-alert(ng-if="conflicts.length > 0")
|
||||
p.text-center
|
||||
| The following files already exist in this project:
|
||||
ul.text-center.list-unstyled.row-spaced-small
|
||||
li(ng-repeat="conflict in conflicts"): strong {{ conflict }}
|
||||
p.text-center.row-spaced-small
|
||||
| Do you want to overwrite them?
|
||||
p.text-center
|
||||
a(href, ng-click="doUpload()").btn.btn-primary Overwrite
|
||||
|
|
||||
a(href, ng-click="cancel()").btn.btn-default Cancel
|
||||
|
||||
.modal-body(
|
||||
fine-upload
|
||||
endpoint="/project/{{ project_id }}/upload"
|
||||
template-id="qq-file-uploader-template"
|
||||
multiple="true"
|
||||
auto-upload="false"
|
||||
on-complete-callback="onComplete"
|
||||
on-upload-callback="onUpload"
|
||||
on-validate-batch="onValidateBatch"
|
||||
on-error-callback="onError"
|
||||
on-submit-callback="onSubmit"
|
||||
on-cancel-callback="onCancel"
|
||||
control="control"
|
||||
params="{'folder_id': parent_folder_id}"
|
||||
)
|
||||
.modal-footer
|
||||
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
|
||||
|
||||
include ./new-file-modal
|
||||
|
||||
script(type='text/ng-template', id='deleteEntityModalTemplate')
|
||||
.modal-header
|
||||
|
|
|
@ -69,14 +69,6 @@ aside#left-menu.full-size(
|
|||
a(href="#" ng-click="richText()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| Rich Text
|
||||
li
|
||||
a(href="#" ng-click="openProjectLinkedFileModal()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| Project-Linked-File Modal
|
||||
li
|
||||
a(href="#" ng-click="openLinkedFileModal()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| URL-Linked-File Modal
|
||||
|
||||
|
||||
h4(ng-show="!anonymous") #{translate("settings")}
|
||||
|
|
217
services/web/app/views/project/editor/new-file-modal.pug
Normal file
217
services/web/app/views/project/editor/new-file-modal.pug
Normal file
|
@ -0,0 +1,217 @@
|
|||
script(type='text/ng-template', id='newFileModalTemplate')
|
||||
.modal-header
|
||||
h3 Add Files
|
||||
.modal-body.modal-new-file
|
||||
table
|
||||
tr
|
||||
td.modal-new-file--list
|
||||
ul.list-unstyled
|
||||
li(ng-class="type == 'doc' ? 'active' : null")
|
||||
a(href, ng-click="type = 'doc'")
|
||||
i.fa.fa-fw.fa-file
|
||||
|
|
||||
| New File
|
||||
li(ng-class="type == 'upload' ? 'active' : null")
|
||||
a(href, ng-click="type = 'upload'")
|
||||
i.fa.fa-fw.fa-upload
|
||||
|
|
||||
| Upload
|
||||
li(ng-class="type == 'project' ? 'active' : null")
|
||||
a(href, ng-click="type = 'project'")
|
||||
i.fa.fa-fw.fa-folder-open
|
||||
|
|
||||
| From Another Project
|
||||
li(ng-class="type == 'url' ? 'active' : null")
|
||||
a(href, ng-click="type = 'url'")
|
||||
i.fa.fa-fw.fa-globe
|
||||
|
|
||||
| From External URL
|
||||
td(class="modal-new-file--body modal-new-file--body-{{type}}")
|
||||
div(ng-if="type == 'doc'", ng-controller="NewDocModalController")
|
||||
form(novalidate, name="newDocForm")
|
||||
label(for="name") File Name
|
||||
input.form-control(
|
||||
type="text",
|
||||
placeholder="File Name",
|
||||
required,
|
||||
ng-model="inputs.name",
|
||||
on-enter="create()",
|
||||
select-name-on="open",
|
||||
valid-file,
|
||||
name="name"
|
||||
)
|
||||
div.alert.alert-danger.row-spaced-small(ng-if="error")
|
||||
div(ng-switch="error")
|
||||
span(ng-switch-when="already exists") #{translate("file_already_exists")}
|
||||
span(ng-switch-default) {{error}}
|
||||
div.alert.alert-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile")
|
||||
| #{translate('files_cannot_include_invalid_characters')}
|
||||
div(ng-if="type == 'upload'", ng-controller="UploadFileModalController")
|
||||
.alert.alert-warning.small.modal(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
|
||||
.alert.alert-warning.small.modal(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")}
|
||||
.alert.alert-warning.small.modal(ng-if="notLoggedIn") #{translate("session_expired_redirecting_to_login", {seconds:"{{secondsToRedirect}}"})}
|
||||
.alert.alert-warning.small.modal(ng-if="conflicts.length > 0")
|
||||
p.text-center
|
||||
| The following files already exist in this project:
|
||||
ul.text-center.list-unstyled.row-spaced-small
|
||||
li(ng-repeat="conflict in conflicts"): strong {{ conflict }}
|
||||
p.text-center.row-spaced-small
|
||||
| Do you want to overwrite them?
|
||||
p.text-center
|
||||
a(href, ng-click="doUpload()").btn.btn-primary Overwrite
|
||||
|
|
||||
a(href, ng-click="cancel()").btn.btn-default Cancel
|
||||
div(
|
||||
fine-upload
|
||||
endpoint="/project/{{ project_id }}/upload"
|
||||
template-id="qq-file-uploader-template"
|
||||
multiple="true"
|
||||
auto-upload="false"
|
||||
on-complete-callback="onComplete"
|
||||
on-upload-callback="onUpload"
|
||||
on-validate-batch="onValidateBatch"
|
||||
on-error-callback="onError"
|
||||
on-submit-callback="onSubmit"
|
||||
on-cancel-callback="onCancel"
|
||||
control="control"
|
||||
params="{'folder_id': parent_folder_id}"
|
||||
)
|
||||
div(ng-if="type == 'project'", ng-controller="ProjectLinkedFileModalController")
|
||||
div
|
||||
form
|
||||
.form-controls
|
||||
label(for="project-select") Select a Project
|
||||
span(ng-show="state.inFlight.projects")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-select"
|
||||
ng-model="data.selectedProjectId"
|
||||
ng-disabled="!shouldEnableProjectSelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a Project
|
||||
option(
|
||||
ng-repeat="project in data.projects"
|
||||
value="{{ project._id }}"
|
||||
) {{ project.name }}
|
||||
|
||||
.form-controls.row-spaced-small(ng-if="!state.isOutputFilesMode")
|
||||
label(for="project-entity-select") Select a File
|
||||
span(ng-show="state.inFlight.entities")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-entity-select"
|
||||
ng-model="data.selectedProjectEntity"
|
||||
ng-disabled="!shouldEnableProjectEntitySelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a File
|
||||
option(
|
||||
ng-repeat="projectEntity in data.projectEntities"
|
||||
value="{{ projectEntity.path }}"
|
||||
) {{ projectEntity.path.slice(1) }}
|
||||
|
||||
.form-controls.row-spaced-small(ng-if="state.isOutputFilesMode")
|
||||
label(for="project-entity-select") Select an Output File
|
||||
span(ng-show="state.inFlight.compile")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-output-file-select"
|
||||
ng-model="data.selectedProjectOutputFile"
|
||||
ng-disabled="!shouldEnableProjectOutputFileSelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select an Output File
|
||||
option(
|
||||
ng-repeat="outputFile in data.projectOutputFiles"
|
||||
value="{{ outputFile.path }}"
|
||||
) {{ outputFile.path }}
|
||||
div.toggle-output-files-button
|
||||
| or
|
||||
a(
|
||||
href="#"
|
||||
ng-click="toggleOutputFilesMode()"
|
||||
)
|
||||
span(ng-show="state.isOutputFilesMode") select from source files
|
||||
span(ng-show="!state.isOutputFilesMode") select from output files
|
||||
|
||||
.form-controls.row-spaced-small
|
||||
label(for="name") File Name In This Project
|
||||
input.form-control(
|
||||
type="text"
|
||||
placeholder="example.tex"
|
||||
required
|
||||
ng-model="data.name"
|
||||
name="name"
|
||||
)
|
||||
div.alert.alert-danger.row-spaced-small(ng-if="state.error") Error, something went wrong!
|
||||
div(ng-if="type == 'url'", ng-controller="UrlLinkedFileModalController")
|
||||
form(novalidate, name="newLinkedFileForm")
|
||||
label(for="url") URL to fetch the file from
|
||||
input.form-control(
|
||||
type="text",
|
||||
placeholder="www.example.com/my_file",
|
||||
required,
|
||||
ng-model="inputs.url",
|
||||
focus-on="open",
|
||||
on-enter="create()",
|
||||
name="url"
|
||||
)
|
||||
.row-spaced.small
|
||||
label(for="name") File name in this project
|
||||
input.form-control(
|
||||
type="text",
|
||||
placeholder="my_file",
|
||||
required,
|
||||
ng-model="inputs.name",
|
||||
ng-change="nameChangedByUser = true"
|
||||
valid-file,
|
||||
on-enter="create()",
|
||||
name="name"
|
||||
)
|
||||
.text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile")
|
||||
| #{translate('files_cannot_include_invalid_characters')}
|
||||
div.alert.alert-danger.row-spaced-small(ng-if="error")
|
||||
div(ng-switch="error")
|
||||
span(ng-switch-when="already exists") #{translate("file_already_exists")}
|
||||
span(ng-switch-default) {{error}}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="cancel()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
ng-disabled="state.inflight || !state.valid"
|
||||
ng-click="create()"
|
||||
ng-hide="type == 'upload'"
|
||||
)
|
||||
span(ng-hide="state.inflight") #{translate("create")}
|
||||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
script(type="text/template", id="qq-file-uploader-template")
|
||||
div.qq-uploader-selector
|
||||
div(qq-hide-dropzone="").qq-upload-drop-area-selector.qq-upload-drop-area
|
||||
span.qq-upload-drop-area-text-selector #{translate('drop_files_here_to_upload')}
|
||||
div Drag here
|
||||
div.row-spaced-small.small #{translate('or')}
|
||||
div.row-spaced-small
|
||||
div.qq-upload-button-selector.btn.btn-primary
|
||||
| Select from your computer
|
||||
span.qq-drop-processing-selector
|
||||
span #{translate('processing')}
|
||||
span.qq-drop-processing-spinner-selector
|
||||
ul.qq-upload-list-selector
|
||||
li
|
||||
div.qq-progress-bar-container-selector
|
||||
div(
|
||||
role="progressbar"
|
||||
aria-valuenow="0"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
class="qq-progress-bar-selector qq-progress-bar"
|
||||
)
|
||||
span.qq-upload-file-selector.qq-upload-file
|
||||
span.qq-upload-size-selector.qq-upload-size
|
||||
a(type="button").qq-btn.qq-upload-cancel-selector.qq-upload-cancel #{translate('cancel')}
|
||||
button(type="button").qq-btn.qq-upload-retry-selector.qq-upload-retry #{translate('retry')}
|
||||
span(role="status").qq-upload-status-text-selector.qq-upload-status-text
|
|
@ -17,7 +17,7 @@ services:
|
|||
PROJECT_HISTORY_ENABLED: 'true'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url'
|
||||
LINKED_URL_PROXY: 'http://localhost:6543'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url,project_file'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url,project_file,project_output_file'
|
||||
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
|
||||
depends_on:
|
||||
- redis
|
||||
|
|
|
@ -4,10 +4,12 @@ define [
|
|||
App.controller "FileTreeController", ["$scope", "$modal", "ide", "$rootScope", ($scope, $modal, ide, $rootScope) ->
|
||||
$scope.openNewDocModal = () ->
|
||||
$modal.open(
|
||||
templateUrl: "newDocModalTemplate"
|
||||
controller: "NewDocModalController"
|
||||
templateUrl: "newFileModalTemplate"
|
||||
controller: "NewFileModalController"
|
||||
size: 'lg'
|
||||
resolve: {
|
||||
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
|
||||
type: () -> 'doc'
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -22,37 +24,12 @@ define [
|
|||
|
||||
$scope.openUploadFileModal = () ->
|
||||
$modal.open(
|
||||
templateUrl: "uploadFileModalTemplate"
|
||||
controller: "UploadFileModalController"
|
||||
scope: $scope
|
||||
resolve: {
|
||||
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
|
||||
}
|
||||
)
|
||||
|
||||
$scope.openLinkedFileModal = window.openLinkedFileModal = () ->
|
||||
unless 'url' in window.data.enabledLinkedFileTypes
|
||||
console.warn("Url linked files are not enabled")
|
||||
return
|
||||
$modal.open(
|
||||
templateUrl: "linkedFileModalTemplate"
|
||||
controller: "LinkedFileModalController"
|
||||
scope: $scope
|
||||
resolve: {
|
||||
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
|
||||
}
|
||||
)
|
||||
|
||||
$scope.openProjectLinkedFileModal = window.openProjectLinkedFileModal = () ->
|
||||
unless 'project_file' in window.data.enabledLinkedFileTypes
|
||||
console.warn("Project linked files are not enabled")
|
||||
return
|
||||
$modal.open(
|
||||
templateUrl: "projectLinkedFileModalTemplate"
|
||||
controller: "ProjectLinkedFileModalController"
|
||||
scope: $scope
|
||||
templateUrl: "newFileModalTemplate"
|
||||
controller: "NewFileModalController"
|
||||
size: 'lg'
|
||||
resolve: {
|
||||
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
|
||||
type: () -> 'upload'
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -67,42 +44,10 @@ define [
|
|||
$scope.$broadcast "delete:selected"
|
||||
]
|
||||
|
||||
App.controller "NewDocModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
$scope.inputs =
|
||||
name: "name.tex"
|
||||
$scope.state =
|
||||
inflight: false
|
||||
|
||||
$modalInstance.opened.then () ->
|
||||
$timeout () ->
|
||||
$scope.$broadcast "open"
|
||||
, 200
|
||||
|
||||
$scope.create = () ->
|
||||
name = $scope.inputs.name
|
||||
if !name? or name.length == 0
|
||||
return
|
||||
$scope.state.inflight = true
|
||||
ide.fileTreeManager
|
||||
.createDoc(name, parent_folder)
|
||||
.then () ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
.catch (response)->
|
||||
{ data } = response
|
||||
$scope.error = data
|
||||
$scope.state.inflight = false
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
]
|
||||
|
||||
App.controller "NewFolderModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
$scope.inputs =
|
||||
$scope.inputs =
|
||||
name: "name"
|
||||
$scope.state =
|
||||
inflight: false
|
||||
|
@ -118,10 +63,10 @@ define [
|
|||
return
|
||||
$scope.state.inflight = true
|
||||
ide.fileTreeManager
|
||||
.createFolder(name, parent_folder)
|
||||
.createFolder(name, $scope.parent_folder)
|
||||
.then () ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
$modalInstance.dismiss('done')
|
||||
.catch (response)->
|
||||
{ data } = response
|
||||
$scope.error = data
|
||||
|
@ -131,10 +76,60 @@ define [
|
|||
$modalInstance.dismiss('cancel')
|
||||
]
|
||||
|
||||
App.controller "NewFileModalController", [
|
||||
"$scope", "type", "parent_folder", "$modalInstance"
|
||||
($scope, type, parent_folder, $modalInstance) ->
|
||||
$scope.type = type
|
||||
$scope.parent_folder = parent_folder
|
||||
$scope.state = {
|
||||
inflight: false
|
||||
valid: true
|
||||
}
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
$scope.create = () ->
|
||||
$scope.$broadcast 'create'
|
||||
$scope.$on 'done', () ->
|
||||
$modalInstance.dismiss('done')
|
||||
]
|
||||
|
||||
App.controller "NewDocModalController", [
|
||||
"$scope", "ide", "$timeout"
|
||||
($scope, ide, $timeout) ->
|
||||
$scope.inputs =
|
||||
name: "name.tex"
|
||||
|
||||
validate = () ->
|
||||
name = $scope.inputs.name
|
||||
$scope.state.valid = (name? and name.length > 0)
|
||||
$scope.$watch 'inputs.name', validate
|
||||
|
||||
$timeout () ->
|
||||
$scope.$broadcast "open"
|
||||
, 200
|
||||
|
||||
$scope.$on 'create', () ->
|
||||
name = $scope.inputs.name
|
||||
if !name? or name.length == 0
|
||||
return
|
||||
$scope.state.inflight = true
|
||||
ide.fileTreeManager
|
||||
.createDoc(name, $scope.parent_folder)
|
||||
.then () ->
|
||||
$scope.state.inflight = false
|
||||
$scope.$emit 'done'
|
||||
.catch (response)->
|
||||
{ data } = response
|
||||
$scope.error = data
|
||||
$scope.state.inflight = false
|
||||
|
||||
]
|
||||
|
||||
App.controller "UploadFileModalController", [
|
||||
"$scope", "$rootScope", "ide", "$modalInstance", "$timeout", "parent_folder", "$window"
|
||||
($scope, $rootScope, ide, $modalInstance, $timeout, parent_folder, $window) ->
|
||||
$scope.parent_folder_id = parent_folder?.id
|
||||
"$scope", "$rootScope", "ide", "$timeout", "$window"
|
||||
($scope, $rootScope, ide, $timeout, $window) ->
|
||||
$scope.parent_folder_id = $scope.parent_folder?.id
|
||||
$scope.project_id = ide.project_id
|
||||
$scope.tooManyFiles = false
|
||||
$scope.rateLimitHit = false
|
||||
$scope.secondsToRedirect = 10
|
||||
|
@ -162,7 +157,7 @@ define [
|
|||
if response.success
|
||||
$rootScope.$broadcast 'file:upload:complete', response
|
||||
if uploadCount == 0 and response? and response.success
|
||||
$modalInstance.close("done")
|
||||
$scope.$emit 'done'
|
||||
), 250
|
||||
|
||||
$scope.onValidateBatch = (files)->
|
||||
|
@ -210,30 +205,44 @@ define [
|
|||
$scope.doUpload = () ->
|
||||
$scope.control?.q?.uploadStoredFiles()
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
]
|
||||
|
||||
App.controller "ProjectLinkedFileModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
"$scope", "ide", "$timeout",
|
||||
($scope, ide, $timeout) ->
|
||||
|
||||
$scope.data =
|
||||
projects: null # or []
|
||||
selectedProjectId: null
|
||||
projectEntities: null # or []
|
||||
projectOutputFiles: null # or []
|
||||
selectedProjectEntity: null
|
||||
selectedProjectOutputFile: null
|
||||
buildId: null
|
||||
name: null
|
||||
$scope.state =
|
||||
inFlight:
|
||||
projects: false
|
||||
entities: false
|
||||
create: false
|
||||
error: false
|
||||
$scope.state.inFlight =
|
||||
projects: false
|
||||
entities: false
|
||||
compile: false
|
||||
$scope.state.isOutputFilesMode = false
|
||||
$scope.state.error = false
|
||||
|
||||
$scope.$watch 'data.selectedProjectId', (newVal, oldVal) ->
|
||||
return if !newVal
|
||||
$scope.data.selectedProjectEntity = null
|
||||
$scope.getProjectEntities($scope.data.selectedProjectId)
|
||||
$scope.data.selectedProjectOutputFile = null
|
||||
if $scope.state.isOutputFilesMode
|
||||
$scope.compileProjectAndGetOutputFiles($scope.data.selectedProjectId)
|
||||
else
|
||||
$scope.getProjectEntities($scope.data.selectedProjectId)
|
||||
|
||||
$scope.$watch 'state.isOutputFilesMode', (newVal, oldVal) ->
|
||||
return if !newVal and !oldVal
|
||||
$scope.data.selectedProjectOutputFile = null
|
||||
if newVal == true
|
||||
$scope.compileProjectAndGetOutputFiles($scope.data.selectedProjectId)
|
||||
else
|
||||
$scope.getProjectEntities($scope.data.selectedProjectId)
|
||||
|
||||
# auto-set filename based on selected file
|
||||
$scope.$watch 'data.selectedProjectEntity', (newVal, oldVal) ->
|
||||
|
@ -242,15 +251,31 @@ define [
|
|||
if fileName
|
||||
$scope.data.name = fileName
|
||||
|
||||
# auto-set filename based on selected file
|
||||
$scope.$watch 'data.selectedProjectOutputFile', (newVal, oldVal) ->
|
||||
return if !newVal
|
||||
if newVal == 'output.pdf'
|
||||
project = _.find($scope.data.projects, (p) -> p._id == $scope.data.selectedProjectId)
|
||||
$scope.data.name = if project?.name? then "#{project.name}.pdf" else 'output.pdf'
|
||||
else
|
||||
fileName = newVal.split('/').reverse()[0]
|
||||
if fileName
|
||||
$scope.data.name = fileName
|
||||
|
||||
_setInFlight = (type) ->
|
||||
$scope.state.inFlight[type] = true
|
||||
|
||||
_reset = (opts) ->
|
||||
isError = opts.err == true
|
||||
inFlight = $scope.state.inFlight
|
||||
inFlight.projects = inFlight.entities = inFlight.create = false
|
||||
inFlight.projects = inFlight.entities = inFlight.compile = false
|
||||
$scope.state.inflight = false
|
||||
$scope.state.error = isError
|
||||
|
||||
$scope.toggleOutputFilesMode = () ->
|
||||
return if !$scope.data.selectedProjectId
|
||||
$scope.state.isOutputFilesMode = !$scope.state.isOutputFilesMode
|
||||
|
||||
$scope.shouldEnableProjectSelect = () ->
|
||||
{ state, data } = $scope
|
||||
return !state.inFlight.projects && data.projects
|
||||
|
@ -259,16 +284,33 @@ define [
|
|||
{ state, data } = $scope
|
||||
return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId
|
||||
|
||||
$scope.shouldEnableCreateButton = () ->
|
||||
$scope.shouldEnableProjectOutputFileSelect = () ->
|
||||
{ state, data } = $scope
|
||||
return !state.inFlight.projects && !state.inFlight.compile && data.projects && data.selectedProjectId
|
||||
|
||||
|
||||
validate = () ->
|
||||
state = $scope.state
|
||||
data = $scope.data
|
||||
return !state.inFlight.projects &&
|
||||
$scope.state.valid = !state.inFlight.projects &&
|
||||
!state.inFlight.entities &&
|
||||
data.projects &&
|
||||
data.selectedProjectId &&
|
||||
data.projectEntities &&
|
||||
data.selectedProjectEntity &&
|
||||
(
|
||||
(
|
||||
!$scope.state.isOutputFilesMode &&
|
||||
data.projectEntities &&
|
||||
data.selectedProjectEntity
|
||||
) ||
|
||||
(
|
||||
$scope.state.isOutputFilesMode &&
|
||||
data.projectOutputFiles &&
|
||||
data.selectedProjectOutputFile
|
||||
)
|
||||
) &&
|
||||
data.name
|
||||
$scope.$watch 'state', validate, true
|
||||
$scope.$watch 'data', validate, true
|
||||
|
||||
$scope.getUserProjects = () ->
|
||||
_setInFlight('projects')
|
||||
|
@ -295,50 +337,84 @@ define [
|
|||
.catch (err) ->
|
||||
_reset(err: true)
|
||||
|
||||
$scope.compileProjectAndGetOutputFiles = (project_id) =>
|
||||
_setInFlight('compile')
|
||||
ide.$http.post("/project/#{project_id}/compile", {
|
||||
check: "silent",
|
||||
draft: false,
|
||||
incrementalCompilesEnabled: false
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
.then (resp) ->
|
||||
if resp.data.status == 'success'
|
||||
filteredFiles = resp.data.outputFiles.filter (f) ->
|
||||
f.path.match(/.*\.(pdf|png|jpeg|jpg|gif)/)
|
||||
$scope.data.projectOutputFiles = filteredFiles
|
||||
$scope.data.buildId = filteredFiles?[0]?.build
|
||||
console.log ">> build_id", $scope.data.buildId
|
||||
_reset(err: false)
|
||||
else
|
||||
$scope.data.projectOutputFiles = null
|
||||
_reset(err: true)
|
||||
.catch (err) ->
|
||||
console.error(err)
|
||||
_reset(err: true)
|
||||
|
||||
$scope.init = () ->
|
||||
$scope.getUserProjects()
|
||||
$timeout($scope.init, 0)
|
||||
|
||||
$scope.create = () ->
|
||||
$scope.$on 'create', () ->
|
||||
projectId = $scope.data.selectedProjectId
|
||||
path = $scope.data.selectedProjectEntity
|
||||
name = $scope.data.name
|
||||
if !name || !path || !projectId
|
||||
_reset(err: true)
|
||||
return
|
||||
if $scope.state.isOutputFilesMode
|
||||
provider = 'project_output_file'
|
||||
payload = {
|
||||
source_project_id: projectId,
|
||||
source_output_file_path: $scope.data.selectedProjectOutputFile,
|
||||
build_id: $scope.data.buildId
|
||||
}
|
||||
else
|
||||
provider = 'project_file'
|
||||
payload = {
|
||||
source_project_id: projectId,
|
||||
source_entity_path: $scope.data.selectedProjectEntity
|
||||
}
|
||||
_setInFlight('create')
|
||||
ide.fileTreeManager
|
||||
.createLinkedFile(name, parent_folder, 'project_file', {
|
||||
source_project_id: projectId,
|
||||
source_entity_path: path
|
||||
})
|
||||
.createLinkedFile(name, $scope.parent_folder, provider, payload)
|
||||
.then () ->
|
||||
_reset(err: false)
|
||||
$modalInstance.close()
|
||||
$scope.$emit 'done'
|
||||
.catch (response)->
|
||||
{ data } = response
|
||||
_reset(err: true)
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
|
||||
]
|
||||
|
||||
# TODO: rename all this to UrlLinkedFilModalController
|
||||
App.controller "LinkedFileModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
App.controller "UrlLinkedFileModalController", [
|
||||
"$scope", "ide", "$timeout"
|
||||
($scope, ide, $timeout) ->
|
||||
$scope.inputs =
|
||||
name: ""
|
||||
url: ""
|
||||
$scope.nameChangedByUser = false
|
||||
$scope.state =
|
||||
inflight: false
|
||||
|
||||
$modalInstance.opened.then () ->
|
||||
$timeout () ->
|
||||
$scope.$broadcast "open"
|
||||
, 200
|
||||
$timeout () ->
|
||||
$scope.$broadcast "open"
|
||||
, 200
|
||||
|
||||
validate = () ->
|
||||
{name, url} = $scope.inputs
|
||||
if !name? or name.length == 0
|
||||
$scope.state.valid = false
|
||||
else if !url? or url.length == 0
|
||||
$scope.state.valid = false
|
||||
else
|
||||
$scope.state.valid = true
|
||||
$scope.$watch 'inputs.name', validate
|
||||
$scope.$watch 'inputs.url', validate
|
||||
|
||||
$scope.$watch "inputs.url", (url) ->
|
||||
if url? and url != "" and !$scope.nameChangedByUser
|
||||
|
@ -347,7 +423,7 @@ define [
|
|||
if parts.length > 1 # Wait for at one /
|
||||
$scope.inputs.name = parts[0]
|
||||
|
||||
$scope.create = () ->
|
||||
$scope.$on 'create', () ->
|
||||
{name, url} = $scope.inputs
|
||||
if !name? or name.length == 0
|
||||
return
|
||||
|
@ -355,15 +431,13 @@ define [
|
|||
return
|
||||
$scope.state.inflight = true
|
||||
ide.fileTreeManager
|
||||
.createLinkedFile(name, parent_folder, 'url', {url})
|
||||
.createLinkedFile(name, $scope.parent_folder, 'url', {url})
|
||||
.then () ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
$scope.$emit 'done'
|
||||
.catch (response)->
|
||||
{ data } = response
|
||||
$scope.error = data
|
||||
$scope.state.inflight = false
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
]
|
||||
|
|
|
@ -4,12 +4,6 @@ define [
|
|||
], (App) ->
|
||||
App.controller "TestControlsController", ($scope) ->
|
||||
|
||||
$scope.openProjectLinkedFileModal = () ->
|
||||
window.openProjectLinkedFileModal()
|
||||
|
||||
$scope.openLinkedFileModal = () ->
|
||||
window.openLinkedFileModal()
|
||||
|
||||
$scope.richText = () ->
|
||||
current = window.location.toString()
|
||||
target = "#{current}#{if window.location.search then '&' else '?'}rt=true"
|
||||
|
|
|
@ -261,3 +261,43 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-new-file {
|
||||
padding: 0;
|
||||
table {
|
||||
width: 100%;
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
.toggle-output-files-button {
|
||||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
.modal-new-file--list {
|
||||
background-color: @modal-footer-background-color;
|
||||
width: 220px;
|
||||
ul {
|
||||
li {
|
||||
padding: (@line-height-computed / 4);
|
||||
a {
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
li.active {
|
||||
background-color: white;
|
||||
a {
|
||||
color: @link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-new-file--body {
|
||||
padding: 20px;
|
||||
padding-top: (@line-height-computed / 4);
|
||||
}
|
||||
|
||||
.modal-new-file--body-upload {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
|
|
@ -10,13 +10,17 @@
|
|||
}
|
||||
.qq-uploader-selector {
|
||||
text-align: center;
|
||||
.drag-here {
|
||||
border: 1px dashed #666;
|
||||
vertical-align: middle;
|
||||
}
|
||||
border: 1px dashed #666;
|
||||
border-radius: 6px;
|
||||
vertical-align: middle;
|
||||
.help {
|
||||
margin-top: 6px;
|
||||
}
|
||||
min-height: 200px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
/*.qq-upload-button-selector {
|
||||
display: block;
|
||||
|
|
|
@ -8,6 +8,8 @@ MockFileStoreApi = require './helpers/MockFileStoreApi'
|
|||
request = require "./helpers/request"
|
||||
User = require "./helpers/User"
|
||||
|
||||
MockClsiApi = require "./helpers/MockClsiApi"
|
||||
|
||||
|
||||
express = require("express")
|
||||
LinkedUrlProxy = express()
|
||||
|
@ -116,7 +118,6 @@ describe "LinkedFiles", ->
|
|||
provider: 'project_file',
|
||||
source_project_id: @project_two_id,
|
||||
source_entity_path: "/#{@source_doc_name}",
|
||||
source_project_display_name: "plf-test-two"
|
||||
}
|
||||
expect(firstFile.name).to.equal('test-link.txt')
|
||||
done()
|
||||
|
@ -149,7 +150,7 @@ describe "LinkedFiles", ->
|
|||
source_entity_path: "/#{@source_doc_name}",
|
||||
}, (error, response, body) =>
|
||||
expect(response.statusCode).to.equal 403
|
||||
expect(body).to.equal 'Cannot create linked file'
|
||||
expect(body).to.equal 'You do not have access to this project'
|
||||
done()
|
||||
|
||||
describe "with a linked project_file from a v1 project that has not been imported", ->
|
||||
|
@ -344,3 +345,105 @@ describe "LinkedFiles", ->
|
|||
|
||||
# TODO: Add test for asking for host that return ENOTFOUND
|
||||
# (This will probably end up handled by the proxy)
|
||||
|
||||
describe "creating a linked output file", ->
|
||||
before (done) ->
|
||||
async.series [
|
||||
(cb) =>
|
||||
@owner.createProject 'output-test-one', {template: 'blank'}, (error, project_id) =>
|
||||
@project_one_id = project_id
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.getProject @project_one_id, (error, project) =>
|
||||
@project_one = project
|
||||
@project_one_root_folder_id = project.rootFolder[0]._id.toString()
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.createProject 'output-test-two', {template: 'blank'}, (error, project_id) =>
|
||||
@project_two_id = project_id
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.getProject @project_two_id, (error, project) =>
|
||||
@project_two = project
|
||||
@project_two_root_folder_id = project.rootFolder[0]._id.toString()
|
||||
cb(error)
|
||||
], done
|
||||
|
||||
it 'should import the project.pdf file from the source project', (done) ->
|
||||
@owner.request.post {
|
||||
url: "/project/#{@project_one_id}/linked_file",
|
||||
json:
|
||||
name: 'test.pdf',
|
||||
parent_folder_id: @project_one_root_folder_id,
|
||||
provider: 'project_output_file',
|
||||
data:
|
||||
source_project_id: @project_two_id,
|
||||
source_output_file_path: "project.pdf",
|
||||
build_id: '1234-abcd'
|
||||
}, (error, response, body) =>
|
||||
new_file_id = body.new_file_id
|
||||
@existing_file_id = new_file_id
|
||||
expect(new_file_id).to.exist
|
||||
@owner.getProject @project_one_id, (error, project) =>
|
||||
return done(error) if error?
|
||||
firstFile = project.rootFolder[0].fileRefs[0]
|
||||
expect(firstFile._id.toString()).to.equal(new_file_id.toString())
|
||||
expect(firstFile.linkedFileData).to.deep.equal {
|
||||
provider: 'project_output_file',
|
||||
source_project_id: @project_two_id,
|
||||
source_output_file_path: "project.pdf",
|
||||
build_id: '1234-abcd'
|
||||
}
|
||||
expect(firstFile.name).to.equal('test.pdf')
|
||||
done()
|
||||
|
||||
it 'should refresh the file', (done) ->
|
||||
@owner.request.post {
|
||||
url: "/project/#{@project_one_id}/linked_file/#{@existing_file_id}/refresh",
|
||||
json: true
|
||||
}, (error, response, body) =>
|
||||
new_file_id = body.new_file_id
|
||||
expect(new_file_id).to.exist
|
||||
expect(new_file_id).to.not.equal @existing_file_id
|
||||
@refreshed_file_id = new_file_id
|
||||
@owner.getProject @project_one_id, (error, project) =>
|
||||
return done(error) if error?
|
||||
firstFile = project.rootFolder[0].fileRefs[0]
|
||||
expect(firstFile._id.toString()).to.equal(new_file_id.toString())
|
||||
expect(firstFile.name).to.equal('test.pdf')
|
||||
done()
|
||||
|
||||
describe "with a linked project_output_file from a v1 project that has not been imported", ->
|
||||
before (done) ->
|
||||
async.series [
|
||||
(cb) =>
|
||||
@owner.createProject 'output-v1-test-one', {template: 'blank'}, (error, project_id) =>
|
||||
@project_one_id = project_id
|
||||
cb(error)
|
||||
(cb) =>
|
||||
@owner.getProject @project_one_id, (error, project) =>
|
||||
@project_one = project
|
||||
@project_one_root_folder_id = project.rootFolder[0]._id.toString()
|
||||
@project_one.rootFolder[0].fileRefs.push {
|
||||
linkedFileData: {
|
||||
provider: "project_output_file",
|
||||
v1_source_doc_id: 9999999, # We won't find this id in the database
|
||||
source_output_file_path: "project.pdf",
|
||||
build_id: '123'
|
||||
},
|
||||
_id: "abcdef",
|
||||
rev: 0,
|
||||
created: new Date(),
|
||||
name: "whatever.pdf"
|
||||
}
|
||||
@owner.saveProject @project_one, cb
|
||||
], done
|
||||
|
||||
it 'should refuse to refresh', (done) ->
|
||||
@owner.request.post {
|
||||
url: "/project/#{@project_one_id}/linked_file/abcdef/refresh",
|
||||
json: true
|
||||
}, (error, response, body) =>
|
||||
expect(response.statusCode).to.equal 409
|
||||
expect(body).to.equal "Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file"
|
||||
done()
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
express = require("express")
|
||||
bodyParser = require "body-parser"
|
||||
app = express()
|
||||
|
||||
module.exports = MockClsiApi =
|
||||
run: () ->
|
||||
app.post "/project/:project_id/compile", (req, res, next) =>
|
||||
|
||||
compile = (req, res, next) =>
|
||||
res.status(200).send {
|
||||
compile:
|
||||
status: 'success'
|
||||
|
@ -21,6 +23,9 @@ module.exports = MockClsiApi =
|
|||
]
|
||||
}
|
||||
|
||||
app.post "/project/:project_id/compile", compile
|
||||
app.post "/project/:project_id/user/:user_id/compile", compile
|
||||
|
||||
app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) ->
|
||||
filename = req.params[0]
|
||||
if filename == 'project.pdf'
|
||||
|
@ -30,6 +35,12 @@ module.exports = MockClsiApi =
|
|||
else
|
||||
res.sendStatus(404)
|
||||
|
||||
app.get "/project/:project_id/user/:user_id/build/:build_id/output/:output_path", (req, res, next) =>
|
||||
res.status(200).send("hello")
|
||||
|
||||
app.get "/project/:project_id/status", (req, res, next) =>
|
||||
res.status(200).send()
|
||||
|
||||
app.listen 3013, (error) ->
|
||||
throw error if error?
|
||||
.on "error", (error) ->
|
||||
|
|
|
@ -44,6 +44,8 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
else
|
||||
@._id = file_id
|
||||
@rev = 0
|
||||
if options.linkedFileData?
|
||||
@linkedFileData = options.linkedFileData
|
||||
|
||||
@docName = "doc-name"
|
||||
@docLines = ['1234','abc']
|
||||
|
@ -121,6 +123,35 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
.calledWithMatch(project_id, projectHistoryId, userId, changesMatcher)
|
||||
.should.equal true
|
||||
|
||||
describe 'copyFileFromExistingProjectWithProject, with linkedFileData', ->
|
||||
|
||||
beforeEach ->
|
||||
@oldProject_id = "123kljadas"
|
||||
@oldFileRef = {
|
||||
_id:"oldFileRef",
|
||||
name:@fileName,
|
||||
linkedFileData: @linkedFileData
|
||||
}
|
||||
@ProjectEntityMongoUpdateHandler._confirmFolder = sinon.stub().yields(folder_id)
|
||||
@ProjectEntityMongoUpdateHandler._putElement = sinon.stub().yields(null, {path:{fileSystem: @fileSystemPath}})
|
||||
|
||||
@ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject @project, folder_id, @oldProject_id, @oldFileRef, userId, @callback
|
||||
|
||||
it 'should copy the file in FileStoreHandler', ->
|
||||
@FileStoreHandler.copyFile
|
||||
.calledWith(@oldProject_id, @oldFileRef._id, project_id, file_id)
|
||||
.should.equal true
|
||||
|
||||
it 'should put file into folder by calling put element, with the linkedFileData', ->
|
||||
@ProjectEntityMongoUpdateHandler._putElement
|
||||
.calledWithMatch(
|
||||
@project,
|
||||
folder_id,
|
||||
{ _id: file_id, name: @fileName, linkedFileData: @linkedFileData},
|
||||
"file"
|
||||
)
|
||||
.should.equal true
|
||||
|
||||
describe 'updateDocLines', ->
|
||||
beforeEach ->
|
||||
@path = "/somewhere/something.tex"
|
||||
|
@ -285,7 +316,7 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
beforeEach ->
|
||||
@path = "/path/to/file"
|
||||
|
||||
@newFile = {_id: file_id, rev: 0, name: @fileName}
|
||||
@newFile = {_id: file_id, rev: 0, name: @fileName, linkedFileData: @linkedFileData}
|
||||
@TpdsUpdateSender.addFile = sinon.stub().yields()
|
||||
@ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
|
||||
@ProjectEntityUpdateHandler.addFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback
|
||||
|
@ -330,7 +361,7 @@ describe 'ProjectEntityUpdateHandler', ->
|
|||
@newFileUrl = "new-file-url"
|
||||
@FileStoreHandler.uploadFileFromDisk = sinon.stub().yields(null, @newFileUrl)
|
||||
|
||||
@newFile = _id: new_file_id, name: "dummy-upload-filename", rev: 0
|
||||
@newFile = _id: new_file_id, name: "dummy-upload-filename", rev: 0, linkedFileData: @linkedFileData
|
||||
@oldFile = _id: file_id
|
||||
@path = "/path/to/file"
|
||||
@ProjectEntityMongoUpdateHandler._insertDeletedFileReference = sinon.stub().yields()
|
||||
|
|
Loading…
Reference in a new issue