質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.78%

Redmineの添付ファイル削除について

解決済

回答 4

投稿

  • 評価
  • クリップ 1
  • VIEW 4,195

score 8

ご存知の方もいると思いますが、Redmineの添付ファイルは、
[Redmineインストールディレクトリ] / filesディレクトリに保存されます。

多くのユーザーが長期間使用していると、チケット数も添付ファイル数も膨大となり、
ステータスが終了(完了)した不要なチケットを削除する方もいると思います。

ステータスが終了(完了)してから一定期間経過したチケットを、バッチ処理により、
DBテーブル上のチケット情報をSQLで削除したいのですが、ここで問題となるのが
添付ファイルです。

Redmineの添付ファイルは、容量節約のため、異なるチケットで同じファイルが
アップロードされると、物理的なファイルは一つで、両チケットが同じファイル名を
共有する形で保存されます。

そのためバッチ処理によりチケットを削除するとき、それに紐づく添付ファイルも
消したいのですが、別チケットで同ファイルを共有してる可能性が出てきます。

もちろん手作業で時間をかければ、他に共有してるチケットがいないことを
判別可能だと思いますが、これをバッチ処理で判断して消すには、何かよい方法があるでしょうか。

以上、よろしくお願い致します。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 4

checkベストアンサー

+1

attachmentsテーブルのdisk_filename(物理的なファイル名)が重複している場合は削除しないという条件にすればできませんか。

例えば、以下のように「ステータスが終了(完了)」「終了してから6か月経過」「disk_filenameが重複していない」の条件を満たすレコードを取得して、

SELECT
  db_redmine.issues.id
  , db_redmine.issue_statuses.is_closed
  , db_redmine.issues.closed_on
  , db_redmine.attachments.disk_filename
  , db_redmine.attachments.filename 
FROM
  db_redmine.issues 
  INNER JOIN db_redmine.issue_statuses 
    ON db_redmine.issue_statuses.id = db_redmine.issues.status_id 
  INNER JOIN db_redmine.attachments 
    ON db_redmine.attachments.id = db_redmine.issues.id 
  INNER JOIN ( 
    SELECT DISTINCT
      db_redmine.attachments.disk_filename 
    FROM
      db_redmine.attachments
  ) Query1 
    ON db_redmine.attachments.disk_filename = Query1.disk_filename 
WHERE
  db_redmine.issue_statuses.is_closed = 1 
  AND db_redmine.issues.closed_on < (Now() - INTERVAL 6 MONTH)


取得したファイル名(filename)だけを削除すれば良いのかなと。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/02/01 10:38

    ご回答ありがとうございました。
    いただいたSQLを参考に、バッチプログラムを組んでみたいと思います。

    キャンセル

  • 2020/02/02 12:52

    View Customize pluginのスクリプトを作ってみました。

    キャンセル

+1

前回のスクリプトは使わないで、こちらを使ってみてください。

//パスのパターン:/projects/.*/settings
//挿入位置:全ページのヘッダ
//種別:JavaScript

var admin = ViewCustomize.context.user.admin;
var apikey = ViewCustomize.context.user.apiKey;
var closedissueList = []; //添付ファイル削除対象のチケット
var deletefileList = []; //削除対象の添付ファイル

$(function(){
  if (admin == true){
    //「設定」画面 =>「情報」タブ => 「名称」の後ろに「添付ファイル検索・削除」ボタンを追加 
    var target = $('input#project_name').parent();
    var searchbtn = '&nbsp;' + '<input type="button" id="searchfile" value="添付ファイル検索">';
    var deletebtn = '&nbsp;' + '<input type="button" id="deletefile" value="添付ファイル削除">';
    $(target).append(searchbtn).append(deletebtn);
    $('input#deletefile').attr("disabled", true).css("color", "#fff").css("background-color", "#d6d6d6"); //削除ボタンを一時的に無効化

    //検索ボタンを押下したら、サーバから添付ファイル情報を取得する
    $('input#searchfile').click(function(){
      //配列を初期化 
      closedissueList = [];
      deletefileList = []; 

      $('input#deletefile').attr("disabled", true).css("color", "#fff").css("background-color", "#d6d6d6");  
      $.when(
        getclosedissueid(),
      ).done(function(){ 
        getdeletefile();
      })
    })

    //削除ボタンを押下したら、サーバからファイルを削除する
    $('input#deletefile').click(function(){
      var result = confirm('削除処理を続けますか?');
      if(result) {
        closeissueattfiledelete();
        $('input#deletefile').attr("disabled", true).css("color", "#fff").css("background-color", "#d6d6d6"); 
      } else {
        return false;
      }                    
    })   
  }
})

//6か月前の日付を取得
function getbeforesixmonth(dt){
  dt.setMonth(dt.getMonth() - 6);
  var year = dt.getFullYear();
  var month = ('0' + (dt.getMonth()+1)).slice(-2);
  var day = ('0' + dt.getDate()).slice(-2);
  var beforesixmonth = year+"-"+month+"-"+day;
  return beforesixmonth;
}

//対象プロジェクトのステータスが終了(完了)で、終了日が6か月以前のチケットを取得
function getclosedissueid(){
  var deferred = new $.Deferred();
  var projectid = $('input#project_identifier').attr('value'); //プロジェクト識別子
  var dt = new Date();

  $.ajax({ 
    type: "GET",
    url: '/issues.json?limit=1000&project_id=' + projectid + '&status_id=closed&closed_on=%3C%3D' + getbeforesixmonth(dt),
    headers: {
    'X-Redmine-API-Key': apiKey
    },
    dataType: "text",
    contentType: 'application/json',
  }).done(function(data){
    data=JSON.parse(data);
    data= data["issues"];
    for (i in data) {  
      issueid=data[i].id;      
      closedissueList.push(issueid);
    }
    setTimeout(function() {    
      deferred.resolve();
    }, 300);
  })
  return deferred;
}

//チケットのリストに基づいて、削除対象ファイルの情報を取得
function getdeletefile(){
  $.each(closedissueList, function(i){ 
    $.ajax({ 
      type: "GET",
      url: '/issues/' + closedissueList[i] + '.json?include=attachments', 
      headers: {
      'X-Redmine-API-Key': apiKey
      },
      dataType: "text",
      contentType: 'application/json',
    }).done(function(data){
      data=JSON.parse(data);
      issue_id = data.issue.id; //チケットID      
      data = data.issue.attachments;
      for (i in data){
        fileid = data[i].id; //添付ファイルID
        filename = data[i].filename; //添付ファイル名  
        deletefileList.push({issue_id, fileid, filename});        
        console.log(deletefileList);
        if (deletefileList.length > 0){
          $('input#deletefile').attr("disabled", false).css("color", "#555").css("background-color", "#f0f0f0"); //情報を取得したら削除ボタンを有効化
        }
      }       
    })
  })
}

//サーバから添付ファイルを削除する
function closeissueattfiledelete(){
  $.each(deletefileList, function(i){ 
    $.ajax({
      type: "DELETE",
      url: '/attachments/' + deletefileList[i].fileid + '.json',
      headers: {
      'X-Redmine-API-Key': apiKey
      },
      dataType: "text",
      contentType: 'application/json',
    }).done(function(){
      console.log("チケット[" + deletefileList[i].issue_id + "]の添付ファイル[" + deletefileList[i].filename + "]を削除しました");
    }).fail(function(){
      alert("失敗しました"); 
    })    
  })
}

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/02/04 01:33

    jqueryでのやり方、ありがとうございます。
    こういうやり方もあるのですね。参考にさせていただきます。
    ただ今回は、画面からではなくバッチファイルを作成し、統合運用管理システムより
    自動起動させることを考えております。そのためどちらかというと作成いただいたSQLのほうを
    メインとして使わせていただきたいと思います。
    ありがとうございました。

    キャンセル

+1

削除対象ではないチケットの添付ファイルと比較して、その中に重複している物理ファイルがないかどうかを調べないといけないですね。前回作成したのは、削除対象のチケットの中だけで重複しているかという処理になってしまっていたので、修正してみました。
===> 修正したSQLです(MySQL)。

SELECT
  db_redmine.issues.id AS チケットID
  , db_redmine.attachments.disk_filename AS 物理ファイル名
  , db_redmine.attachments.id AS 添付ファイルID 
FROM
  db_redmine.issues 
  INNER JOIN db_redmine.issue_statuses 
    ON db_redmine.issue_statuses.id = db_redmine.issues.status_id 
  LEFT JOIN db_redmine.attachments 
    ON db_redmine.issues.id = db_redmine.attachments.container_id 
  LEFT OUTER JOIN ( 
    SELECT
      db_redmine.issues.id
      , db_redmine.attachments.disk_filename 
    FROM
      db_redmine.issues 
      INNER JOIN db_redmine.issue_statuses 
        ON db_redmine.issue_statuses.id = db_redmine.issues.status_id 
      LEFT JOIN db_redmine.attachments 
        ON db_redmine.issues.id = db_redmine.attachments.container_id 
    WHERE
      db_redmine.issue_statuses.is_closed = 0 
      AND db_redmine.issues.project_id = 3 
    UNION 
    SELECT
      db_redmine.issues.id
      , db_redmine.attachments.disk_filename 
    FROM
      db_redmine.issues 
      INNER JOIN db_redmine.issue_statuses 
        ON db_redmine.issue_statuses.id = db_redmine.issues.status_id 
      LEFT JOIN db_redmine.attachments 
        ON db_redmine.issues.id = db_redmine.attachments.container_id 
    WHERE
      db_redmine.issue_statuses.is_closed = 1 
      AND db_redmine.issues.project_id = 3 
      AND db_redmine.issues.closed_on >= (Now() - INTERVAL 6 MONTH)
  ) Query1 
    ON db_redmine.attachments.disk_filename = Query1.disk_filename 
WHERE
  db_redmine.attachments.disk_filename IS NOT NULL 
  AND Query1.disk_filename IS NULL 
  AND db_redmine.issue_statuses.is_closed = 1 
  AND db_redmine.issues.project_id = 3 
  AND db_redmine.issues.closed_on < (Now() - INTERVAL 6 MONTH) 
ORDER BY
  db_redmine.issues.id
  , db_redmine.attachments.disk_filename

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/02/04 01:25

    何度もご支援いただきありがとうございます。
    幸い利用者が使い始めたばかりのため、削除対象となるようなチケットはなく、
    管理側としては多少時間があるため、いただいた情報をもとに、
    削除の仕組みを作りこみたいと思います。
    ありがとうございました。

    キャンセル

0

Redmineの画面上で、Rest APIを使って処理を作成してみました。この方法だと、チケットの履歴にも添付ファイル削除の記録が残ります。

サーバ管理者でログインして、「設定」画面 =>「情報」タブで動作させるようにしました。

1)../redmine/app/helpers/attachments_helper.rb に処理を追加

  def render_api_attachment_attributes(attachment, api)
    api.id attachment.id
    api.filename attachment.filename
    api.filesize attachment.filesize
    api.content_type attachment.content_type
    api.description attachment.description
    api.content_url download_named_attachment_url(attachment, attachment.filename)
    api.disk_filename attachment.disk_filename #この行を追加する
   if attachment.thumbnailable?
      api.thumbnail_url thumbnail_url(attachment)
    end


2)View Customize pluginでスクリプトを登録
パスのパターン:/projects/.*/settings
挿入位置:全ページのヘッダ
種別:JavaScript

var admin = ViewCustomize.context.user.admin;
var apikey = ViewCustomize.context.user.apiKey;
var closedissueList = []; //添付ファイル削除対象のチケット
var deletefileList = []; //削除対象の添付ファイル
var openandundersixissueList = []; //添付ファイル削除対象外のチケット 
var notdeletefileList = []; //削除対象外の添付ファイル
var resultdeletefileList = []; //実際に削除する添付ファイル

$(function(){
  if (admin == true){
    //「設定」画面 =>「情報」タブ => 「名称」の後ろに「添付ファイル検索・削除」ボタンを追加 
    var target = $('input#project_name').parent();
    var searchbtn = '&nbsp;' + '<input type="button" id="searchfile" value="添付ファイル検索">';
    var deletebtn = '&nbsp;' + '<input type="button" id="deletefile" value="添付ファイル削除">';
    $(target).append(searchbtn).append(deletebtn);
    $('input#deletefile').attr("disabled", true).css("color", "#fff").css("background-color", "#d6d6d6"); //削除ボタンを一時的に無効化

    //検索ボタンを押下したら、サーバから添付ファイル情報を取得する
    $('input#searchfile').click(function(){
      //配列を初期化 
      closedissueList = [];
      deletefileList = []; 
      openandundersixissueList = []; 
      notdeletefileList = [];
      resultdeletefileList = []; 

      $('input#deletefile').attr("disabled", true).css("color", "#fff").css("background-color", "#d6d6d6");  
      $.when(
        getclosedissueid(),
        getclosedundersixmonthissueid(),
        getopenissueid()   
      ).done(function(){ 
        getdeletefile();
        getnotdeletefile(); 
        setTimeout(function() {  
          $('input#deletefile').attr("disabled", false).css("color", "#555").css("background-color", "#f0f0f0"); //情報を取得したら削除ボタンを有効化 
        }, 300);       
      })
    })

    //削除ボタンを押下したら、サーバからファイルを削除する
    $('input#deletefile').click(function(){
      $.when(
        pushresultfile() 
      ).done(function(){ 
        if (resultdeletefileList.length == 0){
          alert("削除対象のファイルはありません");
        }else{  
          closeissueattfiledelete();
        }                 
      })    
    })   
  }
})

//6か月前の日付を取得
function getbeforesixmonth(dt){
  dt.setMonth(dt.getMonth() - 6);
  var year = dt.getFullYear();
  var month = ('0' + (dt.getMonth()+1)).slice(-2);
  var day = ('0' + dt.getDate()).slice(-2);
  var beforesixmonth = year+"-"+month+"-"+day;
  return beforesixmonth;
}

//対象プロジェクトのステータスが終了(完了)で、終了日が6か月以前のチケットを取得
function getclosedissueid(){
  var deferred = new $.Deferred();
  var projectid = $('input#project_identifier').attr('value'); //プロジェクト識別子
  var dt = new Date();

  $.ajax({ 
    type: "GET",
    url: '/issues.json?limit=1000&project_id=' + projectid + '&status_id=closed&closed_on=%3C%3D' + getbeforesixmonth(dt),
    headers: {
    'X-Redmine-API-Key': apiKey
    },
    dataType: "text",
    contentType: 'application/json',
  }).done(function(data){
    data=JSON.parse(data);
    data= data["issues"];
    for (i in data) {  
      issueid=data[i].id;      
      closedissueList.push(issueid);
    }
    setTimeout(function() {    
      deferred.resolve();
    }, 300);
  })
  return deferred;
}

//対象プロジェクトのステータスが終了(完了)で、終了日が6か月未満のチケットを取得
function getclosedundersixmonthissueid(){
  var deferred = new $.Deferred();
  var projectid = $('input#project_identifier').attr('value'); //プロジェクト識別子
  var dt = new Date();

  $.ajax({ 
    type: "GET",
    url: '/issues.json?limit=1000&project_id=' + projectid + '&status_id=closed&closed_on=%3E%3C' + getbeforesixmonth(dt),
    headers: {
    'X-Redmine-API-Key': apiKey
    },
    dataType: "text",
    contentType: 'application/json',
  }).done(function(data){
    data=JSON.parse(data);
    data= data["issues"];
    for (i in data) {  
      issueid=data[i].id;      
      openandundersixissueList.push(issueid);
    }
    setTimeout(function() {    
      deferred.resolve();
    }, 300);
  })
  return deferred;
}

//対象プロジェクトのステータスが未完了のチケットを取得
function getopenissueid(){
  var deferred = new $.Deferred();
  var projectid = $('input#project_identifier').attr('value'); //プロジェクト識別子
  var dt = new Date();

  $.ajax({ 
    type: "GET",
    url: '/issues.json?limit=1000&project_id=' + projectid,
    headers: {
    'X-Redmine-API-Key': apiKey
    },
    dataType: "text",
    contentType: 'application/json',
  }).done(function(data){
    data=JSON.parse(data);
    data= data["issues"];
    for (i in data) {  
      issueid=data[i].id;      
      openandundersixissueList.push(issueid);
    }
    setTimeout(function() {    
      deferred.resolve();
    }, 300);
  })
  return deferred;
}

//チケットのリストに基づいて、削除対象ファイルの情報を取得
function getdeletefile(){
  $.each(closedissueList, function(i){ 
    $.ajax({ 
      type: "GET",
      url: '/issues/' + closedissueList[i] + '.json?include=attachments', 
      headers: {
      'X-Redmine-API-Key': apiKey
      },
      dataType: "text",
      contentType: 'application/json',
    }).done(function(data){
      data=JSON.parse(data);
      issue_id = data.issue.id; //チケットID      
      data = data.issue.attachments;
      for (i in data){
        fileid = data[i].id; //添付ファイルID
        diskfilename = data[i].disk_filename; //サーバ上の物理ファイル名  
        deletefileList.push({issue_id, fileid, diskfilename});        
        //console.log(deletefileList); 
      }       
    })
  })
}

//チケットのリストに基づいて、削除対象外のファイル情報を取得
function getnotdeletefile(){
  $.each(openandundersixissueList, function(i){ 
    $.ajax({ 
      type: "GET",
      url: '/issues/' + openandundersixissueList[i] + '.json?include=attachments', 
      headers: {
      'X-Redmine-API-Key': apiKey
      },
      dataType: "text",
      contentType: 'application/json',
    }).done(function(data){
      data=JSON.parse(data);
      data = data.issue.attachments;
      for (i in data){
        diskfilename = data[i].disk_filename; //サーバ上の物理ファイル名  
        notdeletefileList.push(diskfilename); 
        //console.log(notdeletefileList); 
      }       
    })
  })
}

//削除対象と削除対象外を比較し、削除対象外に存在しなかったら削除対象を結果配列にpush
function pushresultfile(){
  var deferred = new $.Deferred();   
  var a = deletefileList;
  if (a.length == 0){
    deferred.resolve(); //削除対象の添付ファイルがない場合は処理を終了 
  }
  var b = notdeletefileList; 
  for (var i = 0; i < deletefileList.length; i++) {
    if ($.inArray(a[i]["diskfilename"], b) < 0) {
      resultdeletefileList.push(deletefileList[i]);
    }
    console.log(resultdeletefileList); 
    setTimeout(function() {    
     deferred.resolve();
    }, 300); 
  }     
  return deferred;
}

//サーバから添付ファイルを削除する
function closeissueattfiledelete(){
  $.each(resultdeletefileList, function(i){ 
    $.ajax({
      type: "DELETE",
      url: '/attachments/' + resultdeletefileList[i].fileid + '.json',
      headers: {
      'X-Redmine-API-Key': apiKey
      },
      dataType: "text",
      contentType: 'application/json',
    }).done(function(){
      console.log("チケット[" + resultdeletefileList[i].issue_id + "]の添付ファイル[" + resultdeletefileList[i].diskfilename + "]を削除しました");
    }).fail(function(){
      alert("失敗しました"); 
    })    
  })
}


プログラムは本職ではないので、おかしなところがあるかもしれません。一応動作確認はしましたが、必要なファイルが削除されるかもしれませんので、気を付けて使ってください。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/02/02 13:10 編集

    いま気が付いたのですが、考え方に間違いがありました。削除対象ではないチケットの添付ファイルと比較して、その中に重複している物理ファイルがないかどうかを調べないといけないですね。
    前回作成したのは、削除対象のチケットの中だけで重複しているかという処理になってしまっていたので、修正してみました。

    キャンセル

  • 2020/02/03 12:11

    さらに気が付きましたが、こんな面倒なことをする必要がありませんでした。添付時に同じファイルを共有しているので、削除の際も他のチケットで使われていれば削除しないようになっていました。ですので、削除用のスクリプトは物理ファイルの重複をチェックする必要がないです。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.78%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る