From 2fd9eac5d2ffa3f97e9dc5ca9fda6e9be75c8ed2 Mon Sep 17 00:00:00 2001
From: Shane Kilkelly <shane@kilkelly.me>
Date: Fri, 8 Jun 2018 16:06:47 +0100
Subject: [PATCH] Backend for project output file agent

---
 .../LinkedFiles/ProjectOutputFileAgent.coffee | 102 ++++++++++++++++++
 app/views/project/editor/binary-file.pug      |  20 +++-
 2 files changed, 121 insertions(+), 1 deletion(-)
 create mode 100644 app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee

diff --git a/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee b/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee
new file mode 100644
index 000000000..4850abd0e
--- /dev/null
+++ b/app/coffee/Features/LinkedFiles/ProjectOutputFileAgent.coffee
@@ -0,0 +1,102 @@
+FileWriter = require('../../infrastructure/FileWriter')
+AuthorizationManager = require('../Authorization/AuthorizationManager')
+ProjectGetter = require('../Project/ProjectGetter')
+FileWriter = require('../../infrastructure/FileWriter')
+Settings = require 'settings-sharelatex'
+CompileManager = require '../Compile/CompileManager'
+CompileController = require '../Compile/CompileController'
+ClsiCookieManager = require '../Compile/ClsiCookieManager'
+_ = require "underscore"
+request = require "request"
+
+
+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
+
+
+OutputFileFetchFailedError = (message) ->
+	error = new Error(message)
+	error.name = 'OutputFileFetchFailedError'
+	error.__proto__ = OutputFileFetchFailedError.prototype
+	return error
+OutputFileFetchFailedError.prototype.__proto__ = Error.prototype
+
+
+module.exports = ProjectOutputFileAgent = {
+
+	sanitizeData: (data) ->
+		return {
+			source_project_id: data.source_project_id,
+			source_output_file_path: data.source_output_file_path
+		}
+
+	canCreate: (data) -> true
+
+	decorateLinkedFileData: (data, callback = (err, newData) ->) ->
+		callback = _.once(callback)
+		ProjectGetter.getProject data.source_project_id, {name: 1}, (err, project) ->
+			return callback(err) if err?
+			if !project?
+				return callback(new ProjectNotFoundError())
+			callback(err, _.extend(data, {source_project_display_name: project.name}))
+
+	checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
+		callback = _.once(callback)
+		{ source_project_id } = data
+		AuthorizationManager.canUserReadProject current_user_id, source_project_id, null, (err, canRead) ->
+			return callback(err) if err?
+			callback(null, canRead)
+
+	_validate: (data) ->
+		data.source_project_id? && data.source_output_file_path?
+
+	writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) ->
+		callback = _.once(callback)
+		# TODO:
+		#   - Compile project
+		#   - Get output file content
+		#   - Write to disk
+		#   - callback with fs-path
+		if !ProjectOutputFileAgent._validate(data)
+			return callback(new BadDataError())
+		{ source_project_id, source_output_file_path } = data
+		CompileManager.compile source_project_id, null, {}, (err) ->
+			return callback(err) if err?
+			url = "#{Settings.apis.clsi.url}/project/#{source_project_id}/output/#{source_output_file_path}"
+			ClsiCookieManager.getCookieJar source_project_id, (err, jar)->
+				return callback(err) if err?
+				oneMinute = 60 * 1000
+				# the base request
+				options = { url: url, method: "GET", timeout: oneMinute, jar : jar }
+				readStream = request(options)
+				readStream.on "error", callback
+				readStream.on "response", (response) ->
+					if 200 <= response.statusCode < 300
+						FileWriter.writeStreamToDisk project_id, readStream, callback
+					else
+						error = new OutputFileFetchFailedError("Output file fetch failed: #{url}")
+						error.statusCode = response.statusCode
+						callback(error)
+
+	handleError: (error, req, res, next) ->
+		if error instanceof BadDataError
+			res.status(400).send("The submitted data is not valid")
+		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
+			next(error)
+}
diff --git a/app/views/project/editor/binary-file.pug b/app/views/project/editor/binary-file.pug
index f4ccf8c9d..bd0e4b973 100644
--- a/app/views/project/editor/binary-file.pug
+++ b/app/views/project/editor/binary-file.pug
@@ -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
@@ -61,7 +63,23 @@ div.binary-file.full-size(
 				|
 				| 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")
+					| {{ openFile.linkedFileData.source_project_display_name }}
+				span(ng-if='openFile.linkedFileData.v1_source_doc_id')
+					| {{ openFile.linkedFileData.source_project_display_name }}
+				| : {{ 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"
-- 
GitLab