diff --git a/apps/dashboard/app/controllers/concerns/pathable.rb b/apps/dashboard/app/controllers/concerns/pathable.rb new file mode 100644 index 0000000000..57947dceab --- /dev/null +++ b/apps/dashboard/app/controllers/concerns/pathable.rb @@ -0,0 +1,47 @@ +module Pathable + extend ActiveSupport::Concern + + def normalized_path(path) + Pathname.new("/#{path.to_s.chomp('/').delete_prefix('/')}") + end + + def parse_path(path = nil, filesystem = nil) + normal_path = normalized_path(path || resolved_path) + filesystem ||= resolved_fs + if filesystem == 'fs' + @path = PosixFile.new(normal_path) + @filesystem = 'fs' + elsif ::Configuration.remote_files_enabled? && filesystem != 'fs' + @path = RemoteFile.new(normal_path, filesystem) + @filesystem = filesystem + else + @path = PosixFile.new(normal_path) + @filesystem = filesystem + raise StandardError, I18n.t('dashboard.files_remote_disabled') + end + end + + def validate_path! + if posix_file? + AllowlistPolicy.default.validate!(@path) + elsif @path.remote_type.nil? + raise StandardError, "Remote #{@path.remote} does not exist" + elsif ::Configuration.allowlist_paths.present? && (@path.remote_type == 'local' || @path.remote_type == 'alias') + # local and alias remotes would allow bypassing the AllowListPolicy + # TODO: Attempt to evaluate the path of them and validate? + raise StandardError, "Remotes of type #{@path.remote_type} are not allowed due to ALLOWLIST_PATH" + end + end + + def posix_file? + @path.is_a?(PosixFile) + end + + def resolved_path + raise NoMethodError, "Must implement resolved_path in #{self.class.to_s} to use Pathable concern" + end + + def resolved_fs + raise NoMethodError, "Must implement resolved_fs in #{self.class.to_s} to use Pathable concern" + end +end diff --git a/apps/dashboard/app/controllers/files_controller.rb b/apps/dashboard/app/controllers/files_controller.rb index 72dbd662e5..502a9ed40c 100644 --- a/apps/dashboard/app/controllers/files_controller.rb +++ b/apps/dashboard/app/controllers/files_controller.rb @@ -3,6 +3,7 @@ # The controller for all the files pages /dashboard/files class FilesController < ApplicationController include ActionController::Live + include Pathable before_action :strip_sendfile_headers, only: [:fs] @@ -39,8 +40,8 @@ def fs end end - # FIXME: below is a large block that should be moved to a model - # if moved to a model the exceptions can be handled there and + # FIXME: below is a large block that should be moved to a concern (Zipable, perhaps?) + # if moved to a concern the exceptions can be handled there and # then this code will be simpler to read # and we can avoid rescuing in a block so we can reintroduce # the block braces which is the Rails convention with the respond_to formats. @@ -192,41 +193,16 @@ def strip_sendfile_headers request.headers['HTTP_X_ACCEL_MAPPING'] = nil end - def normalized_path(path = params[:filepath]) - Pathname.new("/#{path.to_s.chomp('/').delete_prefix('/')}") + # Required for use with Pathable concern (app/controllers/concerns/pathable.rb) + def resolved_path + params[:filepath] end - - def parse_path(path = params[:filepath], filesystem = params[:fs]) - normal_path = normalized_path(path) - if filesystem == 'fs' - @path = PosixFile.new(normal_path) - @filesystem = 'fs' - elsif ::Configuration.remote_files_enabled? && filesystem != 'fs' - @path = RemoteFile.new(normal_path, filesystem) - @filesystem = filesystem - else - @path = PosixFile.new(normal_path) - @filesystem = filesystem - raise StandardError, I18n.t('dashboard.files_remote_disabled') - end - end - - def validate_path! - if posix_file? - AllowlistPolicy.default.validate!(@path) - elsif @path.remote_type.nil? - raise StandardError, "Remote #{@path.remote} does not exist" - elsif ::Configuration.allowlist_paths.present? && (@path.remote_type == 'local' || @path.remote_type == 'alias') - # local and alias remotes would allow bypassing the AllowListPolicy - # TODO: Attempt to evaluate the path of them and validate? - raise StandardError, "Remotes of type #{@path.remote_type} are not allowed due to ALLOWLIST_PATH" - end + + # Required for use with Pathable concern (app/controllers/concerns/pathable.rb) + def resolved_fs + params[:fs] end - - def posix_file? - @path.is_a?(PosixFile) - end - + def download? params[:download] end diff --git a/apps/dashboard/app/controllers/projects_controller.rb b/apps/dashboard/app/controllers/projects_controller.rb index cb77117e73..958186913a 100644 --- a/apps/dashboard/app/controllers/projects_controller.rb +++ b/apps/dashboard/app/controllers/projects_controller.rb @@ -2,10 +2,17 @@ # The controller for project pages /dashboard/projects. class ProjectsController < ApplicationController + include Pathable + # GET /projects/:id def show project_id = show_project_params[:id] @project = Project.find(project_id) + + parse_path + validate_path! + @files = @path.ls + if @project.nil? respond_to do |format| message = I18n.t('dashboard.jobs_project_not_found', project_id: project_id) @@ -27,6 +34,19 @@ def show end end end + + # GET /projects/:project_id/files/*filepath + def files + @project = Project.find(files_params[:project_id]) + parse_path(files_params[:filepath]) + validate_path! + Rails.logger.debug("\n\n\n==============================================================") + Rails.logger.debug("ProjectsController#files: request: #{request.methods.sort}") + Rails.logger.debug("==============================================================\n\n\n") + @files = @path.ls + + render(partial: 'projects/directory', locals: { project_id: @project.id, path: @path, files: @files }) + end # GET /projects def index @@ -162,6 +182,16 @@ def stop_job private + # Required for use with Pathable concern (app/controllers/concerns/pathable.rb) + def resolved_path + @project&.directory.to_s + end + + # Required for use with Pathable concern (app/controllers/concerns/pathable.rb) + def resolved_fs + 'fs' + end + def templates Project.templates.map do |project| label = project.title @@ -179,6 +209,10 @@ def project_params .permit(:name, :directory, :description, :icon, :id, :template) end + def files_params + params.permit(:project_id, :filepath) + end + def show_project_params params.permit(:id) end diff --git a/apps/dashboard/app/javascript/application.js b/apps/dashboard/app/javascript/application.js index b8639950eb..3b3fc068e6 100644 --- a/apps/dashboard/app/javascript/application.js +++ b/apps/dashboard/app/javascript/application.js @@ -20,6 +20,7 @@ import 'datatables.net'; import 'datatables.net-bs4/js/dataTables.bootstrap4'; import 'datatables.net-select/js/dataTables.select'; import 'datatables.net-plugins/api/processing().mjs'; +import "@hotwired/turbo-rails" import Rails from '@rails/ujs'; diff --git a/apps/dashboard/app/views/layouts/application.html.erb b/apps/dashboard/app/views/layouts/application.html.erb index 531b92fe86..613e185fb8 100644 --- a/apps/dashboard/app/views/layouts/application.html.erb +++ b/apps/dashboard/app/views/layouts/application.html.erb @@ -1,3 +1,4 @@ +<%= return "turbo_rails/frame" if turbo_frame_request? %> diff --git a/apps/dashboard/app/views/projects/_directory.html.erb b/apps/dashboard/app/views/projects/_directory.html.erb new file mode 100644 index 0000000000..6e719b1aef --- /dev/null +++ b/apps/dashboard/app/views/projects/_directory.html.erb @@ -0,0 +1,21 @@ +<%= turbo_frame_tag "project_files" do %> + + + + + + + + + + + + + + + <%= render partial: "files", locals: { project_id: project.id, path: path, files: files } %> + +
+ + TypeNameActionsSizeModified atOwnerMode
+<% end %> \ No newline at end of file diff --git a/apps/dashboard/app/views/projects/_files.html.erb b/apps/dashboard/app/views/projects/_files.html.erb new file mode 100644 index 0000000000..14c39c432d --- /dev/null +++ b/apps/dashboard/app/views/projects/_files.html.erb @@ -0,0 +1,54 @@ + + + + + + + + <%= link_to("..", project_files_path(project_id: project_id, filepath: sanitize("#{path}/..")), data: { turbo_frame: "project_files" }) %> + + + + + + + + <% files.each do |file| %> + + + + + + <%= file[:type] %> + + + <%= link_to(file[:name], project_files_path(project_id: project_id, filepath: sanitize("#{path}/#{file[:name]}")), data: { turbo_frame: "project_files" }) %> + + + Actions +
+ + + +
+ + + <%= file[:size] %> + + + <%= file[:modified_at] %> + + + <%= file[:owner] %> + + + <%= file[:mode] %> + + + <%- end -%> \ No newline at end of file diff --git a/apps/dashboard/app/views/projects/show.html.erb b/apps/dashboard/app/views/projects/show.html.erb index 29d1522dc9..c5f2380a79 100644 --- a/apps/dashboard/app/views/projects/show.html.erb +++ b/apps/dashboard/app/views/projects/show.html.erb @@ -60,7 +60,7 @@ -
+

Active Jobs

<%= render(partial: 'job_details', collection: @project.active_jobs, as: :job, locals: { project: @project }) %> @@ -77,6 +77,10 @@
+
+

<%= "#{@project.name} (Project ID: #{@project.id})" %>

+ <%= render partial: 'directory', locals: { project: @project, path: @path, files: @files } %> +
<% unless @project.readme_path.nil? %> diff --git a/apps/dashboard/config/routes.rb b/apps/dashboard/config/routes.rb index 54c639252c..d5e93352d8 100644 --- a/apps/dashboard/config/routes.rb +++ b/apps/dashboard/config/routes.rb @@ -15,6 +15,9 @@ post 'save', on: :member end end + if Configuration.can_access_files? + get 'projects/:project_id/files/*filepath' => 'projects#files', as: 'project_files' + end end # in production, if the user doesn't have access to the files app directory, we hide the routes diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 51f03c7e74..7eb1d7cead 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -2,6 +2,7 @@ "name": "openondemand-dashboard", "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", + "@hotwired/turbo-rails": "^8.0.12", "@popperjs/core": "^2.11.8", "@rails/ujs": "^7.0.1", "@uppy/core": "^4.0", diff --git a/apps/dashboard/yarn.lock b/apps/dashboard/yarn.lock index 19cee67424..4b49dde273 100644 --- a/apps/dashboard/yarn.lock +++ b/apps/dashboard/yarn.lock @@ -12,11 +12,29 @@ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5" integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg== +"@hotwired/turbo-rails@^8.0.12": + version "8.0.12" + resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-8.0.12.tgz#6f1a2661122c0a2bf717f3bc68b5106638798c89" + integrity sha512-ZXwu9ez+Gd4RQNeHIitqOQgi/LyqY8J4JqsUN0nnYiZDBRq7IreeFdMbz29VdJpIsmYqwooE4cFzPU7QvJkQkA== + dependencies: + "@hotwired/turbo" "^8.0.12" + "@rails/actioncable" "^7.0" + +"@hotwired/turbo@^8.0.12": + version "8.0.12" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.12.tgz#50aa8345d7f62402680c6d2d9814660761837001" + integrity sha512-l3BiQRkD7qrnQv6ms6sqPLczvwbQpXt5iAVwjDvX0iumrz6yEonQkNAzNjeDX25/OJMFDTxpHjkJZHGpM9ikWw== + "@popperjs/core@^2.11.8": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@rails/actioncable@^7.0": + version "7.2.200" + resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.2.200.tgz#7f56b3313762dbb85b64490aa33c5f431419aafd" + integrity sha512-gVmi3MabEa+Bkatvw0/k1Y3WTidcIf3qNbb9L8qc+AmT2UmkVqUZhJpSLvKmH10twCYIGzn7yySW/lOpg81Duw== + "@rails/ujs@^7.0.1": version "7.1.500" resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.1.500.tgz#84bb037b6a823ec7fb7782a2ac03452deb505128"