#9 Active Storage - Duplicated columns removed

This commit is contained in:
Karel Pičman 2025-12-05 09:22:09 +01:00
parent 54ead3ded4
commit 3a50485653
43 changed files with 587 additions and 398 deletions

View File

@ -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)
@ -89,7 +89,7 @@ class DmsfFilesController < ApplicationController
params[:disposition] = 'attachment' if params[:filename].present?
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
@ -139,21 +139,18 @@ class DmsfFilesController < ApplicationController
upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, file_upload)
if upload
revision.size = upload.size
revision.disk_filename = revision.new_storage_filename
revision.file.attach(
io: File.open(upload.tempfile_path),
filename: revision.disk_filename,
content_type: revision.mime_type,
filename: file_upload.filename,
content_type: Redmine::MimeType.of(file_upload.filename),
identify: false
)
end
else
revision.size = last_revision.size
revision.disk_filename = last_revision.disk_filename
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]
@ -330,8 +327,8 @@ class DmsfFilesController < ApplicationController
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,
filename: filename_for_content_disposition(@file.name),
type: @file.last_revision.content_type,
disposition: 'inline'
end
else

View File

@ -32,7 +32,7 @@ class DmsfPublicUrlsController < ApplicationController
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

@ -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
@ -84,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
@ -97,7 +94,7 @@ module DmsfUploadHelper
new_revision.file.attach(
io: File.open(committed_file[:tempfile_path]),
filename: new_revision.name,
content_type: new_revision.mime_type,
content_type: new_revision.content_type,
identify: false
)
file.last_revision = new_revision

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?
@ -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,28 +313,12 @@ class DmsfFile < ApplicationRecord
file = DmsfFile.new
file.dmsf_folder_id = folder.id if folder
file.project_id = project.id
title = last_revision&.title
if DmsfFile.visible.exists?(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, name: filename)
basename = File.basename(filename, '.*')
extname = File.extname(filename)
1.step do |i|
title = "#{basename} (#{i})"
gen_filename = "#{title}#{extname}"
unless DmsfFile.visible.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 = title
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
new_revision.workflow = nil
new_revision.dmsf_workflow_id = nil
@ -350,7 +334,7 @@ class DmsfFile < ApplicationRecord
if last_revision.file.attached?
begin
new_revision.file.attach(
io: StringIO.new(last_revision.file.blob.download),
io: StringIO.new(last_revision.file.download),
filename: filename,
content_type: new_revision.file.content_type,
identify: false
@ -367,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
@ -504,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
@ -537,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
@ -573,8 +570,7 @@ class DmsfFile < ApplicationRecord
end
rescue StandardError => e
Rails.logger.error do
%(An error occurred while generating preview for #{last_revision.file.name} to #{target}\n
Exception was: #{e.message})
%(An error occurred while generating preview for #{name} to #{target}\nException was: #{e.message})
end
''
end
@ -604,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)
@ -640,10 +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?
@ -657,10 +645,10 @@ class DmsfFile < ApplicationRecord
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?
Redmine::Thumbnail.generate last_revision.file.download, target, size, pdf?
rescue StandardError => e
Rails.logger.error do
%(An error occured while generating thumbnail for #{last_revision.disk_file} to #{target}\n
%(An error occured while generating thumbnail for #{last_revision.file&.blob&.filename} to #{target}\n
Exception was: #{e.message})
end
nil

View File

@ -90,13 +90,9 @@ class DmsfFileRevision < ApplicationRecord
}
)
validates :title, presence: true
validates :title, length: { maximum: 255 }
validates :title, presence: true, dmsf_file_name: true, length: { maximum: 255 }
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
validates :description, length: { maximum: 1.kilobyte }
validates :size, dmsf_max_file_size: true
@ -118,8 +114,11 @@ class DmsfFileRevision < ApplicationRecord
file.blob&.checksum
end
def mime_type
file.blob&.content_type
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)
@ -191,49 +190,9 @@ 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.title = title
new_revision.description = description
@ -325,7 +284,7 @@ class DmsfFileRevision < ApplicationRecord
file.attach(
io: open_file,
filename: dmsf_file.name,
content_type: mime_type.presence || Redmine::MimeType.of(disk_filename),
content_type: content_type,
identify: false
)
end
@ -396,7 +355,7 @@ 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

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_folder_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_link_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

@ -22,6 +22,38 @@ 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)
# Check name uniqueness among files
project_id = record.dmsf_file.project_id
dmsf_folder_id = record.dmsf_file.dmsf_folder_id
id = record.dmsf_file_id
DmsfFile
.visible
.where(project_id: project_id, dmsf_folder_id: dmsf_folder_id)
.where.not(id: id)
.find_each do |file|
if file.name == value || file.title == value
record.errors.add attribute, :taken
break
end
end
# Check name uniqueness among folders
DmsfFolder.visible.where(project_id: project_id, dmsf_folder_id: dmsf_folder_id).find_each do |folder|
if folder.title == value
record.errors.add attribute, :taken
break
end
end
# Check name uniqueness among links
DmsfLink.visible.where(project_id: project_id, dmsf_folder_id: dmsf_folder_id).find_each do |link|
if link.name == value
record.errors.add attribute, :taken
break
end
end
end
end

View File

@ -0,0 +1,44 @@
# 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 DmsfFolderNameValidator < 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)
# Check name uniqueness among files
DmsfFile.visible.where(project_id: record.project_id, dmsf_folder_id: record.dmsf_folder_id).find_each do |file|
if file.name == value || file.title == value
record.errors.add attribute, :taken
break
end
end
# Check name uniqueness among links
DmsfLink.visible.where(project_id: record.project_id, dmsf_folder_id: record.dmsf_folder_id).find_each do |link|
if link.name == value
record.errors.add attribute, :taken
break
end
end
end
end

View File

@ -0,0 +1,44 @@
# 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 DmsfLinkNameValidator < 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)
# Check name uniqueness among files
DmsfFile.visible.where(project_id: record.project_id, dmsf_folder_id: record.dmsf_folder_id).find_each do |file|
if file.name == value || file.title == value
record.errors.add attribute, :taken
break
end
end
# Check name uniqueness among folders
DmsfFolder.visible.where(project_id: record.project_id, dmsf_folder_id: record.dmsf_folder_id).find_each do |folder|
if folder.title == value
record.errors.add attribute, :taken
break
end
end
end
end

View File

@ -71,7 +71,7 @@
<% 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),
static_dmsf_file_path(dmsf_file, filename: filename, download: dmsf_file.last_revision&.id),
class: 'icon icon-download', data: { cy: "icon__download--dmsf_file_#{dmsf_file.id}" },
disabled: false %>
</li>

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

@ -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

@ -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

View File

@ -133,7 +133,7 @@
</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.checksum.present? %>
<div class="status attribute">

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

@ -47,7 +47,7 @@ class ActiveStorageMigration < ActiveRecord::Migration[7.0]
r.file.attach(
io: File.open(path),
filename: r.name,
content_type: r.mime_type,
content_type: r.content_type,
identify: false
)
# Remove the original file
@ -60,18 +60,51 @@ class ActiveStorageMigration < ActiveRecord::Migration[7.0]
end
end
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 = r.disk_file(search_if_not_exists: false)
new_path = disk_file
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 = f.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}"
@ -100,4 +133,19 @@ class ActiveStorageMigration < ActiveRecord::Migration[7.0]
false
end
end
def storage_base_path(dmsf_file_revision)
time = dmsf_file_revision.created_at || DateTime.current
DmsfFile.storage_path.join(time.strftime('%Y')).join time.strftime('%m')
end
def disk_file(dmsf_file_revision)
path = storage_base_path
begin
FileUtils.mkdir_p path
rescue StandardError => e
Rails.logger.error e.message
end
path.join(dmsf_file_revision.disk_filename).to_s
end
end

View File

@ -1,27 +0,0 @@
# 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/>.
# Restore DmsfFileRevision.updated_at from DmsfFileRevision.temp_updated_at column
class RestoreUpdatedAt < ActiveRecord::Migration[7.0]
# temp_updated_at => updated_at
def up
DmsfFileRevision.update_all 'updated_at = temp_updated_at'
remove_column :dmsf_file_revisions, :temp_updated_at
end
end

View File

@ -1,34 +0,0 @@
# 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 column
class RemoveDuplicitiesFromRevision < ActiveRecord::Migration[7.0]
def up
remove_column :dmsf_file_revisions, :digest
remove_column :dmsf_file_revisions, :mime_type
# We need to keep the size despite the fact that it's duplicated in active_storage_blobs to speed up the main
# document view
end
def down
add_column :dmsf_file_revisions, :digest, :string, limit: 64, default: '', null: false
add_column :dmsf_file_revisions, :mime_type, :string
# Recalculation of these columns for all revisions is technically possible but costs are too high.
end
end

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

@ -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
@ -278,7 +278,7 @@ module RedmineDmsf
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

@ -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
@ -272,10 +266,8 @@ 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
new_revision.copy_file_content StringIO.new(file.last_revision.file.download)
# Save
new_revision.save && dest.resource.file.save
@ -304,7 +296,6 @@ module RedmineDmsf
file.last_revision.name = dest.resource.basename
file.last_revision.title = DmsfFileRevision.filename_to_title(dest.resource.basename)
end
file.name = dest.resource.basename
# Save Changes
if file.last_revision.save && file.save
dest.exist? ? NoContent : Created
@ -376,7 +367,6 @@ 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
# Save Changes
unless new_file.last_revision.save && new_file.save
new_file.delete commit: true
@ -587,7 +577,6 @@ 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
@ -627,7 +616,6 @@ module RedmineDmsf
raise UnprocessableEntity
end
new_revision.disk_filename = new_revision.new_storage_filename unless reuse_revision
if new_revision.save
if request.body.respond_to?(:rewind)
new_revision.copy_file_content request.body
@ -775,7 +763,6 @@ 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
r = DmsfFileRevision.new
@ -786,14 +773,18 @@ module RedmineDmsf
r.user = User.current
r.name = basename
r.size = 0
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)
revision.file.attach(
io: File.new(upload.tempfile_path),
filename: file_upload.filename,
content_type: Redmine::MimeType.of(file_upload.filename),
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

@ -31,14 +31,6 @@ active_storage_attachment_4:
blob_id: 4
created_at: <%= Time.now %>
#active_storage_attachment_5:
# id: 5
# name: 'shared_file'
# record_type: 'DmsfFileRevision'
# record_id: 5
# blob_id: 5
# created_at: <%= Time.now %>
active_storage_attachment_6:
id: 6
name: 'shared_file'
@ -102,3 +94,11 @@ active_storage_attachment_13:
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 %>

View File

@ -130,3 +130,14 @@ active_storage_blob_13:
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

@ -4,7 +4,6 @@ dmsf_file_revisions_001:
dmsf_file_id: 1
source_dmsf_file_revision_id: NULL
name: "test.txt"
disk_filename: "test.txt"
size: 3
title: "Test File"
description: 'Some file :-)'
@ -25,7 +24,6 @@ dmsf_file_revisions_002:
dmsf_file_id: 2
source_dmsf_file_revision_id: NULL
name: "test2.txt"
disk_filename: "test2.txt"
size: 3
title: "Test File"
description: NULL
@ -46,7 +44,6 @@ dmsf_file_revisions_003:
dmsf_file_id: 3
source_dmsf_file_revision_id: NULL
name: 'deleted.txt'
disk_filename: 'deleted.txt'
size: 3
title: 'Test File'
description: NULL
@ -66,7 +63,6 @@ dmsf_file_revisions_004:
dmsf_file_id: 4
source_dmsf_file_revision_id: NULL
name: 'test4.txt'
disk_filename: 'test4.txt'
size: 3
title: 'Test File'
description: NULL
@ -86,7 +82,6 @@ dmsf_file_revisions_006:
dmsf_file_id: 7
source_dmsf_file_revision_id: NULL
name: 'test.gif'
disk_filename: 'test.gif'
size: 310
title: 'Test image'
description: NULL
@ -106,7 +101,6 @@ dmsf_file_revisions_007:
dmsf_file_id: 8
source_dmsf_file_revision_id: NULL
name: 'test.pdf'
disk_filename: 'test.pdf'
size: 6942
title: 'Test PDF'
description: NULL
@ -125,8 +119,7 @@ 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
title: 'My File'
description: NULL
@ -146,7 +139,6 @@ dmsf_file_revisions_009:
dmsf_file_id: 10
source_dmsf_file_revision_id: NULL
name: 'zero.txt'
disk_filename: 'zero.txt'
size: 0
title: 'Zero Size File'
description: NULL
@ -166,7 +158,6 @@ dmsf_file_revisions_010:
dmsf_file_id: 5
source_dmsf_file_revision_id: NULL
name: 'test.txt'
disk_filename: 'test.txt'
size: 4
title: 'Test File'
description: 'Some file :-)'
@ -186,7 +177,6 @@ dmsf_file_revisions_011:
dmsf_file_id: 12
source_dmsf_file_revision_id: NULL
name: 'test.txt'
disk_filename: 'test.txt'
size: 4
title: 'Test File'
description: 'Some file :-)'
@ -206,7 +196,6 @@ dmsf_file_revisions_012:
dmsf_file_id: 6
source_dmsf_file_revision_id: NULL
name: 'test.mp4'
disk_filename: 'test.mp4'
size: 4
title: 'test video'
description: 'A video :-)'
@ -226,7 +215,6 @@ dmsf_file_revisions_013:
dmsf_file_id: 13
source_dmsf_file_revision_id: NULL
name: 'test.odt'
disk_filename: 'test.odt'
size: 10445
title: 'Test office document'
description: 'LibreOffice text'
@ -240,3 +228,22 @@ dmsf_file_revisions_013:
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
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

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

View File

@ -142,9 +142,9 @@ 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'
}
}

View File

@ -48,7 +48,7 @@ class DmsfFileApiTest < RedmineDmsf::Test::IntegrationTest
# <name>test5.txt</name>
# <content_url>http://www.example.com/dmsf/files/1/view?download=5</content_url>
# <size>4</size>
# <mime_type>text/plain</mime_type>
# <content_type>text/plain</content_type>
# <title>Test File</title>
# <description/>
# <workflow>1</workflow>
@ -71,7 +71,7 @@ class DmsfFileApiTest < RedmineDmsf::Test::IntegrationTest
# <name>test.txt</name>
# <content_url>http://www.example.com/dmsf/files/1/view?download=1</content_url>
# <size>4</size>
# <mime_type>text/plain</mime_type>
# <content_type>text/plain</content_type>
# <title>Test File</title>
# <description>Some file :-)</description>
# <workflow>1</workflow>
@ -160,7 +160,7 @@ class DmsfFileApiTest < RedmineDmsf::Test::IntegrationTest
assert_response :success
revision = DmsfFileRevision.order(:created_at).last
assert revision.present?
assert_equal 'text/plain', revision.mime_type
assert_equal 'text/plain', revision.content_type
end
def test_upload_document_exceeded_attachment_max_size

View File

@ -154,11 +154,11 @@ class DmsfWebdavGetTest < RedmineDmsf::Test::IntegrationTest
folder = DmsfFolder.find_by(id: 1)
assert_not_nil folder
assert response.body.match(@folder1.title),
"Expected to find #{folder.title} in return data"
"Expected to find #{folder.title} in the response"
file = DmsfFile.find_by(id: 1)
assert_not_nil file
assert response.body.match(file.name),
"Expected to find #{file.name} in return data"
"Expected to find #{file.name} in the response"
end
def test_user_assigned_to_project_dmsf_module_not_enabled

View File

@ -308,7 +308,7 @@ class DmsfWebdavMoveTest < RedmineDmsf::Test::IntegrationTest
temp_file = DmsfFile.find_file_by_name @project1, nil, temp_file_name
assert_not temp_file, "File '#{temp_file_name}' should not exist yet."
# Move the original file to AAAAAAAA.tmp. The original file should not changed but a new file should be created.
# Move the original file to AAAAAAAA.tmp. The original file should not be changed but a new file should be created.
assert_no_difference '@file1.dmsf_file_revisions.count' do
process :move,
"/dmsf/webdav/#{@project1.identifier}/#{@file1.name}",

View File

@ -255,7 +255,7 @@ class DmsfWebdavPutTest < RedmineDmsf::Test::IntegrationTest
put "/dmsf/webdav/#{@project1.identifier}/file1.tmp", params: '1234', headers: credentials
assert_response :success
file1 = DmsfFile.find_by(project_id: @project1.id, dmsf_folder: nil, name: 'file1.tmp')
file1 = DmsfFile.find_file_by_name(@project1, nil, 'file1.tmp')
assert file1
assert_difference 'file1.dmsf_file_revisions.count', 0 do
put "/dmsf/webdav/#{@project1.identifier}/file1.tmp", params: '5678', headers: credentials
@ -268,7 +268,7 @@ class DmsfWebdavPutTest < RedmineDmsf::Test::IntegrationTest
put "/dmsf/webdav/#{@project1.identifier}/~$file2.txt", params: '1234', headers: credentials
assert_response :success
file2 = DmsfFile.find_by(project_id: @project1.id, dmsf_folder_id: nil, name: '~$file2.txt')
file2 = DmsfFile.find_file_by_name(@project1, nil, '~$file2.txt')
assert file2
assert_difference 'file2.dmsf_file_revisions.count', 0 do
put "/dmsf/webdav/#{@project1.identifier}/~$file2.txt", params: '5678', headers: credentials
@ -286,7 +286,7 @@ class DmsfWebdavPutTest < RedmineDmsf::Test::IntegrationTest
'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do
put "/dmsf/webdav/#{@project1.identifier}/file3.dump", params: '1234', headers: credentials
assert_response :success
file3 = DmsfFile.find_by(project_id: @project1.id, dmsf_folder_id: nil, name: 'file3.dump')
file3 = DmsfFile.find_file_by_name(@project1, nil, 'file3.dump')
assert file3
assert_difference 'file3.dmsf_file_revisions.count', 0 do
put "/dmsf/webdav/#{@project1.identifier}/file3.dump", params: '5678', headers: credentials
@ -304,7 +304,7 @@ class DmsfWebdavPutTest < RedmineDmsf::Test::IntegrationTest
params: '1234',
headers: @admin.merge!({ content_type: :text })
assert_response :created
assert DmsfFile.find_by(project_id: @project5.id, dmsf_folder: nil, name: 'test-1234.txt')
assert DmsfFile.find_file_by_name(@project5, nil, 'test-1234.txt')
end
def test_put_keep_title

View File

@ -28,34 +28,112 @@ class DmsfFileRevisionTest < RedmineDmsf::Test::UnitTest
@revision1 = DmsfFileRevision.find 1
@revision2 = DmsfFileRevision.find 2
@revision3 = DmsfFileRevision.find 3
@revision7 = DmsfFileRevision.find 7
@revision8 = DmsfFileRevision.find 8
@revision13 = DmsfFileRevision.find 13
@wf1 = DmsfWorkflow.find 1
end
def test_file_title_length_validation
file = DmsfFileRevision.new(title: Array.new(256).map { 'a' }.join,
name: 'Test Revision',
major_version: 1)
assert file.invalid?
assert_equal ['Title is too long (maximum is 255 characters)'], file.errors.full_messages
def test_name_presence
@revision1.name = ''
assert @revision1.invalid?
assert_includes @revision1.errors.full_messages, 'Name cannot be blank'
end
def test_file_name_length_validation
file = DmsfFileRevision.new(name: Array.new(256).map { 'a' }.join,
title: 'Test Revision',
major_version: 1)
assert file.invalid?
assert_equal ['Name is too long (maximum is 255 characters)'], file.errors.full_messages
def test_title_presence
@revision1.title = ''
assert @revision1.invalid?
assert_includes @revision1.errors.full_messages, 'Title cannot be blank'
end
def test_file_disk_filename_length_validation
file = DmsfFileRevision.new(disk_filename: Array.new(256).map { 'a' }.join,
title: 'Test Revision',
name: 'Test Revision',
major_version: 1)
assert file.invalid?
assert_equal ['Disk filename is too long (maximum is 255 characters)'], file.errors.full_messages
def test_title_length_validation
@revision1.title = String.new('a' * 256)
assert @revision1.invalid?
assert_includes @revision1.errors.full_messages, 'Title is too long (maximum is 255 characters)'
end
def test_name_length_validation
@revision1.name = String.new('a' * 256)
assert @revision1.invalid?
assert_includes @revision1.errors.full_messages, 'Name is too long (maximum is 255 characters)'
end
def test_name_uniqueness_validation
User.current = @admin
# Duplicity among files names
@revision7.name = @revision1.name
assert @revision7.invalid?
assert_includes @revision7.errors.full_messages, 'Name has already been taken'
# Duplicity among invisible files is all right
@revision7.name = @revision3.name
assert_not @revision7.invalid?
# Duplicity among files titles
@revision7.name = @revision1.title
assert @revision7.invalid?
assert_includes @revision7.errors.full_messages, 'Name has already been taken'
# Duplicity among folders
@revision7.name = @folder1.title
assert @revision7.invalid?
assert_includes @revision7.errors.full_messages, 'Name has already been taken'
# Duplicity among links
@revision7.name = @folder_link1.name
assert @revision7.invalid?
assert_includes @revision7.errors.full_messages, 'Name has already been taken'
# Name is all right
@revision7.name = 'xxx'
assert @revision7.valid?
end
def test_title_uniqueness_validation
User.current = @admin
# Duplicity among files names
@revision7.title = @revision1.name
assert @revision7.invalid?
assert_includes @revision7.errors.full_messages, 'Title has already been taken'
# Duplicity among invisible files is all right
@revision7.title = @revision3.name
assert_not @revision7.invalid?
# Duplicity among files titles
@revision7.title = @revision1.title
assert @revision7.invalid?
assert_includes @revision7.errors.full_messages, 'Title has already been taken'
# Duplicity among folders
@revision7.title = @folder1.title
assert @revision7.invalid?
assert_includes @revision7.errors.full_messages, 'Title has already been taken'
# Duplicity among links
@revision7.title = @folder_link1.name
assert @revision7.invalid?
assert_includes @revision7.errors.full_messages, 'Title has already been taken'
# Name is all right
@revision7.title = 'xxx'
assert @revision7.valid?
end
def test_name_invalid_characters_validation
@revision1.name << DmsfFolder::INVALID_CHARACTERS[0]
assert @revision1.invalid?
assert_includes @revision1.errors.full_messages,
"Name #{l('activerecord.errors.messages.error_contains_invalid_character')}"
end
def test_title_invalid_characters_validation
@revision1.title << DmsfFolder::INVALID_CHARACTERS[0]
assert @revision1.invalid?
assert_includes @revision1.errors.full_messages,
"Title #{l('activerecord.errors.messages.error_contains_invalid_character')}"
end
def test_delete_restore
@ -70,56 +148,6 @@ class DmsfFileRevisionTest < RedmineDmsf::Test::UnitTest
assert_nil DmsfFileRevision.find_by(id: @revision13.id)
end
def test_new_storage_filename
# Create a file.
f = DmsfFile.new
f.project_id = 1
f.name = 'Testfile.txt'
f.dmsf_folder = nil
f.notification = RedmineDmsf.dmsf_default_notifications?
f.save
# Create two new revisions, r1 and r2
r1 = DmsfFileRevision.new
r1.minor_version = 0
r1.major_version = 1
r1.dmsf_file = f
r1.user = User.current
r1.name = 'Testfile.txt'
r1.title = DmsfFileRevision.filename_to_title(r1.name)
r1.description = nil
r1.comment = nil
r1.size = 4
r2 = r1.clone
r2.minor_version = 1
assert r1.valid?
assert r2.valid?
# This is a very stupid since the generation and storing of files below must be done during the
# same second, so wait until the microsecond part of the DateTime is less than 10 ms, should be
# plenty of time to do the rest then.
wait_timeout = 2_000
while DateTime.current.usec > 10_000
wait_timeout -= 10
flunk 'Waited too long.' if wait_timeout <= 0
sleep 0.01
end
# First, generate the r1 storage filename and save the file
r1.disk_filename = r1.new_storage_filename
assert r1.save
# Just make sure the file exists
File.binwrite r1.disk_file, '1234'
# Directly after the file has been stored generate the r2 storage filename.
# Hopefully the seconds part of the DateTime.current has not changed and the generated filename will
# be on the same second but it should then be increased by 1.
r2.disk_filename = r2.new_storage_filename
assert_not_equal r1.disk_filename, r2.disk_filename, 'The disk filename should not be equal for two revisions.'
end
def test_invalid_filename_extension
with_settings(attachment_extensions_allowed: 'txt') do
r1 = DmsfFileRevision.new
@ -334,4 +362,12 @@ class DmsfFileRevisionTest < RedmineDmsf::Test::UnitTest
def test_checksum
assert_equal @revision1.checksum, @revision1.file.blob.checksum
end
def test_content_type
assert_equal 'text/plain', @revision1.content_type
@revision1.file.blob.content_type = ''
assert_equal 'text/plain', @revision1.content_type
@revision1.file.blob.filename = 'data'
assert_equal 'application/octet-stream', @revision1.content_type
end
end

View File

@ -28,12 +28,6 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest
@wf2 = DmsfWorkflow.find 2
end
def test_file_name_length_validation
file = DmsfFile.new(name: Array.new(256).map { 'a' }.join)
assert file.invalid?
assert_equal ['Name is too long (maximum is 255 characters)'], file.errors.full_messages
end
def test_project_file_count_differs_from_project_visibility_count
assert_not_same @project1.dmsf_files.all.size, @project1.dmsf_files.visible.all.size
end
@ -188,15 +182,13 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest
# Text
assert_equal 'attachment', @file1.disposition
# Image
assert_equal 'inline', @file7.disposition
assert_equal 'inline', @file6.disposition
# PDF
assert_equal 'inline', @file8.disposition
assert_equal 'inline', @file7.disposition
# Video
@file1.last_revision.disk_filename = 'test.mp4'
assert_equal 'inline', @file1.disposition
assert_equal 'inline', @file6.disposition
# HTML
@file1.last_revision.disk_filename = 'test.html'
assert_equal 'inline', @file1.disposition
assert_equal 'inline', @file14.disposition
end
def test_image
@ -209,8 +201,6 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest
assert @file1.text?
assert_not @file7.text?
assert_not @file8.text?
@file1.last_revision.disk_filename = 'test.c'
assert @file1.text?
end
def test_pdf
@ -221,25 +211,28 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest
def test_video
assert_not @file1.video?
@file1.last_revision.disk_filename = 'test.mp4'
assert @file1.video?
assert @file6.video?
end
def test_html
assert_not @file1.html?
@file1.last_revision.disk_filename = 'test.html'
assert @file1.html?
assert @file14.html?
end
def test_office_doc
assert_not @file1.office_doc?
assert @file13.office_doc?
end
def test_markdown
assert_not @file1.markdown?
@file1.last_revision.disk_filename = 'test.md'
@file1.last_revision.file.blob.content_type = 'text/markdown'
assert @file1.markdown?
end
def test_textile
assert_not @file1.textile?
@file1.last_revision.disk_filename = 'test.textile'
@file1.last_revision.file.blob.content_type = 'text/textile'
assert @file1.textile?
end
@ -293,10 +286,6 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest
assert @file1.watched_by?(@jsmith)
end
def test_office_doc
assert @file13.office_doc?
end
def test_previewable
assert(@file13.previewable?) if RedmineDmsf::Preview.office_available?
end

View File

@ -21,9 +21,64 @@ require File.expand_path('../../test_helper', __FILE__)
# Folder tests
class DmsfFolderTest < RedmineDmsf::Test::UnitTest
include Redmine::I18n
def setup
super
@link2 = DmsfLink.find 2
@revision1 = DmsfFileRevision.find 1
@revision3 = DmsfFileRevision.find 3
end
def test_title_presence
@folder1.title = ''
assert @folder1.invalid?
assert_includes @folder1.errors.full_messages, 'Title cannot be blank'
end
def test_title_length_validation
@folder1.title = String.new('a' * 256)
assert @folder1.invalid?
assert_includes @folder1.errors.full_messages, 'Title is too long (maximum is 255 characters)'
end
def test_title_invalid_characters_validation
@folder1.title << DmsfFolder::INVALID_CHARACTERS[0]
assert @folder1.invalid?
assert_includes @folder1.errors.full_messages,
"Title #{l('activerecord.errors.messages.error_contains_invalid_character')}"
end
def test_title_uniqueness_validation
User.current = @admin
# Duplicity among files names
@folder1.title = @revision1.name
assert @folder1.invalid?
assert_includes @folder1.errors.full_messages, 'Title has already been taken'
# Duplicity among invisible files is all right
@folder1.title = @revision3.name
assert_not @folder1.invalid?
# Duplicity among files titles
@folder1.title = @revision1.title
assert @folder1.invalid?
assert_includes @folder1.errors.full_messages, 'Title has already been taken'
# Duplicity among folders
@folder1.title = @folder6.title
assert @folder1.invalid?
assert_includes @folder1.errors.full_messages, 'Title has already been taken'
# Duplicity among links
@folder1.title = @folder_link1.name
assert @folder1.invalid?
assert_includes @folder1.errors.full_messages, 'Title has already been taken'
# Name is all right
@folder1.title = 'xxx'
assert @folder1.valid?
end
def test_visibility

View File

@ -21,6 +21,14 @@ require File.expand_path('../../test_helper', __FILE__)
# Link tests
class DmsfLinksTest < RedmineDmsf::Test::UnitTest
include Redmine::I18n
def setup
super
@revision1 = DmsfFileRevision.find 1
@revision3 = DmsfFileRevision.find 3
end
def test_create_folder_link
folder_link = DmsfLink.new
folder_link.target_project_id = @project1.id
@ -57,43 +65,77 @@ class DmsfLinksTest < RedmineDmsf::Test::UnitTest
assert external_link.save, external_link.errors.full_messages.to_sentence
end
def test_validate_name_length
@folder_link1.name = ('a' * 256)
assert_not @folder_link1.save, "Folder link #{@folder_link1.name} should have not been saved"
assert_equal 1, @folder_link1.errors.size
def test_name_length_validation
@folder_link1.name = String.new('a' * 256)
assert @folder_link1.invalid?
assert_includes @folder_link1.errors.full_messages, 'Name is too long (maximum is 255 characters)'
end
def test_validate_name_presence
@folder_link1.name = ''
assert_not @folder_link1.save, "Folder link #{@folder_link1.name} should have not been saved"
assert_equal 1, @folder_link1.errors.size
assert @folder_link1.invalid?
assert_includes @folder_link1.errors.full_messages, 'Name cannot be blank'
end
def test_title_invalid_characters_validation
@folder_link1.name << DmsfFolder::INVALID_CHARACTERS[0]
assert @folder_link1.invalid?
assert_includes @folder_link1.errors.full_messages,
"Name #{l('activerecord.errors.messages.error_contains_invalid_character')}"
end
def test_name_uniqueness_validation
User.current = @admin
# Duplicity among files names
@folder_link1.name = @revision1.name
assert @folder_link1.invalid?
assert_includes @folder_link1.errors.full_messages, 'Name has already been taken'
# Duplicity among invisible files is all right
@folder_link1.name = @revision3.name
assert_not @folder_link1.invalid?
# Duplicity among files titles
@folder_link1.name = @revision1.title
assert @folder_link1.invalid?
assert_includes @folder_link1.errors.full_messages, 'Name has already been taken'
# Duplicity among folders
@folder_link1.name = @folder6.title
assert @folder_link1.invalid?
assert_includes @folder_link1.errors.full_messages, 'Name has already been taken'
# Name is all right
@folder_link1.name = 'xxx'
assert @folder_link1.valid?
end
def test_validate_external_url_length
@file_link2.target_type = 'DmsfUrl'
@file_link2.external_url = "https://localhost/#{'a' * 256}"
assert_not @file_link2.save, "External URL link #{@file_link2.name} should have not been saved"
assert_equal 1, @file_link2.errors.size
assert @file_link2.invalid?
assert_includes @file_link2.errors.full_messages, 'URL is too long (maximum is 255 characters)'
end
def test_validate_external_url_presence
@file_link2.target_type = 'DmsfUrl'
@file_link2.external_url = ''
assert_not @file_link2.save, "External URL link #{@file_link2.name} should have not been saved"
assert_equal 1, @file_link2.errors.size
assert @file_link2.invalid?
assert_includes @file_link2.errors.full_messages, 'URL is invalid'
end
def test_validate_external_url_invalid
@file_link2.target_type = 'DmsfUrl'
@file_link2.external_url = 'htt ps://abc.xyz'
assert_not @file_link2.save, "External URL link #{@file_link2.name} should have not been saved"
assert_equal 1, @file_link2.errors.size
assert @file_link2.invalid?
assert_includes @file_link2.errors.full_messages, 'URL is invalid'
end
def test_validate_external_url_valid
@file_link2.target_type = 'DmsfUrl'
@file_link2.external_url = 'https://www.google.com/search?q=寿司'
assert @file_link2.save
assert @file_link2.valid?
end
def test_belongs_to_project
@ -186,7 +228,7 @@ class DmsfLinksTest < RedmineDmsf::Test::UnitTest
def test_copy_to_author
assert_equal @admin.id, @file_link2.user_id
User.current = @jsmith
l = @file_link2.copy_to(@project1, @folder1)
l = @file_link2.copy_to(@project1, @folder6)
assert l
assert_equal @jsmith.id, l.user_id, 'Author must be updated when copying'
end

View File

@ -37,10 +37,14 @@ class DmsfQueryTest < RedmineDmsf::Test::UnitTest
end
def test_dmsf_count
n = DmsfFolder.visible.where(project_id: @project1.id).where("title LIKE '%test%'").all.size +
DmsfFile.visible.where(project_id: @project1.id).where("name LIKE '%test%'").all.size +
DmsfLink.visible.where(project_id: @project1.id).where("name LIKE '%test%'").all.size
assert_equal n - 1, @query401.dmsf_count # One folder is not visible due to the permissions
n = DmsfFolder.where(project_id: @project1.id).where("title LIKE '%test%'").all.size +
DmsfLink.where(project_id: @project1.id).where("name LIKE '%test%'").all.size
DmsfFile.where(project_id: @project1.id).find_each do |file|
n += 1 if file.name.include?('test')
end
assert_equal n - 2, @query401.dmsf_count # Two file are not visible due to folder's permissions
end
def test_dmsf_nodes

View File

@ -350,7 +350,7 @@ class DmsfMacrosTest < RedmineDmsf::Test::HelperTest
target: '_blank',
rel: 'noopener',
title: h(@file7.last_revision.try(:tooltip)),
'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}")
'data-downloadurl' => "#{@file7.last_revision.content_type}:#{h(@file7.name)}:#{url}")
assert text.include?(link), text
end
@ -379,7 +379,7 @@ class DmsfMacrosTest < RedmineDmsf::Test::HelperTest
target: '_blank',
rel: 'noopener',
title: h(@file7.last_revision.try(:tooltip)),
'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}")
'data-downloadurl' => "#{@file7.last_revision.content_type}:#{h(@file7.name)}:#{url}")
assert text.include?(link), text
# TODO: arguments src and with and height are swapped
# size = '640x480'
@ -390,7 +390,7 @@ class DmsfMacrosTest < RedmineDmsf::Test::HelperTest
# target: '_blank',
# rel: 'noopener',
# title: h(@file7.last_revision.try(:tooltip)),
# 'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}")
# 'data-downloadurl' => "#{@file7.last_revision.content_type}:#{h(@file7.name)}:#{url}")
# assert text.include?(link), text
height = '480'
text = textilizable("{{dmsftn(#{@file7.id}, height=#{height})}}")
@ -410,7 +410,7 @@ class DmsfMacrosTest < RedmineDmsf::Test::HelperTest
target: '_blank',
rel: 'noopener',
title: h(@file7.last_revision.try(:tooltip)),
'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}")
'data-downloadurl' => "#{@file7.last_revision.content_type}:#{h(@file7.name)}:#{url}")
assert text.include?(link), text
end

View File

@ -56,7 +56,7 @@ class ProjectPatchTest < RedmineDmsf::Test::UnitTest
def test_dmsf_count
User.current = @jsmith
hash = @project1.dmsf_count
assert_equal 9, hash[:files]
assert_equal 10, hash[:files]
assert_equal 5, hash[:folders]
end
@ -70,7 +70,7 @@ class ProjectPatchTest < RedmineDmsf::Test::UnitTest
def test_copy_dmsf
User.current = @jsmith
assert_equal 5, @project1.dmsf_files.visible.all.size
assert_equal 6, @project1.dmsf_files.visible.all.size
assert_equal 3, @project1.dmsf_folders.visible.all.size
assert_equal 2, @project1.file_links.all.size
assert_equal 1, @project1.folder_links.all.size
@ -84,7 +84,7 @@ class ProjectPatchTest < RedmineDmsf::Test::UnitTest
@project5.copy_dmsf @project1
assert_equal 6, @project5.dmsf_files.all.size
assert_equal 7, @project5.dmsf_files.all.size
assert_equal 4, @project5.dmsf_folders.all.size
assert_equal 2, @project5.file_links.all.size
assert_equal 1, @project5.folder_links.all.size

View File

@ -56,10 +56,12 @@ module RedmineDmsf
@file2 = DmsfFile.find 2
@file4 = DmsfFile.find 4
@file5 = DmsfFile.find 5
@file6 = DmsfFile.find 6
@file7 = DmsfFile.find 7
@file8 = DmsfFile.find 8
@file11 = DmsfFile.find 11
@file13 = DmsfFile.find 13
@file14 = DmsfFile.find 14
@folder_link1 = DmsfLink.find 1
@file_link2 = DmsfLink.find 2
@file_link7 = DmsfLink.find 7