Feature #8959 » 0001-Add-Microsoft-Office-documents-preview-via-MarkItDow.patch
| app/controllers/admin_controller.rb | ||
|---|---|---|
| 79 | 79 |
[:text_all_migrations_have_been_run, !ActiveRecord::Base.connection.pool.migration_context.needs_migration?], |
| 80 | 80 |
[:text_minimagick_available, Object.const_defined?(:MiniMagick)], |
| 81 | 81 |
[:text_convert_available, Redmine::Thumbnail.convert_available?], |
| 82 |
[:text_gs_available, Redmine::Thumbnail.gs_available?] |
|
| 82 |
[:text_gs_available, Redmine::Thumbnail.gs_available?], |
|
| 83 |
[:text_markdown_converter_available, Redmine::Markdownizer.available?] |
|
| 83 | 84 |
] |
| 84 | 85 |
@checklist << [:text_default_active_job_queue_changed, Rails.application.config.active_job.queue_adapter != :async] if Rails.env.production? |
| 85 | 86 |
end |
| app/controllers/attachments_controller.rb | ||
|---|---|---|
| 63 | 63 |
render :action => 'image' |
| 64 | 64 |
elsif @attachment.is_pdf? |
| 65 | 65 |
render :action => 'pdf' |
| 66 |
elsif @content = @attachment.markdownized_preview_content |
|
| 67 |
render :action => 'markdownized' |
|
| 66 | 68 |
else |
| 67 | 69 |
render :action => 'other' |
| 68 | 70 |
end |
| app/models/attachment.rb | ||
|---|---|---|
| 82 | 82 | |
| 83 | 83 |
cattr_accessor :thumbnails_storage_path |
| 84 | 84 |
@@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails") |
| 85 |
cattr_accessor :markdownized_previews_storage_path |
|
| 86 |
@@markdownized_previews_storage_path = File.join(Rails.root, "tmp", "markdownized_previews") |
|
| 85 | 87 | |
| 86 | 88 |
before_create :files_to_final_location |
| 87 | 89 |
after_commit :delete_from_disk, :on => :destroy |
| ... | ... | |
| 299 | 301 |
Redmine::MimeType.is_type?('audio', filename)
|
| 300 | 302 |
end |
| 301 | 303 | |
| 304 |
def markdownized_previewable? |
|
| 305 |
readable? && Redmine::Markdownizer.available? && Redmine::Markdownizer.supports?(filename) |
|
| 306 |
end |
|
| 307 | ||
| 308 |
def markdownized_preview_content |
|
| 309 |
return nil unless markdownized_previewable? |
|
| 310 | ||
| 311 |
target = markdownized_preview_cache_path |
|
| 312 |
if Redmine::Markdownizer.convert(diskfile, target) |
|
| 313 |
File.read(target, :mode => "rb") |
|
| 314 |
end |
|
| 315 |
rescue => e |
|
| 316 |
if logger |
|
| 317 |
logger.error( |
|
| 318 |
"An error occured while generating markdownized preview for #{disk_filename} " \
|
|
| 319 |
"to #{target}\nException was: #{e.message}"
|
|
| 320 |
) |
|
| 321 |
end |
|
| 322 |
nil |
|
| 323 |
end |
|
| 324 | ||
| 325 |
def markdownized_preview_cache_path |
|
| 326 |
File.join(self.class.markdownized_previews_storage_path, "#{digest}_#{filesize}.md")
|
|
| 327 |
end |
|
| 328 | ||
| 302 | 329 |
def previewable? |
| 303 | 330 |
is_text? || is_image? || is_video? || is_audio? |
| 304 | 331 |
end |
| ... | ... | |
| 530 | 557 |
Dir[thumbnail_path("*")].each do |thumb|
|
| 531 | 558 |
File.delete(thumb) |
| 532 | 559 |
end |
| 560 |
FileUtils.rm_f(markdownized_preview_cache_path) |
|
| 533 | 561 |
end |
| 534 | 562 | |
| 535 | 563 |
def thumbnail_path(size) |
| app/views/attachments/markdownized.html.erb | ||
|---|---|---|
| 1 |
<%= render :layout => 'layouts/file' do %> |
|
| 2 |
<%= render :partial => 'common/markup', |
|
| 3 |
:locals => {:markup_text_formatting => 'common_mark', :markup_text => @content} %>
|
|
| 4 |
<% end %> |
|
| config/configuration.yml.example | ||
|---|---|---|
| 237 | 237 |
# - example.org |
| 238 | 238 |
# - "*.example.com" |
| 239 | 239 | |
| 240 |
# Preview for Microsoft Office documents |
|
| 241 |
# |
|
| 242 |
# Absolute path (e.g. /usr/local/bin/markitdown) to the MarkItDown command |
|
| 243 |
# used to convert supported attachments to Markdown for preview. |
|
| 244 |
#markdown_converter_command: |
|
| 245 | ||
| 240 | 246 |
# specific configuration options for production environment |
| 241 | 247 |
# that overrides the default ones |
| 242 | 248 |
production: |
| config/locales/en.yml | ||
|---|---|---|
| 1335 | 1335 |
text_minimagick_available: MiniMagick available (optional) |
| 1336 | 1336 |
text_convert_available: ImageMagick convert available (optional) |
| 1337 | 1337 |
text_gs_available: ImageMagick PDF support available (optional) |
| 1338 |
text_markdown_converter_available: MarkItDown available (optional) |
|
| 1338 | 1339 |
text_default_active_job_queue_changed: Default queue adapter which is well suited only for dev/test changed |
| 1339 | 1340 |
text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
|
| 1340 | 1341 |
text_destroy_time_entries: Delete reported hours |
| config/locales/ja.yml | ||
|---|---|---|
| 1103 | 1103 |
notice_new_password_must_be_different: 新しいパスワードは現在のパスワードと異なるものでなければなりません |
| 1104 | 1104 |
setting_mail_handler_excluded_filenames: 除外する添付ファイル名 |
| 1105 | 1105 |
text_convert_available: ImageMagickのconvertコマンドが利用可能 (オプション) |
| 1106 |
text_markdown_converter_available: MarkItDownが利用可能 (オプション) |
|
| 1106 | 1107 |
label_link: リンク |
| 1107 | 1108 |
label_only: 次のもののみ |
| 1108 | 1109 |
label_drop_down_list: ドロップダウンリスト |
| doc/INSTALL | ||
|---|---|---|
| 19 | 19 |
* SCM binaries (e.g. svn, git...), for repository browsing (must be |
| 20 | 20 |
available in PATH) |
| 21 | 21 |
* ImageMagick (to enable Gantt export to png images) |
| 22 |
* MarkItDown (to enable preview for Microsoft Office documents) |
|
| 23 |
To install, run: `pip install 'markitdown[all]'` |
|
| 22 | 24 | |
| 23 | 25 |
Supported browsers: |
| 24 | 26 |
The current version of Firefox, Safari, Chrome, Chromium and Microsoft Edge. |
| lib/redmine/markdownizer.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006- Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 | ||
| 20 |
require 'fileutils' |
|
| 21 |
require 'tempfile' |
|
| 22 |
require 'timeout' |
|
| 23 | ||
| 24 |
module Redmine |
|
| 25 |
module Markdownizer |
|
| 26 |
extend Redmine::Utils::Shell |
|
| 27 | ||
| 28 |
COMMAND = (Redmine::Configuration['markdown_converter_command'] || 'markitdown').freeze |
|
| 29 |
MAX_PREVIEW_SIZE = 100.kilobytes |
|
| 30 |
SUPPORTED_EXTENSIONS = %w(.doc .docx .xls .xlsx .ppt .pptx).freeze |
|
| 31 | ||
| 32 |
def self.supports?(filename) |
|
| 33 |
SUPPORTED_EXTENSIONS.include?(File.extname(filename.to_s).downcase) |
|
| 34 |
end |
|
| 35 | ||
| 36 |
def self.convert(source, target) |
|
| 37 |
return nil unless available? |
|
| 38 |
return target if File.exist?(target) |
|
| 39 | ||
| 40 |
directory = File.dirname(target) |
|
| 41 |
FileUtils.mkdir_p(directory) |
|
| 42 |
cmd = "#{shell_quote COMMAND} #{shell_quote source}"
|
|
| 43 |
pid = nil |
|
| 44 |
output = Tempfile.new('markdownized-preview')
|
|
| 45 | ||
| 46 |
begin |
|
| 47 |
Timeout.timeout(Redmine::Configuration['thumbnails_generation_timeout'].to_i) do |
|
| 48 |
pid = Process.spawn(cmd, out: output.path) |
|
| 49 |
_, status = Process.wait2(pid) |
|
| 50 |
unless status.success? |
|
| 51 |
logger.error("Markdown conversion failed (#{status.exitstatus}):\nCommand: #{cmd}")
|
|
| 52 |
return nil |
|
| 53 |
end |
|
| 54 |
end |
|
| 55 |
rescue Timeout::Error |
|
| 56 |
Process.kill('KILL', pid) if pid
|
|
| 57 |
logger.error("Markdown conversion timed out:\nCommand: #{cmd}")
|
|
| 58 |
return nil |
|
| 59 |
rescue => e |
|
| 60 |
logger.error("Markdown conversion failed:\nCommand: #{cmd}\nException was: #{e.message}")
|
|
| 61 |
return nil |
|
| 62 |
ensure |
|
| 63 |
output.close |
|
| 64 |
end |
|
| 65 | ||
| 66 |
preview = File.binread(output.path, MAX_PREVIEW_SIZE + 1) || +"" |
|
| 67 |
File.binwrite(target, preview.byteslice(0, MAX_PREVIEW_SIZE)) |
|
| 68 |
target |
|
| 69 |
ensure |
|
| 70 |
output&.unlink |
|
| 71 |
end |
|
| 72 | ||
| 73 |
def self.available? |
|
| 74 |
return @available if defined?(@available) |
|
| 75 | ||
| 76 |
begin |
|
| 77 |
`#{shell_quote COMMAND} --version`
|
|
| 78 |
@available = $?.success? |
|
| 79 |
rescue |
|
| 80 |
@available = false |
|
| 81 |
end |
|
| 82 |
logger.warn("Markdown converter command (#{COMMAND}) not available") unless @available
|
|
| 83 |
@available |
|
| 84 |
end |
|
| 85 | ||
| 86 |
def self.logger |
|
| 87 |
Rails.logger |
|
| 88 |
end |
|
| 89 |
end |
|
| 90 |
end |
|
| test/functional/attachments_controller_test.rb | ||
|---|---|---|
| 250 | 250 |
assert_select '.nodata', :text => 'No preview available' |
| 251 | 251 |
end |
| 252 | 252 | |
| 253 |
def test_show_msword |
|
| 254 |
skip unless Redmine::Markdownizer.available? |
|
| 255 | ||
| 256 |
set_tmp_attachments_directory |
|
| 257 |
a = Attachment.new( |
|
| 258 |
:container => Issue.find(1), |
|
| 259 |
:file => uploaded_test_file( |
|
| 260 |
'msword.docx', |
|
| 261 |
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' |
|
| 262 |
), |
|
| 263 |
:author => User.find(1) |
|
| 264 |
) |
|
| 265 |
assert a.save |
|
| 266 | ||
| 267 |
get(:show, :params => {:id => a.id})
|
|
| 268 | ||
| 269 |
assert_response :success |
|
| 270 |
assert_equal 'text/html', @response.media_type |
|
| 271 |
assert_select 'div.filecontent.wiki', :text => /Redmine is a flexible project management web application/ |
|
| 272 |
assert_select '.nodata', :count => 0 |
|
| 273 |
end |
|
| 274 | ||
| 253 | 275 |
def test_show_other_with_no_preview |
| 254 | 276 |
@request.session[:user_id] = 2 |
| 255 | 277 |
get(:show, :params => {:id => 6})
|
| test/unit/attachment_test.rb | ||
|---|---|---|
| 528 | 528 |
assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable? |
| 529 | 529 |
end |
| 530 | 530 | |
| 531 |
def test_markdownized_previewable_should_be_true_for_supported_extensions |
|
| 532 |
skip unless Redmine::Markdownizer.available? |
|
| 533 | ||
| 534 |
attachment = Attachment.new( |
|
| 535 |
:container => Issue.find(1), |
|
| 536 |
:file => uploaded_test_file( |
|
| 537 |
"msword.docx", |
|
| 538 |
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" |
|
| 539 |
), |
|
| 540 |
:author => User.find(1) |
|
| 541 |
) |
|
| 542 |
assert attachment.save |
|
| 543 |
assert_equal true, attachment.markdownized_previewable? |
|
| 544 |
end |
|
| 545 | ||
| 546 |
def test_markdownized_previewable_should_be_false_for_non_supported_extensions |
|
| 547 |
skip unless Redmine::Markdownizer.available? |
|
| 548 | ||
| 549 |
attachment = Attachment.new( |
|
| 550 |
:container => Issue.find(1), |
|
| 551 |
:file => uploaded_test_file("testfile.txt", "text/plain"),
|
|
| 552 |
:author => User.find(1) |
|
| 553 |
) |
|
| 554 |
assert attachment.save |
|
| 555 |
assert_equal false, attachment.markdownized_previewable? |
|
| 556 |
end |
|
| 557 | ||
| 558 |
def test_delete_from_disk_should_delete_markdownized_preview_cache |
|
| 559 |
attachment = Attachment.create!( |
|
| 560 |
:container => Issue.find(1), |
|
| 561 |
:file => uploaded_test_file( |
|
| 562 |
"msword.docx", |
|
| 563 |
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" |
|
| 564 |
), |
|
| 565 |
:author => User.find(1) |
|
| 566 |
) |
|
| 567 |
preview = attachment.markdownized_preview_cache_path |
|
| 568 |
FileUtils.mkdir_p(File.dirname(preview)) |
|
| 569 |
File.write(preview, "preview") |
|
| 570 |
assert File.exist?(preview) |
|
| 571 | ||
| 572 |
attachment.send(:delete_from_disk!) |
|
| 573 |
assert_not File.exist?(preview) |
|
| 574 |
end |
|
| 575 | ||
| 531 | 576 |
if convert_installed? |
| 532 | 577 |
def test_thumbnail_should_generate_the_thumbnail |
| 533 | 578 |
set_fixtures_attachments_directory |
- « Previous
- 1
- 2
- 3
- Next »