🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Redmine

Redmineは、プロジェクトのタスク管理、進捗管理、情報共有が可能な、 オープンソースプロジェクト管理ソフトウェアです。

Q&A

解決済

4回答

14045閲覧

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

otake

総合スコア8

Redmine

Redmineは、プロジェクトのタスク管理、進捗管理、情報共有が可能な、 オープンソースプロジェクト管理ソフトウェアです。

0グッド

1クリップ

投稿2020/01/30 17:49

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

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

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

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

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

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

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

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

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

SQL

1SELECT 2 db_redmine.issues.id AS チケットID 3 , db_redmine.attachments.disk_filename AS 物理ファイル名 4 , db_redmine.attachments.id AS 添付ファイルID 5FROM 6 db_redmine.issues 7 INNER JOIN db_redmine.issue_statuses 8 ON db_redmine.issue_statuses.id = db_redmine.issues.status_id 9 LEFT JOIN db_redmine.attachments 10 ON db_redmine.issues.id = db_redmine.attachments.container_id 11 LEFT OUTER JOIN ( 12 SELECT 13 db_redmine.issues.id 14 , db_redmine.attachments.disk_filename 15 FROM 16 db_redmine.issues 17 INNER JOIN db_redmine.issue_statuses 18 ON db_redmine.issue_statuses.id = db_redmine.issues.status_id 19 LEFT JOIN db_redmine.attachments 20 ON db_redmine.issues.id = db_redmine.attachments.container_id 21 WHERE 22 db_redmine.issue_statuses.is_closed = 0 23 AND db_redmine.issues.project_id = 3 24 UNION 25 SELECT 26 db_redmine.issues.id 27 , db_redmine.attachments.disk_filename 28 FROM 29 db_redmine.issues 30 INNER JOIN db_redmine.issue_statuses 31 ON db_redmine.issue_statuses.id = db_redmine.issues.status_id 32 LEFT JOIN db_redmine.attachments 33 ON db_redmine.issues.id = db_redmine.attachments.container_id 34 WHERE 35 db_redmine.issue_statuses.is_closed = 1 36 AND db_redmine.issues.project_id = 3 37 AND db_redmine.issues.closed_on >= (Now() - INTERVAL 6 MONTH) 38 ) Query1 39 ON db_redmine.attachments.disk_filename = Query1.disk_filename 40WHERE 41 db_redmine.attachments.disk_filename IS NOT NULL 42 AND Query1.disk_filename IS NULL 43 AND db_redmine.issue_statuses.is_closed = 1 44 AND db_redmine.issues.project_id = 3 45 AND db_redmine.issues.closed_on < (Now() - INTERVAL 6 MONTH) 46ORDER BY 47 db_redmine.issues.id 48 , db_redmine.attachments.disk_filename

投稿2020/02/03 11:37

編集2020/02/03 11:38
takashikawai

総合スコア193

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

otake

2020/02/03 16:25

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

0

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

jquery

1//パスのパターン:/projects/.*/settings 2//挿入位置:全ページのヘッダ 3//種別:JavaScript 4 5var admin = ViewCustomize.context.user.admin; 6var apikey = ViewCustomize.context.user.apiKey; 7var closedissueList = []; //添付ファイル削除対象のチケット 8var deletefileList = []; //削除対象の添付ファイル 9 10$(function(){ 11 if (admin == true){ 12 //「設定」画面 =>「情報」タブ => 「名称」の後ろに「添付ファイル検索・削除」ボタンを追加 13 var target = $('input#project_name').parent(); 14 var searchbtn = '&nbsp;' + '<input type="button" id="searchfile" value="添付ファイル検索">'; 15 var deletebtn = '&nbsp;' + '<input type="button" id="deletefile" value="添付ファイル削除">'; 16 $(target).append(searchbtn).append(deletebtn); 17 $('input#deletefile').attr("disabled", true).css("color", "#fff").css("background-color", "#d6d6d6"); //削除ボタンを一時的に無効化 18 19 //検索ボタンを押下したら、サーバから添付ファイル情報を取得する 20 $('input#searchfile').click(function(){ 21 //配列を初期化 22 closedissueList = []; 23 deletefileList = []; 24 25 $('input#deletefile').attr("disabled", true).css("color", "#fff").css("background-color", "#d6d6d6"); 26 $.when( 27 getclosedissueid(), 28 ).done(function(){ 29 getdeletefile(); 30 }) 31 }) 32 33 //削除ボタンを押下したら、サーバからファイルを削除する 34 $('input#deletefile').click(function(){ 35 var result = confirm('削除処理を続けますか?'); 36 if(result) { 37 closeissueattfiledelete(); 38 $('input#deletefile').attr("disabled", true).css("color", "#fff").css("background-color", "#d6d6d6"); 39 } else { 40 return false; 41 } 42 }) 43 } 44}) 45 46//6か月前の日付を取得 47function getbeforesixmonth(dt){ 48 dt.setMonth(dt.getMonth() - 6); 49 var year = dt.getFullYear(); 50 var month = ('0' + (dt.getMonth()+1)).slice(-2); 51 var day = ('0' + dt.getDate()).slice(-2); 52 var beforesixmonth = year+"-"+month+"-"+day; 53 return beforesixmonth; 54} 55 56//対象プロジェクトのステータスが終了(完了)で、終了日が6か月以前のチケットを取得 57function getclosedissueid(){ 58 var deferred = new $.Deferred(); 59 var projectid = $('input#project_identifier').attr('value'); //プロジェクト識別子 60 var dt = new Date(); 61 62 $.ajax({ 63 type: "GET", 64 url: '/issues.json?limit=1000&project_id=' + projectid + '&status_id=closed&closed_on=%3C%3D' + getbeforesixmonth(dt), 65 headers: { 66 'X-Redmine-API-Key': apiKey 67 }, 68 dataType: "text", 69 contentType: 'application/json', 70 }).done(function(data){ 71 data=JSON.parse(data); 72 data= data["issues"]; 73 for (i in data) { 74 issueid=data[i].id; 75 closedissueList.push(issueid); 76 } 77 setTimeout(function() { 78 deferred.resolve(); 79 }, 300); 80 }) 81 return deferred; 82} 83 84//チケットのリストに基づいて、削除対象ファイルの情報を取得 85function getdeletefile(){ 86 $.each(closedissueList, function(i){ 87 $.ajax({ 88 type: "GET", 89 url: '/issues/' + closedissueList[i] + '.json?include=attachments', 90 headers: { 91 'X-Redmine-API-Key': apiKey 92 }, 93 dataType: "text", 94 contentType: 'application/json', 95 }).done(function(data){ 96 data=JSON.parse(data); 97 issue_id = data.issue.id; //チケットID 98 data = data.issue.attachments; 99 for (i in data){ 100 fileid = data[i].id; //添付ファイルID 101 filename = data[i].filename; //添付ファイル名 102 deletefileList.push({issue_id, fileid, filename}); 103 console.log(deletefileList); 104 if (deletefileList.length > 0){ 105 $('input#deletefile').attr("disabled", false).css("color", "#555").css("background-color", "#f0f0f0"); //情報を取得したら削除ボタンを有効化 106 } 107 } 108 }) 109 }) 110} 111 112//サーバから添付ファイルを削除する 113function closeissueattfiledelete(){ 114 $.each(deletefileList, function(i){ 115 $.ajax({ 116 type: "DELETE", 117 url: '/attachments/' + deletefileList[i].fileid + '.json', 118 headers: { 119 'X-Redmine-API-Key': apiKey 120 }, 121 dataType: "text", 122 contentType: 'application/json', 123 }).done(function(){ 124 console.log("チケット[" + deletefileList[i].issue_id + "]の添付ファイル[" + deletefileList[i].filename + "]を削除しました"); 125 }).fail(function(){ 126 alert("失敗しました"); 127 }) 128 }) 129} 130

投稿2020/02/03 03:13

takashikawai

総合スコア193

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

otake

2020/02/03 16:33

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

0

ベストアンサー

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

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

sql

1SELECT 2 db_redmine.issues.id 3 , db_redmine.issue_statuses.is_closed 4 , db_redmine.issues.closed_on 5 , db_redmine.attachments.disk_filename 6 , db_redmine.attachments.filename 7FROM 8 db_redmine.issues 9 INNER JOIN db_redmine.issue_statuses 10 ON db_redmine.issue_statuses.id = db_redmine.issues.status_id 11 INNER JOIN db_redmine.attachments 12 ON db_redmine.attachments.id = db_redmine.issues.id 13 INNER JOIN ( 14 SELECT DISTINCT 15 db_redmine.attachments.disk_filename 16 FROM 17 db_redmine.attachments 18 ) Query1 19 ON db_redmine.attachments.disk_filename = Query1.disk_filename 20WHERE 21 db_redmine.issue_statuses.is_closed = 1 22 AND db_redmine.issues.closed_on < (Now() - INTERVAL 6 MONTH)

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

投稿2020/01/31 10:10

takashikawai

総合スコア193

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

otake

2020/02/01 01:38

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

2020/02/02 03:52

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

0

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

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

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

Ruby

1 def render_api_attachment_attributes(attachment, api) 2 api.id attachment.id 3 api.filename attachment.filename 4 api.filesize attachment.filesize 5 api.content_type attachment.content_type 6 api.description attachment.description 7 api.content_url download_named_attachment_url(attachment, attachment.filename) 8 api.disk_filename attachment.disk_filename #この行を追加する 9 if attachment.thumbnailable? 10 api.thumbnail_url thumbnail_url(attachment) 11 end

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

jQuery

1var admin = ViewCustomize.context.user.admin; 2var apikey = ViewCustomize.context.user.apiKey; 3var closedissueList = []; //添付ファイル削除対象のチケット 4var deletefileList = []; //削除対象の添付ファイル 5var openandundersixissueList = []; //添付ファイル削除対象外のチケット 6var notdeletefileList = []; //削除対象外の添付ファイル 7var resultdeletefileList = []; //実際に削除する添付ファイル 8 9$(function(){ 10 if (admin == true){ 11 //「設定」画面 =>「情報」タブ => 「名称」の後ろに「添付ファイル検索・削除」ボタンを追加 12 var target = $('input#project_name').parent(); 13 var searchbtn = '&nbsp;' + '<input type="button" id="searchfile" value="添付ファイル検索">'; 14 var deletebtn = '&nbsp;' + '<input type="button" id="deletefile" value="添付ファイル削除">'; 15 $(target).append(searchbtn).append(deletebtn); 16 $('input#deletefile').attr("disabled", true).css("color", "#fff").css("background-color", "#d6d6d6"); //削除ボタンを一時的に無効化 17 18 //検索ボタンを押下したら、サーバから添付ファイル情報を取得する 19 $('input#searchfile').click(function(){ 20 //配列を初期化 21 closedissueList = []; 22 deletefileList = []; 23 openandundersixissueList = []; 24 notdeletefileList = []; 25 resultdeletefileList = []; 26 27 $('input#deletefile').attr("disabled", true).css("color", "#fff").css("background-color", "#d6d6d6"); 28 $.when( 29 getclosedissueid(), 30 getclosedundersixmonthissueid(), 31 getopenissueid() 32 ).done(function(){ 33 getdeletefile(); 34 getnotdeletefile(); 35 setTimeout(function() { 36 $('input#deletefile').attr("disabled", false).css("color", "#555").css("background-color", "#f0f0f0"); //情報を取得したら削除ボタンを有効化 37 }, 300); 38 }) 39 }) 40 41 //削除ボタンを押下したら、サーバからファイルを削除する 42 $('input#deletefile').click(function(){ 43 $.when( 44 pushresultfile() 45 ).done(function(){ 46 if (resultdeletefileList.length == 0){ 47 alert("削除対象のファイルはありません"); 48 }else{ 49 closeissueattfiledelete(); 50 } 51 }) 52 }) 53 } 54}) 55 56//6か月前の日付を取得 57function getbeforesixmonth(dt){ 58 dt.setMonth(dt.getMonth() - 6); 59 var year = dt.getFullYear(); 60 var month = ('0' + (dt.getMonth()+1)).slice(-2); 61 var day = ('0' + dt.getDate()).slice(-2); 62 var beforesixmonth = year+"-"+month+"-"+day; 63 return beforesixmonth; 64} 65 66//対象プロジェクトのステータスが終了(完了)で、終了日が6か月以前のチケットを取得 67function getclosedissueid(){ 68 var deferred = new $.Deferred(); 69 var projectid = $('input#project_identifier').attr('value'); //プロジェクト識別子 70 var dt = new Date(); 71 72 $.ajax({ 73 type: "GET", 74 url: '/issues.json?limit=1000&project_id=' + projectid + '&status_id=closed&closed_on=%3C%3D' + getbeforesixmonth(dt), 75 headers: { 76 'X-Redmine-API-Key': apiKey 77 }, 78 dataType: "text", 79 contentType: 'application/json', 80 }).done(function(data){ 81 data=JSON.parse(data); 82 data= data["issues"]; 83 for (i in data) { 84 issueid=data[i].id; 85 closedissueList.push(issueid); 86 } 87 setTimeout(function() { 88 deferred.resolve(); 89 }, 300); 90 }) 91 return deferred; 92} 93 94//対象プロジェクトのステータスが終了(完了)で、終了日が6か月未満のチケットを取得 95function getclosedundersixmonthissueid(){ 96 var deferred = new $.Deferred(); 97 var projectid = $('input#project_identifier').attr('value'); //プロジェクト識別子 98 var dt = new Date(); 99 100 $.ajax({ 101 type: "GET", 102 url: '/issues.json?limit=1000&project_id=' + projectid + '&status_id=closed&closed_on=%3E%3C' + getbeforesixmonth(dt), 103 headers: { 104 'X-Redmine-API-Key': apiKey 105 }, 106 dataType: "text", 107 contentType: 'application/json', 108 }).done(function(data){ 109 data=JSON.parse(data); 110 data= data["issues"]; 111 for (i in data) { 112 issueid=data[i].id; 113 openandundersixissueList.push(issueid); 114 } 115 setTimeout(function() { 116 deferred.resolve(); 117 }, 300); 118 }) 119 return deferred; 120} 121 122//対象プロジェクトのステータスが未完了のチケットを取得 123function getopenissueid(){ 124 var deferred = new $.Deferred(); 125 var projectid = $('input#project_identifier').attr('value'); //プロジェクト識別子 126 var dt = new Date(); 127 128 $.ajax({ 129 type: "GET", 130 url: '/issues.json?limit=1000&project_id=' + projectid, 131 headers: { 132 'X-Redmine-API-Key': apiKey 133 }, 134 dataType: "text", 135 contentType: 'application/json', 136 }).done(function(data){ 137 data=JSON.parse(data); 138 data= data["issues"]; 139 for (i in data) { 140 issueid=data[i].id; 141 openandundersixissueList.push(issueid); 142 } 143 setTimeout(function() { 144 deferred.resolve(); 145 }, 300); 146 }) 147 return deferred; 148} 149 150//チケットのリストに基づいて、削除対象ファイルの情報を取得 151function getdeletefile(){ 152 $.each(closedissueList, function(i){ 153 $.ajax({ 154 type: "GET", 155 url: '/issues/' + closedissueList[i] + '.json?include=attachments', 156 headers: { 157 'X-Redmine-API-Key': apiKey 158 }, 159 dataType: "text", 160 contentType: 'application/json', 161 }).done(function(data){ 162 data=JSON.parse(data); 163 issue_id = data.issue.id; //チケットID 164 data = data.issue.attachments; 165 for (i in data){ 166 fileid = data[i].id; //添付ファイルID 167 diskfilename = data[i].disk_filename; //サーバ上の物理ファイル名 168 deletefileList.push({issue_id, fileid, diskfilename}); 169 //console.log(deletefileList); 170 } 171 }) 172 }) 173} 174 175//チケットのリストに基づいて、削除対象外のファイル情報を取得 176function getnotdeletefile(){ 177 $.each(openandundersixissueList, function(i){ 178 $.ajax({ 179 type: "GET", 180 url: '/issues/' + openandundersixissueList[i] + '.json?include=attachments', 181 headers: { 182 'X-Redmine-API-Key': apiKey 183 }, 184 dataType: "text", 185 contentType: 'application/json', 186 }).done(function(data){ 187 data=JSON.parse(data); 188 data = data.issue.attachments; 189 for (i in data){ 190 diskfilename = data[i].disk_filename; //サーバ上の物理ファイル名 191 notdeletefileList.push(diskfilename); 192 //console.log(notdeletefileList); 193 } 194 }) 195 }) 196} 197 198//削除対象と削除対象外を比較し、削除対象外に存在しなかったら削除対象を結果配列にpush 199function pushresultfile(){ 200 var deferred = new $.Deferred(); 201 var a = deletefileList; 202 if (a.length == 0){ 203 deferred.resolve(); //削除対象の添付ファイルがない場合は処理を終了 204 } 205 var b = notdeletefileList; 206 for (var i = 0; i < deletefileList.length; i++) { 207 if ($.inArray(a[i]["diskfilename"], b) < 0) { 208 resultdeletefileList.push(deletefileList[i]); 209 } 210 console.log(resultdeletefileList); 211 setTimeout(function() { 212 deferred.resolve(); 213 }, 300); 214 } 215 return deferred; 216} 217 218//サーバから添付ファイルを削除する 219function closeissueattfiledelete(){ 220 $.each(resultdeletefileList, function(i){ 221 $.ajax({ 222 type: "DELETE", 223 url: '/attachments/' + resultdeletefileList[i].fileid + '.json', 224 headers: { 225 'X-Redmine-API-Key': apiKey 226 }, 227 dataType: "text", 228 contentType: 'application/json', 229 }).done(function(){ 230 console.log("チケット[" + resultdeletefileList[i].issue_id + "]の添付ファイル[" + resultdeletefileList[i].diskfilename + "]を削除しました"); 231 }).fail(function(){ 232 alert("失敗しました"); 233 }) 234 }) 235} 236

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

投稿2020/02/02 03:51

編集2020/02/03 03:16
takashikawai

総合スコア193

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

takashikawai

2020/02/03 03:14 編集

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

2020/02/03 03:11

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問