以前にLouis Moreauxさんの記事を読んでCKEditor5で画像の貼り付けをできるようにしました。以下の記事です。
CKEditor5による画像アップロードを実装する(1) - 準備CKEditor5による画像アップロードを実装する(2) - REST APIの作成
CKEditor5による画像アップロードを実装する(3) - 完成
Allow group APEXObjectManagers to read buckets in compartment APEX
Allow group APEXObjectManagers to manage objects in compartment APEX
APEXからOCIオブジェクト・ストレージを操作する(1) - APIユーザーの作成
さらにLouis MoreauxさんはCKEditor5 Image Upload - Part 3として、画像をデータベースではなくオブジェクト・ストレージに保存する方法を記事として公開しています。
CKEditor5に画像を含めると、文書にを表示する際に含まれている画像を同時にダウンロードするようです。そのため、この機能を組み込んだAPEXアプリケーションをリソース制限の厳しいAlways FreeのAutonomous Database上で動かすと、データベースのセッション数の上限に達してしまいます。
少し残念だったので、画像をオブジェクト・ストレージに保存するように変更して、少ないリソースでもアプリが使えるかどうか確認してみます。
以下は、事前に実施する準備作業です。
ユーザー、グループ、ポリシーなどの作成を行います。事前承認リスエストを生成するため、バケットの権限はreadではなくmanageが必要です。
Allow group APEXObjectManagers to read buckets in compartment APEX
Allow group APEXObjectManagers to manage objects in compartment APEX
APEXからOCIオブジェクト・ストレージを操作する(1) - APIユーザーの作成
バケットはapex_file_storageの代わりにimagesを作成します。
OCI側の準備は以上になります。これから画像をオブジェクト・ストレージに保存するように、アプリケーションに変更を加えます。
RESTサービスのPOSTハンドラの変更
画像のアップロードを受け付けて、BLOBに保存している処理をオブジェクト・ストレージに保存するように変更します。CKEditor5に戻すURLとしてRESTサービスのGETハンドラではなく、オブジェクト・ストレージにアップロードした画像のURLを返します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
l_id rte_images.id%type; | |
C_BASE_URL constant varchar2(80) := 'https://objectstorage.us-ashburn-1.oraclecloud.com/n/ネームスペース名/b/images/o/'; | |
l_request_url varchar2(32767); | |
l_response clob; | |
file_upload_error exception; | |
begin | |
/* | |
* 画像はオブジェクト・ストレージにアップロードする。ここでは単にIDを取得する。 | |
* :content_typeはORDSが設定してくれている。 | |
* :current_userはAPEXセッションによる保護をかけた場合、APP_USERになる。 | |
*/ | |
insert into rte_images(content_type, created_by, created) | |
values(:content_type, :current_user, sysdate) | |
returning id into l_id; | |
/* | |
* アップロードされてきた画像をオブジェクト・ストレージにアップロードする。 | |
*/ | |
l_request_url := C_BASE_URL || l_id; | |
apex_web_service.clear_request_headers; | |
apex_web_service.set_request_headers('Content-Type', :content-type, p_reset => false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => l_request_url | |
,p_http_method => 'PUT' | |
,p_body_blob => :body | |
,p_credential_static_id => 'OCI_API_ACCESS' | |
); | |
if apex_web_service.g_status_code != 200 then | |
raise file_upload_error; | |
end if; | |
:status_code := 201; | |
/* | |
* アップロードに成功した場合は、urlとしてダウンロードするリンクをJSON形式で返す。 | |
*/ | |
apex_json.open_object; | |
apex_json.write( | |
p_name => 'url', | |
p_value => l_request_url | |
); | |
apex_json.close_object; | |
exception | |
when others then | |
/* エラーを返す。 | |
* { | |
* "error": { | |
* "message": "Something wrong!" | |
* } | |
* } | |
*/ | |
apex_json.open_object; | |
apex_json.open_object('error'); | |
apex_json.write('message', 'Something wrong!'); | |
apex_json.close_object; | |
apex_json.close_object; | |
end; |
ネームスペース名は適切な値に置き換えます。
置換文字列の準備
コード中で使用する各種パラメータを置換文字列として定義します。
G_OBJECT_STORAGE_URLとして、以下を設定します。リージョン名、ネームスペース名、バケット名の値を置き換えます。
https://objectstorage.リージョン名.oraclecloud.com/n/ネームスペース名/b/バケット名/o/
ネームスペース名のハードコードを避けるために、置換文字列G_NAMESPACEとしてネームスペース名を設定します。
事前承認リクエストの発行
画像をアップロードするときはクリデンシャルを指定していますが、画像を表示する(ダウンロードする)ときはクリデンシャルを指定する方法がありません。バケットの可視性をプライベートにした上で画像をダウンロードするために、事前承認リクエストを発行します。
事前承認リクエストのパスを保存するアプリケーション・アイテムG_PREAUTH_URLを作成します。
名前はG_PREAUTH_URL、有効範囲はアプリケーション、セキュリティのセッション・ステート保護は一番厳しい制限付き - ブラウザからの設定不可を選びます。
このアプリケーション・アイテムに値を設定するアプリケーション・アイテムの計算を作成します。
事前承認リクエストのURLを取得するために、オブジェクト・ストレージのREST APIを呼び出します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
C_BASE_URL constant varchar2(200) := 'https://objectstorage.us-ashburn-1.oraclecloud.com/n/' || :G_NAMESPACE || '/b/images/p/'; | |
l_request clob; | |
l_response clob; | |
l_response_json json_object_t; | |
l_access_uri varchar2(400); | |
begin | |
select json_object( | |
key 'accessType' value 'AnyObjectRead', | |
key 'name' value 'standard-' || sys_guid(), | |
key 'timeExpires' value systimestamp + interval '1' day | |
) into l_request from dual; | |
apex_web_service.clear_request_headers; | |
apex_web_service.set_request_headers('Accept', 'application/json', p_reset => false); | |
apex_web_service.set_request_headers('Content-Type', 'application/json', p_reset => false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => C_BASE_URL | |
,p_http_method => 'POST' | |
,p_body => l_request | |
,p_credential_static_id => 'OCI_API_ACCESS' | |
); | |
l_response_json := json_object_t(l_response); | |
l_access_uri := l_response_json.get_string('accessUri'); | |
return 'https://objectstorage.us-ashburn-1.oraclecloud.com' || l_access_uri; | |
end; |
計算アイテムとしてG_PREAUTH_URLを選択します。頻度の計算ポイントは認証後を選択します。計算タイプはファンクション本体、計算として上記のコードを記述します。
対話モード・レポートのソース変更
対話モード・レポートのソースのSQL問合せを以下のSELECT文に変更します。
select
ID
, TITLE
-- , CONTENT
, replace(CONTENT, :G_OBJECT_STORAGE_URL, :G_PREAUTH_URL) content
, AUTHOR
, PUBLISHED
, CREATED
, CREATED_BY
, UPDATED
, UPDATED_BY
from RTE_DOCUMENTS
以前はapex_session=の引数を画像URLに付加し、APEXセッションでORDSへのGETリクエストの認証を通していました。この部分を、事前承認済リクエストのURLに置き換えています。
editingDowncastコンバーターの変更
CKEditor5内で画像を表示するときは、editingDowncastコンバーターを構成し、画像URLを見つけてapex_session引数を追加していました。今回はその代わりに、見つけた画像URLを事前承認済リクエストのURLに置き換えています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function(options){ | |
// 画像を扱うプラグインを有効化する。 | |
options.editorOptions.extraPlugins.push( | |
CKEditor5.image.Image, | |
CKEditor5.image.ImageResize, | |
CKEditor5.image.AutoImage, | |
CKEditor5.link.LinkImage, | |
CKEditor5.upload.SimpleUploadAdapter | |
); | |
// 画像とは関係しないが、コード・エディタも有効化する。 | |
options.editorOptions.codeBlock = { | |
languages: [ | |
{ language: 'javascript', label: 'JavaScript', class: 'js javascript js-code' } | |
] | |
}; | |
/* | |
* 画像のアップローダーを構成する。 | |
* | |
* uploadUrlとしてimageUrlつまり、ORDSのREST APIを指定している。 | |
* withCredentialsをtrueにして認証を有効にし、 | |
* Apex-Sessionヘッダーとともに、APEXのアプリケーションIDとセッションIDを送信している。 | |
*/ | |
options.editorOptions.simpleUpload = { | |
uploadUrl: imageUrl, | |
withCredentials: true, | |
headers: { | |
'Apex-Session': apexSession | |
} | |
}; | |
// ツールバーに画像をアップロードするボタンを追加する。 | |
let toolbar = options.editorOptions.toolbar; | |
toolbar.push("uploadImage"); | |
options.editorOptions.toolbar = toolbar; | |
// 画像のツールバーを構成する。 | |
options.editorOptions.image.toolbar = [ | |
'imageStyle:inline', | |
'imageStyle:wrapText', | |
'imageStyle:breakText', | |
'|', | |
'imageResize', | |
'|', | |
'toggleImageCaption', | |
'imageTextAlternative', | |
'|', | |
'LinkImage' | |
]; | |
/* | |
* 編集画面に適用されるeditingDowncastコンバーターを構成する。 | |
* CKEditorにはmodelとviewという考え方があり、MarkdownやHTMLはview、それをCKEditorはmodelに変換して取り込む。 | |
* データの編集はmodelに実施する。upcastはMarkdownやHTMLを取り込み、modelに変換する処理、 | |
* downcastはmodelからMarkdownやHTMLを生成する処理で、editingDowncastはmodelを編集するキャンパスに表示する | |
* 際に適用される。 | |
* | |
* editingDowncastを使って、画像URLが見つかったら(イメージを見つけるとattribute:srcのイベントが発生する)、 | |
* そのイベントに渡されるdata.attributeNewValueが画像のURLなので、そのURLを事前承認済みリクエストのURLに | |
* 置き換えている。 | |
*/ | |
options.executeOnInitialization = editor => { | |
editor.conversion.for( 'editingDowncast' ).add( dispatcher => { | |
dispatcher.on( | |
'attribute:src', | |
( evt, data, conversionApi ) => { | |
if ( !conversionApi.consumable.test( data.item, 'attribute:src' ) ) { | |
return; | |
} | |
if ( data.attributeNewValue ) { | |
if ( data.attributeNewValue.startsWith("&G_OBJECT_STORAGE_URL.")) { | |
data.attributeNewValue = data.attributeNewValue.replace("&G_OBJECT_STORAGE_URL.","&G_PREAUTH_URL."); | |
} | |
} | |
}, | |
{ priority: 'high' } | |
); | |
} ); | |
} | |
return options; | |
} |
ページ・アイテムP2_CONTENTの詳細のJavaScript初期化コードを入れ替えます。
画像の紐付けの変更
ドキュメントに含まれるイメージの参照先が、G_IMAGE_URLからG_OBJECT_STORAGE_URLに変更されています。文書と画像を紐づけているプロセス画像の紐付けのPL/SQLコードを以下に変更します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
l_link varchar2(32767); | |
l_id varchar2(80); | |
i pls_integer; | |
l_images apex_t_varchar2; | |
begin | |
/* | |
* アップロードされたCLOBに含まれている画像URLを見つけ、 | |
* 表RTE_IMAGESのDOCUMENT_IDに、現在編集中のドキュメントIDを設定する。 | |
*/ | |
i := 1; | |
while true | |
loop | |
-- 画像URLを見つける。 | |
l_link := regexp_substr(:P2_CONTENT, :G_OBJECT_STORAGE_URL || '([0-9]+)',1,i); | |
if l_link is null then | |
-- なければ終了。 | |
exit; | |
else | |
-- あれば画像URLよりID部分を取り出す。 | |
i := i + 1; | |
l_id := regexp_replace(l_link, :G_OBJECT_STORAGE_URL || '([0-9]+)', '\1'); | |
apex_string.push(l_images,l_id); | |
end if; | |
end loop; | |
-- インサートの場合は表CKE_IMAGESに指定したドキュメントIDの列はない。なので、効果があるのはアップデート時のみ。 | |
update rte_images set document_id = null where document_id = :P2_ID; | |
-- CLOBデータに含まれている画像IDのDOCUMENT_IDを、現在作成または更新中の表RTE_DOCUMENTSのIDにする。 | |
-- アップロードされた画像はドキュメント間で共有されることはないため、これで問題ない。 | |
update rte_images set document_id = :P2_ID where id in (select column_value from table(l_images)); | |
/* | |
* 文章を記述中に画像を貼り付けた後に削除した場合、その画像は表RTE_IMAGESに | |
* DOCUMENT_IDがNULLの状態で残る。この画像については、定期的に実行するバッチなどで消去する | |
* 必要がある。 | |
*/ | |
end; |
外部キー制約の変更
表RTE_IMAGESには外部キー制約RTE_IMAGES_DOCUMENT_ID_FKが設定されていて、親である表RTE_DOCUMENTSから文書が削除されると、CASCADE設定により含まれる画像も削除されるようになっていました。オブジェクト・ストレージに保存した画像は自動的には保存されないため、CASCADEからON DELETE SET NULLに変更します。
alter table "RTE_IMAGES" drop constraint "RTE_IMAGES_DOCUMENT_ID_FK";
alter table "RTE_IMAGES" add constraint "RTE_IMAGES_DOCUMENT_ID_FK"
foreign key ("DOCUMENT_ID") references "RTE_DOCUMENTS" ("ID") on delete set null;
表RTE_IMAGESの列DOCUMENT_IDがNULLの画像は、親となる文書が存在していません。定期的なハウスキーピングのジョブにて、行の削除と同時にオブジェクト・ストレージ上の画像の削除を行なうようにするよ良いでしょう。
以上でCKEditor5による画像のアップロード先/ダウンロード先をオブジェクト・ストレージに変更できました。
今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/ORDS_REST_WKSP_APEXDEV_rteditor_2023_07_04_2.sql
https://github.com/ujnak/apexapps/blob/master/exports/ckeditor-image-sample-obs.zip
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完