iPhoneに溜まっている写真をパソコンで直に吸い上げたいっ！


# 背景
私、iPhone使いです。
音楽聞く習慣がないので、パソコンにiTunes入れる必要性を感じません。と云うか入れたくありません。

私、秘密主義者です。
誰に覗かれるか分からないクラウドストレージはイマイチ不安。と云うか全く信用していません。

私、激烈心配性です。
離席中にひょっとして誰かにファイル抜かれたら嫌だなぁ。と云う事でWPDも無効化しちゃいました。
<br/>
……………………
iPhoneに溜まり続ける写真。
何とかしてパソコンに吸い上げられないかなぁ…
<br/>

```text:なお
        このドラマは大筋に於いてフィクションであり、
        登場する人物・団体・シチュエーションとかは
        現実世界での出来事とは、ほぼほぼ無関係…
                                  と思って下さい
                                
```

# 方針
なんて我儘で贅沢な悩みでしょうか。
こんだけ周りに壁を立てておいて、写真だけは特別扱いしたいなんて。

ま、実際問題としてそんな事って良くありますけどね。
でも、そんな状況に出くわしてしまうと、思わずめらめらと何かが燃え上がってしまう自分がいます。
<br/>
さて、そんな時取り敢えず思いつくのはやっぱりWebアプリですね。クラウドに向かっては出て行きたくないので、パソコンとiPhone間のプライベートネットワークって感じでしょうか。
となると、万人が「あーこりゃ簡単・便利！」って感じにはなりそうにもありませんが、まぁ「こんな事もできるんだねぇ…」位に思って貰えれば嬉しいかな。

# 構成
全体的な想定構成を決めておきましょう。
基本的に、動作検証の取れた自前の環境って事ですが…

* iPhone
* Windowsパソコン<br/>OSはWin10
* 宅内の無線LAN環境が必要です
* パソコン側にIISが必要ですね<br/>事前に設定しておきましょう<br/>※[ここら辺](https://creativeweb.jp/personal-site/iis/)が参考になるかも

# 対応
一番オーソドックスな方法です。パソコンをWebサーバに見立てて（？いや、しっかりとWebサーバですけど）、iPhoneからそこにアクセスする感じです。
まず、iPhoneでパソコンにアクセスできるかどうか確認してみましょう。

パソコンのIPアドレスを確認した処、現時点では`WiFi`アダプタのIPアドレスは`192.168.0.11`でした。

![image](https://qiita-image-store.s3.amazonaws.com/0/136132/7d728acc-6007-105b-2dec-f88058337d05.png)

んじゃ、`iPhone`で`safari`立ち上げて`http://192.168.0.11`とすると…

![image20160904213159610.png](https://qiita-image-store.s3.amazonaws.com/0/136132/fdd2b1bc-40bb-638f-805f-d0c9c73f55aa.png)

よしよし。

じゃぁ、画像をアップロードする為に、`c:\inetpub\wwwroot`の下に以下のファイルを置きましょう。（`UTF-8`で保存してね）

```html:select.html
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=300">
<title>画像転送</title>
</head>
<body>
<h1>画像転送</h1><br/>
<form action="upload.aspx" method="post" enctype="multipart/form-data">
<input type="file" name="upfile" multiple="multiple"/><br/><br/>
<input type="submit" value="転送"/>
</form>
</body>
</html>
```

```html:upload.aspx
<%@ Page Language="C#" CodeFile="upload.aspx.cs" Inherits="MyPage" %>
<% Proc(); %>
```

```csharp:upload.aspx.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Web;

public partial class MyPage : System.Web.UI.Page {
	const string imgPath = @"c:\users\■■■■\desktop\upImage";
	
	protected void Proc() {
		for(int i=0; i<Request.Files.Count; i++) {
			string fileName = Path.GetFileName(Request.Files[i].FileName);
			fileName = Regex.Replace(FileName, @"(?=\.\w+$)|(?<!\.\w+)$", DateTime.Now.ToString("yyyyMMddHHmmssfff"));
			Request.Files[i].SaveAs(Path.Combine(imgPath, fileName));
			Thread.Sleep(1);
		}
		Response.Write(@"<!DOCTYPE HTML>");
        Response.Write(@"<html lang=""ja"">");
        Response.Write(@"<head>");
        Response.Write(@"<meta charset=""UTF-8"">");
        Response.Write(@"<meta name=""viewport"" content=""width=300"">");
        Response.Write(@"<title>転送完了</title>");
        Response.Write(@"</head>");
        Response.Write(@"<body>");
		Response.Write(@"<a href=""select.html""><button>戻る</button></a>");
		Response.Write(@"</body>");
        Response.Write(@"</html>");
	}
}
```

あ、`■■■■`の所は自分のユーザ名にして下さい。
そして、デスクトップに`upImage`フォルダを作って、`EveryOne`に対して`FullControl`を与えておく必要があります。ここら辺が、`IIS`のセットアップと共に面倒臭いハードルですかねぇ…

で、`safari`で`http://192.168.0.11/select.html`にアクセスすると…

![image20160904225759384.png](https://qiita-image-store.s3.amazonaws.com/0/136132/914bf52a-b6e1-5493-a0dc-6ac58b2cc80c.png)

ファイル選択ボタンを押すと…

![image20160904225759392.png](https://qiita-image-store.s3.amazonaws.com/0/136132/db2b5a5d-c23c-5860-2996-b24afd251750.png)

よしよしよし…

画像を選択して、転送ボタンを押すと

![image20160904225824981.png](https://qiita-image-store.s3.amazonaws.com/0/136132/444873e8-06e0-1882-9174-3a85a146a1bd.png)

デスクトップのupImageに画像ファイルが転送されました。
よしよしよしよし…

# 別解
できるのはできたけど、やっぱり`IIS`立てたり、転送先を絶対パスで固定したり、妙なアクセス権付けたり…
と、イマイチ感がハンパないです。

なんとかなんないのー、`どらヱモ～〇っ！`
<br/>
　—　という事で、良いもの見つけました。
その名も`OWIN`。

`IIS`不要！
起動するだけでＯＫ！！

自分の中に`Web`サーバを抱え込んでくれる、と云う優れモノ。
つまり`exe`を起動するだけで外部からの`Web`アクセスを引き受けてくれると云う…
<br/>
では、始まりー

* Visual Studio Community 2015（等） を準備して下さい。
* 新規プロジェクトを始めましょう<br/>
![image](https://qiita-image-store.s3.amazonaws.com/0/136132/07c5bdac-4bf2-a9cb-bb6a-6c564f48755e.png)<br/>
![image](https://qiita-image-store.s3.amazonaws.com/0/136132/54a22d59-c973-6914-900d-89aa3d223bd0.png)<br/>
* ↓この画面は、今回`Azure`使わないので（もし出ても）`Cancel`しちゃってOKです
![image](https://qiita-image-store.s3.amazonaws.com/0/136132/81731d57-dd31-12c9-96db-4cb7c96c8c4f.png)<br/>
* ↓プロジェクトに新しい項目を追加しましょう
![image](https://qiita-image-store.s3.amazonaws.com/0/136132/587f13fe-d8c7-7f23-ab2d-7db196bc9a34.png)<br/>
* ↓OWIN Startup クラスを追加します
![image](https://qiita-image-store.s3.amazonaws.com/0/136132/23886509-cf28-ffe8-654e-d511a31836d3.png)<br/>
* ↓ツール→NuGet、経由でOWINHostを追加しましょう（色々確認されるけど、気にしない気にしない）
![image](https://qiita-image-store.s3.amazonaws.com/0/136132/15e377a6-3f11-38dd-4922-7f82f0c9702c.png)<br/>
![image](https://qiita-image-store.s3.amazonaws.com/0/136132/ac1843c2-058e-6fdd-9972-52aa16eccd55.png)![image](https://qiita-image-store.s3.amazonaws.com/0/136132/c542ff8d-1b17-163b-f696-23c4d0edc18d.png)![image](https://qiita-image-store.s3.amazonaws.com/0/136132/a00d15b5-064e-729d-304b-5703dd7cca7a.png)
* ↓追加された`Startup1`クラスの`Configuration`メソッドを整備しましょう<br/>こんな感じかなっ<br/>（本当は多分コントローラとかで対応するんだろうけど、今回は自分の為だけなので楽をしてみました）

```csharp:Startup1.cs
public class Startup1 {
	public void Configuration(IAppBuilder app) {
		// アプリケーションの設定方法の詳細については、http://go.microsoft.com/fwlink/?LinkID=316888 を参照してください
		app.Run(context => {
			StringBuilder sb = new StringBuilder();

			switch(context.Request.Path.Value) {
			case "/":
				context.Response.Redirect("/select");
				break;

			case "/select":
				sb.AppendLine(@"<!DOCTYPE HTML>");
				sb.AppendLine(@"<html lang=""ja"">");
				sb.AppendLine(@"<head>");
				sb.AppendLine(@"<meta charset=""UTF-8"">");
				sb.AppendLine(@"<meta name=""viewport"" content=""width = 300"">");
				sb.AppendLine(@"<title>画像転送</title>");
				sb.AppendLine(@"</head>");
				sb.AppendLine(@"<body>");
				sb.AppendLine(@"<h1>画像転送</h1><br/>");
				sb.AppendLine(@"<form action=""upload"" method=""post"" enctype=""multipart/form-data"">");
				sb.AppendLine(@"<input type=""file"" name=""upfile"" multiple=""multiple""/><br/><br/>");
				sb.AppendLine(@"<input type=""submit"" value=""転送""/>");
				sb.AppendLine(@"</form>");
				sb.AppendLine(@"</body>");
				sb.AppendLine(@"</html>");

				context.Response.Write(sb.ToString());
				break;

			case "/upload":
        		for(int i=0; i<context.Request.Files.Count; i++) {
        			string fileName = Path.GetFileName(context.Request.Files[i].FileName),;
        			fileName = Regex.Replace(fileName, @"(?=\.\w+$)|(?<!\.\w+)$", DateTime.Now.ToString("yyyyMMddHHmmssfff"));
        			context.Request.Files[i].SaveAs(Path.Combine(imgPath, fileName));
        			Thread.Sleep(1);
        		}

				sb.AppendLine(@"<!DOCTYPE HTML>");
				sb.AppendLine(@"<html lang=""ja"">");
				sb.AppendLine(@"<head>");
				sb.AppendLine(@"<meta charset=""UTF-8"">");
				sb.AppendLine(@"<meta name=""viewport"" content=""width = 300"">");
				sb.AppendLine(@"<title>画像転送</title>");
				sb.AppendLine(@"</head>");
				sb.AppendLine(@"<body>");
				sb.AppendLine(@"<a href=""select""><button>戻る</button></a>");
				sb.AppendLine(@"</body>");
				sb.AppendLine(@"</html>");

				context.Response.Write(sb.ToString());
				break;

			default:
				context.Response.StatusCode = 404;
				break;
			}

			return context.Response.WriteAsync(string.Empty);
		});
    }
}
```

あ～～～でもぉ、コンパイルが通らないぃ～　orz
`OWIN`の`Request`には`Files`がなかったのかー
<br/>
いや、でも何か解決策がある筈！
　　　：
ある筈…
　：

# 力技
見つからない…
`Nancy`ってのでは`Files`を扱っている例は見つけたんですが、残念ながらナンシー嬢、私の環境へのお誘いにはつれない感じ…
コンパイルまでも辿り着けない体たらく。

已むを得ん！
この際無理矢理にでもファイルを取り出してやるっ！

って事で、無理矢理感満載の解決を見ました。
`HTTP`の`InputStream`から直接ファイルを切り出す方式です。
※ エラーになる事を基本的に想定していない実装なので、とても打たれ弱いです
　 今日の所はこれ位で勘弁して下さい…
<br/>
ざっくりと方向性を説明すると—

* `HttpFileCollection `相当のクラスを作る
* `HttpPostedFile`相当のクラスを作る<br/>あれ？<br/>終わってしまった…

では、まず上記クラスから—

```csharp:FileCollection.cs
class FileCollection : NameObjectCollectionBase, IDisposable {
	private FileCollection() : base() { }
	public void Dispose() { ((IDisposable)ms).Dispose(); }
	public PostedFile this[int index] { get { return (PostedFile)this.BaseGet(index); } }

	public PostedFile this[string key] {
		get { return (PostedFile)this.BaseGet(key); }
		set { this.BaseSet(key, value); }
	}

	public string[] AllStringValues { get { return (string[])this.BaseGetAllValues(typeof(string)); } }
	public bool HasKeys { get { return this.BaseHasKeys(); } }
	public void Add(string key, PostedFile value) { this.BaseAdd(key, value); }
	public void Remove(string key) { this.BaseRemove(key); }
	public void Remove(int index) { this.BaseRemoveAt(index); }
	public void Clear() { this.BaseClear(); }

	MemoryStream ms = null;

	static public FileCollection GetFiles(IOwinContext context) {
		FileCollection fc = new FileCollection();

		HttpListenerRequest req = ((context.Request.Environment["System.Net.HttpListenerContext"]) as HttpListenerContext).Request;
		string boundary = "--" + Regex.Match(req.ContentType, @"(?<=boundary=)(.+(?=; )|.+$)").Value;

		fc.ms = new MemoryStream((int)req.ContentLength64);
		byte[] work = new byte[1024 * 1024];
		int cnt = 0;
		using(Stream stream = req.InputStream) {
			while((cnt = stream.Read(work, 0, work.Length)) > 0)
				fc.ms.Write(work, 0, cnt);
		}

		fc.ms.Seek(0, SeekOrigin.Begin);

		if(fc.ms.GetString(req.ContentEncoding, boundary.Length) != boundary) return fc;

		while(fc.ms.GetString(req.ContentEncoding, 2) == "\r\n") {
			string filename = null;
			string filetype = null;
			long spos = 0;
			long epos = 0;

			string line = null;
			while((line = fc.ms.GetLine(req.ContentEncoding)) != string.Empty) {
				if(line.StartsWith("Content-Disposition: ")) {
					filename = Regex.Match(line, @"(?<=filename=)(.+(?=; )|.+$)").Value.Trim('"');
					continue;
				}
				if(line.StartsWith("Content-Type: ")) {
					filetype = Regex.Match(line, @"(?<= ).+$").Value;
					continue;
				}
			}

			spos = fc.ms.Position;
			do {
				epos = fc.ms.SkipLine();
			} while(!fc.ms.IsFileEnd(req.ContentEncoding, boundary));

			fc.Add(Path.GetFileName(filename), new PostedFile(fc.ms, filetype, filename, spos, epos));
		}

		return fc;
	}
}
```

<br/>

```csharp:PostedFile.cs
class PostedFile {
	public PostedFile(Stream baseStream, string type, string name, long start, long end) {
		this.Base = baseStream;
		this.StartPos = start;
		this.ContentLength = (int)(end - start);
		this.ContentType = type;
		this.FileName = name;
	}

	Stream Base = null;

	private long StartPos = 0;
	public int ContentLength { get; private set; }
	public string ContentType { get; private set; }
	public string FileName { get; private set; }
	public Stream InputStream { get { return new SubStream(Base, StartPos, ContentLength); } }

	public void SaveAs(string filename) {
		byte[] work = new byte[1024 * 1024];
		int c = 0;

		using(Stream st = InputStream)
		using(FileStream fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
			while((c = st.Read(work, 0, work.Length)) > 0)
				fs.Write(work, 0, c);
		}
	}
}
```

後はヘルパさん達…

```csharp:SubStream.cs
class SubStream : Stream {
	public SubStream(Stream s, long start, long length) {
		this.stream = s;
		this.start = start;
		this.length = length;

		this.stream.Seek(this.start, SeekOrigin.Begin);
	}

	Stream stream;
	long start;
	long length;

	public override long Length { get { return length; } }
	public override bool CanRead { get { return this.Position < length; } }
	public override bool CanWrite { get { return false; } }
	public override bool CanSeek { get { return true; } }

	public override void Flush() { }

	public override long Position {
		get { return this.stream.Position - this.start; }
		set { this.stream.Position = this.start + value; }
	}

	public override int Read(byte[] buffer, int offset, int count) {
		if(!CanRead) return 0;

		count = (int)Math.Min(count, this.Length - this.Position);
		return this.stream.Read(buffer, offset, count);
	}

	public override long Seek(long offset, SeekOrigin origin) {
		switch(origin) {
		case SeekOrigin.Begin:
			this.stream.Seek(this.start + offset, origin);
			break;

		case SeekOrigin.Current:
			this.stream.Seek(offset, origin);
			break;

		case SeekOrigin.End:
			this.Seek(this.start + this.length + offset, origin);
			break;
		}

		return this.Position;
	}

	public override void Write(byte[] buffer, int offset, int count) {
		throw new NotImplementedException();
	}

	public override void SetLength(long value) {
		throw new NotImplementedException();
	}
}
```

<br/>

```csharp:ExtClass.cs
static class ExtClass {
	static public string GetString(this Stream Me, Encoding encoding, int length) {
		byte[] work = new byte[length];
		Me.Read(work, 0, length);
		return encoding.GetString(work);
	}

	static public string GetLine(this Stream Me, Encoding encoding) {
		List<byte> work = new List<byte>();
		while(!work.IsLineEnd())
			work.Add((byte)Me.ReadByte());
		return encoding.GetString(work.ToArray()).TrimEnd('\r', '\n');
	}

	static public long SkipLine(this Stream Me) {
		for(byte b1 = (byte)Me.ReadByte(), b2 = 0; !(b2 == '\r' && b1 == '\n'); b1 = (byte)Me.ReadByte())
			b2 = b1;
		return Me.Position - 2;
	}

	static public bool IsLineEnd(this List<byte> Me) {
		if(Me.Count < 2) return false;
		return (Me[Me.Count - 2] == '\r') && (Me[Me.Count - 1] == '\n');
	}

	static public bool IsFileEnd(this Stream Me, Encoding encoding, string boundary) {
		byte[] work = encoding.GetBytes(boundary);
		foreach(byte b in work)
			if(b != Me.ReadByte()) {
				Me.Seek(-1, SeekOrigin.Current);
				return false;
			}
		return true;
	}
}
```

<br/>
で、`Startup1.cs`の一部を—

```csharp:Startup1.csの一部
                        ：
    switch(context.Request.Path.Value) {
                        ：
                        ：
		case "/upload":
			FileCollection fc = FileCollection.GetFiles(context);
			for(int i = 0; i < fc.Count; i++) {
				string fileName = Path.GetFileName(fc[i].FileName);
				fileName = Regex.Replace(fileName, @"(?=\.\w+$)|(?<!\.\w+)$", DateTime.Now.ToString("yyyyMMddHHmmssfff"));
				fc[i].SaveAs(Path.Combine(@"pic", fileName));
				Thread.Sleep(1);
			}		

                        ：
                        ：
    }
```

こんな感じ…
<br/>
実際にiPhoneと連携したい時は、デバッグ実行では無理っぽい感じなので、以下の手順で実行完了を整えてください。

* まず、ビルドしときましょう
* ソリューションフォルダ（`TestOwin.sln`がある処）からみた、`packages\OwinHost.3.0.1\tools`フォルダを丸ごとデスクトップにコピーします<br/>※`PS`のスクリプトファイルは気になる人は消してもいいです
* プロジェクトフォルダ（`TestOwin.csproj`がある処）配下にある`bin`フォルダをデスクトップに作った`tools`フォルダにコピーします
* `tools`フォルダの中に`pic`フォルダを作っておいて下さい
* あ、`tools`って名前が`なんだかなー`って時は好きな名前に変えて貰って大丈夫

で、実行環境は整いました。
今度は実行手順です。（`tools`フォルダで話を続けます）

* 管理者でコマンドウィドウを開きます
* `cd c:\Users\■■■\Desktop\tools`でカレントディレクトリを移動します
* `OwinHost.exe -u http://192.168.3.3:5000`<br/>で、以下のような画面が出てアクセス待ちになります
![image](https://qiita-image-store.s3.amazonaws.com/0/136132/c1a80b87-df55-5665-62f7-dce0919094c5.png)
尚、上記コマンドの`IP`アドレスは実行しているパソコンのアドレスを指定して、ポートは…　まぁ5000を指定しとけば良いと思います<br/>
因みに、前の方とIPアドレス体系が変わってしまっているのは、プロバイダ乗り換えたからです…`(^_^;`
* アクセス待ちになったら、`iPhone`で`safari`立ち上げて、`http://192.168.3.3:5000`にアクセスすると…<br/>
`IIS`パターンと同じ画面が出るだけなので画面貼るのは省略しますが、`tools\pic`の下に画像が転送されるのが確認出来る筈です
* 転送が一通り終わったら、コマンド画面で`Enter`キーを押しましょう

# 一捻り
画像ファイルが転送できる事は前ケースで確認できました。
でも、前提が色々なぁ…

つまり、家にいてホームネットワークが構築されている状況が仮定されています。
ホームネットワークがない時にはどうしたら良いでしょうか？
例えば、旅行中とか、もにょもにょとか…
<br/>
そういう時は、パソコンとiPhone間で直接ネットワークを構築するって技があります。
これならば、外出していても写真を吸い上げることができます。

直接ネットワークを構築する時には、パソコン側をWiFi親機にしてあげる必要があります。
[SoftAP](http://www.infraexpert.com/study/wireless48.html)って奴ですかねぇ。
まぁ、ここら辺を確認してみて下さい。

以下ざっとした手順。（`cmd.exe`を管理者モードで起動しておいて下さい）

1.`netsh wlan set hostednetwork mode=allow`
2.`netsh wlan set hostednetwork ssid=■■■` （`■■■`欄は希望のSSID）
3.`netsh wlan set hostednetwork key=■■■ keyusage=persistent` （`■■■`欄は希望のパスワード）

※ここまでは一度設定したら、毎回入力は不要

4.`netsh wlan start hostednetwork`※転送を開始するときに入力
5.`netsh wlan stop hostednetwork`※転送が完了したら入力

………
なんですけど、ここまでやって衝撃の事実にぶち当たりました。
私のパソコン`hostednetwork`モードに対応していない…
（ぼっこいネットワークカード使いやがって、もー）
<br/>
という事で、一応情報の提供までという事で。
御粗末様でした…