0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターの拡張機能だけで添付ファイルのリネームを実現する

0
Last updated at Posted at 2026-08-19

はじめに

プリザンターの添付ファイル項目はファイルをアップロードした時点のファイル名がそのまま保持されます。「ファイル名を後から付け直したい」「分かりやすい名前に揃えたい」というときも、編集画面の UI からはリネームできず、いったん削除して同名で再アップロードするしかありません。

今回は拡張スクリプト拡張サーバスクリプト拡張SQL だけを使って、添付ファイル項目に「リネーム」ボタンを追加し、ファイル名を後から変更できるようにしてみます。テーブルごとの設定変更が要らないため、すべてのテーブルにまとめて適用できます。

バージョン 1.5.4.0 を対象にしています

添付ファイル項目の仕組みを見てみる

モデルに保存される JSON

添付ファイル項目(AttachmentsAAttachmentsZ。Enterprise Edition の項目拡張を使っている場合は Attachments001〜 も)は、レコードの列に下記のような JSON 配列として保存されています。

[
  {
    "Guid": "31DE9B93C26342D186646E723D7EB8E1",
    "Name": "report_202703.xlsx",
    "Size": 11904,
    "HashCode": "xjugT/ALXg+G5dcjgs6CTPZ7DAeFDTXnZ8kavI8AQZY="
  }
]

Name プロパティが画面に表示されるファイル名です。Guid は添付ファイルそのものを指す ID で、/binaries/{Guid}/download でファイルを取得できます。

ファイル実体の保存先

実ファイルは Binaries テーブル(または Binaries テーブル + ローカルフォルダ)に保存されています。ダウンロード時のファイル名は、本体コード(FileContentResults.cs)の Bytes メソッドで Binaries.FileNamefileDownloadName に渡しているため、モデル JSON 側の Name ではなく Binaries.FileName が使われます

return new ResponseFile(
    fileContent: new MemoryStream(bin, false),
    fileDownloadName: dataRow.String("FileName"),
    contentType: contentType);

つまり「リネーム」を完成させるには、

  1. モデル JSON の Name(編集画面・一覧画面の表示名)
  2. Binaries.FileNameBinaries.Title(ダウンロード時のファイル名)

の両方を書き換える必要があります。

実現方針

両者を整合させるため、次の流れで実装します。

モデル JSON の更新は $p.apiUpdate だけで完結します。Binaries テーブルの同期は、レコード更新後に毎回自動で動く**拡張SQL(OnUpdated)**に任せます。こうすると、拡張機能から SQL を直接呼び出す必要がなく、本体の標準フローに乗せられます。

拡張SQL でBinariesテーブルを同期する

まず、レコード更新後に Binaries.FileNameBinaries.Title をモデル JSON の Name に合わせる拡張SQL を作ります。

App_Data/Parameters/ExtendedSqls/ に以下の 2 ファイルを配置します。

ExtendedSqls/SyncAttachmentFileName.json
{
    "Description": "Syncs Binaries.FileName/Title with the attachment Name in the model JSON.",
    "Controllers": ["items"],
    "OnUpdated": true,
    "CommandText": "-- loaded from .json.sql"
}

Controllers: ["items"] でユーザがアクセスするテーブル系画面の更新だけを対象にし、OnUpdated で更新の直後に SQL を実行します。

ExtendedSqls/SyncAttachmentFileName.json.sql
update "Binaries"
set "FileName" = sub."Name",
    "Title" = sub."Name"
from (
    select
        upper(jsonb_array_elements(coalesce(nullif(t.col, '')::jsonb, '[]'::jsonb))->>'Guid') as "Guid",
        jsonb_array_elements(coalesce(nullif(t.col, '')::jsonb, '[]'::jsonb))->>'Name' as "Name"
    from (
        select unnest(array[
            "AttachmentsA","AttachmentsB","AttachmentsC","AttachmentsD","AttachmentsE",
            "AttachmentsF","AttachmentsG","AttachmentsH","AttachmentsI","AttachmentsJ",
            "AttachmentsK","AttachmentsL","AttachmentsM","AttachmentsN","AttachmentsO",
            "AttachmentsP","AttachmentsQ","AttachmentsR","AttachmentsS","AttachmentsT",
            "AttachmentsU","AttachmentsV","AttachmentsW","AttachmentsX","AttachmentsY",
            "AttachmentsZ"
        ]) as col
        from "Results"
        where "SiteId" = {{SiteId}} and "ResultId" = {{Id}}
        union all
        select unnest(array[
            "AttachmentsA","AttachmentsB","AttachmentsC","AttachmentsD","AttachmentsE",
            "AttachmentsF","AttachmentsG","AttachmentsH","AttachmentsI","AttachmentsJ",
            "AttachmentsK","AttachmentsL","AttachmentsM","AttachmentsN","AttachmentsO",
            "AttachmentsP","AttachmentsQ","AttachmentsR","AttachmentsS","AttachmentsT",
            "AttachmentsU","AttachmentsV","AttachmentsW","AttachmentsX","AttachmentsY",
            "AttachmentsZ"
        ]) as col
        from "Issues"
        where "SiteId" = {{SiteId}} and "IssueId" = {{Id}}
    ) as t
) as sub
where upper("Binaries"."Guid") = sub."Guid"
  and ("Binaries"."FileName" <> sub."Name" or "Binaries"."Title" <> sub."Name");

更新中のレコードを {{SiteId}}{{Id}} のプレースホルダで絞り込み、その JSON 配列を jsonb_array_elements で展開して Guid → Name の対応表に変換し、Binaries テーブルを更新しています。union allResultsIssues の両方をカバーしているので、テーブル種別(記録テーブル/期限付きテーブル)にかかわらず動作します。

上記は PostgreSQL の例です。SQL Server の場合は jsonb_array_elements の代わりに OPENJSONunnest(array[…]) の代わりに CROSS APPLY (VALUES (…)) v(col) を使うなどの読み替えが必要です。

拡張SQL はレコード更新の直後に毎回実行されます。where 句で "FileName" <> sub."Name" のように差分があるときだけ UPDATE するようにして、無駄な更新を避けています。

拡張スクリプトでリネームボタンを追加する

次に、編集画面の各添付ファイル項目に「リネーム」ボタンを追加し、クリックで $p.apiUpdate を呼び出すスクリプトを作ります。

拡張スクリプトとして App_Data/Parameters/ExtendedScripts/ に配置します。

ExtendedScripts/RenameAttachment.js
(function () {
  /**
   * 添付ファイル項目の表示テキスト「ファイル名 (サイズ)」からファイル名のみを抽出する。
   */
  function ra_splitNameAndSize(text) {
    var match = String(text).match(/^([\s\S]*?)([\s\u3000]*[((][^()()]+[))])\s*$/);
    return match
      ? { name: match[1], suffix: match[2] }
      : { name: text, suffix: '' };
  }

  /**
   * 添付ファイル項目の要素から、サイトID・レコードID・項目名・ファイルGUIDを取得する。
   */
  function ra_resolveContext($item) {
    var $container = $item.closest('.control-attachments-items');
    var containerId = $container.attr('id') || '';
    var columnName = containerId.replace(/\.items$/, '');
    var $hidden = $('[id$="_' + columnName + '"]').filter('input[type=hidden]').first();
    if (!$hidden.length) return null;
    var prefix = $hidden.attr('id').replace('_' + columnName, '');
    var recordId = $('#' + prefix + '_' + (prefix === 'Issues' ? 'IssueId' : 'ResultId')).val()
      || $p.getControl(prefix === 'Issues' ? 'IssueId' : 'ResultId');
    return {
      siteId: $p.siteId(),
      recordId: recordId,
      columnName: columnName,
      guid: ($item.attr('id') || '').toUpperCase(),
      hidden: $hidden
    };
  }

  /**
   * リネーム処理本体。
   */
  function ra_rename(item) {
    var $item = $(item);
    var $link = $item.find('a.file-name').filter(function () {
      return $(this).text().length > 0;
    }).last();
    var parts = ra_splitNameAndSize($link.text());
    var currentName = parts.name;

    var newName = window.prompt('新しいファイル名を入力してください', currentName);
    if (newName === null) return;
    newName = newName.replace(/[\\/:*?"<>|]/g, '_').trim();
    if (!newName || newName === currentName) return;

    var ctx = ra_resolveContext($item);
    if (!ctx || !ctx.recordId) {
      alert('レコードIDが取得できませんでした(新規作成中はリネームできません)');
      return;
    }

    // 現在のリストを取得して該当ファイルだけ Name を更新する
    var list;
    try { list = JSON.parse(ctx.hidden.val() || '[]'); } catch (e) { list = []; }
    var found = false;
    list.forEach(function (f) {
      if (String(f.Guid || '').toUpperCase() === ctx.guid) {
        f.Name = newName;
        found = true;
      }
    });
    if (!found) {
      alert('対象ファイルが見つかりませんでした。画面を再読込してから試してください。');
      return;
    }

    var hash = {};
    hash[ctx.columnName] = list;
    $p.apiUpdate({
      id: ctx.recordId,
      data: { AttachmentsHash: hash, ApiVersion: 1.1 },
      done: function () {
        // 編集中のフォームにも反映し、画面表示を更新する
        ctx.hidden.val(JSON.stringify(list));
        $link.text(newName + parts.suffix);
        $p.clearMessage();
        $p.setMessage('#Message', JSON.stringify({
          Css: 'alert-success',
          Text: 'ファイル名を「' + newName + '」に変更しました'
        }));
      },
      fail: function () {
        $p.clearMessage();
        $p.setMessage('#Message', JSON.stringify({
          Css: 'alert-error',
          Text: 'ファイル名の変更に失敗しました'
        }));
      }
    });
  }

  /**
   * 添付ファイル項目にリネームボタンを追加する。
   */
  function ra_decorate() {
    $('.control-attachments-items .control-attachments-item.already-attachments').each(function () {
      var $item = $(this);
      if ($item.find('.rename-file').length) return;
      var $btn = $('<div class="ui-icon ui-icon-pencil rename-file" title="ファイル名を変更"></div>')
        .css({ cursor: 'pointer', display: 'inline-block' })
        .on('click', function (e) {
          e.preventDefault();
          e.stopPropagation();
          ra_rename($item[0]);
        });
      var $delete = $item.find('.delete-file').first();
      if ($delete.length) {
        $delete.before($btn);
      } else {
        $item.append($btn);
      }
    });
  }

  // 編集画面表示時に装飾する
  $p.events.on_editor_load = function () { ra_decorate(); };

  // 追加アップロード後などにも再装飾する
  $(document).ajaxComplete(function () { setTimeout(ra_decorate, 0); });
})();

スクリプトの解説

  • すべての関数名に ra_Rename Attachment)プレフィックスを付けて、他の拡張スクリプトとの名前衝突を避けています
  • ra_resolveContext で、クリックされた添付ファイル要素から「サイトID・レコードID・項目名(AttachmentsA など)・ファイル GUID・フォームの hidden 要素」を一括で取得しています
  • 入力されたファイル名は Windows・Linux で禁止文字となる \ / : * ? " < > |_ に置換しています(本体コード Attachment.OnDeserializedFiles.ValidateFileName と同じ方針)
  • 既存ファイルの Name だけを書き換えた JSON を AttachmentsHash として $p.apiUpdate に渡します。Added / Deleted を付けていないので本体は SQL を書き込まず、モデル JSON だけが更新されます
  • 更新が成功したら、編集中フォームの hidden 値と画面上のリンクテキストも同期させ、保存忘れによる「画面と DB の食い違い」を防いでいます
  • レコード更新の直後に拡張SQL SyncAttachmentFileNameOnUpdated で発火し、Binaries.FileNameBinaries.Title を同じ値に揃えます

新規作成画面(レコード未保存)ではレコード ID が存在しないため、リネームできるのは保存済みの添付ファイルだけです。新規アップロード時のファイル名は OS 上のファイル名がそのまま使われるので、アップロード前にリネームしておきましょう。

動作確認

実際にテーブルを用意して動作を確認してみましょう。

1. 添付ファイル項目にリネームボタンが追加される

編集画面を開くと、アップロード済みファイルの行に鉛筆アイコンの「リネーム」ボタン(ui-icon-pencil)が表示されます。削除ボタンの左隣に配置されます。

2. ファイル名を変更する

リネームボタンをクリックすると、現在のファイル名が入った入力ダイアログが表示されます。新しい名前を入力して OK を押すと、$p.apiUpdate でモデル JSON が更新され、画面のファイル名表示が即座に切り替わります。

3. ダウンロード時のファイル名も変わっている

更新直後に拡張SQL が走り、Binaries.FileName も同期されています。リネーム後にファイルをダウンロードすると、保存ダイアログに新しいファイル名が表示されることが確認できます。

4. SQL でも確認してみる

PostgreSQL であれば、pgAdmin などで以下のクエリを実行するとモデル JSON と Binaries テーブルが同じファイル名になっていることを確認できます。

select b."Guid", b."FileName", b."Title"
from "Binaries" b
where b."Guid" = '31de9b93c26342d186646e723d7eb8e1';

まとめ

拡張スクリプト$p.apiUpdate拡張SQL の3点セットで、添付ファイル項目のファイル名を後から変更する仕組みを実装しました。

  • 拡張スクリプトで添付ファイル項目に「リネーム」ボタンを追加し、$p.apiUpdateAttachmentsHash でモデル JSON の Name を書き換える
  • 拡張SQL の OnUpdatedBinaries.FileNameBinaries.Title を同期し、ダウンロード時のファイル名にも反映する
  • いずれも全テーブル横断で動作するため、テーブルごとに管理画面で個別設定する必要がない
  • 禁止文字(\ / : * ? " < > |)はクライアント側で _ に置換し、本体のバリデーションと整合させる

「あとからファイル名を直したい」というニーズは意外と多いものです。拡張機能だけで完結できるので、本体改修なしに導入できます。皆さんも是非試してみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?