diff --git a/BrainPortal/app/controllers/userfiles_controller.rb b/BrainPortal/app/controllers/userfiles_controller.rb index 3aaf0ad38..13bcdcd8c 100644 --- a/BrainPortal/app/controllers/userfiles_controller.rb +++ b/BrainPortal/app/controllers/userfiles_controller.rb @@ -40,7 +40,7 @@ class UserfilesController < ApplicationController around_action :permission_check, :only => [ :download, :update_multiple, :delete_files, :create_collection, :change_provider, :quality_control, - :export_file_list + :export_file_list, :create_virtual_collection ] MAX_DOWNLOAD_MEGABYTES = 400 @@ -1001,6 +1001,15 @@ def create_collection #:nodoc: return end + virtual_files = FileCollection.where(id: filelist, type: ['CivetVirtualStudy', 'VirtualFileCollection']).to_a + + if virtual_files.present? + virtual_files = Userfile.find_accessible_by_user(virtual_files.pluck(:id), current_user, :access_requested => :read) + flash[:error] = "Collections of Virtual Collection are not allowed. Exclude #{virtual_files.map(&:name).to_sentence} " if virtual_files + redirect_to :action => :index, :format => request.format.to_sym + return + end + collection = FileCollection.new( :user_id => current_user.id, :group_id => file_group, @@ -1040,6 +1049,84 @@ def create_collection #:nodoc: end + # Create a virtual collection from the selected files. + def create_virtual_collection #:nodoc: + filelist = params[:file_ids].uniq || [] + data_provider_id = params[:data_provider_id_for_collection] + collection_name = params[:collection_name] + file_group = current_assignable_group.id + + if data_provider_id.blank? + flash[:error] = "No data provider selected.\n" + redirect_to :action => :index + return + end + + # Handle collection name + if collection_name.blank? + suffix = Time.now.to_i + while Userfile.where(:user_id => current_user.id, :name => "VirtualCollection-#{suffix}").first.present? + suffix += 1 + end + collection_name = "VirtualCollection-#{suffix}" + end + + if ! Userfile.is_legal_filename?(collection_name) + flash[:error] = "Error: collection name '#{collection_name}' is not acceptable (illegal characters?)." + redirect_to :action => :index, :format => request.format.to_sym + return + end + + # Check if the collection name chosen by the user already exists for this user on the data_provider + if current_user.userfiles.exists?(:name => collection_name, :data_provider_id => data_provider_id) + flash[:error] = "Error: collection with name '#{collection_name}' already exists." + redirect_to :action => :index, :format => request.format.to_sym + return + end + + userfiles = Userfile.find_accessible_by_user(filelist, current_user, :access_requested => :read) + + # todo double check how 0 is possible, bad files should cause exception + if userfiles.count == 0 + flash[:error] = "Error: Inaccessible files selected." + redirect_to :action => :index, :format => request.format.to_sym + return + end + + collection = VirtualFileCollection.new( + :user_id => current_user.id, + :group_id => file_group, + :data_provider_id => data_provider_id, + :name => collection_name + ) + + collection.save! + collection.cache_prepare + coldir = collection.cache_full_path + Dir.mkdir(coldir) + + collection.set_virtual_file_collection(userfiles) + + # Save the content and DB model + + collection.sync_to_provider + collection.save + collection.set_size + + # Find the files + userfiles = Userfile + .find_all_accessible_by_user(current_user, :access_requested => :read) + .where(:id => filelist).all.to_a + + if userfiles.empty? + flash[:error] = "You need to select some files first." + redirect_to(:action => :index) + return + end + redirect_to(:controller => :userfiles, :action => :show, :id => collection.id) + + end + # Copy or move files to a new provider. def change_provider #:nodoc: @@ -1206,7 +1293,82 @@ def download #:nodoc: end end - #Extract a file from a collection and register it separately + # Extract files from a virtual collection and register them separately + # in the database. + def extract_from_virtual_collection #:nodoc: + success = failure = 0 + + unless params[:collection_files] && params[:collection_files].size > 0 + flash[:notice] = "No files selected for extraction" + redirect_to :action => :show + return + end + + collection_ids_file_pairs = params[:collection_files].map {|x| x.split('#', 2)} + collections_file_names = collection_ids_file_pairs.group_by { |first, _| first } + # add the object to hash and validate provider access, while keeping primitive keys + + # todo - add option to specify target data_provider + collections_file_names.each do |collection_id, obj_file_list| + collection = FileCollection.find_accessible_by_user(collection_id, current_user, :access_requested => :read) + obj_file_list.each { |pair| pair[0] = collection } + data_provider = collection.data_provider + if data_provider.read_only? + flash[:error] = "Unfortunately file #{obj_filelist.second.first} of collection #{collection.name} is located on a not writable DataProvider #{data_provider.name}, so we can't extract its internal files." + redirect_to :action => :show + return + end + end + + results = collections_file_names.map do |collection_id, collection_file_pairs| + collection = collection_file_pairs.first.first + data_provider = collection.data_provider + collection_path = collection.cache_full_path + collection_file_pairs.map do |_, file| # Extract each file + # Validations; make sure "file" is a path inside the collection + rel_path = Pathname.new(file) + next :not_relative unless rel_path.relative? + full_path = collection_path.parent + rel_path + full_path = File.realpath(full_path.to_s) rescue nil + next :not_resolve unless full_path + next :is_symlink if File.symlink?(full_path.to_s) + next :not_file unless File.file?(full_path.to_s) + next :outside_col unless full_path.start_with? collection_path.to_s + basename = rel_path.basename.to_s + file_type = Userfile.suggested_file_type(basename) || SingleFile + userfile = file_type.new( + :name => basename, + :user_id => current_user.id, + :group_id => collection.group_id, + :data_provider_id => data_provider.id + ) + Dir.chdir(collection_path.parent) do + next :cannot_save_userfile unless userfile.save + userfile.addlog("Extracted from collection '#{collection.name}'.") + begin + userfile.cache_copy_from_local_file(full_path.to_s) + next :ok + rescue + userfile.data_provider_id = nil # nullifying will skip the provider_erase() in the destroy() + userfile.destroy + next :exception_copy + end + end + end + end + success = results.flatten.count { |x| x == :ok } + failure = results.flatten.size - success + if success > 0 + flash[:notice] = "#{success} files were successfully extracted." + end + if failure > 0 + flash[:error] = "#{failure} files could not be extracted." + end + redirect_to :action => :index + end + + + #Extract files from a collection and register it separately #in the database. def extract_from_collection #:nodoc: success = failure = 0 diff --git a/BrainPortal/app/helpers/userfiles_helper.rb b/BrainPortal/app/helpers/userfiles_helper.rb index ad0eb5089..3432b3a47 100644 --- a/BrainPortal/app/helpers/userfiles_helper.rb +++ b/BrainPortal/app/helpers/userfiles_helper.rb @@ -110,11 +110,11 @@ def data_link(file_name, userfile, replace_div_id="sub_viewer_filecollection_cbr end elsif display_name =~ /\.html$/i # TODO: this will never happen if we ever create a HtmlFile model with at least one viewer link_to "#{display_name}", - stream_userfile_path(@userfile, :file_path => file_name, :disposition => 'inline'), + stream_userfile_path(userfile, :file_path => file_name, :disposition => 'inline'), :target => '_BLANK' else link_to h(display_name), - url_for(:action => :content, :content_loader => :collection_file, :arguments => file_name) + url_for(:action => :content, :id => userfile.id, :content_loader => :collection_file, :arguments => file_name) end end diff --git a/BrainPortal/app/views/userfiles/_dialogs.html.erb b/BrainPortal/app/views/userfiles/_dialogs.html.erb index 4e670c292..08a395e4c 100644 --- a/BrainPortal/app/views/userfiles/_dialogs.html.erb +++ b/BrainPortal/app/views/userfiles/_dialogs.html.erb @@ -349,6 +349,36 @@ +
+
+ + + + ⚠ Invalid! + +
+ + + <%= + data_provider_select('data_provider_id_for_collection', + { :data_providers => writable_dps }, + { + :id => 'co-dp', + :class => 'dlg-fld', + :'data-placeholder' => "A data provider..." + } + ) + %> +

+
+
+ +
+
  • +
    + New virtual collection + +
    +
  • + +
  • . +# +-%> + +<% if @userfile.is_locally_synced? %> + + <% @cbfl = @userfile.cached_cbrain_file_list %> + +

    + This Virtual File Collection consists of other CBRAIN files and collections + listed below. This simple view does allow you to explore sub-directories or + subfiles, for that you still can use Virtual File Collection view. + Note you only see files that you have assess too. +

    This is an experimental feature. + The files and collection could change name or content over time, potentially making collection unusable. + In the + table below, the names of the files are repeated: the first one is the name as + known within CBRAIN, and the second one is the original name of file when collection + was created. +

    + + <% + csv_array = @cbfl.cached_csv_array + per_page = 500 + nb_row = csv_array.size + page = (params[:page] || 1).to_i + page = 1 if page < 1 + csv_array = WillPaginate::Collection.create(page, per_page) do |pager| + pager.replace(csv_array[(page-1)*per_page, per_page] || []) + pager.total_entries = csv_array.size + pager + end + %> + +
    + + <%= will_paginate csv_array, + :params => { :controller => :userfiles, :action => :show, :sort_index => @sort_index }, + :container => false + %> + (<%= pluralize nb_row, "file" %> in this list) + +
    + + + + <% + ################################ + # Top headers of table + ################################ + %> + + + <% attlist = @cbfl.class.const_get('ATTRIBUTES_LIST') %> + <% attlist.each do |att| %> + <% att = :project if att == :group_id %> + + <% end %> + + + <% + ################################ + # Main body of table + ################################ + %> + + <% csv_array.each do |cvs_row| %> + + <% cur_file = nil %> + <% attlist.each_with_index do |att,idx|%> + <% val = cvs_row[idx] %> + + <% end %> + + <% end %> + +
    <%= att.to_s.sub(/_id\z/,"").classify.gsub(/(.+)([A-Z])/, '\1 \2') %>
    + <% if att == :id %> + <% cur_file = Userfile.find_all_accessible_by_user(current_user, :access_requested => :read).where(:id => val).first %> + <%= val %> : <%= link_to_userfile_if_accessible(cur_file) %> + <% else %> + <%= val.nil? ? "-" : val %> + <% end %> +
    +

    + <%= link_to 'Extract a file list', { :action => 'export_file_list', :file_ids => @userfile.get_userfiles.pluck(:id) }, :class => "button", :method => :post %> + +<% end %> diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_directory_contents.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_directory_contents.html.erb new file mode 100644 index 000000000..c237a665b --- /dev/null +++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_directory_contents.html.erb @@ -0,0 +1,85 @@ + +<%- +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +-%> + +<% limit = 500 %> +<% base_dir = base_directory rescue params[:base_directory] %> +<% base_dir = base_dir.presence || "." %> + +<% file_list ||= ( @userfile.list_linked_files(base_dir, [:regular, :directory, :link]) rescue [] ) %> +<% @userfile&.list_errors&.each do |error_message| %> + "> + + Error. <%= error_message %> + +<% end %> + +<% if file_list.blank? %> + + "> + + (Empty) + + + +<% else %> + + <% for file in file_list[0,limit] %> + <% fname = file.name %> + <% fname = fname.delete_prefix(@userfile.name + '/') unless @userfile.id == file.userfile.id %> + <% if file.symbolic_type == :directory %> + <%= on_click_ajax_replace( { :element => "tr", + :url => url_for( + :id => file.userfile.id, + :action => :display, + :viewer => "subdirectory_contents", + :viewer_userfile_class => "VirtualFileCollection", + :base_directory => ".", + :apply_div => "false" + ), + :position => "after", + :before => "Loading..." + }, + { :class => "#{cycle("list-odd", "list-even")}", + :id => file.name.gsub(/\W+/, "_") + } + ) do %> + <%= render :file => @viewer.partial_path(:plain_file_list_row), :locals => {:file => file} %> + <% end %> + <% else %> + "> + <%= render :file => @viewer.partial_path(:plain_file_list_row), :locals => {:file => file} %> + + <% end %> + <% end %> + + <% if file_list.size > limit %> + "> + + <%= (" " * 6 * file_list.first.depth).html_safe %> ... <%= image_tag "/images/lotsa_files_icon.png" %> <%= pluralize(file_list.size-limit, "more entry") %> + + + <% end %> + +<% end %> + diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.html.erb new file mode 100644 index 000000000..9ac5a84f8 --- /dev/null +++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.html.erb @@ -0,0 +1,39 @@ + +<%- +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +-%> + +<% + # This partial can be invoked directly as a viewer from the display action, + # or rendered as part of another piece of view code. As such it will + # accept a "base_directory" either as a params[] or as a local variable + base_dir = base_directory rescue params[:base_directory] +%> + +<%= render :file => VirtualFileCollection.view_path(:file_collection_form), + :locals => { :base_directory => base_dir } %> + +
    +This is a virtual file collection, which means the parts of this collections are not stored together, but are other +collections or individual files, registered in CBRAIN separately. This is an experimental feature. + +You can navigate the data that you have access below. diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.json.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.json.erb new file mode 100644 index 000000000..4cd7bc06f --- /dev/null +++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.json.erb @@ -0,0 +1,2 @@ +<%= @userfile.list_linked_files.to_json %> + diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_form.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_form.html.erb new file mode 100644 index 000000000..c956f8616 --- /dev/null +++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_form.html.erb @@ -0,0 +1,62 @@ + +<%- +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +-%> + +<% + # This partial requires one local variable: + # + # base_directory : the subdirectory inside the userfile where we start to render the directory content +%> + +<% if @userfile.num_files && @userfile.num_files > 0 %> + + + <%= form_for @userfile, :as => :userfile, + :url => { :controller => :userfiles, + :action => :extract_from_virtual_collection + }, + :html => { :method => :post, + :id => "userfile_edit_#{@userfile.id}_#{base_directory}" + } do |f| %> + + <%= ajax_element(display_userfile_path(@userfile, + :viewer => :file_collection_top_table, + :viewer_userfile_class => :VirtualFileCollection, + :base_directory => base_directory, + ), :class => "loading_message") do %> +
    + Loading... +
    + <% end %> + + <% if @userfile.is_locally_synced? %> +

    + <%= submit_tag "Extract Files from Sub-Collections" %> + <% end %> + + + + <% end %> + +<% end %> + diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_top_table.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_top_table.html.erb new file mode 100644 index 000000000..2c202d240 --- /dev/null +++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_top_table.html.erb @@ -0,0 +1,47 @@ + +<%- +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +-%> + +<% + # This partial requires one params variable: + # params[:base_directory] : a relative path to the subdirectory inside the userfile; we will list from that point. +%> + +<% + base_dir = params[:base_directory].presence || "" +%> +

    + + + + + + + + <%= render :file => VirtualFileCollection.view_path(:directory_contents), + :locals => { :base_directory => base_dir } + %> +
    + + FileDLSize
    + diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_plain_file_list_row.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_plain_file_list_row.html.erb new file mode 100644 index 000000000..98322cad1 --- /dev/null +++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_plain_file_list_row.html.erb @@ -0,0 +1,87 @@ + +<%- +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +-%> + +<% + # This partial receives one local variable: + # file : a file structure as returned by the FileCollection listing method + # Note that is contains a full relative path, starting with the @userfile's name itself. +%> + + + <% if @userfile.is_locally_synced? && file.symbolic_type == :regular %> + <%= check_box_tag("collection_files[]", "#{@userfile.id}##{file.name}", false, :class => "collection_checkbox", :id => nil) %> + <% end %> + + + +   + +

    + <% if file.symbolic_type == :directory %> + <%= image_tag "/images/folder_icon_solid.png" %> + <% else %> + <%= image_tag "/images/file_icon.png" %> + <% end %> + +   + <% bname = Pathname.new(file.name).basename.to_s %> + + <% fname = file.name %> + <% fname = fname.delete_prefix(@userfile.name + '/') if @userfile != file.userfile %> + + <% if file.size > 0 %> + <%= data_link fname, file.userfile %> + <% else %> + <%= bname %> + <% end %> + + <% if file.symbolic_type == :directory %> +   + Expand + + <% end %> +
    + + + + <% if file.symbolic_type == :regular && file.size > 0 && file.size < UserfilesController::MAX_DOWNLOAD_MEGABYTES.megabytes %> + + <% if @userfile.id != file.userfile.id %> + <% download_url = url_for(:action => :content, :content_loader => :collection_file, :id => file.userfile.id, :arguments => fname) %> + <% else %> + <% download_url = url_for(:action => :download, :id => file.userfile.id) %> + <% end %> + + <%= link_to download_url do %> + + <% end %> + + <% end %> + + + + <% if file.symbolic_type != :directory %> + <%= colored_pretty_size(file.size) %> + <% end %> + diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_subdir_plain_file_list_row.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_subdir_plain_file_list_row.html.erb new file mode 100644 index 000000000..5f292d7e2 --- /dev/null +++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_subdir_plain_file_list_row.html.erb @@ -0,0 +1,75 @@ + +<%- +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +-%> + +<% + # This partial receives one local variable: + # file : a file structure as returned by the FileCollection listing method + # Note that is contains a full relative path, starting with the @userfile's name itself. +%> + + + <% if @userfile.is_locally_synced? && file.symbolic_type == :regular %> + <%= check_box_tag("collection_files[]", "#{@userfile.id}##{file.name}", false, :class => "collection_checkbox", :id => nil) %> + <% end %> + + + + <%= (" " * 6 * file.depth).html_safe %> + +
    + <% if file.symbolic_type == :directory %> + <%= image_tag "/images/folder_icon_solid.png" %> + <% else %> + <%= image_tag "/images/file_icon.png" %> + <% end %> + +   + + <% if file.size > 0 %> + <%= data_link file.name, @userfile %> + <% else %> + <%= Pathname.new(file.name).basename.to_s %> + <% end %> + + <% if file.symbolic_type == :directory %> +   + Expand + + <% end %> +
    + + + + <% if file.symbolic_type == :regular && file.size > 0 && file.size < UserfilesController::MAX_DOWNLOAD_MEGABYTES.megabytes %> + <%= link_to url_for(:action => :content, :content_loader => :collection_file, :arguments => file.name) do %> + + <% end %> + <% end %> + + + + <% if file.symbolic_type != :directory %> + <%= colored_pretty_size(file.size) %> + <% end %> + diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_subdirectory_contents.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_subdirectory_contents.html.erb new file mode 100644 index 000000000..a657e23f7 --- /dev/null +++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_subdirectory_contents.html.erb @@ -0,0 +1,75 @@ + +<%- +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +-%> + +<% limit = 500 %> +<% base_dir = base_directory rescue params[:base_directory] %> +<% base_dir = base_dir.presence || "." %> + +<% file_list ||= ( @userfile.list_files(base_dir, [:regular, :directory]) rescue [] ) %> + +<% if file_list.blank? %> + + "> + + (Empty) + + + +<% else %> + + <% for file in file_list[0,limit] %> + <% if file.symbolic_type == :directory %> + <%= on_click_ajax_replace( { :element => "tr", + :url => url_for(:action => :display, + :viewer => "subdirectory_contents", + :viewer_userfile_class => "VirtualFileCollection", + :base_directory => file.name.sub(/\A[^\/]+\//, ""), + :apply_div => "false" + ), + :position => "after", + :before => "Loading..." + }, + { :class => "#{cycle("list-odd", "list-even")}", + :id => file.name.gsub(/\W+/, "_") + } + ) do %> + <%= render :file => @viewer.partial_path(:subdir_plain_file_list_row), :locals => {:file => file} %> + <% end %> + <% else %> + "> + <%= render :file => @viewer.partial_path(:subdir_plain_file_list_row), :locals => {:file => file} %> + + <% end %> + <% end %> + + <% if file_list.size > limit %> + "> + + <%= (" " * 6 * file_list.first.depth).html_safe %> ... <%= image_tag "/images/lotsa_files_icon.png" %> <%= pluralize(file_list.size-limit, "more entry") %> + + + <% end %> + +<% end %> + diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/virtual_file_collection.rb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/virtual_file_collection.rb new file mode 100644 index 000000000..093978db8 --- /dev/null +++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/virtual_file_collection.rb @@ -0,0 +1,256 @@ + +# +# CBRAIN Project +# +# Copyright (C) 2008-2024 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This file collection is collection of other Userfiles or Collections +# It is implemented using soft links +# two level collections are forbidden to prevent recursion or other issues +class VirtualFileCollection < FileCollection + + Revision_info=CbrainFileRevision[__FILE__] #:nodoc: + + CSV_BASENAME = "_virtual_file_collection.cbcsv" + # todo. add .bidsignore file, otherwise bids validation. Or we can allow CBRAIN filenames starting with dot + + reset_viewers # we opted to ignore superclass viewers rather than adjust them + # this viewer closely resembles one for regular File Collection + has_viewer :name => 'Virtual File Collection', :partial => :file_collection , :if => :is_locally_synced? + # an alternative viewer more stable for large collections + has_viewer :name => 'Simple List', :partial => :cbrain_file_list, :if => :is_locally_synced? + def self.pretty_type #:nodoc: + "Virtual File Collection" + end + + + def set_size! + self.size, self.num_files = Rails.cache.fetch("VirtualFileCollection_#{self.id || "#{current_user.id}_#{self.data_provider_id}_#{self.name}"}#size", expires_in: 3.minutes) do + userfiles = self.get_userfiles + [userfiles.sum(&:size), userfiles.sum(&:num_files)] + end + self.assign_attributes(size: self.size , num_files: self.num_files) if self.id + true + end + + # Sync the VirtualFileCollection, with the files too + def sync_to_cache(deep=true) #:nodoc: + syncstat = self.local_sync_status(:refresh) + return true if syncstat && syncstat.status == 'InSync' + super() + if deep && ! self.archived? + self.sync_files + self.update_cache_symlinks + end + @cbfl = files= nil # flush internal cache + true + end + + # Invokes the local sync_to_cache with deep=false; this means the + # constitute FileCollection are not synchronized and symlinks not created. + # This method is used by FileCollection when archiving or unarchiving. + def sync_to_cache_for_archiving + result = sync_to_cache(false) + self.erase_cache_symlinks rescue nil + result + end + + # When syncing to the provider, we locally erase + # the symlinks, because they make no sense outside + # of the local Rails app. + # FIXME: this method has a slight race condition, + # after syncing to the provider we recreate the + # symlinks, but if another program tries to access + # them during that time they might not yet be there. + def sync_to_provider #:nodoc: + self.cache_writehandle do # when the block ends, it will trigger the provider upload + self.erase_cache_symlinks unless self.archived? + end + self.make_cache_symlinks unless self.archived? + true + end + + # Sets the set of SingleFiles and FileCollections that constitute VirtualFileCollection. + # The CSV file inside the study will be created/updated, + # as well as all the symbolic links. The content + # is NOT synced to the provider side. + def set_virtual_file_collection(userfiles) + cb_error "Multi layer collections are not supported." if userfiles.any? { |f| f.is_a?(VirtualFileCollection) || f.is_a?(CivetVirtualStudy) } + + content = CbrainFileList.create_csv_file_from_userfiles(userfiles) + # This optimize so we don't reload the content for making the symlinks + @cbfl = CbrainFileList.new + @cbfl.load_from_content(content) + # Prepare CSV content + + @files = nil + + # Write CSV content to the interal CSV file + self.cache_prepare + Dir.mkdir(self.cache_full_path) unless Dir.exist?(self.cache_full_path) + File.write(csv_cache_full_path.to_s, content) + self.update_cache_symlinks + self.cache_is_newer + end + + # List linked files or directories, as if present directly + def list_linked_files(dir=:all, allowed_types = :regular) + if allowed_types.is_a? Array + types = allowed_types.dup + else + types = [allowed_types] + end + types.map!(&:to_sym) + types << :file if types.delete(:regular) + + # for combination of :top and :directory file type data are maid up, + # to avoid running file stats command which should not affect file browsing + # alternatively, new option(s) can be added to list_files/cache_collection_index, + # or new dir_info method + if (dir == :top || dir == '.') + cloned_files = self.list_files(:top, :link).cb_deep_clone # no altering the cache of list_files methods + userfiles_by_name = self.get_userfiles.index_by(&:name) + return cloned_files.filter_map do |file| + fname = file.name.split('/')[1] # gets basename + userfile = userfiles_by_name[fname] + if types.include?(:directory) && userfile.is_a?(FileCollection) + file.symbolic_type = :directory + file.userfile = userfile + file + elsif types.include?(:file) && userfile.is_a?(SingleFile) + file = userfile.list_files.first.clone + file.name = self.name + '/' + fname + file.symbolic_type = :regular + file.userfile = userfile + file + end + end + end + + userfiles = self.get_userfiles + + if dir.is_a? String + name, dir = dir.split '/' + dir |= '.' + userfiles = userfiles.select { |x| x.name == name } + end + + userfiles.map do |userfile| + userfile.list_files(dir, allowed_types).each do |f| + f.name = self.name + '/' + f.name + end + f.userfile = userfile + end.flatten + end + + # Returns the files IDs + def get_ids + self.get_userfiles.map(&:id) + end + + # Returns the list of files in the internal CbrainFileList + # The list is cached internally and access control is applied + # based on the owner of the VirtualFileCollection. + def get_userfiles #:nodoc: + + @cbfl = self.cached_cbrain_file_list # loads cbrain file list content + @files ||= @cbfl.userfiles_accessible_by_user!(self.user).compact + file_names = @files.map(&:name) + dup_names = file_names.select { |name| file_names.count(name) >1 }.uniq + cb_error "Virtual file collection contains duplicate filenames #{dup_names.join(',')}" if dup_names.present? + @files.each do |f| + cb_error "Nested virtual file collections are not supported, remove file with id #{f.id}" if ( + f.is_a?(VirtualFileCollection) || + f.type == CivetVirtualStudy + ) + end + end + + # list errors, to be shown to user and issues in the virtual collection + # despite exception above with some luck and effort user can render file view, so + # we show that error messages instead of the directory tree + def list_errors + @cbfl = self.cached_cbrain_file_list + @files ||= @cbfl.userfiles_accessible_by_user!(self.user).compact + file_names = @files.map(&:name) + dup_names = file_names.select { |name| file_names.count(name) >1 }.uniq + errors = [] + errors << "Virtual file collection contains duplicate filenames #{dup_names.join(',')}" if dup_names.present? + virtual_files_names = @files.select do |f| + f.is_a?(VirtualFileCollection) || f.type == "CivetVirtualStudy" + end.map(&:name) + errors << "Nested virtual file collections are not supported, remove files #{virtual_files_names.join ", "}" if virtual_files_names.present? + errors + end + + # caches cbrain file list file in memory + def cached_cbrain_file_list + if @cbfl.blank? + @cbfl = CbrainFileList.new + file_content = File.read(csv_cache_full_path.to_s) + @cbfl.load_from_content(file_content) + end + @cbfl + end + + #==================================================================== + # Support methods, not part of this model's API. + #==================================================================== + + protected + + # Synchronize each file + def sync_files #:nodoc: + self.get_userfiles.each { |uf| uf.sync_to_cache } + end + + # Clean up ALL symbolic links + def erase_cache_symlinks #:nodoc: + Dir.chdir(self.cache_full_path) do + Dir.glob('*').each do |entry| + # FIXME how to only erase symlinks that points to a CBRAIN cache or local DP? + # Parsing the value of the symlink is tricky... + File.unlink(entry) if File.symlink?(entry) + end + end + end + + # This cleans up any old symbolic links, then recreates them. + # Note that this does not sync the files themselves. + def update_cache_symlinks #:nodoc: + self.erase_cache_symlinks + self.make_cache_symlinks + end + + # Create symbolic links in cache for each element of the virtual collection + # Note that this does not sync the files themselves. + def make_cache_symlinks #:nodoc: + self.get_userfiles.each do |uf| + link_value = uf.cache_full_path + link_path = self.cache_full_path + link_value.basename + File.unlink(link_path) if File.symlink?(link_path) && File.readlink(link_path) != link_value + File.symlink(link_value, link_path) unless File.exist?(link_path) + end + end + + def csv_cache_full_path #:nodoc: + self.cache_full_path + CSV_BASENAME + end + +end diff --git a/BrainPortal/config/routes.rb b/BrainPortal/config/routes.rb index d8aa1c02e..6848b1f91 100644 --- a/BrainPortal/config/routes.rb +++ b/BrainPortal/config/routes.rb @@ -141,6 +141,7 @@ get 'stream/*file_path' => 'userfiles#stream' get 'display' post 'extract_from_collection' + post 'extract_from_virtual_collection' end collection do post 'download' @@ -149,6 +150,7 @@ post 'create_parent_child' delete 'delete_files' post 'create_collection' + post 'create_virtual_collection' put 'update_multiple' post 'change_provider' post 'compress' diff --git a/BrainPortal/public/javascripts/userfiles.js b/BrainPortal/public/javascripts/userfiles.js index e22b4046f..62b7a32a9 100644 --- a/BrainPortal/public/javascripts/userfiles.js +++ b/BrainPortal/public/javascripts/userfiles.js @@ -124,7 +124,8 @@ $(function() { rename: '/userfiles/:id', update: $('#prop-dialog > form').attr('action'), tags: '/tags/:id', - create_collection: $('#collection-dialog > form').attr('action') + create_collection: $('#collection-dialog > form').attr('action'), + create_virtual_collection: $('#virtual-collection-dialog > form').attr('action') }; /* Userfiles actions/operations */ @@ -386,6 +387,20 @@ $(function() { return defer(function () { uform.submit(); }).promise(); }, + /* + * Create a new VirtualFileCollection containing the currently selected files. + * The new collection's name and target data provider are to be specified + * in the HTML form argument +form. + */ + create_virtual_collection: function (form) { + var uform = userfiles.children('form'); + + setup_form(uform, urls.create_virtual_collection, 'POST', form); + clear_selection(true); + + return defer(function () { uform.submit(); }).promise(); + }, + /* * Tag-related operations; generic CRUD with parameters +id+ (tag ID) and * +data+ (tag attributes as a JS object) @@ -1097,6 +1112,40 @@ $(function() { .toggleClass('ui-state-disabled', !valid); }); + /* New virtual collection dialog */ + $('#virtual-collection-dialog') + .dialog('option', 'buttons', { + 'Cancel': function (event) { + $(this).trigger('close.uf'); + }, + 'Create': function (event) { + var dialog = $(this); + + dialog.trigger('close.uf'); + userfiles.create_virtual_collection(dialog.children('form')[0]); + } + }) + .unbind('open.uf.col-open') + .bind( 'open.uf.col-open', function () { + $(this).dialog('option', 'title', + 'New virtual collection - ' + formatted_selection() + ); + }) + .undelegate('#co-name', 'input.uf.co-name-check') + .delegate( '#co-name', 'input.uf.co-name-check', function () { + var valid = /^\w[\w~!@#%^&*()-+=:[\]{}|<>,.?]*$/.test($(this).val()); + + $('#co-invalid-name').css({ + visibility: valid ? 'hidden' : 'visible' + }); + + $('#virtual-collection-dialog') + .parent() + .find(':button:contains("Create")') + .prop('disabled', !valid) + .toggleClass('ui-state-disabled', !valid); + }); + /* Delete files confirmation dialog */ $('#delete-confirm') .unbind('open.uf.del-cfrm-open')