Как перенести большой файл из надстройки MS Word (VBA) на веб-сервер?
-
03-07-2019 - |
Вопрос
Обзор
У меня есть надстройка 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?Есть какие-нибудь примеры?
Решение
В итоге я реализовал первый вариант, упомянутый в исходном вопросе.
Я "разделяю" файл на части в 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
Теперь, на стороне сервера, метод веб-службы принимает несколько важных параметров.
- строковое имя файла:Имя файла (каждый фрагмент имеет одинаковое имя файла )
- байт[] ZIP-байты:Содержимое Каждого фрагмента
- индекс int:Индекс (используется в сочетании с именем файла для предоставления уникальных упорядоченных частичных файлов в файловой системе)
- bool является фрагментом:Это флаг "я закончил - продолжайте и объедините все "куски" и уберите за собой".
int index и bool - это фрагмент.С этим контекстом, предоставленным из мира 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);
}
}
}
Вот код на C # для объединения всех "фрагментов" 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);
}
}
}
}
Другие советы
Я передавал большие файлы, подобные этому, используя кодировку MTOM.
Более подробная информация о MTOM здесь: http://msdn.microsoft.com/en-us/library/aa395209.aspx
Вы можете скачать образец MTOM здесь: http://msdn.microsoft.com/en-us/library/ms751514.aspx
Ознакомьтесь с книгой Бустаманте о WCF, если хотите узнать больше о MTOM.
Что касается вызова VBA, я не эксперт в этой области, поэтому у меня нет никакой информации по этому поводу.