megutech

自身の備忘録として主にWEBサーバー周りの技術について投稿しています。

FPDIでPDF1.5以降の圧縮されたファイルを何とかしたい

FPDIで色々していたら This PDF document probably uses a compression technique which is not supported by the free parser shipped with FPDI.というエラーが。

どうやらPDF1.5以降の圧縮されたPDFの回答は無償版のFPDIではできないとのこと。
んじゃライセンス買おうかと思ったけど、当然高い。

ということで無償で何とかする方法が以下。

環境

Service Version
CentOS 7.3
PHP 7.2.2

対応

QPDFインストール

必要なパッケージをインストール

$ yum install libjpeg-turbo libjpeg-turbo-devel

どうやらCentoOS7.3のgcc4.8では9.1.1のビルドが通らないらしいので、9.1.0を使用。

$ cd /usr/local/src
$ wget https://github.com/qpdf/qpdf/releases/download/release-qpdf-9.1.0/qpdf-9.1.1.tar.gz
$ tar xvfz qpdf-9.1.0.tar.gz
$ cd ./qpdf-9.1.0
$ ./configure --prefix=/usr/local/qpdf/v9.1.0
$ make
$ make install

パスを通す

$ cd /usr/local/qpdf/
$ ln -s ./v9.1.0 ./current
$ cd ./current/bin
$ ln -s /usr/local/qpdf/current/bin/qpdf 

PHPから使ってみる

以下適当なサンプル。

<?php

use setasign\Fpdi\TcpdfFpdi;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

/**
 * 圧縮の解除
 *
 * @param string $inputPath
 * @param string $outputPath
 * @return string 圧縮解除したファイルのパス
 */
function uncompress(string $inputPath, string $outputPath): string
{
    if (! setlocale(LC_CTYPE, "UTF8", "ja_JP.UTF-8")) {
        throw \Exception('skip setlocale() failed');
    }

    $outputPath = escapeshellcmd($outputPath);
    $inputPath = escapeshellcmd($inputPath);
    $command = "qpdf --stream-data=uncompress --force-version=1.4 {$inputPath} {$outputPath}";

    $process = Process::fromShellCommandline($command);
    $process->run();

    if (! $process->isSuccessful()) {
        throw new ProcessFailedException($process);
    }

    return $outputPath;
}


$pdf = new TcpdfFpdi();
$pdf->setSourceFile(self::uncompress('/your/inputPath', '/your/outputPath'));

こんな感じで良い感じに書いてください。

雑感

Ghostscriptとかでやる方法などもあるみたいだけど、とりあえずこれで動いてるから一旦終わり!

あとPHPのサンプルは適当なので悪しからず。実際はこんな感じのコードをLaravelで書いて確認しました。

PHPでコマンド実行しようとしたら日本語が消失した

PHPでコマンドを実行する時はOSコマンドインジェクション対策としてescapeshellcmdescapeshellargなどでエスケープすることが多いと思うが、その際にマルチバイト文字が含まれているとマルチバイト文字が空文字に変換されてしまって困った。

環境

Service Version
PHP 7.2.2

解決

実行前にロケール情報を設定してあげることで回避できた。

<?php

if (! setlocale(LC_CTYPE, "UTF8", "ja_JP.UTF-8")) {
    throw \Exception('Not exist Locale.');
}

$cmd = escapeshellcmd($cmd);

雑感

そもそもマルチバイトがコマンドに入るような設計がどうなの。

Safariでは206 Partial Contentに対応していないとVideoタグでmp4が再生できない

S3に保存されたmp4動画をPHPを経由して配信していたのだが、iOS/MacSafariだと再生できない事が判明した。

原因はVideoタグなどのリソースはRangeヘッダーを付けてリクエストが投げられるのだが、ここで正しく206 Partial Contentを返してあげないとSafariでは動画を再生できないというものだった。

chromeとかは再生してくれるのにね。流石Safari。すごいぞSafari。滅びればいいのに。

で、以下どうやって対策をとったかという話。

環境

Service Version
PHP 7.2.2
Laravel 6.5.0

問題のコード

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class ResourceController extends Controller
{
    public function download()
    {
        return Storage::disk('s3')->download('your/file/path/movie.mp4', 'movie.mp4');
    }
}

対応

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\StreamedResponse;

class ResourceController extends Controller
{
    public function download()
    {
        $disk = Storage::disk("s3");
        $size = $disk->size('your/file/path/movie.mp4');

        $start = 0;
        $status = 200;
        $length = $size;

        $headers = [
            'Content-Type' => 'video/mp4',
            'Content-Length' => $size,
            'Accept-Ranges' => 'bytes'
        ];

        if ($range = Request::server('HTTP_RANGE', false)) {
            list($param, $range) = explode('=', $range);
            if (strtolower(trim($param)) !== 'bytes') {
                abort(400);
            }

            list($from, $to) = explode('-', $range);
            if ($from === '') {
                $end = $size - 1;
                $start = $end - intval($from);
            } else if ($to === '') {
                $start = intval($from);
                $end = $size - 1;
            } else {
                $start = intval($from);
                $end = intval($to);
            }
            $length = $end - $start + 1;

            $headers['Content-Length'] = $length;
            $headers['Content-Range'] = sprintf('bytes %d-%d/%d', $start, $end, $size);
            $status = 206;
        }

        $response = new StreamedResponse(function() use ($disk, $start, $length) {
            $stream = $disk->readStream('your/file/path/movie.mp4');
            fseek($stream, $start, SEEK_SET);
            echo fread($stream, $length);
            fclose($stream);
        }, $status, $headers);

        $response->headers->set(
            'Content-Disposition',
            $response->headers->makeDisposition(
                'attachment',
                "movie.mp4",
                str_replace('%', '', Str::ascii("movie.mp4"))
            )
        );

        return $response;
    }
}

上記は実際に書いたコードを何とか一つにまとめたものです。

動作確認はしてないです。(もちろん上記のようにまとめる前の本チャンコードでは動作確認取れてます。)

まあこんな感じ。

めんどくさ。

無停止でCentOS7 on EC2のEBSストレージを拡張する

遅ればせながら今回の案件で初めてEC2を触ることに。

そこでまずはステージング環境を作ろうとごにょごにょしてたら、ディスクを8GiBで作成してしましました。

ステージング環境とはいえもう少し欲しかったのでディスクを拡張しようとしたらAWSではなんと無停止で拡張可能とのこと!再起動も不要!マジ最高!ってなったんですが、やり方が分からず詰まったりしたので備忘録か。

AWSコンソールからEBSボリュームを増やす

AWSコンソールからEC2のボリュームサイズを変更する。

変更後ステータスがin-use-optimizingが100%になるのを待ちましょう。

いや、待つ必要があるかは分かんないんですが。この辺誰か情報ください。

私は8 -> 40GiBに変更で1時間ほどかかりました。

f:id:citrus_soda:20191121122039p:plain
AWS Console

f:id:citrus_soda:20191121122102p:plain
AWS Console

f:id:citrus_soda:20191121122116p:plain
AWS Console

web上での操作は以上です。

拡張されたボリュームを適用する

拡張が完了したので早速sshログイン後確認してみよう。

$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
/dev/nvme0n1p1   8.0G  896M  7.2G   11% /

しかし拡張されていない。

おかしい・・・。何かが、あったに・・・違いない・・・。

「そう、誰もパーティショニングを拡張していないのである!」

まずは落ち着いてブロックデバイスを見てみる。

$ lsblk
NAME        MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
nvme0n1     259:0    0  40G  0 disk
└─nvme0n1p1 259:1    0   8G  0 part /

lsblkでブロック確認するとdisk側がpartitionより大きくなって、先ほど指定したサイズに変更されていることが分かる。

というわけでnvme0n1のパーティション、nvme0n1p1をgrowpartで拡張する。

nvme0n1p1をどうやって拡張するんだろうと思ったら、nvme0n1のパーティション1(nvme0n1p1)を拡張するので、/dev/nvme0n1 1と指定するみたい。

$ sudo growpart /dev/nvme0n1 1
CHANGED: partition=1 start=2048 old: size=16775168 end=16777216 new: size=83883999,end=83886047
$ lsblk
NAME        MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
nvme0n1     259:0    0  40G  0 disk
└─nvme0n1p1 259:1    0  40G  0 part /

これでパーティションがディスクのサイズまで拡張された。

ただこの時点ではファイルシステムにまで影響していないので、最後にファイルシステムを拡張する。

$ sudo xfs_growfs /dev/nvme0n1p1

最後にちゃんと拡張されているか確認して完了だ。

$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
/dev/nvme0n1p1    40G  897M   40G    3% /

以上ーー!

参考

ダウンタイムゼロでEC2(CentOS7)のEBSボリュームを拡張する方法

Laravel6でMinIOを使う

Laravelではドキュメント通りに必要なパッケージをインストールすればS3はすぐに使うことができるようになる。

ただ開発環境はMinIOで代用したい場合などは、少し工夫が必要となる。

というのもバケット指定方法が違うのだ。

ということで config/filesystems.phps3とは別の設定を追加する。

config/filesystems.php

<?php
return [
     // ~
    'disks' => [
        // ~
        'minio' => [
            'driver' => 's3',
            'endpoint' => env('MINIO_ENDPOINT', 'http://127.0.0.1:9000'),
            'use_path_style_endpoint' => true,
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
        ],
    ],
    // ~
];

ここで重要なのが、endpointuse_path_style_endpointである。

これを指定することでMinIOに対応してくれる。

ただしdiskの指定をminioにする必要があるため、ファサードを書き換えてしまったり、何か処理を噛ませるといいだろう。そのあたりは各々のプロジェクトで考えてほしい。

おまけ

参考までにファサード書き換えで本番環境と開発環境でs3の向き先を変える方法を記載しておく。

config/app.php

<?php
return [
    // ~
    'providers' => [
        // ~
        App\Filesystem\FilesystemServiceProvider::class,
        // ~
    ],
    
    'providers' => [
        // ~
        // 'Storage' => Illuminate\Support\Facades\Storage::class,
        'Storage' => App\Filesystem\Storage::class,
        // ~
    ],
    // ~
];

app/Filesystem/FilesystemServiceProvider.php

<?php

namespace App\Filesystem;

use Illuminate\Support\ServiceProvider;

class FilesystemServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind('filesystem.expansion', function($app) {
            return new \App\Filesystem\FilesystemManager($app);
        });
    }
    
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

app/Filesystem/Storage.php

<?php

namespace App\Filesystem;

use Illuminate\Support\Facades\Storage as OrgStorage;

/**
 * @method static \Illuminate\Contracts\Filesystem\Filesystem disk(string $name = null)
 *
 * @see \Illuminate\Filesystem\FilesystemManager
 */
class Storage extends OrgStorage
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'filesystem.expansion';
    }
}

app/Filesystem/FilesystemManager

<?php

namespace App\Filesystem;

use Illuminate\Filesystem\FilesystemManager as OrgFilesystemManager;

/**
 * @method static \Illuminate\Contracts\Filesystem\Filesystem disk(string $name = null)
 *
 * @see \Illuminate\Filesystem\FilesystemManager
 */
class FilesystemManager extends OrgFilesystemManager
{
    /**
     * Get a filesystem instance.
     *
     * @param  string|null  $name
     * @return \Illuminate\Contracts\Filesystem\Filesystem
     */
    public function disk($name = null)
    {
        if (config('app.debug') && ($name === 's3')) {
            $name = 'minio';
        }
        return parent::disk($name);
    }
}

S3互換のMinIOをCentOS7にインストールする

S3を使ったサービスを開発したいが、開発中は課金が発生してほしくない。
そんなわがままなあなたにMinIOが答えてくれるだろう。
今回はこのMinIOをCentOS7にインストールし、起動スクリプトを書くまでを記す。

ユーザーの追加

$ sudo useradd minio -s /sbin/nologin

インストール

公式サイトからダウンロード

$ cd /usr/local/src
$ wget https://dl.min.io/server/minio/release/linux-amd64/minio
$ chmod +x minio
$ chown minio:minio minio
$ mv minio /usr/local/bin

systemd用スクリプトを作成

今回はgithubにアップされているスクリプトを活用する。

$ cs /etc/systemd/system/
$ curl -O https://raw.githubusercontent.com/minio/minio-service/master/linux-systemd/minio.service
$ sed -i -e 's/minio-user/minio/' minio.service

データディレクトリを作成

$ mkdir /var/lib/minio
$ chown mino:minio /var/lib/minio
chmod 700 /var/lib/mino

設定ファイルの生成

/etc/default/minio

MINIO_VOLUMES=/var/lib/minio
MINIO_ACCESS_KEY={適当なセキュアな文字列}
MINIO_SECRET_KEY={適当なセキュアな文字列}

サービスの有効化

$ systemctl start minio
$ statemctl status minio
$ systemctl enable minio

Can't resolve 'fs'

browser向けjsの環境構築中、環境によって変えたい値を.envで管理したいという事でdotenv-webpackをインストールしたのだが、bundle作成時にエラーが出てつまったので共有をしておく。

環境

Package Ver
webpack 4.41.0
webpack-cli 3.3.9
webpack-dev-server 3.8.1
dotenv-webpack 1.7.0

経緯

他のプロジェクトでもdotenv-webpackをブラウザ向けに使っていたこともあり、特に何も考えずにdotenv-webpackをインストールしてビルドした。

$ npm install dotenv-webpack

webpack.config.js

const path = require('path');
const Dotenv = require('dotenv-webpack');

module.exports = {
  // ...
  plugins: [
    new Dotenv({ systemvars: true })
  ],
  // ...
};

package.json

{
  // ...
  "script": {
    // ...
    "dev": "webpack-dev-server",
    // ...
  },
  // ...
}
$ npm run dev

すると以下のエラーが発生した。

ERROR in ./node_modules/dotenv/lib/main.js
Module not found: Error: Can't resolve 'fs' in '/xxx/xxx/node_modules/dotenv/lib'
 @ ./node_modules/dotenv/lib/main.js 24:11-24
 @ ./src/api/config.ts
 @ ./src/api/encode.ts
 @ ./src/api/index.ts
 @ ./src/index.ts

よくある間違った解法

Can't resolve 'fs'などでググるwebpack.config.jsのtargetをnodeに変更しましょうという記事が出てくるが、これはサーバーサイドの時の話である。

ブラウザ向けの場合はこの設定をしてしまうと動かない。ではどうすればいいかというと、fsモジュールを使わないようにするしかない。モジュール内で使われているのであれば代替モジュールを検討するほかなくなる。

しかしそれは非常に困る。だって他のプロジェクトではdotenv-webpackちゃんと動いてるし。

解決

css-loaderのissueに同じような問題が上がっていたので、それを参考にwebpack.config.jsnode: { fs: 'empty'}を追加した。

webpack.config.js

const path = require('path');
const Dotenv = require('dotenv-webpack');

module.exports = {
  // ...
  plugins: [
    new Dotenv({ systemvars: true })
  ],
  node: {
    fs: 'empty'
  },
  // ...
};

詳細は公式ドキュメントに記載されているが、要約するとNode用モジュールをポリフィルもしくはモックして、ブラウザーでも実行できるようにする!という事らしい。

そしてemptyは空のオブジェクトを提供するという意味なので、dotenv内のfsは何もしない空モジュールという事になる。

ブラウザ用ではfsなんて使わないので、これで問題無。無事解決。良かった良かった。