MS Wordアドイン(VBA)からWebサーバーに大きなファイルを転送する方法は?
-
03-07-2019 - |
質問
概要
VBA(Visual Basic for Applications)で記述されたMicrosoft Wordアドインを使用して、ドキュメントとその関連コンテンツ(埋め込みメディア)をすべてzipアーカイブに圧縮します。 zipアーカイブを作成した後、ファイルをバイト配列に変換し、ASMX Webサービスにポストします。これはほとんど動作します。
問題
主な問題は、大きなファイルをWebサイトに転送することです。約40MBのファイルを正常にアップロードできますが、140MBのファイルはアップロードできません(タイムアウト/一般的なエラー)。
2番目の問題は、zipアーカイブが大きすぎる場合、クライアントマシンのメモリが不足してVBScript Wordアドインでバイト配列を構築できなくなることです。
潜在的なソリューション
次のオプションを検討しており、いずれかのオプションまたはその他の提案に関するフィードバックを探しています。
オプション1
クライアント(MS Word VBA)でファイルストリームを開き、1つの「チャンク」を読み取ります。一度に、「チャンク」を組み立てるASMX Webサービスに送信します。サーバー上のファイルに。
これには、アプリケーションに追加の依存関係やコンポーネントを追加しないという利点があります。既存の機能を変更するだけです。 (このソリューションはさまざまなサーバー環境で機能し、比較的簡単にセットアップできるため、依存関係は少ないほうが良いです。)
質問:
- これを行う例や推奨される手法はありますか(VBAのクライアントまたはC#/ VB.NETのWebサービスのいずれか)
オプション2
WCFは、「チャンク」によって大きなファイルを転送する問題の解決策を提供する可能性があることを理解しています。またはストリーミングデータ。しかし、私はWCFにあまり詳しくないので、WCFの機能や、VBAからWCFサービスと通信できるかどうかはわかりません。これには、別の依存関係(.NET 3.0)を追加するという欠点があります。しかし、WCFを使用することが間違いなくより良いソリューションである場合、その依存関係を取ることを気にしないかもしれません。
質問:
- WCFはこの種の大きなファイル転送を確実にサポートしていますか?もしそうなら、これには何が含まれますか?リソースや例はありますか?
- VBAからWCFサービスを呼び出すことはできますか?例はありますか?
解決
最終的に、元の質問で参照されたオプション1を実装しました。
私は「チャンク」 VBAでファイルを作成し、各「チャンク」を転送します。 Webサービスへ。ソリューションのVBA部分は、次のコードに基づいています。チャンクで進行状況を確認して大きなファイルをコピーする通知。ただし、ファイルシステムにコピーする代わりに、サーバーに送信します。
コード:VBA Land
ファイルチャンクを作成する(不気味な)VBAコードを次に示します。
Function CopyFileByChunk(fileName As String, sSource As String) As Boolean
Dim FileSize As Long, OddSize As Long, SoFar As Long
Dim Buffer() As Byte, f1 As Integer, ChunkSize As Long
On Error GoTo CopyFileByChunk_Error
f1 = FreeFile: Open sSource For Binary Access Read As #f1
FileSize = LOF(f1)
If FileSize = 0 Then GoTo Exit_CopyFileByChunk ' -- done!
ChunkSize = 5505024 '5.25MB
OddSize = FileSize Mod ChunkSize
Dim index As Integer
index = 0
If OddSize Then
ReDim Buffer(1 To OddSize)
Get #f1, , Buffer
index = index + 1
SoFar = OddSize
If UploadFileViaWebService(Buffer, fileName, index, SoFar = FileSize) Then
g_frmProgress.lblProgress = "Percent uploaded: " & Format(SoFar / FileSize, "0.0%")
Debug.Print SoFar, Format(SoFar / FileSize, "0.0%")
DoEvents
Else
GoTo CopyFileByChunk_Error
End If
End If
If ChunkSize Then
ReDim Buffer(1 To ChunkSize)
Do While SoFar < FileSize
Get #f1, , Buffer
index = index + 1
SoFar = SoFar + ChunkSize
If UploadFileViaWebService(Buffer, fileName, index, SoFar = FileSize) Then
g_frmProgress.lblProgress = "Percent uploaded: " & Format(SoFar / FileSize, "0.0%")
Debug.Print SoFar, Format(SoFar / FileSize, "0.0%")
DoEvents
Else
GoTo CopyFileByChunk_Error
End If
Loop
End If
CopyFileByChunk = True
Exit_CopyFileByChunk:
Close #f1
Exit Function
CopyFileByChunk_Error:
CopyFileByChunk = False
Resume Exit_CopyFileByChunk
End Function
これは、チャンクをサーバーにアップロードする参照VBAメソッドです。
Public Function UploadFileViaWebService(dataChunk() As Byte, fileName As String, index As Integer, lastChunk As Boolean) As Boolean
On Error GoTo ErrHand
Dim blnResult As Boolean
blnResult = False
'mdlConvert.SetProgressInfo "Connecting to the web server:" & vbNewLine & _
DQUOT & server_title() & DQUOT
If InternetAttemptConnect(0) = 0 Then
On Error Resume Next
Dim strSoapAction As String
Dim strXml As String
strXml = "<?xml version=""1.0"" encoding=""utf-8""?>" & _
"<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" & _
"<soap:Body>" & _
"<UploadZipFile xmlns=""http://something.com/"">" & _
"<zipBytes></zipBytes>" & _
"<index>" & index & "</index>" & _
"<isLastChunk>" & IIf(lastChunk, 1, 0) & "</isLastChunk>" & _
"</UploadZipFile>" & _
"</soap:Body>" & _
"</soap:Envelope>"
Dim objXmlhttp As Object
Dim objDom As Object
Set objXmlhttp = New MSXML2.xmlhttp
' Load XML
Set objDom = CreateObject("MSXML2.DOMDocument")
objDom.LoadXML strXml
'insert data chunk into XML doc
objDom.SelectSingleNode("//zipBytes").dataType = "bin.base64"
objDom.SelectSingleNode("//zipBytes").nodeTypedValue = dataChunk
' Open the webservice
objXmlhttp.Open "POST", webServiceUrl, False
' Create headings
strSoapAction = "http://something.com/UploadZipFile"
objXmlhttp.setRequestHeader "Content-Type", "text/xml; charset=utf-8"
objXmlhttp.setRequestHeader "SOAPAction", strSoapAction
' Send XML command
objXmlhttp.send objDom.XML
' Get all response text from webservice
Dim strRet
strRet = objXmlhttp.responseText
' Close object
Set objXmlhttp = Nothing
Set objDom = Nothing
'get the error if any
Set objDom = CreateObject("MSXML2.DOMDocument")
objDom.LoadXML strRet
Dim isSoapResponse As Boolean
isSoapResponse = Not (objDom.SelectSingleNode("//soap:Envelope") Is Nothing)
Dim error As String
If Not isSoapResponse Then
error = "Woops"
Else
error = objDom.SelectSingleNode("//soap:Envelope/soap:Body/soap:Fault/faultstring").text
End If
If error <> "" Then
ShowServerError error, True
blnResult = False
Else
Err.Clear 'clear the error caused in the XPath query above
blnResult = True
End If
'close dom object
Set objDom = Nothing
Else
GetErrorInfo "UploadFileViaWebService:InternetCheckConnection"
End If
ErrHand:
If Err.Number <> 0 Then
ShowError Err, "UploadFileViaWebService"
blnResult = False
End If
UploadFileViaWebService = blnResult
End Function
コード:C#ASMX Webサービス
サーバー側では、Webサービスメソッドがいくつかの重要なパラメーターを受け入れます。
- string fileName:の名前 ファイル(各チャンクには同じファイルがあります 名前)
- byte [] zipBytes:コンテンツ 各チャンクの
- int index:インデックス (fileNameと組み合わせて使用 一意の順序付き部分を提供する ファイルシステム上のファイル)
- bool isLastChunk:これで完了です。 先に進み、すべての「チャンク」をマージします。 自分の後片付け&quot;フラグ。
intインデックスとbool isLastChunk。 VBAの世界から提供されるこのコンテキストでは、これらの各ファイルチャンクを保存し、isLastChunkフラグがtrueのときにそれらを結合するのに十分な知識があります。
/// <summary>
/// Accepts a chunk of a zip file. Once all chunks have been received, combines the chunks into a zip file that is processed.
/// </summary>
/// <param name="fileName">Name of the file.</param>
/// <param name="zipBytes">The collection of bytes in this chunk.</param>
/// <param name="index">The index of this chunk.</param>
/// <param name="isLastChunk">if set to <c>true</c> this is the last chunk.</param>
/// <returns>Whether the file was successfully read and parsed</returns>
/// <exception cref="ParserException">An error occurred while trying to upload your file. The details have been written to the system log.</exception>
[WebMethod]
public bool UploadZipFile(string fileName, byte[] zipBytes, int index, bool isLastChunk)
{
try
{
const string ModuleRootUrl = "/Somewhere/";
string folderName = HostingEnvironment.MapPath("~" + ModuleRootUrl);
string fullDirectoryName = Path.Combine(folderName, Path.GetFileNameWithoutExtension(fileName));
try
{
if (!Directory.Exists(fullDirectoryName))
{
Directory.CreateDirectory(fullDirectoryName);
}
string pathAndFileName = Path.Combine(fullDirectoryName, AddIndexToFileName(fileName, index));
using (var stream = new MemoryStream(zipBytes))
{
WriteStreamToFile(stream, pathAndFileName);
}
if (isLastChunk)
{
try
{
MergeFiles(fullDirectoryName, fileName, index);
// file transfer is done.
// extract the zip file
// and do whatever you need to do with its contents
// we'll assume that it works - but your "parsing" should return true or false
return true;
}
finally
{
DeleteDirectoryAndAllContents(fullDirectoryName);
}
}
}
catch
{
DeleteDirectoryAndAllContents(fullDirectoryName);
throw;
}
}
return false;
}
これは、各着信チャンクをファイルシステムに書き込むC#コードです。
/// <summary>
/// Writes the contents of the given <paramref name="stream"/> into a file at <paramref name="newFilePath"/>.
/// </summary>
/// <param name="stream">The stream to write to the given file</param>
/// <param name="newFilePath">The full path to the new file which should contain the contents of the <paramref name="stream"/></param>
public static void WriteStreamToFile(Stream stream, string newFilePath)
{
using (FileStream fs = File.OpenWrite(newFilePath))
{
const int BlockSize = 1024;
var buffer = new byte[BlockSize];
int numBytes;
while ((numBytes = stream.Read(buffer, 0, BlockSize)) > 0)
{
fs.Write(buffer, 0, numBytes);
}
}
}
すべてのzipファイル&quot; chunks&quot;をマージするC#コードを次に示します。
/// <summary>
/// Merges each file chunk into one complete zip archive.
/// </summary>
/// <param name="directoryPath">The full path to the directory.</param>
/// <param name="fileName">Name of the file.</param>
/// <param name="finalChunkIndex">The index of the last file chunk.</param>
private static void MergeFiles(string directoryPath, string fileName, int finalChunkIndex)
{
var fullNewFilePath = Path.Combine(directoryPath, fileName);
using (var newFileStream = File.Create(fullNewFilePath))
{
for (int i = 1; i <= finalChunkIndex; i++)
{
using (var chunkFileStream = new FileStream(AddIndexToFileName(fullNewFilePath, i), FileMode.Open))
{
var buffer = new byte[chunkFileStream.Length];
chunkFileStream.Read(buffer, 0, (int)chunkFileStream.Length);
newFileStream.Write(buffer, 0, (int)chunkFileStream.Length);
}
}
}
}
他のヒント
MTOMエンコーディングを使用して、このような大きなファイルを送信しました。
MTOMの詳細はこちら: http://msdn.microsoft.com /en-us/library/aa395209.aspx
ここからMTOMサンプルをダウンロードできます。 http://msdn.microsoft .com / en-us / library / ms751514.aspx
MTOMについてさらに知りたい場合は、WCFに関するBustamanteの本をご覧ください。
VBAコールに関しては、私はその分野の専門家ではないため、それに関する情報はありません。