当たり前と思われがちだけど、とても便利なRedmineのファイル添付機能の内側(Attachment 編)


原田です。季節は秋になっているはずですけど、まだまだ暑いですね。今も外履きはサンダルで過ごしている私ですけど、そろそろ靴を履くべきか悩んでいます。できればもう少しこの暑さを楽しんでいたいですね。

Redmineはファイルが添付できます

今更何を言っているのかと思われる方もいらっしゃるでしょうけど、添付ファイル機能は地味ですがとても便利です。短い文章だけでは伝えることが難しい内容も、画像や各種ファイルを組み合わせることで自分の思いを伝えることができます。

今回は添付ファイルを管理している Attachment についてご紹介します。

Enter ".help" for usage hints.
sqlite> .header on
sqlite> .mode column
sqlite> .width 10 20
sqlite> pragma table_info('attachments');
cid         name                  type        notnull     dflt_value  pk
----------  --------------------  ----------  ----------  ----------  ----------
0           id                    integer     1                       1
1           container_id          integer     0                       0
2           container_type        varchar(30  0                       0
3           filename              varchar     1           ''          0
4           disk_filename         varchar     1           ''          0
5           filesize              integer(8)  1           0           0
6           content_type          varchar     0           ''          0
7           digest                varchar(64  1           ''          0
8           downloads             integer     1           0           0
9           author_id             integer     1           0           0
10          created_on            datetime    0                       0
11          description           varchar     0                       0
12          disk_directory        varchar     0                       0

attachmentsテーブルの構造

Attachment とは

Attachment は、Redmineの公式サイト上では2006年6月29日の リビジョン 4 が初登場ですけど、2006年6月25日にリリースされたRedmine 0.1.0には既に含まれていたのではないかと思われます。当時のAttachmentは80行程度のシンプルなものでしたが、2019年9月時点の 最新版 では500行を超え多機能になっていると想像できます。

さまざまな画面でファイルが添付できます

Redmineは チケット・Wiki・文書・ニュース・フォーラム で添付ファイルを扱うことができます。Attachmentとこれらの画面で使われているモデルとの連携はAttachmentに定義されている belongs_to :container, :polymorphic => trueacts_as_attachable を利用し実現しています(acts_as_attachableに関しては別の機会にご紹介したいと思います)。

class Attachment < ActiveRecord::Base
  include Redmine::SafeAttributes
  belongs_to :container, :polymorphic => true
  belongs_to :author, :class_name => "User"

Attachment#container 宣言部(app/models/attachment.rb)

class Issue < ActiveRecord::Base
  ...
  has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
  has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all

  acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed

acts_as_attachable 宣言部(app/models/issue.rb)

同じファイルのアップロードは同一のものとして扱います

添付ファイルは各画面上からアップロードすると[Redmineインストールディレクトリ] / filesディレクトリ下に保存されます。

Redmineを使用していると同じファイルを上記のさまざまな画面から複数回アップロード(添付)することもあり得ると思います。Attachmentではafter_commitコールバック実行時にAttachment#reuse_existing_file_if_possible を呼び出し同じファイルは同一のものとして扱い複数保存することを防いでいます。これは同一ファイルを再利用しハードディスクの使用量を減らすためにRedmine 3.4.0で追加された機能(Redmine.org - Patch #25215: Re-use existing identical disk files for new attachments)です。

class Attachment < ActiveRecord::Base
  ...
  after_commit :reuse_existing_file_if_possible, :on => :create
  ...

  private

  def reuse_existing_file_if_possible
    original_diskfile = nil

    reused = with_lock do
      if existing = Attachment
                      .where(digest: self.digest, filesize: self.filesize)
                      .where('id <> ? and disk_filename <> ?',
                             self.id, self.disk_filename)
                      .first
        existing.with_lock do

          original_diskfile = self.diskfile
          existing_diskfile = existing.diskfile

          if File.readable?(original_diskfile) &&
            File.readable?(existing_diskfile) &&
            FileUtils.identical?(original_diskfile, existing_diskfile)

            self.update_columns disk_directory: existing.disk_directory,
                                disk_filename: existing.disk_filename
          end
        end
      end
    end
    if reused
      File.delete(original_diskfile)
    end
  rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
    # Catch and ignore lock errors. It is not critical if deduplication does
    # not happen, therefore we do not retry.
    # with_lock throws ActiveRecord::RecordNotFound if the record isnt there
    # anymore, thats why this is caught and ignored as well.
  end

同一ファイルの再利用処理(再利用時はアップロードしたファイルを削除)

サムネイルも作成可能です

画像ファイルのサムネイルを表示する機能(Redmine.org - Feature #3510: Inserting image thumbnails inside the wiki)とPDFファイルのサムネイル表示する機能(Redmine.org - Feature #22481: Show thumbnails for PDF attachments)がRedmineに組み込まれたことにより、添付ファイルが画像またはPDFであればサムネイルの作成が可能になりました。


チケット画面:サムネイルの表示

添付ファイルをクラウド環境に保存するには

クラウドを利用してRedmineを動かしているという記事を見る機会が増えました。クラウド環境としてはAmazon Web Services(AWS)を利用されている方が多いようです。Redmineはオンプレで稼働するけど、ファイルの損失を防ぐため「添付ファイルはクラウドに保存したい」という要望もあるかと思います。

そこでRedmineのプラグインを探してみたところ、 Redmine S3 であればクラウドストレージサービス(Amazon S3)に添付ファイルの保存ができるようです。


Redmine S3:README
Redmine S3 のプログラムコードを確認しましたが、このプラグインは Redmine 2.1 までにしか対応していないようです。最新のRedmineでこのプラグインを利用するのなら足らない機能の追加やAWS-SDK Ver1からVer3への変更等の見直しが必要になると思いました。これはプラグインを継続して提供するための宿命でしょうけど、 Attachment のバージョンアップにも追従しなければならない点を考慮するとメンテナンスの面から見ると大変かもしれません。


My Redmine

こちらの記事もオススメです!
あらためて感心したRedmineのデータ利用方法(UserPrefernce 編)
Redmineの中で定義されているモデル「UserPreference」のようなデータの持ち方は、Redmineをもっと便利にすると思います。
台湾最大オープンソースイベントCOSCUPは熱かった!
IT先進国台湾で開催されたオープンソースソフトウェアのイベントに参加してきました。
仕事をしながら深圳大学に1ヶ月留学してきた記録
中国の深圳大学に語学留学して、中国語を4週間学んできました。授業の様子と滞在中の生活を紹介。
ファーエンドテクノロジーに入社して2ヶ月が経ちました
7月にファーエンドテクノロジーに入社したサポート担当の福田です。よろしくお願いします。
社員旅行の段取りをRedmineで行いました
Redmineのwiki、フォーラム、ニュース、バージョンなどの機能を活用して、社員旅行の段取りを行いました。
ファーエンドテクノロジーからのお知らせ(2019/10/01更新)
2019年10月 オライリー本の全冊公開日のお知らせ(もくもく勉強会も同時開催)
ファーエンドテクノロジーが所蔵するオライリー本(全冊)公開日のご案内です。公開日には「もくもく勉強会」も同時開催します。
ファーエンドテクノロジーの岩石と吉岡がRubyWorld Conference2019に登壇します
11月7日(木)〜8日(金)に島根県松江市で開催される「RubyWorld Conference 2019」に弊社の岩石と吉岡が登壇します。
「FAREND NEWS」2019年第5号 発行
広報紙「FAREND NEWS」2019年第5号を発行しました。弊社サービスの運用・サポートに携わっているスタッフや弊社の取り組みをご紹介します。
RubyWorld Conference 2019 (11/7・8開催) にPlatinumスポンサーとして協賛
11月7日(木)〜8日(金)に島根県松江市で開催される「RubyWorld Conference 2019」にPlatinumスポンサーとして協賛します。
Redmineの最新情報をメールでお知らせする「Redmine News」配信中
新バージョンやセキュリティ修正のリリース情報、そのほか最新情報を迅速にお届け