ttlog

日々の開発で得た知見の技術メモ。モバイルアプリネタが多いです。

大量のファイルをタイムスタンプの日付毎にフォルダ分け

大量の写真ファイルを整理したかったのでRubyでサクッと書き殴り。 とはいえ、また使いそうな気もするのでメモしておきます。

require 'fileutils'

Dir.chdir("xxx")   # ファイルが入っているディレクトリ
Dir.each_child(".") { |file|
  timestamp = File.mtime(file)
  dirname = timestamp.strftime("%F") # "YYYY-MM-DD"形式(ISO-8601)
  if File.directory?(file) then
    next  # ディレクトリは無視
  end
  if not Dir.exist?(dirname) then
    Dir.mkdir(dirname)
  end
  FileUtils.mv(file, dirname)
}

Firebaseのプロジェクト上限数の追加リクエストをしてみました

最近は技術検証等でFirebaseプロジェクトを作ることが多く、上限数に引っかかってしまいました。

不要なプロジェクトを削除しても30日間は復旧可能な論理削除のような状態で残っているようで、削除後もプロジェクト追加が出来なかったので、以下のページから上限数追加のリクエストを送ってみました。ちなみに以下のページはプロジェクト作成時に上限数に達している場合は誘導のリンクが表示されるはずです。(スクショ撮り忘れ)

support.google.com

名前とかメールアドレスを適当に記入し、無料アプリを作りたかったので「Free Services」にチェック、プロジェクト数はとりあえず「5」にしておいてSubmitで良いだろと思いきや、最後の項目も必須とのこと。

Any other things we need to be aware of to help us understand the request? *

和訳: その他、要望を理解するために気をつけるべきことがあれば教えてください。by DeepL

気をつけるべきことって何だ...と思いつつも以下のように適当に記載してSubmit。

We want to develop iOS and Android app using Firebase Auth, Firestore, Cloud Messaging, and Functions. We also want to do some technical research on Firebase for our clients.

「Firebase Auth、Firestore、Cloud Messaging、Functionsを使ったiOSAndroidアプリを開発したいと考えています。また、クライアントのためにFirebaseの技術的な調査も行いたいです。」的な内容です。

で、Submitするやいなや数秒でメールを受信。

Quota Granted

一瞬で承認されました。 これ間違いなくメッセージ読んでないなと思いつつ、めでたしめでたし。

Firebaseエミュレータが起動しない

以前、Zennのスクラップ機能にメモを残しながらFlutter/Firebaseアプリを作成していましたが約1年が経過。

長らくFlutter/Firebaseから離れてしまっていましたが再開。 そしてFirebaseエミュレータを起動しようとしたら↓のようなエラーが。

⚠  hosting: Port 5000 is not open on localhost, could not start Hosting Emulator. {"metadata":{"emulator":{"name":"hosting"},"message":"Port 5000 is not open on localhost, could not start Hosting Em
ulator."}}
⚠  hosting: To select a different host/port, specify that host/port in a firebase.json config file:
      {
        // ...
        "emulators": {
          "hosting": {
            "host": "HOST",
            "port": "PORT"
          }
        }
      } {"metadata":{"emulator":{"name":"hosting"},"message":"To select a different host/port, specify that host/port in a firebase.json config file:\n      {\n        // ...\n        \"emulators\":
 {\n          \"hosting\": {\n            \"host\": \"\u001b[33mHOST\u001b[39m\",\n            \"port\": \"\u001b[33mPORT\u001b[39m\"\n          }\n        }\n      }"}}
i  emulators: Shutting down emulators. {"metadata":{"emulator":{"name":"hub"},"message":"Shutting down emulators."}}

Error: Could not start Hosting Emulator, port taken.

Firebase Hostingで使用する5000番ポートが開かないと。 どうやらmacOS MontereyからAirPlayが5000番を使うようになったらしく、ポートの衝突が起きているらしい。 firebase.jsonを開き、"hosting"のポート番号を変更すると起動出来るようになりました。

     "hosting": {
-      "port": 5000
+      "port": 5002
     },

参考

Azure DevOps(Boards)のWork ItemをOrganization間で移行する

はじめに

あまり無いことだとは思いますが、Azure DevOpsの情報を別Organization(企業)へ移行する必要が生じました。 Azure Reposのソースコードの移行はGitなので簡単に出来たのですが、Azure BoardsのWork Itemについてはなかなか情報が見つかりませんでした。 Azure DevOps CLIを使ってみたり、移行元でQueriesを使い全WorkItemをExcelにエクスポートし、移行先でインポートする方法などもありましたが、添付ファイル(Attachment)の移行が出来なかったり等、色々問題がありそうなので採用出来ず。 いろいろ調べた結果、Azure DevOps Migration ToolsというOSSツールを使用することである程度の移行が出来たため、手順を記録しておきます。

これらの手順を実行する場合は自己責任にてお願いします。

Azure DevOps Migration Tools とは

名前の通り、Azure DevOpsの移行用ツールです。C#で記述されているオープンソースのツールになります。移行対象はAzure Boards(Work Item)がメインですが、Azure Test Plansの移行もbeta版ですが提供されているようです。(2020年10月現在)

Windows専用ツールであり、Windows向けパッケージマネージャであるChocolateyからインストール出来ます。

公式サイトはこちらです。

また、GitHubリポジトリはこちらです。

注意点

このツールは公式サイトに以下の記載がある通り、初心者向けのツールではありません。Azure DevOpsの構造に精通していないと運用は難しいかと思います。

WARNING: This tool is not designed for a novice. This tool was developed to support the scenarios below, and the edge cases that have been encountered by the 30+ contributors from around the Azure DevOps community. You should be comfortable with the TFS/Azure DevOps object model, as well as debugging code in Visual Studio.

こんな記事を書いている私ですが、およそ精通してるとは言えないため、使い方が誤っている可能性は大いにあります...ご了承ください...

何が移行出来た?

  • タイトル
  • State(New, Active, Closed, etc.)
  • 親子関係
    • ただし、移行出来たものもあれば何故か出来なかった(Unparentになった)ものも...
  • コメント
  • 添付ファイル(Attachment)
  • Area
  • Iteration
  • Tags

etc...おおよそのものは移行できるのではないかと思います。

何が移行できなかった?

  • アサイン先(Assigned To)
    • 移行元のユーザのまま。(当たり前ですが)
    • あとでQueriesを使い移行元ユーザを抽出 → 一括で変更は出来たため、そこまで問題無し。
  • ブランチ、Pull Request等のLink情報
    • 移行自体は出来るけど、移行先のLink対象と一致せずエラーになり、結局は自分で設定し直すことに。

設定次第ではもしかしたら出来たかもしれないけど、私の知識では不可能でした。

移行手順

移行先プロジェクトのWork Itemを拡張する

移行先のWork Itemに移行状態を記載するというツールの仕様上、これを記載するためにWork ItemにCustom Fieldを追加する必要があります(※移行先のみ)。 まずはこのCustom Fieldを追加します。

Work Itemを拡張するためのInherited Processを作成する

  • Organization Settings > Boards > Processを開く。
  • Processesタブから、ベースとなるProcess(私の場合は”Agile")の右側の"..."をクリックし、"Create inherited process"を開く。
  • Process name, Descriptionを記述し、”Create process"を選択し、新しいProcessを作成する。

作成したInherited ProcessにField追加

  • 作成したProcessを選択し、"Bug"を選択する。
  • "Layout"タブから"New field"を選択する。
  • 表示されたポップアップで"Create a field"を選択し、以下を入力して"Add field"を選択する。
    • Name: "ReflectedWorkItemID"
    • Type: "Text (single line)"
  • 以上をWork Item Type全て(Epic, Feature, etc.)に適用する(何故か"Bug"に追加しただけで動作してしまったけど、恐らく全部に適用した方が良さそう?)

ここで設定した名前("ReflectedWorkItemID")は後述する設定ファイルに記述する名前になります。

移行先プロジェクトのProcessを作成したInherited Processに変更する

  • Organization Settings > Boards > Processを開く。
  • Processesタブから、移行先のプロジェクトが適用しているProcessを開く。
  • Projectsタブを開き、プロジェクト名右側の"..."をクリックし、"→ Change process"を選択する。
  • 新しく作成したProcessを選択し、保存する。

ちなみにProcessについては以下の記事が分かりやすく書いていただいており参考になります。
https://qiita.com/mstakaha1113/items/2c857e85ed6203d93028

Azure DevOps Migration Toolsのインストール

以下の二通りがあるようです。

  • (recommended)Install from Chocolatey
  • Download the latest release from GitHub and unzip

私は前者のChocolateyを使用しました。

  1. Chocolateyのインストール(https://chocolatey.org/install)
  2. コマンドプロンプトから以下を実行
    • choco install vsts-sync-migrator

成功するとc:\tools\vstssyncmigration\にインストールされ、この中のmigrate.exeを使用して移行します。

設定ファイルの作成

基本的には公式サイトのGetting Startedにサンプルの設定ファイルを含めて記載されています。

  1. コマンドプロンプトを開き、c:\tools\vstssyncmigration\へ移動する
  2. ./migration.exe initを実行
  3. configuration.jsonが作成されるので、それを編集する

私の場合はサンプルの設定ファイルを色々いじった結果、以下のようになりました。

{
  "Version": "11.0",
  "TelemetryEnableTrace": false,
  "workaroundForQuerySOAPBugEnabled": false,
  "Source": {
    "Collection": "https://dev.azure.com/{移行元Organization}",
    "Project": "{移行元プロジェクト名}",
    "ReflectedWorkItemIDFieldName": "ReflectedWorkItemID",
    "AllowCrossProjectLinking": false,
    "PersonalAccessToken": "{移行元で作成したPersonal Access Token(PAT)}",
    "LanguageMaps": {
      "AreaPath": "Area",
      "IterationPath": "Iteration"
    }
  },
  "Target": {
    "Collection": "https://dev.azure.com/{移行先Organization}",
    "Project": "{移行先プロジェクト名}",
    "ReflectedWorkItemIDFieldName": "ReflectedWorkItemID",
    "AllowCrossProjectLinking": false,
    "PersonalAccessToken": "{移行元で作成したPersonal Access Token(PAT)}",
    "LanguageMaps": {
      "AreaPath": "Area",
      "IterationPath": "Iteration"
    }
  },
  "GitRepoMapping": null,
  "Processors": [
    {
      "ObjectType": "NodeStructuresMigrationConfig",
      "PrefixProjectToNodes": false,
      "Enabled": true
    },
    {
      "ObjectType": "WorkItemMigrationConfig",
      "QueryBit": "AND  [Microsoft.VSTS.Common.ClosedDate] = '' AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan')",
      "OrderBit": "[System.ChangedDate] desc",
      "Enabled": true,
      "LinkMigration": true,
      "AttachmentMigration": true,
      "AttachmentWorkingPath": "c:\\temp\\WorkItemAttachmentWorkingFolder\\",
      "FixHtmlAttachmentLinks": false,
      "SkipToFinalRevisedWorkItemType": false,
      "WorkItemCreateRetryLimit": 5,
      "FilterWorkItemsThatAlreadyExistInTarget": true,
      "PauseAfterEachWorkItem": false,
      "AttachmentMazSize": 480000000,
      "CollapseRevisions": false,
      "LinkMigrationSaveEachAsAdded": false
    }
  ]
}
  • "Version"はインストールしたAzure DevOps Migration Toolsのバージョンを記述
    • ./migration.exe --versionで確認
  • "Source"以下に移行元の情報を記述
  • "Target"以下に移行先の情報を記述
  • "ReflectedWorkItemIDFieldName"にはWork Itemを拡張した際に追加したCustom Fieldに指定した名前("ReflectedWorkItemID")を記述
  • "FieldMaps", "WorkItemTypeDefinition"は不要そうだったので削除
    • 移行元と移行先でFieldやWorkItemTypeが異なる場合のマッピング設定?
  • "Processors"内にそれぞれの"Enabled"はtrueに変更(trueにしないとそのProcessorが起動しない)

移行開始

以下のコマンドで移行ツールを起動します。

./migration.exe --config configuration.json

途中、移行元/移行先のMicrosoftアカウントへのログインが表示されるかもしれませんが、必要に応じてログインすれば続行するかと思います。

まとめ

私は以上の手順で、不完全ではありますが移行が出来ました。

最初に書きましたが、私自身あまりAzure DevOpsに詳しくなく、他のプロジェクトでも同様に移行できるかは全く不明なため、これらの手順を実行する場合はあくまで自己責任にてお願いします。

iOS 端末のモデル識別子を取得する

開発中のアプリで端末の機種を判定したかったのですが、 UIDevice.current.modeliPhoneiPod touch としか返さないため、もう少し詳細な情報を取得する方法を調べました。

今回のコードにより iPhone12,3 等の形式の識別子を取得することが出来ます。

実装コード

今回はUIDeviceのextensionとして実装してみます。

extension UIDevice {
  var modelIdentifier: String {
      var systemInfo = utsname()
      uname(&systemInfo)
      let machineMirror = Mirror(reflecting: systemInfo.machine)
      let identifier = machineMirror.children.reduce("") { identifier, element in
          guard let value = element.value as? Int8, value != 0 else { return identifier }
          return identifier + String(UnicodeScalar(UInt8(value)))
      }
      return identifier
  }
}

検索すると、ここから更にモデル名(iPhone12,3iPhone 11 Pro)にマッピングする記事を見かけますが、個人的にはこの状態で十分だったため止めておきます。

マッピング自体は以下が参考になるかと思います。

Models - The iPhone Wiki (Identifier列を参照)

Expressサーバ上にGatsbyサイトをデプロイしてみた

最近Gatsbyをチマチマ触っているので、Expressサーバ上に載せてみました。 基本的にはExpressの静的ファイル配布機能を使用しているだけですが。

※モバイル畑なので、Web関連はあまり詳しくありません。

プロジェクトの作成

$ gatsby new foo
$ cd foo
$ npm install express

(余談ですが、npm installに --save オプションって不要になってたんですね。)

サーバ側の実装

プロジェクト直下にindex.jsを作成

$ touch index.js

index.jsの編集

const express = require("express")
const app = express()
const port = 3000

// `gatsby build` の結果がpublicディレクトリに出力される
app.use(express.static("public/"))

app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}`)
})

起動スクリプト

package.jsonの編集

...
  "scripts": {
    "prestart": "gatsby build",
    "start": "node index.js"
  }
...

サーバ起動

$ npm start

以下にアクセスして、サイトが表示されることを確認。

http://localhost:3000

設定アプリ(Settings Bundle)にUUIDを表示する方法

たまにですが、設定アプリに「ユーザーID」といった項目名でUUIDを表示しているアプリを見かけます。 開発中のアプリでも端末識別用のUUIDを設定アプリに表示する必要があったため方法を調べてみたのですが、 予め決まっている固定値を表示する方法がほとんどで、アプリ側で生成したUUIDを表示させるといった情報が意外と見つからなかったため、自分が試して実現した方法を書き記しておこうと思います。

結論

Settings Bundleの値はUserDefaultsで取得出来る、ということはアプリ側から書き込みすれば設定アプリに表示出来るのではと考え、予め設定項目だけ作成しておき、アプリ起動時にUserDefaultsに書き込むことで表示することが出来ました。

※ただ、当然ながらアプリインストール直後で起動していない時点では書き込みが出来ておらずデフォルト値が表示されます。この辺りの解決方法は見つかっていません。

実装手順

Settings.bundle を作成

通常通り、ターゲットのルートディレクトリにSettings.bundleを作成します。

公式の情報はこの辺りでしょうか。 → Implementing an iOS Settings Bundle

設定値の作成

以下のように設定値をRoot.plistに作成します。

  • Item 0: Group
    • Title: "UUID"
  • Item 1: Title
    • Default Value: "xxx"
    • Identifier: "uuid"

f:id:tommy10344:20200717000525p:plain

  • UUIDは長い文字列のため、項目名は専用のGroup(Item 0)のTitleに表示するようにし、Item 1にTitleを表示させずに横幅を伸ばすようにしています。
  • Default Valueは何かしら記載しておかないと項目自体が設定アプリに表示されないようです。(半角スペース(" ")でも項目の表示はされましたが、これ良いのかな...)
  • Item 1のIdentifierの値は後ほどUserDefaultsに書き込む際に使用します。

UserDefaultsに書き込み

今回はUUIDとして UIDevice.current.identifierForVendor を使用しています。

identifierForVendor

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    ...
    
    // UUID取得
    let uuid = UIDevice.current.identifierForVendor!
    // UserDefaultsに書き込み。
    // Settings.bundleの"Identifier"で指定した値をKeyにして書き込む。
    UserDefaults.standard.set(uuid.uuidString, forKey: "uuid")
    
    ...
}

このようにしてアプリを起動後、設定アプリを確認するとUUIDが表示されているかと思います。

f:id:tommy10344:20200717002203p:plain

まとめ

所々微妙な部分があるのですが、この方法でとりあえずは実現出来ました。

またここまでの内容を簡単なサンプルコードとしてGitHubにアップしています。

GitHub - tommy10344/SettingsBundleUUID