ネットワークプレースの作り方がわからん

Windows7 では、WebDAVをネットワークドライブとして割り当てることができました。
「ネットワークドライブとWebDAV over SSL」

ですが、WindowsXP(SP3)では失敗してしまいました。
XPでは、ネットワークドライブの割り当てに、WebDAVは使えないようです。

XPではWebフォルダ(ネットワークプレース?)を作成することで解決できるようです・・・が、肝心のネットワークプレースをWindowsAPIを使って作成方法がマイクロソフトからは公開されてないみたいです。

すみません、Windowsのネットワーク関係のことがよく分かってません。ので、まるでトンチンカンなことを書いてるかもしません。備忘録なので、ご注意を。)

ネットワークプレースの仕組み自体はカンタン。。。というか、ネットワークプレース自体がフォルダーリンクなので、

  1. ディレクトリを作成
  2. 作ったディレクトリの中に target という名のショートカットを作成
  3. そのショートカットにリードオンリー属性をつける。
  4. 下記内容のDesktop.ini(要システム・隠し属性)ファイルを作成
    [.ShellClassInfo]
    CLSID2={0AFACED1-E828-11D1-9187-B532F1E9575D}
    Flags=2
    
  5. ディレクトリにリードオンリー属性に。

これだけ。ショートカットはIShellLink/IPersistFile インターフェイスで作れます。

だだ、この方法では WebDAV(https/http)のネットワークプレースは作れません。
理由は至極当然で、上記(2)のWebDAVのアドレスへのショートカットが原因で、具体的には、https/http などのURIリンク先を単純にIShellLink::SetPath()に渡しても、ショートカットファイル自体は作れても、エクスプローラからWebフォルダとしては認識されません。

リンク先が、HTTP/HTTPSのURLではなく、FTP(ftp://server_name/directory/)や共有フォルダ(\\server_name\folder)のURIならうまくいきます。

このHTTP/HTTPSへのショートカットファイルは、普通のショートカットファイルではなく、どうやら特殊な(非公開の)方法で作られる必要があるようです。
もしかしたら公式のMSDNドキュメントに記述されているのかもしれません。が、結構検索かけて探してみたけどわかりませんでした。ご存じの方は教えて欲しいです (--;;;
WebDAV APIというのがあって、DavAddConnectionなどのようなAPIはありますが、Vista以降用のもののようですし、そもそも DavAddConnectionなどのAPIはSSL接続用、というようなことが書かれてますので、ちょっと違うみたいです。

で、もうちょっとグーグル先生に粘って聞いてみると・・・マイクロソフトのユーザーフォーラム?でそれらしき情報がありました。
【How to create web folders programmatically using Windows Script Host】

英文を要約すると、target.lnk そのもののバイト配列を用意してファイルに書き込むという、かなり力技で強引な(^^;;解決方法が提示されてます。
エクスプローラシェルでWebフォルダを作った後、そのtarget.lnkを解析したみたいですね。

この情報を元に、整理してC/C++で書き直して見ました。

#pragma comment(lib,"shell32.lib")

#define URI_LIMIT_SIZE 1024

#include <windows.h>
#include <strsafe.h>
#include <shlobj.h>
/*
 pURI : WebDAVへのhttp/httpsから始まるURLアドレス
 pDir : ネットワークプレースを作成するディレクトリ
 pName : ネットワークプレースの名前
*/
HRESULT CreateURILinkByEmbedByteArray(PCTSTR pURI,PCTSTR pDir,PCTSTR pName)
{
  HRESULT hres = S_OK;

  TCHAR szPath[MAX_PATH] = {0};
  TCHAR szDir[MAX_PATH] = {0};

  WCHAR szName[URI_LIMIT_SIZE] = {0};
  WCHAR szURI[URI_LIMIT_SIZE] = {0};
  
  HANDLE hFile = NULL;
  DWORD dwLen = 0;
  
  BYTE part1[] = 
    { 0x4c,0x00,0x00,0x00,0x01,0x14,0x02,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,
      0x00,0x00,0x00,0x46,0x81,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0x00,0x14,0x00,
      0x1f,0x50,0xe0,0x4f,0xd0,0x20,0xea,0x3a,0x69,0x10,0xa2,0xd8,0x08,0x00,0x2b,0x30,
      0x30,0x9d,0x14,0x00,0x2e,0x00,0x00,0xdf,0xea,0xbd,0x65,0xc2,0xd0,0x11,0xbc,0xed,
      0x00,0xa0,0xc9,0x0a,0xb5,0x0f,0xa4,0x00,0x4c,0x50,0x00,0x01,0x42,0x57,0x00,0x00,
      0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,
      0x00,0x00};
  BYTE part2[] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 };
  BYTE nameLen[] = {0x00, 0x00};
  BYTE uriLen[] = {0x00, 0x00};

  int cbLen = lstrlen(pName);

  part1[0x4d] = part1[0x77] = (cbLen <= 44) ? 0x00 : 0x01;

  nameLen[0] = cbLen % 0x100;
  nameLen[1] = cbLen / 0x100;

//ワイド文字に変換。ユニコードでビルドする場合は、単にコピーするだけ。
#ifdef UNICODE
  StringCchCopy(szURI,URI_LIMIT_SIZE,pURI);
  StringCchCopy(szName,MAX_PATH,pName);
#else
  MultiByteToWideChar(CP_ACP,0,pURI,-1,szURI,URI_LIMIT_SIZE);
  MultiByteToWideChar(CP_ACP,0,pName,-1,szName,MAX_PATH);
#endif

  cbLen = lstrlen(pURI);
  uriLen[0] = cbLen % 0x100;
  uriLen[1] = cbLen / 0x100;
  
  //ディレクトリの作成
  StringCchPrintf(szDir,
                  MAX_PATH,
                  TEXT("%s%s%s"),
                  pDir,
                  (szDir[lstrlen(szDir) - 1] != TEXT('\\')) ? TEXT("\\") : TEXT(""),
                  pName);
  SHCreateDirectoryEx(NULL,szDir,NULL);

  //target.lnkのパスを生成  
  StringCchPrintf(szPath,MAX_PATH,TEXT("%s\\%s"),szDir,TEXT("target.lnk"));

  //シェルリンク・ファイルへの書込み
  hFile = CreateFile(szPath,
                     GENERIC_WRITE,
                     0,
                     NULL,
                     CREATE_NEW,
                     FILE_ATTRIBUTE_NORMAL,
                     NULL);

  if(hFile == INVALID_HANDLE_VALUE || GetLastError() == ERROR_ALREADY_EXISTS)
    {
      hres = E_FAIL;
      goto cleanup;
    }

  //埋め込み
  WriteFile(hFile,reinterpret_cast<PVOID>(part1),sizeof(part1),&dwLen,NULL);
  WriteFile(hFile,reinterpret_cast<PVOID>(nameLen),2,&dwLen,NULL);
  WriteFile(hFile,reinterpret_cast<PVOID>(szName),sizeof(WCHAR) * (lstrlenW(szName) + 1),&dwLen,NULL);
  WriteFile(hFile,reinterpret_cast<PVOID>(uriLen),2,&dwLen,NULL);
  WriteFile(hFile,reinterpret_cast<PVOID>(szURI),sizeof(WCHAR) * (lstrlenW(szURI) + 1),&dwLen,NULL);
  WriteFile(hFile,reinterpret_cast<PVOID>(part2),sizeof(part2),&dwLen,NULL);

  CloseHandle(hFile);
  
  //ファイルにRO属性を設定
  SetFileAttributes(szPath,FILE_ATTRIBUTE_READONLY);

  //ディレクトリにRO属性
  SetFileAttributes(szDir,FILE_ATTRIBUTE_READONLY);

cleanup:
  return hres;
}

一応、会社で使っているWindows XP SP3ではネットワークプレースをプログラムから作成できました。
が!、当然ながら、Windows7では、うまくいきません(笑)
Windows7/Vista から仕様が変わったようです。当たり前か(^^;;;

う~ん・・・なんかこう、すんなり旨い方法がないものだろうか・・・。

ネットワークドライブとWebDAV over SSL

Windows7だとWebDAVをネットワークドライブとして割り当てることができるので、ログオン時はいつも再接続させたい・・・のですが、クライアント認証を使ってアクセスする関係上、自動的に再接続できません。「ログオン時に再接続する」にチェックを入れているのですが、「再接続できませんでした」とかなんとかいうメッセージが出てエラーになってしまいます。

というわけで、今まではデスクトップに、以下のようなコマンドファイルを置いてその都度ダブルクリックすることで回避してました。

;---- allocate_network_drive.cmd ----
net use M: https://webdav_over_ssl/my_folder/
start M:

ただ、ダブルクリックすると、コマンドプロンプトのウィンドウが出たりして、なんかスマートじゃありません。。。

net use なんちゃら の部分を、自前でコーディングすれば事足ります。幸いにも、Windows API には WNetAddConnection2() という便利なAPIがあるので、このAPIをコールすれば一発でできそうです(^^)

早速、C++超手抜きネットワークドライブ・クラスを書いてみました。今年から積極的にC#を使おうとか、宣言しながら、早速C++使ってます。すみません(笑) 直接API叩くのがカンタンなので・・・。

#pragma comment(lib,"mpr.lib")
/**********************************************************
 簡易ネットワークドライブ・クラス 
 NetworkDrive.h
***********************************************************/
#include <windows.h>
#include <strsafe.h>
#include <tchar.h>

class CNetworkDrive
{
private:
  PTSTR m_strURI;
  PTSTR m_strDevice;

public:
  //コンストラクタ&デストラクタ
  CNetworkDrive(PCTSTR uri,PCTSTR device)
    {
      m_strURI    = CreateAndCopyString(uri);
      m_strDevice = CreateAndCopyString(device);
    }
  virtual ~CNetworkDrive()
    {
      DestroyString(m_strURI);
      DestroyString(m_strDevice);
    }

  DWORD Allocate(bool bRemember = false)
    {
      NETRESOURCE NetResource = {0};

      NetResource.dwType       = RESOURCETYPE_DISK;
      NetResource.lpLocalName  = m_strDevice;
      NetResource.lpRemoteName = m_strURI;
      
      return WNetAddConnection2(&NetResource,NULL,NULL,CONNECT_INTERACTIVE | (bRemember ? CONNECT_UPDATE_PROFILE : 0));
    }
  
  DWORD Cancel(bool bUpdate = false,bool bForce = true)
    {
      DWORD dwFlags = bUpdate ? CONNECT_UPDATE_PROFILE : 0;
      BOOL bwForce = bForce ? TRUE : FALSE;

      return WNetCancelConnection2(m_strDevice,dwFlags,bwForce);
    }

//雑関数
private:
  static PTSTR CreateAndCopyString(PCTSTR src)
    {
      DWORD dwNum = 0;
      PTSTR dest = NULL;
      
      if((dwNum = lstrlen(src)) > 0)
        {
          dest = new TCHAR[dwNum+1];
          StringCchCopy(dest,dwNum+1,src);
        }
      
      return dest;
    }

  static void DestroyString(PTSTR str)
    {
      if(str)
        delete [] str;
    }
  
};

これを、使って、以下のようなテストコードで試してみたところ、うまく行きました。

#pragma comment(lib,"user32.lib")

#include <windows.h>
#include <tchar.h>
#include "NetworkDrive.h"

#define URI TEXT("https://my_webdav_site/my_folder/")
#define DEVICE TEXT("Z:")

//スタートアップ
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
  return MessageBox(NULL,
                    (CNetworkDrive(URL,DRIVE).Allocate() == NO_ERROR) ? TEXT("接続しました。") : TEXT("失敗ました。"),
                    TEXT("メッセージ"),
                    MB_OK);
}

最初、エラーで接続できなくてググっても分からず、全く原因が分からくて途方に暮れてたのですが・・・結局ドキュメントを見落としてました。。。英語なんでサラッと流し読みしたのが悪かった(^_^;)

WNetAddConnection2 APIに渡す最後の引数のフラグに、CONNECT_INTERACTIVE を指定していなかったのが原因でした。これを指定することで、クライアント証明書選択ダイアログ(っていうのかな?)が出て正常に認証が済み、無事ネットワークドライブを割り当てることができました。

やっぱりドキュメントはちゃんと読まないといけませんねぇ・・・

今年はC#を本気で使っていこう

と、思ってます(^^;

本来は年始の挨拶ですが、喪中なので、「寒中お見舞い申し上げます」 です(m_m)

C#はちょこちょこと触ってつまみ食い程度に使っていたのですが、JScript/Perlなどの便利なLL言語の手軽さに負けてあまり本気で使っていなかったのですが、とある解説本を読んでC#に傾倒しつつあります。

C# 3.0 になって、これまでのC# 2.0 のときに感じた僕の不満点が一気に解決されていました。特にラムダ式とLINQは、非常に煩わしいコード記述を、まるでスクリプトを書いているような感覚で、うまく使えば非常に読みやすいコードの記述ができる武器になりそうです。 普通ならスクリプトで組むようなものでも、これからは気軽にC#を使っていこうかな、と思うようになってきました。

C#といえば、事実上、Windows の .NET Framework上でしか動作しない、という認識でしたが、オープンソースで開発されている Monoをインストールすれば、Windows以外のプラットフォームでも十分使えるようです。CentOSに mono をインストールして、Windows上でビルドしたプログラムが動くかテストしてみましたが、すんなり動くことにちょっと感動しました(笑) フォームを使ったものも含めて、完全に互換性が保てるようになれば、状況が変わると思いますねぇ。

いままでは、どんな小さなプログラムでも、使うか使わないかは別にしてWebサーバーでの使用も考えて、Perl や PHP を使ってコーディングすることがほとんどでしたが、Mono がもっとオープンソース系のサーバー系OSで普及してくれれば、今まで以上にC# を気軽に使えるんですがねぇ~。どうなんでしょうねぇ~。

よくC# と Java は設計思想が似てて、言語的にC#はJavaを真似してる、という意見もありますが、C# を知れば知るほど、Javaとは全く違う言語だということが分かります。

いろんな意見がありますが、僕的に思うことは、 趣味でプログラムするなら、JavaよりC#の方が断然面白い、です。

が、ちょっと、「それはやりすぎじゃね?」 と思うこともあります。
たとえば・・・

/* C# 2.0のときは、*/
List&lt;string&gt; a = new List&lt;string&gt;();

と記述しますが、C++のtemplateときもそうだったんですが・・・同じ語が繰り返し出てきたりして、ずーーーーーと前から不満でした。が、 C# 3.0になって var キーワードが使えるようになったおかげで、スマートに書くことができますよね。

/* C# 3.0 では・・・*/
var a = new List&lt;string&gt;();

これはいい。a の型をコンパイラが考えてくれる。

/* でもこれは・・・すでに何をやってるのか僕には見当もつきません(^^;;;*/
var ar = new[]{1,2,3};

/*  これは下と同じですが */
int[] ar = new int[]{1,2,3};

/* こっちの方がわかりやすいし何をやっているかも一目瞭然だと思うのですが・・・。 */
var ar = new int[]{1,2,3};

こんなやりすぎ感が漂う C# ですが、Perl ほどワケワカメな書き方はできないし、この、程々のやり過ぎ感がちょうどいいんじゃないかと思います(^^;;;

.NET Framework のライブラリが膨大すぎて、必要なクラスがどの名前空間にあるか、すでにライブラリ・クラスの海の中におぼれかけてますが、今年からC#を極力使っていきたいと思います。

でもWindowsでちょっと凝ったことしたいときは、やっぱり Windows APIを直接叩く必要があるのを、次のWindows8でどうにかしてほしいです。。。