2023年1月19日木曜日

Microsoft OneDriveを操作するAPEXアプリの作成(5) - ファイルとフォルダの操作

 ファイルの作成、更新、削除、およびフォルダの作成、更新(名前の変更)、削除を実装します。

最初にMicrosoft Graph APIを呼び出して、上記の操作を行うプロシージャを実装したパッケージMS356_ONEDRIVEを作成します。

以下のコードをデータベースで実行します。

create or replace package ms365_onedrive
as
/*
* 個人用OneDriveを操作するMicrosoft Graph APIのエンドポイント
*/
G_ENDPOINT constant varchar2(80) := 'https://graph.microsoft.com/v1.0/me/drive/items/';
/**
* OneDriveにファイルを新規作成する。
*
* @param p_file タイプがファイル参照のページ・アイテム値
* @param p_folder_id ファイルをアップロードするフォルダ(DriveItem)のアイテムID
* @param p_credential_static_id Web資格証明の静的ID
*/
procedure create_file(
p_file in varchar2
,p_folder_id in varchar2
,p_credential_static_id in varchar2
);
/**
* ファイルの内容を置き換える。
*
* @param p_file タイプがファイル参照のページ・アイテム値
* @param p_item_id 置き換えるファイル(DriveItem)のアイテムID
* @param p_credential_static_id Web資格証明の静的ID
*/
procedure update_file(
p_file in varchar2
,p_item_id in varchar2
,p_credential_static_id in varchar2
);
/**
* DriveItem - フォルダ、ファイル - を削除する。
*
* @param p_item_id 削除するDriveItemのアイテムID
* @param p_credential_static_id Web資格証明の静的ID
*/
procedure delete_drive_item(
p_item_id in varchar2
,p_credential_static_id in varchar2
);
/**
* フォルダを作成する。
*
* @param p_parent_folder_id フォルダを作成するフォルダのアイテムID
* @param p_folder_name 作成するフォルダの名前
* @param p_credential_static_id Web資格証明の静的ID
*/
procedure create_folder(
p_parent_folder_id in varchar2
,p_folder_name in varchar2
,p_credential_static_id in varchar2
);
/**
* フォルダの名前を変更する。
*
* @param p_folder_id 名前を変更するフォルダのアイテムID
* @param p_folder_name 変更するフォルダの名前
* @param p_credential_static_id Web資格証明の静的ID
*/
procedure rename_folder(
p_folder_id in varchar2
,p_folder_name in varchar2
,p_credential_static_id in varchar2
);
end ms365_onedrive;
/
create or replace package body ms365_onedrive
as
/**
* MS Graph APIの呼び出しでエラーが発生していないかどうか確認する。
* エラーがあれば例外を発生させ、処理を中断する。
*/
procedure check_error(
p_status_code in number
,p_response in clob
)
as
l_response_json json_object_t;
l_error json_object_t;
begin
apex_debug.info('status code = %s', p_status_code);
if p_status_code >= 400 then
apex_debug.info(p_response);
l_response_json := json_object_t(p_response);
l_error := treat(l_response_json.get('error') as json_object_t);
raise_application_error(-20001,l_error.get_string('message'));
end if;
end check_error;
/**
* uploadUrlを取得して、ファイルをアップロードする。
*/
procedure upload_file(
p_file in varchar2
,p_request_url in varchar2
,p_conflict_behavior in varchar2
,p_credential_static_id in varchar2
)
as
/* アップロードされたファイル本体 */
l_blob_content blob;
/* Graph API */
l_response clob;
l_response_json json_object_t;
l_upload_url varchar2(32767);
l_request clob;
begin
/* APEXにアップロードされたファイルをBLOBとして取得する */
select blob_content into l_blob_content
from apex_application_temp_files where name = p_file;
/* 最初にuploadUrlの取得を取得する。 */
apex_debug.info('MS Graph API URL: %s', p_request_url);
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => p_request_url
,p_http_method => 'POST'
,p_body => json_object(
'item' value json_object(
'@microsoft.graph.conflictBehavior' value p_conflict_behavior
) format json
)
,p_credential_static_id => p_credential_static_id
);
check_error(apex_web_service.g_status_code, l_response);
l_response_json := json_object_t(l_response);
l_upload_url := l_response_json.get_string('uploadUrl');
apex_debug.info('uploadUrl = %s', l_upload_url);
/* 取得したuploadUrlを呼び出して、ファイルの内容をアップロードする。 */
apex_web_service.clear_request_headers;
l_response := apex_web_service.make_rest_request(
p_url => l_upload_url
,p_http_method => 'PUT'
,p_body_blob => l_blob_content
,p_credential_static_id => p_credential_static_id
);
check_error(apex_web_service.g_status_code, l_response);
end upload_file;
procedure create_file(
p_file in varchar2
,p_folder_id in varchar2
,p_credential_static_id in varchar2
)
as
l_filename apex_application_temp_files.filename%type;
l_request_url varchar2(32767);
begin
select filename into l_filename from apex_application_temp_files where name = p_file;
l_request_url := G_ENDPOINT || p_folder_id || ':/'
|| utl_url.escape(l_filename,true,'utf-8') || ':/createUploadSession';
upload_file(
p_file => p_file
,p_request_url => l_request_url
,p_conflict_behavior => 'fail'
,p_credential_static_id => p_credential_static_id
);
end create_file;
procedure update_file(
p_file in varchar2
,p_item_id in varchar2
,p_credential_static_id in varchar2
)
as
l_filename apex_application_temp_files.filename%type;
/* Graph API */
l_request_url varchar2(32767);
l_response clob;
begin
/* 既存のDriveItemを宛先にして、ファイルをアップロードする。 */
l_request_url := G_ENDPOINT || p_item_id || '/createUploadSession';
upload_file(
p_file => p_file
,p_request_url => l_request_url
,p_conflict_behavior => 'replace'
,p_credential_static_id => p_credential_static_id
);
/* アップロードが完了したので、ファイル名を変更する。 */
select filename into l_filename from apex_application_temp_files where name = p_file;
l_request_url := G_ENDPOINT || p_item_id;
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => l_request_url
,p_http_method => 'PATCH'
,p_body => json_object(
'name' value l_filename
)
,p_credential_static_id => p_credential_static_id
);
check_error(apex_web_service.g_status_code, l_response);
end update_file;
procedure delete_drive_item(
p_item_id in varchar2
,p_credential_static_id in varchar2
)
as
l_request_url varchar2(32767);
l_response clob;
begin
l_request_url := G_ENDPOINT || p_item_id;
apex_debug.info('MS Graph API URL: %s', l_request_url);
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => l_request_url
,p_http_method => 'DELETE'
,p_credential_static_id => p_credential_static_id
);
check_error(apex_web_service.g_status_code, l_response);
end delete_drive_item;
procedure create_folder(
p_parent_folder_id in varchar2
,p_folder_name in varchar2
,p_credential_static_id in varchar2
)
as
l_request_url varchar2(32767);
l_response clob;
l_response_json json_object_t;
l_upload_url varchar2(32767);
l_request clob;
begin
l_request_url := G_ENDPOINT || p_parent_folder_id || '/children';
apex_debug.info('MS Graph API URL: %s', l_request_url);
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => l_request_url
,p_http_method => 'POST'
,p_body => json_object(
'name' value p_folder_name
,'folder' value '{}' format json
,'@microsoft.graph.conflictBehavior' value 'fail'
)
,p_credential_static_id => p_credential_static_id
);
check_error(apex_web_service.g_status_code, l_response);
end create_folder;
procedure rename_folder(
p_folder_id in varchar2
,p_folder_name in varchar2
,p_credential_static_id in varchar2
)
as
l_request_url varchar2(32767);
l_response clob;
begin
l_request_url := G_ENDPOINT || p_folder_id;
apex_debug.info('MS Graph API URL: %s', l_request_url);
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => l_request_url
,p_http_method => 'PATCH'
,p_body => json_object(
'name' value p_folder_name
)
,p_credential_static_id => p_credential_static_id
);
check_error(apex_web_service.g_status_code, l_response);
end rename_folder;
end ms365_onedrive;
/

すべてのプロシージャは引数p_credential_static_idとして、Azure ADで認証されたWeb資格証明を必要としています。ここで指定するWeb資格証明を、置換文字列に定義します。

アプリケーション定義置換を開き、置換文字列としてG_CREDENTIAL_NAME置換値MS_AZURE_ADとします。

変更の適用をクリックし、保存します。


パッケージMS365_ONEDRIVEのプロシージャを呼び出す際に、引数p_credential_static_idに渡す値はアイテムG_CREDENTIAL_NAMEと指定できるようになりました。

これから、アプリケーションにドロワーのページを2つ作成します。ひとつはファイルの操作、もうひとつはフォルダの操作を行います。

最初に、ファイルを操作するページを作成します。

ページの作成を実行し、フォームを選択します。


ページ定義ページ番号は対話モード・レポートから呼び出す際の宛先として使用するため、必ずを指定します。名前Fileページ・モードとしてドロワーを選択します。

データ・ソースローカル・データベースソース・タイプとしてSQL問合せを選択し、SQL SELECT文を入力として以下を記述します。
select
    'this_is_dummy_data_for_item_id' "ITEM_ID"
    ,'this_is_dummy_data_for_folder_id' "FOLDER_ID"
    ,'this_is_dummy_data_for_file' "FILE"
from dual
ページ作成ウィザードによって作成されるページに、ページ・アイテムとしてP4_ITEM_IDP4_FOLDER_IDP4_FILEが作成されます。また、ページ・タイプにフォームを選んでいるため、ボタンとしてCREATESAVEDELETEおよびCANCELも作成されます。

ページ・タイプがフォームの場合、ナビゲーションは作成されません。

へ進みます。


主キー列1としてITEM_ID(Varchar2)を選択します。ITEM_IDの指定がない場合は作成のページ、指定がある場合は変更または削除を行うページになります。

ページの作成をクリックします。


ページが作成されます。

作成されたページに含まれるリージョンFile識別タイプを、フォームから静的コンテンツに変更します。また、ヘッダーの前にあるプロセス初期化フォームFileを削除します。


ページ・アイテムP4_ITEM_IDP4_FOLDER_IDを選択し、識別タイプ非表示セッション・ステートストレージリクエストごと(メモリーのみ)に変更します。


ページ・アイテムP4_FILEを選択し、識別タイプファイル参照...に変更します。設定ファイルをパージするタイミングEnd of Requestとします。セッション・ステートストレージリクエストごと(メモリーのみ)に変更します。

検証最大値に値が設定されている場合は、空白にします。


左ペインでプロセス・ビューを開きます。最初にプロセスプロセス・フォームFileを削除します。


ファイルの作成、更新、削除を行うプロセスを作成します。

プロセスを作成し、識別名前ファイルの作成とします。タイプとしてAPIの呼出しを選択します。設定パッケージとしてMS365_ONEDRIVEプロシージャまたはファンクションとしてCREATE_FILEを選択します。

サーバー側の条件ボタン押下時としてCREATEを選択します。


パラメータとしてp_file、p_folder_id、p_credential_static_idが現れますが、p_filep_folder_idはデフォルトでページ・アイテムP4_FILEP4_FOLDER_IDが選択されます。

残りのp_credential_static_idは、タイプアイテムアイテムとしてG_CREDENTIAL_NAMEを指定します。


ファイルの更新を行うプロセスを作成します。プロシージャまたはファンクションUPDATE_FILEボタン押下時にはSAVEを選択します。パラメータp_credential_static_idとしてアイテムG_CREDENTIAL_NAMEを設定します。


ファイルの削除を行うプロセスを作成します。プロシージャまたはファンクションDELETE_DRIVE_ITEMボタン押下時にはDELETEを選択します。


ファイルを操作するページは以上で完成です。

続いてフォルダを操作するページを作成します。作成手順はファイルのドロワーと同じです。

ページ定義ページ番号名前Folderとします。ページ・モードドロワーです。データ・ソースSQL SELECT文を入力として以下を記述します。
select
    'This_is_dummy_data_for_parent_folder_id' "PARENT_FOLDER_ID"
    ,'This_is_dummy_data_for_folder_id' "FOLDER_ID"
    ,'This_is_dummy_data_for_folder_name' "FOLDER_NAME"
from dual

主キー列1としてFOLDER_ID(Varchar2)を選択します。


作成されたページに含まれるリージョンFolderを選択し、タイプ静的コンテンツに変更します。また、ヘッダーの前のプロセス初期化フォームFolderを削除します。


作成されたページ・アイテムP5_FOLDER_IDP5_PARENT_FOLDER_ID識別タイプ非表示にします。セッション・ステートストレージリクエストごと(メモリーのみ)とします。


ページ・アイテムP5_FOLDER_NAMEは、セッション・ステートストレージリクエストごと(メモリーのみ)に変更し、検証最大値80程度に拡張します。


フォルダの作成、更新(名前の変更)、削除を行うプロセスを作成します。

最初にプロセスプロセス・フォームFolderを削除します。


フォルダの作成を行うプロセスのプロシージャまたはファンクションは、CREATE_FOLDERです。ボタン押下時としてCREATEを選択します。


フォルダの更新をするプロセスのプロシージャまたはファンクションは、RENAME_FOLDERです。ボタン押下時としてSAVEを選択します。


フォルダの削除をするプロセスのプロシージャまたはファンクションは、DELETE_DRIVE_ITEMです。ボタン押下時としてDELETEを選択します。


パラメータp_item_idはデフォルトでアイテムが選択されません。アイテムとしてP5_FOLDER_IDを指定します。


フォルダを操作するページは以上で完成です。

ページ番号のページFolderを開き、対話モード・レポートFolderよりファイルとフォルダの操作を呼び出せるようにします。

リージョンFolderにボタンを作成します。

識別ボタン名CREATEラベルCreateとします。レイアウト位置として対話モード・レポートの検索バーの右を選びます。

動作アクションとして、このアプリケーションのページにリダイレクトを選択します。


ターゲットをクリックし、リンク・ビルダー・ターゲットを開きます。

ターゲットページはファイルの操作を実装したドロワーのページであるアイテムの設定として名前P4_FOLDER_IDに値&P3_ITEM_ID.を設定します。


同様の手順にて、フォルダを作成するボタンを作成します。

識別ボタン名CREATE_FOLDERラベルCreate Folderとします。レイアウト位置として対話モード・レポートの検索バーの右を選びます。

動作アクションとして、このアプリケーションのページにリダイレクトを選択します。


ターゲットをクリックし、リンク・ビルダー・ターゲットを開きます。

ターゲットページはフォルダの操作を実装したドロワーのページであるアイテムの設定として名前P5_PARENT_FOLDER_IDに値&P3_ITEM_ID.を設定します。


すでに作成済みのファイルまたはフォルダを選択して、ドロワーを開く実装を追加します。

対話モード・レポートFolderローカル後処理SQL問合せに、以下の2つの列を追加します。ファイルの編集を行うL_EXIST_ITEMと、フォルダの編集を行うL_EXIST_FOLDERです。
,apex_page.get_url(
    p_page => 4
    ,p_items => 'P4_FOLDER_ID,P4_ITEM_ID'
    ,p_values => :P3_ITEM_ID || ',' || ID
) L_EXIST_ITEM
,apex_page.get_url(
    p_page => 5
    ,p_items => 'P5_FOLDER_ID,P5_FOLDER_NAME'
    ,p_values => ID || ',' || NAME
) L_EXIST_FOLDER
select ID,
CTAG,
ETAG,
SHA1HASH,
SHA256HASH,
QUICKXORHASH,
MIMETYPE,
NAME,
SIZE_,
WIDTH,
HEIGHT,
ISO,
FNUMBER,
CAMERAMAKE,
CAMERAMODEL,
FOCALLENGTH,
TAKENDATETIME,
EXPOSURENUMERATOR,
EXPOSUREDENOMINATOR,
WIDTH2,
FOURCC,
HEIGHT2,
BITRATE,
DURATION,
FRAMERATE,
AUDIOFORMAT,
AUDIOCHANNELS,
AUDIOBITSPERSAMPLE,
AUDIOSAMPLESPERSECOND,
ID2,
DISPLAYNAME,
SCOPE,
SHAREDDATETIME,
WEBURL,
ALTITUDE,
LATITUDE,
LONGITUDE,
ID3,
DISPLAYNAME2,
ID4,
DISPLAYNAME3,
COMMENTCOUNT,
CREATEDDATETIME,
LASTMODIFIEDDATETIME,
ID5,
DISPLAYNAME4,
ID6,
DISPLAYNAME5,
CREATEDDATETIME2,
ID7,
NAME2,
PATH,
DRIVEID,
DRIVETYPE,
LASTMODIFIEDDATETIME2,
DOWNLOADURL
,apex_page.get_url(
p_page => :APP_PAGE_ID
,p_items => 'P3_ITEM_ID'
,p_values => ID
) L_FOLDER_LINK
,apex_page.get_url(
p_page => 4
,p_items => 'P4_FOLDER_ID,P4_ITEM_ID'
,p_values => :P3_ITEM_ID || ',' || ID
) L_EXIST_ITEM
,apex_page.get_url(
p_page => 5
,p_items => 'P5_FOLDER_ID,P5_FOLDER_NAME'
,p_values => ID || ',' || NAME
) L_EXIST_FOLDER
from #APEX$SOURCE_DATA#


追加された列L_EXIST_ITEML_EXIST_FOLDERを選択し、識別タイプ非表示に変更します。


列IDをクリックして、ファイルやフォルダの編集画面が開くようにします。

IDを選択し、列の書式HTML式として以下を記述します。

{if DOWNLOADURL/}
<a href="#L_EXIST_ITEM#">#ID#</a>
{else/}
<a href="#L_EXIST_FOLDER#">#ID#</a>
{endif/}


ファイルやフォルダの編集が完了し、ドロワーが閉じたときに対話モード・レポートFolderがリフレッシュされるように、動的アクションを作成します。このようにすることで、作成、更新、削除したファイルやフォルダが、ドロワーを閉じた時点で対話モード・レポートの表示に反映されます。ページ作成ウィザードを使ってレポートとフォームのページを作成すると、この動的アクションはウィザードが作成してくれます。

左ペインで動的アクション・ビューを開きます。ダイアログのクローズ上でコンテキスト・メニューを開き、動的アクションの作成を実行します。


作成された動的アクションの識別名前は、ファイルおよびフォルダの編集とします。タイミングイベントダイアログのクローズ選択タイプリージョンリージョンとしてFolderを選択します。


リージョンFolderがドロワーを開く操作を起動したHTML要素を含むため、タイミングのリージョンにFolderを選択しています。リフレッシュの対象だからではありません。

TRUEアクションを選択します。識別名前Folderのリフレッシュアクションとしてリフレッシュを選択します。影響を受ける要素として、リフレッシュの対象であるリージョンFolderを指定します。


以上でファイルとフォルダの操作を行うページの実装は完了です。

アプリケーションの実装は以上で完了です。

最後の記事は、いくつか作業中に気付いたことを紹介します。

続く