class Admin::FileManagersController < OrbitAdminController include ActionView::Helpers::NumberHelper before_action :set_base_url, except: [:settings, :update_settings] def render_403 render :file => "#{Rails.root}/app/views/errors/403.html", :layout => false, :status => 403, :formats => [:html] end def render_404 render :file => "#{Rails.root}/app/views/errors/404.html", :layout => false, :status => 404, :formats => [:html] end def forbidden_error render :body => nil, :status => 403 end def index end def module @current_user = current_user @current_user_id = @current_user.id @recycle_bin = params[:recycle_bin] == 'true' @file = nil if params[:file_id] upload_item = FileManagerUpload.where(:id=> params[:file_id]).first @upload_item = upload_item if upload_item @active_version = @upload_item.version @id = @upload_item.id @is_img = !(@upload_item.filename.match(/\.(jpg|gif|png|webp)$/i).nil?) if upload_item.is_trash absolute_path = upload_item.file_manager_trash.trash_path else absolute_path = upload_item.get_real_path end @all_uploads = get_uploads(upload_item.path, true, {:is_trash=>false}) if File.file?(absolute_path) if params[:download] send_file(absolute_path) elsif File.size(absolute_path) > 1_000_000 @file = "File is too big!" @too_big = true else @file = File.read(absolute_path) @file = fix_encoding(@file) end end end end if @recycle_bin recycle_bin else check_path_exist('') populate_directory(@module_key, '') end if request.xhr? if @file render :partial => "file" elsif @recycle_bin render :partial => "recycle_bin" else render :partial => "module" end end end def recycle_bin @thread = Multithread.where(:key=>"clean_trashes_#{@current_user_id}").first @thread_title = nil if @thread && @thread.status[:status] == 'finish' @thread = nil elsif @thread @thread_title = I18n.t("file_manager.clean_up_recycle_bin") end if @thread.nil? @thread = Multithread.where(:key=>"recover_all_#{@current_user_id}").first if @thread && @thread.status[:status] == 'finish' @thread = nil elsif @thread @thread_title = I18n.t("file_manager.recover_all") end end @relative_path = I18n.t("file_manager.recycle_bin") # @relative_to_pwd = @root_path query_hash = {is_slave: false} if @only_editable_for_uploader query_hash[:user_id] = @current_user_id end trashes = get_trashes(query_hash).page(params[:page]).per(10) @trashes = trashes @module_info_dict = trashes.map{|trash| trash.module}.uniq.map{|k| [k, FileManagerSetting.get_module_info(k)]}.to_h @directory = trashes.map do |record| file = record.trash_path stat = File.stat(file) rescue nil next if stat.nil? is_file = record.is_file real_path_absolute = File.expand_path(file) file = file.sub(/^#{Regexp.escape(@root_path)}/,'/') entry = "#{file}#{is_file ? '': '/'}" version = (record.record_only ? record.get_version : nil) org_path = record.path.sub(/^#{Regexp.escape(@root_path)}/,'') file_name = File.basename(file) if version org_path += " (version #{version})" file_name += " (version #{version})" end # is_editable = @default_editable || check_editable("#{@relative_to_pwd}/#{entry}", @current_user_id) { size: (is_file ? (number_to_human_size stat.size rescue '-'): '-'), type: (is_file ? :file : :directory), date: (stat.mtime.strftime(@format_time) rescue '-'), relative: my_escape(file).gsub('%2F', '/'), entry: entry, absolute: real_path_absolute, org_path: org_path, deleted_at: (record.created_at.strftime(@format_time) rescue '-'), name: file_name, module: record.module, id: record.id.to_s, upload_id: record.file_manager_upload_id.to_s, version: (record.record_only ? record.get_version : nil) # is_editable: is_editable } end.compact end def recover_all @current_user_id = current_user.id thread = Multithread.where(:key=>"recover_all_#{@current_user_id}").first if thread.nil? thread = Multithread.create(:key=>"recover_all_#{@current_user_id}",:status=>{:status=>'Processing'}) else thread.update(:status=>{:status=>'Processing'}) end Thread.new do query_hash = {is_slave: false} if @only_editable_for_uploader query_hash[:user_id] = @current_user_id end trashes = get_trashes(query_hash) all_count = trashes.count puts_every_count = [all_count * 3 / 100, 1].max current_count = 0 finish_percent = 0 thread.update(:status=>{:status=>'Recovering','all_count'=>all_count,'current_count'=>current_count,'finish_percent'=>finish_percent}) trashes.each do |trash| trash.recover_file current_count += 1 if current_count % puts_every_count == 0 finish_percent = (current_count * 100.0 / all_count).round(1) thread.update(:status=>{:status=>'Recovering','all_count'=>all_count,'current_count'=>current_count,'finish_percent'=>finish_percent}) end end thread.update(:status=>{:status=>'finish','all_count'=>all_count,'current_count'=>all_count,'finish_percent'=>100}) end render :json => {id: thread.id.to_s, title: I18n.t("file_manager.recover_all")} end def clean_trashes @current_user_id = current_user.id thread = Multithread.where(:key=>"clean_trashes_#{@current_user_id}").first if thread.nil? thread = Multithread.create(:key=>"clean_trashes_#{@current_user_id}",:status=>{:status=>'Processing'}) else thread.update(:status=>{:status=>'Processing'}) end Thread.new do query_hash = {is_slave: false} if @only_editable_for_uploader query_hash[:user_id] = @current_user_id end trashes = get_trashes(query_hash) all_count = trashes.count puts_every_count = [all_count * 3 / 100, 1].max current_count = 0 finish_percent = 0 thread.update(:status=>{:status=>'Deleting','all_count'=>all_count,'current_count'=>current_count,'finish_percent'=>finish_percent}) trashes.each do |trash| trash.delete_file current_count += 1 if current_count % puts_every_count == 0 finish_percent = (current_count * 100.0 / all_count).round(1) thread.update(:status=>{:status=>'Deleting','all_count'=>all_count,'current_count'=>current_count,'finish_percent'=>finish_percent}) end end thread.update(:status=>{:status=>'finish','all_count'=>all_count,'current_count'=>all_count,'finish_percent'=>100}) end render :json => {id: thread.id.to_s, title: I18n.t("file_manager.clean_up_recycle_bin")} end def get_trash_count @current_user_id = current_user.id query_hash = {is_slave: false} if @only_editable_for_uploader query_hash[:user_id] = @current_user_id end trashes = get_trashes(query_hash) render :json => {count: trashes.count} end def destroy if params[:permanent] == 'true' if params[:type] == 'upload' upload_record = FileManagerUpload.where(:id=>params[:id]).first upload_record.delete_file_permanent(true) else trash = FileManagerTrash.where(:id=>params[:id]).first trash.delete_file end render :body => nil, :status => 204 else upload_record = FileManagerUpload.where(:id=>params[:id]).first if params[:type] == 'upload' newest_record = upload_record.delete_file(true) if newest_record data = {'id': newest_record.id.to_s, 'version': newest_record.version} else data = {} end render :json => data and return else absolute_path = upload_record.path if FileManagerUpload.check_restricted(absolute_path) forbidden_error else relative_path = absolute_path is_file = !(File.directory?(absolute_path)) unless is_file relative_path += '/' end upload_records = get_uploads(relative_path, is_file, {:is_default=>true}) if is_file upload_records.each{|record| record.delete_file(false,current_user.id, params[:force_delete] == 'true') } else has_matched = false master_record_idx = upload_records.index{|record| record.path == relative_path} master_record = upload_records[master_record_idx] force_delete = (params[:force_delete] == 'true') master_record.delete_file(false,current_user.id, force_delete) upload_records.delete_at(master_record_idx) if upload_records.count != 0 if master_record.file_manager_trash trash_path = master_record.file_manager_trash.trash_path upload_records.each do |record| record.delete_file(false,current_user.id, force_delete, true, Pathname.new(trash_path).join(record.path.sub(/^#{Regexp.escape(relative_path)}/,'')).to_s) end end end end end end render :body => nil, :status => 204 end end def recover trash = FileManagerTrash.where(:id=> params[:id]).first if trash trash.recover_file render :body => nil, :status => 204 else render :body => nil, :status => 404 end end def rename upload = FileManagerUpload.where(:id=>params[:id]).first absolute_path = upload.path @root_path = File.dirname(absolute_path) new_path = safe_expand_path(params[:new_name]) if new_path != absolute_path if File.exists?(new_path) render :body => nil, :status => 403 else relative_path = absolute_path new_relative_path = Pathname.new(@root_path).join(params[:new_name]) parent = new_path.split('/')[0..-2].join('/') FileUtils.mkdir_p(parent) upload_records = get_uploads(relative_path) FileUtils.mv(absolute_path, new_path) upload_records.each{|r| r.update_path(new_relative_path)} render :body => nil, :status => 204 end else render :body => nil, :status => 200 end end def save if params[:content].present? upload_item = FileManagerUpload.where(:id=>params[:id]).first if upload_item.is_trash absolute_path = upload_item.file_manager_trash.trash_path else absolute_path = upload_item.path end forbidden_error and return unless File.exist?(absolute_path) file_relative_path = absolute_path is_file = true new_upload = upload_item.generate_new_upload File.open(absolute_path,"w+"){|f| f.write(params[:content])} render :json => {id: new_upload.id.to_s} else render :body => nil, :status => 400 end end def upload_file input_file = params[:file] if input_file if params[:type] == 'update' upload_item = FileManagerUpload.where(:id=>params[:id]).first relative_path = upload_item.path if input_file.original_filename != upload_item.filename new_relative_path = Pathname.new(File.dirname(relative_path)).join(input_file.original_filename) upload_records = get_uploads(relative_path) FileUtils.mv(relative_path, new_relative_path) upload_records.each{|r| r.update_path(new_relative_path)} upload_item.reload else new_relative_path = relative_path end upload_item = upload_item.generate_new_upload File.open(new_relative_path, 'wb') do |file| file.write(input_file.read) end else if params[:id] != 'asset' forbidden_error and return end asset = current_user.assets.new asset.data = input_file asset.title_translations = I18n.available_locales.map{|l| [l, input_file.original_filename]}.to_h asset.save! upload_item = FileManagerUpload.where(:model=> "Asset", :related_id=> asset.id).first params[:remote] = true end end if params[:remote].nil? redirect_to params[:url] else render :json => { id: upload_item.id.to_s, filename: input_file.original_filename, size: number_to_human_size(input_file.size), date: upload_item.created_at.strftime(@format_time) } end end def settings FileManagerRoot.create if FileManagerRoot.count == 0 @root = FileManagerRoot.first end def update_settings @root = FileManagerRoot.first @root.update_attributes(file_manager_root_params) redirect_to admin_file_managers_settings_path end def file_manager_root_params params.require(:file_manager_root).permit! end private def get_trashes(query_hash={}) query_hash[:file_manager_setting_id] = @setting_id ? @setting_id : FileManagerRoot.first.id if @module_key && @module_key != 'all' query_hash[:module] = @module_key end @trashes = FileManagerTrash.where(query_hash).order(:created_at=>-1) end def get_uploads(tmp_path=nil, is_file=nil, extra_condition={}, order_hash={:version=>-1}) query_hash = {} if tmp_path != nil query_hash[:path] = tmp_path end if is_file != nil query_hash[:is_file] = is_file query_hash[:path] = /^#{Regexp.escape(query_hash[:path].to_s)}/ if query_hash[:path] end query_hash[:file_manager_setting_id] = @setting_id if @setting_id FileManagerUpload.where(query_hash.merge(extra_condition)).order(order_hash).to_a end def fix_encoding(str) if str.valid_encoding? str else try_list = ["utf-8","utf-16", "big-5"] success_flag = false try_list.each do |enc| begin str.encode!(str.encoding, enc) if str.valid_encoding? success_flag = true break end rescue => e next end end unless success_flag str = "unknown encoding!" @unknown_encoding = true end str end end def check_editable(path=nil, current_user_id=nil) query_hash = {:path=>path,:user_id=>current_user_id} query_hash[:file_manager_setting_id] = @setting_id FileManagerUpload.where(query_hash).count != 0 end def my_escape(string) string.gsub(/([^ a-zA-Z0-9_.-]+)/) do '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase end end def populate_directory(module_key, current_url) if module_key == 'all' @uploads = FileManagerUpload.where(:is_default=> true) else @uploads = FileManagerUpload.where(:module=> module_key, :is_default=> true) end @uploads = @uploads.where(:is_trash=> false) if params[:keywords].present? @uploads = @uploads.where(:filename=> /#{::Regexp.escape(params[:keywords])}/) end @uploads = @uploads.order_by(:created_at => :desc) @uploads = @uploads.page(params[:page]).per(10) current_url = '' if current_url == '/' @module_info_dict = @uploads.map{|upload_item| upload_item.module}.uniq.map{|k| [k, FileManagerSetting.get_module_info(k)]}.to_h @directory = @uploads.map do |upload_item| file = upload_item.filename real_path_absolute = upload_item.path stat = (File.exist?(real_path_absolute) ? File.stat(real_path_absolute) : nil) next if stat.nil? is_file = stat.file? entry = "#{file}#{is_file ? '': '/'}" file_relative_path = upload_item.path is_editable = @default_editable || check_editable(file_relative_path, @current_user_id) { module: upload_item.module, is_file: is_file, size: ((is_file && stat) ? number_to_human_size(stat.size) : '-'), type: (is_file ? :file : :directory), date: (stat ? stat.mtime.strftime(@format_time) : '-'), relative: my_escape("#{current_url}#{file}").gsub('%2F', '/'), entry: entry, absolute: real_path_absolute, is_editable: is_editable, id: (upload_item ? upload_item.id.to_s : nil) } end.compact end def safe_expand_path(path) current_directory = File.expand_path(@root_path) tested_path = File.expand_path(path, @root_path) if @disable_path_traversal && !(tested_path.starts_with?(current_directory)) raise ArgumentError, 'Should not be parent of root' end tested_path end def check_path_exist(path) @absolute_path = safe_expand_path(path) @relative_path = path raise ActionController::RoutingError, 'Not Found' unless File.exists?(@absolute_path) @absolute_path end def set_base_url @base_url = ENV['BASE_URL'] || 'root' @root_path = ENV['BASE_DIRECTORY'] || FileManagerRoot::RootPath @root = FileManagerRoot.first @disable_path_traversal = @root.disable_path_traversal @format_time = I18n.locale.to_s == 'zh_tw' ? '%Y/%m/%d %H:%M' : '%d %b %Y %H:%M' @setting = FileManagerSetting.first @only_editable_for_uploader = false @setting_id = nil if @setting @setting_id = @setting.id @root_path = Pathname.new(@root_path).join(@setting.root_path).to_s @only_editable_for_uploader = @setting.only_editable_for_uploader end @default_editable = !@only_editable_for_uploader @module_key = params[:id] @display_module_name = (@module_key == 'all') @display_uploader_region = (@module_key == 'asset') @module = ModuleApp.where(:key=> @module_key).first module_info = FileManagerSetting.get_module_info(@module_key) @module_title = (module_info ? module_info[:title] : nil) @only_select_folder = (params[:select_mode] == 'true') end end