Editor: Allow file retrieval after code run
This commit is contained in:

committed by
Sebastian Serth

parent
fb9672c7a4
commit
60078701f5
35
app/controllers/concerns/file_conversion.rb
Normal file
35
app/controllers/concerns/file_conversion.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module FileConversion
|
||||
private
|
||||
|
||||
def convert_files_json_to_files(files_json)
|
||||
all_file_types = FileType.all
|
||||
directories = []
|
||||
files = files_json['files'].filter_map do |file|
|
||||
# entryType: `-` describes a regular file, `d` a directory. See `info ls` for others
|
||||
directories.push(file['name']) if file['entryType'] == 'd'
|
||||
next unless file['entryType'] == '-'
|
||||
|
||||
extension = File.extname(file['name'])
|
||||
name = File.basename(file['name'], extension)
|
||||
path = File.dirname(file['name']).sub(%r{^(?>\./|\.)}, '').presence
|
||||
file_type = all_file_types.detect {|ft| ft.file_extension == extension } || FileType.new(file_extension: extension)
|
||||
CodeOcean::File.new(
|
||||
name: name,
|
||||
path: path,
|
||||
size: file['size'],
|
||||
owner: file['owner'],
|
||||
group: file['group'],
|
||||
permissions: file['permissions'],
|
||||
updated_at: file['modificationTime'],
|
||||
file_type: file_type
|
||||
)
|
||||
end
|
||||
[augment_files_for_download(files), directories]
|
||||
end
|
||||
|
||||
def augment_files_for_download(files)
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
70
app/controllers/live_streams_controller.rb
Normal file
70
app/controllers/live_streams_controller.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LiveStreamsController < ApplicationController
|
||||
# Including ActionController::Live changes all actions in this controller!
|
||||
# Therefore, it is extracted into a separate controller
|
||||
include ActionController::Live
|
||||
|
||||
def download_submission_file
|
||||
begin
|
||||
@submission = authorize AuthenticatedUrlHelper.retrieve!(Submission, request, force_render_host: false)
|
||||
rescue Pundit::NotAuthorizedError
|
||||
# TODO: Option to disable?
|
||||
# Using the submission ID parameter would allow looking up the corresponding exercise ID
|
||||
# Therefore, we just redirect to the root_path, but actually expect to redirect back (that should work!)
|
||||
redirect_back(fallback_location: root_path, alert: t('exercises.download_file_tree.gone'))
|
||||
end
|
||||
|
||||
desired_file = params[:filename].to_s
|
||||
runner = Runner.for(current_user, @submission.exercise.execution_environment)
|
||||
fallback_location = implement_exercise_path(@submission.exercise)
|
||||
send_runner_file(runner, desired_file, fallback_location)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_runner_file(runner, desired_file, redirect_fallback = root_path)
|
||||
filename = File.basename(desired_file)
|
||||
send_stream(filename: filename, disposition: 'attachment') do |stream|
|
||||
runner.download_file desired_file do |chunk, overall_size, content_type|
|
||||
unless response.committed?
|
||||
# Disable Rack::ETag, which would otherwise cause the response to be cached
|
||||
# See https://github.com/rack/rack/issues/1619#issuecomment-848460528
|
||||
response.set_header('Last-Modified', Time.now.httpdate)
|
||||
response.set_header('Content-Length', overall_size) if overall_size
|
||||
response.set_header('Content-Type', content_type) if content_type
|
||||
# Commit the response headers immediately, as streaming would otherwise remove the Content-Length header
|
||||
# This will prevent chunked transfer encoding from being used, which is okay as we know the overall size
|
||||
# See https://github.com/rails/rails/issues/18714
|
||||
response.commit!
|
||||
end
|
||||
|
||||
if stream.connected?
|
||||
stream.write chunk
|
||||
else
|
||||
# The client disconnected, so we stop streaming
|
||||
break
|
||||
end
|
||||
end
|
||||
rescue Runner::Error
|
||||
redirect_back(fallback_location: redirect_fallback, alert: t('exercises.download_file_tree.gone'))
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Taken from Rails 7, remove when upgrading
|
||||
# rubocop:disable all
|
||||
def send_stream(filename:, disposition: "attachment", type: nil)
|
||||
response.headers["Content-Type"] =
|
||||
(type.is_a?(Symbol) ? Mime[type].to_s : type) ||
|
||||
Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete(".")) ||
|
||||
"application/octet-stream"
|
||||
|
||||
response.headers["Content-Disposition"] =
|
||||
ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
|
||||
|
||||
yield response.stream
|
||||
ensure
|
||||
response.stream.close
|
||||
end
|
||||
# rubocop:enable all
|
||||
end
|
@@ -3,6 +3,7 @@
|
||||
class SubmissionsController < ApplicationController
|
||||
include CommonBehavior
|
||||
include Lti
|
||||
include FileConversion
|
||||
include SubmissionParameters
|
||||
include Tubesock::Hijack
|
||||
|
||||
@@ -177,7 +178,7 @@ class SubmissionsController < ApplicationController
|
||||
send_and_store client_socket, message
|
||||
end
|
||||
|
||||
runner_socket.on :exit do |exit_code|
|
||||
runner_socket.on :exit do |exit_code, files|
|
||||
@testrun[:exit_code] = exit_code
|
||||
exit_statement =
|
||||
if @testrun[:output].empty? && exit_code.zero?
|
||||
@@ -200,6 +201,12 @@ class SubmissionsController < ApplicationController
|
||||
@testrun[:status] = :out_of_memory
|
||||
end
|
||||
|
||||
downloadable_files, = convert_files_json_to_files files
|
||||
if downloadable_files.present?
|
||||
js_tree = FileTree.new(downloadable_files).to_js_tree
|
||||
send_and_store client_socket, {cmd: :files, data: js_tree}
|
||||
end
|
||||
|
||||
close_client_connection(client_socket)
|
||||
end
|
||||
end
|
||||
@@ -427,4 +434,19 @@ class SubmissionsController < ApplicationController
|
||||
rescue JSON::ParserError
|
||||
{cmd: :write, stream: stream, data: data}
|
||||
end
|
||||
|
||||
def augment_files_for_download(files)
|
||||
submission_files = @submission.collect_files
|
||||
files.filter_map do |file|
|
||||
# Reject files that were already present in the submission
|
||||
# We further reject files that share the same name (excl. file extension) and path as a file in the submission
|
||||
# This is, for example, used to filter compiled .class files in Java submissions
|
||||
next if submission_files.any? {|submission_file| submission_file.filepath_without_extension == file.filepath_without_extension }
|
||||
|
||||
# Downloadable files get a signed download_path and an indicator whether we performed a privileged execution
|
||||
file.download_path = AuthenticatedUrlHelper.sign(download_stream_file_submission_url(@submission, file.filepath), @submission)
|
||||
file.privileged_execution = @submission.execution_environment.privileged_execution
|
||||
file
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Reference in New Issue
Block a user