トップ 追記   RSS 1.0 FEED  

Journal InTime


2023-12-20 (Wed) [長年日記]

_ MatzにっきでMongoDBを使うのをやめた

MatzにっきをHeroku化した際にMongoDBを使うようにしていたが、MongoDBが不自由なライセンスになってしまったのでDefaultIO(ファイル)に戻した。

移行用のスクリプトはなさそうだったのでtdiary-io-mongodbに同梱されているtdiary-mongodb-convertを以下のように書き換えて使用した。

#!/usr/bin/env ruby

require 'optparse'
require 'erb'
require 'tdiary/view_helper'
require 'tdiary/base'
require 'tdiary/comment'
require 'tdiary/comment_manager'
require 'tdiary/referer_manager'
require 'tdiary/style'
require 'tdiary/cache/file'
require 'ostruct'
require 'tdiary/core_ext'
require 'tdiary/io/default'
require 'tdiary/io/mongodb'
require 'tdiary/application'

def load_diaries(mongo_url, style_path)
  options = {
    'style.path' => style_path
  }
  conf = OpenStruct.new({
    database_url: mongo_url,
    options: options
  })
  tdiary = OpenStruct.new({conf: conf})
  TDiary::IO::MongoDB.load_cgi_conf(conf)
  mongo_io = TDiary::IO::MongoDB.new(tdiary)

  mongo_io.calendar.each do |y, ms|
    ms.each do |m|
      puts "loading #{y}-#{m}"
      month = Time.local(y.to_i, m.to_i)
      mongo_io.transaction(month) do |diaries|
        yield month, diaries
        TDiary::TDiaryBase::DIRTY_NONE
      end
    end
  end
end

def update_diaries(default_io, month, new_diaries)
  default_io.transaction(month) do |diaries|
    diaries.update(new_diaries)
    TDiary::TDiaryBase::DIRTY_DIARY | TDiary::TDiaryBase::DIRTY_COMMENT
  end
end

def store_diaries(data_path, style_path, mongo_url = nil)
  options = {'style.path' => [style_path]}
  conf = OpenStruct.new({options: options, data_path: "#{data_path}/"})
  tdiary = OpenStruct.new({conf: conf})
  TDiary::IO::Default.load_cgi_conf(conf)
  default_io = TDiary::IO::Default.new(tdiary)

  load_diaries(mongo_url, style_path) do |month, diaries|
    p month
    update_diaries(default_io, month, diaries)
  end
end

def store_conf(conf_path, mongo_url = nil)
  conf = OpenStruct.new({options: {}, database_url: mongo_url})
  tdiary = OpenStruct.new({conf: conf})
  s = TDiary::IO::MongoDB.load_cgi_conf(conf)
  File.write(conf_path, s)
end

args = {}
OptionParser.new do |opts|
  opts.banner = 'Usage: tdiary-mongodb-convert [options] <data_path>'
  opts.on('-c CONF', '--conf=CONF', 'store only tdiary.conf'){|v| args[:conf] = v}
  opts.on('-s PATH', '--style=STYLE', 'style path'){|v| args[:style] = v}
  opts.on('-m URL',  '--mongo=URL', 'URL of mongoDB'){|v| args[:mongo] = v}
  opts.parse!(ARGV)

  args[:data] = ARGV.shift
  unless args[:data] || args[:conf]
    $stderr.print opts.help
    exit 1
  end
end

if args[:conf]
  store_conf(args[:conf], args[:mongo])
else
  store_diaries(args[:data], args[:style], args[:mongo])
end

実行手順は以下のとおり(~/diary下のファイルが上書きされてしまうので注意)。

$ bundle exec tdiary_mongodb2default -s lib/tdiary/style -m <MongoDBのURL> ~/diary
$ bundle exec tdiary_mongodb2default -c ~/diary/tdiary.conf -m <MongoDBのURL>
Tags: Ruby tDiary

2023-12-03 (Sun) [長年日記]

_ ConoHa VPSへの移行とCentOS Stream化

この日記のサーバをDebian bookworkにアップグレードしようとした時に sudo find /etc -delete -name '*.dpkg-*' -o -name '*.ucf-*' -o -name '*.merge-error' で/etcをふっとばしてやっぱりVMイメージのバックアップを取りたいなと思ったのと、ConoHa VPSの512MBプランが12か月契約の割引で321円/月だったのでさくらVPSからConoHa VPSに移行した。

誤算だったのは512MBプランがUbuntu 22.03に対応してなかったことで、サーバ再構築でUbuntu 22.04を入れようとすると「失敗しました」とだけ表示される。 Ubuntu 20.04だとサーバ再構築が成功するが、22.04にアップグレードするとやっぱりkernel panicで起動しなくなってしまう。 そもそも契約時にUbuntu 22.04を選択したつもりだったのにCentOS Stream 9になってておかしいと思ったんだよな。

というわけであきらめて当分CentOS Stream 9を使ってみることにした。 firewall-cmdでhttp/httpsを空ける必要があったのとsavelogが使えなくなったこと以外はスムーズに移行できた。

Tags: Linux

2023-11-21 (Tue) [長年日記]

_ jquery.validate.jsのfocusInvalidが動作しない

jquery.validate.jsのfocusInvalidが動作しないケース(もう一回submitボタンを押すとフォーカスが当たる)があったので調べていたら、Lenisの影響だったようで、以下のように一時的にLenisを無効化するようにしたら直った。

  $('#entry-form').validate({
    ...
    invalidHandler: function(event, validator) {
      lenis.stop();
      setTimeout(() => {
        lenis.start();
      }, 100);
    }
  });
Tags: JavaScript

2023-11-18 (Sat) [長年日記]

_ NANKANG AW-1

NANKANG AW-1

ちょっと前にスタッドレスに換えていたが、今回ははじめて台湾製タイヤにしてみた。4本で39,864円なり。

換えた直後は接地感が無でちょっと怖かったが 、馴染んだのか馴れたのかそれほど気にならなくなった。

雪とか凍結路で早く試してみたい。

Tags: MAZDA3

2023-11-14 (Tue) [長年日記]

_ esaのwebhookでJekyllのページを更新する

会社のサイトをリニューアルしたので、esaのGitHub webhookでニュースの更新をしようとしたけど、ファイル名やfrontmatterが決め打ちでJekyllではうまく行かなかったので、AWS Lambdaで自前のWebhookを実装して更新するようにした。

雑だけど画像の添付にも対応している(弊社は外部からesaの画像を見えない設定にしているので、webhookでダウンロードしてリポジトリにcommitするようにした)。

GitHubの認証はFine-grained personal access tokenを使ったけど、有効期限が最長1年なので更新を忘れそう。

require "openssl"
require "json"
require "rack"
require "octokit"
require "esa"

def lambda_handler(event:, context:)
  body = event["body"]
  sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), ENV["ESA_SECRET"], body)
  header_sig = event["headers"]["x-esa-signature"].delete_prefix("sha256=")
  unless Rack::Utils.secure_compare(sig, header_sig)
    STDERR.puts("Invalid signature: #{sig} != #{header_sig}")
    return { statusCode: 400, body: "Invalid signature" }
  end
  json = JSON.parse(body)
  case json["kind"]
  when "post_create", "post_update"
    if json["post"]["wip"]
      puts("WIP: do nothing")
    else
      year, month, day, title =
        json["post"]["name"].scan(%r"Public/News/(\d+)/(\d+)/(\d+)/(.*)")[0]
      number = json["post"]["number"]
      filename = "news/_posts/#{year}-#{month}-#{day}-#{number}.md"
      post_news_entry(filename, title, json["post"]["body_md"])
    end
  end
  { statusCode: 200, body: "Posted a news entry" }
end

def post_news_entry(filename, title, content)
  github = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
  esa = Esa::Client.new(access_token: ENV["ESA_TOKEN"], current_team: "nacl")
  repo = "NaCl-Ltd/www.netlab.jp"
  ref = "heads/main"

  sha_latest_commit = github.ref(repo, ref).object.sha
  sha_base_tree = github.commit(repo, sha_latest_commit).commit.tree.sha

  img_files = []
  s = content.gsub(%r'src="(https://files.esa.io/uploads/.*?)"') {
    basename = File.basename($1)
    path = "img/news/#{basename}"
    url = esa.signed_url(URI.parse($1).path).body["url"]
    data = Net::HTTP.get(URI(url))
    sha = github.create_blob(repo, [data].pack("m"), "base64")
    img_files << {
      :path => path,
      :mode => "100644", 
      :type => "blob", 
      :sha => sha
    }
    "src=\"/#{path}\""
  }

  body = <<EOF
---
title: #{title}
layout: news
---

#{s}
EOF
  blob_sha = github.create_blob(repo, [body].pack("m"), "base64")
  
  sha_new_tree = github.create_tree(
    repo, 
    [
      {
        :path => filename, 
        :mode => "100644", 
        :type => "blob", 
        :sha => blob_sha
      },
      *img_files
    ], 
    {:base_tree => sha_base_tree }
  ).sha
  commit_message = "Committed via esa webhook"
  sha_new_commit = github.create_commit(repo, commit_message, sha_new_tree, sha_latest_commit).sha
  github.update_ref(repo, ref, sha_new_commit)
end
Tags: Ruby

_ 採用エントリーフォームをAWS Lambda + Amazon SESで作った

採用エントリーフォームをデザイン会社さんがPHPで作ろうとされていたので、自分で作りますんで……ということでAWS Lambda (Ruby 3.2) + Amazon SESで作った。

gemを使うとメンテナンスが面倒だなと思ってmultipart/form-dataのパースやMIMEメールの整形を雑に自前で書いたけど、esaのwebhookの方で結局gemを使うことになってしまったので、こちらもgemを使ってもよかったかもしれない。

require 'json'
require 'uri'
require 'securerandom'
require 'aws-sdk-ses'

Part = Data.define(:name, :value)
Attachment = Data.define(:filename, :body)

def lambda_handler(event:, context:)
  form = parse_multipart(event["body"].unpack1("m"))
  validate(form)
  result = send_email(form)
  { statusCode: 302, headers: { Location: ENV["SUCCESS_URL"] } }
rescue => e
  STDERR.puts({
    errorMessage: e.message,
    errorType: e.class.name,
    stackTrace: e.backtrace
  }.to_json)
  { statusCode: 302, headers: { Location: ENV["ERROR_URL"] } }
end

def parse_multipart(s)
  boundary = s.slice(/\A--.*?\r\n/)
  parts = s.byteslice(boundary.bytesize, s.bytesize - 2 - 2 * boundary.bytesize - 2).split("\r\n" + boundary)
  parts.each_with_object({}) { |s, h|
    part = parse_part(s)
    h[part.name] = part.value
  }
end

def parse_part(s)
  header, value = s.split(/^\r\n/)
  cd_params = header.slice(/^Content-Disposition: *form-data;([^\r\n]*)\r\n/)
  name = cd_params.slice(/name="(.*?)"/, 1)
  filename = cd_params.slice(/filename="(.*?)"/, 1)
  if filename
    filename.force_encoding("utf-8")
    v = Attachment.new(filename, value)
  else
    value.force_encoding("utf-8")
    v = value
  end
  Part.new(name, v)
end

def validate(form)
  %w(name email message).each do |name|
    if form[name].nil? || form[name].empty?
      raise "#{name} is missing"
    end
  end
  if !form["file"].is_a?(Attachment)
    raise "file should be an attachment"
  end
end

def send_email(form)
  sender = ENV["MAIL_SENDER"]
  recipient = ENV["MAIL_RECIPIENT"]
  subject = '=?UTF-8?B?5o6h55So5b+c5Yuf?='

  boundary = SecureRandom.hex
  encoded_file, encoded_filename, file_type = encode_file(form["file"])

  raw_message = <<EOF
From: #{sender}
To: #{recipient}
Subject: =?UTF-8?B?5o6h55So5b+c5Yuf?=
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=#{boundary}

--#{boundary}
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

お名前: #{form["name"]}
メールアドレス: #{form["email"]}
メッセージ・自己PR:
#{form["message"]}
--#{boundary}
Content-Type: #{file_type}
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename*=#{encoded_filename}

#{encoded_file}
EOF

  encoded_file2, encoded_filename2, file_type2 = encode_file(form["file2"])
  if encoded_file2
    raw_message << <<EOF
--#{boundary}
Content-Type: #{file_type2}
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename*=#{encoded_filename2}

#{encoded_file2}
EOF
  end

  raw_message << "--#{boundary}--\n"
  raw_message.gsub!(/\n/, "\r\n")
  
  ses = Aws::SES::Client.new(region: 'ap-northeast-1')

  ses.send_raw_email(
    destinations: [recipient],
    raw_message: {
      data: raw_message
    },
    source: sender
  )
  puts 'Email sent to ' + recipient
end

def encode_file(file)
  return nil if file.nil? || file.body.nil? || file.filename.nil?
  encoded_file = [file.body].pack("m")
  encoded_filename = "utf8''" + URI.encode_www_form_component(file.filename)
  file_type = file.filename.match?(/.pdf\z/i) ?
    "application/pdf" : "appliation/octet-stream"
  return encoded_file, encoded_filename, file_type
end
Tags: Ruby