狛ログ

2022年10月26日水曜日

【Laravel】created_atの日付フォーマットを変更する方法。

10月 26, 2022
こんにちは、オフィス狛 技術部のmmm(むー)です。

以前、Laravelのプロジェクトで、日付のフォーマットを編集したかったのですが、レスポンスの項目名がcreated_at の場合、うまく変換できず少しはまりました。

その時、調べたことについて記事に残します。

前提条件

■Laravel バージョン
$ php artisan -V
Laravel Framework 8.27.0


■SQL Server バージョン
SELECT @@VERSION;
Microsoft SQL Server 2017 (RTM-CU30) (KB5013756) - 14.0.3451.2 (X64)  	Jun 22 2022 18:20:15  	Copyright (C) 2017 Microsoft Corporation 	Developer Edition (64-bit) on Linux (Ubuntu 18.04.6 LTS)


1. したかったこと

当初、SQLでフォーマットを指定して、created_at カラムの値をレスポンスしたかったのですが、期待した形の値が取得できませんでした。
※コードは必要な箇所以外、簡易化して記載します。
127は「ISO 8601 (タイム ゾーン Z)」となります。
class ConditionsController extends Controller
{
    public function index(Request $request)
    {

			$conditions = Condition::select(
			            DB::raw("CONVERT(VARCHAR, created_at, 127) as created_at")
			        )->get()->first();

			return response()->json($conditions);
    }
}

/*
■結果
{
    "created_at": "2022-09-07T02:48:46.947000Z"
}
*/

本来、「2022-09-07T11:48:46.947」のようなフォーマットになる想定でしたが、
created_atの値は「2022-09-07T02:48:46.947000Z」でした。

一方、下記は全く同じ書き方ですが、取得するカラム名が created_at ではない場合、期待通りにフォーマットを変換することができました。
class ConditionsController extends Controller
{
    public function index(Request $request)
    {

			$conditions = Condition::select(
			            DB::raw("CONVERT(VARCHAR, created_at, 127) as test") // 別名でtestを設定
			        )->get()->first();

			return response()->json($conditions);
    }
}

/*
■結果
{
    "test": "2022-09-07T11:48:46.947"
}
*/

created_atカラムに、testという別名を付けると、値は「2022-09-07T11:48:46.947」でした。

2. 原因と解決策

調査した結果、created_atupdated_atはデフォルトの場合、EloquentがCarbonインスタンスへ変換していることがわかりました。
Modelから返ってきた値をCarbonのフォーマットを使用して編集するように修正しました。
class ConditionsController extends Controller
{
    public function index(Request $request)
    {

			$conditions = Condition::select(
			            DB::raw("CONVERT(VARCHAR, created_at, 127) as created_at")
			        )->get()->first();
			$response['created_at'] = $conditions['created_at']->toIso8601String(); // 変換処理を追記

			return response()->json($response);
    }
}

/*
■結果
{
    "created_at": "2022-09-07T11:48:46+09:00"
}
*/

今回は toIso8601String() を使用しましたが、基本的なフォーマットは下記ページに用意されています。
参考:https://carbon.nesbot.com/docs/#api-conversion

3. 補足

今回はControllerでフォーマットの修正を行いましたが、Modelで下記のように、属性を指定してフォーマットを指定することもできます。
protected $casts = [
    'created_at' => 'datetime:Y/m/d',
];

さらに、Laravelで get...Attribute (...は加工したい項目名)と記載して、フォーマットを指定することも可能です。
public function getCreatedAtAttribute($value)
{
    return Carbon::parse($value)->format('Y/m/d');
}

上記は一例となります。Laravelでは様々な機能が用意されているので、もっといろんな方法があると思います。

参考になりましたら、幸いです。

【Laravel】プログレスバーが表示されるコマンド実行の処理を作成する方法。

10月 26, 2022
こんにちは。オフィス狛 技術部のmmm(むー)です。

担当しているプロジェクトで、Laravelのコマンドラインで実行するバッチ処理がいくつかあるのですが、ユーザー数が日に日に増えていき実行時間が長くなっていきました。

普段は自動で実行しているので特に問題ないのですが、先日テストのため手動でバッチを実行しないといけないことがあり、

(この処理いつ終わるんだろう・・・・)

となってしまいました😖

処理を実行して放置すれば良いとしても、目安もわからず、都度確認するのは大変だと思いますので、今回はコマンド実行の処理とプログレスバーの作成方法をご紹介いたします。

前提条件

■Laravel バージョン
$ php artisan -V
Laravel Framework 8.27.0


1. コマンドを作成する

コマンドを作成します。今回は例として、ランキング作成の処理としました。
# php artisan make:command [コマンド名]

$ php artisan make:command CreateRankingCommand

自動で作成されたファイルを確認すると下記のようになっています。
// app/Console/Commands/CreateRankingCommand.php

<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;

class CreateRankingCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:name';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        return 0;
    }
}

まず、2つの項目を設定します。
1. $signatureに、コマンド名を設定します。ここに設定した名前を使用して、コマンドラインで処理を実行することになります。
2. $description に、このコマンドの説明を記載します。

修正すると、以下のようになりました。
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:create_ranking'; // 修正しました

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'ランキングを作成します。'; // 修正しました
    

次に、コマンドが実行できるか確認します。

処理内容はhandle()内に本来記載しますが、初期状態から何も修正していないので、特にエラー等が表示されなければ問題ありません。
# php artisan command:[コマンド名]

$ php artisan command:create_ranking

また、以下のようにコマンドが作成できているか確認することもできます。
$ php artisan list | grep command:
  command:create_ranking                  ランキングを作成します。


2. プログレスバーを作成する

コマンドが実行できることは確認できたので、処理とプログレスバーを作成します。

処理内容は、handle()メソッドの中に記載します。
大変簡素ですが、今回はただループするだけの処理を作成します。
public function handle()
    {
        $count = 10;

        for ($i = 1; $i <= $count; $i++) {
            // 本来、ここで何か処理を行います
        }

        return 0;
    }

ここにプログレスバーを表示する処理を追記していきます。
コードの説明としては、下記①②③のコメントに記載した通りです。
public function handle()
    {
        $count = 10;

        // ①処理の総ステップ数を指定します
        $progressBar = $this->output->createProgressBar($count);

        for ($i = 1; $i <= $count; $i++) {
            // 本来、ここで何か処理を行います

            // ②プログレスバーの表示を1つ分進めます
            $progressBar->advance();
        }

        // ③プログレスバーの表示を完了状態にします
        $progressBar->finish();

        return 0;
    }

処理を実行すると下記のようになります。
※この処理は中身がないため、すぐ終わります。
$ php artisan command:create_ranking
10/10 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


とても簡単に処理の進捗度を表示できるようになりました!

以上となります。
参考にして頂ければ幸いです。

2022年10月3日月曜日

【Python/Flask】AWSのALB(プロキシ)経由でWebシステムにアクセスしたらHTTPSがHTTPに書き換わって盛大にハマった話。

10月 03, 2022
技術部のyuckieee(ゆっきー)です。
今回は、PythonのFlaskフレームワークを使用し、Webシステム構築をした際にハマった事象について、解決策と合わせて、ご紹介しようと思います。

■Webシステム概要

発生事象の説明をする前に、まずは今回開発を行ったWebシステムの概要について共有しておきます。超ザックリとした概要ではありますが、以下のような構成・仕様となっていました。

【システム構成】
・ALB(ロードバランサー)
・EC2(Web/APサーバ)
 - OS:RedHatLinux8.x(EC2)
 - Web/AP:Apache 2.4.xx(mod_wsgiでFlaskと連携)
 - 言語:Python 3.8
 - フレームワーク:Flask
・通信プロトコル
 - クライアント⇔ALB:HTTPS(TCP/443※)
  ※HTTPで接続された場合でもALB側でHTTPSに変換して、リクエスト自体は受け付ける
 - ALB⇔EC2:HTTP(TCP/80)

(構成イメージ)

【Webシステムの仕様】
 ・BASIC認証を使用して、ログイン認証を行う
 ・ログイン認証後、トップページでユーザ情報をセッションに格納して他ページで使用する
 ・他ページはユーザ情報必須のため、トップページ以外へのダイレクトアクセス※は非許可
  ※ブラウザにURLを直接入力したり、お気に入りからアクセスした場合など
 ・ダイレクトアクセス検知時は、トップページに強制遷移させ、必ずトップページ経由とさせる

(画面遷移イメージ)

■発生した事象

Webシステム開発が完了し、システム構成で説明した通りの通信経路となるよう、Webシステムにアクセスする際のURLを以下のように変更しました。(URLはイメージです)

【開発時】http://websystem.com/
※Webシステムのあるサーバに直接アクセスするために設定されたWebシステムのURL
  ↓
【開発完了後】https://alb.websystem.com/
※AWSのALBを介してアクセスするために設定されたWebシステムのURL

そして、動作確認をしようと【開発完了後】のURLにアクセスしたのですが、通常アクセス時のとおりトップページから他ページに遷移しようとしても、トップページへの強制遷移が発生し、他ページに遷移ができない状態に陥りました。
明日からユーザ側で試験利用と言っているのに、軽くパニックです(笑)

(画面遷移イメージ)

■直接原因

Webシステムの仕様から考えると、ダイレクトアクセス検知によってトップページに強制転送されているのだろうと感じていました。 そして、その直感は当たっており、この仕様に絡んで以下2つの仕組みにより起こった問題であることが分かりました。

① 相対URLでの画面遷移

Webシステムのページ遷移に使用するURLは、Flaskのurl_forというメソッドを使用しており、このメソッドで生成されるURLは相対URLがデフォルトとなっており、このWebシステムでもデフォルト指定にて使用していました。
今回の場合、直前のアクセス元であるALBがhttps → httpに書き換えてリクエストを投げてきているため、受け取ったhttp://~から始まる絶対パスを元に、相対指定でURLが作成されて画面遷移されることになりました。

② HTTPSサイトからHTTPサイト遷移によるリファラ削除

突然何だ?!と思うかもしれませんが、このWebシステムではダイレクトアクセスの検知を、リクエストヘッダ内にあるリファラ(遷移元URL情報)の存在チェックで行っていました。
想定では、ダイレクトアクセスの場合、リファラには遷移元URLが入っていないため、ここをチェックすることでダイレクトアクセスの判定が可能と考えていたためです。
ですが、①の画面遷移を受け付けたブラウザは「HTTPSサイト(安全)」から「HTTPサイト(非安全)」への遷移が発生したと検知し、セキュリティリスク回避のためリファラの内容を削除してリクエストしていました。

その結果、リクエストを受け取ったWebシステムは、リファラなし(=ダイレクトアクセス)と判断し、トップページに強制転送していた訳ですね。

リファラという用語がピンとこない方は、公式ページを参照してみてください。
Referer - HTTP - MDN Web Docs

■根本原因

ここまで、実際に事象を引き起こした仕組みについて説明してきました。
ですが、そもそもAWSのALBを経由しなければ、正常に動作していたわけです。なのに、なぜこの事象が発生したのでしょうか?

それは、AWSのALBを経由したことでリクエスト情報が書き換わったためです。

今回でいうと、ALBからEC2(Web/APサーバ)への転送時に通信プロトコルをhttps → httpに書き換えており、転送リクエストを受け取ったEC2(Web/APサーバ)から見るとリクエスト元はALBなので何も間違ってはいませんし、そういう仕様なのです。
そのため、開発者は、プロキシ経由が発生すると分かった時点で、クライアントのリクエスト情報を正確に取得できるよう設計考慮が必要でした。

■解決方法

それでは、実際に根本原因に対処していきましょう。
今回は、クライアントの情報をちゃんと取得できれば解決するわけです。
でも、リクエスト情報が書き換えられてるのにどうすれば?

ご安心ください!

今回使用したALBの場合、経由前のリクエスト情報が別のヘッダーに退避されています。
それが「X-Forwarded-xxx」ヘッダーです。
「X-Forwarded-xxx」ヘッダーには、いくつか種類があり、通信プロトコルが格納されているヘッダーは「X-Fowarded-Proto」となります。

ヘッダー名 説明
X-Forwarded-For プロキシ経由時、クライアントのIPアドレスが格納されます。複数プロキシ経由時はカンマ区切りで表記され、プロキシ経由ごとに右端にIPアドレスが追加されていきます。
X-Forwarded-Host リクエストヘッダー内でクライアントから要求された元のホストを特定するための事実上の標準となっているヘッダーです。
X-Forwarded-Proto プロキシまたはロードバランサーへ接続するのに使っていたクライアントの通信プロトコル (HTTP または HTTPS) を特定するために事実上の標準となっているヘッダーです。

ただし、これらのヘッダーは標準化されたものでなく、書き換え容易且つ、経由するプロキシ(今回はALB)によって挙動が異なる可能性もあるので、ご注意ください。ALBの仕様については以下に記載がありました。
HTTP ヘッダーと Application Load Balancer

なお、「X-Forwarded-xxx」ヘッダーに関する詳細は以下を参照して確認してみてください。
※「X-Fowarded-xxx」は、現状「X-Forwarded」ヘッダに移行されるようなので「X-Fowarded」ヘッダーに関するリンクも張っておきます。
X-Forwarded-Proto - HTTP - MDN Web Docs
Forwarded - HTTP - MDN Web Docs - Mozilla
RFC 7239 Forwarded HTTP Extension

ここまでの説明から、この事象を解決するには「X-Fowarded-xxx」ヘッダーにあるプロトコル情報を使えば良い。というのが、ふんわり頭に浮かんだのではないかと思います。
それでは、このWebシステムでは、どのように「X-Forwarded-Proto」ヘッダーの値で書き換えればよいのでしょうか?
自分でゴリゴリ実装することも可能ですが、今回使用したPythonのフレームワークであるFlaskでは、既に対応するミドルウェアが提供されていました。
それが「X-Forwarded-For Proxy Fix」というミドルウェアです。

X-Forwarded-For Proxy Fix - Werkzeug

このミドルウェアを使用することで、経由(信頼)するプロキシ数に応じてリクエスト元情報の補正を行うことが可能です。 使用方法などの詳細は、上記公式ページを参照して、確認してみてください。
具体的な実装例は以下となります。

from werkzeug.middleware.proxy_fix import ProxyFix

def app():
~略~
    app = Flask(__name__)
    app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
~略~

インスタンス作成時に今回問題となった「X-Forwareded-Proto」ヘッダの値を使用して、通信プロトコル情報が補正されるように設定します。 公式を参照すると、「X-Fowareded-Proto」ヘッダーを補正したい場合は、引数にx_proto=(信頼する)プロキシ経由数の形式で指定すれば良いようです。
ヘッダーごとに引数が異なるようなので、詳細は公式参照ください。

上記を設定することで、無事問題は解決することが出来ました。

<おまけ>
対処方法を色々と探していたのですが、他の方法もあるようでした。
プロキシによっては「X-Forwarded-xxx」ヘッダーが設定されていないなどもあると思うので、参考までに紹介しておきます。 ただし、ヘッダーが取得可能な時は、わざわざ手間を増やしたり、セキュリティリスク高める必要はないと思うので、推奨は致しません。

・直接原因① Flaskが認識するリクエスト元プロトコルを"HTTPS"固定に変更する
Flaskのインスタンス生成時、リクエスト元のプロトコル情報を直接"HTTPS"(固定)に書き換えることで対処する方法です。以下、実装例です。
def create_app():
...省略...
    class SchemeFix:
        def __init__(self, app):
            self.app = app

        def __call__(self, environ, start_response):
            environ['wsgi.url_scheme'] = 'https'
            return self.app(environ, start_response)

    app = Flask(__name__)
    app.wsgi_app = SchemeFix(app.wsgi_app)
    
    return app

・直接原因② リファラが削除されないように設定を変更する
直接原因②はリファラが削除されるのが問題なので、削除されないようにすればいいじゃん!という対策です。対象のWebページ(HTML)に対して、以下のようなメタ情報を定義してリファラの送信制限を緩和させます。
<meta name="referrer" content="origin-when-crossorigin">

上記content="origin-when-crossorigin"の記載が該当箇所になります。
この設定の場合、同一のプロトコル水準 (HTTP→HTTP, HTTPS→HTTPS) で同一オリジンのリクエストを行う場合はオリジン、パス、クエリー文字列が送信され、オリジン間リクエストや安全性の低下する移動先 (HTTPS→HTTP) ではオリジンのみを送信します。
今回のダイレクトアクセスチェックでは、リファラの値有無を確認しているため、オリジンのみでも値があれば事象は解消可能となります。
全ての情報が必要な場合はcontent="unsafe-url"を設定することでリファラを取得可能ですが、安全性の面から非推奨となっているため、使用は回避するべきだと思います。

Referrer-Policy - HTTP - MDN Web Docs

■まとめ

Webシステムだけでなく、インフラも含めた全体的な視点でシステム構築をすることの難しさと面白さを知りました。
徐々に仕組みを理解し、謎が解けた時は本当に楽しいですね。
今後も幅広い領域の経験を積みながら、レベルアップしていければと思います。

Visual Studioでデバッグ時に「ID XXXXXのプロセスは実行されていません」のエラーからの復帰方法。

10月 03, 2022
オフィス狛 技術部のHammarです。

先日Visual StudioでIIS Expressからデバッグ時に、急に「ID XXXXXのプロセスは実行されていません」と表示されるようになり、全くデバッグが出来なくなりました。
いろいろ先人の復帰方法を見ながら試してみても、どうもうまくいかず軽くハマっていたんですが、意外と簡単な方法で復帰したので、お知らせしたいと思います。

実行環境

  • Windows10
  • Visual Studio 2017
  •       - IIS Express(Google Chrome)よりデバッグ

事象

先日までは普通にデバッグ開始ボタンよりデバッグできていて、ある日同じようにデバッグ開始したところ、起動してブラウザは開くんですが、「このサイトにアクセスできません」のエラーが発生して、上手く接続できていない状態になっていました。
その時、Visual Studioもデバッグモードになっているものの、設定したデバッグポイントが、「ブレークポイントは、現在の設定ではヒットしません。このドキュメントのシンボルが読み込まれていません。」と表示されてしまっていました。
なので、一旦デバッグを停止し、もう一度デバッグを開始したところ、下記のように「ID XXXXXのプロセスは実行されていません」が表示され、以降はデバッグをやり直しても、Visual Studioを起動しなおしても同エラーが出続けてしまう状態に陥ってしまいました。

解消方法

調べていくと、似たような状態になっている記事をいくつか見つけたので、手っ取り早くできる方法を2つ3つやってみたのですが、どうも解決しませんでした。
(「IIS Expressサーバーを起動できません。」というエラーと一緒に発生している記事の内容を主に試してみたので、ちょっと今回の事象とは微妙に違ったのかもしれません)
で、調べをすすめていくと、簡単に解消できた方法が以下の内容です。

■csprojファイルの下記3行を削除する

<DevelopmentServerPort>xxxxx</DevelopmentServerPort> 
<DevelopmentServerVPath></DevelopmentServerVPath>
<IISUrl>http://localhost:xxxxx/</IISUrl>

上記はデバッグ時に起動するIIS ExpressのURLや、仮想開発サーバーのポート、パスを設定している部分なのですが、こちら3行を削除して、プロジェクトをリロードすれば、なんとあっさりデバッグが正常にできるようになりました。
ちなみに、上記を削除しても、再度デバッグ実行したタイミングで、記述が自動的に再作成されるので、特に問題は無いようです。

また本エラーの解消方法調べていくと、結構対応方法が多様でバラバラなので、もしかしたら一概に、この方法で解決します!とはならないかもしれないのですが、とりあえず簡単に試せる方法ではあるので、もし同様のエラーに陥った方は、まずは一旦試していただければと思います。

node.jsでnockライブラリを使って外部API呼び出しをモック化させる。

10月 03, 2022
オフィス狛 技術部のHammarです。

node.jsでAPIを実装しているプロジェクトにユニットテストを実施することになり、いろいろと自動化させています。
このユニットテストにおいて、外部のAPIを呼び出す処理は、実際に外部APIを呼ばずに、APIモックを作成してテストするということになり、今回nockというライブラリを使って簡単にモックを作成ができたので、ご紹介したいと思います。

実行環境

※ユニットテストではjestを使います
  • Node.js  v12.14.0
  • jest           v27.5.1
  • nock        v13.2.4


以下代表的なGETとPOSTのテストについてサンプルを書いてみます。

GETの場合

テスト対象となるプログラムの例として、以下GETリクエストのプログラムを作成します。
■testGetRequest.js
const request = require('request');

exports.getUserInfo = async () => {
  // リクエスト作成
  const requests = {
    url: 'https://test.com/users',
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
    },
    json: true,
  };

  // リクエスト送信
  return new Promise((resolve, reject) => {
    request(requests, async (error, response, body) => {
      if (error) {
        reject(error);
      } else if (response.statusCode !== 200) {
        reject(response);
      } else {
        resolve(body);
      }
    });
  });
};
上記をnockを使ったテストコードを書くと以下のようになります。
■testGetRequest.test.js
const nock = require('nock');

describe('ユーザー情報取得のTest', () => {
  let resData;

  beforeEach(() => {
    // レスポンス
    resData = {
      user_id: '1',
      user_name: 'テスト太郎',
    };
  });

  afterEach(() => {
    nock.cleanAll();
    jest.clearAllMocks();
  });

  it('ユーザー情報取得リクエスト', async () => {
    // APIリクエストのモック
    nock('https://test.com')
      .get('/users')
      .reply(200, resData);

    const repos = await testGetRequest.getUserInfo();

    expect(repos.user_id).toEqual('1');
    expect(repos.user_name).toEqual('テスト太郎');
    expect(nock.isDone()).toBe(true);
  });
上記のnock('https://test.com').get('/users')が、テスト対象プログラム(testGetRequest.js)のgetUserInfo()で呼んでいるAPIリクエストのパスと一致、かつHTTPメソッドも同じにすることによって、resDataに記述したモックするレスポンス内容を返すことができるようになります。
ちなみにnock.isDone()で、リクエストをしたかどうかを判定できます。

続いて、POSTリクエストの例が以下になります。

POSTの場合

■testPostRequest.js
const request = require('request');

exports.entryUserInfo = async (userId, userName) => {
  // リクエストBODY
  const bodyData = {
    user_id: userId,
    user_name: userName,
  };

  // リクエスト作成
  const requests = {
    url: 'https://test.com/users',
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: bodyData,
    json: true,
  };

  // リクエスト送信
  return new Promise((resolve, reject) => {
    request(requests, async (error, response, body) => {
      if (error) {
        reject(error);
      } else if (response.statusCode !== 200) {
        reject(response);
      } else {
        resolve(body);
      }
    });
  });
};
上記のテストコードを書くと以下のようになります。
■testPostRequest.test.js
const nock = require('nock');

describe('ユーザー情報登録のTest', () => {
  let userId;
  let userName;
  let reqBody;
  let resData;
  
  beforeEach(() => {
    userId = '1';
    userName = 'テスト太郎'

    reqBody = {
      user_id: userId,
      user_name: userName,
    };
    // レスポンス
    resData = {
      message: '登録完了しました',
    };
  });

  afterEach(() => {
    nock.cleanAll();
    jest.clearAllMocks();
  });

  it('ユーザー情報登録リクエスト', async () => {
    // APIリクエストのモック
    nock('https://test.com')
      .post('/users', reqBody)
      .reply(200, resData);

    const repos = await testPostRequest.entryUserInfo(userId, userName);

    expect(repos.message).toEqual('登録完了しました');
    expect(nock.isDone()).toBe(true);
  });
});
GETの時と同様に、リクエストのパスとHTTPメソッドを一致させることでモック作成が可能となります。
POSTの場合は、上記.post('/users', reqBody)でリクエストボディの内容を設定できます。
ちなみに、テストを実行した後は、nock.cleanAll()でモックをクリアする必要がありますので、忘れずに。

上記のように、思っていた以上に楽にAPIリクエストのモックを作成することが出来るようになりました。
公式の情報を確認すると、さらに細かくモック設定できそうなので、細かなテストにも対応できそうです!

2022年9月30日金曜日

Photoshopの「書き出し形式」で数倍に書き出した時に画質が劣化してしまうオブジェクト・劣化しないオブジェクトの違い。

9月 30, 2022


こんにちは、オフィス狛 デザイン部のSatoです。


初歩的な話なのですが、Photoshopの書き出し形式機能で小さい画像を2倍など大きいサイズを指定して書き出すと一部のオブジェクトの画質が劣化し、ぼやけてしまうことがあります。

過去に無知故に2倍(2x)で書き出せるなら…と作成したデータを2xで書き出したところ、画像の一部だけが少し劣化してぼやけてしまったことがありました。

この時、画像全体の画質が劣化する訳では無く画像の一部だけ劣化するのが不思議で、画質が落ちる箇所・画質が落ちない箇所の差が何なのかが気になりました🤔

今回は、Photoshopの「書き出し形式」で、元サイズより大きい書き出した時に画質が落ちてしまうオブジェクト・綺麗なオブジェクトの違いについて解説しようと思います。

Photoshopの書き出し形式機能自体は、私が書いた上記の記事でも解説しています。
合わせて読んでいただけると、書き出し形式機能についての基礎もわかりますよ。




書き出し形式機能を利用して、拡大して書き出した場合に画質が落ちぼやけるオブジェクト・画質が落ちずぼやけないオブジェクトの一覧表を作ってみました。

この一覧表自体、Photoshopの書き出し機能を利用しキャンバスサイズの3倍(3x)のサイズで書き出してみたので、クリックして拡大表示してみてくだい👇

同じ画像内でも、こんなに画質が変わってきます。

ぼやけないカテゴリの画像は画質が落ちていませんが、ぼやけるカテゴリに入れている画像はかなり画質が劣化してしまいにじんでしまっています😞


さて、この画質が劣化する・しないの違いは何かというと……。
スマートオブジェクト以外は、「ラスタ形式のオブジェクト」か「ベクタ形式のオブジェクト」かです。


ラスタ形式のオブジェクトは、色のついたピクセルで作られたデータで、拡大するとぼやけてしまいます。

ベクタ形式のオブジェクトは、点と曲線の数値で作られたデータで、拡大してもぼやけません。

ラスタ形式とベクタ形式についての解説は、アドビのラスター&ベクトルが分かりやすいと思います。ぜひ読んでみてください。



しかし、上の説明で
「Photoshopのようなグラフィックソフトで作成するオブジェクトは全部ラスタ形式のはずでは?」
と疑問に思われる方もいらっしゃるかもしれません。

実はグラフィックソフトのPhotoshopでも、ベクタ形式のオブジェクトを扱っています!


Photoshopのベクタ形式のオブジェクトをご紹介します👇

ペンツールやシェイプツール(図形ツール・カスタムシェイプ)などで製図した図形「シェイプ
シェイプで製図した際に作られるレイヤー「シェイプレイヤー」は下記のような見た目になります。



字ツールで文字を打ち込んだ際に作成される「テキスト
テキストを入力した際に作られるレイヤー「テキストレイヤー」は下記のような見た目になります。



Illustratorで作成したオブジェクトをPhotoshopのレイヤーにコピー&ペーストした際に出るペースト形式選択で、スマートオブジェクトを選択した際に作成される「ベクトルスマートオブジェクト




ベクトルスマートオブジェクトをペーストした際に作られるレイヤー「ベクトルスマートオブジェクトレイヤー」は下記のような見た目になります。



以上、3つのオブジェクトは全てベクタ形式のオブジェクトです。


また、ラスタ形式ではありませんが、「元の画像のサイズが書き出したサイズより大きいスマートオブジェクト」は縮小しても元のサイズのデータを保持している為、劣化しません。

しかし、スマートオブジェクトはあくまで元のサイズのデータを保持しているだけですので、「元の画像のサイズが書き出したサイズより小さいスマートオブジェクト」は劣化してしまいます。
スマートオブジェクトも完璧という訳ではないのです。



まとめると、
シェイプレイヤー」「テキストレイヤー」「ベクトルスマートオブジェクトレイヤー」「元の画像のサイズが書き出したサイズより大きいスマートオブジェクト
以上の4つのオブジェクトであれば、ラスタライズしない限りは画質が落ちる事なく等倍で書き出すことが可能です。

逆に「通常レイヤーのオブジェクト」や「元の画像のサイズが書き出したサイズよりも小さいスマートオブジェクト」などのオブジェクトが含まれるキャンバスを等倍で書き出すのは、画質が落ちてしまうので、やめた方が良いでしょう。

Photoshopを使われる方は、覚えておいて損はないと思います🙌

【Sourcetree】カスタムアクションで特定コミット間の差分ファイルをサクッと抽出する。(Mac版)

9月 30, 2022

技術部のyuckieee(ゆっきー)です。
色々なプロジェクトで開発を行っていて、ちょいちょい発生する作業で面倒だなって思っていることがありました。それは納品物やリリース対象物の準備です。

何かというと、運用保守中に追加開発などが発生した場合に、プログラムの差分ファイルのみを納品物やリリース対象として準備する必要があります。
これ、中々に面倒くさいんですよね。間違えたら大変だし、毎回ドキドキしちゃいます(笑)

そこで、出来るだけ間違いが起こらないように自動化出来ないか探してみた結果、私がいつも使っているGit管理ツール「Sourcetree」のカスタムアクションを試してみたら良さげだったので、ご紹介しようと思います。

■作成したカスタムアクションの概要

まずはイメージ共有のため、今回作成したカスタムアクションの概要を説明します。
ざっくりした動作仕様としては「Sourcetreeで現在選択されているリポジトリ-ブランチに存在するコミット間の差分ファイルを取得する。」です。 利用イメージ(方法)は以下のとおり。

[利用イメージ(方法)]
Sourcetreeの該当ブランチの履歴から差分ファイルをとりたいコミットを選択のうえ、カスタムアクションを実行します。 差分を取るためにコミットは2つ選択。ただし、1つだけ選択した場合でも、選択したコミットから最新コミットまでを対象と実行します。
実行時は、経過が分かるようにダイアログにログが表示されます。
実行後、ログ記載の出力先に差分ファイル(zip)、差分ファイル一覧、実行結果ログが格納されます。
以上です!

■カスタムアクションの実装方法

実装完了までの大まかな流れは以下のとおりです。

[実装の流れ]
① 呼び出しスクリプト作成
② カスタムアクション登録
③ 動作確認

それではサクッと詳細の説明に入ります。

① 呼び出しスクリプト作成

最初にカスタムアクションで呼び出されるスクリプトを作成します。
今回はシェルスクリプト(sh)で作成し、実際に作成したコードは以下のとおりです。(スクリプトの引数は②で説明しますが、$1にリポジトリ名、$2$3にコミットIDが指定されています)
#!/bin/sh
#########################################
# ファイル名: export_diff.sh
# 処理内容 : 該当コミット間の差分ファイル出力
#########################################

#########################################
# 変数定義
#########################################
repo_name=$(basename "$(pwd)")
branch_name=$(git rev-parse --abbrev-ref @)
file_name="$(basename "$(pwd)")_diff.zip"
list_name="$(basename "$(pwd)")_diff_list.log"
today="$(date +"%Y%m%d")"
dir_name="export_diff/$repo_name/$branch_name/$today"
log_file=export_diff_result.log

#########################################
# 出力処理 
#########################################
export_diff() {
    # リモートブランチと同期を実行した上で、出力対象をダイアログ表示
    echo ""
    echo "出力を開始します($(date))"
    echo "-------------------"
    echo "■リモートブランチと同期"
    echo "-------------------"
    git pull
    echo ""
    echo "-------------------"
    echo "■出力対象"
    echo "-------------------"
    echo "レポジトリ:${1}"
    echo "ブランチ:${branch_name}"
    echo ""
    echo "-------------------"

    # 差分ファイル出力処理
    if  [ "$3" != "" ]; then
        # 2コミット間の差分ファイルを出力
        git archive --worktree-attributes -o "$file_name" "$2" $(git diff --name-only --diff-filter=du "$3" "$2")
        echo "■差分対象に含まれるコミット"
        echo "-------------------"
        git log "$3"..."$2" --pretty=format:"%h : %s"
        echo ""
    elif  [ "$2" != "" ]; then
        # 選択されたコミットから最新コミットまでを出力
        git archive --worktree-attributes -o "$file_name" HEAD $(git diff --name-only --diff-filter=d "$2" HEAD)
        echo "■差分対象に含まれるコミット"
        echo "-------------------"
        git log "$2"...HEAD --pretty=format:"%h : %s"
        echo ""
    else
        # 上記以外の場合は正しく出力できないためエラーとする
        echo "【ERROR】指定できるコミット数は1又は2です。"
        echo "-------------------"
        exit
    fi

    # 出力された差分ファイルの一覧をリスト出力(ディレクトリのみの表示は除く)
    zipinfo -1 "$file_name" | grep -v /$ > "$list_name"
    echo ""
    echo "-------------------"
    echo "■出力ファイル一覧"
    echo "-------------------"
    cat "$list_name"
    echo ""

    # 出力されたファイル移動(混乱しないように)
    mv "$file_name" ~/"$dir_name"
    mv "$list_name" ~/"$dir_name"
    echo "-------------------"
    echo "■ファイル出力先"
    echo "-------------------"
    echo "${HOME}/$dir_name"
    echo "-------------------"
    echo ""
    echo "出力が完了しました($(date))"
}

#########################################
# メイン処理
#########################################
if [ $# -gt 3 ]; then
    # 選択したコミットが4つ以上の場合は正しく出力できないためエラーとする
    echo "【ERROR】指定できるコミット数は1又は2です。"
    echo "-------------------"
    exit
fi

# 出力ディレクトリを作成
mkdir -p ~/"$dir_name"

# 出力処理を呼び出し
export_diff "$1" "$2" "$3" 2>&1 | tee "${HOME}/$dir_name/$log_file"

...長い!!!(笑)
ダイアログ表示やファイル移動等の余分なコードが含まれていますが、重要なのは以下に抜粋したコマンドです。
git archive --format=zip -o release.zip  --worktree-attributes "$2" $(git diff --name-only --diff-filter=d "$2" "$3")

上記は、Gitが提供している「Git管理対象のみを圧縮形式で出力してくれる」コマンドです。
出力対象の指定がない場合は、指定したブランチ又は、コミット時点のGit管理対象の全ファイルが出力されますが、ファイル名及び、パスを指定することで出力対象ファイルを絞り込むことが出来ます。
今回は、このファイル名・パス指定にgit diffコマンドを使用することで差分ファイルの抽出を実現しました。
以降、git diffgit archive、それぞれのコマンドについて紹介します。

git diffコマンド
差分ファイルを抽出するために使用したコマンドです。
オプションを省いた場合のシンプルな構成は以下のとおりです。
git diff <変更前のコミットID> <変更後のコミットID>
例) git diff 1668413042b61f16f0f353fd66b53cd4c0dbc5d5 2d13b702a783bb2c8252ba07f5410619ee3a0186 
なお、コミットIDの指定順序が逆転してしまうと「削除したファイル」と「新規作成されたファイル」の識別が逆転してしまう等、その後の出力に影響があるので注意してください。

以降に私が指定が必要そうだと考えたオプションについて補足します。

[コマンドオプション]
--name-only  (必須)
差分比較結果としてファイル名(パス含む)のみを返す指定です。
本オプションを指定しない場合、ズラズラとコミット間の比較内容が表示されるのですが、それがgit archiveに引数として渡ってしまうとエラーになりますので指定必須です。

--diff-filter=<出力対象の指定>  (必須)
例)--diff-filter=ACMR
指定することで、差分取得対象を絞り込むことが出来ます。
このオプションは、大文字で記述すると「出力対象」を、小文字で記述すると「出力除外対象」を指定することが出来ます。それぞれのオプション文字列の説明を下表にまとめました。

指定可能なオプション文字列    
オプション
文字列
説明
A/a 追加されたファイル
C/c コピーされたファイル
D/d 削除されたファイル
M/m ファイル内容又は、ファイル属性が変更されたファイル
R/r ファイル名が変更されたファイル
T/t ファイルタイプ(通常ファイル、シンボリックファイル、サブモジュール等)が変更されたファイル
U/u マージされていないファイル(コンフリクトファイル確認時に使用されるようです)
B/b ペアリングが壊れているファイル
X/x 不明な変更タイプ(通常は有り得ないようです)

今回は差分に「削除されたファイルのパス」が返ってしまうとgit archive時に対象無しのエラーになってしまうので「明確に出力対象としたいファイルのみ」を指定しました。
この指定方法以外に--diff-filter=dとすることで、削除されたファイルを「出力対象外」にすることも可能です。

git archiveコマンド
指定したコミット時点のGit管理対象ファイルを圧縮出力してくれるコマンドです。(ブランチ指定も可能ですが、Sourcetreeから引数として受け取れないので説明割愛します)
指定したコミットID時点のファイルが出力対象となりますので、当たり前ではありますが、必ず変更後のコミットIDを指定するようにしてください。
git archive <変更後コミットID>
例) git archive 1668413042b61f16f0f353fd66b53cd4c0dbc5d5

[コマンドオプション]
--format=<圧縮形式>   (任意)
例) --format=zip
出力される差分ファイルの圧縮形式として、zip形式又はtar形式が指定可能です。
このオプションが指定されていない場合は、ファイル名に付与された拡張子から推測され、推測ができない場合はtar形式となるようです。
今回はzip形式で出力したいため、念のため、フォーマットも指定しておきました。

-o <ファイル名> (必須)
※--output=<ファイル名>としても同じ
例) -o export_diff.zip
本オプションを指定しない場合は、標準出力(コンソール)に結果が出力されます。
結果をファイルに出力したい場合は、本オプションを付与した上で、 出力ファイル名称を指定する必要があります。

--worktree-attributes(任意)
Git管理対象となっているファイルでも、今回の出力対象として含めたくない!というファイルがある場合は、本オプションを使用します。 例えば、本番環境へのリリース対象としてReadMeや環境変数のサンプルファイルなどは不要で、これらを除外したい、と言う時に使います。

なお、除外対象は、リポジトリ直下にある.gitattributesファイルに記述することで認識されます。
以下のように対象ファイル export-ignoreと書き込めばOKです。
README.md export-ignore
*.example export-ignore
試してみた感じでは、今回の実装内容であれば、本ファイルをリモートリポジトリにマージしていなくても適用されるようでした。

--prefix=<プレフィックス文字列/> (任意)
例)--prefix=prod/
今回は使用しませんでしたが、出力されるファイルを特定のディレクトリでラップしたい場合に指定します。 例えば--prefix=prod/と入力しておくと、出力されたzipを解凍した際にprodディレクトリの配下に差分ファイルが格納されることになります。
試してみたところ--prefix=prod/app1/など、ディレクトリを多階層で指定することもできるようでした。

ザックリ説明はここまでとなりますが、各コマンドには他にもオプションがありますので、公式ドキュメントを参考にしながら試してみるのも面白いと思います。
git-diffコマンド (Documentation - Reference)
git-archiveコマンド (Documentation - Reference)

なお、スクリプトについては、私のやりたように書いていますが、自分仕様にカスタマイズいただければと思います♪(見やすさ重視でechoの乱れ咲きさせてたり...笑)

② カスタムアクションの登録

①で作ったスクリプトを実際にSourceTreeに登録します。
Sourcetreeを開き、左上の[Sourcetree]>[環境設定...]を押下し、設定画面を開きます。そのうえで、右上にある[>>]>[カスタムアクション]を選択してカスタムアクションの定義画面を開き、左下の[追加]ボタンを押下してください。

[注意事項]
Sourcetree日本語版では、不具合(確認したところ、4.1及び、4.2.0は駄目でした)により上記の流れで画面を開いた場合、カスタムアクションが非活性となっており選択できないようです!(笑)
選択できない場合は、対処法として2パターン有り、1つ目が言語を英語に切り替えてSourcetreeの再立ち上げを行う方法、2つ目がトップ画面で任意のリポジトリ右クリックメニューから[カスタムアクション]>[編集...]を選択する方法です。どちらかお好きな方をお試しください。

登録画面が開きますので、必要事項を入力し[OK]を押下して、登録を完了させます。

【登録内容】
 メニューキャプション:差分出力(zip) ※1
 別のウィンドウで開く:チェックオフorオン どちらでも可 ※2
 フル出力を表示:チェックオン ※3
 実行するスクリプト:①で作成したスクリプトを選択
 パラメータ:$REPO $SHA ※4
※1:メニューに表示される名称なので、お好きなものを指定してください。
※2:選択したファイルをVSCodeなどの別ウィンドウで表示したい場合は、このチェックをオンにします。なお、このチェックをオンにすることでダイアログも別ウィンドウとなるのでダイアログの拡大縮小が可能になります。
※3:チェックがオフの場合は、エラーのみがダイアログ表示されます。今回は全て表示したいのでチェックをオンにします。
※4:$SHAについては、複数コミットを選択した場合、コミットタイミングが新しいコミットIDから順に、選択した全てのコミットIDがスクリプトの引数として渡されます。(順番の選択順には依らないようです)

③ 動作確認

最後に動作確認を行います。
今回登録したカスタムアクションは、できるだけ汎用的に作っているので、どのリポジトリ、ブランチでも対応可能(な、はず)ですので、皆さんも動作確認してみてください。
実行方法は、「■作成したカスタムアクションの概要」と同様の流れになるため、説明は割愛しますが、コミットの選択方法について、少しだけ補足します。

[実行時の補足事項]
Sourcetreeのデフォルトでは、履歴に「すべてのブランチ」のコミットが表示されています。
この場合、親ブランチの差分を取得しようとした際に、間違って未マージの子ブランチのコミットを選択してしまっても、それらが差分出力対象となっているようでした。(これは--diff-filter=uとしても結果は変わりませんでした)

そのため、履歴の表示フィルターを使用して「現在のブランチ」のみの表示にしておくと、ミスがなくて良いのかなと思います。

ここまでで、カスタムアクションの実装は完了となります!
これでサクッと差分ファイルの抽出ができるようになったので、これからのリリース準備作業も捗りそうです♪
ただし、自動化しても、間違いがないかのダブルチェックは怠らず!!

■まとめ

Sourcetreeやgitには、沢山の便利機能やコマンドがあるのに活用できていないなぁ、と、改めて感じました。
特に今回使ったSourcetreeのカスタムアクションは、利用の幅が広く、もっと色々な応用がききそうだなと感じたため、継続して色々と試してみたいと思います。