Files
codeocean/app/models/code_ocean/file.rb
Sebastian Serth db56a690c7 Add option to suppress feedback messages
This is used to dynamically exclude some test results from being shown to users, but still allows them to run in the background (e.g., for research).
2023-07-27 10:38:49 +02:00

186 lines
5.9 KiB
Ruby

# frozen_string_literal: true
require File.expand_path('../../uploaders/file_uploader', __dir__)
module CodeOcean
class File < ApplicationRecord
include DefaultValues
DEFAULT_WEIGHT = 1.0
ROLES = %w[regular_file main_file reference_implementation executable_file teacher_defined_test user_defined_file
user_defined_test teacher_defined_linter].freeze
TEACHER_DEFINED_ROLES = ROLES - %w[user_defined_file]
OWNER_READ_PERMISSION = 0o400
OTHER_READ_PERMISSION = 0o004
after_initialize :set_default_values
before_validation :clear_weight, unless: :teacher_defined_assessment?
before_validation :hash_content, if: :content_present?
before_validation :set_ancestor_values, if: :incomplete_descendent?
attr_writer :size
# These attributes are mainly used when retrieving files from a runner
attr_accessor :download_path, :owner, :group, :privileged_execution
attr_reader :permissions
belongs_to :context, polymorphic: true
belongs_to :file, class_name: 'CodeOcean::File', optional: true # This is only required for submissions and is validated below
alias ancestor file
belongs_to :file_type
has_many :files, class_name: 'CodeOcean::File'
has_many :testruns
has_many :comments
alias descendants files
mount_uploader :native_file, FileUploader
scope :editable, -> { where(read_only: false) }
scope :visible, -> { where(hidden: false) }
ROLES.each do |role|
scope :"#{role}s", -> { where(role:) }
end
scope :teacher_defined_assessments, -> { where(role: %w[teacher_defined_test teacher_defined_linter]) }
default_scope { order(path: :asc, name: :asc) }
validates :feedback_message, if: :teacher_defined_assessment?, presence: true
validates :feedback_message, absence: true, unless: :teacher_defined_assessment?
validates :hashed_content, if: :content_present?, presence: true
validates :hidden, inclusion: [true, false]
validates :hidden_feedback, inclusion: [true, false]
validates :name, presence: true
validates :read_only, inclusion: [true, false]
validates :role, inclusion: {in: ROLES}
validates :weight, if: :teacher_defined_assessment?, numericality: true, presence: true
validates :weight, absence: true, unless: :teacher_defined_assessment?
validates :file, presence: true if :context.is_a?(Submission)
validates_with FileNameValidator, fields: %i[name path file_type_id]
ROLES.each do |role|
define_method("#{role}?") { self.role == role }
end
def read
if native_file?
return nil unless native_file_location_valid?
native_file.read
else
content
end
end
def native_file_location_valid?
real_location = Pathname(native_file.current_path).realpath
upload_location = Pathname(::File.join(native_file.root, 'uploads')).realpath
real_location.fnmatch? ::File.join(upload_location.to_s, '**')
end
def ancestor_id
file_id || id
end
def clear_weight
self.weight = nil
end
private :clear_weight
def teacher_defined_assessment?
teacher_defined_test? || teacher_defined_linter?
end
def content_present?
content? || native_file?
end
private :content_present?
def filepath
if path.present?
::File.join(path, name_with_extension)
else
name_with_extension
end
end
def filepath_without_extension
if path.present?
::File.join(path, name)
else
name
end
end
def hash_content
self.hashed_content = Digest::MD5.new.hexdigest(read || '')
end
private :hash_content
def incomplete_descendent?
file_id.present? && file_type_id.blank?
end
private :incomplete_descendent?
def name_with_extension
name.to_s + (file_type&.file_extension || '')
end
def name_with_extension_and_size
"#{name_with_extension} (#{ActionController::Base.helpers.number_to_human_size(size)})"
end
def set_ancestor_values
%i[feedback_message file_type hidden name path read_only role weight].each do |attribute|
send(:"#{attribute}=", ancestor.send(attribute))
end
end
private :set_ancestor_values
def set_default_values
set_default_values_if_present(content: '', hidden: false, read_only: false)
set_default_values_if_present(weight: DEFAULT_WEIGHT) if teacher_defined_assessment?
end
private :set_default_values
def visible
!hidden
end
def size
@size ||= if native_file?
native_file.size
else
content.size
end
end
def permissions=(permission_string)
# We iterate through the permission string (e.g., `rwxrw-r--`) as received through Linux
# For each character in the string, we check for a corresponding permission (which is available if the character is not `-`)
# Then, we use a bit shift to move a `1` to the position of the given permission.
# First, it is moved within a group (e.g., `r` in `rwx` is moved twice to the left, `w` once, `x` not at all)
# Second, the bit is moved in accordance with the group (e.g., the `owner` is moved twice, the `group` once, the `other` group not at all)
# Finally, a sum is created, which technically could be an OR operation as well.
@permissions = permission_string.chars.map.with_index do |permission, index|
next 0 if permission == '-' # No permission
bit = 0b1 << ((2 - index) % 3) # Align bit in respective group
bit << ((2 - (index / 3)) * 3) # Align bit in bytes (for the group)
end.sum
end
def missing_read_permissions?
return false if permissions.blank?
# We use a bitwise AND with the permission bits and compare that to zero
if privileged_execution.present?
(permissions & OWNER_READ_PERMISSION).zero?
else
(permissions & OTHER_READ_PERMISSION).zero?
end
end
end
end