2015/06/25

OneDriveとデスクトップアプリ

OneDriveをWindows 8.1に同期して使っているとき、OneDrive中のファイルをデスクトップアプリから扱う方法に関して。

1. オンラインのみのファイルの判別


Windows 8.1ではOneDrive中のファイルを「オフラインで利用可能」か「オンラインのみ」に設定できるが、「オンラインのみ」にした場合、これをデスクトップアプリのSystem.IO.FileStreamで開こうとすると、例外(System.IO.IOException)が飛ぶ。

例えば、以下のうち下の利用可能性を「オンラインのみ」にしたファイルがそれに当たる。


同じフォルダをコマンドプロンプトから「dir /a」で表示させたもの。「オンラインのみ」のファイルはサイズが括弧で囲まれている。


「オンラインのみ」のファイルはファイルエントリだけがローカルにあって、データ本体はクラウド上にあるものが必要に応じて流れてくるものなので、その状態では開けないのは別に不思議ではない。

問題はそれを事前にどうやって判別するかで、WinRTではStorageFile.IsAvailableStorageFile.AttributesのLocallyIncompleteを見ればいい。一方、デスクトップアプリではそのものずばりの情報が出てこなくて時間を取られたが、答えは簡単だった。
このファイル属性のうち、Oのオフライン属性がそれで、OneDriveで「オンラインのみ」のファイルにはこれが付く。System.IO.FileAttributesのOfflineの項を見ると、"The file is offline. The data of the file is not immediately available."とあるので(OneDriveとは一言も出てこないが)、これらしい。

ということで、以下はテストコードとその結果。
using System;
using System.IO;
using System.Linq;
private class OneDriveFile
{
public static void CheckFileIsAvailable(string filePath)
{
var fileAttribute = File.GetAttributes(filePath);
Console.WriteLine("Attributes: {0}", fileAttribute);
var isAvailable = !fileAttribute.HasFlag(FileAttributes.Offline);
Console.WriteLine("Available: {0}", isAvailable);
try
{
new FileStream(filePath, FileMode.Open, FileAccess.Read).Close();
Console.WriteLine("CAN read.");
}
catch (IOException)
{
Console.WriteLine("CANNOT read.");
}
}
public void CheckFiles()
{
var folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "SkyDrive", "Reference");
CheckFileIsAvailable(Path.Combine(folderPath, "容量計算.xlsx"));
// Attributes: Archive
// Available: True
// CAN read.
CheckFileIsAvailable(Path.Combine(folderPath, "暗号技術.pdf"));
// Attributes: Hidden, System, Archive, SparseFile, ReparsePoint, Offline
// Available: False
// CANNOT read.
Console.ReadLine();
}
}
view raw OneDriveFile.cs hosted with ❤ by GitHub
見ての通り、「オンラインのみ」のファイルかどうかはオフライン属性の有無で判別できることが確認できる(オンラインとオフラインの用語がややこしい)。ちなみに、これらのファイルに付いている属性は、初めにエクスプローラーで属性を表示させたものと当然ながら一致している。

2. オンラインのみのファイルへのアクセス


オーソドックスにはOneDriveのAPIを使えばいい話だが、MSDNに以下のエントリがあった。
このサンプルコードをふむふむと読み始めて、途中で意味が呑み込めてきて……驚愕。

デスクトップアプリからWinRTのAPIを使うお約束をすっ飛ばし、デスクトップアプリでも使えるとされていないStorageFileクラスを、WinRTのアセンブリを動的ロードしてリフレクションして実行するラッパー、とは!

いや、Microsoftの人が書いているのだからできるのだろうが、何という力業。積極的に使いたいかは別にして、そういうのもあるのか……と。

3. Windows 10での変更


Windows 10ではまたOneDriveに変更があるらしく、現在のInsider Previewでは「オンラインのみ」の設定はなくなり、すべて「オフラインで利用可能」と同じく、データ本体がローカルにコピーされるようになっている。したがって、「オンラインのみ」のファイルを扱う方法はWindows 8.1で最後になるらしい。いい加減安定させた方がいいと思うが。

2015/06/20

水晶雫グラフィグ

水晶雫ちゃんの3回目の誕生日に合わせて、イラストレーターの桐野霞先生が水晶雫グラフィグを公開されていたので、早速作ってみました。
グラフィグというのは独特の四角いスタイルにデフォルメされた、キャラクターペーパークラフトのフォーマットのようなもの、だと思います。

1. 出来上がり


PDFをキンコーズでプリントアウトしたものから数時間の作業後、完成したお姿。
Suisho Shizuku Graphig

カラーレーザープリンターによる出力ということで、印刷物と変わらない美麗さでした。
Suisho Shizuku Graphig

グラフィグなので、とくに後ろから見ると箱箱しています。
Suisho Shizuku Graphig

首は回せるので、少し動きは付けられます。
Suisho Shizuku Graphig

これだけデフォルメしても雫ちゃんのイメージは保たれているのはさすがです。部品を作っている間はカクカクしてるなと思っていたのが、全部合わせた途端、なるほどなるほど、これはこれで、と顕現する感じです。

2. 参考


作り方に複雑なことはないですが、参考になりそうなことを。
  • キンコーズでの出力は、マットで210g/m2ぐらいの紙を使用。強度的には厚い方がいいですが、厚すぎると狙った位置できれいに折るのが難しくなります。結果的にはこれ以上厚いと難しくなる感じ。費用は、PC利用を除いたプリントアウトだけだと一枚50円強で、1セット2枚で100円強でした。一応、ミスに備えて予備の分もプリントアウトしておくとベター(自分は使った)。

  • 頭部の頬の部分の折り線が少しズレてるので、先に顎の部分を折り、机の角で箱に組みながら折る位置を順々に出していくといいです。この際、差し込むベロの部分は切り落として、のりしろと接着剤だけで固定するようにした方が調整はしやすいです。ただ、頭の側面は髪ですっぽり隠れるので、あまり神経質になる必要はないです。

  • 足の上下をぴったり接着したら、上下の長さが少し違っていて切り込まないといけなくなったので、仮組みをよくやった方がいいです。
まあしかし、手軽にできるのがペーパークラフトのいいところだと思うので、あくまで気楽にどうぞ。

2015/06/18

WinRTでの設定の保存(続)

PasswordVaultを使った設定の保存を確認しているうち、アプリをアンインストールしてもPasswordVaultに保存された情報は消えずに残り、再度アプリをインストールすると読み出せることに気づきました。

残るといっても暗号化されているので大過ないといえばないですが、一応、暗号化したファイルをLocalFolder/RoamingFolderに保存する(LocalFolder/RoamingFolderはアンインストール時に削除される)ものも追加していたら全体がややこしくなってきたので、まとめ直しました。
これを使った設定クラスは、例えば以下のようになります。
public class Settings : SettingsBase
{
private Settings()
{ }
public static Settings Current { get; } = new Settings();
[Roaming]
public string Name
{
get { return GetValue<string>(); }
set { SetValue(value); }
}
[Roaming]
public int Age
{
get { return GetValue<int>(); }
set { SetValue(value); }
}
public RaceType Race
{
get { return GetValue(RaceType.Human); }
set { SetValue(value); }
}
[Roaming]
public Schedule Schedule
{
get { return GetValue<Schedule>(); }
set { SetValue(value); }
}
[CryptFile]
[Roaming]
public Point WayPoint
{
get { return GetValue<Point>(); }
set { SetValue(value); }
}
[CryptFile]
public Color SymbolColor
{
get { return GetValue(Colors.RoyalBlue); }
set { SetValue(value); }
}
[CryptVault]
public string SecretPhrase
{
get { return GetValue<string>(); }
set { SetValue(value); }
}
}
view raw Settings.cs hosted with ❤ by GitHub
以下の属性で保存先と暗号化の有無を指定します。
  • RoamingAttribute : ローミングする。
  • CryptVaultAttibute : PasswordVaultに保存する。
  • CryptFileAttribute : 暗号化したファイルをLocalFolder/RoamingFolderに保存する。
PasswordVaultはローミングされるので、CryptVaultAttibuteの場合はRoamingAttributeは影響しません。また、CryptVaultAttibuteはCryptFileAttributeより優先されます。

型は、直接扱えないものはシリアライズして保存するので制限はない、はずですが、シリアライズ特有の問題は起こり得るので、それぞれ対処を。各プロパティの初回アクセス時にリフレクションを使ってどう扱うかを決めますが、結果をキャッシュしたものを次回から使うので、コストはそんなには高くないと思います。

まあ実際はここまでやらずとも、DictionaryにまとめてシリアライズしてRoamingFolderに保存一本でもいいんですが、あるものは使ってみたいという性分なので。

[追記] LocalSettings/RoamingSettingsでサポートされる型

LocalSettings/RoamingSettingsはIDictionaryになっていて、この値の型はObjectですが、実際に保存できるのはLocalSettingsの説明によればWindows Runtime base data typesとされています。

このリストの型をテストしてみたところ(voidを除く)、なぜかUriだけはサポートされてない型という例外が出て駄目でした。一方、ここにはないbyte[]でも保存できるので、どんな型でもシリアライズしてbyte[]にするか、そのままbyte[]に変換すれば、直接出し入れは可能です。

[修正]

例のシングルトンインスタンスの生成方法を修正。C# 6.0の理解が足りてませんでした。

2015/06/07

WinRTでの資格情報の保存

WinRTで資格情報、パスワードであったりアクセストークンであったり、に限らず、秘密を要する情報を安全に保存するにはWindows.Security.Credentialsが使えます。
ざっくりまとめると、
  • リソース名(Resource)、ユーザー名(UserName)、パスワード(Password)から成るPasswordCredentialオブジェクトをPasswordVaultクラスのメソッドを使って書き込み・読み出しする。
  • リソース名は、Twitterのようなサービス名、あるいは複数のユーザー情報をまとめるためのグループ名のようなもの。とくに分類する必要がなければ何でもいい。
  • ユーザー名とパスワードは、そのままの意味で使ってもいいし、要はDictionaryにおけるKeyとValueの組み合わせと同じなので、別の意味を持たせてもいい。
  • 一つのアプリで使えるPasswordCredentialの上限は10(後述)。
これをどう使うかというと、
  • 保存するときは、リソース名、ユーザー名、パスワードでPasswordCredentialを作成して、PasswordVault.Addメソッドを使う。

  • 保存したPasswordCredentialを取得するときは、
    • リソース名とユーザー名が分かっていればPasswordVault.Retrieveメソッドを使う。指定したPasswordCredentialが存在しなければ例外(System.Exception)が飛ぶので、要try-catch。
    • リソース名でまとめて取得するにはPasswordVault.FindAllByResourceメソッド、ユーザー名でまとめて取得するにはPasswordVault.FindAllByUserNameメソッドを使う。これらも存在しなければ例外が飛ぶので、要try-catch。
    • 全部まとめて取得するにはPasswordVault.RetrieveAllメソッドを使う。これは存在しなくても例外は飛ばないので、実はこれからフィルターして探し出せばtry-catchなしで済む。
    • なお、まとめて取得したPasswordCredentialはパスワードが空なので、パスワードを見るにはその前にPasswordCredential.RetrievePasswordメソッドを実行する要あり。

  • 保存したPasswordCredentialのパスワードを修正するときは、同じリソース名、ユーザー名でPasswordCredentialを作成してPasswordVault.Addメソッドを使えば上書きされる(PasswordCredentialを取得して修正しても、保存したものには反映されない)。

  • 保存したPasswordCredentialを削除するときは、一旦取得してからPasswordVault.Removeメソッドを使う。
これらをまとめてみたもの。
using System;
using System.Linq;
using System.Collections.Generic;
using Windows.Security.Credentials;
public class CredentialService
{
private string _resourceName;
private PasswordVault _vault;
public CredentialService(string resourceName)
{
if (String.IsNullOrEmpty(resourceName))
throw new ArgumentNullException("resourceName");
_resourceName = resourceName;
_vault = new PasswordVault();
}
public string Get(string key)
{
try
{
var credential = _vault.Retrieve(_resourceName, key);
return credential.Password;
}
catch
{
// If no such credential, a System.Exception will be thrown.
return null;
}
}
public void Set(string key, string value)
{
if (value != null)
{
// Create or overwrite a credential.
var credential = new PasswordCredential(_resourceName, key, value);
_vault.Add(credential);
}
else
{
// Remove a credential.
try
{
var credential = _vault.Retrieve(_resourceName, key);
_vault.Remove(credential);
}
catch
{
// If no such credential, a System.Exception will be thrown.
}
}
}
public IEnumerable<KeyValuePair<string, string>> GetAll()
{
var credentials = _vault.RetrieveAll().Where(x => x.Resource == _resourceName);
foreach (var credential in credentials)
{
credential.RetrievePassword(); // This is required to fill Password property.
yield return new KeyValuePair<string, string>(credential.UserName, credential.Password);
}
}
public void RemoveAll()
{
try
{
var credentials = _vault.FindAllByResource(_resourceName);
foreach (var credential in credentials)
{
_vault.Remove(credential);
}
}
catch
{
// If no such credential, a System.Exception will be thrown.
}
}
}
ここからGet/Setメソッドを静的メソッドにして、WinRTでの設定を保存するクラスでプロパティのバッキングストアに使えるようにすれば、秘密を要する情報も設定クラスでまとめて管理できるようになるかなと思います。

ちなみに、PasswordCredentialの上限はPasswordVault.Addメソッドの説明には10と書かれてますが、10より少ない状態から一気に作成すれば10を超えて作成できたりしたので、上限をチェックするアルゴリズムに現状は穴があるっぽいです。

それはともかく、量が多い場合は暗号化したファイルをLocalFolderなりRoamingFolderに保存しろという話で、そういう例もあります。