#9 Active Storage - basic functionality

This commit is contained in:
Karel Pičman 2025-10-15 09:55:22 +02:00
parent b69451807a
commit da8456bc74
16 changed files with 258 additions and 295 deletions

View File

@ -18,14 +18,13 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
source 'https://rubygems.org' do source 'https://rubygems.org' do
gem 'active_record_union'
gem 'activestorage'
gem 'ox' # Dav4Rack gem 'ox' # Dav4Rack
gem 'rake' unless Dir.exist?(File.expand_path('../../redmine_dashboard', __FILE__)) gem 'rake' unless Dir.exist?(File.expand_path('../../redmine_dashboard', __FILE__))
gem 'simple_enum'
gem 'uuidtools' gem 'uuidtools'
gem 'zip-zip' unless Dir.exist?(File.expand_path('../../vault', __FILE__)) gem 'zip-zip' unless Dir.exist?(File.expand_path('../../vault', __FILE__))
# Redmine extensions
gem 'active_record_union'
gem 'simple_enum'
group :xapian do group :xapian do
gem 'xapian-ruby' gem 'xapian-ruby'
end end

View File

@ -1,4 +1,4 @@
# Redmine DMSF Plugin 4.2.4 devel # Redmine DMSF Plugin 5.0.0 devel
[![GitHub CI](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml/badge.svg?branch=devel)](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml) [![GitHub CI](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml/badge.svg?branch=devel)](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml)
[![Support Ukraine Badge](https://bit.ly/support-ukraine-now)](https://github.com/support-ukraine/support-ukraine) [![Support Ukraine Badge](https://bit.ly/support-ukraine-now)](https://github.com/support-ukraine/support-ukraine)
@ -13,7 +13,7 @@ The development has been supported by [Kontron](https://www.kontron.com) and has
Project home: <https://github.com/picman/redmine_dmsf> Project home: <https://github.com/picman/redmine_dmsf>
Redmine Document Management System "Features" plugin is distributed under GNU General Public License v3 (GPL). Redmine Document Management System "Features" plugin is distributed under GNU General Public License v3 (GPL).
33Redmine is a flexible project management web application, released under the terms of the GNU General Public License v2 (GPL) at <https://www.redmine.org/> Redmine is a flexible project management web application, released under the terms of the GNU General Public License v2 (GPL) at <https://www.redmine.org/>
Further information about the GPL license can be found at Further information about the GPL license can be found at
<https://www.gnu.org/licenses/gpl-3.0.html> <https://www.gnu.org/licenses/gpl-3.0.html>
@ -296,7 +296,8 @@ instance is stopped.
### WebDAV ### WebDAV
In order to enable WebDAV module, it is necessary to put the following code into yor `config/additional_environment.rb` In order to enable WebDAV module, it is necessary to put the following code into your
`config/additional_environment.rb`:
```ruby ```ruby
# Redmine DMSF's WebDAV # Redmine DMSF's WebDAV
@ -304,6 +305,42 @@ require Rails.root.join('plugins', 'redmine_dmsf', 'lib', 'redmine_dmsf', 'webda
config.middleware.insert_before ActionDispatch::Cookies, RedmineDmsf::Webdav::CustomMiddleware config.middleware.insert_before ActionDispatch::Cookies, RedmineDmsf::Webdav::CustomMiddleware
``` ```
### Active Storage
Documents are stored using Active Storage. It requires the following lines to be added into
`config/additional_environment.rb`:
```ruby
# Active storage
require 'active_storage/engine'
require Rails.root.join('plugins', 'redmine_dmsf', 'lib', 'redmine_dmsf', 'xapian_analyzer').to_s
config.active_storage.service = :local # Store files locally
#config.active_storage.service = :amazon # Store files on Amazon S3
config.active_storage.analyzers.append RedmineDmsf::XapianAnalyzer # Index uploaded files for Xapian full-text search
```
Then install Active Storage with the following commands:
```shell
bin/rails active_storage:install RAILS_ENV=production
bin/rails db:migrate RAILS_ENV=production
```
Configure your DMS files storage in config/storage.yml:
```yml
local:
service: Disk
root: <%= Rails.root.join('dmsf_as') %>
## Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
#amazon:
# service: S3
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# bucket: your_own_bucket-<%= Rails.env %>
# region: "" # e.g. 'us-east-1'
```
### Installation in a sub-uri ### Installation in a sub-uri
In order to documents and folders are available via WebDAV in case that the Redmine is configured to be run in a sub-uri In order to documents and folders are available via WebDAV in case that the Redmine is configured to be run in a sub-uri

View File

@ -510,7 +510,7 @@ class DmsfController < ApplicationController
raise DmsfAccessError unless User.current.allowed_to?(:email_documents, @project) raise DmsfAccessError unless User.current.allowed_to?(:email_documents, @project)
zip = Zip.new zip = Zip.new
zip_entries(zip, selected_folders, selected_files) zip_entries zip, selected_folders, selected_files
zipped_content = zip.finish zipped_content = zip.finish
max_filesize = RedmineDmsf.dmsf_max_email_filesize max_filesize = RedmineDmsf.dmsf_max_email_filesize
@ -548,7 +548,7 @@ class DmsfController < ApplicationController
def download_entries(selected_folders, selected_files) def download_entries(selected_folders, selected_files)
zip = Zip.new zip = Zip.new
zip_entries(zip, selected_folders, selected_files) zip_entries zip, selected_folders, selected_files
zip.dmsf_files.each do |f| zip.dmsf_files.each do |f|
# Action # Action
audit = DmsfFileRevisionAccess.new audit = DmsfFileRevisionAccess.new

View File

@ -52,7 +52,7 @@ class DmsfFilesController < ApplicationController
end end
check_project @revision.dmsf_file check_project @revision.dmsf_file
raise ActionController::MissingFile if @file.deleted? raise ActionController::MissingFile if @file.deleted? || !@revision.file.attached?
# Action # Action
access = DmsfFileRevisionAccess.new access = DmsfFileRevisionAccess.new
@ -82,12 +82,12 @@ class DmsfFilesController < ApplicationController
# Text preview # Text preview
elsif !api_request? && params[:download].blank? && (@file.size <= Setting.file_max_size_displayed.to_i.kilobyte) && elsif !api_request? && params[:download].blank? && (@file.size <= Setting.file_max_size_displayed.to_i.kilobyte) &&
(@file.text? || @file.markdown? || @file.textile?) && !@file.html? && formats.include?(:html) (@file.text? || @file.markdown? || @file.textile?) && !@file.html? && formats.include?(:html)
@content = File.read(@revision.disk_file, mode: 'rb') @content = @revision.file.download
render action: 'document' render action: 'document'
# Offer the file for download # Offer the file for download
else else
params[:disposition] = 'attachment' if params[:filename].present? params[:disposition] = 'attachment' if params[:filename].present?
send_file @revision.disk_file, send_data @revision.file.download,
filename: filename, filename: filename,
type: @revision.detect_content_type, type: @revision.detect_content_type,
disposition: params[:disposition].presence || @revision.dmsf_file.disposition disposition: params[:disposition].presence || @revision.dmsf_file.disposition
@ -142,6 +142,11 @@ class DmsfFilesController < ApplicationController
revision.disk_filename = revision.new_storage_filename revision.disk_filename = revision.new_storage_filename
revision.mime_type = upload.mime_type revision.mime_type = upload.mime_type
revision.digest = upload.digest revision.digest = upload.digest
revision.file.attach(
io: File.open(upload.tempfile_path),
filename: revision.disk_filename,
content_type: revision.mime_type
)
end end
else else
revision.size = last_revision.size revision.size = last_revision.size
@ -155,17 +160,7 @@ class DmsfFilesController < ApplicationController
ok = true ok = true
if revision.save if revision.save
revision.assign_workflow params[:dmsf_workflow_id] revision.assign_workflow params[:dmsf_workflow_id]
if upload if @file.locked? && !@file.locks.empty?
begin
FileUtils.mv upload.tempfile_path, revision.disk_file(search_if_not_exists: false)
rescue StandardError => e
Rails.logger.error e.message
flash[:error] = e.message
revision.destroy
ok = false
end
end
if ok && @file.locked? && !@file.locks.empty?
begin begin
@file.unlock! @file.unlock!
flash[:notice] = "#{l(:notice_file_unlocked)}, " flash[:notice] = "#{l(:notice_file_unlocked)}, "

View File

@ -96,8 +96,11 @@ module DmsfUploadHelper
a = Attachment.find_by_token(committed_file[:token]) a = Attachment.find_by_token(committed_file[:token])
committed_file[:tempfile_path] = a.diskfile if a committed_file[:tempfile_path] = a.diskfile if a
end end
FileUtils.mv committed_file[:tempfile_path], new_revision.disk_file(search_if_not_exists: false) new_revision.file.attach(
FileUtils.chmod 'u=wr,g=r', new_revision.disk_file(search_if_not_exists: false) io: File.open(committed_file[:tempfile_path]),
filename: new_revision.name,
content_type: new_revision.mime_type
)
file.last_revision = new_revision file.last_revision = new_revision
files.push file files.push file
container.dmsf_file_added file if container && !new_object container.dmsf_file_added file if container && !new_object

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# An asynchronous job to remove file from Xapian full-text search index
class RemoveFromIndexJob < ApplicationJob
def self.schedule(key)
perform_later key
end
def perform(key)
url = File.join(key[0..1], key[2..3], key)
stem_lang = RedmineDmsf.dmsf_stemming_lang
db_path = File.join RedmineDmsf.dmsf_index_database, stem_lang
db = Xapian::WritableDatabase.new(db_path, Xapian::DB_OPEN)
found = false
db.postlist('').each do |it|
doc = db.document(it.docid)
dochash = Hash[*doc.data.scan(%r{(url|sample|modtime|author|type|size)=/?([^\n\]]+)}).flatten]
next unless url == dochash['url']
db.delete_document it.docid
found = true
break
end
Rails.logger.warn "Document's URL '#{url}' not found in the index" unless found
rescue StandardError => e
Rails.logger.error e.message
ensure
db&.close
end
end

View File

@ -177,21 +177,21 @@ class DmsfFile < ApplicationRecord
if locked_for_user? && !User.current.allowed_to?(:force_file_unlock, project) if locked_for_user? && !User.current.allowed_to?(:force_file_unlock, project)
Rails.logger.info l(:error_file_is_locked) Rails.logger.info l(:error_file_is_locked)
if lock.reverse[0].user if lock.reverse[0].user
errors.add(:base, l(:title_locked_by_user, user: lock.reverse[0].user)) errors.add :base, l(:title_locked_by_user, user: lock.reverse[0].user)
else else
errors.add(:base, l(:error_file_is_locked)) errors.add :base, l(:error_file_is_locked)
end end
return false return false
end end
begin begin
# Revisions and links of a deleted file SHOULD be deleted too
dmsf_file_revisions.each { |r| r.delete(commit: commit, force: true) }
if commit if commit
destroy destroy
else else
self.deleted = STATUS_DELETED self.deleted = STATUS_DELETED
self.deleted_by_user = User.current self.deleted_by_user = User.current
save save
# Associated revisions should be marked as deleted too
dmsf_file_revisions.each { |r| r.delete(commit: commit, force: true) }
end end
rescue StandardError => e rescue StandardError => e
Rails.logger.error e.message Rails.logger.error e.message
@ -444,25 +444,24 @@ class DmsfFile < ApplicationRecord
matchset = enquire.mset(0, 1000) matchset = enquire.mset(0, 1000)
matchset&.matches&.each do |m| matchset&.matches&.each do |m|
docdata = m.document.data { url } docdata = m.document.data
dochash = Hash[*docdata.scan(%r{(url|sample|modtime|author|type|size)=/?([^\n\]]+)}).flatten] dochash = Hash[*docdata.scan(%r{(url|sample|modtime|author|type|size)=/?([^\n\]]+)}).flatten]
filename = dochash['url'] next unless dochash['url'] =~ %r{^\w{2}/\w{2}/(\w+)$} # /76/df/76dfsp2ubbgq4yvq90zrfoyxt012
next unless filename
dmsf_attrs = filename.scan(%r{^\d{4}/\d{2}/(\d{12}_(\d+)_.*)$}) key = Regexp.last_match(1)
id_attribute = 0 blob = ActiveStorage::Blob.find_by(key: key)
id_attribute = dmsf_attrs[0][1] if dmsf_attrs.length.positive? attachment = blob&.attachments&.first
next if dmsf_attrs.empty? || id_attribute.to_i.zero? dmsf_file_revision = attachment&.record
dmsf_file = DmsfFile.visible.where(limit_options).find_by(id: id_attribute) next unless dmsf_file_revision
dmsf_file = dmsf_file_revision.dmsf_file
next unless dmsf_file && DmsfFolder.permissions?(dmsf_file.dmsf_folder) && next unless dmsf_file && DmsfFolder.permissions?(dmsf_file.dmsf_folder) &&
user.allowed_to?(:view_dmsf_files, dmsf_file.project) && user.allowed_to?(:view_dmsf_files, dmsf_file.project) &&
(project_ids.blank? || project_ids.include?(dmsf_file.project_id)) (project_ids.blank? || project_ids.include?(dmsf_file.project_id))
rev_id = DmsfFileRevision.where(dmsf_file_id: dmsf_file.id, disk_filename: dmsf_attrs[0][0])
.pick(:id)
if dochash['sample'] if dochash['sample']
Redmine::Search.cache_store.write("DmsfFile-#{dmsf_file.id}-#{rev_id}", Redmine::Search.cache_store.write("DmsfFile-#{dmsf_file.id}-#{dmsf_file_revision.id}",
dochash['sample'].force_encoding('UTF-8')) dochash['sample'].force_encoding('UTF-8'))
end end
break if options[:limit].present? && results.count >= options[:limit] break if options[:limit].present? && results.count >= options[:limit]
@ -551,12 +550,14 @@ class DmsfFile < ApplicationRecord
def pdf_preview def pdf_preview
return '' unless previewable? return '' unless previewable?
target = File.join(DmsfFile.previews_storage_path, "#{File.basename(last_revision&.disk_file.to_s, '.*')}.pdf") target = File.join(DmsfFile.previews_storage_path, "#{last_revision.file.blob.key}.pdf")
begin begin
RedmineDmsf::Preview.generate last_revision&.disk_file.to_s, target last_revision.file.open do |f|
RedmineDmsf::Preview.generate f.path, target
end
rescue StandardError => e rescue StandardError => e
Rails.logger.error do Rails.logger.error do
%(An error occurred while generating preview for #{last_revision&.disk_file} to #{target}\n %(An error occurred while generating preview for #{last_revision.file.name} to #{target}\n
Exception was: #{e.message}) Exception was: #{e.message})
end end
'' ''
@ -567,15 +568,16 @@ class DmsfFile < ApplicationRecord
result = +'No preview available' result = +'No preview available'
if text? if text?
begin begin
f = File.new(last_revision.disk_file) last_revision.file.open do |f|
f.each_line do |line| f.each_line do |line|
case f.lineno case f.lineno
when 1 when 1
result = line result = line
when limit.to_i + 1 when limit.to_i + 1
break break
else else
result << line result << line
end
end end
end end
rescue StandardError => e rescue StandardError => e

View File

@ -30,6 +30,8 @@ class DmsfFileRevision < ApplicationRecord
belongs_to :dmsf_workflow_assigned_by_user, class_name: 'User' belongs_to :dmsf_workflow_assigned_by_user, class_name: 'User'
belongs_to :dmsf_workflow belongs_to :dmsf_workflow
has_one_attached :shared_file
has_many :dmsf_file_revision_access, dependent: :destroy has_many :dmsf_file_revision_access, dependent: :destroy
has_many :dmsf_workflow_step_assignment, dependent: :destroy has_many :dmsf_workflow_step_assignment, dependent: :destroy
@ -98,6 +100,20 @@ class DmsfFileRevision < ApplicationRecord
validates :description, length: { maximum: 1.kilobyte } validates :description, length: { maximum: 1.kilobyte }
validates :size, dmsf_max_file_size: true validates :size, dmsf_max_file_size: true
def file
unless shared_file.attached?
# If no file is attached, look at the source revision
# This way we prevent the same file from being attached to multiple revisions
sr = source_revision
while sr
return sr.shared_file if sr.shared_file.attached?
sr = sr.source_revision
end
end
shared_file
end
def visible?(_user = nil) def visible?(_user = nil)
deleted == STATUS_ACTIVE deleted == STATUS_ACTIVE
end end
@ -300,19 +316,8 @@ class DmsfFileRevision < ApplicationRecord
end end
def copy_file_content(open_file) def copy_file_content(open_file)
sha = Digest::SHA256.new file.attach io: open_file, filename: dmsf_file.name
File.open(disk_file(search_if_not_exists: false), 'wb') do |f| self.digest = file.blob.checksum
if open_file.respond_to?(:read)
while (buffer = open_file.read(8192))
f.write buffer
sha.update buffer
end
else
f.write open_file
sha.update open_file
end
end
self.digest = sha.hexdigest
end end
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
@ -400,14 +405,27 @@ class DmsfFileRevision < ApplicationRecord
end end
def delete_source_revision def delete_source_revision
derived_revisions = []
DmsfFileRevision.where(source_dmsf_file_revision_id: id).find_each do |d| DmsfFileRevision.where(source_dmsf_file_revision_id: id).find_each do |d|
# Derived revision without its own file
derived_revisions << d unless d.shared_file.attached?
# Replace the source revision
d.source_revision = source_revision d.source_revision = source_revision
d.save! d.save!
end end
return unless RedmineDmsf.physical_file_delete? return unless shared_file.attached?
dependencies = DmsfFileRevision.where(disk_filename: disk_filename).all.size if derived_revisions.empty?
FileUtils.rm_f(disk_file) if dependencies <= 1 # Remove the file from Xapian index
RemoveFromIndexJob.schedule file.blob.key if RedmineDmsf::Plugin.lib_available?('xapian')
# Remove the file
shared_file.purge_later if RedmineDmsf.physical_file_delete?
else
# Move the shared file to an unattached derived revision
d = derived_revisions.first
d.shared_file.attach shared_file.blob
shared_file.detach
end
end end
def copy_custom_field_values(values, source_revision = nil) def copy_custom_field_values(values, source_revision = nil)

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Add index
class AddIndexOnSourceDmsfFileRevisionId < ActiveRecord::Migration[7.0]
def change
add_index :dmsf_file_revisions, :source_dmsf_file_revision_id
end
end

View File

@ -1,168 +0,0 @@
#!/usr/bin/ruby -W0
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Xabier Elkano, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require 'optparse'
########################################################################################################################
# BEGIN Configuration parameters
# Configure the following parameters (most of them can be configured through the command line):
########################################################################################################################
# Redmine installation directory
REDMINE_ROOT = File.expand_path('../../../', __dir__)
# DMSF document location REDMINE_ROOT/FILES
FILES = 'dmsf'
# omindex binary path
# To index "non-text" files, use omindex filters
# e.g.: tesseract OCR engine as a filter for PNG files
OMINDEX = '/usr/bin/omindex'
# OMINDEX += " --filter=image/png:'tesseract -l chi_sim+chi_tra %f -'"
# OMINDEX += " --filter=image/jpeg:'tesseract -l chi_sim+chi_tra %f -'"
# Directory containing Xapian databases for omindex (Attachments indexing)
db_root_path = File.expand_path('dmsf_index', REDMINE_ROOT)
# Verbose output, false/true
verbose = false
# Define stemmed languages to index attachments Eg. [ 'english', 'italian', 'spanish' ]
# Available languages are danish, dutch, english, finnish, french, german, german2, hungarian, italian, kraaij_pohlmann,
# lovins, norwegian, porter, portuguese, romanian, russian, spanish, swedish and turkish.
stem_langs = ['english']
ENVIRONMENT = File.join(REDMINE_ROOT, 'config/environment.rb')
env = 'production'
########################################################################################################################
# END Configuration parameters
########################################################################################################################
retry_failed = false
no_delete = false
max_size = ''
overwrite = false
VERSION = '0.3'
optparse = OptionParser.new do |opts|
opts.banner = 'Usage: xapian_indexer.rb [OPTIONS...]'
opts.separator('')
opts.separator("Index Redmine's DMS documents")
opts.separator('')
opts.separator('')
opts.separator('Options:')
opts.on('-d', '--index_db DB_PATH', 'Absolute path to index database according plugin settings in UI') do |db|
db_root_path = db
end
opts.on('-s', '--stemming_lang a,b,c', Array, 'Comma separated list of stemming languages for indexing') do |s|
stem_langs = s
end
opts.on('-v', '--verbose', 'verbose') do
verbose = true
end
opts.on('-e', '--environment ENV', 'Rails ENVIRONMENT(development, testing or production), default production') do |e|
env = e
end
opts.on('-V', '--version', 'show version and exit') do
$stdout.puts VERSION
exit
end
opts.on('-h', '--help', 'show help and exit') do
$stdout.puts opts
exit
end
opts.on('-R', '--retry-failed', 'retry files which omindex failed to extract text') do
retry_failed = true
end
opts.on('-p', '--no-delete', 'skip the deletion of records corresponding to deleted files') do
no_delete = true
end
opts.on('-m', '--max-size SIZE', "maximum size of file to index(e.g.: '5M', '1G',...)") do |m|
max_size = m
end
opts.on('', '--overwrite', 'create the database anew instead of updating') do
overwrite = true
end
opts.separator('')
opts.separator('Examples:')
opts.separator(' xapian_indexer.rb -s english,italian -v')
opts.separator(' xapian_indexer.rb -d $HOME/index_db -s english,italian -v')
opts.separator('')
opts.summary_width = 25
end
optparse.parse!
ENV['RAILS_ENV'] = env
def log(text, verbose, error: false)
if error
warn text
elsif verbose
$stdout.puts text
end
end
def system_or_raise(command, verbose)
if verbose
system command, exception: true
else
system command, out: '/dev/null', exception: true
end
end
log "Trying to load Redmine environment <<#{ENVIRONMENT}>>...", verbose
begin
require ENVIRONMENT
log "Redmine environment [RAILS_ENV=#{env}] correctly loaded ...", verbose
# Indexing documents
stem_langs.each do |lang|
filespath = RedmineDmsf.dmsf_storage_directory
unless File.directory?(filespath)
warn "'#{filespath}' doesn't exist."
exit 1
end
databasepath = File.join(db_root_path, lang)
unless File.directory?(databasepath)
log "#{databasepath} does not exist, creating ...", verbose
FileUtils.mkdir_p databasepath
end
cmd = "#{OMINDEX} -s #{lang} --db #{databasepath} #{filespath} --url / --depth-limit=0"
cmd << ' -v' if verbose
cmd << ' --retry-failed' if retry_failed
cmd << ' -p' if no_delete
cmd << " -m #{max_size}" if max_size.present?
cmd << ' --overwrite' if overwrite
log cmd, verbose
system_or_raise cmd, verbose
end
log 'Redmine DMS documents indexed', verbose
rescue LoadError => e
warn e.message
exit 1
end
exit 0

View File

@ -27,7 +27,7 @@ Redmine::Plugin.register :redmine_dmsf do
author_url 'https://github.com/picman/redmine_dmsf/graphs/contributors' author_url 'https://github.com/picman/redmine_dmsf/graphs/contributors'
author 'Vít Jonáš / Daniel Munn / Karel Pičman' author 'Vít Jonáš / Daniel Munn / Karel Pičman'
description 'Document Management System Features' description 'Document Management System Features'
version '4.2.4 devel' version '5.0.0 devel'
requires_redmine version_or_higher: '6.1.0' requires_redmine version_or_higher: '6.1.0'

View File

@ -47,7 +47,9 @@ module RedmineDmsf
end end
def add_dmsf_file(dmsf_file, member = nil, root_path = nil, path = nil) def add_dmsf_file(dmsf_file, member = nil, root_path = nil, path = nil)
raise DmsfFileNotFoundError unless dmsf_file&.last_revision && File.exist?(dmsf_file.last_revision.disk_file) raise DmsfFileNotFoundError unless dmsf_file&.last_revision
raise DmsfFileNotFoundError unless dmsf_file.last_revision.file.attached?
if path if path
string_path = path string_path = path
@ -62,7 +64,7 @@ module RedmineDmsf
zip_entry = ::Zip::Entry.new(@zip_file, string_path, nil, nil, nil, nil, nil, nil, zip_entry = ::Zip::Entry.new(@zip_file, string_path, nil, nil, nil, nil, nil, nil,
::Zip::DOSTime.at(dmsf_file.last_revision.updated_at)) ::Zip::DOSTime.at(dmsf_file.last_revision.updated_at))
@zip_file.put_next_entry zip_entry @zip_file.put_next_entry zip_entry
File.open(dmsf_file.last_revision.disk_file, 'rb') do |f| dmsf_file.last_revision.file.open do |f|
while (buffer = f.read(8192)) while (buffer = f.read(8192))
@zip_file.write buffer @zip_file.write buffer
end end
@ -71,31 +73,6 @@ module RedmineDmsf
@dmsf_files << dmsf_file @dmsf_files << dmsf_file
end end
def add_attachment(attachment, path)
return if @files.include?(path)
raise DmsfFileNotFoundError unless File.exist?(attachment.diskfile)
zip_entry = ::Zip::Entry.new(@zip_file, path, nil, nil, nil, nil, nil, nil,
::Zip::DOSTime.at(attachment.created_on))
@zip_file.put_next_entry zip_entry
File.open(attachment.diskfile, 'rb') do |f|
while (buffer = f.read(8192))
@zip_file.write buffer
end
end
@files << path
end
def add_raw_file(filename, data)
return if @files.include?(filename)
zip_entry = ::Zip::Entry.new(@zip_file, filename, nil, nil, nil, nil, nil, nil, ::Zip::DOSTime.now)
@zip_file.put_next_entry zip_entry
@zip_file.write data
@files << filename
end
def add_dmsf_folder(dmsf_folder, member, root_path = nil) def add_dmsf_folder(dmsf_folder, member, root_path = nil)
string_path = dmsf_folder.dmsf_path_str + File::SEPARATOR string_path = dmsf_folder.dmsf_path_str + File::SEPARATOR
string_path = string_path[(root_path.length + 1)..string_path.length] if root_path string_path = string_path[(root_path.length + 1)..string_path.length] if root_path

View File

@ -46,6 +46,8 @@ module RedmineDmsf
office_bin = RedmineDmsf.office_bin.presence || 'libreoffice' office_bin = RedmineDmsf.office_bin.presence || 'libreoffice'
cmd = "#{shell_quote(office_bin)} --convert-to pdf --headless --outdir #{shell_quote(dir)} #{shell_quote(source)}" cmd = "#{shell_quote(office_bin)} --convert-to pdf --headless --outdir #{shell_quote(dir)} #{shell_quote(source)}"
if system(cmd) if system(cmd)
filename = "#{File.basename(source, '.*')}.pdf"
FileUtils.mv File.join(dir, filename), target
target target
else else
Rails.logger.error "Creating preview failed (#{$CHILD_STATUS}):\nCommand: #{cmd}" Rails.logger.error "Creating preview failed (#{$CHILD_STATUS}):\nCommand: #{cmd}"

View File

@ -697,10 +697,7 @@ module RedmineDmsf
# implementation of service for request, which allows for us to pipe a single file through # implementation of service for request, which allows for us to pipe a single file through
# also best-utilising Dav4rack's implementation. # also best-utilising Dav4rack's implementation.
def download def download
raise NotFound unless file&.last_revision raise NotFound unless file.last_revision&.file&.attached?
disk_file = file.last_revision.disk_file
raise NotFound unless disk_file && File.exist?(disk_file)
raise Forbidden unless !parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder) raise Forbidden unless !parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder)
# If there is no range (start of ranged download, or direct download) then we log the # If there is no range (start of ranged download, or direct download) then we log the
@ -719,7 +716,9 @@ module RedmineDmsf
Rails.logger.error "Could not send email notifications: #{e.message}" Rails.logger.error "Could not send email notifications: #{e.message}"
end end
end end
File.new disk_file file.last_revision.file.open do |f|
File.new f.path
end
end end
def reuse_version_for_locked_file?(file) def reuse_version_for_locked_file?(file)

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
module RedmineDmsf
# ActiveRecord Analyzer for Xapian
class XapianAnalyzer < ActiveStorage::Analyzer
def self.accept?(blob)
return false unless RedmineDmsf::Plugin.lib_available?('xapian') && blob.byte_size < 1_024 * 1_024 * 3 # 3MB
@blob = blob
true
end
def metadata
index
{}
end
private
def index
stem_lang = RedmineDmsf.dmsf_stemming_lang
db_path = File.join RedmineDmsf.dmsf_index_database, stem_lang
url = File.join(@blob.key[0..1], @blob.key[2..3])
dir = File.join(Dir.tmpdir, @blob.key)
FileUtils.mkdir dir
@blob.open do |file|
FileUtils.mv file.path, File.join(dir, @blob.key)
system "omindex -s \"#{stem_lang}\" -D \"#{db_path}\" --url=/#{url} \"#{dir}\" -p", exception: true
end
rescue StandardError => e
Rails.logger.error e.message
ensure
FileUtils.rm_f dir
end
end
end

View File

@ -51,32 +51,6 @@ class DmsfZipTest < RedmineDmsf::Test::HelperTest
end end
end end
def test_add_attachment
assert File.exist?(@attachment6.diskfile), @attachment6.diskfile
@zip.add_attachment @attachment6, @attachment6.filename
assert_equal 0, @zip.dmsf_files.size
zip_file = @zip.finish
Zip::File.open(zip_file) do |file|
file.each do |entry|
assert_equal @attachment6.filename, entry.name
end
end
end
def test_add_raw_file
filename = 'data.txt'
content = '1,2,3'
@zip.add_raw_file filename, content
assert_equal 0, @zip.dmsf_files.size
zip_file = @zip.finish
Zip::File.open(zip_file) do |file|
file.each do |entry|
assert_equal filename, entry.name
assert_equal content, entry.get_input_stream.read
end
end
end
def test_read def test_read
@zip.add_dmsf_file @dmsf_file1 @zip.add_dmsf_file @dmsf_file1
assert_not_empty @zip.read assert_not_empty @zip.read