From 3a4d3e8b2b6dcf46341811ef5214d3a57f383c9f Mon Sep 17 00:00:00 2001 From: Karel Picman Date: Tue, 12 Apr 2016 12:07:16 +0200 Subject: [PATCH] File Storage Directory does not change #522 --- Gemfile | 12 +-- app/controllers/dmsf_upload_controller.rb | 110 +++++++++++---------- app/models/dmsf_file.rb | 111 +++++++++++----------- app/models/dmsf_file_revision.rb | 22 ++--- 4 files changed, 132 insertions(+), 123 deletions(-) diff --git a/Gemfile b/Gemfile index e74c0d78..eed28897 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ # encoding: utf-8 -# +# # Redmine plugin for Document Management System "Features" # # Copyright (C) 2011 Vít Jonáš @@ -22,12 +22,12 @@ source 'https://rubygems.org' -gem 'rubyzip', '>= 1.0.0' -gem 'zip-zip' # Just to avoid 'cannot load such file -- zip/zip' error +gem 'rubyzip' +gem 'zip-zip' gem 'simple_enum' -gem 'uuidtools', '~> 2.1.1' -gem 'dav4rack', '~> 0.3.0' +gem 'uuidtools' +gem 'dav4rack' -group :xapian do +group :xapian do gem 'xapian-full-alaveteli', :require => false end \ No newline at end of file diff --git a/app/controllers/dmsf_upload_controller.rb b/app/controllers/dmsf_upload_controller.rb index 59d31f67..f472fdb5 100644 --- a/app/controllers/dmsf_upload_controller.rb +++ b/app/controllers/dmsf_upload_controller.rb @@ -1,5 +1,5 @@ # encoding: utf-8 -# +# # Redmine plugin for Document Management System "Features" # # Copyright (C) 2011 Vít Jonáš @@ -21,16 +21,16 @@ class DmsfUploadController < ApplicationController unloadable - + menu_item :dmsf - + before_filter :find_project before_filter :authorize before_filter :find_folder, :except => [:upload_file, :upload, :commit] - + helper :all helper :dmsf_workflows - + accept_api_auth :upload, :commit def upload_files @@ -38,7 +38,7 @@ class DmsfUploadController < ApplicationController @uploads = [] if uploaded_files && uploaded_files.is_a?(Hash) # standard file input uploads - uploaded_files.each_value do |uploaded_file| + uploaded_files.each_value do |uploaded_file| upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, uploaded_file) @uploads.push(upload) if upload end @@ -53,9 +53,9 @@ class DmsfUploadController < ApplicationController end end - # async single file upload handling - def upload_file - @tempfile = params[:file] + # async single file upload handling + def upload_file + @tempfile = params[:file] unless @tempfile.original_filename render_404 return @@ -72,21 +72,21 @@ class DmsfUploadController < ApplicationController rescue Exception => e Rails.logger.error e.message end - render :layout => nil, :json => { :jsonrpc => '2.0', - :error => { - :code => 103, - :message => l(:header_minimum_filesize), - :details => l(:error_minimum_filesize, - :file => @tempfile.original_filename.to_s) + render :layout => nil, :json => { :jsonrpc => '2.0', + :error => { + :code => 103, + :message => l(:header_minimum_filesize), + :details => l(:error_minimum_filesize, + :file => @tempfile.original_filename.to_s) } } else render :layout => false end end - + # REST API document upload - def upload + def upload unless request.content_type == 'application/octet-stream' render :nothing => true, :status => 406 return @@ -109,69 +109,69 @@ class DmsfUploadController < ApplicationController } end end - - def commit_files + + def commit_files commit_files_internal params[:commited_files] - end - + end + # REST API file commit def commit - attachments = params[:attachments] + attachments = params[:attachments] if attachments && attachments.is_a?(Hash) @folder = DmsfFolder.visible.find_by_id attachments[:folder_id].to_i if attachments[:folder_id].present? # standard file input uploads uploaded_files = attachments.select { |key, value| key == 'uploaded_file'} - uploaded_files.each_value do |uploaded_file| + uploaded_files.each_value do |uploaded_file| upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, uploaded_file) uploaded_file[:disk_filename] = upload.disk_filename end end - commit_files_internal uploaded_files + commit_files_internal uploaded_files end private - + def commit_files_internal(commited_files) if commited_files && commited_files.is_a?(Hash) @files = [] failed_uploads = [] commited_files.each_value do |commited_file| name = commited_file[:name] - + new_revision = DmsfFileRevision.new file = DmsfFile.visible.find_file_by_name(@project, @folder, name) unless file link = DmsfLink.find_link_by_file_name(@project, @folder, name) file = link.target_file if link end - + unless file file = DmsfFile.new file.project = @project file.name = name file.folder = @folder - file.notification = Setting.plugin_redmine_dmsf[:dmsf_default_notifications].present? + file.notification = Setting.plugin_redmine_dmsf[:dmsf_default_notifications].present? new_revision.minor_version = 0 new_revision.major_version = 0 - else + else if file.last_revision last_revision = file.last_revision new_revision.source_revision = last_revision new_revision.major_version = last_revision.major_version - new_revision.minor_version = last_revision.minor_version + new_revision.minor_version = last_revision.minor_version else new_revision.minor_version = 0 new_revision.major_version = 0 end end - + if file.locked_for_user? failed_uploads.push(commited_file) next end commited_disk_filepath = "#{DmsfHelper.temp_dir}/#{commited_file[:disk_filename].gsub(/[\/\\]/,'')}" - + new_revision.file = file new_revision.user = User.current new_revision.name = name @@ -183,43 +183,49 @@ class DmsfUploadController < ApplicationController new_revision.major_version = commited_file[:custom_version_major].to_i new_revision.minor_version = commited_file[:custom_version_minor].to_i else - new_revision.increase_version(version, true) + new_revision.increase_version(version, true) end new_revision.mime_type = Redmine::MimeType.of(new_revision.name) new_revision.size = File.size(commited_disk_filepath) - - # Need to save file first to generate id for it in case of creation. - # File id is needed to properly generate revision disk filename + + # Need to save file first to generate id for it in case of creation. + # File id is needed to properly generate revision disk filename if commited_file[:dmsf_file_revision].present? commited_file[:dmsf_file_revision][:custom_field_values].each_with_index do |v, i| new_revision.custom_field_values[i].value = v[1] end end - + if new_revision.valid? && file.save new_revision.disk_filename = new_revision.new_storage_filename else failed_uploads.push(commited_file) next end - + if new_revision.save - new_revision.assign_workflow(commited_file[:dmsf_workflow_id]) - FileUtils.mv(commited_disk_filepath, new_revision.disk_file) - file.set_last_revision new_revision - @files.push(file) + new_revision.assign_workflow(commited_file[:dmsf_workflow_id]) + begin + FileUtils.mv(commited_disk_filepath, new_revision.disk_file) + file.set_last_revision new_revision + @files.push(file) + rescue Exception => e + Rails.logger.error e.message + flash[:error] = e.message + failed_uploads.push(file) + end else failed_uploads.push(commited_file) end end - unless @files.empty? - @files.each { |file| log_activity(file, 'uploaded') if file } + unless @files.empty? + @files.each { |file| log_activity(file, 'uploaded') if file } if (@folder && @folder.notification?) || (!@folder && @project.dmsf_notification?) begin recipients = DmsfMailer.get_notify_users(@project, @files) recipients.each do |u| DmsfMailer.files_updated(u, @project, @files).deliver - end + end if Setting.plugin_redmine_dmsf[:dmsf_display_notified_recipients] == '1' unless recipients.empty? to = recipients.collect{ |r| r.name }.first(DMSF_MAX_NOTIFICATION_RECEIVERS_INFO).join(', ') @@ -238,22 +244,22 @@ class DmsfUploadController < ApplicationController end respond_to do |format| format.js - format.api { - render_validation_errors(failed_uploads) unless failed_uploads.empty? + format.api { + render_validation_errors(failed_uploads) unless failed_uploads.empty? } format.html { redirect_to dmsf_folder_path(:id => @project, :folder_id => @folder) } end - + end - + def log_activity(file, action) Rails.logger.info "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} #{User.current.login}@#{request.remote_ip}/#{request.env['HTTP_X_FORWARDED_FOR']}: #{action} dmsf://#{file.project.identifier}/#{file.id}/#{file.last_revision.id}" end - + def find_folder - @folder = DmsfFolder.visible.find(params[:folder_id]) if params.keys.include?('folder_id') + @folder = DmsfFolder.visible.find(params[:folder_id]) if params.keys.include?('folder_id') rescue DmsfAccessError render_403 end - + end diff --git a/app/models/dmsf_file.rb b/app/models/dmsf_file.rb index d656734d..d8cd8deb 100644 --- a/app/models/dmsf_file.rb +++ b/app/models/dmsf_file.rb @@ -29,13 +29,13 @@ end class DmsfFile < ActiveRecord::Base unloadable - - include RedmineDmsf::Lockable + + include RedmineDmsf::Lockable belongs_to :project belongs_to :folder, :class_name => 'DmsfFolder', :foreign_key => 'dmsf_folder_id' belongs_to :deleted_by_user, :class_name => 'User', :foreign_key => 'deleted_by_user_id' - + has_many :revisions, -> { order("#{DmsfFileRevision.table_name}.major_version DESC, #{DmsfFileRevision.table_name}.minor_version DESC, #{DmsfFileRevision.table_name}.updated_at DESC") }, :class_name => 'DmsfFileRevision', :foreign_key => 'dmsf_file_id', :dependent => :destroy @@ -44,10 +44,10 @@ class DmsfFile < ActiveRecord::Base has_many :referenced_links, -> { where target_type: DmsfFile.model_name.to_s}, :class_name => 'DmsfLink', :foreign_key => 'target_id', :dependent => :destroy accepts_nested_attributes_for :revisions, :locks, :referenced_links, :project - + STATUS_DELETED = 1 STATUS_ACTIVE = 0 - + scope :visible, -> { where(:deleted => STATUS_ACTIVE) } scope :deleted, -> { where(:deleted => STATUS_DELETED) } @@ -61,27 +61,27 @@ class DmsfFile < ActiveRecord::Base existing_file = DmsfFile.visible.find_file_by_name(self.project, self.folder, self.name) errors.add(:name, l('activerecord.errors.messages.taken')) unless existing_file.nil? || existing_file.id == self.id - end + end acts_as_event :title => Proc.new { |o| o.name }, - :description => Proc.new { |o| - desc = Redmine::Search.cache_store.fetch("DmsfFile-#{o.id}") + :description => Proc.new { |o| + desc = Redmine::Search.cache_store.fetch("DmsfFile-#{o.id}") if desc - Redmine::Search.cache_store.delete("DmsfFile-#{o.id}") + Redmine::Search.cache_store.delete("DmsfFile-#{o.id}") else desc = o.description desc += ' / ' if o.description.present? && o.last_revision.comment.present? desc += o.last_revision.comment if o.last_revision.comment.present? - end + end desc }, :url => Proc.new { |o| {:controller => 'dmsf_files', :action => 'show', :id => o} }, :datetime => Proc.new { |o| o.updated_at }, :author => Proc.new { |o| o.last_revision.user } - + acts_as_searchable :columns => ["#{table_name}.name", "#{DmsfFileRevision.table_name}.title", "#{DmsfFileRevision.table_name}.description", "#{DmsfFileRevision.table_name}.comment"], :project_key => 'project_id', - :date_column => "#{table_name}.updated_at" + :date_column => "#{table_name}.updated_at" before_create :default_values def default_values @@ -96,19 +96,22 @@ class DmsfFile < ActiveRecord::Base @@storage_path = nil def self.storage_path - unless @@storage_path.present? - @@storage_path = Setting.plugin_redmine_dmsf['dmsf_storage_directory'].strip if Setting.plugin_redmine_dmsf['dmsf_storage_directory'].present? - @@storage_path = Pathname(Redmine::Configuration['attachments_storage_path']).join('dmsf') if @@storage_path.blank? && Redmine::Configuration['attachments_storage_path'].present? - @@storage_path = Rails.root.join('files/dmsf').to_s if @@storage_path.blank? - Dir.mkdir(@@storage_path) unless File.exists?(@@storage_path) - end + path = Setting.plugin_redmine_dmsf['dmsf_storage_directory'].strip if Setting.plugin_redmine_dmsf['dmsf_storage_directory'].present? + path = Pathname(Redmine::Configuration['attachments_storage_path']).join('dmsf') if path.blank? && Redmine::Configuration['attachments_storage_path'].present? + path = Rails.root.join('files/dmsf').to_s if path.blank? + DmsfFile.storage_path = path if path != @@storage_path @@storage_path end # Lets introduce a write for storage path, that way we can also # better interact from test-cases etc - def self.storage_path=(obj) - @@storage_path = obj + def self.storage_path=(path) + begin + FileUtils.mkdir_p(path) unless File.exists?(path) + rescue Exception => e + Rails.logger.error e.message + end + @@storage_path = path end def self.find_file_by_name(project, folder, name) @@ -128,7 +131,7 @@ class DmsfFile < ActiveRecord::Base def set_last_revision(new_revision) @last_revision = new_revision end - + def deleted? self.deleted == STATUS_DELETED end @@ -141,7 +144,7 @@ class DmsfFile < ActiveRecord::Base end begin # Revisions and links of a deleted file SHOULD be deleted too - self.revisions.each { |r| r.delete(commit, true) } + self.revisions.each { |r| r.delete(commit, true) } if commit self.destroy else @@ -161,7 +164,7 @@ class DmsfFile < ActiveRecord::Base errors[:base] << l(:error_parent_folder) return false end - self.revisions.each { |r| r.restore } + self.revisions.each { |r| r.restore } self.deleted = STATUS_ACTIVE self.deleted_by_user = nil save @@ -172,8 +175,8 @@ class DmsfFile < ActiveRecord::Base end def description - self.last_revision ? self.last_revision.description : '' - end + self.last_revision ? self.last_revision.description : '' + end def version self.last_revision ? self.last_revision.version : '0' @@ -291,33 +294,33 @@ class DmsfFile < ActiveRecord::Base def self.search(tokens, projects = nil, options = {}, user = User.current) tokens = [] << tokens unless tokens.is_a?(Array) projects = [] << projects if projects.is_a?(Project) - project_ids = projects.collect(&:id) if projects - - if options[:offset] + project_ids = projects.collect(&:id) if projects + + if options[:offset] limit_options = ["dmsf_files.updated_at #{options[:before] ? '<' : '>'} ?", options[:offset]] end - + if options[:titles_only] columns = [searchable_options[:columns][1]] - else + else columns = searchable_options[:columns] end - + token_clauses = columns.collect{ |column| "(LOWER(#{column}) LIKE ?)" } - sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') find_options = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort] project_conditions = [] - project_conditions << Project.allowed_to_condition(user, :view_dmsf_files) + project_conditions << Project.allowed_to_condition(user, :view_dmsf_files) project_conditions << "#{DmsfFile.table_name}.project_id IN (#{project_ids.join(',')})" if project_ids.present? - results = [] - + results = [] + scope = self.visible.joins(:project, :revisions) - scope = scope.limit(options[:limit]) unless options[:limit].blank? + scope = scope.limit(options[:limit]) unless options[:limit].blank? scope = scope.where(limit_options) unless limit_options.blank? - scope = scope.where(project_conditions.join(' AND ')) + scope = scope.where(project_conditions.join(' AND ')) results = scope.where(find_options).uniq.to_a if !options[:titles_only] && $xapian_bindings_available @@ -361,7 +364,7 @@ class DmsfFile < ActiveRecord::Base enquire.query = query matchset = enquire.mset(0, 1000) - if matchset + if matchset matchset.matches.each { |m| docdata = m.document.data{url} dochash = Hash[*docdata.scan(/(url|sample|modtime|author|type|size)=\/?([^\n\]]+)/).flatten] @@ -372,14 +375,14 @@ class DmsfFile < ActiveRecord::Base id_attribute = dmsf_attrs[0][1] if dmsf_attrs.length > 0 next if dmsf_attrs.length == 0 || id_attribute == 0 next unless results.select{|f| f.id.to_s == id_attribute}.empty? - + dmsf_file = DmsfFile.visible.where(limit_options).where(:id => id_attribute).first if dmsf_file - if user.allowed_to?(:view_dmsf_files, dmsf_file.project) && - (project_ids.blank? || (project_ids.include?(dmsf_file.project.id))) - Redmine::Search.cache_store.write("DmsfFile-#{dmsf_file.id}", - dochash['sample'].force_encoding('UTF-8')) if dochash['sample'] + if user.allowed_to?(:view_dmsf_files, dmsf_file.project) && + (project_ids.blank? || (project_ids.include?(dmsf_file.project.id))) + Redmine::Search.cache_store.write("DmsfFile-#{dmsf_file.id}", + dochash['sample'].force_encoding('UTF-8')) if dochash['sample'] break if(!options[:limit].blank? && results.count >= options[:limit]) results << dmsf_file end @@ -389,14 +392,14 @@ class DmsfFile < ActiveRecord::Base end end end - + [results, results.count] end - + def self.search_result_ranks_and_ids(tokens, user = User.current, projects = nil, options = {}) r = self.search(tokens, projects, options, user)[0] r.map{ |f| [f.updated_at.to_i, f.id]} - end + end def display_name if self.name.length > 50 @@ -404,33 +407,33 @@ class DmsfFile < ActiveRecord::Base end self.name end - + def image? self.last_revision && !!(self.last_revision.disk_filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png|svg)$/i) end - + def preview(limit) result = 'No preview available' if (self.last_revision.disk_filename =~ /\.(txt|ini|diff|c|cpp|php|csv|rb|h|erb|html|css|py)$/i) begin f = File.new(self.last_revision.disk_file) - f.each_line do |line| + f.each_line do |line| case f.lineno when 1 result = line when limit.to_i + 1 break else - result << line - end + result << line + end end rescue Exception => e result = e.message end - end + end result end - + def formatted_name(format) if self.last_revision self.last_revision.formatted_name(format) @@ -438,5 +441,5 @@ class DmsfFile < ActiveRecord::Base self.name end end - + end diff --git a/app/models/dmsf_file_revision.rb b/app/models/dmsf_file_revision.rb index 505a3aff..b11a257b 100644 --- a/app/models/dmsf_file_revision.rb +++ b/app/models/dmsf_file_revision.rb @@ -27,12 +27,12 @@ class DmsfFileRevision < ActiveRecord::Base belongs_to :folder, :class_name => 'DmsfFolder', :foreign_key => 'dmsf_folder_id' belongs_to :deleted_by_user, :class_name => 'User', :foreign_key => 'deleted_by_user_id' has_many :access, :class_name => 'DmsfFileRevisionAccess', :foreign_key => 'dmsf_file_revision_id', :dependent => :destroy - has_many :dmsf_workflow_step_assignment, :dependent => :destroy - accepts_nested_attributes_for :access, :dmsf_workflow_step_assignment, :file, :user - + has_many :dmsf_workflow_step_assignment, :dependent => :destroy + accepts_nested_attributes_for :access, :dmsf_workflow_step_assignment, :file, :user + STATUS_DELETED = 1 STATUS_ACTIVE = 0 - + scope :visible, -> { where(:deleted => STATUS_ACTIVE) } scope :deleted, -> { where(:deleted => STATUS_DELETED) } @@ -42,7 +42,7 @@ class DmsfFileRevision < ActiveRecord::Base :datetime => Proc.new {|o| o.updated_at }, :description => Proc.new {|o| o.comment }, :author => Proc.new {|o| o.user } - + acts_as_activity_provider :type => 'dmsf_file_revisions', :timestamp => "#{DmsfFileRevision.table_name}.updated_at", :author_key => "#{DmsfFileRevision.table_name}.user_id", @@ -51,11 +51,11 @@ class DmsfFileRevision < ActiveRecord::Base joins( "INNER JOIN #{DmsfFile.table_name} ON #{DmsfFileRevision.table_name}.dmsf_file_id = #{DmsfFile.table_name}.id " + "INNER JOIN #{Project.table_name} ON #{DmsfFile.table_name}.project_id = #{Project.table_name}.id"). - where("#{DmsfFile.table_name}.deleted = ?", STATUS_ACTIVE) + where("#{DmsfFile.table_name}.deleted = ?", STATUS_ACTIVE) - validates :title, :presence => true + validates :title, :presence => true validates_format_of :name, :with => DmsfFolder.invalid_characters, - :message => l(:error_contains_invalid_character) + :message => l(:error_contains_invalid_character) def project self.file.project if self.file @@ -136,7 +136,7 @@ class DmsfFileRevision < ActiveRecord::Base project_base = project.identifier.gsub(/[^\w\.\-]/,'_') storage_base << "/p_#{project_base}" end - Dir.mkdir(storage_base) unless File.exists?(storage_base) + FileUtils.mkdir_p(storage_base) unless File.exists?(storage_base) "#{storage_base}/#{self.disk_filename}" end @@ -249,7 +249,7 @@ class DmsfFileRevision < ActiveRecord::Base parts = self.version.split '.' parts.size == 2 ? parts[0].to_i * 1000 + parts[1].to_i : 0 end - + def formatted_name(format) return self.name if format.blank? if self.name =~ /(.*)(\..*)$/ @@ -257,7 +257,7 @@ class DmsfFileRevision < ActiveRecord::Base ext = $2 else filename = self.name - end + end format.sub!('%t', self.title) format.sub!('%f', filename) format.sub!('%d', self.updated_at.strftime('%Y%m%d%H%M%S'))