Compare commits

...

No commits in common. "main" and "active_storage" have entirely different histories.

116 changed files with 1927 additions and 1044 deletions

195
.github/workflows/rubyonrails.yml vendored Normal file
View File

@ -0,0 +1,195 @@
# 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/>.
# GitHub CI script
name: "GitHub CI"
on:
push:
branches: ["active_storage"]
pull_request:
branches: ["active_storage"]
jobs:
plugin_tests:
strategy:
fail-fast: false
matrix:
engine: [mysql, postgresql, sqlite]
include:
- engine: mysql
# Database configuration for Redmine
database_configuration: >
test:
adapter: mysql2
database: test
username: redmine
password: redmine
encoding: utf8mb4
collation: utf8mb4_unicode_ci
# SQL commands to create a database for Redmine
sql1: CREATE DATABASE IF NOT EXISTS test CHARACTER SET utf8mb4;
sql2: CREATE USER 'redmine'@'localhost' IDENTIFIED BY 'redmine';
sql3: GRANT ALL PRIVILEGES ON test.* TO 'redmine'@'localhost';
# SQL client command
database_command: mysql -uroot -proot -e
# SQL service
database_service: mysql
- engine: postgresql
# Database configuration for Redmine
database_configuration: >
test:
adapter: postgresql
database: test
username: redmine
password: redmine
host: localhost
# SQL commands to create a database for Redmine
sql1: CREATE ROLE redmine LOGIN ENCRYPTED PASSWORD 'redmine' NOINHERIT VALID UNTIL 'infinity';
sql2: CREATE DATABASE test WITH ENCODING='UTF8' OWNER=redmine;
sql3: ALTER USER redmine CREATEDB;ALTER ROLE redmine WITH SUPERUSER;
# SQL client command
database_command: sudo -u postgres psql -c
# SQL service
database_service: postgresql
- engine: sqlite
# Database configuration for Redmine
database_configuration: >
test:
adapter: sqlite3
database: db/redmine.sqlite3
# No database needed here. It's just a file.
runs-on: ubuntu-latest
env:
RAILS_ENV: test
NAME: redmine_dmsf
steps:
- name: Install dependencies
# Install necessary packages
run: |
sudo apt-get update
sudo apt-get install -y litmus libreoffice subversion xapian-omega
- name: Clone Redmine
# Get the latest stable Redmine
run: svn export https://svn.redmine.org/redmine/branches/6.1-stable/ /opt/redmine
- name: Checkout code
uses: actions/checkout@v3
- name: Link the plugin
# Link the plugin to the redmine folder
run: |
ln -s $(pwd) /opt/redmine/plugins/redmine_dmsf
- name: Install Ruby and gems
uses: ruby/setup-ruby@v1 # The latest major version
with:
bundler-cache: true
ruby-version: '3.2'
- name: Setup database
# Create the database
run: |
echo "${{matrix.database_configuration}}" > /opt/redmine/config/database.yml
if [[ "${{matrix.database_service}}" ]]; then
sudo systemctl start ${{matrix.engine}}
fi
if [[ "${{matrix.database_command}}" ]]; then
${{matrix.database_command}} "${{matrix.sql1}}"
${{matrix.database_command}} "${{matrix.sql2}}"
${{matrix.database_command}} "${{matrix.sql3}}"
fi
- name: Configure WebDAV
# Add configuration for WebDAV to work
run: |
cp /opt/redmine/config/additional_environment.rb.example /opt/redmine/config/additional_environment.rb
echo "# WebDAV" >> /opt/redmine/config/additional_environment.rb
echo "config.log_level = :info" >> /opt/redmine/config/additional_environment.rb
echo "# Redmine DMSF's WebDAV" >> /opt/redmine/config/additional_environment.rb
echo "require \"#{Rails.root}/plugins/redmine_dmsf/lib/redmine_dmsf/webdav/custom_middleware\"" >> /opt/redmine/config/additional_environment.rb
echo "config.middleware.insert_before ActionDispatch::Cookies, RedmineDmsf::Webdav::CustomMiddleware" >> /opt/redmine/config/additional_environment.rb
- name: Configure Active Storage
# Add configuration for Active Storage to work
run: |
echo "# Active Storage" >> /opt/redmine/config/additional_environment.rb
echo "require 'active_storage/engine'" >> /opt/redmine/config/additional_environment.rb
echo "require Rails.root.join('plugins', 'redmine_dmsf', 'lib', 'redmine_dmsf', 'xapian_analyzer').to_s" >> /opt/redmine/config/additional_environment.rb
echo "config.active_storage.service = :test" >> /opt/redmine/config/additional_environment.rb
echo "config.active_storage.analyzers.append RedmineDmsf::XapianAnalyzer" >> /opt/redmine/config/additional_environment.rb
echo "# Active Storage" > /opt/redmine/config/storage.yml
echo "test:" >> /opt/redmine/config/storage.yml
echo " service: Disk" >> /opt/redmine/config/storage.yml
echo " root: <%= Rails.root.join('files', 'dmsf') %>" >> /opt/redmine/config/storage.yml
- name: Install Redmine
# Install Redmine
run: |
cd /opt/redmine
bundle config set --local without 'development'
bundle install
bundle exec rake generate_secret_token
bin/rails active_storage:install
bundle exec rake db:migrate
bundle exec rake redmine:plugins:migrate
bundle exec rake redmine:load_default_data
bundle exec rake assets:precompile
env:
REDMINE_LANG: en
- name: Standard tests
# Run the tests
run: |
cd /opt/redmine
bundle exec rake redmine:plugins:test:units
bundle exec rake redmine:plugins:test:functionals
bundle exec rake redmine:plugins:test:integration
- name: Helpers tests
run: |
cd /opt/redmine
ruby plugins/redmine_dmsf/test/helpers/dmsf_files_helper_test.rb
ruby plugins/redmine_dmsf/test/helpers/dmsf_helper_test.rb
ruby plugins/redmine_dmsf/test/helpers/dmsf_links_helper_test.rb
ruby plugins/redmine_dmsf/test/helpers/dmsf_queries_helper_test.rb
- name: Rubocop
# Run the Rubocop tests
run: |
cd /opt/redmine
bundle exec rubocop -c plugins/redmine_dmsf/.rubocop.yml plugins/redmine_dmsf/
- name: Litmus
Prepare Redmine's environment for WebDAV testing
Run Puma server
Run Litmus tests (Omit 'http' tests due to 'timeout waiting for interim response' and locks due to complex bogus conditional)
Shutdown Puma
Clean up Redmine's environment from WebDAV testing
run: |
cd /opt/redmine
bundle exec rake redmine:dmsf_webdav_test_on
bundle exec rails server -u Puma -e test -d
sleep 5
litmus http://localhost:3000/dmsf/webdav/dmsf_test_project admin admin
kill $(pgrep -f puma)
bundle exec rake redmine:dmsf_webdav_test_off
env:
TESTS: "basic copymove props"
- name: Cleanup
# Rollback plugin's changes to the database
# Stop the database engine
run: |
cd /opt/redmine
bundle exec rake redmine:plugins:migrate VERSION=0
if [[ "${{matrix.database_service}}" ]]; then
sudo systemctl stop ${{matrix.engine}}
fi
- name: Archive test.log
if: always()
uses: actions/upload-artifact@v4
with:
name: "test_${{matrix.engine}}.log"
path: /opt/redmine/log/test.log

View File

@ -118,6 +118,8 @@ Rails/SkipsModelValidations:
- db/migrate/20140519133201_trash_bin.rb
- db/migrate/07_dmsf_1_4_4.rb
- db/migrate/20240829093801_rename_dmsf_digest_token.rb
- db/migrate/20251015130601_active_storage_migration.rb
- db/migrate/20251017101001_restore_updated_at.rb
Rails/ThreeStateBooleanColumn:
Exclude:
@ -130,6 +132,10 @@ Rails/UniqueValidationWithoutIndex:
- app/models/dmsf_file.rb
- app/models/dmsf_workflow_step.rb # Impossible due to steps sorting
Style/ClassVars:
Exclude:
- lib/redmine_dmsf.rb # @@xapian_available
Style/ExpandPathArguments:
Enabled: false

View File

@ -1,6 +1,9 @@
Changelog for Redmine DMSF
==========================
4.2.4 *????-??-??*
------------------
4.2.3 *2025-10-06*
------------------

View File

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

377
README.md Normal file
View File

@ -0,0 +1,377 @@
# 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)
[![Support Ukraine Badge](https://bit.ly/support-ukraine-now)](https://github.com/support-ukraine/support-ukraine)
Redmine DMSF is Document Management System Features plugin for Redmine issue tracking system; It is aimed to replace current Redmine's Documents module.
Redmine DMSF now comes bundled with WebDAV functionality: if switched on within plugin settings this will be accessible from _/dmsf/webdav_.
WebDAV functionality is provided through Dav4Rack library.
The development has been supported by [Kontron](https://www.kontron.com) and has been released as open source thanks to their generosity.
Project home: <https://github.com/picman/redmine_dmsf>
Redmine Document Management System "Features" plugin is distributed under GNU General Public License v3 (GPL).
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
<https://www.gnu.org/licenses/gpl-3.0.html>
## Features
* Directory structure
* Document versioning / revision history
* Document locking
* Multi (drag/drop depending on browser) upload/download
* Direct document or document link sending via email
* Configurable document approval workflow
* Document access auditing
* Integration with Redmine's activity feed
* Wiki macros for a quick content linking
* Full read/write WebDAV functionality
* Optional document content full-text search
* Documents and folders' symbolic links
* Trash bin
* Documents attachable to issues
* Office documents are displayed inline
* Editing of office documents
* REST API
* DMS Document revision as a custom field type
* Compatible with Redmine 6
## Dependencies
* Redmine 6.1 or higher
### Full-text search (optional)
#### Indexing
If you want to use full-text search features, you must setup file content indexing.
It is necessary to index DMSF files with omindex before searching attempts to receive some output:
1. Change the configuration part of redmine_dmsf/extra/xapian_indexer.rb file according to your environment.
(The path to the index database set in xapian_indexer.rb must corresponds to the path set in the plugin's settings.)
2. Run `ruby redmine_dmsf/extra/xapian_indexer.rb -v`
This command should be run on regular basis (e.g. from cron)
Example of cron job (once per hour at 8th minute):
8 * * * * root /usr/bin/ruby redmine_dmsf/extra/xapian_indexer.rb
See redmine_dmsf/extra/xapian_indexer.rb for help.
#### Searching
If you want to use fulltext search abilities, install xapian packages. In case of using of Bitnami
stack or Ruby installed via RVM it might be necessary to install Xapian bindings from sources. See https://xapian.org
for details.
To index some files with omega you may have to install some other packages like
xpdf, antiword, ...
From Omega documentation:
* HTML (.html, .htm, .shtml, .shtm, .xhtml, .xhtm)
* PHP (.php) - our HTML parser knows to ignore PHP code
* text files (.txt, .text)
* SVG (.svg)
* CSV (Comma-Separated Values) files (.csv)
* PDF (.pdf) if pdftotext is available (comes with poppler or xpdf)
* PostScript (.ps, .eps, .ai) if ps2pdf (from ghostscript) and pdftotext (comes with poppler or xpdf) are available
* OpenOffice/StarOffice documents (.sxc, .stc, .sxd, .std, .sxi, .sti, .sxm, .sxw, .sxg, .stw) if unzip is available
* OpenDocument format documents (.odt, .ods, .odp, .odg, .odc, .odf, .odb, .odi, .odm, .ott, .ots, .otp, .otg, .otc, .otf, .oti, .oth) if unzip is available
* MS Word documents (.dot) if antiword is available (.doc files are left to libmagic, as they may actually be RTF (AbiWord saves RTF when asked to save as .doc, and Microsoft Word quietly loads RTF files with a .doc extension), or plain-text).
* MS Excel documents (.xls, .xlb, .xlt, .xlr, .xla) if xls2csv is available (comes with catdoc)
* MS Powerpoint documents (.ppt, .pps) if catppt is available (comes with catdoc)
* MS Office 2007 documents (.docx, .docm, .dotx, .dotm, .xlsx, .xlsm, .xltx, .xltm, .pptx, .pptm, .potx, .potm, .ppsx, .ppsm) if unzip is available
* Wordperfect documents (.wpd) if wpd2text is available (comes with libwpd)
* MS Works documents (.wps, .wpt) if wps2text is available (comes with libwps)
* MS Outlook message (.msg) if perl with Email::Outlook::Message and HTML::Parser modules is available
* MS Publisher documents (.pub) if pub2xhtml is available (comes with libmspub)
* AbiWord documents (.abw)
* Compressed AbiWord documents (.zabw)
* Rich Text Format documents (.rtf) if unrtf is available
* Perl POD documentation (.pl, .pm, .pod) if pod2text is available
* reStructured text (.rst, .rest) if rst2html is available (comes with docutils)
* Markdown (.md, .markdown) if markdown is available
* TeX DVI files (.dvi) if catdvi is available
* DjVu files (.djv, .djvu) if djvutxt is available
* XPS files (.xps) if unzip is available
* Debian packages (.deb, .udeb) if dpkg-deb is available
* RPM packages (.rpm) if rpm is available
* Atom feeds (.atom)
* MAFF (.maff) if unzip is available
* MHTML (.mhtml, .mht) if perl with MIME::Tools is available
* MIME email messages (.eml) and USENET articles if perl with MIME::Tools and HTML::Parser is available
* vCard files (.vcf, .vcard) if perl with Text::vCard is available
You can use following commands to install some of the required indexing tools:
On Debian use:
```
sudo apt-get install xapian-omega ruby-xapian libxapian-dev poppler-utils antiword unzip catdoc libwpd-tools \
libwps-tools gzip unrtf catdvi djview djview3 uuid uuid-dev xz-utils libemail-outlook-message-perl
```
On Ubuntu use:
```
sudo apt-get install xapian-omega ruby-xapian libxapian-dev poppler-utils antiword unzip catdoc libwpd-tools \
libwps-tools gzip unrtf catdvi djview djview3 uuid uuid-dev xz-utils libemail-outlook-message-perl
```
On CentOS use:
```
sudo yum install xapian-core xapian-bindings-ruby libxapian-dev poppler-utils antiword unzip catdoc libwpd-tools \
libwps-tools gzip unrtf catdvi djview djview3 uuid uuid-dev xz libemail-outlook-message-perl
```
## Inline displaying of office documents (optional)
If LibreOffice binary `libreoffice` is present in the server, office documents (.odt, .ods,...) are displayed inline.
The command must be runable by the web app's user. Test it in advance, e.g:
`sudo -u www-data libreoffice --convert-to pdf my_document.odt`
`libreoffice` package is available in the most of Linux distributions, e.g. on Debain based systems:
```
sudo apt install libreoffice liblibreoffice-java
```
## Usage
DMSF is designed to act as project module, so it must be checked as an enabled module within the project settings.
Search will now automatically search DMSF content when a Redmine search is performed, additionally a "Documents" and "Folders" check box will be visible, allowing you to search DMSF content exclusively.
## Linking DMSF object from Wiki entries (macros)
You can link DMSF object from Wikis using a macro tag `{{ }}`. List of available macros with their description is
available from the wiki's toolbar.
## Hooks
You can implement these hooks in your plugin and extend DMSF functionality in certain events.
E.g.
class DmsfUploadControllerHooks < Redmine::Hook::Listener
def dmsf_upload_controller_after_commit(context={})
context[:controller].flash[:info] = 'Okay'
end
end
**dmsf_upload_controller_after_commit**
Called after all uploaded files are committed.
parameters: *files*
**dmsf_helper_upload_after_commit**
Called after an individual file is committed. The controller is not available.
Parameters: *file*
**dmsf_workflow_controller_before_approval**
Called before an approval. If the hook returns false, the approval is not recorded.
parameters: *revision*, *step_action*
**dmsf_files_controller_before_view**
Allows a preview of the file by an external plugin. If the hook returns true, the file is not sent by DMSF. It is
expected that the file is sent by the hook.
parameters: *file*
## Setup / Upgrade
You can either clone the master branch or download the latest zipped version. Before installing ensure that the Redmine
instance is stopped.
git clone git@github.com:picman/redmine_dmsf.git
wget https://github.com/picman/redmine_dmsf/archive/master.zip
1. In case of upgrade **BACKUP YOUR DATABASE, ORIGINAL PLUGIN AND THE FOLDER WITH DOCUMENTS** first!!!
2. Put redmine_dmsf plugin directory into plugins. The plugins sub-directory must be named just **redmine_dmsf**. In case
of need rename _redmine_dmsf-x.y.z_ to *redmine_dmsf*.
3. **Go to the redmine directory**
`cd redmine`
4. Install dependencies:
`bundle install`
4.1 In production environment
bundle config set --local without 'development test'
bundle install
4.2 Without Xapian fulltext search (on Windows)
bundle config set --local without 'xapian'
bundle install
5. Install Active Storage => [Active Storage](#active-storage)
6. Enable WebDAV => [WebDAV](#webdav)
7. Initialize/Update database
`RAILS_ENV=production bundle exec rake redmine:plugins:migrate NAME=redmine_dmsf`
8. Install assets
`RAILS_ENV="production" bundle exec rake assets:precompile`
9. The access rights must be set for web server, e.g.:
`chown -R www-data:www-data plugins/redmine_dmsf`
10. Restart the web server, e.g.:
`systemctl restart apache2`
11. You should configure the plugin via Redmine interface: Administration -> Plugins -> DMSF -> Configure. (You should check and then save the plugin's configuration after each upgrade.)
12. Don't forget to grant permissions for DMSF in Administration -> Roles and permissions
13. Assign DMSF permissions to appropriate roles.
14. There are a few handy rake tasks:
I) To convert documents from the standard Redmine document module
Available options:
* project - id or identifier of a project (default to all projects)
* dry_run - perform just a check without any conversion
* issues - Convert also files attached to issues
Example:
rake redmine:dmsf_convert_documents project=test RAILS_ENV="production"
(If you don't run the rake task as the web server user, don't forget to change the ownership of the imported files, e.g.
chown -R www-data:www-data /redmine/files/dmsf
afterwards)
II) To alert all users who are expected to do an approval in the current approval steps
Example:
rake redmine:dmsf_alert_approvals RAILS_ENV="production"
III) To create missing checksums for all document revisions
Available options:
* dry_run - test, no changes to the database
* forceSHA256 - replace old MD5 with SHA256
Example:
bundle exec rake redmine:dmsf_create_digests RAILS_ENV="production"
bundle exec rake redmine:dmsf_create_digests forceSHA256=1 RAILS_ENV="production"
bundle exec rake redmine:dmsf_create_digests dry_run=1 RAILS_ENV="production"
IV) To maintain DMSF
* Remove all files with no database record from the document directory
* Remove all links project_id = -1 (added links to an issue which hasn't been created)
Available options:
* dry_run - No physical deletion but to list of all unused files only
Example:
rake redmine:dmsf_maintenance RAILS_ENV="production"
rake redmine:dmsf_maintenance dry_run=1 RAILS_ENV="production"
### WebDAV
In order to enable WebDAV module, it is necessary to put the following code into your
`config/additional_environment.rb`:
```ruby
# Redmine DMSF's WebDAV
require Rails.root.join('plugins', 'redmine_dmsf', 'lib', 'redmine_dmsf', 'webdav', 'custom_middleware').to_s
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
In order to documents and folders are available via WebDAV in case that the Redmine is configured to be run in a sub-uri
it's necessary to add the following configuration option into your `config/additional_environment.rb`:
```ruby
config.relative_url_root = '/redmine'
```
## Uninstalling DMSF
Before uninstalling the DMSF plugin, please ensure that the Redmine instance is stopped.
1. `cd [redmine-install-dir]`
2. `rake redmine:plugins:migrate NAME=redmine_dmsf VERSION=0 RAILS_ENV=production`
3. `rm plugins/redmine_dmsf -Rf`
After these steps re-start your instance of Redmine.
## Contributing
If you've added something, why not share it. Fork the repository (github.com/picman/redmine_dmsf),
make the changes and send a pull request to the maintainers.
Changes with tests, and full documentation are preferred.
## Additional Documentation
[CHANGELOG.md](CHANGELOG.md) - Project changelog
---
Special thanks to <a href="https://jetbrains.com"><img src="jetbrains-variant-3.svg" alt="JetBrains logo" width="59" height="68"></a> for providing an excellent IDE.

View File

@ -510,7 +510,7 @@ class DmsfController < ApplicationController
raise DmsfAccessError unless User.current.allowed_to?(:email_documents, @project)
zip = Zip.new
zip_entries(zip, selected_folders, selected_files)
zip_entries zip, selected_folders, selected_files
zipped_content = zip.finish
max_filesize = RedmineDmsf.dmsf_max_email_filesize
@ -548,7 +548,7 @@ class DmsfController < ApplicationController
def download_entries(selected_folders, selected_files)
zip = Zip.new
zip_entries(zip, selected_folders, selected_files)
zip_entries zip, selected_folders, selected_files
zip.dmsf_files.each do |f|
# Action
audit = DmsfFileRevisionAccess.new
@ -583,7 +583,7 @@ class DmsfController < ApplicationController
end
selected_files.each do |selected_file_id|
file = DmsfFile.visible.find_by(id: selected_file_id)
raise DmsfFileNotFoundError unless file&.last_revision && File.exist?(file.last_revision&.disk_file)
raise DmsfFileNotFoundError unless file.last_revision&.file&.attached?
unless (file.project == @project) || User.current.allowed_to?(:view_dmsf_files, file.project)
raise DmsfAccessError

View File

@ -52,7 +52,7 @@ class DmsfFilesController < ApplicationController
end
check_project @revision.dmsf_file
raise ActionController::MissingFile if @file.deleted?
raise ActionController::MissingFile if @file.deleted? || !@revision.file.attached?
# Action
access = DmsfFileRevisionAccess.new
@ -67,7 +67,7 @@ class DmsfFilesController < ApplicationController
Rails.logger.error "Could not send email notifications: #{e.message}"
end
# Allow a preview of the file by an external plugin
results = call_hook(:dmsf_files_controller_before_view, { file: @revision.disk_file })
results = call_hook(:dmsf_files_controller_before_view, { file: @revision.file.download })
return if results.first == true
member = Member.find_by(user_id: User.current.id, project_id: @file.project.id)
@ -82,14 +82,14 @@ class DmsfFilesController < ApplicationController
# Text preview
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)
@content = File.read(@revision.disk_file, mode: 'rb')
@content = @revision.file.download
render action: 'document'
# Offer the file for download
else
params[:disposition] = 'attachment' if params[:filename].present?
send_file @revision.disk_file,
send_data @revision.file.download,
filename: filename,
type: @revision.detect_content_type,
type: @revision.content_type,
disposition: params[:disposition].presence || @revision.dmsf_file.disposition
end
rescue DmsfAccessError => e
@ -131,41 +131,30 @@ class DmsfFilesController < ApplicationController
revision.minor_version = DmsfUploadHelper.db_version(params[:version_minor])
revision.patch_version = DmsfUploadHelper.db_version(params[:version_patch])
# New content
if params[:dmsf_attachments].present?
keys = params[:dmsf_attachments].keys
file_upload = params[:dmsf_attachments][keys.first] if keys&.first
end
if file_upload
upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, file_upload)
if upload
revision.size = upload.size
revision.disk_filename = revision.new_storage_filename
revision.mime_type = upload.mime_type
revision.digest = upload.digest
a = Attachment.find_by_token(file_upload[:token]) if file_upload
if a
revision.size = a.filesize
revision.shared_file.attach(
io: File.open(a.diskfile),
filename: a.filename,
content_type: a.content_type.presence || 'application/octet-stream',
identify: false
)
end
else
revision.size = last_revision.size
revision.disk_filename = last_revision.disk_filename
revision.mime_type = last_revision.mime_type
revision.digest = last_revision.digest
end
# Custom fields
revision.copy_custom_field_values(params[:dmsf_file_revision][:custom_field_values], last_revision)
@file.name = revision.name
ok = true
if revision.save
revision.assign_workflow params[:dmsf_workflow_id]
if upload
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?
if @file.locked? && !@file.locks.empty?
begin
@file.unlock!
flash[:notice] = "#{l(:notice_file_unlocked)}, "
@ -333,20 +322,6 @@ class DmsfFilesController < ApplicationController
redirect_to trash_dmsf_path(@project)
end
def thumbnail
tbnail = @file.thumbnail(size: params[:size])
if tbnail
if stale?(etag: tbnail)
send_file tbnail,
filename: filename_for_content_disposition(@file.last_revision.disk_file),
type: @file.last_revision.detect_content_type,
disposition: 'inline'
end
else
head :not_found
end
end
private
def find_file

View File

@ -29,10 +29,10 @@ class DmsfPublicUrlsController < ApplicationController
revision = dmsf_public_url.dmsf_file.last_revision
begin
# IE has got a tendency to cache files
expires_in(0.years, 'must-revalidate' => true)
send_file(revision.disk_file,
expires_in 0.years, 'must-revalidate' => true
send_data(revision.file.download,
filename: filename_for_content_disposition(revision.name),
type: revision.detect_content_type,
type: revision.content_type,
disposition: dmsf_public_url.dmsf_file.disposition)
rescue StandardError => e
Rails.logger.error e.message

View File

@ -49,7 +49,6 @@ class DmsfUploadController < ApplicationController
@uploads.push upload
params[:committed_files][key][:disk_filename] = upload.disk_filename
params[:committed_files][key][:digest] = upload.digest
params[:committed_files][key][:tempfile_path] = upload.tempfile_path
end
commit_files if params[:committed_files].present?
@ -66,11 +65,6 @@ class DmsfUploadController < ApplicationController
# REST API and Redmine attachment form
def upload
unless request.media_type == 'application/octet-stream'
head :not_acceptable
return
end
@attachment = Attachment.new(file: request.body)
@attachment.author = User.current
@attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
@ -114,7 +108,6 @@ class DmsfUploadController < ApplicationController
uploaded_file[:disk_filename] = upload.disk_filename
uploaded_file[:tempfile_path] = upload.tempfile_path
uploaded_file[:size] = upload.size
uploaded_file[:digest] = upload.digest
uploaded_file[:mime_type] = upload.mime_type
end
commit_files_internal uploaded_files

View File

@ -34,7 +34,7 @@ module DmsfHelper
def self.sanitize_filename(filename)
# Get only the filename, not the whole path
just_filename = File.basename(filename.gsub('\\\\', '/'))
# Replace all non alphanumeric, hyphens or periods with underscore
# Replace all non-alphanumeric, hyphens or periods with underscore
just_filename.gsub!(/[^\w.\-]/, '_')
# Keep the extension if any
if !/^[a-zA-Z0-9_.\-]*$/.match?(just_filename) && just_filename =~ /(.[a-zA-Z0-9]+)$/

View File

@ -42,7 +42,6 @@ module DmsfUploadHelper
else
file = DmsfFile.new
file.project_id = project.id
file.name = name
file.dmsf_folder = folder
file.notification = RedmineDmsf.dmsf_default_notifications?
end
@ -68,9 +67,7 @@ module DmsfUploadHelper
new_revision.patch_version = if committed_file[:version_patch].present?
DmsfUploadHelper.db_version committed_file[:version_patch]
end
new_revision.mime_type = committed_file[:mime_type]
new_revision.size = committed_file[:size]
new_revision.digest = committed_file[:digest]
# Custom fields
new_revision.copy_custom_field_values(committed_file[:custom_field_values])
# Need to save file first to generate id for it in case of creation.
@ -86,8 +83,6 @@ module DmsfUploadHelper
next
end
new_revision.disk_filename = new_revision.new_storage_filename
if new_revision.save
new_revision.assign_workflow committed_file[:dmsf_workflow_id]
begin
@ -96,8 +91,12 @@ module DmsfUploadHelper
a = Attachment.find_by_token(committed_file[:token])
committed_file[:tempfile_path] = a.diskfile if a
end
FileUtils.mv committed_file[:tempfile_path], new_revision.disk_file(search_if_not_exists: false)
FileUtils.chmod 'u=wr,g=r', new_revision.disk_file(search_if_not_exists: false)
new_revision.shared_file.attach(
io: File.open(committed_file[:tempfile_path]),
filename: new_revision.name,
content_type: committed_file[:mime_type],
identify: false
)
file.last_revision = new_revision
files.push file
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

@ -42,15 +42,6 @@ class DmsfFile < ApplicationRecord
scope :visible, -> { where(deleted: STATUS_ACTIVE) }
scope :deleted, -> { where(deleted: STATUS_DELETED) }
validates :name, dmsf_file_name: true
validates :name, length: { maximum: 255 }
validates :name,
uniqueness: {
scope: %i[dmsf_folder_id project_id deleted],
conditions: -> { where(deleted: STATUS_ACTIVE) },
case_sensitive: true
}
acts_as_event(
title: proc { |o|
@searched_revision = nil
@ -81,7 +72,7 @@ class DmsfFile < ApplicationRecord
url: proc { |o|
if @searched_revision
{ controller: 'dmsf_files', action: 'view', id: o.id, download: @searched_revision.id,
filename: o.name }
filename: @searched_revision.name }
else
{ controller: 'dmsf_files', action: 'view', id: o.id, filename: o.name }
end
@ -104,7 +95,7 @@ class DmsfFile < ApplicationRecord
acts_as_watchable
acts_as_searchable(
columns: [
"#{table_name}.name",
"#{DmsfFileRevision.table_name}.name",
"#{DmsfFileRevision.table_name}.title",
"#{DmsfFileRevision.table_name}.description",
"#{DmsfFileRevision.table_name}.comment"
@ -143,11 +134,19 @@ class DmsfFile < ApplicationRecord
end
def self.find_file_by_name(project, folder, name)
findn_file_by_name project&.id, folder, name
dmsf_files = visible.where(dmsf_files: { project_id: project&.id, dmsf_folder_id: folder&.id })
dmsf_files.each do |file|
return file if file.name == name
end
nil
end
def self.findn_file_by_name(project_id, folder, name)
visible.find_by project_id: project_id, dmsf_folder_id: folder&.id, name: name
def self.find_file_by_title(project, folder, name)
dmsf_files = visible.where(dmsf_files: { project_id: project&.id, dmsf_folder_id: folder&.id })
dmsf_files.each do |file|
return file if file.title == name
end
nil
end
def approval_allowed_zero_minor
@ -155,10 +154,7 @@ class DmsfFile < ApplicationRecord
end
def last_revision
unless defined?(@last_revision)
@last_revision = deleted? ? dmsf_file_revisions.first : dmsf_file_revisions.visible.first
end
@last_revision
@last_revision ||= deleted? ? dmsf_file_revisions.first : dmsf_file_revisions.visible.first
end
def deleted?
@ -177,21 +173,21 @@ class DmsfFile < ApplicationRecord
if locked_for_user? && !User.current.allowed_to?(:force_file_unlock, project)
Rails.logger.info l(:error_file_is_locked)
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
errors.add(:base, l(:error_file_is_locked))
errors.add :base, l(:error_file_is_locked)
end
return false
end
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
destroy
else
self.deleted = STATUS_DELETED
self.deleted_by_user = User.current
save
# Associated revisions should be marked as deleted too
dmsf_file_revisions.each { |r| r.delete(commit: commit, force: true) }
end
rescue StandardError => e
Rails.logger.error e.message
@ -211,16 +207,20 @@ class DmsfFile < ApplicationRecord
save
end
def name
last_revision&.name.to_s
end
def title
last_revision ? last_revision.title : name
last_revision&.title.to_s
end
def description
last_revision ? last_revision.description : ''
last_revision&.description.to_s
end
def version
last_revision ? last_revision.version : '0'
last_revision&.version.to_s
end
def workflow
@ -228,7 +228,7 @@ class DmsfFile < ApplicationRecord
end
def size
last_revision ? last_revision.size : 0
last_revision&.size.to_i
end
def dmsf_path
@ -313,22 +313,13 @@ class DmsfFile < ApplicationRecord
file = DmsfFile.new
file.dmsf_folder_id = folder.id if folder
file.project_id = project.id
if DmsfFile.exists?(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, name: filename)
1.step do |i|
gen_filename = " #{filename} #{l(:dmsf_copy, n: i)}"
unless DmsfFile.exists?(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, name: gen_filename)
filename = gen_filename
break
end
end
end
file.name = filename
file.notification = RedmineDmsf.dmsf_default_notifications?
if file.save && last_revision
new_revision = last_revision.clone
new_revision.name = filename
new_revision.title = File.basename(filename, '.*')
new_revision.dmsf_file = file
new_revision.disk_filename = new_revision.new_storage_filename
# Assign the same workflow if it's a global one or we are in the same project
# Assign the same workflow if it's a global one, or we are in the same project
new_revision.workflow = nil
new_revision.dmsf_workflow_id = nil
new_revision.dmsf_workflow_assigned_by_user_id = nil
@ -340,8 +331,17 @@ class DmsfFile < ApplicationRecord
new_revision.set_workflow wf.id, nil
new_revision.assign_workflow wf.id
end
if File.exist? last_revision.disk_file
FileUtils.cp last_revision.disk_file, new_revision.disk_file(search_if_not_exists: false)
if last_revision.file.attached?
begin
new_revision.shared_file.attach(
io: StringIO.new(last_revision.file.download),
filename: filename,
content_type: new_revision.file.content_type,
identify: false
)
rescue ActiveStorage::FileNotFoundError => e
Rails.logger.error e
end
end
new_revision.comment = l(:comment_copied_from, source: "#{self.project.identifier}:#{dmsf_path_str}")
new_revision.custom_values = []
@ -351,10 +351,18 @@ class DmsfFile < ApplicationRecord
v.value = cv.value
new_revision.custom_values << v
end
# Check the name and title
basename = File.basename(filename, '.*')
extname = File.extname(filename)
i = 1
while new_revision.invalid? && i < 1_000
new_revision.title = "#{basename} (#{i})"
new_revision.name = "#{new_revision.title}#{extname}"
i += 1
end
if new_revision.save
file.last_revision = new_revision
else
errors.add :base, new_revision.errors.full_messages.to_sentence
Rails.logger.error new_revision.errors.full_messages.to_sentence
file.delete commit: true
file = nil
@ -397,7 +405,7 @@ class DmsfFile < ApplicationRecord
results = scope.where(find_options).uniq.to_a
results.delete_if { |x| !DmsfFolder.permissions?(x.dmsf_folder) }
if !options[:titles_only] && RedmineDmsf::Plugin.lib_available?('xapian')
if !options[:titles_only] && RedmineDmsf.xapian_available
database = nil
begin
lang = RedmineDmsf.dmsf_stemming_lang
@ -444,25 +452,24 @@ class DmsfFile < ApplicationRecord
matchset = enquire.mset(0, 1000)
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]
filename = dochash['url']
next unless filename
next unless dochash['url'] =~ %r{^\w{2}/\w{2}/(\w+)$} # /76/df/76dfsp2ubbgq4yvq90zrfoyxt012
dmsf_attrs = filename.scan(%r{^\d{4}/\d{2}/(\d{12}_(\d+)_.*)$})
id_attribute = 0
id_attribute = dmsf_attrs[0][1] if dmsf_attrs.length.positive?
next if dmsf_attrs.empty? || id_attribute.to_i.zero?
key = Regexp.last_match(1)
blob = ActiveStorage::Blob.find_by(key: key)
attachment = blob&.attachments&.first
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) &&
user.allowed_to?(:view_dmsf_files, dmsf_file.project) &&
(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']
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'))
end
break if options[:limit].present? && results.count >= options[:limit]
@ -489,29 +496,34 @@ class DmsfFile < ApplicationRecord
end
def text?
filename = last_revision&.disk_filename
Redmine::MimeType.is_type?('text', filename) ||
Redmine::SyntaxHighlighting.filename_supported?(filename)
return false unless last_revision
filename = last_revision.file&.blob&.filename.to_s
last_revision.file&.blob&.text? || Redmine::SyntaxHighlighting.filename_supported?(filename)
end
def image?
Redmine::MimeType.is_type?('image', last_revision&.disk_filename)
last_revision && last_revision.file&.blob&.image?
end
def pdf?
Redmine::MimeType.of(last_revision&.disk_filename) == 'application/pdf'
last_revision&.content_type == 'application/pdf'
end
def video?
Redmine::MimeType.is_type?('video', last_revision&.disk_filename)
return false unless last_revision
Redmine::MimeType.is_type?('video', last_revision.file.blob&.filename&.to_s)
end
def html?
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/html'
last_revision&.content_type == 'text/html'
end
def office_doc?
case File.extname(last_revision&.disk_filename)
return false unless last_revision
case File.extname(last_revision.file.blob&.filename&.to_s)
when '.odt', '.ods', '.odp', '.odg', # LibreOffice
'.doc', '.docx', '.docm', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.pptm', # MS Office
'.rtf' # Universal
@ -522,11 +534,11 @@ class DmsfFile < ApplicationRecord
end
def markdown?
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/markdown'
last_revision&.content_type == 'text/markdown'
end
def textile?
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/x-textile'
last_revision&.content_type == 'text/textile'
end
def disposition
@ -534,7 +546,7 @@ class DmsfFile < ApplicationRecord
end
def thumbnailable?
Redmine::Thumbnail.convert_available? && (image? || (pdf? && Redmine::Thumbnail.gs_available?))
last_revision.file&.variable?
end
def previewable?
@ -551,13 +563,14 @@ class DmsfFile < ApplicationRecord
def pdf_preview
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
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
Rails.logger.error do
%(An error occurred while generating preview for #{last_revision&.disk_file} to #{target}\n
Exception was: #{e.message})
%(An error occurred while generating preview for #{name} to #{target}\nException was: #{e.message})
end
''
end
@ -567,15 +580,16 @@ class DmsfFile < ApplicationRecord
result = +'No preview available'
if text?
begin
f = File.new(last_revision.disk_file)
f.each_line do |line|
case f.lineno
when 1
result = line
when limit.to_i + 1
break
else
result << line
last_revision.file.open do |f|
f.each_line do |line|
case f.lineno
when 1
result = line
when limit.to_i + 1
break
else
result << line
end
end
end
rescue StandardError => e
@ -586,11 +600,7 @@ class DmsfFile < ApplicationRecord
end
def formatted_name(member)
if last_revision
last_revision.formatted_name(member)
else
name
end
last_revision&.formatted_name(member)
end
def owner?(user)
@ -622,33 +632,6 @@ class DmsfFile < ApplicationRecord
nil
end
def extension
File.extname(last_revision.disk_filename).strip.downcase[1..] if last_revision
end
def thumbnail(options = {})
size = options[:size].to_i
if size.positive?
# Limit the number of thumbnails per image
size = (size / 50) * 50
# Maximum thumbnail size
size = 800 if size > 800
else
size = Setting.thumbnails_size.to_i
end
size = 100 unless size.positive?
target = File.join(Attachment.thumbnails_storage_path, "#{id}_#{last_revision.digest}_#{size}.thumb")
begin
Redmine::Thumbnail.generate last_revision.disk_file.to_s, target, size, pdf?
rescue StandardError => e
Rails.logger.error do
%(An error occured while generating thumbnail for #{last_revision.disk_file} to #{target}\n
Exception was: #{e.message})
end
nil
end
end
def locked_title
if locked_for_user?
return l(:title_locked_by_user, user: lock.reverse[0].user) if lock.reverse[0].user

View File

@ -30,6 +30,8 @@ class DmsfFileRevision < ApplicationRecord
belongs_to :dmsf_workflow_assigned_by_user, class_name: 'User'
belongs_to :dmsf_workflow
has_one_attached :shared_file
has_many :dmsf_file_revision_access, dependent: :destroy
has_many :dmsf_workflow_step_assignment, dependent: :destroy
@ -88,16 +90,42 @@ class DmsfFileRevision < ApplicationRecord
}
)
validates :title, presence: true
validates :title, length: { maximum: 255 }
validates :title, presence: true, length: { maximum: 255 }, dmsf_file_name: true
validates :major_version, presence: true
validates :name, dmsf_file_name: true
validates :name, length: { maximum: 255 }
validates :disk_filename, length: { maximum: 255 }
validates :name, dmsf_file_extension: true
validates :name,
presence: true,
dmsf_file_name: true,
length: { maximum: 255 },
dmsf_file_extension: true,
dmsf_file_revision_name: true
validates :description, length: { maximum: 1.kilobyte }
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 checksum
file.blob&.checksum
end
def content_type
res = file.blob&.content_type
res = Redmine::MimeType.of(file.blob&.filename) if res.blank?
res = 'application/octet-stream' if res.blank?
res
end
def visible?(_user = nil)
deleted == STATUS_ACTIVE
end
@ -110,14 +138,6 @@ class DmsfFileRevision < ApplicationRecord
dmsf_file&.dmsf_folder
end
def self.remove_extension(filename)
filename[0, (filename.length - File.extname(filename).length)]
end
def self.filename_to_title(filename)
remove_extension(filename).gsub(/_+/, ' ')
end
def delete(commit: false, force: true)
if dmsf_file.locked_for_user?
errors.add :base, l(:error_file_is_locked)
@ -167,51 +187,10 @@ class DmsfFileRevision < ApplicationRecord
ver
end
def storage_base_path
time = created_at || DateTime.current
DmsfFile.storage_path.join(time.strftime('%Y')).join time.strftime('%m')
end
def disk_file(search_if_not_exists: true)
path = storage_base_path
begin
FileUtils.mkdir_p(path)
rescue StandardError => e
Rails.logger.error e.message
end
filename = path.join(disk_filename)
if search_if_not_exists && !File.exist?(filename)
# Let's search for the physical file in source revisions
dmsf_file.dmsf_file_revisions.where(created_at: ...created_at).order(created_at: :desc).each do |rev|
filename = rev.disk_file
break if File.exist?(filename)
end
end
filename.to_s
end
def new_storage_filename
raise DmsfAccessError, 'File id is not set' unless dmsf_file&.id
filename = DmsfHelper.sanitize_filename(name)
timestamp = DateTime.current.strftime('%y%m%d%H%M%S')
timestamp.succ! while File.exist? storage_base_path.join("#{timestamp}_#{dmsf_file.id}_#{filename}")
"#{timestamp}_#{dmsf_file.id}_#{filename}"
end
def detect_content_type
content_type = mime_type
content_type = Redmine::MimeType.of(disk_filename) if content_type.blank?
content_type = 'application/octet-stream' if content_type.blank?
content_type
end
def clone
new_revision = DmsfFileRevision.new
new_revision.dmsf_file = dmsf_file
new_revision.disk_filename = disk_filename
new_revision.size = size
new_revision.mime_type = mime_type
new_revision.title = title
new_revision.description = description
new_revision.workflow = workflow
@ -221,7 +200,6 @@ class DmsfFileRevision < ApplicationRecord
new_revision.source_revision = self
new_revision.user = User.current
new_revision.name = name
new_revision.digest = digest
new_revision
end
@ -300,19 +278,7 @@ class DmsfFileRevision < ApplicationRecord
end
def copy_file_content(open_file)
sha = Digest::SHA256.new
File.open(disk_file(search_if_not_exists: false), 'wb') do |f|
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
shared_file.attach io: open_file, filename: dmsf_file.name
end
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
@ -350,20 +316,6 @@ class DmsfFileRevision < ApplicationRecord
format2
end
def create_digest
self.digest = Digest::SHA256.file(path).hexdigest
rescue StandardError => e
Rails.logger.error e.message
self.digest = 0
end
# Returns either MD5 or SHA256 depending on the way self.digest was computed
def digest_type
return nil if digest.blank?
digest.size < 64 ? 'MD5' : 'SHA256'
end
def tooltip
text = description.presence || +''
if comment.present?
@ -395,19 +347,32 @@ class DmsfFileRevision < ApplicationRecord
end
def protocol
@protocol ||= PROTOCOLS[mime_type.downcase] if mime_type
@protocol ||= PROTOCOLS[content_type.downcase] if content_type.present?
@protocol
end
def delete_source_revision
derived_revisions = []
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.save!
end
return unless RedmineDmsf.physical_file_delete?
return unless shared_file.attached?
dependencies = DmsfFileRevision.where(disk_filename: disk_filename).all.size
FileUtils.rm_f(disk_file) if dependencies <= 1
if derived_revisions.empty?
# Remove the file from Xapian index
RemoveFromIndexJob.schedule file.blob.key if RedmineDmsf.xapian_available
# 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
def copy_custom_field_values(values, source_revision = nil)

View File

@ -28,9 +28,9 @@ class DmsfFolder < ApplicationRecord
belongs_to :deleted_by_user, class_name: 'User'
belongs_to :user
has_many :dmsf_folders, -> { order :title }, dependent: :destroy, inverse_of: :dmsf_folder
has_many :dmsf_folders, dependent: :destroy, inverse_of: :dmsf_folder
has_many :dmsf_files, dependent: :destroy
has_many :folder_links, -> { where(target_type: 'DmsfFolder').order(:name) },
has_many :folder_links, -> { where(target_type: 'DmsfFolder') },
class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder
has_many :file_links, -> { where(target_type: 'DmsfFile') },
class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder
@ -91,7 +91,7 @@ class DmsfFolder < ApplicationRecord
datetime: proc { |o| o.updated_at },
author: proc { |o| o.user }
validates :title, presence: true, dmsf_file_name: true
validates :title, presence: true, length: { maximum: 255 }, dmsf_file_name: true
validates :title, uniqueness: { scope: %i[dmsf_folder_id project_id deleted],
conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true }
validates :description, length: { maximum: 65_535 }

View File

@ -26,7 +26,9 @@ class DmsfLink < ApplicationRecord
belongs_to :deleted_by_user, class_name: 'User'
belongs_to :user
validates :name, presence: true, length: { maximum: 255 }
validates :name, presence: true, length: { maximum: 255 }, dmsf_file_name: true
validates :name, uniqueness: { scope: %i[dmsf_folder_id project_id deleted],
conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true }
# There can be project_id = -1 when attaching links to an issue. The project_id is assigned later when saving the
# issue.
validates :external_url, length: { maximum: 255 }

View File

@ -20,7 +20,7 @@
# Upload
class DmsfUpload
attr_accessor :name, :disk_filename, :mime_type, :title, :description, :comment, :major_version, :minor_version,
:patch_version, :locked, :workflow, :custom_values, :tempfile_path, :digest, :token
:patch_version, :locked, :workflow, :custom_values, :tempfile_path, :token
attr_reader :size
def disk_file
@ -32,12 +32,11 @@ class DmsfUpload
if a
uploaded = {
disk_filename: DmsfHelper.temp_filename(a.filename),
content_type: a.content_type,
content_type: a.content_type.presence || 'application/octet-stream',
original_filename: a.filename,
comment: uploaded_file[:description],
tempfile_path: a.diskfile,
token: uploaded_file[:token],
digest: a.digest
token: uploaded_file[:token]
}
DmsfUpload.new project, folder, uploaded
else
@ -54,7 +53,6 @@ class DmsfUpload
@size = 0
@tempfile_path = ''
@token = ''
@digest = ''
if RedmineDmsf.empty_minor_version_by_default?
@major_version = 1
@minor_version = nil
@ -86,10 +84,9 @@ class DmsfUpload
end
@tempfile_path = uploaded[:tempfile_path]
@token = uploaded[:token]
@digest = uploaded[:digest]
if file.nil? || file.last_revision.nil?
@title = DmsfFileRevision.filename_to_title(@name)
@title = File.basename(@name, '.*')
@description = uploaded[:comment]
if RedmineDmsf.empty_minor_version_by_default?
@major_version = 1

View File

@ -22,11 +22,10 @@ class DmsfFileExtensionValidator < ActiveModel::EachValidator
include Redmine::I18n
def validate_each(record, attribute, value)
return unless attribute.to_s == 'name'
extension = File.extname(value)
return if Attachment.valid_extension?(extension)
record.errors.add(:base, l(:error_attachment_extension_not_allowed, extension: extension))
record.errors.add attribute, l(:error_attachment_extension_not_allowed, extension: extension)
end
end

View File

@ -22,6 +22,7 @@ class DmsfFileNameValidator < ActiveModel::EachValidator
ALL_INVALID_CHARACTERS = /\A[^#{DmsfFolder::INVALID_CHARACTERS}]*\z/
def validate_each(record, attribute, value)
# Check invalid characters
record.errors.add attribute, :error_contains_invalid_character unless ALL_INVALID_CHARACTERS.match?(value)
end
end

View File

@ -0,0 +1,35 @@
# 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/>.
# File name validator
class DmsfFileRevisionNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# Check name/title uniqueness
DmsfFile
.visible
.where(project_id: record.dmsf_file.project_id, dmsf_folder_id: record.dmsf_file.dmsf_folder_id)
.where.not(id: record.dmsf_file_id)
.find_each do |dmsf_file|
if dmsf_file.name == value
record.errors.add attribute, :taken
break
end
end
end
end

View File

@ -23,12 +23,11 @@
<% unless @locked || @system_folder %>
<% if @file_manipulation_allowed %>
<%= link_to sprite_icon('add', l(:label_document_new)),
multi_dmsf_upload_path(id: @project, folder_id: @folder), class: 'icon icon-add',
data: { cy: 'button__new-file--dmsf' } %>
multi_dmsf_upload_path(id: @project, folder_id: @folder), class: 'icon icon-add' %>
<% end %>
<% if @folder_manipulation_allowed %>
<%= link_to sprite_icon('add', l(:link_create_folder)), new_dmsf_path(id: @project, parent_id: @folder),
class: 'icon icon-add', data: { cy: 'button__create-folder--dmsf' } %>
class: 'icon icon-add' %>
<% end %>
<% end %>
<%= actions_dropdown do %>

View File

@ -46,11 +46,11 @@
</p>
</div>
<p>
<%= submit_tag l(:button_copy), id: 'copy_button', data: { cy: "button__copy--dmsf" } %>
<%= submit_tag l(:button_copy), id: 'copy_button' %>
<%# TODO: Lock and proper permissions %>
<% if User.current.allowed_to?(:folder_manipulation, @project) &&
User.current.allowed_to?(:file_manipulation, @project) %>
<%= submit_tag l(:button_move), id: 'move_button', data: { cy: "button__move--dmsf" } %>
<%= submit_tag l(:button_move), id: 'move_button' %>
<% end %>
</p>
<% end %>

View File

@ -120,8 +120,7 @@
<% end %>
</div>
<div class="form-actions">
<%= submit_tag create ? l(:button_create) : l(:submit_save), class: 'button-positive',
data: { cy: "button__submit--dmsf_folder" } %>
<%= submit_tag create ? l(:button_create) : l(:submit_save), class: 'button-positive' %>
</div>
<% end %>

View File

@ -19,32 +19,28 @@
<li>
<%= context_menu_link sprite_icon('edit', l(:button_edit)), dmsf_file_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-edit', data: { cy: "icon__edit--dmsf_file_#{dmsf_file.id}" },
disabled: !allowed || (locked && !unlockable) %>
class: 'icon icon-edit', disabled: !allowed || (locked && !unlockable) %>
</li>
<% unless dmsf_link %>
<li>
<%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"),
copymove_entries_path(id: project, folder_id: folder, ids: ["file-#{dmsf_file.id}"],
back_url: back_url), title: l(:title_copy), class: 'icon icon-copy',
data: { cy: "icon__copy--dmsf_file_#{dmsf_file.id}" } %>
back_url: back_url), title: l(:title_copy), class: 'icon icon-copy' %>
</li>
<li>
<%= link_to sprite_icon('link', l(:label_link_to)),
new_dmsf_link_path(project_id: dmsf_file.project.id, dmsf_folder_id: dmsf_file.dmsf_folder,
dmsf_file_id: dmsf_file.id, type: 'link_to', back_url: back_url),
title: l(:title_create_link), class: 'icon dmsf-icon-link',
data: { cy: "icon__link_to--dmsf_file_#{dmsf_file.id}" } %>
title: l(:title_create_link), class: 'icon dmsf-icon-link' %>
</li>
<% end %>
<li>
<% if locked %>
<%= context_menu_link sprite_icon('unlock', l(:button_unlock)), unlock_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-unlock', data: { cy: "icon__unlock--dmsf_file_#{dmsf_file.id}" },
title: l(:title_locked_by_user, user: dmsf_file.locked_by), disabled: !unlockable %>
class: 'icon icon-unlock', title: l(:title_locked_by_user, user: dmsf_file.locked_by), disabled: !unlockable %>
<% else %>
<%= context_menu_link sprite_icon('lock', l(:button_lock)), lock_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-lock', data: { cy: "icon__lock--dmsf_file_#{dmsf_file.id}" }, disabled: !allowed %>
class: 'icon icon-lock', disabled: !allowed %>
<% end %>
</li>
<% if notifications %>
@ -52,14 +48,11 @@
<% if dmsf_file.notification %>
<%= context_menu_link sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-email', data: { cy: "icon__email--dmsf_file_#{dmsf_file.id}" },
disabled: !allowed || locked %>
class: 'icon icon-email', disabled: !allowed || locked %>
<% else %>
<%= context_menu_link sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-email-add',
data: { cy: "icon__email_add--dmsf_file_#{dmsf_file.id}" },
disabled: !allowed || locked %>
class: 'icon icon-email-add', disabled: !allowed || locked %>
<% end %>
</li>
<% end %>
@ -71,16 +64,14 @@
<% member = Member.find_by(user_id: User.current.id, project_id: dmsf_file.project.id) %>
<% filename = dmsf_file.last_revision&.formatted_name(member) %>
<%= context_menu_link sprite_icon('download', l(:button_download)),
static_dmsf_file_path(dmsf_file, filename: filename),
class: 'icon icon-download', data: { cy: "icon__download--dmsf_file_#{dmsf_file.id}" },
disabled: false %>
static_dmsf_file_path(dmsf_file, filename: filename, download: dmsf_file.last_revision&.id),
class: 'icon icon-download', disabled: false %>
</li>
<li>
<%= context_menu_link sprite_icon('email', l(:field_mail)),
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
email_entries: true, back_url: back_url),
method: :post, class: 'icon icon-email',
data: { cy: "icon__email--dmsf_file_#{dmsf_file.id}" }, disabled: !email_allowed %>
method: :post, class: 'icon icon-email', disabled: !email_allowed %>
</li>
<% if RedmineDmsf.dmsf_webdav? %>
<li>

View File

@ -21,23 +21,19 @@
<li>
<%= context_menu_link sprite_icon('edit', l(:button_edit)),
edit_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url),
class: 'icon icon-edit', data: { cy: "icon__edit--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked %>
class: 'icon icon-edit', disabled: !allowed || locked %>
</li>
<% end %>
<% unless dmsf_link %>
<li>
<%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"),
copymove_entries_path(id: project, folder_id: folder, ids: ["folder-#{dmsf_folder.id}"],
back_url: back_url), class: 'icon icon-copy',
data: { cy: "icon__copy--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked %>
back_url: back_url), class: 'icon icon-copy', disabled: !allowed || locked %>
</li>
<li>
<%= context_menu_link sprite_icon('link', l(:label_link_to)),
new_dmsf_link_path(project_id: project.id, dmsf_folder_id: dmsf_folder.id, type: 'link_to',
back_url: back_url), class: 'icon dmsf-icon-link',
data: { cy: "icon__link_to--dmsf_folder_#{dmsf_folder.id}" } %>
back_url: back_url), class: 'icon dmsf-icon-link' %>
</li>
<% end %>
<% unless edit %>
@ -46,13 +42,11 @@
<%= context_menu_link sprite_icon('unlock', l(:button_unlock)),
unlock_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url),
title: l(:title_locked_by_user, user: dmsf_folder.locked_by), class: 'icon icon-unlock',
data: { cy: "icon__unlock--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || !unlockable %>
<% else %>
<%= context_menu_link sprite_icon('lock', l(:button_lock)),
lock_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url),
class: 'icon icon-lock', data: { cy: "icon__lock--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed %>
class: 'icon icon-lock', disabled: !allowed %>
<% end %>
</li>
<% end %>
@ -62,13 +56,11 @@
<%= context_menu_link sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder,
back_url: back_url), class: 'icon icon-email',
data: { cy: "icon__email--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked || !dmsf_folder.notification? %>
<% else %>
<%= context_menu_link sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder,
back_url: back_url), class: 'icon icon-email-add',
data: { cy: "icon__email_add--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked || dmsf_folder.notification? %>
<% end %>
</li>
@ -79,7 +71,6 @@
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
download_entries: true, back_url: back_url),
method: :post, class: 'icon icon-download',
data: { cy: "icon__download--dmsf_folder_#{dmsf_folder.id}" },
id: 'dmsf-cm-download', disabled: false %>
</li>
<li>
@ -87,7 +78,6 @@
entries_operations_dmsf_path(id: dmsf_folder.project, folder_id: folder, ids: params[:ids],
email_entries: true, back_url: back_url),
method: :post, class: 'icon icon-email',
data: { cy: "icon__email--dmsf_folder_#{dmsf_folder.id}" },
disabled: !email_allowed %>
</li>
<% end %>

View File

@ -22,14 +22,12 @@
<%= link_to sprite_icon('edit', l(:button_edit)),
edit_root_dmsf_path(id: project),
title: l(:link_edit, title: l(:link_documents)),
class: 'icon icon-edit',
data: { cy: 'button__edit--dmsf' } %>
class: 'icon icon-edit' %>
<% elsif !locked %>
<%= link_to sprite_icon('edit', l(:button_edit)),
edit_dmsf_path(id: project, folder_id: folder, redirect_to_folder_id: folder.id),
title: l(:link_edit, title: h(folder.title)),
class: 'icon icon-edit',
data: { cy: 'button__edit--dmsf' } %>
class: 'icon icon-edit' %>
<% end %>
<% if folder && (!locked || User.current.allowed_to?(:force_file_unlock, project)) %>
<% if folder.locked? %>
@ -37,13 +35,11 @@
sprite_icon('unlock', l(:button_unlock)),
unlock_dmsf_path(id: project, folder_id: folder, current: request.url),
title: l(:title_unlock_folder),
class: 'icon icon-unlock',
data: { cy: 'button__unlock--dmsf' } %>
class: 'icon icon-unlock' %>
<% else %>
<%= link_to sprite_icon('lock', l(:button_lock)),
lock_dmsf_path(id: project, folder_id: folder, current: request.url),
title: l(:title_lock_folder), class: 'icon icon-lock',
data: { cy: 'button__lock--dmsf' } %>
title: l(:title_lock_folder), class: 'icon icon-lock' %>
<% end %>
<% end %>
<% if notifications && !locked %>
@ -51,14 +47,12 @@
<%= link_to sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_path(id: project, folder_id: folder),
title: l(:title_notifications_active_deactivate),
class: 'icon icon-email',
data: { cy: 'button__notifications-off--dmsf' } %>
class: 'icon icon-email' %>
<% else %>
<%= link_to sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_path(id: project, folder_id: folder),
title: l(:title_notifications_not_active_activate),
class: 'icon icon-email-add',
data: { cy: 'button__notifications-on--dmsf' } %>
class: 'icon icon-email-add' %>
<% end %>
<% end %>
<% if file_manipulation_allowed && !locked %>
@ -66,8 +60,7 @@
new_dmsf_link_path(project_id: project.id, dmsf_folder_id: folder ? folder.id : folder,
type: 'link_from'),
title: l(:title_create_link),
class: 'icon dmsf-icon-link',
data: { cy: 'button__create-link--dmsf' } %>
class: 'icon dmsf-icon-link' %>
<% end %>
<% end %>
<%= render partial: 'dmsf_context_menus/watch', locals: { object: folder ? folder : project } %>
@ -75,8 +68,7 @@
<%= link_to sprite_icon('del', l(:link_trash_bin)),
trash_dmsf_path(project),
title: l(:link_trash_bin),
class: 'icon icon-del',
data: { cy: 'button__trash--dmsf' } %>
class: 'icon icon-del' %>
<% else %>
<span class="icon icon-del">
<%= sprite_icon('del', l(:link_trash_bin)) %>

View File

@ -40,7 +40,8 @@
<% member = Member.find_by(user_id: User.current.id, project_id: file.project.id) %>
<% filename = file.last_revision&.formatted_name(member) %>
<%= link_to sprite_icon('download', l(:button_download)),
static_dmsf_file_path(file, filename: filename), class: 'icon icon-download', disabled: false %>
static_dmsf_file_path(file, filename: filename, download: file.last_revision&.id),
class: 'icon icon-download', disabled: false %>
<%= render partial: 'dmsf_context_menus/watch', locals: { object: file } %>
<%= delete_link(dmsf_file_path(id: file, details: true),
back_url: dmsf_folder_path(id: file.project, folder_id: file.dmsf_folder)) if file_delete_allowed %>

View File

@ -74,7 +74,7 @@
<%= f.text_area :comment, rows: 2, label: l(:label_comment), class: 'wiki-edit dmsf-description' %>
</p>
<div class="form-actions">
<%= f.submit l(:button_create), class: 'button-positive', data: { cy: "button__submit--file_dmsf"} %>
<%= f.submit l(:button_create), class: 'button-positive' %>
</div>
<% end %>
<% end %>

View File

@ -27,7 +27,7 @@
target: '_blank',
rel: 'noopener',
title: h(dmsf_file.last_revision.try(:tooltip)),
'data-downloadurl' => "#{dmsf_file.last_revision.detect_content_type}:#{h(dmsf_file.name)}:#{file_view_url}" %>
'data-downloadurl' => "#{dmsf_file.last_revision.content_type}:#{h(dmsf_file.name)}:#{file_view_url}" %>
</td>
<td class="<%= cls %>">
<span class="size">(<%= number_to_human_size dmsf_file.last_revision.size %>)</span>

View File

@ -25,12 +25,20 @@
<div class="thumbnails">
<% end %>
<% images.each do |file| %>
<div>
<div class="thumbnail" title="<%= file.name %>">
<% if link_to # Redmine classic %>
<%= link_to image_tag(dmsf_thumbnail_path(file), alt: file.title), view_dmsf_file_url(file) %>
<% size = Setting.thumbnails_size.to_i %>
<%= link_to image_tag(file.last_revision&.file&.variant(resize_to_limit: [size, size]),
alt: file.title,
style: "max-width: #{size}px; max-height: #{size}px;",
loading: 'lazy'),
view_dmsf_file_url(file) %>
<% else # jQuery gallery %>
<%= image_tag(dmsf_thumbnail_path(file),
{ :'data-fullsrc' => view_dmsf_file_url(file), alt: file.title }) %>
<%= image_tag(file.last_revision&.file&.variant(resize_to_limit: [size, size]),
:'data-fullsrc' => view_dmsf_file_url(file),
alt: file.title,
style: "max-width: #{size}px; max-height: #{size}px;",
loading: 'lazy') %>
<% end %>
</div>
<% end %>

View File

@ -14,7 +14,7 @@ api.dmsf_file do
api.dmsf_string "{{dmsf(#{@file.id},#{@file.name},#{r.id})}}"
api.content_url view_dmsf_file_url(@file, download: r)
api.size r.size
api.mime_type r.mime_type
api.mime_type r.content_type
api.title r.title
api.description r.description
api.workflow r.workflow
@ -35,7 +35,7 @@ api.dmsf_file do
api.dmsf_workflow_started_by_user_id r.dmsf_workflow_started_by_user_id
api.dmsf_workflow_started_at r.dmsf_workflow_started_at
api.dmsf_worklfow_state r.workflow_str(false)
api.digest r.digest
api.digest r.checksum
render_api_custom_values r.visible_custom_field_values, api
end
end

View File

@ -133,12 +133,12 @@
</div>
<div class="status attribute">
<%= content_tag :div, l(:label_mime), class: 'label' %>
<%= content_tag :div, revision.mime_type, class: 'value' %>
<%= content_tag :div, revision.content_type, class: 'value' %>
</div>
<% if revision.digest.present? %>
<% if revision.checksum.present? %>
<div class="status attribute">
<%= content_tag :div, l(:field_digest), class: 'label' %>
<%= content_tag :div, "#{revision.digest_type}: #{revision.digest}", class: 'value wiki' %>
<%= content_tag :div, revision.checksum, class: 'value wiki' %>
</div>
<% end %>
<%= render 'dmsf/custom_fields', object: revision %>

View File

@ -20,7 +20,6 @@
<div class="box tabular dmfs-box-tabular">
<%= hidden_field_tag "committed_files[#{i}][disk_filename]", upload.disk_filename %>
<%= hidden_field_tag "committed_files[#{i}][token]", upload.token %>
<%= hidden_field_tag "committed_files[#{i}][digest]", upload.digest %>
<%= hidden_field_tag "committed_files[#{i}][size]", upload.size %>
<div class="splitcontent">
<div class="splitcontentleft">

View File

@ -31,9 +31,8 @@
locals: { multiple: true, container: nil, description: true, awf: false } %>
</span>
<div class="form-actions">
<%= submit_tag l(:label_upload), data: { cy: 'button__submit--dmsf-upload--project' }, class: 'button-positive',
<%= submit_tag l(:label_upload), class: 'button-positive',
id: "dmsf-upload-button" %>
<%= submit_tag l(:label_dmsf_upload_commit), data: { cy: 'button__submit--dmsf-upload-commit--project' },
class: 'button-positive' %>
<%= submit_tag l(:label_dmsf_upload_commit), class: 'button-positive' %>
</div>
<% end %>

View File

@ -22,6 +22,5 @@
"content_type": "<%= @tempfile.content_type.gsub('"', '').html_safe %>",
"disk_filename": "<%= @disk_filename.html_safe %>",
"tempfile_path": "<%= @tempfile_path.html_safe %>",
"digest": "<%= @digest.html_safe %>",
"token": "<%= @token.html_safe %>"
}

View File

@ -41,7 +41,6 @@
<% end %>
<div class="form-actions">
<%= submit_tag l(:label_dmsf_commit),
data: { cy: 'button__submit__commit-file--project' },
class: 'button-positive',
onclick: "$('#ajax-indicator').show();" %>
</div>

View File

@ -86,7 +86,7 @@
<td><%= link_to_user User.find_by(id: row['author_id'].present? ? row['author_id'] : row['user_id']) %></td>
<td><%= DmsfWorkflowStepAction.action_str(row['action']) %></td>
<td>
<% if (row['step'].to_i == @dmsf_workflow.dmsf_workflow_steps.last.step) && (revision.workflow == DmsfWorkflow::STATE_APPROVED) && (row['action'] != DmsfWorkflowStepAction::ACTION_DELEGATE) %>
<% if (row['step'].to_i == @dmsf_workflow.dmsf_workflow_steps.last&.step) && (revision.workflow == DmsfWorkflow::STATE_APPROVED) && (row['action'] != DmsfWorkflowStepAction::ACTION_DELEGATE) %>
<%= l(:title_approved) if row['created_at'].present? %>
<% else %>
<%= DmsfWorkflowStepAction.workflow_str(row['action']) %>

View File

@ -23,12 +23,12 @@
<% @path = settings_project_path(@project, tab: 'dmsf_workflow') %>
<p>
<%= link_to sprite_icon('add', l(:label_dmsf_workflow_new)), new_dmsf_workflow_path(project_id: @project&.id),
class: 'icon icon-add', data: { cy: "button__new--dmsf-workflow" } %>
class: 'icon icon-add' %>
</p>
<% else %>
<div class="contextual">
<%= link_to sprite_icon('add', l(:label_dmsf_workflow_new)), new_dmsf_workflow_path(project_id: @project&.id),
class: 'icon icon-add', data: { cy: "button__new--dmsf-workflow" } %>
class: 'icon icon-add' %>
</div>
<h2><%= l(:label_dmsf_workflow_plural) %></h2>
<% end %>

View File

@ -19,7 +19,8 @@
<div class="contextual">
<%= link_to "#{l(:button_download)} (#{number_to_human_size(@file.size)})",
static_dmsf_file_path(@file, download: @file.last_revision, filename: @file.last_revision.disk_filename),
static_dmsf_file_path(@file, download: @file.last_revision,
filename: @file.last_revision.file&.blob&.filename),
class: 'icon icon-download', disabled: false %>
</div>

View File

@ -328,7 +328,7 @@
<%= l(:label_full_text) %>
</em>
<% if RedmineDmsf::Plugin.lib_available?('xapian') %>
<% if RedmineDmsf.xapian_available %>
<p>
<%= content_tag :label, l(:label_index_database) %>
<%= text_field_tag 'settings[dmsf_index_database]', RedmineDmsf.dmsf_index_database, size: 50 %>
@ -373,6 +373,14 @@
<%= l(:label_default)%>: <%= l(:general_text_No) %>
</em>
</p>
<p>
<%= content_tag :label, l(:label_maximum_xapian_filesize) %>
<%= text_field_tag 'settings[dmsf_max_xapian_filesize]', RedmineDmsf.dmsf_max_xapian_filesize, size: 10 %>
<em class="info">
<%= l(:note_maximum_xapian_filesize) %><br>
<%= l(:label_default) %>: 3
</em>
</p>
<% else %>
<p class="warning"><%= l(:warning_xapian_not_available) %></p>
<% end %>

View File

@ -70,7 +70,7 @@ cs:
link_modified: Změněno
link_ver: Ver.
link_author: Autor
title_check_for_zip_download_or_email: Vybrat pro stažení jako zip nebo emailem
title_check_for_zip_download_or_email: Vybrat pro stažení jako zip nebo e-mailem
title_check_for_restore_or_delete: Vybrat pro obnovení nebo smazání
title_notifications_active_deactivate: "Notifikace aktivní: Deaktivovat"
@ -82,9 +82,9 @@ cs:
title_unlock_file: Odemknout a umožnit změny ostatním uživatelům
title_lock_file: Zamknout a zabránit změnám ostatních uživatelů
title_download_checked: Stáhnout vybrané jako Zip
title_send_checked_by_email: Zaslat vybrané emailem
title_send_checked_by_email: Zaslat vybrané e-mailem
link_user_preferences: Vaše nastavení
heading_send_documents_by_email: Odeslat dokumenty emailem
heading_send_documents_by_email: Odeslat dokumenty e-mailem
label_email_from: Od
label_email_to: Komu
label_email_cc: Kopie
@ -125,7 +125,7 @@ cs:
option_version_custom: Vlastní
label_new_content: Nový obsah
label_maximum_files_download: Maximální počet najednou stažených souborů
note_maximum_number_of_files_downloaded: Maximální počet najednou stažených souborů jako Zip nebo odeslaných emailem.
note_maximum_number_of_files_downloaded: Maximální počet najednou stažených souborů jako Zip nebo odeslaných e-mailem.
0 znamená bez omezení.
label_file_storage_directory: Složka pro uložení souborů
label_index_database: Index databáze
@ -144,8 +144,8 @@ cs:
label_default_notifications: Výchozí notifikace pro soubory
heading_uploaded_files: Nahrané soubory
link_documents: Dokumenty
permission_view_dmsf_file_revision_accesses: View downloads in Activity stream
permission_view_dmsf_file_revisions: View revisions in Activity stream
permission_view_dmsf_file_revision_accesses: Zobrazit stažení v aktivitách
permission_view_dmsf_file_revisions: Zobrazit revize v aktivitách
permission_view_dmsf_folders: Procházet dokumenty
permission_user_preferences: Nastavení uživatele
permission_view_dmsf_files: Zobrazit dokumenty
@ -155,8 +155,8 @@ cs:
permission_manage_workflows: Spravovat schvalovací procesy
permission_file_delete: Mazat dokumenty
permission_display_system_folders: Zobrazit systémové složky
permission_file_approval: File approval
permission_email_documents: Email documents
permission_file_approval: Schvalovat dokumenty
permission_email_documents: Posílat dokumenty e-mailem
label_file: Soubor
field_folder: Složka
error_file_commit_require_uploaded_file: Potvrzení vyžaduje nahraný soubor
@ -182,7 +182,7 @@ cs:
menu_dmsf: DMS # Project tab title
label_physical_file_delete: Fyzické smazání souboru
user_is_not_project_member: Nejste členem projektu
heading_access_downloads_emails: Stažené/Emaily
heading_access_downloads_emails: Stažené/E-maily
heading_access_first: První
heading_access_last: Poslední
label_dmsf_updated: Změněno
@ -199,11 +199,11 @@ cs:
error_target_folder_same: Cílový složka a projekt jsou stejné jako aktuální
title_copy: Kopírovat
error_max_email_filesize_exceeded: "Přesáhli jste maximální velikost souboru, který lze poslat emailem.
error_max_email_filesize_exceeded: "Přesáhli jste maximální velikost souboru, který lze poslat e-mailem.
(%{number} MB)"
note_maximum_email_filesize: Omezí se maximální velikost souboru, který může být poslán emailem. 0 znamená neomezený.
note_maximum_email_filesize: Omezí se maximální velikost souboru, který může být poslán e-mailem. 0 znamená neomezený.
Číslo je v MB.
label_maximum_email_filesize: Maximální velikost souboru emailu
label_maximum_email_filesize: Maximální velikost souboru e-mailu
header_minimum_filesize: Chyba souboru.
error_minimum_filesize: "Soubor %{file} má nulovou velikost a nebude přiložen."
parent_directory: Nadřazená složka
@ -317,9 +317,9 @@ cs:
label_links_only: pouze odkazy
label_display_notified_recipients: Zobrazit příjemce notifikací
note_display_notified_recipients: Uživatel bude informován o příjemcích právě odeslané emailové notifikace.
note_display_notified_recipients: Uživatel bude informován o příjemcích právě odeslané e-mailové notifikace.
warning_email_notifications: "Notifikační email poslán na uživatele %{to}"
warning_email_notifications: "Notifikační e-mail poslán na uživatele %{to}"
link_trash_bin: Koš
title_restore: Obnovit
@ -384,8 +384,7 @@ cs:
label_enable_cjk_ngrams: Povolit generování n-gramů pro CJK texty
text_enable_cjk_ngrams: "Pokud je povoleno, sekvence čínských nebo japonských znaků jsou rozděleny do jednotlivých
znaků nebo skupin znaků. Znaky si nesou informaci o své pozici. Znaky psané latinkou jsou rozděleny normálně do
slov. Odpovídající proměná prostředí musí být použita při indexaci.
např.: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
slov."
label_dmsf_fast_links: Rychlé odkazy
text_dmsf_fast_links_info: Při vytváření odkazů budete moci zadat přímo ID cílové složky za účelem zrychlení
@ -493,6 +492,10 @@ cs:
warning_folder_unlockable: Složku nelze odemknout
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximální velikost souboru pro indexaci
note_maximum_xapian_filesize: Omezuje maximální velikost souboru pro indexaci. Větší soubory nebudou indexovány.
Velikost je v MB.
activerecord:
errors:
messages:

View File

@ -378,9 +378,7 @@ de:
label_enable_cjk_ngrams: Aktiviere die Erstellung von n-grams aus Koreanischen Texten
text_enable_cjk_ngrams: "Mit dieser Aktivierung werden Koreanische Zeichenfolgen in Monograms and Bigrams zerlegt.
Monograms enthalten Informationen zur Position. Nicht-Koreanische Zeichenfolgen werden in Wörter zerlegt. Die entsprechende
Option muss beim Indexieren verwendet werden,
z.B. XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
Monograms enthalten Informationen zur Position. Nicht-Koreanische Zeichenfolgen werden in Wörter zerlegt."
label_dmsf_fast_links: Schnelle Verknüpfung
text_dmsf_fast_links_info: Ermöglicht durch Eingabe der Ordner-ID auf einfache Art und Weise eine Verknüpfung
@ -489,6 +487,10 @@ de:
warning_folder_unlockable: Der Ordner kann nicht entsperrt werden
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximale Dateigröße für den Index
note_maximum_xapian_filesize: Begrenzt die maximale Dateigröße für die Indizierung. Größere Dateien werden nicht
indiziert. Angabe in MB.
activerecord:
errors:
messages:

View File

@ -382,9 +382,7 @@ en:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal."
label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ en:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -382,9 +382,7 @@ es:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal."
label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ es:
label_dmsf_commit: Commit
label_dmsf_upload_commit: Upload and commit
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -471,6 +471,9 @@ fa:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -382,9 +382,7 @@ fr:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal"
label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ fr:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -381,9 +381,7 @@ hu:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal."
label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -491,6 +489,9 @@ hu:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -382,9 +382,7 @@ it: # Italian strings thx 2 Matteo Arceci!
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal."
label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ it: # Italian strings thx 2 Matteo Arceci!
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -382,9 +382,7 @@ ja:
label_enable_cjk_ngrams: CJKテキストから n-grams の生成を有効にする
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal."
label_dmsf_fast_links: 高速リンク
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -493,6 +491,9 @@ ja:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -381,10 +381,8 @@ ko:
label_email_reply_to: 회신
label_enable_cjk_ngrams: CJK 텍스트로부터 n-gram 생성을 활성화
text_enable_cjk_ngrams: "이 기능을 사용하면 CJK 문자 범위가 유니그램과 바이그램으로 분할되고, 유니그램은 위치 정보를 전달합니다. CJK 범위 외의 문자는 일반적인 단어로 분할됩니다. 해당 옵션은 색인 시점에 사용됩니다. 예: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
text_enable_cjk_ngrams: "이 기능을 사용하면 CJK 문자 범위가 유니그램과 바이그램으로 분할되고, 유니그램은 위치 정보를 전달합니다.
CJK 범위 외의 문자는 일반적인 단어로 분할됩니다."
label_dmsf_fast_links: 빠른 링크
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ ko:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -492,6 +492,9 @@ nl:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -382,9 +382,7 @@ pl:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal."
label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ pl:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -382,9 +382,7 @@ pt-BR:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal."
label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ pt-BR:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -382,9 +382,7 @@ sl:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal."
label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ sl:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -383,10 +383,7 @@ uk:
label_enable_cjk_ngrams: Дозводити генерацію n-грам з тексту CJK
text_enable_cjk_ngrams: "Якщо ввімкнено цю функцію, діапазони символів CJK розбиваються на уніграми і біграми, при
цьому уніграми містять позиційну інформацію. Символи, відмінні від CJK, розбиваються на слова як зазвичай.
Відповідна опція повинна бути використана під час індексування
Приклад: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
цьому уніграми містять позиційну інформацію. Символи, відмінні від CJK, розбиваються на слова як зазвичай."
label_dmsf_fast_links: Швидкі посилання
text_dmsf_fast_links_info: Ви зможете вручну ввести ідентифікатор кінцевої папки під час створення посилань або
переміщення файлів чи папок, щоб пришвидшити процес створення посилань.
@ -494,6 +491,9 @@ uk:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -381,9 +381,8 @@ zh-TW:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal."
label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -491,6 +490,9 @@ zh-TW:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -382,9 +382,7 @@ zh:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
unigrams carrying positional information. Non-CJK characters are split into words as normal."
label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ zh:
warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord:
errors:
messages:

View File

@ -22,4 +22,8 @@ class AddDigestToRevision < ActiveRecord::Migration[4.2]
def up
add_column :dmsf_file_revisions, :digest, :string, limit: 40, default: '', null: false
end
def down
remove_column :dmsf_file_revisions, :digest
end
end

View File

@ -25,7 +25,7 @@ class ChangeRevisionDigestLimitTo64 < ActiveRecord::Migration[4.2]
def down
# Mysql2::Error: Data too long for column 'digest'
# Recalculation of checksums for all revisions is technically possible but costs are to high.
# Recalculation of checksums for all revisions is technically possible but costs are too high.
# change_column :dmsf_file_revisions, :digest, :string, limit: 40
end
end

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

@ -0,0 +1,158 @@
# 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/>.
# Migrate documents to/from Active Storage
class ActiveStorageMigration < ActiveRecord::Migration[7.0]
# File system -> Active Storage
def up
$stdout.puts 'It could be a very long process. Be patient...'
# We need to keep updated_at column unchanged and due to the asynchronous file analysis there is probably no better
# way how to achieve that.
add_column :dmsf_file_revisions, :temp_updated_at, :datetime,
default: nil, null: true, if_not_exists: true
DmsfFileRevision.update_all 'temp_updated_at = updated_at'
# Remove the Xapian database as it will be rebuilt from scratch during the migration
if xapian_database_removed?
$stdout.puts 'The Xapian database has been removed as it will be rebuilt from scratch during the migration'
end
Dir.glob(DmsfFile.storage_path.join('**/*')).each do |path|
# Print out the currently processed directory
unless File.file?(path)
$stdout.puts path
next
end
# Process a file
disk_filename = File.basename(path)
$stdout.print path
found = false
DmsfFileRevision.where(disk_filename: disk_filename)
.order(source_dmsf_file_revision_id: :asc)
.each
.with_index do |r, i|
found = true
if i.zero?
r.shared_file.attach io: File.open(path), filename: r.name
# Remove the original file
FileUtils.rm path
key = r.file.blob.key
$stdout.puts " => #{File.join(key[0..1], key[2..3], key)} (#{r.file.blob.filename})"
else
# The other revisions should have set the source revision
warn("r#{r.id}.source_dmsf_file_revision_id is null") unless r.source_dmsf_file_revision_id
end
end
found ? $stdout.print("\n") : $stdout.print(" revision not found, skipped\n")
end
# Remove columns duplicated in ActiveStorage
remove_column :dmsf_file_revisions, :digest
remove_column :dmsf_file_revisions, :mime_type
remove_column :dmsf_file_revisions, :disk_filename
remove_column :dmsf_files, :name
# We need to keep the size despite the fact that it's duplicated in active_storage_blobs to speed up the main
# document view
# Restore updated_at column
DmsfFileRevision.update_all 'updated_at = temp_updated_at'
remove_column :dmsf_file_revisions, :temp_updated_at
$stdout.puts 'Done'
end
# Active Storage -> File system
def down
$stdout.puts 'It could be a very long process. Be patient...'
# Restore removed columns
add_column :dmsf_file_revisions, :digest, :string, limit: 64, default: '', null: false
add_column :dmsf_file_revisions, :mime_type, :string
add_column :dmsf_file_revisions, :disk_filename, :string, default: '', null: false
add_column :dmsf_files, :name, :string, default: '', null: false
# Migrate attachments
ActiveStorage::Attachment.find_each do |a|
r = a.record
new_path = disk_file(r, a)
unless File.exist?(new_path)
a.blob.open do |f|
# Move the attachment
FileUtils.mv f.path, new_path
r.record_timestamps = false # Do not modify updated_at column
DmsfFileRevision.no_touching do
# Mime type
r.mime_type = a.blob.content_type
# Disk filename
r.disk_filename = File.basename(new_path)
# Digest
# We leave the digest calculation to dmsf_create_digests.rake task
r.save
end
r.dmsf_file.record_timestamps = false # Do not modify updated_at column
DmsfFile.no_touching do
# Filename
r.dmsf_file.name = r.dmsf_file.last_revision.name
r.dmsf_file.save
end
end
key = a.blob.key
$stdout.puts "#{File.join(key[0..1], key[2..3], key)} (#{a.blob.filename}) => #{new_path}"
end
# Remove the original file
r.record_timestamps = false # Do not modify updated_at column
DmsfFileRevision.no_touching do
a.purge
end
end
# Remove the Xapian database as it is useless now and has to be rebuilt with xapian_indexer.rb
if xapian_database_removed?
$stdout.puts 'Xapian database have been removed as it is useless now and has to be rebuilt with xapian_indexer.rb'
end
$stdout.puts 'Done'
end
private
# Delete Xapian database
def xapian_database_removed?
if RedmineDmsf.xapian_available
FileUtils.rm_rf File.join(RedmineDmsf.dmsf_index_database, RedmineDmsf.dmsf_stemming_lang)
true
else
false
end
end
def storage_base_path(rev)
time = rev.created_at || DateTime.current
DmsfFile.storage_path.join(time.strftime('%Y')).join time.strftime('%m')
end
def new_storage_filename(rev, name)
filename = DmsfHelper.sanitize_filename(name)
timestamp = DateTime.current.strftime('%y%m%d%H%M%S')
timestamp.succ! while File.exist? storage_base_path(rev).join("#{timestamp}_#{rev.dmsf_file.id}_#{filename}")
"#{timestamp}_#{rev.dmsf_file.id}_#{filename}"
end
def disk_file(rev, attachment)
path = storage_base_path(rev)
begin
FileUtils.mkdir_p path
rescue StandardError => e
Rails.logger.error e.message
end
filename = new_storage_filename(rev, attachment.blob&.filename&.to_s)
path.join(filename).to_s
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 'Vít Jonáš / Daniel Munn / Karel Pičman'
description 'Document Management System Features'
version '4.2.3'
version '5.0.0 devel'
requires_redmine version_or_higher: '6.1.0'
@ -62,7 +62,8 @@ Redmine::Plugin.register :redmine_dmsf do
'empty_minor_version_by_default' => '0',
'remove_original_documents_module' => '0',
'dmsf_webdav_authentication' => 'Digest',
'dmsf_really_delete_files' => '0'
'dmsf_really_delete_files' => '0',
'dmsf_max_xapian_filesize' => 3
}
end

View File

@ -19,6 +19,18 @@
# Main module
module RedmineDmsf
# Return true if the given gem is installed
def self.lib_available?(path)
require path
true
rescue LoadError => e
Rails.logger.debug e.message
false
end
mattr_accessor :xapian_available, instance_writer: false
@@xapian_available = RedmineDmsf.lib_available?('xapian')
# Settings
class << self
def dmsf_max_file_download
@ -38,11 +50,12 @@ module RedmineDmsf
end
def dmsf_index_database
if Setting.plugin_redmine_dmsf['dmsf_index_database'].present?
Setting.plugin_redmine_dmsf['dmsf_index_database'].strip
else
File.expand_path('dmsf_index', Rails.root)
end
dir = if Setting.plugin_redmine_dmsf['dmsf_index_database'].present?
Setting.plugin_redmine_dmsf['dmsf_index_database'].strip
else
File.expand_path('dmsf_index', Rails.root)
end
FileUtils.mkdir_p dir
end
def dmsf_stemming_lang
@ -211,6 +224,14 @@ module RedmineDmsf
value = Setting.plugin_redmine_dmsf['dmsf_default_notifications']
value.to_i.positive? || value == 'true'
end
def dmsf_max_xapian_filesize
if Setting.plugin_redmine_dmsf['dmsf_max_xapian_filesize'].present?
Setting.plugin_redmine_dmsf['dmsf_max_xapian_filesize'].to_i
else
3
end
end
end
end

View File

@ -47,7 +47,9 @@ module RedmineDmsf
end
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
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::DOSTime.at(dmsf_file.last_revision.updated_at))
@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))
@zip_file.write buffer
end
@ -71,31 +73,6 @@ module RedmineDmsf
@dmsf_files << dmsf_file
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)
string_path = dmsf_folder.dmsf_path_str + File::SEPARATOR
string_path = string_path[(root_path.length + 1)..string_path.length] if root_path

View File

@ -91,7 +91,7 @@ module RedmineDmsf
rel: 'noopener',
class: 'icon icon-file',
title: h(revision.try(:tooltip)),
'data-downloadurl' => "#{revision.detect_content_type}:#{h(revision.dmsf_file.name)}:#{file_view_url}"
'data-downloadurl' => "#{revision.content_type}:#{h(revision.dmsf_file.name)}:#{file_view_url}"
)
end
end

View File

@ -174,7 +174,6 @@ module RedmineDmsf
uploaded_file[:size] = upload.size
uploaded_file[:mime_type] = upload.mime_type
uploaded_file[:tempfile_path] = upload.tempfile_path
uploaded_file[:digest] = upload.digest
if params[:dmsf_attachments_wfs].present? && params[:dmsf_attachments_wfs][key].present?
uploaded_file[:workflow_id] = params[:dmsf_attachments_wfs][key].to_i
end

View File

@ -49,7 +49,7 @@ module RedmineDmsf
target: '_blank',
rel: 'noopener',
title: h(revision.tooltip),
'data-downloadurl' => "#{file.last_revision.detect_content_type}:#{h(file.name)}:#{url}"
'data-downloadurl' => "#{file.last_revision.content_type}:#{h(file.name)}:#{url}"
end
# dmsff - link to a folder
@ -241,16 +241,22 @@ module RedmineDmsf
{{dmsftn(file_id)}} -- with default height 200 (auto width)
{{dmsftn(file_id1 file_id2 file_id3)}} -- multiple thumbnails
{{dmsftn(file_id, size=300)}} -- with size 300x300
{{dmsftn(file_id, height=300)}} -- with height (auto width)
{{dmsftn(file_id, width=300)}} -- with width (auto height)
{{dmsftn(file_id, height=300)}} -- with height (default width)
{{dmsftn(file_id, width=300)}} -- with width (default height)
{{dmsftn(file_id, size=640x480)}} -- with size 640x480}
macro :dmsftn do |_obj, args|
raise ArgumentError if args.empty? # Requires file id
args, options = extract_macro_options(args, :size, :width, :height, :title)
size = options[:size]
width = options[:width]
height = options[:height]
if options[:size].present?
width, height = options[:size].split('x')
height = width if height.blank?
else
width = options[:width].presence || Setting.thumbnails_size.to_i
height = options[:height].presence || Setting.thumbnails_size.to_i
end
ids = args[0].split
html = []
ids.each do |id|
@ -260,25 +266,21 @@ module RedmineDmsf
next
end
raise ::I18n.t(:notice_not_authorized) unless User.current&.allowed_to?(:view_dmsf_files, file.project)
raise ::I18n.t(:error_not_supported_image_format) unless file.image?
raise ::I18n.t(:error_not_supported_image_format) unless file&.thumbnailable?
member = Member.find_by(user_id: User.current.id, project_id: file.project.id)
filename = file.last_revision.formatted_name(member)
url = static_dmsf_file_url(file, filename: filename)
img = if size
image_tag(url, alt: filename, title: file.title, size: size)
elsif height
image_tag(url, alt: filename, title: file.title, width: 'auto', height: height)
elsif width
image_tag(url, alt: filename, title: file.title, width: width, height: 'auto')
else
image_tag(url, alt: filename, title: file.title, width: 'auto', height: 200)
end
html << link_to(img, url,
img = image_tag(file.last_revision&.file&.variant(resize_to_limit: [width, height]),
alt: filename,
style: "max-width: #{width}px; max-height: #{height}px;",
loading: 'lazy')
html << link_to(img,
url,
target: '_blank',
rel: 'noopener',
title: h(file.last_revision.try(:tooltip)),
'data-downloadurl' => "#{file.last_revision.detect_content_type}:#{h(file.name)}:#{url}")
'data-downloadurl' => "#{file.last_revision.content_type}:#{h(file.name)}:#{url}")
end
safe_join html
end

View File

@ -31,7 +31,7 @@ module RedmineDmsf
def get_image_filename(attrname)
if attrname =~ %r{/dmsf/files/(\d+)/}
file = DmsfFile.find_by(id: Regexp.last_match(1))
file&.last_revision&.disk_file
file.last_revision.file&.blob&.filename if file&.last_revision
else
super
end

View File

@ -49,9 +49,9 @@ module RedmineDmsf
# New methods
def self.prepended(base)
base.class_eval do
has_many :dmsf_files, -> { where(dmsf_folder_id: nil).order(:name) },
has_many :dmsf_files, -> { where(dmsf_folder_id: nil) },
class_name: 'DmsfFile', foreign_key: 'project_id', dependent: :destroy
has_many :dmsf_folders, -> { where(dmsf_folder_id: nil).order(:title) },
has_many :dmsf_folders, -> { where(dmsf_folder_id: nil) },
class_name: 'DmsfFolder', foreign_key: 'project_id', dependent: :destroy
has_many :dmsf_workflows, dependent: :destroy
has_many :folder_links, -> { where dmsf_folder_id: nil, target_type: 'DmsfFolder' },

View File

@ -39,4 +39,4 @@ module RedmineDmsf
end
# Apply the patch
Puma::Const.include RedmineDmsf::Patches::PumaPatch if RedmineDmsf::Plugin.lib_available?('puma/const')
Puma::Const.include RedmineDmsf::Patches::PumaPatch if RedmineDmsf.lib_available?('puma/const')

View File

@ -36,14 +36,5 @@ module RedmineDmsf
end
false
end
# Return true if the given gem is installed
def self.lib_available?(path)
require path
true
rescue LoadError => e
Rails.logger.debug e.message
false
end
end
end

View File

@ -34,8 +34,8 @@ module RedmineDmsf
@office_available = $CHILD_STATUS.success?
rescue StandardError
@office_available = false
Rails.logger.warn l(:note_dmsf_office_bin_not_available, value: office_bin, locale: :en)
end
Rails.logger.warn l(:note_dmsf_office_bin_not_available, value: office_bin, locale: :en) unless @office_available
@office_available
end
@ -46,6 +46,8 @@ module RedmineDmsf
office_bin = RedmineDmsf.office_bin.presence || 'libreoffice'
cmd = "#{shell_quote(office_bin)} --convert-to pdf --headless --outdir #{shell_quote(dir)} #{shell_quote(source)}"
if system(cmd)
filename = "#{File.basename(source, '.*')}.pdf"
FileUtils.mv File.join(dir, filename), target
target
else
Rails.logger.error "Creating preview failed (#{$CHILD_STATUS}):\nCommand: #{cmd}"

View File

@ -55,7 +55,7 @@ module RedmineDmsf
end
# Gather collection of objects that denote current entities child entities
# Used for listing directories etc, implemented basic caching because otherwise
# Used for listing directories etc., implemented basic caching because otherwise
# Our already quite heavy usage of DB would just get silly every time we called
# this method.
def children
@ -63,12 +63,12 @@ module RedmineDmsf
@children = []
if folder
# Folders
folder.dmsf_folders.visible.each do |f|
@children.push child(f.title) if DmsfFolder.permissions?(f, allow_system: false)
folder.dmsf_folders.visible.each do |folder|
@children.push child(folder.title) if DmsfFolder.permissions?(folder, allow_system: false)
end
# Files
folder.dmsf_files.visible.pluck(:name).each do |name|
@children.push child(name)
folder.dmsf_files.visible.each do |file|
@children.push child(file.name)
end
end
end
@ -92,7 +92,7 @@ module RedmineDmsf
def content_type
if file
if file.last_revision
file.last_revision.detect_content_type
file.last_revision.content_type
else
'application/octet-stream'
end
@ -126,13 +126,7 @@ module RedmineDmsf
end
def etag
ino = if file&.last_revision && File.exist?(file.last_revision.disk_file)
File.stat(file.last_revision.disk_file).ino
else
2
end
format '%<node>x-%<size>x-%<modified>x',
node: ino, size: content_length, modified: (last_modified ? last_modified.to_i : 0)
format '%<node>x-%<size>x-%<modified>x', node: 0, size: content_length, modified: last_modified.to_i
end
def content_length
@ -230,8 +224,9 @@ module RedmineDmsf
dest = ResourceProxy.new(dest_path, @request, @response, @options.merge(user: @user))
return PreconditionFailed if !dest.resource.is_a?(DmsfResource) || dest.resource.project.nil?
parent = dest.resource.parent
parent = dest_path.end_with?('/') && !collection? ? dest.resource : dest.resource.parent
raise Forbidden unless dest.resource.project.module_enabled?(:dmsf)
if !parent.exist? || (!User.current.admin? && (!DmsfFolder.permissions?(folder, allow_system: false) ||
!DmsfFolder.permissions?(parent.folder, allow_system: false)))
raise Forbidden
@ -247,13 +242,11 @@ module RedmineDmsf
!User.current.allowed_to?(:folder_manipulation, dest.resource.project))
raise Forbidden
end
return MethodNotAllowed unless folder # Moving sub-project not enabled
return MethodNotAllowed unless folder # Moving subprojects is not enabled
raise Locked if folder.locked_for_user?
# Change the title
folder.title = dest.resource.basename
return PreconditionFailed unless folder.save
# Move to a new destination
folder.move_to(dest.resource.project, parent.folder) ? Created : PreconditionFailed
else
@ -272,13 +265,9 @@ module RedmineDmsf
new_revision = dest.resource.file.last_revision.clone
new_revision.increase_version DmsfFileRevision::PATCH_VERSION
end
# The file on disk must be renamed from .tmp to the correct filetype or else Xapian won't know how to index.
# Copy file.last_revision.disk_file to new_revision.disk_file
# Copy the file
new_revision.size = file.last_revision.size
new_revision.disk_filename = new_revision.new_storage_filename
File.open(file.last_revision.disk_file, 'rb') do |f|
new_revision.copy_file_content f
end
new_revision.copy_file_content StringIO.new(file.last_revision.file.download)
# Save
new_revision.save && dest.resource.file.save
# Delete (and destroy) the file that should have been renamed and return what should have been returned
@ -304,9 +293,8 @@ module RedmineDmsf
# Update Revision and names of file [We can link to old physical resource, as it's not changed]
if file.last_revision
file.last_revision.name = dest.resource.basename
file.last_revision.title = DmsfFileRevision.filename_to_title(dest.resource.basename)
file.last_revision.title = File.basename(dest.resource.basename, '.*')
end
file.name = dest.resource.basename
# Save Changes
if file.last_revision.save && file.save
dest.exist? ? NoContent : Created
@ -340,7 +328,7 @@ module RedmineDmsf
res = NoContent
end
return PreconditionFailed unless parent.exist? && parent.folder
return PreconditionFailed unless parent.exist? && (parent.folder || parent.project)
if collection?
# Permission check if they can manipulate folders and view folders
@ -348,7 +336,7 @@ module RedmineDmsf
# Manipulate folders on destination project :folder_manipulation
# View folders on destination project :view_dmsf_folders
# View files on the source project :view_dmsf_files
# View fodlers on the source project :view_dmsf_folders
# View folders on the source project :view_dmsf_folders
raise Forbidden unless User.current.admin? ||
(User.current.allowed_to?(:folder_manipulation, dest.resource.project) &&
User.current.allowed_to?(:view_dmsf_folders, dest.resource.project) &&
@ -378,13 +366,18 @@ module RedmineDmsf
# Update Revision and names of file (We can link to old physical resource, as it's not changed)
new_file.last_revision.name = dest.resource.basename
new_file.name = dest.resource.basename
new_file.last_revision.title = File.basename(dest.resource.basename, '.*')
# Save Changes
new_file.last_revision.save && new_file.save ? res : PreconditionFailed
unless new_file.last_revision.save && new_file.save
new_file.delete commit: true
return PreconditionFailed
end
res
end
end
# Lock Check
# Lock check
# Check for the existence of locks
def lock_check(args = {})
entity = file || folder
@ -584,19 +577,17 @@ module RedmineDmsf
else
f = DmsfFile.new
f.project_id = project.id
f.name = basename
f.dmsf_folder = parent.folder
f.notification = RedmineDmsf.dmsf_default_notifications?
new_revision = DmsfFileRevision.new
new_revision.minor_version = 1
new_revision.major_version = 0
new_revision.title = DmsfFileRevision.filename_to_title(basename)
new_revision.title = File.basename(basename, '.*')
end
new_revision.dmsf_file = f
new_revision.user = User.current
new_revision.name = basename
new_revision.mime_type = Redmine::MimeType.of(new_revision.name)
# Phusion passenger does not have a method "length" in its model
# however, includes a size method - so we instead use reflection
@ -625,10 +616,19 @@ module RedmineDmsf
raise UnprocessableEntity
end
new_revision.disk_filename = new_revision.new_storage_filename unless reuse_revision
if new_revision.save
new_revision.copy_file_content request.body
if request.body.respond_to?(:rewind)
request.body.rewind
new_revision.copy_file_content request.body
else # A workaround for Webrick that doesn't support rewind
stream = StringIO.new
while (buffer = request.body.read(8_192))
stream.write buffer
end
stream.rewind
new_revision.copy_file_content stream
stream.close
end
new_revision.save
# Notifications
DmsfMailer.deliver_files_updated project, [f]
@ -636,7 +636,6 @@ module RedmineDmsf
Rails.logger.error new_revision.errors.full_messages.to_sentence
raise InternalServerError
end
Created
end
@ -697,10 +696,7 @@ module RedmineDmsf
# implementation of service for request, which allows for us to pipe a single file through
# also best-utilising Dav4rack's implementation.
def download
raise NotFound unless file&.last_revision
disk_file = file.last_revision.disk_file
raise NotFound unless disk_file && File.exist?(disk_file)
raise NotFound unless file.last_revision&.file&.attached?
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
@ -719,7 +715,9 @@ module RedmineDmsf
Rails.logger.error "Could not send email notifications: #{e.message}"
end
end
File.new disk_file
file.last_revision.file.open do |f|
File.new f.path
end
end
def reuse_version_for_locked_file?(file)
@ -766,27 +764,28 @@ module RedmineDmsf
def create_empty_file
f = DmsfFile.new
f.project_id = project.id
f.name = basename
f.dmsf_folder = parent.folder
if f.save(validate: false) # Skip validation due to invalid characters in the filename
if f.save
r = DmsfFileRevision.new
r.minor_version = 1
r.major_version = 0
r.title = DmsfFileRevision.filename_to_title(basename)
r.title = File.basename(basename, '.*')
r.dmsf_file = f
r.user = User.current
r.name = basename
r.mime_type = Redmine::MimeType.of(r.name)
r.size = 0
r.digest = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
r.disk_filename = r.new_storage_filename
r.available_custom_fields.each do |cf| # Add default value for CFs not existing
next unless cf.default_value
r.custom_field_values << CustomValue.new({ custom_field: cf, value: cf.default_value })
end
if r.save(validate: false) # Skip validation due to invalid characters in the filename
FileUtils.touch r.disk_file(search_if_not_exists: false)
r.shared_file.attach(
io: File.new(DmsfHelper.temp_filename(basename), File::CREAT),
filename: basename,
content_type: 'application/octet-stream',
identify: false
)
return f
end
end

View File

@ -41,8 +41,8 @@ module RedmineDmsf
end
# Files
if User.current.allowed_to?(:view_dmsf_files, project)
project.dmsf_files.visible.pluck(:name).each do |name|
@children.push child(name)
project.dmsf_files.visible.each do |file|
@children.push child(file.name)
end
end
@children

View File

@ -0,0 +1,56 @@
# 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.xapian_available &&
blob.byte_size < 1_024 * 1_024 * RedmineDmsf.dmsf_max_xapian_filesize # MB
@blob = blob
true
end
def metadata
{ xapian: indexed? }
end
private
def indexed?
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)
env = 'XAPIAN_CJK_NGRAM=true ' if RedmineDmsf.dmsf_enable_cjk_ngrams?
FileUtils.mkdir dir
@blob.open do |file|
FileUtils.mv file.path, File.join(dir, @blob.key)
system "#{env}omindex -s \"#{stem_lang}\" -D \"#{db_path}\" --url=/#{url} \"#{dir}\" -p", exception: true
end
true
rescue StandardError => e
Rails.logger.error e.message
false
ensure
FileUtils.rm_f dir
end
end
end

View File

@ -0,0 +1,104 @@
---
active_storage_attachment_1:
id: 1
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 1
blob_id: 1
created_at: <%= Time.now %>
active_storage_attachment_2:
id: 2
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 2
blob_id: 2
created_at: <%= Time.now %>
active_storage_attachment_3:
id: 3
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 3
blob_id: 3
created_at: <%= Time.now %>
active_storage_attachment_4:
id: 4
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 4
blob_id: 4
created_at: <%= Time.now %>
active_storage_attachment_6:
id: 6
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 6
blob_id: 6
created_at: <%= Time.now %>
active_storage_attachment_7:
id: 7
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 7
blob_id: 7
created_at: <%= Time.now %>
active_storage_attachment_8:
id: 8
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 8
blob_id: 8
created_at: <%= Time.now %>
active_storage_attachment_9:
id: 9
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 9
blob_id: 9
created_at: <%= Time.now %>
active_storage_attachment_10:
id: 10
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 10
blob_id: 10
created_at: <%= Time.now %>
active_storage_attachment_11:
id: 11
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 11
blob_id: 11
created_at: <%= Time.now %>
active_storage_attachment_12:
id: 12
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 12
blob_id: 12
created_at: <%= Time.now %>
active_storage_attachment_13:
id: 13
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 13
blob_id: 13
created_at: <%= Time.now %>
active_storage_attachment_14:
id: 14
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 14
blob_id: 14
created_at: <%= Time.now %>

143
test/fixtures/active_storage_blobs.yml vendored Normal file
View File

@ -0,0 +1,143 @@
---
active_storage_blob_1:
id: 1
key: '5lge4yv88jwzt7xl76vri2be1v01'
filename: 'test.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_2:
id: 2
key: '5lge4yv88jwzt7xl76vri2be1v02'
filename: 'test2.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_3:
id: 3
key: '5lge4yv88jwzt7xl76vri2be1v03'
filename: 'deleted.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_4:
id: 4
key: '5lge4yv88jwzt7xl76vri2be1v04'
filename: 'test4.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_6:
id: 6
key: '5lge4yv88jwzt7xl76vri2be1v06'
filename: 'test.gif'
content_type: 'image/gif'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 310
checksum : 'iuUAMbuGLEpp8rq1zR8gUQ=='
created_at: <%= Time.now %>
active_storage_blob_7:
id: 7
key: '5lge4yv88jwzt7xl76vri2be1v07'
filename: 'test.pdf'
content_type: 'application/pdf'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 6942
checksum : 'U3aozufhXIqhAj0n8yCXIA=='
created_at: <%= Time.now %>
active_storage_blob_8: # File is not physically present
id: 8
key: '5lge4yv88jwzt7xl76vri2be1v08'
filename: 'myfile.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_9:
id: 9
key: '5lge4yv88jwzt7xl76vri2be1v09'
filename: 'zero.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 0
checksum : '1B2M2Y8AsgTpgAmY7PhCfg=='
created_at: <%= Time.now %>
active_storage_blob_10:
id: 10
key: '5lge4yv88jwzt7xl76vri2be1v10'
filename: 'test.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_11:
id: 11
key: '5lge4yv88jwzt7xl76vri2be1v11'
filename: 'test.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_12:
id: 12
key: '5lge4yv88jwzt7xl76vri2be1v12'
filename: 'test.mp4'
content_type: 'video/mp4'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 2037627
checksum : 'rqqmDUGlB3dKOB6bUVPV+g=='
created_at: <%= Time.now %>
active_storage_blob_13:
id: 13
key: '5lge4yv88jwzt7xl76vri2be1v13'
filename: 'test.odt'
content_type: 'application/vnd.oasis.opendocument.text'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 10179
checksum : 'k08HeKksIVI7PXr1aEVbjg=='
created_at: <%= Time.now %>
active_storage_blob_14:
id: 14
key: '5lge4yv88jwzt7xl76vri2be1v14'
filename: 'test.html'
content_type: 'text/html'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 10179
checksum : 'RV3RPuaIjvHzOXpvTLhI3w=='
created_at: <%= Time.now %>

View File

@ -3,10 +3,8 @@ dmsf_file_revisions_001:
id: 1
dmsf_file_id: 1
source_dmsf_file_revision_id: NULL
name: "test.txt"
disk_filename: "test.txt"
size: 4
mime_type: text/plain
name: "test.txt"
size: 3
title: "Test File"
description: 'Some file :-)'
workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL
@ -18,7 +16,6 @@ dmsf_file_revisions_001:
user_id: 1
dmsf_workflow_assigned_by_user_id: 1
dmsf_workflow_started_by_user_id: 1
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00
#revision for file on non-enabled project
@ -27,9 +24,7 @@ dmsf_file_revisions_002:
dmsf_file_id: 2
source_dmsf_file_revision_id: NULL
name: "test2.txt"
disk_filename: "test2.txt"
size: 4
mime_type: text/plain
size: 3
title: "Test File"
description: NULL
workflow: NULL
@ -41,7 +36,6 @@ dmsf_file_revisions_002:
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00
#revision for deleted file on dmsf-enabled project
@ -50,9 +44,7 @@ dmsf_file_revisions_003:
dmsf_file_id: 3
source_dmsf_file_revision_id: NULL
name: 'deleted.txt'
disk_filename: 'deleted.txt'
size: 4
mime_type: 'text/plain'
size: 3
title: 'Test File'
description: NULL
workflow: NULL
@ -64,7 +56,6 @@ dmsf_file_revisions_003:
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_004:
@ -72,9 +63,7 @@ dmsf_file_revisions_004:
dmsf_file_id: 4
source_dmsf_file_revision_id: NULL
name: 'test4.txt'
disk_filename: 'test4.txt'
size: 4
mime_type: 'text/plain'
size: 3
title: 'Test File'
description: NULL
workflow: NULL
@ -86,39 +75,14 @@ dmsf_file_revisions_004:
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_005:
id: 5
dmsf_file_id: 1
source_dmsf_file_revision_id: NULL
name: 'test5.txt'
disk_filename: 'test5.txt'
size: 4
mime_type: 'application/vnd.oasis.opendocument.text'
title: 'Test file'
description: NULL
workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL
minor_version: 1
major_version: 1
comment: 'Wrong mime type in order to have Edit content menu item'
deleted: 0
deleted_by_user_id: NULL
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:28 +02:00
dmsf_file_revisions_006:
id: 6
dmsf_file_id: 7
source_dmsf_file_revision_id: NULL
name: 'test.gif'
disk_filename: 'test.gif'
size: 4
mime_type: 'image/gif'
size: 310
title: 'Test image'
description: NULL
workflow: NULL
@ -130,7 +94,6 @@ dmsf_file_revisions_006:
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
digest: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_007:
@ -138,9 +101,7 @@ dmsf_file_revisions_007:
dmsf_file_id: 8
source_dmsf_file_revision_id: NULL
name: 'test.pdf'
disk_filename: 'test.pdf'
size: 4
mime_type: 'application/pdf'
size: 6942
title: 'Test PDF'
description: NULL
workflow: NULL
@ -152,17 +113,14 @@ dmsf_file_revisions_007:
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
digest: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_008:
id: 8
dmsf_file_id: 9
source_dmsf_file_revision_id: NULL
name: 'myfile.txt'
disk_filename: 'myfile.txt' # The file is not physically present
name: 'myfile.txt' # The file is not physically present
size: 0
mime_type: 'text/plain'
title: 'My File'
description: NULL
workflow: NULL
@ -181,9 +139,7 @@ dmsf_file_revisions_009:
dmsf_file_id: 10
source_dmsf_file_revision_id: NULL
name: 'zero.txt'
disk_filename: 'zero.txt'
size: 0
mime_type: 'text/plain'
title: 'Zero Size File'
description: NULL
workflow: NULL
@ -195,7 +151,6 @@ dmsf_file_revisions_009:
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
digest: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_010:
@ -203,9 +158,7 @@ dmsf_file_revisions_010:
dmsf_file_id: 5
source_dmsf_file_revision_id: NULL
name: 'test.txt'
disk_filename: 'test.txt'
size: 4
mime_type: 'text/plain'
title: 'Test File'
description: 'Some file :-)'
workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL
@ -217,7 +170,6 @@ dmsf_file_revisions_010:
user_id: 1
dmsf_workflow_assigned_by_user_id: 1
dmsf_workflow_started_by_user_id: 1
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_011:
@ -225,9 +177,7 @@ dmsf_file_revisions_011:
dmsf_file_id: 12
source_dmsf_file_revision_id: NULL
name: 'test.txt'
disk_filename: 'test.txt'
size: 4
mime_type: 'text/plain'
title: 'Test File'
description: 'Some file :-)'
workflow: 0
@ -239,7 +189,6 @@ dmsf_file_revisions_011:
user_id: 1
dmsf_workflow_assigned_by_user_id: 1
dmsf_workflow_started_by_user_id: 1
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_012:
@ -247,9 +196,7 @@ dmsf_file_revisions_012:
dmsf_file_id: 6
source_dmsf_file_revision_id: NULL
name: 'test.mp4'
disk_filename: 'test.mp4'
size: 4
mime_type: 'video/mp4'
title: 'test video'
description: 'A video :-)'
workflow: 0
@ -261,7 +208,6 @@ dmsf_file_revisions_012:
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2022-02-03 13:39:27 +02:00
dmsf_file_revisions_013:
@ -269,9 +215,7 @@ dmsf_file_revisions_013:
dmsf_file_id: 13
source_dmsf_file_revision_id: NULL
name: 'test.odt'
disk_filename: 'test.odt'
size: 4
mime_type: 'application/vnd.oasis.opendocument.text'
size: 10445
title: 'Test office document'
description: 'LibreOffice text'
workflow: 0
@ -283,5 +227,23 @@ dmsf_file_revisions_013:
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
digest: '89a6d0ea9aafc21a978152f3e4977812d5d7d623505749471f256a90fc7c5f72'
created_at: 2017-04-01 08:54:00 +02:00
dmsf_file_revisions_014:
id: 14
dmsf_file_id: 14
source_dmsf_file_revision_id: NULL
name: 'test.html'
size: 10445
title: 'Webpage'
description: 'HTML document'
workflow: 0
minor_version: 0
major_version: 1
comment: NULL
deleted: 0
deleted_by_user_id: NULL
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
created_at: 2017-04-01 08:54:00 +02:00

View File

@ -3,7 +3,6 @@ dmsf_files_001:
id: 1
project_id: 1
dmsf_folder_id: NULL
name: 'test.txt'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -13,7 +12,6 @@ dmsf_files_002:
id: 2
project_id: 2
dmsf_folder_id: NULL
name: 'test.txt'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -23,7 +21,6 @@ dmsf_files_003:
id: 3
project_id: 1
dmsf_folder_id: NULL
name: 'deleted.txt'
notification: false
deleted: 1
deleted_by_user_id: 1
@ -32,7 +29,6 @@ dmsf_files_004:
id: 4
project_id: 1
dmsf_folder_id: 2
name: 'test.txt'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -41,7 +37,6 @@ dmsf_files_005:
id: 5
project_id: 1
dmsf_folder_id: 5
name: 'test.txt'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -50,7 +45,6 @@ dmsf_files_006:
id: 6
project_id: 2
dmsf_folder_id: 3
name: 'test.mp4'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -59,7 +53,6 @@ dmsf_files_007:
id: 7
project_id: 1
dmsf_folder_id: 8
name: 'test.gif'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -68,7 +61,6 @@ dmsf_files_008:
id: 8
project_id: 1
dmsf_folder_id: NULL
name: 'test.pdf'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -77,7 +69,6 @@ dmsf_files_009:
id: 9
project_id: 1
dmsf_folder_id: NULL
name: 'myfile.txt'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -86,7 +77,6 @@ dmsf_files_010:
id: 10
project_id: 1
dmsf_folder_id: NULL
name: 'zero.txt'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -95,7 +85,6 @@ dmsf_files_011:
id: 11
project_id: 1
dmsf_folder_id: 9
name: 'zero.txt'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -104,7 +93,6 @@ dmsf_files_012:
id: 12
project_id: 5
dmsf_folder_id: NULL
name: 'test.txt'
notification: false
deleted: 0
deleted_by_user_id: NULL
@ -113,6 +101,12 @@ dmsf_files_013:
id: 13
project_id: 1
dmsf_folder_id: NULL
name: 'test.odt'
deleted: 0
deleted_by_user_id: NULL
dmsf_files_014:
id: 14
project_id: 1
dmsf_folder_id: NULL
deleted: 0
deleted_by_user_id: NULL

View File

@ -51,4 +51,10 @@ wfsa9:
id: 9
dmsf_workflow_step_id: 5
user_id: 2
dmsf_file_revision_id: 1
dmsf_file_revision_id: 1
wfsa10:
id: 10
dmsf_workflow_step_id: 6
user_id: 2
dmsf_file_revision_id: 2

View File

@ -36,4 +36,12 @@ wfs5:
step: 3
name: '3rd step'
user_id: 2
operator: 1
operator: 1
wfs6:
id: 6
dmsf_workflow_id: 2
step: 1
name: '1st step'
user_id: 2
operator: 1

View File

@ -1 +0,0 @@
123

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<body>
Hello world!
</body>
</html>

View File

@ -73,7 +73,7 @@ class DmsfContextMenusControllerTest < RedmineDmsf::Test::TestCase
assert_select 'a.icon-unlock', text: l(:button_unlock)
assert_select 'a.icon-unlock.disabled', text: l(:button_edit_content), count: 0
assert_select 'a.icon-file', text: l(:button_edit_content)
assert_select 'a.icon-file.disabled', text: l(:button_edit_content), count: 0
assert_select 'a.icon-file.disabled', text: l(:button_edit_content), count: 1
end
end

View File

@ -142,14 +142,14 @@ class DmsfFilesControllerTest < RedmineDmsf::Test::TestCase
version_major: @file1.last_revision.major_version,
version_minor: @file1.last_revision.minor_version + 1,
dmsf_file_revision: {
title: @file1.last_revision.title,
name: @file1.last_revision.name,
description: @file1.last_revision.description,
title: @file1.title,
name: @file1.name,
description: @file1.description,
comment: 'New revision'
}
}
end
assert_redirected_to dmsf_folder_path(id: @file1.project)
assert_not_nil @file1.last_revision.digest
assert_not_nil @file1.last_revision.checksum
end
end

View File

@ -31,6 +31,7 @@ class DmsfWorkflowsControllerTest < RedmineDmsf::Test::TestCase
@wfs4 = DmsfWorkflowStep.find 4 # step 2
@wfs5 = DmsfWorkflowStep.find 5 # step 3
@wf1 = DmsfWorkflow.find 1
@wf2 = DmsfWorkflow.find 2
@wf3 = DmsfWorkflow.find 3
@wfsa2 = DmsfWorkflowStepAssignment.find 2
@revision1 = DmsfFileRevision.find 1
@ -429,8 +430,8 @@ class DmsfWorkflowsControllerTest < RedmineDmsf::Test::TestCase
def test_log_member_global_wf
post '/login', params: { username: 'jsmith', password: 'jsmith' }
get "/dmsf_workflows/#{@wf3.id}/log",
params: { project_id: @project1.id, dmsf_file_id: @file1.id, format: 'js' },
get "/dmsf_workflows/#{@wf2.id}/log",
params: { project_id: @project1.id, dmsf_file_id: @file2.id, format: 'js' },
xhr: true
assert_response :success
assert_template :log

View File

@ -36,9 +36,9 @@ class MyControllerTest < RedmineDmsf::Test::TestCase
end
def test_page_with_open_approvals_no_approval
post '/login', params: { username: 'jsmith', password: 'jsmith' }
@jsmith.pref[:my_page_layout] = { 'top' => ['open_approvals'] }
@jsmith.pref.save!
post '/login', params: { username: 'admin', password: 'admin' }
@admin.pref[:my_page_layout] = { 'top' => ['open_approvals'] }
@admin.pref.save!
get '/my/page'
assert_response :success
assert_select 'div#list-top' do

Some files were not shown because too many files have changed in this diff Show More