AWS CDK x CloudFront x S3 x Basic認証 x index.html対応 x 独自ドメイン

概要 AWS CDKを用いて、CloudFront + S3による静的サイトの作成を行いました。合わせて、CloudFront Functionを用いて、Basic認証とURLにファイル名や拡張子を含まないリクエストにindex.htmlを追加する処理を加えています。さらに、独自ドメインの追加も行いましたので、その備忘録です。 色々と不完全ですが、以下のリポジトリでソースコードを公開しています。 https://github.com/nakamura196/staticBasic 以下のような.envファイルを用意してcdk deployを実行する想定です。 CERT_ARN=arn:aws:acm:xxxx RECORD_NAME=aaa.bbb.com BUCKET_NAME=aaa.bbb.com REGION=us-east-1 ACCOUNT=yyyy DOMAIN_NAME=bbb.com それぞれの説明は以下のとおりです。 項目 説明 例 CERT_ARN 証明書のARN arn:aws:acm:xxxx RECORD_NAME 設定したいドメイン名 aaa.bbb.com BUCKET_NAME ファイルを格納するS3バケット名 aaa.bbb.com REGION リージョン名 us-east-1 ACCOUNT AWSのアカウント名(12 桁の数値 ) 123456789012 DOMAIN_NAME ホストゾーン名 bbb.com Stack 以下のStackを作成しました。 import { Stack, StackProps, RemovalPolicy, aws_cloudfront, aws_cloudfront_origins, aws_iam, } from "aws-cdk-lib"; import { Construct } from "constructs"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as iam from "aws-cdk-lib/aws-iam"; import * as cloudfront from "aws-cdk-lib/aws-cloudfront"; import * as route53 from "aws-cdk-lib/aws-route53"; import * as targets from "aws-cdk-lib/aws-route53-targets"; import { Certificate } from "aws-cdk-lib/aws-certificatemanager"; import * as dotenv from "dotenv"; dotenv.config(); export class StaticBasicStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const recordName = process.env.RECORD_NAME || ""; const domainName = process.env.DOMAIN_NAME || ""; const bucketName = process.env.BUCKET_NAME || ""; const cert = process.env.CERT_ARN || ""; // ホストゾーンIDを取得 const hostedZoneId = route53.HostedZone.fromLookup(this, "HostedZoneId", { domainName, }); // S3バケットを作成 const websiteBucket = new s3.Bucket(this, "WebsiteBucket", { removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, bucketName, }); // CloudFront用のOrigin Access Identityを作成 const originAccessIdentity = new cloudfront.OriginAccessIdentity( this, "OriginAccessIdentity", { comment: `${bucketName}-identity`, } ); // S3バケットポリシーを設定 const webSiteBucketPolicyStatement = new iam.PolicyStatement({ actions: ["s3:GetObject"], effect: iam.Effect.ALLOW, principals: [ new aws_iam.CanonicalUserPrincipal( originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId ), ], resources: [`${websiteBucket.bucketArn}/*`], }); websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement); // CloudFront Functionの設定 const cfFunction = new aws_cloudfront.Function(this, "CloudFrontFunction", { code: aws_cloudfront.FunctionCode.fromFile({ filePath: "assets/redirect.js", }), }); // 証明書を取得 const certificate = Certificate.fromCertificateArn( this, "Certificate", cert ); // CloudFrontの設定 const distribution = new aws_cloudfront.Distribution(this, "distribution", { domainNames: [recordName ], certificate, comment: `${bucketName}-cloudfront`, defaultRootObject: "index.html", defaultBehavior: { allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD, cachedMethods: aws_cloudfront.CachedMethods.CACHE_GET_HEAD, cachePolicy: aws_cloudfront.CachePolicy.CACHING_OPTIMIZED, viewerProtocolPolicy: aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, origin: new aws_cloudfront_origins.S3Origin(websiteBucket, { originAccessIdentity, }), functionAssociations: [ { function: cfFunction, eventType: aws_cloudfront.FunctionEventType.VIEWER_REQUEST, }, ], }, priceClass: aws_cloudfront.PriceClass.PRICE_CLASS_ALL, }); // Route53レコード設定 new route53.ARecord(this, "Route53RecordSet", { // ドメイン指定 recordName, // ホストゾーンID指定 zone: hostedZoneId, // エイリアスターゲット設定 target: route53.RecordTarget.fromAlias( new targets.CloudFrontTarget(distribution) ), }); } } まとめ 色々と考慮不足の点があるかと思いますが、AWS CDKの便利さを体感することができました。他の方の参考になる部分がありましたら幸いです。 ...

2023年5月16日 · 2 分 · Nakamura

GoogleドライブとGoogle Apps Scriptを用いて匿名のファイルアップローダを作成する

概要 GoogleドライブとGoogle Apps Scriptを用いて匿名のファイルアップローダを作成する機会がありましたので、その備忘録です。 以下の記事などを参考にさせていただきました。 https://qiita.com/v2okimochi/items/06ed1ce7c56a877a1e10 ウェブアプリの作成 まず、以下のURLから、Apps Scriptにアクセスします。 https://script.google.com/ 「新しいプロジェクト」をクリック。 以下のような画面が表示されます。 以下のコードをコピペします。2行目の<Google Driveのアップロード用フォルダのID>について、事前にGoogleドライブでアップロード用のフォルダを作成しておき、そのIDを取得しておいてください。 // 定数: Google Driveのアップロード用フォルダのID const FOLDER_ID = '<Google Driveのアップロード用フォルダのID>'; // doGet関数: index.htmlファイルを表示する function doGet() { return HtmlService.createHtmlOutputFromFile('index'); } // processForm関数: フォームオブジェクトを受け取り、Google Driveにファイルをアップロードする function processForm(formObject) { // フォームからファイルデータを取得 var formBlob = formObject.myFile; // アップロード用フォルダを取得 var uploadFolder = DriveApp.getFolderById(FOLDER_ID); // 現在の日時をフォルダ名に使用 var today = new Date(); const folderName = today.toString(); // アップロード用フォルダ内に新しいフォルダを作成 const customFolder = uploadFolder.createFolder(folderName); // 新しいフォルダ内にファイルをアップロード customFolder.createFile(formBlob); // 新しいフォルダ名を戻り値として返す return folderName; } 次に、画面左上の「+」ボタンを押して、HTMLを選択します。 ファイル名に「index」を与えます。 以下のコードをコピペします。 <!DOCTYPE html> <html> <head> <base target="_top"> <script> // フォームのデフォルトの送信動作を無効にする function preventFormSubmit() { var forms = document.querySelectorAll('form'); for (var i = 0; i < forms.length; i++) { forms[i].addEventListener('submit', function(event) { event.preventDefault(); }); } } window.addEventListener('load', preventFormSubmit); // アップロードボタンを最初は無効にする document.addEventListener("DOMContentLoaded", function () { document.getElementById("upload").disabled = true; }, false); // 制限サイズ以内のファイルが選択されたらアップロードボタンを有効にする function changeSubmitButton() { const len = document.getElementById("file").files.length; const size = document.getElementById("file").files[0].size; const maxSize = 1024 * 1024 * 10; // 10MB const uploadButton = document.getElementById("upload"); if (len > 0 && size < maxSize) { uploadButton.disabled = false; } else { uploadButton.disabled = true; } } // アップロードボタンが押されたらファイルをアップロード function handleFormSubmit(formObject) { document.getElementById("upload").disabled = true; const div = document.getElementById('progress'); div.innerHTML = 'アップロード中...'; // アップロード成功した場合はupdateView()実行 google.script.run.withSuccessHandler(updateView).processForm(formObject); } // アップロード完了画面に変える(動的) function updateView(id) { var div = document.getElementById('myform'); div.innerHTML = `<div>アップロードが完了しました。</div>`; } </script> </head> <body> <div id="myform" style="text-align:center;"> ファイルを選択してからアップロードしてください(10MBまで)<br><br> <form onsubmit="handleFormSubmit(this)"> <input id="file" name="myFile" type="file" onchange="changeSubmitButton()" /> <input id="upload" type="submit" value="アップロード" /> </form> <div id="progress"></div> </div> </body> </html> デプロイ 画面右上の「デプロイ」ボタンをクリック後、「新しいデプロイ」をクリックします。 ...

2023年5月11日 · 2 分 · Nakamura

Google スプレッドシートの更新をGitHubに通知する

概要 Google Apps Scriptを用いて、Googleスプレッドシートが更新された際、GitHubに通知を送る方法を調べました。合わせて、StrapiやContentfulからGitHubに通知を送る方法も調べたので、備忘録として記録します。 Google Apps Script 以下のようなスクリプトを用意することで、スプレッドシートの更新をGitHubに通知できました。 const token = "ghp_xxx" const owner = "yyy" const repo = "zzz" const event_type = "aaa" function postSheetChange() { const headers = { "Accept": "application/vnd.github+json", "Authorization": `Bearer ${token}`, "Content-Type": "application/json" } var payload = JSON.stringify({ event_type }); var requestOptions = { method: 'POST', headers, payload, }; UrlFetchApp.fetch(`https://api.github.com/repos/${owner}/${repo}/dispatches`, requestOptions) } トリガーの設定方法などは以下の記事が参考になりました。 https://businesschatmaster.com/slack/spreadsheet-change-notification なお、Googleスプレッドシートの更新の都度、GitHubへの通知が行くので、GitHub Actionsのほうで、以下のようなconcurrencyを設定しておくほうがよさそうです。 または、トリガーを特定のセル(列)が更新された時だけ、などにすることも考えられます。 name: Build Test concurrency: cancel-in-progress: true on: repository_dispatch: types: - update_content jobs: build: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v3 ... Strapi Strapiのwebhookでは、bodyをカスタマイズできないため、以下のようなプロキシサーバを立てる必要があるようです。 ...

2023年4月27日 · 1 分 · Nakamura

Drupal: ネストされたフィールドを検索する一例

概要 以下の記事で、Strapiを用いたネストされたフィールドに対する検索方法を調査しました。 今回は同様のことをDrupalで行う方法を調査します。この調査にあたり、以下の記事で、BookとAuthorのコンテンツを登録済みです。 フィルタリングの方法については、以下の記事が参考になりました。 https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/filtering 検索例 以下に対する検索を行います。 /jsonapi/node/book? hobby=danceであるauthorを含むbookの検索 SHORT filter[field_authors.field_hobby]=dance または filter[field_authors.field_hobby][value]=dance NORMAL filter[ex1][condition][path]=field_authors.field_hobby&filter[ex1][condition][value]=dance hobbyにdanを含むauthorを含むbookの検索 SHORT filter[field_authors.field_hobby][operator]=CONTAINS&filter[field_authors.field_hobby][value]=dan NORMAL filter[ex1][condition][path]=field_authors.field_hobby&filter[ex1][condition][operator]=CONTAINS&filter[ex1][condition][value]=dan (参考)hobbyがplayまたはsingであるauthorを含むbookの検索 filter[ex1][condition][path]=field_authors.field_hobby&filter[ex1][condition][operator]=IN&filter[ex1][condition][value][1]=sing&filter[ex1][condition][value][2]=play (参考)Search APIを使う 以下のモジュールを使用することで、複数のコンテンツタイプに対する検索や、フィールド名の指定、ファセットの追加、などができそうです。 https://www.drupal.org/project/jsonapi_search_api 以下の記事で使い方を紹介していますので、参考にしてください。 <https://nakamura196.pages.dev/ja/posts/8d7aa7c33abffc/#search-api> indexの作成 例えば、Search APIのindexとして、以下のように設定します。 これにより、以下のURLからもbookの情報が得られます。 /jsonapi/index/book 通常のjsonapi(/jsonapi/node/bookなど)と比較して、metaという項目にcountが含まれることで、検索結果の全数が確認できます。(通常のjsonapiでも追加する方法があるかもしれませんが、調査不足により不明です。) { "jsonapi": { "version": "1.0", "meta": { "links": { "self": { "href": "http://jsonapi.org/format/1.0/" } } } }, "data": [...], "meta": { "count": 4 }, "links": { "self": { "href": "https://xxx/jsonapi/index/book" } } } フィルタリング また、「hobby=danceであるauthorを含むbookの検索」については、先のindex作成において、Property path「field_authors:entity:field_hobby」をMachine name「field_hobby」に割り当てましたので、以下のシンプルなクエリで実行できました。 SHORT filter[field_hobby]=dance NORMAL filter[ex1][condition][path]=field_hobby&filter[ex1][condition][value]=dance ファセット さらに、(本記事執筆時点での不確かな知識において、)Search APIを使用する大きな利点の一つとして、facetsが使用できるようになる点が挙げられます。 { "facets": [ { "id": "field_hobby", "label": "authors » Content » hobby", "path": "field_hobby", "terms": [ { "url": "https://xxx/jsonapi/index/book?filter%5Bfield_hobby-facet%5D%5Bcondition%5D%5Bpath%5D=field_hobby&filter%5Bfield_hobby-facet%5D%5Bcondition%5D%5Boperator%5D=IN&filter%5Bfield_hobby-facet%5D%5Bcondition%5D%5Bvalue%5D%5B0%5D=dance&filter%5Bfield_hobby-facet%5D%5Bcondition%5D%5Bvalue%5D%5B1%5D=play", "values": { "value": "play", "label": "play", "active": false, "count": 1 } }, { "url": "https://xxx/jsonapi/index/book?filter%5Bfield_hobby-facet%5D%5Bcondition%5D%5Bpath%5D=field_hobby&filter%5Bfield_hobby-facet%5D%5Bcondition%5D%5Boperator%5D=IN&filter%5Bfield_hobby-facet%5D%5Bcondition%5D%5Bvalue%5D%5B0%5D=dance&filter%5Bfield_hobby-facet%5D%5Bcondition%5D%5Bvalue%5D%5B1%5D=sing", "values": { "value": "sing", "label": "sing", "active": false, "count": 1 } }, { "url": "https://xxx/jsonapi/index/book?filter%5Bfield_hobby%5D=dance", "values": { "value": "dance", "label": "dance", "active": true, "count": 2 } } ] } ] } まとめ 不正確な情報もあるかもしれませんが、Drupalでのnested構造に対する検索と、JSON:APIとSearch APIの組み合わせについて、参考になりましたら幸いです。 ...

2023年4月24日 · 1 分 · Nakamura

Strapi: 深くネストされたフィールドで結果をフィルタリングする方法

概要 以下での記事で、深くネストされたフィールドで結果をフィルタリングする方法が紹介されています。 https://strapi.io/blog/deep-filtering-alpha-26 上記の通り、コンテンツタイプやフィールドを用意することで、意図した結果を得ることができました。 注意点 上記の記事のコメントにもありますが、本文中で「\」が含まれていますが、これは不要なようです。 誤 GET /api/books?filters\[authors\][hobby][$contains]=dance 以下のように、「\」なしのクエリにより、意図した結果が得られました。 正 GET /api/books?filters[authors][hobby][$contains]=dance まとめ 参考になりましたら幸いです。

2023年4月22日 · 1 分 · Nakamura

Drupal: カスタムモジュールを用いて、コンテンツタイプとフィールドを追加する

概要 Drupalのカスタムモジュールを用いて、コンテンツタイプとフィールドを追加する方法の備忘録です。 以下の2つの記事が参考になりました。 https://www.drupal.org/docs/drupal-apis/entity-api/creating-a-custom-content-type-in-drupal-8 https://www.digitalnadeem.com/drupal/how-to-create-content-type-fields-and-view-while-installing-custom-module-in-drupal-9-using-configuration-manager/ Car Brandの例 先に紹介した一つ目の記事の通り進めると、コンテンツタイプ「Car Brand」、フィールド「body」を追加することができました。 なお、上記の記事ではカスタムモジュールの作成の部分がスキップされています。まずはじめに以下のようなフォルダ、およびファイルを作成します。 name: foobar description: サンプルモジュール package: Custom type: module version: 1.0 core_version_requirement: ^8 || ^9 独自のフィールドの追加 上記を参考に、コンテンツタイプを追加することができましたが、独自のフィールドを追加するには、Fieldに加えて、Field storageというものも追加する必要がありました。 このField storageのymlの記述方法がわからなかった際に、冒頭で紹介した2つ目のリンクである、以下の記事が参考になりました。 https://www.digitalnadeem.com/drupal/how-to-create-content-type-fields-and-view-while-installing-custom-module-in-drupal-9-using-configuration-manager/ モジュール「Configuration Manager」を使うことで、すでに登録済みのFieldやField storageの定義内容を確認することができました。 上記を踏まえて、IIIF Mediaというコンテンツタイプを作成して、iiif_image_urlという文字列を格納するフィールドと、iiif_image_widthとiiif_image_heightという数値を格納するフィールドを作成してみます。以下のようなファイルが必要です。 コンテンツタイプ modules/custom/foobar/config/install/node.type.iiif_media.yml # node.type.iiif_media.yml langcode: en status: true dependencies: enforced: module: - foobar # This is the name of the module we're using for this example name: 'IIIF Media' type: iiif_media description: 'Content type for IIIF Media' help: '' new_revision: false preview_mode: 1 display_submitted: true フィールド:iiif_image_url(string) Field storage modules/custom/foobar/config/install/field.storage.node.field_iiif_image_url.yml ...

2023年4月21日 · 2 分 · Nakamura

Drupal: カスタムRESTリソースを作成する

概要 以下を参考に、カスタムRESTリソースを作成しました。 https://www.drupal.org/docs/drupal-apis/restful-web-services-api/custom-rest-resources 上記の記事の通り進めることで、以下のURLから、JSONの結果を得ることができました。 /demo_rest_api/demo_resource { "message": "Hello, this is a rest service" } REST UIモジュール 上記の記事において、以下の記載がありました。 If you are using the REST UI contrib module, you should now be able to see it in the list of available endpoints and you should be able to configure the GET method. この点については、以下の記事を参考に、REST UIモジュールを有効化しました。 https://www.studio-umi.jp/blog/12/357 IIIF Presentation APIの試作 次に、コンテンツ毎にJSON作成に取り組みます。具体的には、/iiif/3/{id}/manifestというパスにアクセスすると、IIIF Presentation API v3に基づく情報を出力するようにしてみます。 以下の記事が参考になりました。 https://medium.com/drupaljournal/create-custom-rest-resource-for-get-and-post-method-in-drupal-8-e445330be3ff 以下のようなファイルを作成します。以下の例では、とりあえず$nodeからtitleを取得しています。これを応用することで、他のフィールドの情報も取得することができそうです。 <?php namespace Drupal\demo_rest_api\Plugin\rest\resource; use Drupal\node\Entity\Node; use Drupal\node\NodeInterface; use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; /** * Annotation for get method * * @RestResource( * id = "iiif_presentation_3", * label = @Translation("IIIF Presentation API v3"), * uri_paths = { * "canonical" = "/iiif/3/{id}/manifest" * } * ) */ class Presentation3 extends ResourceBase { /** * Responds to GET requests. It will return serialize json format of node * object. * * @param $id * Node id. */ public function get($id) { if ($id) { // Load node $node = Node::load($id); if ($node instanceof NodeInterface) { $manifest = [ "@context" => "http://iiif.io/api/presentation/3/context.json", "type" => "Manifest", "label" => [ "none" => [ $node->get("title")[0]->get("value")->getValue() ] ] ]; $response = new ResourceResponse($manifest); // Configure caching for results if ($response instanceof CacheableResponseInterface) { $response->addCacheableDependency($node); } return $response; } return new ResourceResponse('Article doesn\'t exist', 400); } return new ResourceResponse('Article Id is required', 400); } } パーミッションの許可 以下のページで、Anonymous userにチェックを入れて、外部からのアクセスを許可します。 ...

2023年4月20日 · 2 分 · Nakamura

VueUseを用いたテキスト選択(Nuxt3)

概要 Nuxt3(Vue3)を用いたテキストの選択機能の実装にあたり、VueUseを使用してみましたので、その備忘録です。 https://vueuse.org/ デモ 以下のページからお試しいただけます。 https://nuxt3-demo-git-main-nakamura196.vercel.app/textSelection ソースコードは以下です。 https://github.com/nakamura196/nuxt3-demo/blob/main/pages/textSelection.vue インストール方法 以下のページに記載があります。 https://vueuse.org/guide/ 具体的な手順は、以下のとおりです。 npm i -D @vueuse/nuxt @vueuse/core // nuxt.config.ts export default defineNuxtConfig({ modules: [ '@vueuse/nuxt', ], }) まとめ テキスト選択以外にも、便利な機能が色々と使えるようなので、引き続き試してみたいと思います。

2023年4月19日 · 1 分 · Nakamura

Next.js for DrupalにおけるDrupal Search APIを用いた検索(ファセット検索など)

概要 Next.js for Drupalを試してみました。 https://next-drupal.org/ 以下の「Get Started」の通りにすすめることで、Next.jsとDrupalを連携させることができました。 https://next-drupal.org/learn/quick-start また、以下の記事で、ファセット検索の実装例が紹介されています。 https://next-drupal.org/guides/search-api 本記事では、特に後者のファセット検索の実現に関する備忘録です。 Search API 以下、Serverとindexを作成します。 公式サイトでは以下が参考になります。 https://www.drupal.org/docs/contributed-modules/search-api 日本語サイトでは以下が参考になります。 https://www.acquia.com/jp/blog/introduction-to-search-api-1 Serverの作成 indexの作成 今回、test_index_20230417というインデックスを作成します。 さらに、タイトルをフィールドとして追加しました。 その後、インデクシングを行います。 JSON:API 上記を行ったところで、キャッシュをクリアします。 /admin/config/development/performance その後、以下のURLからエンドポイントにアクセスできるようになります。 /jsonapi/index/test_index_20230417 以下のようなクエリパラメータによって、検索結果の絞り込みができます。 /jsonapi/index/test_index_20230417?filter[title]=更新したタイトル { "jsonapi": { "version": "1.0", "meta": { "links": { "self": { "href": "http://jsonapi.org/format/1.0/" } } } }, "data": [ { "type": "node--service", "id": "82a34c35-f1b7-49eb-81ac-f15d0deac22c", "links": { "self": { "href": "https://xxx/jsonapi/node/service/82a34c35-f1b7-49eb-81ac-f15d0deac22c?resourceVersion=id%3A5075" } }, "attributes": { "drupal_internal__nid": 4, "drupal_internal__vid": 5075, "langcode": "en", "revision_timestamp": "2023-04-12T08:19:00+00:00", "revision_log": null, "status": true, "title": "更新したタイトル", "created": "2023-04-11T02:09:35+00:00", "changed": "2023-04-12T08:19:00+00:00", "promote": false, "sticky": false, "default_langcode": true, "revision_translation_affected": true, ... Facets 以下にアクセスします。 ...

2023年4月17日 · 1 分 · Nakamura

Contentfulの全文検索は2文字以上の検索語が必要?

Contentfulを使用していますが、2文字以上の検索語が必要そうでした。 A query will ignore search tokens with less than 2 characters. https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/full-text-search-on-a-field この点は日本語などを含むデータに対して注意が必要そうです。 追記 microCMSでは、1文字からの検索が可能でした。 https://microcms.io/

2023年4月14日 · 1 分 · Nakamura

Drupalのコンテンツの一括削除の方法

概要 Drupalのコンテンツの一括削除の方法を調べたので、その備忘録です。以下の記事が参考になりました。 https://www.webwash.net/how-to-bulk-delete-content-in-drupal/ 以下の3つの方法が紹介されていました。 Drupal コア UI の使用 (Using Drupal Core UI) Drush の使用 (Using Drush) ビューの一括操作 (VBO) の使用 (Using Drupal Views Bulk Operations (VBO)) Drupal コア UI の使用 以下、日本語訳です。 小さな Drupal サイトがあり、削除するノードが約 300 未満の場合は、この方法を使用する必要があります。これは、Drupal コアの UI を使用すると、デフォルトで一度に 50 ノードしか削除できないためです。サイトが大きくなると、これは面倒になります。 Drush の使用 以下、日本語訳です。 コマンド ラインを使用すると、Drush を使用できます。これが推奨される方法です。 例えば以下では、記事コンテンツ タイプのすべてのノードを削除します。 drush entity:delete node --bundle=article ただ、上記のサイトにも記載がありましたが、PHP メモリ制限にひっかかることがあるようです。Amazon Lightsail上に立てた512MBメモリの環境において、5,000件のコンテンツの一括削除を試みたところ、以下のように制限に引っかかってしまいました。ただし、再度同じコマンドを実行することで、無事に一括削除ができました。 drush entity:delete node --bundle=article 2550/5057 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░] 50% PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 20480 bytes) in /opt/bitnami/drupal/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php on line 76 PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 32768 bytes) in /opt/bitnami/drupal/vendor/symfony/http-foundation/Response.php on line 895 specify the chunk/batch size デフォルトでは50件ずつ処理されますが、–chunks=100のように引数を加えることで、バッチサイズを変更できました。 ...

2023年4月14日 · 1 分 · Nakamura

DrupalとAmazon OpenSearch Serviceを接続する

概要 DrupalとAmazon OpenSearch Serviceを接続する機会がありましたので、その備忘録です。以下の記事が参考になりました。 https://www.acquia.com/jp/blog/intergration-with-drupal-and-elasticsearch モジュールのインストール drupal/search_apiとdrupal/elasticsearch_connectorに加えて、nodespark/des-connectorをインストールする必要がありました。 (バージョンの指定方法など、改善の余地があるかもしれません。) composer require "nodespark/des-connector:^7.x-dev" composer require 'drupal/search_api:^1.29' composer require "drupal/elasticsearch_connector ^7.0@alpha" 続けて、以下で有効化します。 drush pm:enable search_api elasticsearch_connector DrupalをElasticsearchに接続する クラスタ 以下にアクセス /admin/config/search/elasticsearch-connector 「Add cluster」をクリックします。 そして、以下のように値を入力します。Amazon OpenSearch ServiceでBasic認証をかけている方法を前提としています。注意点として、「Server URL」に入力する値の末尾に、:443が必要でした。 Elasticsearchサーバーへの接続が成功すると、Cluster Statusに yellow や green と表示されます。 インデックス 次に、任意の作業ですが、インデックスを作成します。「Add index」ボタンを押して、nameだけ入力してみます。結果、以下のようにインデックスが作成されました。 Search APIの設定 今度は、Search API側の設定を行います。以下にアクセスします。 /admin/config/search/search-api サーバの追加 「Add Server」ボタンを押します。特に、BackendにElasticsearchを選択して、Clusterに先程作成したクラスタを選択します。 インデックスの追加 以下の画面には出ていませんが、「Datasources」のところで、「Content」を選択しています。また「Server」で、上記で作成したサーバを選択しています。 次に、末尾の「Save and add fields」ボタンをクリックします。 フィールドの追加 今回は最低限のフィールドとして、Titleを追加しておきます。 追加したフィールドに対するFulltext検索を可能とするために、「Type」を変更します。これらの設定は適宜変更してください。 再インデクシング 「Save changes」を押すと、以下のように、reindexingへのリンク(黄色)が表示されます。 ...

2023年4月13日 · 1 分 · Nakamura

DrupalでGraphQLを試す

概要 DrupalでGraphQLを試してみましたので、その備忘録です。以下の文書が参考になりました。 https://drupal-graphql.gitbook.io/graphql/ Amazon LightsailにインストールされたDrupalを前提とします。 モジュールのインストール 以下のモジュールをインストールします。 https://www.drupal.org/project/graphql ただし、以下のモジュールを事前にインストールする必要がありました。 https://www.drupal.org/project/typed_data 結果、以下により、インストールができました。 cd /home/bitnami/stack/drupal composer require 'drupal/typed_data:^1.0@beta' composer require 'drupal/graphql:^4.4' GUIからのモジュールインストール 関連する以下の3つのモジュール全てにチェックを入れて、インストールしました。 設定 以下にアクセスします。 /admin/config/graphql 「Create server」で、以下のように設定しました。test_serverという名前にしました。 Explore 以下にアクセスします。 /admin/config/graphql/servers/manage/test_server/explorer 以下のような画面に遷移します。 以下のクエリを投げてみます。 { articles { total } } 以下のような結果が得られます。 { "data": { "articles": { "total": 5054 } } } まとめ 今後、独自のスキーマを用いた使用方法についても調査してみたいと思います。参考になりましたら幸いです。

2023年4月12日 · 1 分 · Nakamura

StrapiにGraphQLを追加する

概要 以下の記事で、StrapiをAmazon Lightsail上に立ち上げました。 今回は、GraphQLを追加して、使用してみます。 GraphQLプラグインのインストール 以下を実行しました。backendなどのパスは適宜読み替えてください。 cd /opt/bitnami/apache2/htdocs/backend yarn add @strapi/plugin-graphql そして、アプリを起動します。 yarn develop そして、/graphqlにアクセスすると、以下のような画面が表示されます。 今回、servicesというコンテンツタイプを作成し、titleというフィールドを作成済みでした。なので、以下のようなクエリを発行することで、その一覧やメタデータを取得できました。 # Write your query or mutation here query getServices { services { data { id attributes { title createdAt updatedAt } } } } 以下が結果です。 { "data": { "services": { "data": [ { "id": "1", "attributes": { "title": "Cultural Japan", "createdAt": "2023-04-10T21:59:08.768Z", "updatedAt": "2023-04-10T21:59:12.752Z" } } ] } } } まとめ 次は、Next.jsなどとの連携を試してみたいと思います。

2023年4月12日 · 1 分 · Nakamura

Pythonを使ってDrupalのタクソノミーの登録とコンテンツへの追加

概要 以下のシリーズの続きです。 今回は、タクソノミーの登録とコンテンツへの追加を行います。 タクソノミーの登録 事前に、ne_classというタクソノミーをGUIを通じて作成しました。以下のようなURLで一覧できます。 /jsonapi/taxonomy_term/ne_class 以下、新しいタクソノミーを登録するプログラムです。host, username, passwordは適宜設定してください。 payload = { "data": { "type": "taxonomy_term--ne_class", "attributes": { "name": "干瀬", } } } _type = "ne_class" url = f"{host}/jsonapi/taxonomy_term/{_type}" r = requests.post(url, headers=headers, auth=(username, password), json=payload) r.json() 以下のような結果が得られます。 {'jsonapi': {'version': '1.0', 'meta': {'links': {'self': {'href': 'http://jsonapi.org/format/1.0/'}}}}, 'data': {'type': 'taxonomy_term--ne_class', 'id': '17c70bd6-e6fc-46e2-bb5d-2377ba9c8ab8', ... コンテンツへの追加 事前に、PlaceというコンテンツタイプをGUIを通じて作成しました。また、field_ne_classを作成して、上記のタクソノミーne_classを値の候補として設定しました。 relationshipsのidに先程取得したタクソノミーのidを指定しています。 payload = { "data": { "type": "node--place", "attributes": { "title": "xxx" }, "relationships": { "field_ne_class": { "data": { "type": "taxonomy_term--ne_class", "id": "17c70bd6-e6fc-46e2-bb5d-2377ba9c8ab8", } } } } } _type = "place" url = f"{host}/jsonapi/node/{_type}" r = requests.post(url, headers=headers, auth=(username, password), json=payload) r.json() まとめ タクソノミーの追加やコンテンツへの関連付けも機械的に行うことができそうです。他のよりよい方法もあるかと思いますが、参考になりましたら幸いです。 ...

2023年4月11日 · 1 分 · Nakamura

Pythonを使ってDrupalのコンテンツを更新・削除する

概要 以下の記事で、コンテンツの新規登録の方法を記載しました。 今回は、既存のコンテンツの更新・削除を試みます。 アイテムの絞り込み 以下のようなプログラムにより、登録済みのコンテンツを取得することができます。今回は、titleが「更新前のタイトル」のコンテンツを取得しました。res["data"]は配列になります。 username = "xxx" password = "xxx" host = "xxx" query = { "title": "更新前のタイトル" } item_type = "article" filters = [] for key, value in query.items(): filters.append(f'filter[{key}]={value}') filter_str = '&'.join(filters) endpoint = f'{host}/jsonapi/node/{item_type}?{filter_str}' r = requests.get(endpoint, headers=headers, auth=(username, password)) res = r.json() len(res['data']) 更新対象のコンテンツのID取得 730f844d-b476-4485-8957-c33fccb7f8acのようなIDが得られます。 item = res['data'][0] item_id = item['id'] 更新 typeとidを指定して更新します。 ...

2023年4月11日 · 1 分 · Nakamura

Pythonを使ってDrupalにコンテンツを追加する

概要 Pythonを使ってDrupalにコンテンツを追加する機会がありましたので、その備忘録です。以下の記事を参考にしました。 https://weimingchenzero.medium.com/use-python-to-call-drupal-9-core-restful-api-to-create-new-content-9f3fa8628ab4 Drupalの準備 Amazon Lightsailに作成しました。以下の記事などが参考になります。 https://annai.co.jp/article/use-aws-lightsail モジュール 以下をインストールします。 HTTP Basic Auth JSON:API RESTful Web Services Serialization JSON:APIの設定変更 以下にアクセスして、設定を変更します。 </admin/config/services/jsonapi> Python {ipアドレス or ドメイン名}、{パスワード}を適宜設定してください。 Amazon Lightsailの場合、初期ユーザ名はuserです。またパスワードは以下のコマンドで確認します。 cat ~/bitnami_application_password import requests from requests.auth import HTTPBasicAuth endpoint = 'http://{ipアドレス or ドメイン名}/jsonapi/node/article' u = 'user' p = '{パスワード}' headers = { 'Accept': 'application/vnd.api+json', 'Content-Type': 'application/vnd.api+json' } payload = { "data": { "type": "node--article", "attributes": { "title": "What's up from Python", "body": { "value": "Be water. My friends.", "format": "plain_text" } } } } r = requests.post(endpoint, headers=headers, auth=(u, p), json=payload) r.text その他 以下のようにnote_typeの登録も試みました。 payload = { "data": { "type": "node_type--node_type", "attributes": { "title": "テスト", "description": "node_typeのテストです。" } } } url = f"{host}/jsonapi/node_type/node_type" r = requests.post(url, headers=headers, auth=(username, password), json=payload) r.json() 結果、以下のように、Method Not Allowedとなりました。 ...

2023年4月11日 · 1 分 · Nakamura

Amazon LightsailでStrapiを動かす(SSL, 独自ドメイン)

概要 Amazon LightsailでStrapiを動かす機会がありましたので、その備忘録です。以下の記事を参考にしました。 https://zenn.dev/holykzm/articles/1e54cc25207657 インスタンス Node.jsを選択します。 メモリは1GB以上のものを選択してください。Lightsail上でビルドする場合、メモリ不足でエラーが発生します。 SSL、独自ドメイン 以下を参考にしてください。 https://zenn.dev/nakamura196/articles/5772d6c918508a#独自ドメインの付与 Static IPを付与して、Route 53で独自ドメインを設定し、以下を実行します。 sudo /opt/bitnami/bncert-tool ProxyPassの設定 (より適切な記述箇所があるかと思いますが、)以下を追記します。 /opt/bitnami/apache2/conf/httpd.conf # 末尾に以下を追加 ProxyPass / http://localhost:1337/ ProxyPassReverse / http://localhost:1337/ apacheの再起動 sudo /opt/bitnami/ctlscript.sh restart apache Strapi のインストール cd /opt/bitnami/apache2/htdocs/ npx create-strapi-app@latest backend --quickstart アプリが起動するので、Ctrl+Cなどで一旦停止します。 pm2 pm2のインストール sudo npm install pm2 -g server.jsの作成 cd /opt/bitnami/apache2/htdocs/backend vi server.js /opt/bitnami/apache2/htdocs/backend/server.js const strapi = require('@strapi/strapi'); strapi(/* {...} */).start(); ビルド cd /opt/bitnami/apache2/htdocs/backend NODE_ENV=production npm run build サーバの起動 NODE_ENV=production pm2 start server.js --name strapi 以下のように、strapiを起動できました。 ...

2023年4月11日 · 1 分 · Nakamura

ShExファイルを作成してみる

概要 ShExは、wikipediaにおいて、以下のように説明されています。 Shape Expressionsは、Resource Description Frameworkを検証および記述するためのデータモデリング言語 このShExファイルの作成を試みましたので、その備忘録です。 shexファイルを作成する 今回、data/tmp/merged.ttlにあるRDFデータを起点とします。shexerを用いて、RDFデータからshexファイルを作成します。 pip install shexer RDFデータ内のクラスの一覧を取得する from rdflib import Graph input_nt_file = "data/tmp/merged.ttl" graph = Graph() graph.parse(input_nt_file, format="turtle") knows_query = """ SELECT DISTINCT ?cls WHERE { ?a a ?cls }""" qres = graph.query(knows_query) target_classes = [] for row in qres: target_classes.append(f"{row.cls}") target_classes 取得したクラスを対象に、処理を行う。 from shexer.shaper import Shaper from shexer.consts import NT, SHEXC, SHACL_TURTLE, TURTLE shaper = Shaper(target_classes=target_classes, input_format=TURTLE, graph_file_input=input_nt_file) output_file = "data/tmp/shapes.shex" shaper.shex_graph(output_file=output_file, acceptance_threshold=0.1) print("Done!") 結果、以下のようなshexファイルが作成されました。 :教育メタデータ { exp:指導要領コード IRI +; # 100.0 % # 12.307692307692308 % obj: IRI. Cardinality: {7} rdf:type [data:教育メタデータ] ; # 100.0 % schema:geo IRI +; # 100.0 % # 21.53846153846154 % obj: IRI. Cardinality: {1} # 12.307692307692308 % obj: IRI. Cardinality: {3} # 12.307692307692308 % obj: IRI. Cardinality: {6} # 10.76923076923077 % obj: IRI. Cardinality: {2} exp:学年 @:学年 +; # 100.0 % # 21.53846153846154 % obj: @:学年. Cardinality: {5} # 16.923076923076923 % obj: @:学年. Cardinality: {1} # 10.76923076923077 % obj: @:学年. Cardinality: {6} # 10.76923076923077 % obj: @:学年. Cardinality: {4} # 10.76923076923077 % obj: @:学年. Cardinality: {3} exp:教科 @:教科 +; # 100.0 % # 18.461538461538463 % obj: @:教科. Cardinality: {8} # 15.384615384615385 % obj: @:教科. Cardinality: {3} # 12.307692307692308 % obj: @:教科. Cardinality: {6} # 12.307692307692308 % obj: @:教科. Cardinality: {4} # 10.76923076923077 % obj: @:教科. Cardinality: {5} rdfs:label xsd:string ; # 100.0 % exp:学習指導案 IRI ; # 100.0 % exp:時代 @:時代 *; # 96.92307692307692 % obj: @:時代. Cardinality: + # 23.076923076923077 % obj: @:時代. Cardinality: {2} # 15.384615384615385 % obj: @:時代. Cardinality: {1} # 15.384615384615385 % obj: @:時代. Cardinality: {3} # 13.846153846153847 % obj: @:時代. Cardinality: {4} # 12.307692307692308 % obj: @:時代. Cardinality: {6} ... shexをTurtle形式に変換する ここから、上記で作成したshexファイルをTurtle形式に変換してみます。 ...

2023年4月3日 · 3 分 · Nakamura

Nuxt3 x babylon.jsで.glbファイルをロードする

概要 Nuxt3 x babylon.jsにおいて、.glbファイルのロードを試みた際にエラーが発生しましたので、その備忘録です。 エラーの内容 以下のエラーが発生しました。 Unable to load from ./models/test.glb: importScene of undefined from undefined version: undefined, exporter version: undefinedimportScene has failed JSON parse 対応内容 以下を追加でインストールすることで対応できました。 npm install @babylonjs/loaders 結果、以下のようなjsファイルで表示することができました。 import { Engine, Scene, FreeCamera, Vector3, HemisphericLight, SceneLoader, } from "@babylonjs/core"; import "@babylonjs/loaders/glTF"; const myScene = { engine: null, scene: null, // シーンを作成する関数 createScene: function (canvas) { // エンジンとシーンの初期化 const engine = new Engine(canvas); const scene = new Scene(engine); myScene.engine = engine; myScene.scene = scene; // カメラの設定 const camera = new FreeCamera("camera1", new Vector3(0, 5, -10), scene); camera.setTarget(Vector3.Zero()); camera.attachControl(canvas, true); // 光源の設定 new HemisphericLight("light", Vector3.Up(), scene); // GLBモデルの読み込み SceneLoader.Append( "./models/", "test.glb", scene, function (/*newMeshes*/) { // const mesh = scene.meshes[0]; // シーン内のカメラとライトを作成または更新 scene.activeCamera = null; scene.createDefaultCameraOrLight(true); scene.activeCamera.attachControl(canvas, false); } ); // レンダリングループ engine.runRenderLoop(() => { scene.render(); }); }, }; export default myScene; まとめ 同様のエラーでお困りの方の参考になりましたら幸いです。 ...

2023年3月29日 · 1 分 · Nakamura