Merge pull request #655 from sharelatex/sk-linked-files-output-redux

Linked files from project output
This commit is contained in:
Shane Kilkelly 2018-06-25 10:28:29 +01:00 committed by GitHub
commit f6424ada40
21 changed files with 1159 additions and 583 deletions

View file

@ -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?

View file

@ -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)
}

View file

@ -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,
}

View file

@ -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)

View 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()
}

View file

@ -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)
}

View file

@ -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('://')

View file

@ -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"

View file

@ -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)

View file

@ -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"

View file

@ -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")
| &nbsp;
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")
| &nbsp;
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
| &nbsp;
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
| &nbsp;
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

View file

@ -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")}

View 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
| &nbsp;
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")
| &nbsp;
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")
| &nbsp;
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")
| &nbsp;
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&nbsp;
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

View file

@ -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

View file

@ -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')
]

View file

@ -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"

View file

@ -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;
}

View file

@ -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;

View file

@ -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()

View file

@ -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) ->

View file

@ -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()