Как перенести большой файл из надстройки MS Word (VBA) на веб-сервер?

Обзор

У меня есть надстройка Microsoft Word, написанная на VBA (Visual Basic для приложений), которая сжимает документ и все связанное с ним содержимое (встроенный носитель) в zip-архив. После создания zip-архива он затем превращает файл в массив байтов и отправляет его в веб-службу ASMX. Это в основном работает.

Проблемы

Основная проблема, с которой я столкнулся, — это передача больших файлов на веб-сайт. Я могу успешно загрузить файл размером около 40 МБ, но не 140 МБ (время ожидания/общий сбой).

Второстепенная проблема заключается в том, что построение массива байтов в надстройке VBScript Word может завершиться ошибкой из-за нехватки памяти на клиентском компьютере, если ZIP-архив слишком велик.

Возможные решения

Я рассматриваю следующие варианты и ищу отзывы о любом варианте или любые другие предложения.

Вариант первый

Открытие файлового потока на клиенте (MS Word VBA) и чтение одного «фрагмента» за раз и передача в веб-службу ASMX, которая собирает «фрагменты» в файл на сервере.

Преимущество этого заключается в том, что в приложение не добавляются какие-либо дополнительные зависимости или компоненты, я бы только изменил существующую функциональность. (Чем меньше зависимостей, тем лучше, поскольку это решение должно работать в различных серверных средах и его относительно легко настроить.)

Вопрос:

  • Существуют ли примеры выполнения этого или какие-либо рекомендуемые методы (либо на клиенте в VBA, либо в веб-службе на C#/VB.NET)?

Вариант второй

Я понимаю, что WCF может предоставить решение проблемы передачи больших файлов путем «объединения» или потоковой передачи данных. Однако я не очень хорошо знаком с WCF и не уверен, на что именно он способен и могу ли я общаться со службой WCF из VBA. Недостатком этого является добавление еще одной зависимости (.NET 3.0). Но если использование WCF определенно является лучшим решением, я могу не возражать против этой зависимости.

Вопросы:

  • Поддерживает ли WCF надежную передачу больших файлов такого рода? Если да, то с чем это связано? Любые ресурсы или примеры?
  • Вы можете вызвать службу WCF из VBA? Есть примеры?

person Ian Robinson    schedule 29.04.2009    source источник
comment
К сожалению, это не идеальное решение, так как оно требует дополнительной настройки и настройки на клиентском сервере, что усложняет работу. Я думаю, что второй вариант (выше) отсутствует, и в настоящее время я иду по пути первого варианта. Если не получится, вероятно, придется прибегнуть к FTP.   -  person Ian Robinson    schedule 30.04.2009
comment
Я реализовал вариант номер один. Смотрите мой ответ ниже.   -  person Ian Robinson    schedule 06.12.2009


Ответы (2)


В итоге я реализовал вариант один, указанный в исходном вопросе.

Я «разбиваю» файл на VBA и передаю каждый «кусок» в веб-службу. Я основал часть решения 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

Теперь на стороне сервера метод веб-службы принимает несколько важных параметров.

  1. строка fileName: имя файла (каждый фрагмент имеет одно и то же имя файла)
  2. byte[] zipBytes: содержимое каждого чанка
  3. int index: индекс (используется вместе с fileName для предоставления уникальных упорядоченных частичных файлов в файловой системе)
  4. bool isLastChunk: это флаг «я закончил, продолжайте объединять все «фрагменты» и убирать за собой».

int index и bool isLastChunk. С этим контекстом, предоставленным из мира VBA, я знаю достаточно, чтобы сохранить каждый из этих фрагментов файла, а затем объединить их, когда флаг isLastChunk установлен.

   /// <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-файла:

/// <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);
            }
        }
    }
}
person Ian Robinson    schedule 05.12.2009
comment
зачем сохранять как несколько файлов, а потом объединять? Почему бы просто не сохранить в один непрерывный файл? - person mwag; 16.11.2015

Я передал такие большие файлы, используя кодировку MTOM.

Дополнительные сведения о MTOM см. здесь: http://msdn.microsoft.com/en-us/library/aa395209.aspx

Образец MTOM можно загрузить здесь: http://msdn.microsoft.com/en-us/library/ms751514.aspx

Прочтите книгу Бустаманте о WCF, если хотите узнать больше о MTOM.

Что касается вызова VBA, я не эксперт в этой области, поэтому у меня нет никакой информации о нем.

person Donn Felker    schedule 04.05.2009