MS Word Add-In (VBA)에서 웹 서버로 큰 파일을 전송하는 방법은 무엇입니까?
-
03-07-2019 - |
문제
개요
VBA (Applications for Applications for Applications for Applications)로 작성된 Microsoft Word 추가 기능이 있으며 문서와 모든 관련 내용 (임베디드 미디어)을 ZIP 아카이브로 압축합니다. Zip Archive를 작성한 후 파일을 바이트 배열로 바꾸고 ASMX 웹 서비스에 게시합니다. 이것은 대부분 작동합니다.
문제
내가 가진 주요 문제는 큰 파일을 웹 사이트로 전송하는 것입니다. 약 40MB의 파일을 성공적으로 업로드 할 수는 있지만 140MB (타임 아웃/일반 실패)는 아닙니다.
보조 문제는 zip 아카이브가 너무 커지면 클라이언트 시스템에서 메모리가 부족하여 VBScript Word Add-In에 바이트 배열을 구축하면 실패 할 수 있다는 것입니다.
잠재적 솔루션
다음 옵션을 고려하고 있으며 옵션 또는 기타 제안에 대한 피드백을 찾고 있습니다.
옵션 1
클라이언트 (MS Word VBA)에서 파일 스트림을 열고 한 번에 하나의 "청크"를 읽고 서버의 파일에 "청크"를 조립하는 ASMX 웹 서비스로 전송합니다.
이는 응용 프로그램에 추가 종속성 또는 구성 요소를 추가하지 않으면 기존 기능 만 수정하는 이점이 있습니다. (이 솔루션은 다양한 서버 환경에서 작동하고 비교적 쉽게 설정하기 때문에 종속성이 적습니다.)
의문:
- 이 작업 또는 권장 기술을 수행하는 예가 있습니까 (VBA의 클라이언트 또는 C#/vb.net의 웹 서비스)?
옵션 2
WCF가 "청킹"또는 스트리밍 데이터를 통해 대형 파일을 전송하는 문제에 대한 솔루션을 제공 할 수 있다는 것을 알고 있습니다. 그러나 저는 WCF에 익숙하지 않으며 VBA의 WCF 서비스와 통신 할 수있는 것이 정확히 무엇인지 확실하지 않습니다. 이것은 다른 종속성을 추가하는 단점이 있습니다 (.NET 3.0). 그러나 WCF를 사용하는 것이 확실히 더 나은 솔루션이라면 그 종속성을 취하지 않을 수 있습니다.
질문:
- WCF는 이러한 특성의 큰 파일 전송을 확실하게 지원합니까? 그렇다면 이것이 무엇을 포함합니까? 리소스 나 예제가 있습니까?
- VBA에서 WCF 서비스를 호출 할 수 있습니까? 예가 있습니까?
해결책
원래 질문에서 참조 된 옵션 1을 구현하게되었습니다.
나는 VBA의 파일을 "청크"하고 각 "청크"를 웹 서비스로 전송합니다. 나는 여기에있는 코드에 대한 솔루션의 VBA 부분을 기반으로합니다. 진행 상황 알림으로 청크로 큰 파일을 복사하십시오. 그러나 파일 시스템에 복사하는 대신 서버로 보냅니다.
코드 : VBA 토지
파일 청크를 생성하는 (Fugly) 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 웹 서비스
이제 서버 측에서 웹 서비스 방법은 몇 가지 중요한 매개 변수를 수용합니다.
- String filename : 파일 이름 (각 청크는 동일한 파일 이름을 가지고 있음)
- 바이트 [] zipbytes : 각 청크의 내용
- int index : 인덱스 (파일 시스템에서 고유 한 부분 파일을 제공하기 위해 파일 이름과 함께 사용)
- 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 파일 "청크"를 병합하는 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 전화에 관해서는, 저는 해당 분야의 전문가가 아니므로 정보가 없습니다.