俺、サービス売って家買うんだ

Swift, Kotlin, Vue.js, 統計, GCP / このペースで作ってればいつか2-3億で売れるのがポっと出来るんじゃなかろうか

Homebrewでインストールした MySQL の my.cnf の置き場所と一般ログの設定方法

f:id:ie-kau:20170407132034p:plain


my.cnf の置き場所

手持ちのMacにHomebrewでMySQLををインストールした場合にどこにmy.cnfを置けばよいか調べたのでメモ。 ただインストールするだけでは、ここ↓には設置されない。

/etc/my.cnf

以下のコマンドを叩いて設定を見る。

mysql --help
Default options are read from the following files in the given order:
/etc/my.cnf /etc/mysql/my.cnf /usr/local/etc/my.cnf ~/.my.cnf

とのことらしいので、素直に /etc/my.cnf に設置してやればOK!

一般ログの設定

実行されているクエリなどを見るために一般ログを流す。
ログファイルのパーミッションに気をつけつつ、my.cnfにこんな感じで記述しておけば良い。

[mysqld]
general_log=1
general_log_file=/var/log/mysql/general-query.log

Xcode8.3, Swift 3.1にアップデートしたメモ

f:id:ie-kau:20151120074851p:plain


概要

先程Xcode のバージョンをアップデートしたのでその時に対応したことのメモです。

いつも通りXVimが起動しない

既にissueが上がっており対応してくれてた人がいるのでその方のリポジトリにあるコードで一旦対応。 本家にマージされたらまた乗り換えよう。

issue

github.com

これを使わせていただく

github.com

# 昔のものを削除
cd ~/XVim
make uninstall
rm -r ~/XVim

# いつも通りcondesign
sudo codesign -f -s XcodeSigner /Applications/Xcode.app  

# 入れる
cd ~
git clone https://github.com/keith/XVim.git
git fetch origin xcode-8.3-release
git checkout xcode-8.3-release
make

Swiftの修正

2系→3系のときと違い破壊的変更は無いのでほとんど何も変わらない。 自分がいじっているプロダクトでいうと以下だけ変更

-   var delegate: Reportable  & Linkable!
+   var delegate: (Reportable & Linkable)!

複数のプロトコルを継承する際にカッコで囲む必要がでた。

Swiftで左から右にグラデーションがかかるViewを作る

f:id:ie-kau:20151120074851p:plain


連日のSwiftメモです。

作りたい物

こんな感じで左から右にグラデーションがかかるボタンを作ります。

f:id:ie-kau:20170323192947p:plain

ソースコード

buttonViewがグラデーションをかける対象のViewとなります。

 //グラデーションの設定
let gradientLayer = CAGradientLayer()
gradientLayer.frame = buttonView.bounds
let color1 = UIColor.hexStr(hexStr: "#a6a1ff", alpha: 1.0).cgColor as CGColor
let color2 = UIColor.hexStr(hexStr: "#f871d0", alpha: 1.0).cgColor as CGColor
gradientLayer.colors = [color1, color2]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
buttonView.layer.insertSublayer(gradientLayer,at:0)
buttonView.layer.cornerRadius = 5
buttonView.clipsToBounds = true

これでOK! 16進数のカラーコードを扱うextensionを用意していますが、色はどう指定しても構いません。

Swift で○○秒前みたいにするエクステンション

f:id:ie-kau:20151120074851p:plain


こんにちは、はやとです。最近はSwiftばっかりやってて楽しいです。 Swift3.0で、SNSでよくある「○○秒前」みたいに表示するエクステンションを書いたので置いておきます。

環境

  • Swift3.0
  • extensionで実装 (Dateクラスを拡張)

コード

Date.swift

import Foundation
import UIKit

extension Date {
    func timeAgoSinceDate(numericDates:Bool) -> String {
        let calendar = NSCalendar.current
        let now = NSDate()
        let earliest = now.earlierDate(self as Date)
        let latest = (earliest == now as Date) ? self : now as Date
        let components = calendar.dateComponents([.minute , .hour , .day , .weekOfYear , .month , .year , .second], from: earliest, to: latest as Date)
        
        if (components.year! >= 2) {
            return "\(components.year!) 年前"
        } else if (components.year! >= 1){
            if (numericDates){
                return "1 年前"
            } else {
                return "去年"
            }
        } else if (components.month! >= 2) {
            return "\(components.month!) ヶ月前"
        } else if (components.month! >= 1){
            if (numericDates){
                return "1 ヶ月前"
            } else {
                return "先月"
            }
        } else if (components.weekOfYear! >= 2) {
            return "\(components.weekOfYear!) 週間前"
        } else if (components.weekOfYear! >= 1){
            if (numericDates){
                return "1 週間前"
            } else {
                return "先週"
            }
        } else if (components.day! >= 2) {
            return "\(components.day!) 日前"
        } else if (components.day! >= 1){
            if (numericDates){
                return "1 日前"
            } else {
                return "昨日"
            }
        } else if (components.hour! >= 2) {
            return "\(components.hour!) 時間前"
        } else if (components.hour! >= 1){
            if (numericDates){
                return "1 時間前"
            } else {
                return "1 時間前"
            }
        } else if (components.minute! >= 2) {
            return "\(components.minute!) 分前"
        } else if (components.minute! >= 1){
            if (numericDates){
                return "1 分前"
            } else {
                return "1 分前"
            }
        } else if (components.second! >= 3) {
            return "\(components.second!) 秒前"
        } else {
            return "ついさっき"
        }
        
    }
    
}

で、ViewControllerとかで

let dateString = Date() //現在時刻
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" //フォーマット合わせる
let posTimeText:String = formatter.date(from: dateString!)!.timeAgoSinceDate(numericDates: true)

ExpressでGoogle Cloud Storageに画像をアップロードする

f:id:ie-kau:20170222235628j:plain

やりたきこと

  • Expressで稼働しているWebサービスで画像をGoogle Cloud Storage(以下GCS)にアップロードする
  • サブドメインで画像を閲覧できるようにする

各種バージョン

Node 7.x
Express 4.x

事前準備

以下は完了している

  • ドメインは取得
  • Google Cloud DNSの設定

1. image.[ドメイン].comというサブドメインを設定する

Google Cloud DNSでCNAMEレコードに該当サブドメインを追加します。

f:id:ie-kau:20170222235440p:plain

2. ドメインの所有権の認証をする

ドメイン名を持つバケットを作成するには、GCPにログインしている管理者が対象のドメインの所有者であることを確認しておかなければなりません。

ドメインを持つバケットを作成できるユーザー

ドメインに確認済みのオーナーが複数いる場合、これらのオーナーのみが名前にドメインを使用したバケットを作成することができます。ドメインに確認済のオーナーがいない場合、確認済のウェブサイト オーナーが名前にドメインを使用したバケットを作成することができます。少なくとも 1 人の確認済のオーナーがドメインまたはウェブサイトに含まれていないと、名前にドメインを使用したバケットを作成することはできません。ドメイン所有権はウェブサイト所有権よりも制御レベルが高いため、ドメイン名を持つバケットを作成できるユーザーをサイトで厳しく制御する必要がある場合に役立ちます。

たとえば、「example.com」というサイトの管理を担当する IT スタッフ メンバーが 2 人いるとします。必要な確認(以下を参照)を完了すると、「example.com」、「reports.example.com」、「downloads.example.com」など、ドメイン名を持つバケットを作成できるユーザーはその 2 人のみになります。

確認済のウェブサイトまたはドメイン オーナーは、Search Console を使用してウェブサイトまたはドメイン オーナーを追加することができます。Search Console のダッシュボードで、管理するウェブサイトを検索し、[プロパティの管理] > [ユーザーを追加/削除] を選択します。ドメイン所有者を追加するには、[プロパティ所有者の管理] リンクを選択します。ドメイン所有者は、他のドメイン所有者を追加することができます。

※引用元
Domain-Named Bucket Verification  |  Cloud Storage Documentation  |  Google Cloud Platform

これは引用にあるようにSearch Consoleから対応できます。

3. Google Cloud Platform API に利用する鍵の作成

「IAMと管理」の「サービス アカウント」でGCPのAPIを利用するサービスアカウントと鍵を作成します。

f:id:ie-kau:20170222235332p:plain

ここで作成したjsonの鍵はバケットにファイルをアップロードする時に利用します。

4. GCSのバケットを作る

利用したいサブドメインと同じバケット名でバケットを作ります。 ドメインの所有権が確認されてない場合はここでエラーになります。

f:id:ie-kau:20170222235225p:plain

Expressの設定

利用するモジュール

ソースコード

const express = require('express')
const router = express.Router()
// -- その他色々 -- 
const multer  = require('multer')
const storage = require('@google-cloud/storage')
const gcs = storage({
  projectId: '[projectId]',
  keyFilename: '[/path/to/key.json]' // 事前準備3で作成した鍵
})

const bucket = gcs.bucket('image.[ドメイン].jp')
bucket.acl.default.add({
  entity: "allUsers",  // 外部アクセスを可能にする
  role: storage.acl.READER_ROLE
}, err => {})

const imageUploader = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 10 * 1024 * 1024  // ファイルサイズ上限
  }
}).single('image')

router.post('/upload', (req, res, next) => {
  imageUploader(req, res, err => {
    if (err) {
      var error = null
      switch(err.code) {
        case 'LIMIT_FILE_SIZE':
          error = {code: 1111, message: 'ファイルが大きすぎます'}
          break
        default:
          error = err
          break
      }
      next(error)
      return
    }

    const mimetype = req.file.mimetype
    const type = [
      { ext: 'gif', mime: 'image/gif' },
      { ext: 'jpg', mime: 'image/jpeg' },
      { ext: 'png', mime: 'image/png' }
    ].find(ext => {
      return ext.mime === mimetype
    })

    if (!type) {
      next({code: 1111, message: "不正なファイル形式です"})
      return
    }

    const blob = bucket.file(`uploads/${req.file.originalname}`)
    const opts = { 
      metadata: { 
        contentType: mimetype,
        cacheControl: "public, max-age=300"
      } 
    }
    const blobStream = blob.createWriteStream(opts)

    blobStream.on('error', err => {
      next(err)
      return
    })

    blobStream.on('finish', () => {
      const publicUrl = `http://${bucket.name}/${blob.name}`
      res.json({ok: 1, imageUrl: publicUrl})
    })

    blobStream.end(req.file.buffer)
  })
})

コードは端折りましたがこれで image.[ドメイン].jp のバケットの uploadsというフォルダにファイルをアップロードすることができました。

課題

  • GCSでサーブした場合のSSL対応はどうすればいいのか?

【Swift】関数にオプショナルのブロックを渡す

f:id:ie-kau:20151120074851p:plain:w400


括弧が多くて意味不明になったので一旦メモ。

func execute(completion: (() -> Void)?) {
    if let unwraped = completion {
        unwraped()
    }
}

execute { 
    print("hoge") // hoge
}

これだとコンパイルエラー

func execute(completion: () -> Void?) {
    if let unwraped = completion {
        unwraped()
    }
}

error: initializer for conditional binding must have Optional type, not ‘() -> Void?’ if let unwraped = completion {

Moment.jsを使ってタイムゾーンがEtc/GMTの日付と時刻をAsia/Tokyoに変更する

f:id:ie-kau:20170130023225p:plain:w400


iOSのIn-App Purchase の Auto-Renewing subscription (自動更新型の購読)を利用した開発では購入処理が走った際や、期限切れの際にAppleから得られるレシートの検証をAppleのサーバーにpostして行う必要があります。
当然ながら有効期限を表すexpires_date など時間に関するデータのタイムゾーンは標準時です。Appleの場合Etc/GMT。

現在進めている開発ではDBやサーバーのOSはAisa/Tokyoで動作させているので、データベース追加時や期限切れの確認のときには当然タイムゾーン変更する処理が必要になります。

バックエンドはNodeで作っているので、時間を扱うのに便利なMoment.jsを利用してタイムゾーンを変更していきます。

返ってくるデータの例 : 2017-01-30 16:03:34 Etc/GMT

const datetime = "2017-01-30 16:03:34 Etc/GMT".split(" ")
moment.tz(`${datetime[0]} ${datetime[1]}`, datetime[2]).clone().tz("Asia/Tokyo").format("YYYY-MM-DD HH:mm:ss")
// => 2017-01-31 01:03:34

と言った具合簡単にタイムゾーンの変更ができます。
とはいえ調べる必要があったのでメモメモ。

関係ないけどUTCとGMTの違いがよくわかってなかったので参考。

citizen.jp

  • GMT = 英国のグリニッジ(経度0度)の地方平均時(平均太陽が南中する時を正午とする)のこと
  • UTC = 国際原子時と世界時の誤差が0.9秒以上ならないようにうるう秒で調整をしたのがUTC(協定世界時:Coordinated universal time)

一般的には同義と考えて良い。