/** * WPML compatibility functions * * @global array $duplicated_posts Array to store the posts being duplicated. * * @package Yoast\WP\Duplicate_Post * @since 3.2 */ add_action( 'admin_init', 'duplicate_post_wpml_init' ); /** * Add handlers for WPML compatibility. */ function duplicate_post_wpml_init() { if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { add_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'shutdown', 'duplicate_wpml_string_packages', 11 ); } } global $duplicated_posts; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: Renaming a global variable is a BC break. $duplicated_posts = []; /** * Copy post translations. * * @global SitePress $sitepress Instance of the Main WPML class. * @global array $duplicated_posts Array of duplicated posts. * * @param int $post_id ID of the copy. * @param WP_Post $post Original post object. * @param string $status Status of the new post. */ function duplicate_post_wpml_copy_translations( $post_id, $post, $status = '' ) { global $sitepress; global $duplicated_posts; remove_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10 ); remove_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10 ); $current_language = $sitepress->get_current_language(); $trid = $sitepress->get_element_trid( $post->ID ); if ( ! empty( $trid ) ) { $translations = $sitepress->get_element_translations( $trid ); $new_trid = $sitepress->get_element_trid( $post_id ); foreach ( $translations as $code => $details ) { if ( $code !== $current_language ) { if ( $details->element_id ) { $translation = get_post( $details->element_id ); if ( ! $translation ) { continue; } $new_post_id = duplicate_post_create_duplicate( $translation, $status ); if ( ! is_wp_error( $new_post_id ) ) { $sitepress->set_element_language_details( $new_post_id, 'post_' . $translation->post_type, $new_trid, $code, $current_language ); } } } } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: see above. $duplicated_posts[ $post->ID ] = $post_id; } } /** * Duplicate string packages. * * @global array() $duplicated_posts Array of duplicated posts. */ function duplicate_wpml_string_packages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: renaming the function would be a BC-break. global $duplicated_posts; foreach ( $duplicated_posts as $original_post_id => $duplicate_post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $original_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $original_post_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $new_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $duplicate_post_id ); if ( is_array( $original_string_packages ) ) { foreach ( $original_string_packages as $original_string_package ) { $translated_original_strings = $original_string_package->get_translated_strings( [] ); foreach ( $new_string_packages as $new_string_package ) { $cache = new WPML_WP_Cache( 'WPML_Package' ); $cache->flush_group_cache(); $new_strings = $new_string_package->get_package_strings(); foreach ( $new_strings as $new_string ) { if ( isset( $translated_original_strings[ $new_string->name ] ) ) { foreach ( $translated_original_strings[ $new_string->name ] as $language => $translated_string ) { do_action( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 'wpml_add_string_translation', $new_string->id, $language, $translated_string['value'], $translated_string['status'] ); } } } } } } } } ロケット台 ZIPファイル連携 – Raqqa

ロケット台 ZIPファイル連携

この方針にすると、後続開発はかなり整理されます。

今後は「各種Import機能を作り直す」のではなく、既存の保存経路を順番に共通ファイル管理へ接続し、最後に本番ロケット台で保存先をS3へ切り替える流れになります。

全体方針

現在のOdoo画面・Portal画面
        ↓
既存のモデルとアップロード処理
        ↓
ir.attachment
        ↓
inquisitor.file.registry
        ↓
現在:Odoo filestore
        ↓
本番:S3対応ストレージ

Odoo-inquisitor側は、ファイルの業務上の意味を管理します。

  • どの案件か
  • 何の資料か
  • 誰が提出したか
  • Portal公開対象か
  • 元ZIPは何か
  • 現行版か
  • SHA256
  • MIMEタイプ
  • ファイルサイズ

ロケット台側は、実ファイルの置き場所を管理します。

  • ローカルfilestore
  • S3 bucket
  • object key
  • IAM権限
  • 暗号化
  • バックアップ
  • ライフサイクル
  • 復旧

この分担にすると、Odoo-inquisitorのコードがAWS構成へ依存しません。

STEP 4

既存全アップロード経路の共通ファイル管理接続

STEP 4では、STEP 3で接続できていない経路を調査して追加します。

主な対象は次です。

内部設計Import PACK
無料ASIS/TOBE
有料Portal ZIP・Asset
顧客提出ファイル
RAQQAから顧客へ渡すファイル
Portalメッセージ添付
Project Material
Customer File
Project Pack
Deliverable
Graph Export
その他の案件関連添付

既存画面やアップロードボタンは変えません。

STEP 4の成果は、どの既存画面からファイルを追加しても、対応するinquisitor.file.registryが作られる状態です。

STEP 4で確認すること

例えば顧客がPortalから資料を提出した場合です。

顧客Portalからアップロード
  ↓
既存Customer File作成
  ↓
ir.attachment作成
  ↓
inquisitor.file.registry作成

共通管理には次が入ります。

案件
添付
ファイル区分:顧客提出資料
発生元:Customer File
Portal区分
ファイルサイズ
SHA256
MIMEタイプ
取込日時
保存方式:odoo_filestore

同様に、RAQQAが顧客へ資料を公開した場合も登録します。

STEP 5

共通ファイル管理の完全性確認

STEP 5では新しい保存機能を追加するより、登録漏れがないかを検証する仕組みを作るのが適切です。

例えば、次を検出します。

案件に紐づくir.attachmentがある
しかしinquisitor.file.registryがない

または、

inquisitor.file.registryがある
しかしattachmentが削除済み

あるいは、

SHA256が空欄
サイズが空欄
元モデルが不明

想定する確認機能

管理画面に新しい業務画面を増やす必要はありません。

共通ファイル管理一覧の検索フィルタとして、次を追加する程度で十分です。

管理未登録
添付不明
SHA256未計算
案件未設定
保存先未設定

必要ならManagerが実行できる次の保守処理を設けます。

共通管理を再同期

これは既存ファイルを移動せず、メタデータだけを再生成します。

STEP 6

S3移行準備情報の確定

ここでもまだS3接続は行いません。

Odoo-inquisitor側で、将来必要になる保存先情報の形式を確定します。

例えば次です。

storage_backend
external_storage_key
storage_bucket_alias
storage_region
storage_version_id
storage_etag
storage_synced_at
storage_state

ただし、すべてを今すぐモデルへ追加する必要はありません。

最低限は次で十分です。

storage_backend
external_storage_key
storage_state
storage_synced_at

状態例です。

odoo_filestore
migration_pending
external
migration_failed

現時点では全ファイルが次です。

storage_backend = odoo_filestore
storage_state = local

本番でS3へ保存された場合にだけ、

storage_backend = s3
external_storage_key = projects/123/...
storage_state = external

となります。

STEP 7

ファイル整合性・移行台帳

S3へ移す前に、filestore内の実体とOdooの管理情報が一致している必要があります。

STEP 7では、次の移行台帳を作れる状態にします。

attachment_id
registry_id
案件
ファイル名
SHA256
サイズ
MIMEタイプ
現在の保存方式
移行予定object key
移行状態

これは、S3移行プログラムが参照する入力データになります。

Odoo-inquisitor自身がS3へアップロードするのではなく、ロケット台側の移行処理がこの台帳を使います。

STEP 8

ロケット台側でS3保存を実装

ここからはOdoo-inquisitor本体ではなく、デプロイ基盤側の開発です。

担当範囲は次です。

S3 bucket
IAM role
bucket policy
暗号化
versioning
バックアップ
lifecycle
CloudTrailまたはアクセスログ
Odooからのアクセス権限

Odooの添付保存については、採用する方式をロケット台側で決めます。

候補は大きく2つです。

方式A:Odooの添付保存先をS3対応モジュールで切替

ir.attachment
  ↓
S3

業務モデルやPortalは変更不要です。

既存コードも引き続きir.attachmentを使います。

これは現在の方針と最も相性が良いです。

方式B:filestoreを維持して外部同期

ir.attachment
  ↓
filestore
  ↓
定期的にS3へバックアップ

これは実装は簡単ですが、S3が主保存先ではなくバックアップになります。

本番の高可用性を考えるなら、最終的には方式Aが本命です。

STEP 9

既存filestoreからS3への移行

本番前または本番移行時に、既存添付をS3へ移します。

処理は概ね次です。

共通ファイル管理から対象抽出
  ↓
ir.attachmentの実体取得
  ↓
SHA256再計算
  ↓
S3へアップロード
  ↓
S3側サイズ・ETag等確認
  ↓
storage_backend更新
  ↓
external_storage_key保存
  ↓
Portal表示確認

失敗しても、元のfilestoreをすぐに削除しないようにします。

S3移行
  ↓
検証
  ↓
一定期間併存
  ↓
問題なければ旧実体整理

この整理もロケット台側で行います。

STEP 10

本番移行前の総合検証

本番前には、画面単位で次を確認します。

内部設計

  • Design PACKをアップロードできる
  • Artifactを閲覧できる
  • ASIS/TOBE候補が取れる
  • 過去Packageを取得できる
  • 共通管理に登録される

無料Portal

  • ASIS/TOBEだけ表示される
  • 添付がS3保存でも開ける
  • 非公開資料は開けない

有料Portal

  • index.htmlが開ける
  • CSSが適用される
  • PNG・SVGが表示される
  • HTML間リンクが動く
  • 未入金案件では開けない

顧客資料交換

  • 顧客がアップロードできる
  • RAQQA側で閲覧できる
  • RAQQAから顧客へ公開できる
  • Portalメッセージ添付が開ける
  • 一括ダウンロードできる

ストレージ

  • S3 objectが存在する
  • SHA256が一致する
  • MIMEタイプが維持される
  • ファイル名が維持される
  • 認可されていないURLから取得できない

後続Unitの整理

今回の目的に合わせると、今後は次のように整理できます。

STEP 3
共通ファイル管理モデル
完了

STEP 4
既存全アップロード経路を共通管理へ接続

STEP 5
未登録・欠損・不整合の検出と再同期

STEP 6
外部ストレージ用メタデータ確定

STEP 7
S3移行台帳・整合性確認

STEP 8
ロケット台側のS3保存実装

STEP 9
既存filestoreからS3への移行

STEP 10
無料・有料・内部・顧客Portalの総合検証

Import履歴モデルについて

先ほど予定されていたinquisitor.import.runは、この流れでは優先度が下がります。

内部Importだけの詳細な履歴を作るより、まず全ファイル経路を共通管理へ接続する方が重要です。

将来、監査要件が必要になった場合には、汎用的な処理履歴として追加できます。

inquisitor.file.operation

のようにして、

  • Import
  • Portal公開
  • 非公開化
  • S3移行
  • 再同期
  • 一括出力

を共通で記録する方が、内部Import専用の履歴モデルより使いやすい可能性があります。

ただし、現時点では追加しなくて問題ありません。

最終的な完成形

完成後も利用者から見える操作は現在とほぼ変わりません。

内部設計は現在の画面へアップロード
無料資料は現在の画面から公開
有料設計は現在の画面へZIPアップロード
顧客は現在のPortalから資料提出
RAQQAは現在の画面から資料公開

裏側だけが次の構造になります。

既存業務モデル
  ↓
共通ファイル管理
  ↓
ir.attachment
  ↓
S3

この方針なら、後続開発で業務画面を作り直す必要がなく、Odoo-inquisitorとロケット台の責任範囲も明確になります。


Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です