PowerShellにWebDAVをマウントする

最近PowerShellばっかりやっている気がするけど、今回はPowerShellWebDAVサポートを追加した。これを使うとWebDAVサーバー上のリソースをローカルのリソースのように扱える。

ソースは以下から

マウントの仕方

PS > New-PSDrive Hoge Web http://localhost/webdav

認証が必要なWebDAVサーバーの場合

「-Login」パラメータでユーザ名を指定する。New-Driveコマンドレットには-Credentialというパラメータがあるけど、これを指定すると何故かエラーが出てしまう。現状ではまだサポートされていないのかな?
仕方がないので「-Login」という新しいパラメータを追加した。さらっと言ったけどNew-DriveやNew-ItemなどのProviderがサポートするコマンドでは、コマンドの実装を変えることなくパラメータを新しく追加することができるのだ!!

PS > New-PSDrive Hoge Web http://localhost/webdav -Login [ユーザ名]

この後、パスワードの入力を促すダイアログが表示される。

後はローカルのファイルシステムと同じように操作が出来る。今のところサポートしている機能は、

  • New-Item, ni, mkdir
  • Remove-Item, rm, rmdir, del
  • Copy-Item, cp, copy
  • Move-Item, mv, move
  • Rename-Item, ren
  • Get-Item
  • Get-Content
  • タブ補完
ファイルの作成

これで空のファイルが作られる。

PS Hoge:\> New-Item 100.txt

「-value」を指定するとその内容をファイルに追加する。

PS Hoge:\> New-Item 100.txt -value Hello

   PSParentPath: PSWeb\Web::http://localhost/webdav

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
a----        2008/02/26     15:19          5 100.txt

PS Hoge:\> more 100.txt
Hello

「-source」でローカルのファイルを指定すると、そのファイルの内容で新しくファイルを作る。

PS Hoge:\> New-Item 100.txt -source c:\Hoge\100.txt

   PSParentPath: PSWeb\Web::http://localhost/webdav

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
a----        2008/02/26     15:19          5 100.txt

他にもいろいろ。たいていのことはできる。ちなみにSubversionも見るだけなら対応している。

何種類かPowerShellのProviderを作ってわかったのは、Provider作りはだいぶクセがあること。このへんのノウハウ的なことはそのうちまとめないとな。問題は家のVAIOがぶっ壊れて、家でいっさいプログラムが書けないことか・・・

ちょっとPowerShellにも飽きてきたな。しばらく触らんとこ。

以下、全ソース

PSWebSnapIn.cs
using System;
using System.Management.Automation;
using System.ComponentModel;
using System.Diagnostics;

namespace PSWeb.Configuration {
    /// <summary>
    /// PowerShellにSnapInを追加するクラス
    /// </summary>
    [RunInstaller(true)]
    public class PSWebSnapIn : PSSnapIn {
        public PSWebSnapIn() { }

        public override string Description {
            get { return "PowerShellにWebDAVサポートを追加します。"; }
        }
        public override string Name {
            get { return "PSWeb"; }
        }
        public override string Vendor {
            get { return "coma2n"; }
        }
    }
}
PSWebResource.cs

using System;
using System.Diagnostics;

namespace PSWeb {
    /// <summary>
    /// WebDAVリソースの情報を格納するクラス
    /// </summary>
    [Serializable]
    [DebuggerStepThrough]
    public class PSWebResource {
        private string name = string.Empty;
        private long length;

        private DateTime creationDate;
        private DateTime lastWriteTime;

        private bool isCollection = false;

        /// <summary>
        /// 表示名を取得します。
        /// </summary>
        public string Name {
            get { return name; }
        }
        /// <summary>
        /// コンテンツのサイズを取得します。
        /// </summary>
        public long Length {
            get { return length; }
        }
        /// <summary>
        /// 作成日を取得します。
        /// </summary>
        public DateTime CreationDate {
            get { return creationDate; }
        }
        /// <summary>
        /// 最終更新日を取得します。
        /// </summary>
        public DateTime LastWriteTime {
            get { return lastWriteTime; }
        }
        /// <summary>
        /// コレクションかどうかを取得します。
        /// </summary>
        public bool IsCollection {
            get { return isCollection; }
        }

        public PSWebResource(string name, long length, DateTime creationDate, DateTime lastWriteTime) {
            this.name = name;
            this.length = length;
            this.creationDate = creationDate;
            this.lastWriteTime = lastWriteTime;
        }
        public PSWebResource(string name, long length, DateTime creationDate, DateTime lastWriteTime, bool isCollection)
            : this(name, length, creationDate, lastWriteTime) {
            this.isCollection = isCollection;
        }

        public override string ToString() {
            return string.Format("Name = {0}", Name);
        }
    }
}

PSWebException.cs
using System;
using System.Runtime.Serialization;
using System.Diagnostics;

namespace PSWeb {
    /// <summary>
    /// WebDAVアクセス時にエラーが発生した時に投げられる例外クラス
    /// </summary>
    [Serializable]
    [DebuggerStepThrough]
    public class PSWebException : ApplicationException {
        public PSWebException() {
        }
        public PSWebException(string message)
            : base(message) {
        }
        public PSWebException(string message, Exception innerException)
            : base(message, innerException) {
        }
        protected PSWebException(SerializationInfo info, StreamingContext context)
            : base(info, context) {
        }
    }
}
PSWebRequest.cs

using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Xml;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Diagnostics;

namespace PSWeb {
    /// <summary>
    /// WebDAVサーバーに対して各種リクエストを行うクラス
    /// </summary>
    public class PSWebRequest {
        private ICredentials credential;

        private bool useProxy = false;
        private string userAgent = "Mozilla/4.0";

        /// <summary>
        /// 認証オブジェクトを取得、設定します。
        /// </summary>
        public ICredentials Credential {
            get { return credential; }
            set { credential = value; }
        }
        /// <summary>
        /// プロキシを経由するかどうかを取得、設定します。
        /// </summary>
        public bool UseProxy {
            get { return useProxy; }
            set { useProxy = value; }
        }
        /// <summary>
        /// ユーザーエージェントを取得、設定します。(規定ではMozilla/4.0)
        /// </summary>
        public string UserAgent {
            get { return userAgent; }
            set { userAgent = value; }
        }

        public PSWebRequest() {
        }

        /// <summary>
        /// 指定したuriのリソースの情報を取得します(取得に失敗するとnull)。
        /// </summary>
        /// <param name="uri">uri</param>
        /// <returns>リソース情報</returns>
        public PSWebResource GetResource(string uri) {
            try {
                return new List<PSWebResource>(EnumResources(uri, true))[0];

            } catch {
                return null;
            }
        }
        /// <summary>
        /// 指定したuriにあるリソースの一覧を列挙します。
        /// </summary>
        /// <param name="uri">uri</param>
        /// <returns>リソースのコレクション</returns>
        /// <exception cref="PSWebException">WebDavアクセス中に問題が発生した時</exception>
        public IEnumerable<PSWebResource> EnumResources(string uri) {
            return EnumResources(uri, false);
        }
        private IEnumerable<PSWebResource> EnumResources(string uri, bool selfOnly) {
            // 全プロパティを取得
            StringBuilder buffer = new StringBuilder();
            buffer.Append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
            buffer.Append("<D:propfind xmlns:D=\"DAV:\">");
            buffer.Append("<D:allprop/>");
            buffer.Append("</D:propfind>");

            byte[] bytes = Encoding.UTF8.GetBytes(buffer.ToString());

            HttpWebRequest httpReq = CreateWebRequest(uri, credential);
            // 自分以外で1階層のリソースのみを対象にする。
            httpReq.Method = "PROPFIND";
            httpReq.Headers.Add("Depth", selfOnly ? "0" : "1");
            httpReq.ContentType = "text/xml; charset=\"utf-8\"";
            httpReq.ContentLength = bytes.Length;
            try {
                using(Stream request = httpReq.GetRequestStream()) {
                    request.Write(bytes, 0, bytes.Length);
                }
                HttpWebResponse httpRes = (HttpWebResponse)httpReq.GetResponse();
                try {
                    return EnumResourcesInternal(
                        Encoding.UTF8.GetString(ReadBytes(httpRes.GetResponseStream())), !selfOnly
                    );

                } finally {
                    httpRes.Close();
                }

            } catch(WebException exp) {
                throw new PSWebException("リソースの一覧取得に失敗しました", exp);
            }
        }
        private IEnumerable<PSWebResource> EnumResourcesInternal(string xmlString, bool skipRoot) {
            XmlDocument xmlDoc = new XmlDocument();
            XmlNamespaceManager nsmgr = new XmlNamespaceManager(xmlDoc.NameTable);
            nsmgr.AddNamespace("D", "DAV:");
            nsmgr.AddNamespace("lp3", "http://subversion.tigris.org/xmlns/dav/");

            xmlDoc.LoadXml(xmlString); Debug.WriteLine(xmlString);

            bool isRootResource = skipRoot;
            // 1つめのノードは、自分自身なので無視する。
            foreach(XmlNode node in xmlDoc.SelectNodes("//D:response", nsmgr)) {
                if(isRootResource) {
                    isRootResource = false; continue;
                }
                XmlNode mn = node.SelectSingleNode(".//D:displayname", nsmgr);

                yield return new PSWebResource(
                    GetDisplayName(node, nsmgr),
                    GetContentLength(node, nsmgr),
                    GetDate(node, ".//D:creationdate", nsmgr),
                    GetDate(node, ".//D:getlastmodified", nsmgr),
                    node.SelectSingleNode(".//D:resourcetype", nsmgr).ChildNodes.Count != 0
                );
            }
        }
        private static string GetDisplayName(XmlNode node, XmlNamespaceManager nsmgr) {
            string value = GetNodeString(node, ".//D:displayname", nsmgr);
            // Subversionの場合はこっち
            if(value.Length == 0) {
                value = GetNodeString(node, ".//lp3:baseline-relative-path", nsmgr);
                // 相対パスなので、分割してドンケツだけを返す。
                if(value.Length > 0) {
                    string[] values = value.Split('/');

                    return values[values.Length - 1];
                }
            }
            return value;
        }
        private static long GetContentLength(XmlNode node, XmlNamespaceManager nsmgr) {
            string value = GetNodeString(node, ".//D:getcontentlength", nsmgr);

            if(value.Length > 0) {
                long result;

                if(long.TryParse(value, out result)) return result;
            }
            return -1;
        }
        private static DateTime GetDate(XmlNode node, string xpath, XmlNamespaceManager nsmgr) {
            string value = GetNodeString(node, xpath, nsmgr);

            if(value.Length > 0) {
                DateTime result;

                if(DateTime.TryParse(value, out result)) return result;
            }
            return DateTime.MinValue;
        }
        private static string GetNodeString(XmlNode node, string xpath, XmlNamespaceManager nsmgr) {
            XmlNode match = node.SelectSingleNode(xpath, nsmgr);

            return match != null ? match.InnerText : string.Empty;
        }
        /// <summary>
        /// 指定したファイルを指定したuriのリソースとして作成します。
        /// </summary>
        /// <param name="fileName">ファイル名</param>
        /// <param name="uploadUri">アップロード先</param>
        /// <returns>作成したリソースの情報</returns>
        /// <exception cref="PSWebException">WebDavアクセス中に問題が発生した時</exception>
        public PSWebResource CreateResource(string fileName, string uploadUri) {
            byte[] bytes;

            using(FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read)) {
                bytes = new byte[fs.Length];
                fs.Read(bytes, 0, bytes.Length);
            }
            HttpWebRequest httpReq = CreateWebRequest(uploadUri, credential);
            httpReq.Method = "PUT";
            httpReq.ContentLength = bytes.Length;
            try {
                using(Stream request = httpReq.GetRequestStream()) {
                    request.Write(bytes, 0, bytes.Length);
                }
                using(HttpWebResponse httpRes = (HttpWebResponse)httpReq.GetResponse()) { return GetResource(uploadUri); }

            } catch(WebException exp) {
                throw new PSWebException("リソースの作成中に問題が発生しました。", exp);
            }
        }
        /// <summary>
        /// 指定したuriのリソースを削除します。
        /// </summary>
        /// <param name="uri">URI</param>
        /// <exception cref="PSWebException">WebDavアクセス中に問題が発生した時</exception>
        public void DeleteResource(string uri) {
            HttpWebRequest httpReq = CreateWebRequest(uri, credential);
            httpReq.Method = "DELETE";
            try {
                using(HttpWebResponse httpRes = (HttpWebResponse)httpReq.GetResponse()) { }

            } catch(WebException exp) {
                throw new PSWebException("リソースの削除中に問題が発生しました。", exp);
            }
        }
        /// <summary>
        /// 指定したuriのリソースを別のuriでコピーします。
        /// </summary>
        /// <param name="sourceUri">コピー元</param>
        /// <param name="destUri">コピー先</param>
        /// <returns>コピーしたリソースの情報</returns>
        /// <exception cref="PSWebException">WebDavアクセス中に問題が発生した時</exception>
        public PSWebResource CopyResource(string sourceUri, string destUri) {
            HttpWebRequest httpReq = CreateWebRequest(sourceUri, credential);

            httpReq.Method = "COPY";
            httpReq.Headers.Add("Destination", destUri);
            httpReq.Headers.Add("Depth", "0");
            httpReq.Headers.Add("Overwrite", "T");
            try {
                using(HttpWebResponse httpRes = (HttpWebResponse)httpReq.GetResponse()) { return GetResource(destUri); }

            } catch(WebException exp) {
                throw new PSWebException("リソースのコピー中に問題が発生しました。", exp);
            }
        }
        /// <summary>
        /// 指定したアドレスのリソースの名前を変更します。
        /// </summary>
        /// <param name="sourceUri">ソースURI</param>
        /// <param name="destUri">変更後URI</param>
        /// <returns>変更後のリソースの情報</returns>
        /// <exception cref="PSWebException">WebDavアクセス中に問題が発生した時</exception>
        public PSWebResource RenameResource(string sourceUri, string destUri) {
            HttpWebRequest httpReq = CreateWebRequest(sourceUri, credential);
            httpReq.Method = "MOVE";
            httpReq.Headers.Add("Destination", destUri);
            httpReq.Headers.Add("Overwrite", "T");
            try {
                using(HttpWebResponse httpRes = (HttpWebResponse)httpReq.GetResponse()) { return GetResource(destUri); }

            } catch(WebException exp) {
                throw new PSWebException("リソースの名前変更中に問題が発生しました。", exp);
            }
        }
        /// <summary>
        /// 指定したアドレスにコレクションを作成します。
        /// </summary>
        /// <param name="uri">アドレス</param>
        /// <returns>作成したコレクションの情報</returns>
        /// <exception cref="PSWebException">WebDavアクセス中に問題が発生した時</exception>
        public PSWebResource CreateCollection(string uri) {
            HttpWebRequest httpReq = CreateWebRequest(uri, credential);
            httpReq.Method = "MKCOL";
            try {
                using(HttpWebResponse httpRes = (HttpWebResponse)httpReq.GetResponse()) { return GetResource(uri); }

            } catch(WebException exp) {
                HttpWebResponse response = (HttpWebResponse)exp.Response;
                // 405エラーは、コレクションが既に存在する場合と解釈する。
                if(response != null && response.StatusCode == HttpStatusCode.MethodNotAllowed) return GetResource(uri);

                throw new PSWebException("コレクションの作成中に問題が発生しました。", exp);
            }
        }
        /// <summary>
        /// 指定したアドレスのリソースを指定したファイル名でダウンロードします。
        /// </summary>
        /// <param name="uri">アドレス</param>
        /// <param name="fileName">ローカルのファイル名</param>
        /// <exception cref="PSWebException">WebDavアクセス中に問題が発生した時</exception>
        public void DownloadResource(string uri, string fileName) {
            HttpWebRequest httpReq = CreateWebRequest(uri, credential);
            httpReq.Method = "GET";
            try {
                using(HttpWebResponse httpRes = (HttpWebResponse)httpReq.GetResponse()) {
                    byte[] buffer = ReadBytes(httpRes.GetResponseStream());

                    using(FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write)) {
                        fs.Write(buffer, 0, buffer.Length);
                    }
                }

            } catch(WebException exp) {
                throw new PSWebException("リソースの取得中に問題が発生しました。", exp);
            }
        }
        /// <summary>
        /// 指定したリソースが存在するかどうかを調べます。
        /// </summary>
        /// <param name="uri">アドレス</param>
        /// <returns>存在すればtrue、そうでなければfalse</returns>
        public bool ExistResource(string uri) {
            HttpWebRequest httpReq = CreateWebRequest(uri, credential);
            httpReq.Method = "HEAD";
            try {
                using(HttpWebResponse httpRes = (HttpWebResponse)httpReq.GetResponse()) { }

                return true;

            } catch(WebException exp) {
                // NotFoundでなければOKじゃね?
                HttpWebResponse response = (HttpWebResponse)exp.Response;
                if(response != null && response.StatusCode != HttpStatusCode.NotFound) return true;

                return false;
            }
        }
        /// <summary>
        /// 指定した(シーク不可の)ストリームからbyte配列に読み込みます。
        /// </summary>
        /// <param name="inStream">ストリーム</param>
        /// <returns>byte配列</returns>
        private byte[] ReadBytes(Stream inStream) {
            List<byte> buffer = new List<byte>();

            int bt;

            while((bt = inStream.ReadByte()) != -1) {
                buffer.Add((byte)bt);
            }
            inStream.Close();

            return buffer.ToArray();
        }
        /// <summary>
        /// 指定したURIと認証オブジェクトがWebリクエストを作成します。
        /// </summary>
        /// <param name="uri">URI</param>
        /// <param name="credential">認証オブジェクト</param>
        /// <returns>Webリクエスト</returns>
        private HttpWebRequest CreateWebRequest(string uri, ICredentials credential) {
            HttpWebRequest httpReq = (HttpWebRequest)HttpWebRequest.Create(uri);
            httpReq.UserAgent = UserAgent;

            // プロキシを経由すると更新系の処理でエラーが出る。
            if(!useProxy) httpReq.Proxy = null;

            if(credential != null) {
                httpReq.PreAuthenticate = true;     // 事前認証あり
                httpReq.Credentials = credential;
            }
            InitCertificateValidation();

            return httpReq;
        }
        /// <summary>
        /// 証明書の承認処理を初期化します。
        /// </summary>
        private void InitCertificateValidation() {
            // SSL証明書をオールOKにする。(セキュリティがやばい?)
            ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(
                delegate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
                    return true;
                }
            );
        }
    }
}

PSWebProvider.cs

using System;
using System.IO;
using System.Text;
using System.Security;
using System.Management.Automation;
using System.Management.Automation.Provider;
using System.Collections.ObjectModel;
using System.Diagnostics;

namespace PSWeb {
    /// <summary>
    /// WebDAVプロトコルへの操作を提供するプロバイダクラス
    /// </summary>
    [CmdletProvider("Web", ProviderCapabilities.ShouldProcess)]
    public class PSWebProvider : NavigationCmdletProvider, IContentCmdletProvider {
        /// <summary>
        /// パスの区切り文字
        /// </summary>
        private const string PATH_SEPARATOR = "/";

        /// <summary>
        /// New-Driveコマンドレットのパラメータを定義するクラス
        /// </summary>
        [Serializable]
        [DebuggerStepThrough]
        public class NewDriveParameters {
            private SwitchParameter useProxy;
            private PSCredential credential;

            /// <summary>
            /// プロキシを使用するかどうかを取得、設定します。
            /// </summary>
            [Parameter]
            public SwitchParameter UseProxy {
                get { return useProxy; }
                set { useProxy = value; }
            }
            /// <summary>
            /// 認証情報を取得、設定します。
            /// </summary>
            [Parameter, Credential]
            public PSCredential Login {
                get { return credential; }
                set { credential = value; }
            }
        }

        /// <summary>
        /// New-Itemコマンドレットのパラメータを定義するクラス
        /// </summary>
        [Serializable]
        [DebuggerStepThrough]
        public class NewItemParameters {
            private string source;

            /// <summary>
            /// 作成元のファイルを取得、設定します。
            /// </summary>
            [Parameter]
            public string Source {
                get { return source; }
                set { source = value; }
            }
        }

        /// <summary>
        /// Webドライブの情報を格納するクラス
        /// </summary>
        private class PSWebDriveInfo : PSDriveInfo {
            private SwitchParameter useProxy;

            /// <summary>
            /// プロキシを使用するかどうかを取得します。
            /// </summary>
            public SwitchParameter UseProxy {
                get { return useProxy; }
            }

            /// <summary>
            /// 元になるドライブ情報とNew-Driveコマンドのパラメータ情報を設定するコンストラクタ
            /// </summary>
            /// <param name="drive">ドライブ情報</param>
            /// <param name="ndParams">パラメータ情報</param>
            public PSWebDriveInfo(PSDriveInfo drive, NewDriveParameters ndParams)
                : base(drive.Name, drive.Provider, drive.Root, drive.Description, ndParams.Login) {
                this.useProxy = ndParams.UseProxy;
            }
        }

        public PSWebProvider() {
        }

        /// <summary>
        /// Webリクエストを取得します。
        /// </summary>
        /// <returns>Webリクエスト</returns>
        private PSWebRequest GetWebRequest() {
            PSWebDriveInfo drive = (PSWebDriveInfo)base.PSDriveInfo;

            PSWebRequest webReq = new PSWebRequest();
            if(drive != null) {
                if(drive.Credential.UserName != null) {
                    webReq.Credential = drive.Credential.GetNetworkCredential();
                }
                webReq.UseProxy = drive.UseProxy;

            }
            return webReq;
        }
        /// <summary>
        /// 指定したパスのアイテムの一覧をパイプラインに出力します。
        /// </summary>
        /// <param name="path">パス</param>
        /// <param name="recurse">サブフォルダも再帰的に走査するかどうか</param>
        /// <param name="nameOnly">名前だけ出力するかどうか</param>
        private void GetPathItems(string path, bool recurse, bool nameOnly) {
            foreach(PSWebResource o in GetWebRequest().EnumResources(path)) {
                if(base.Stopping) return;

                WriteItemObject(
                    (nameOnly ? (object)o.Name : WrapPSObject(o, path)), Combine(path, o.Name), o.IsCollection
                );
                if(o.IsCollection && recurse) {
                    GetPathItems(Combine(path, o.Name), true, nameOnly);
                }
            }
        }
        /// <summary>
        /// 指定したリソースオブジェクトをパイプラインに書き込みます。
        /// </summary>
        /// <param name="webRes">オブジェクト</param>
        /// <param name="path">パス</param>
        private void WritePSObject(PSWebResource webRes, string path) {
            WriteItemObject(
                WrapPSObject(
                    webRes, GetParentPath(path, base.PSDriveInfo.Root)
                ),
                path, webRes.IsCollection
            );
        }
        /// <summary>
        /// 指定したオブジェクトをPSObjectにラップして返します。
        /// </summary>
        /// <param name="obj">オブジェクト</param>
        /// <param name="parentPath">親のパス</param>
        /// <returns>ラップしたオブジェクト</returns>
        private static PSObject WrapPSObject(object obj, string parentPath) {
            PSObject psObj = PSObject.AsPSObject(obj);
            psObj.Properties.Add(
                new PSNoteProperty("PSParentPath", parentPath)
            );
            return psObj;
        }
        /// <summary>
        /// 指定した二つのパスを連結します。
        /// </summary>
        /// <param name="path1">パス1</param>
        /// <param name="path2">パス2</param>
        /// <returns>「/」で連結したパス</returns>
        private static string Combine(string path1, string path2) {
            if(path1.Length > 0) {
                return string.Format(
                    "{0}{2}{1}", path1, path2, (path1.EndsWith(PATH_SEPARATOR) ? string.Empty : PATH_SEPARATOR)
                );

            } else {
                return path2;
            }
        }

        protected override PSDriveInfo NewDrive(PSDriveInfo drive) {
            NewDriveParameters ndParams = (NewDriveParameters)base.DynamicParameters;

            return new PSWebDriveInfo(drive, ndParams);
        }
        protected override object NewDriveDynamicParameters() {
            return new NewDriveParameters();
        }
        protected override void NewItem(string path, string itemTypeName, object newItemValue) {
            if(!string.IsNullOrEmpty(itemTypeName) &&
                itemTypeName.Equals("Directory", StringComparison.CurrentCultureIgnoreCase)) {

                WritePSObject(
                    GetWebRequest().CreateCollection(path), path
                );

            } else {
                NewItemParameters niParams = (NewItemParameters)base.DynamicParameters;
                // パラメータで作成元を指定された場合はそれを使うが、
                // 指定されなければ空の一時ファイルを作成して、それをアップロードする。
                string sourceFile = string.IsNullOrEmpty(niParams.Source) ?
                    Path.GetTempFileName() :
                    Path.GetFullPath(niParams.Source);
                // 値が指定されている場合はファイルに追加書き込みする。
                if(newItemValue != null) {
                    using(StreamWriter sw = new StreamWriter(sourceFile, true)) {
                        sw.Write(newItemValue.ToString());
                    }
                }
                WritePSObject(
                    GetWebRequest().CreateResource(sourceFile, path), path
                );
            }
        }
        protected override object NewItemDynamicParameters(string path, string itemTypeName, object newItemValue) {
            return new NewItemParameters();
        }
        protected override void CopyItem(string path, string copyPath, bool recurse) {
            WritePSObject(
                GetWebRequest().CopyResource(path, copyPath), copyPath
            );
        }
        protected override void RemoveItem(string path, bool recurse) {
            GetWebRequest().DeleteResource(path);
        }
        protected override void RenameItem(string path, string newName) {
            string destination = Combine(
                GetParentPath(path, base.PSDriveInfo.Root), newName
            );
            WritePSObject(
                GetWebRequest().RenameResource(path, destination), destination
            );
        }
        protected override void MoveItem(string path, string destination) {
            PSWebResource mvRes = GetWebRequest().RenameResource(path, destination);

            WritePSObject(
                GetWebRequest().RenameResource(path, destination), destination
            );
        }
        protected override void GetItem(string path) {
            WritePSObject(
                GetWebRequest().GetResource(path), path
            );
        }
        protected override string GetParentPath(string path, string root) {
            return base.GetParentPath(path, root).Replace("\\", PATH_SEPARATOR);
        }
        protected override void GetChildItems(string path, bool recurse) {
            GetPathItems(path, recurse, false);
        }
        protected override void GetChildNames(string path, ReturnContainers returnContainers) {
            GetPathItems(path, false, true);
        }
        protected override bool IsItemContainer(string path) {
            // 指定したパスがコンテナ(cdできる)かどうか
            return true;
        }
        protected override bool HasChildItems(string path) {
            // そんなの関係ねぇ!!
            return false;
        }
        protected override bool ItemExists(string path) {
            return GetWebRequest().ExistResource(path);
        }
        protected override string MakePath(string parent, string child) {
            if(child.Length == 0) return parent;
            if(parent.Length == 0) return child;

            return Combine(parent, child);
        }
        protected override string NormalizeRelativePath(string path, string basePath) {
            return base.NormalizeRelativePath(path, basePath).Replace(PATH_SEPARATOR, "\\");
        }
        protected override bool IsValidPath(string path) {
            // 指定したパスが有効かどうか
            return true;
        }

        public void ClearContent(string path) {
        }
        public object ClearContentDynamicParameters(string path) {
            return null;
        }
        public IContentReader GetContentReader(string path) {
            return new PSWebContentReader(
                GetWebRequest().GetResourceStream(path), Encoding.Default
            );
        }
        public object GetContentReaderDynamicParameters(string path) {
            return null;
        }
        public IContentWriter GetContentWriter(string path) {
            return null;
        }
        public object GetContentWriterDynamicParameters(string path) {
            return null;
        }
    }
}

PSWebContentReader.cs

using System;
using System.IO;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Management.Automation.Provider;
using System.Diagnostics;

namespace PSWeb {
    /// <summary>
    /// WebDAVリソースのコンテンツを読み込むクラス
    /// </summary>
    public class PSWebContentReader : IContentReader {
        private Stream stream;
        private StreamReader reader;

        /// <summary>
        /// 読み取るストリームを設定するコンストラクタ
        /// </summary>
        /// <param name="stream">ストリーム</param>
        /// <param name="encoding">エンコード</param>
        public PSWebContentReader(Stream stream, Encoding encoding) {
            this.stream = stream;
            this.reader = new StreamReader(stream, encoding);
        }

        public void Close() {
            reader.Close();
            stream.Close();
        }
        public IList Read(long readCount) {
            List<string> lines = new List<string>();

            for(int i = 0; i < readCount; i++) {
                string line = reader.ReadLine();

                if(line == null) break;

                lines.Add(line);
            }
            return lines.Count > 0 ? lines : null;
        }
        public void Seek(long offset, SeekOrigin origin) {
            stream.Seek(offset, origin);
        }

        public void Dispose() {
            if(reader != null) {
                reader.Dispose();
                reader = null;
            }
            if(stream != null) {
                stream.Dispose();
                stream = null;
            }
        }
    }
}

psweb.format.ps1xml

<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
    <ViewDefinitions>
        <View>
            <Name>PSWeb.PSWebResource</Name>
            <ViewSelectedBy>
                <TypeName>PSWeb.PSWebResource</TypeName>
            </ViewSelectedBy>
            <GroupBy>
                <PropertyName>PSParentPath</PropertyName> 
            </GroupBy>
            
            <TableControl>
                <TableHeaders>
                    <TableColumnHeader>
                        <Label>Mode</Label>
                        <Width>7</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>LastWriteTime</Label>
                        <Alignment>Right</Alignment>
                        <Width>25</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>Length</Label>
                        <Alignment>Right</Alignment>
                        <Width>10</Width>
                    </TableColumnHeader>
                    <TableColumnHeader />
                </TableHeaders>
                
                <TableRowEntries>
                    <TableRowEntry>
                        <TableColumnItems>
                            <TableColumnItem>
                                <ScriptBlock>
                                    $catr = "";
                                    if($_.IsCollection) { $catr += "d-" } else { $catr += "a-" }
                                    $catr += "---"
                                    $catr
                                </ScriptBlock>
                            </TableColumnItem>
                            <TableColumnItem>
                                <ScriptBlock>
                                    [String]::Format("{0,10}  {1,8}", $_.LastWriteTime.ToString("d"), $_.LastWriteTime.ToString("t"))
                                </ScriptBlock>
                            </TableColumnItem>
                            <TableColumnItem>
                                <ScriptBlock>if(!$_.IsCollection) { $_.Length }</ScriptBlock>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Name</PropertyName>
                            </TableColumnItem>
                        </TableColumnItems>
                    </TableRowEntry>
                 </TableRowEntries>
            </TableControl>
        </View>
    </ViewDefinitions>
    
</Configuration>