Editor: Allow file retrieval after code run

This commit is contained in:
Sebastian Serth
2022-10-04 15:17:16 +02:00
committed by Sebastian Serth
parent fb9672c7a4
commit 60078701f5
22 changed files with 311 additions and 8 deletions

View 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

View 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

View File

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