mysqldumpの出力をメールで送りつける

備忘録です。

MySQLデービーを使うサイトが増えて、そろそろデービーのバックアップを自動化しようと目論む。

ま、データベースと言っても小規模の小さいデータばっかりなので、どんなに多くても10MBを超すデータベースはないので、mysqldumpをcronで月一回程度実行してその出力を暗号化ZIPに圧縮して1つのメールアドレスに飛ばせば、複数サイトのDBバックアップが出来る!

ま、perlで書けばいいや、と思って書き出したんだけど、Archive::Zipでは暗号化ZIPを作成できないとのこと。使っているサーバーではPHPのバージョンが古くて、これも使えず・・・。zipコマンドを使えば・・・と思うんですが出来ればperl内で完結させたい。

しょうがないのでmysqldumpの出力をそのままCrypt::CBCとかで暗号化してしまえ、ということで。暗号化して送信するスクリプト(cron用)と、複合化するスクリプトを作成。

mysqldump-mail.pl を cronに月イチ夜中の3時ぐらいにサイト毎に5分ぐらいづつずらして登録。後は待っとけばダンプファイルがバックアップされる。

まぁ、でも、小さいデータ量だから可能なわけで・・・、当たり前ですが、数百メガバイトからギガバイト級のデータベースとかだと、絶対やっちゃだめっすけどね(^^;;;

※Crypt::CBCへのコンストラクタ引数は、とりあえず適当に指定しているので、これをそのままコピペして実運用はできません。いえ、しちゃだめです。また mysqldumpは環境毎に異なると思うのでコピペして使える代物ではありません。あしからず、ご了承くださいませ。ないとは思いますが、一応注意書きです。

○mysqldumpを暗号化して送信

#!/usr/bin/perl
##############################################################################
=comment

  'mysqldump' を実行した結果を暗号化して指定したメールアドレスへ送信します。

  ex.
    ./mysqldump-mail.pl --sendto=hoge@hoge.com --user=DBユーザー \
                     --password=DBパスワード --database=データベース名

=cut
##############################################################################
use strict;
use warnings;
use Getopt::Long qw/:config no_ignore_case/;
use MIME::Base64 qw/encode_base64/;
use Crypt::CBC;
use IO::File;
use IO::String;

my $SENDMAIL = '/usr/lib/sendmail';

my %CONFIG = ('user'     => '',
              'password' => '',
              'sendto'   => 'xxx@yyy.com',
              'from'     => 'xxx@zzz.com',
              'database' => '',
              'subject'  => 'MySQL dump at %04d/%02d/%02d',
              'filename' => '',
              'key'      => 'private key',
              'cipher'   => 'DES_EDE3');

#Startup code
&{sub
{
  #arguments
  my @argv = @_;
  my ($y,$m,$d) = (localtime)[5,4,3];
  ($y,$m) = ($y-100,$m+1);

  my %config = %CONFIG;

  my $result = Getopt::Long::GetOptionsFromArray(\@argv,
                                                 'user=s'     => \$config{user},
                                                 'password=s' => \$config{password},
                                                 'from=s'     => \$config{from},
                                                 'sendto=s'   => \$config{sendto},
                                                 'database=s' => \$config{database},
                                                 'key=s'      => \$config{key},
                                                 'cipher=s'   => \$config{cipher});

  $config{subject} = sprintf($config{subject},$y,$m,$d);

  # ファイル名を定義(※ファイル名で日付がわかるようにしておきます)
  my $basename = "mysqldump-$y$m$d";

  $config{filename} = "$basename.sql.enc";

  die "no database...\n" unless $config{database};
  die "no user...\n" unless $config{user};

  my $commandline = sprintf('mysqldump --opt --skip-extended-insert --user=%s --password=%s %s',
                            $config{user},
                            $config{password},
                            $config{database});

  # 暗号化する
  my $sqldump = IO::File->new("$commandline |") || die "can not open\n";

  return &sendmail($sqldump,%config);

}}(@ARGV);

sub encode_stream
{
  my ($in,$out,$key,$cipher) = @_;

  my $cbc = Crypt::CBC->new(-key    => $key,
                            -cipher => $cipher);

  #読込バッファ,暗号化バッファ
  my ($buf,$enc) = ('','');

  #バッファサイズ
  my $buf_size = 57*71;

  $cbc->start('encrypting');
  while(0 < $in->read($buf,$buf_size))
    {
      # 詠み込まれたバッファを暗号化して暗号化バッファに追加
      $enc .= $cbc->crypt($buf);

      while(length $enc > $buf_size)
        {
          $out->print(encode_base64(substr($enc,0,$buf_size,'')));
        }
      $buf = '';
    }
  $out->print(encode_base64($enc)) if($enc);

  $cbc->finish();

  1;
}

sub sendmail
{
  my $ref = shift;
  my %config = @_;

  my $bound = '_xkdjfsdkjfsafdskfjsa_';
  my $boundary = "--$bound";
  my $boundary_end = "--$bound--";

  my $sm = IO::File->new("| $SENDMAIL -i -t");

  $sm->print(<<__MAIL__);
From: $config{from}
To: $config{sendto}
Subject: $config{subject}
Content-Type: Multipart/Mixed; boundary="$bound"
MIME-Version: 1.0

$boundary
Content-Transfer-Encoding: 8bit
Content-type: text/plain; charset=UTF-8

MySQLデータベースダンプファイルです。

$boundary
Content-type: application/octed-stream
Content-Transfer-Encoding:Base64
Content-disposition: attachment; filename=$config{filename}

__MAIL__

  &encode_stream($ref,$sm,$config{key},$config{cipher});

  $sm->print("\n$boundary_end\n");
  $sm->close;

  return 0;
}

 
 

○ファイルを復号化する

#!/usr/bin/perl
##############################################################################
=comment

  Crypt::CBCで暗号化したファイルを復号化します。

  ex.
  ./cbc-decrypt.pl --key=秘密キー --in=入力ファイル

  その他)
  --out=出力ファイル(標準出力) --cipher=アルゴリズム(DES_EDE3)

=cut
##############################################################################
use strict;
use warnings;
use Getopt::Long qw/:config no_ignore_case/;
use Crypt::CBC;
use IO::File;

my %CONFIG = (key    => '',
              in     => '',
              out    => '-',
              cipher => 'DES_EDE3');

#Startup code
&{sub
{
  #arguments
  my @argv = @_;
  my %config = %CONFIG;

  my $result = Getopt::Long::GetOptionsFromArray(\@argv,
                                                 'key=s'    => \$config{key},
                                                 'cipher=s' => \$config{cipher},
                                                 'in=s'     => \$config{in},
                                                 'out=s'    => \$config{out});


  my $encfile = IO::File->new($config{in}) or die "can not open file\n";
  my $fout = $config{out} eq '-' ? IO::File->new_from_fd(fileno STDOUT,'>') : IO::File->new(">$config{out}") or die "can not open output file\n";

  return &decode_stream($encfile,$fout,$config{key},$config{cipher});

}}(@ARGV);


sub decode_stream
{
  my ($in,$out,$key,$cipher) = @_;

  my $cbc = Crypt::CBC->new(-key => $key,-cipher => $cipher);

  my ($buf,$len) = ('',0);
  my $read_size = 4096;

  $in->binmode;
  $out->binmode;

  $cbc->start('decrypting');
  while(0 < ($len = $in->read($buf,$read_size)))
    {
      $out->print($cbc->crypt($buf));
      $buf = '';
    }
  $cbc->finish();

  1;
}

twitterの「いいね」画像をダウンロード 修正版

追記 2017/07/15
C#に書き直した修正版 → https://ptsv.jp/2016/04/20/tweet-image-download-agent/


2015年11月27日・29日 コード修正
コード修正 画像ファイルの正規表現の間違い修正・ちょこっと追加。
コード修正 URL を favorites => likes に変更

2015年11月23日 コード修正
2回目以降のtimelineのURLの決定方法が間違っておりましたので修正しました。ごめんなさい(m_m)

2015年11月17日 コード修正
ログインを行うコードを追加。これにより、ブラウザからクッキーファイルをエクスポートする手間を省くようにした。ログインコードはこれでいいのかどうかわからん。twitterってrails使ってんだっっけ? よくわからんが、適当。
本来はTwitter APIを使わないといけないと思うけど・・・API経由は正直メンドクサイ。勉強する気なし。(m_m)

C#で書き直したプロトタイプをさっき書いたので、これもデバッグが終わり次第また書こっと。
『perlなんかインストールしてねーよ、ボケ!』って方は、C#で書き直した方に ビルドしたやつを置いてます。


今年の2月ぐらいに書いたtwitter画像ダウンロードスクリプトが動かなくなったので修正・修正(^_^;;

2回目以降のタイムライン取得のURLが変更になったみたい?。昼休みにブラウザでアクセスしてデベロッパーツールでちょこっと解析したんですが・・・どのパラメーターで読めばいいのか、いまいち分かんないです。適当です。あくまで、自分用の備忘録なんで、すみません。取れればいいんです(m_m)

twitterにログインするところまで書きなおそうと一瞬思いましたが、また今度にします(・・;
ブラウザでログインしてクッキーエクスポート、twitter.comドメインだけ抜き出して、カレントディレクトリへcookie.txtというファイル名で保存。テキトーですまない。

使い方は下記参考。ログインしたcookie.txtが必要なのは、いいね(旧称:お気に入り)の取得のみ。他人のIDの「いいね」も同様にログインが必要です。この辺よくわからん。自分もしくは他人のIDのタイムラインにログインクッキーは必要ない。です。

#!/usr/bin/perl
=pod

=head1 ツイッターでのタイムラインから、画像のみダウンロードするスクリプト

使い方 targetに画像を取得したいアカウント名を指定する。

(いいね 画像の場合は type に favo をセット(デフォルト) 
(※ いいね 取得は自分以外のアカウントでも必ずログインが必要みたいです。)

> twitter.pl --username=xxxxx --password=yyyyy --type=favo --target=zzzz


(単に任意のアカウントのタイムラインの画像が欲しい場合は type に profileをセット)
(TLに流れているリツイートも含めた画像を取得したい場合は、timelineをセット)
(※通常公開されているアカウントのタイムラインではusename,passwordは必要ありません。
(※非公開アカウントではフォローを許可されたアカウントのusername/passwordが必要です。)

> twitter.pl --type=profile --target=公開アカウント名


ディレクトリ<tmp>に画像が吐き出されます。

=cut

use strict;
use warnings;
use LWP::UserAgent;
use Getopt::Long qw/:config no_ignore_case/;

my %CONFIG = (username => '', password => '', dir => './tmp', type => 'favo', target => '');
my %TIMELINE = (favo    => 'https://twitter.com/%s/likes/timeline',
                profile => 'https://twitter.com/i/profiles/show/%s/media_timeline',
                timeline => 'https://twitter.com/i/profiles/show/%s/timeline');

#エージェント
my $UserAgent = LWP::UserAgent->new(cookie_jar => {});

&{sub
{
  my @argv = @_;
  my $result = Getopt::Long::GetOptionsFromArray(\@argv,
                                                 'username=s' => \$CONFIG{username},
                                                 'password=s' => \$CONFIG{password},
                                                 'type=s'     => \$CONFIG{type},
                                                 'dir=s'      => \$CONFIG{dir},
                                                 'target=s'   => \$CONFIG{target});

  exists $TIMELINE{$CONFIG{type}} or die "invalid type....\n";
  $CONFIG{dir} || die "specified output directory...\n";
  $CONFIG{target} || die "specified target account name...\n";

  mkdir $CONFIG{dir} unless(-e $CONFIG{dir});

  &use_lwp_agent;

}}(@ARGV);

sub use_lwp_agent
{
  my ($param,$result) = ('?include_available_features=1&include_entities=1','');

  if($CONFIG{username} && $CONFIG{password})
    {
      my $response = $UserAgent->get('https://twitter.com/login');
      $result = $response->decoded_content;

      $result =~ /<input type="hidden" value="([\d\w]+?)" name="authenticity_token"(?:\s*\/)?>/ or die "can not detect authenticity_token...\n";

      my $auth = $1;
      $response = $UserAgent->post('https://twitter.com/sessions',
                                   {'session[username_or_email]' => $CONFIG{username},
                                    'session[password]'          => $CONFIG{password},
                                    'authenticity_token'         => $auth,
                                    'remember_me'                => '1',
                                    'redirect_after_login'       => '/' }) or die "can not access login page... \n";

      $result = $response->decoded_content;
      die "failed to login...\n" if($result =~ /error/);
    }

  my $url = sprintf($TIMELINE{$CONFIG{type}},$CONFIG{target});
  do
    {
      my $timeline = $url.$param;

      print "---- getting and parsing\n$timeline ...\n----\n";
      $result = &lwp_agent($timeline,'-') || die "can not get timeline. may be wrong url.\n";

      $param = &get_images($result,\&lwp_agent);

    } while( $param );

  print "done!\n";
}

#タイムラインのJSONデータから画像を取得して、次のタイムラインのパラメータを返す。
sub get_images
{
  my ($json,$agent) = @_;

  $json =~ s/\\\//\//g;
  foreach my $url_($json =~ m!https://pbs\.twimg\.com/media/[\w\-]+\.\w{3,4}(?::large)?!g)
    {
      if($url_ =~ m/([\w\-]+\.\w{3,4})(:large)?$/)
        {
          my $basename = $1;
          my $filename = "$CONFIG{dir}/$basename";
          if($2)
            {
              $url_ =~ s/:large/:orig/;
            }
          else
            {
              $url_ .= ':orig';
            }

          unless(-e $filename)
            {
              print "fetching: $url_\n";
              &$agent($url_,$filename);
              print "saved $basename\n";
            }
        }
    }

  my @ids = $json =~ m/data-tweet-id=\\\"([0-9]+)/g;
  my $max_id = @ids > 0 ? pop @ids : '';

  return $max_id ? "?max_position=${max_id}&include_available_features=1&include_entities=1" : undef;
}

#URLを取得して返す。
sub lwp_agent
{
  my ($url,$ofile) = @_;
  my %options = ();

  if($ofile ne '-')
    {
      if($ofile eq ':src')
        {
          if($url =~ /([\w\-\.%]+?\.\w)$/)
            {
              $ofile = "$CONFIG{dir}/$1";
            }
          else
            {
              goto cleanup;
            }
        }

      return $UserAgent->get($url,':content_file' => $ofile);
    }

cleanup:
  if(my $response = $UserAgent->get($url))
    {
      return $response->decoded_content;
    }

  0;
}

__END__

MDBファイルへのアクセス

備忘録です。

ちょっと前に Accessで作成しているシステムをWebシステムに置き換える案件の仕事をしている時に、MDBファイルを覗く(データ抽出)する必要があった。そのときに使っていたPCには、ランタイム版ではない、本物の? 32bit版のMicrosoft Accessがインストールされていたので問題は無かったのですが、今使っているPCはMicrosoft Officeは入っていない。

いや、たしか、OLEDB経由でMDBファイルは読めるはず・・・と思ってコントロールパネルからODBCドライバが入っているかどうか見てみたけど・・・残念・・・入ってなかった・・・。

ググったら、「Microsoft Access データベース エンジン 2010 再頒布可能コンポーネント(https://www.microsoft.com/ja-jp/download/details.aspx?id=13255)」をインストールすれば読める、ということで、64bit版のドライバを入れて、Visual Studio 2015 community のサーバーエクスプローラで確認。

で、一応32bit版も入れとくか・・・と思って、32bit版もダウンロードしてインストーラーを立ち上げたら、先に64bit版を削除しろ! と怒られた(^^;

ええい、/passive オプションをつけてバージョン・チェックなしでもう一度インストーラーを起動して、問答無用で入れてやりましたよ!32bit版はバージョン違いのものが既にインストールされてた。この辺よく分からん。

odbc64  odbc32

左が %WINDIR%\system32\odbcad32.exe右が%WINDIR%\syswow64\odbcad32.exe
32bit/64bitアプリケーションでちゃんと動くのかな?今度確認してみよ。

でも、これ、下記ような制限があるんだけど、意味が分からない。MDBファイルを読むために使っちゃダメなの? いいの? 分からん・・・

  1. Jet の全般的な代替としての使用。Jet の全般的な代替が必要な場合は、SQL Server Express Edition (英語版) が必要です。
  2. サーバー側アプリケーション内での Jet OLEDB プロバイダーとしての使用。
  3. 一般的なワード プロセッサ、スプレッドシート、またはデータベース管理システムとしての使用。 つまり、ファイル作成の手段としての使用。Microsoft Office または Office オートメーションを使うと、Microsoft Office でサポートされるファイルを作成することができます。
  4. システム サービスまたはサーバー側プログラム (コードがシステム アカウントの下で実行されるもの、複数のユーザー ID を同時に処理するもの、高度に再入可能で動作が不安定になるもの) による使用。これには、ユーザーがログインしていないときにタスク スケジューラーから実行されるプログラムや、ASP.NET などのサーバー側 Web アプリケーションから呼びだされるプログラム、COM+ サービスの元で実行される分散コンポーネントなどがあります。

iframe要素のsandbox属性

先日、iframeを使用している自作のjQueryプラグインを使って構築しているWebアプリの管理画面が突然正常に動かなくなった!

ブラウザの開発者ツールのコンソールから原因を調べたら、iframe要素内で読み込んでいる同じドメイン内のリソースからフォームを投げたり親ウィンドウへのアクセスがことごとくエラーになっていたorz コンソールには、allow-modalsがなんとかかんとか、という見たこともないエラー。

でも、エラーになるブラウザはchromeだけ。FireFoxやIEは問題なかったので、たぶんchromeの最近のバージョンアップでiframe要素のセキュリティが高くなってしまったのかなー、とよくよく調べたら、下記sandbox属性をiframeに追加すればちゃんと動くようになった。どうやらHTML5.1にchromeが対応しただけ? (というか、HTML5.1なんて、そんなもんあったの?無知は嫌だねぇ(_ _) 常にアンテナはっとかないとダメですよねぇ・・・)

  • allow-modals
  • allow-pointer-lock
  • allow-popups
  • allow-popups-to-escape-sandbox
  • allow-top-navigation

名前からだいたいの想像はつくんですが・・・allow-modalsとかallow-popupsとか違いが分からない。また今度時間あったら調べよう。

もともと、<iframe sandbox=”allow-same-origin allow-forms allow-scripts”> という風にしてたんですが・・・もっと調べたら、上記5つのsandbox属性は、HTML5.1から追加された?もののようですね。
(この辺、自分ではよく分かってません(_ _)

ってことは、このまま放っておくと、InternetExplorerはともかく、いずれはFireFoxもMicrosoft Edgeも動かなく・・・。このプラグイン使っているところは、修正したプラグインのJSファイルだけ上書き全部しないとなー。

根本的にはiframe要素をすべて排除して、全てajaxな作り方すれば良かったんですけどね・・・中途半端は良くないですね・・・でも、iframeって便利なんですよね(^.^;;;

ピリオド区切りバージョン番号の比較

Javascript(ECMAScript)で、jQueryのバージョンで分岐させる必要があったのですが、最初は、何も考えずに 等号・不等号演算子で比較してました。手抜きですね(^_^;) しばらくは何の不都合もなく動いていたのですが、使用しているjQueryのバージョンを1系列の最新バージョンにしたら動かなくなった。

当たり前ですね。最新のバージョン番号を $().jqueryで取得すると、1.11.x (xは数字) というのが返ってきます。で、単純に比較すると・・・ダメなのは明白ですね(笑)

ということで、急いで適当に書く。備忘録おわり。

関数名はPHPみたいに version_compare() とかだと長すぎるし、version() にするとバージョン番号を返すものと混同するし、何がいいんでしょうねぇ。まぁ、どうでもいいんですけどね。

ぐおー、perlだとpackで一発なのに・・・