Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/3944 add file browser to projects#show #3981

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 126 additions & 37 deletions apps/dashboard/app/controllers/files_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
class FilesController < ApplicationController
include ActionController::Live

before_action :strip_sendfile_headers, only: [:fs]
before_action :strip_sendfile_headers, only: [:fs, :directory_frame, :file_frame]

def fs
request.format = 'json' if request.headers['HTTP_ACCEPT'].split(',').include?('application/json')

parse_path
@download = fs_params[:download]
parse_path(fs_params[:filepath], fs_params[:fs])
validate_path!

if @path.directory?
Expand All @@ -24,7 +24,7 @@ def fs

format.json do
response.headers['Cache-Control'] = 'no-store'
if params[:can_download]
if fs_params[:can_download]
# check to see if this directory can be downloaded as a zip
can_download, error_message = if ::Configuration.download_enabled?
@path.can_download_as_zip?
Expand All @@ -39,8 +39,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.
Expand Down Expand Up @@ -100,33 +100,56 @@ def fs
show_file
end
rescue StandardError => e
@files = []
flash.now[:alert] = e.message.to_s

logger.error(e.message)
rescue_action(e)
end

respond_to do |format|
format.html do
render :index
end
format.json do
@files = []
# GET - directory for turbo-frame
def directory_frame
sort_by = directory_frame_params[:sort_by] || :name
parse_path(directory_frame_params[:path], 'fs')
validate_path!
@path.raise_if_cant_access_directory_contents
set_files(sort_by)

render( partial: 'files/turbo_frames/directory',
locals: {
path: @path,
files: @files,
sort_by: sort_by
}
)
rescue StandardError => e
rescue_action(e)
end

render :index
end
end
# GET - file for turbo-frame
def file_frame
parse_path(file_frame_params[:path], 'fs')
validate_path!
@path.raise_if_cant_access_directory_contents if @path.directory?
@file = show_file

render( partial: 'files/turbo_frames/file',
locals: {
path: @path,
file: @file,
sort_by: file_frame_params[:sort_by]
}
)
rescue StandardError => e
rescue_action(e)
end

# PUT - create or update
def update
parse_path
parse_path(update_params[:filepath], update_params[:fs])
validate_path!

if params.include?(:dir)
if update_params.include?(:dir)
@path.mkdir
elsif params.include?(:file)
@path.mv_from(params[:file].tempfile)
elsif params.include?(:touch)
elsif update_params.include?(:file)
@path.mv_from(update_params[:file].tempfile)
elsif update_params.include?(:touch)
@path.touch
else
content = request.body.read
Expand All @@ -146,16 +169,16 @@ def update

# POST
def upload
upload_path = uppy_upload_path
upload_path = uppy_upload_path(upload_params[:relativePath], upload_params[:parent], upload_params[:name])

parse_path(upload_path)
parse_path(upload_path, upload_params[:fs])
validate_path!

# Need to remove the tempfile from list of Rack tempfiles to prevent it
# being cleaned up once request completes since Rclone uses the files.
request.env[Rack::RACK_TEMPFILES].reject! { |f| f.path == params[:file].tempfile.path } unless posix_file?
request.env[Rack::RACK_TEMPFILES].reject! { |f| f.path == upload_params[:file].tempfile.path } unless posix_file?

@transfer = @path.handle_upload(params[:file].tempfile)
@transfer = @path.handle_upload(upload_params[:file].tempfile)


if @transfer.kind_of?(Transfer)
Expand All @@ -172,7 +195,7 @@ def upload
end

def edit
parse_path
parse_path(edit_params[:path], edit_params[:fs])
validate_path!

if @path.editable?
Expand All @@ -187,18 +210,37 @@ def edit

private

def rescue_action(exception)
@files = []
flash.now[:alert] = exception.message.to_s

logger.error(exception.message)

respond_to do |format|

format.html do
render :index
end
format.json do
@files = []

render :index
end
end
end

# set these headers to nil so that we (Rails) will read files
# off of disk instead of nginx.
def strip_sendfile_headers
request.headers['HTTP_X_SENDFILE_TYPE'] = nil
request.headers['HTTP_X_ACCEL_MAPPING'] = nil
end

def normalized_path(path = params[:filepath])
def normalized_path(path)
Pathname.new("/#{path.to_s.chomp('/').delete_prefix('/')}")
end

def parse_path(path = params[:filepath], filesystem = params[:fs])
def parse_path(path, filesystem)
normal_path = normalized_path(path)
if filesystem == 'fs'
@path = PosixFile.new(normal_path)
Expand All @@ -225,30 +267,52 @@ def validate_path!
end
end

def set_files(sort_by)
@files = sort_by_column(@path.ls, sort_by)
end

def sort_by_column(files, column)
col = column.to_sym
sorted_files = files.sort_by do |file|
case col
when :name, :owner
file[col].to_s.downcase
when :type
file[:directory] ? 0 : 1
else
file[col].to_i
end
end
end

def posix_file?
@path.is_a?(PosixFile)
end

def download?
params[:download]
@download ||= false
end

def uppy_upload_path
def uppy_upload_path(relative_path, parent, name)
# careful:
#
# File.join '/a/b', '/c' => '/a/b/c'
# Pathname.new('/a/b').join('/c') => '/c'
#
# handle case where uppy.js sets relativePath to "null"
if params[:relativePath] && params[:relativePath] != 'null'
Pathname.new(File.join(params[:parent], params[:relativePath]))
if relative_path && relative_path != 'null'
Pathname.new(File.join(parent, relativePath))
else
Pathname.new(File.join(params[:parent], params[:name]))
Pathname.new(File.join(parent, name))
end
end

def show_file
raise(StandardError, t('dashboard.files_download_not_enabled')) unless ::Configuration.download_enabled?

return File.open(@path.to_s, "r") do |file|
file.read
end if turbo_frame_request?

if posix_file?
send_posix_file
Expand All @@ -272,7 +336,7 @@ def send_posix_file
rescue StandardError => e
logger.warn("failed to determine mime type for file: #{@path} due to error #{e.message}")

if params[:downlaod]
if download?
send_file @path
else
send_file @path, disposition: 'inline'
Expand Down Expand Up @@ -311,4 +375,29 @@ def send_remote_file
ensure
response.stream.close
end

def fs_params
params.permit(:format, :filepath, :fs, :download, :can_download)
end

def directory_frame_params
params.permit(:format, :path, :sort_by)
end

def file_frame_params
params.permit(:format, :path, :sort_by)
end

def update_params
params.permit(:format, :filepath, :fs, :dir, :file, :touch)
end

def upload_params
params.permit(:format, :relativePath, :parent, :name, :fs, :type, :file)
end

def edit_params
params.permit(:format, :path, :fs)
end

end
2 changes: 2 additions & 0 deletions apps/dashboard/app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ class ProjectsController < ApplicationController
def show
project_id = show_project_params[:id]
@project = Project.find(project_id)
@path = @project&.directory

if @project.nil?
respond_to do |format|
message = I18n.t('dashboard.jobs_project_not_found', project_id: project_id)
Expand Down
46 changes: 46 additions & 0 deletions apps/dashboard/app/helpers/files_helper.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Helper for /files pages.
module FilesHelper
include ApplicationHelper

def path_segment_with_slash(filesystem, segment, counter, total)
# TODO: add check for counter == total - 1 if we decide to omit trailing slash on current directory
if counter == 0
Expand All @@ -12,4 +14,48 @@ def path_segment_with_slash(filesystem, segment, counter, total)
segment + " /"
end
end

def files_button(path)
link_to(
frame_path(path),
files_path(fs: 'fs', filepath: path),
target: '_top',
class: 'link-light'
).html_safe
end

def frame_path(path)
return ".../projects#{path.to_s.split('projects')[1]}" if path.to_s.include?('projects')
path_components = path.to_s.split('/')
starting_index = path_components.length < 5 ? 0 : path_components.length - 5
return ".../#{path_components[starting_index..-1].join('/')}"
end

def column_head_link(column, sort_by, path)
link_to(
header_text(column, sort_by),
target_path(column, path),
title: "Show #{path.basename} directory",
class: classes(column, sort_by),
data: { turbo_frame: 'project_directory' }
)
end

def header_text(column, sort_by)
"#{t("dashboard.#{column.to_s}")} #{fa_icon(column.to_s == sort_by.to_s ? 'sort-down' : 'sort', classes: 'fa-md')}".html_safe
end

def target_path(column, path)
directory_frame_path(
{ path: path.to_s,
sort_by: column
}
)
end

def classes(column, sort_by)
classes = ['btn', 'btn-xs', 'btn-hover']
classes << (column.to_s == sort_by.to_s ? ['btn-primary'] : ['btn-outline-primary'])
classes.join(' ')
end
end
1 change: 1 addition & 0 deletions apps/dashboard/app/helpers/projects_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# Helpers for the projects page
module ProjectsHelper

def render_readme(readme_location)
file_content = File.read(readme_location)

Expand Down
7 changes: 7 additions & 0 deletions apps/dashboard/app/javascript/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ import 'datatables.net-bs4/js/dataTables.bootstrap4';
import 'datatables.net-select/js/dataTables.select';
import 'datatables.net-plugins/api/processing().mjs';

// Enables hotwire Turbo Streams/Frames
import "@hotwired/turbo-rails"
import { Turbo } from "@hotwired/turbo-rails"
// Disables Turbo Drive on an app-wide basis to prevent eager-loading links on mouse-over (which is annoying)
// Any links within a <turbo-stream> or <turbo-frame> tag will be eager-loaded as expected.
Turbo.session.drive = false

import Rails from '@rails/ujs';

// Import @popperjs/core for Bootstrap 5
Expand Down
25 changes: 25 additions & 0 deletions apps/dashboard/app/views/files/turbo_frames/_directory.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<%= turbo_frame_tag "project_directory" do %>
<div class="d-flex justify-content-left lead text-weight-800">
<strong><%= frame_path(path) %></strong>
</div>
<table class="table table-striped table-condensed w-100 table-hover caption-right">
<thead>
<tr>
<th><%= column_head_link(:type, sort_by, path) %></th>
<th><%= column_head_link(:name, sort_by, path) %></th>
<th><%= column_head_link(:size, sort_by, path) %></th>
<th><%= column_head_link(:date, sort_by, path) %></th>
<th><%= column_head_link(:owner, sort_by, path) %></th>
<th><%= column_head_link(:mode, sort_by, path) %></th>
</tr>
</thead>
<tbody>
<%= render partial: "files/turbo_frames/files", locals: { path: path, files: files, sort_by: sort_by } %>
</tbody>
</table>
<div class="row content-justied-center col-6 mx-auto">
<div class="btn btn-primary">
<span><i id="new-dir-btn" class="fa fa-folder-open"></i>&nbsp<%= files_button(path) %></span>
</div>
</div>
<% end %>
Loading
Loading