概述

我有一个用 VBA(Visual Basic for Applications)编写的 Microsoft Word 加载项,它将文档及其所有相关内容(嵌入媒体)压缩到 zip 存档中。创建 zip 存档后,它将文件转换为字节数组并将其发布到 ASMX Web 服务。这大多有效。

问题

我遇到的主要问题是将大文件传输到网站。我可以成功上传大约 40MB 的文件,但无法上传 140MB 的文件(超时/一般失败)。

第二个问题是,如果 zip 存档太大,则在 VBScript Word 插件中构建字节数组可能会因客户端计算机内存不足而失败。

潜在的解决方案

我正在考虑以下选项,并寻求有关任一选项或任何其他建议的反馈。

选项一

在客户端 (MS Word VBA) 上打开一个文件流,一次读取一个“块”,然后传输到 ASMX Web 服务,该服务将这些“块”组装成服务器上的一个文件。

这样做的好处是不向应用程序添加任何额外的依赖项或组件,我只会修改现有的功能。(依赖项越少越好,因为此解决方案应该适用于各种服务器环境并且相对容易设置。)

问题:

  • 是否有执行此操作的示例或任何推荐的技术(在 VBA 中的客户端上或在 C#/VB.NET 中的 Web 服务中)?

选项二

我了解 WCF 可能会通过“分块”或流数据传输大文件的问题提供解决方案。但是,我对 WCF 不是很熟悉,并且不确定它到底有什么功能,或者我是否可以从 VBA 与 WCF 服务进行通信。这样做的缺点是需要添加另一个依赖项(.NET 3.0)。但如果使用 WCF 绝对是一个更好的解决方案,我可能不介意采用这种依赖关系。

问题:

  • WCF 是否可靠地支持这种性质的大文件传输?如果是这样,这涉及什么?有资源或例子吗?
  • 您可以从 VBA 调用 WCF 服务吗?有什么例子吗?
有帮助吗?

解决方案

我最终实现了原始问题中引用的选项一。

我在 VBA 中对文件进行“分块”,并将每个“分块”传输到 Web 服务。我将解决方案的 VBA 部分基于此处找到的代码: 按块复制大文件并带有进度通知. 。但是,我没有将其复制到文件系统,而是将其发送到服务器。

代码:VBA土地

下面是创建文件块的(丑陋的)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 服务方法接受一些重要的参数。

  1. 字符串文件名:文件的名称(每个块都有相同的文件名)
  2. 字节[] zipBytes:每个部分的内容
  3. 整数索引:该索引(与文件名一起使用以在文件系统上提供独特的有序部分文件)
  4. bool isLastChunk:这是“我已经完成了 - 继续合并所有“块”,然后在自己之后清理”旗帜。

int index 和 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 文件“块”的 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 的更多信息,请查看 Bustamante 的有关 WCF 的书。

至于VBA调用,我不是该领域的专家,因此我没有任何相关信息。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top