How adding ticket attachments with the Zendesk API using vba in Access [duplicate] - ms-access

Overview
I have a Microsoft Word Add-In, written in VBA (Visual Basic for Applications), that compresses a document and all of it's related contents (embedded media) into a zip archive. After creating the zip archive it then turns the file into a byte array and posts it to an ASMX web service. This mostly works.
Issues
The main issue I have is transferring large files to the web site. I can successfully upload a file that is around 40MB, but not one that is 140MB (timeout/general failure).
A secondary issue is that building the byte array in the VBScript Word Add-In can fail by running out of memory on the client machine if the zip archive is too large.
Potential Solutions
I am considering the following options and am looking for feedback on either option or any other suggestions.
Option One
Opening a file stream on the client (MS Word VBA) and reading one "chunk" at a time and transmitting to ASMX web service which assembles the "chunks" into a file on the server.
This has the benefit of not adding any additional dependencies or components to the application, I would only be modifying existing functionality. (Fewer dependencies is better as this solution should work in a variety of server environments and be relatively easy to set up.)
Question:
Are there examples of doing this or any recommended techniques (either on the client in VBA or in the web service in C#/VB.NET)?
Option Two
I understand WCF may provide a solution to the issue of transferring large files by "chunking" or streaming data. However, I am not very familiar with WCF, and am not sure what exactly it is capable of or if I can communicate with a WCF service from VBA. This has the downside of adding another dependency (.NET 3.0). But if using WCF is definitely a better solution I may not mind taking that dependency.
Questions:
Does WCF reliably support large file transfers of this nature? If so, what does this involve? Any resources or examples?
Are you able to call a WCF service from VBA? Any examples?

I ended up implementing option one referenced in the original question.
I "chunk" the file in VBA and transfer each "chunk" to the web service. I based the VBA portion of the solution on the code found here: Copy Large File by Chunk with Progress Notification. Instead of copying to the file system, however, I send it to the server.
The Code: VBA Land
Here is the (fugly) VBA code that creates the file chunks:
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
Here is the referenced VBA method that uploads the chunks to the server:
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
The Code: C# ASMX Web Service
Now, on the server side, the web service method accepts a few important parameters.
string fileName: The name of the
file (each chunk has the same file
name)
byte[] zipBytes: The contents
of each chunk
int index: The index
(used in conjunction with fileName
to provide unique ordered partial
files on the file system)
bool isLastChunk: This is the "i'm done -
go ahead and merge all the "chunks"
and clean up after yourself" flag.
int index and bool isLastChunk. With this context provided from the VBA world, I know enough to save each of these file chunks and then combine them when the isLastChunk flag is 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;
}
Here is the C# code that writes each incoming chunk the the file system:
/// <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);
}
}
}
Here is the C# code to merge all of the zip file "chunks":
/// <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);
}
}
}
}

I've transmitted large files like this using MTOM encoding.
More info on MTOM here: http://msdn.microsoft.com/en-us/library/aa395209.aspx
You can download an MTOM sample here: http://msdn.microsoft.com/en-us/library/ms751514.aspx
Check out Bustamante's book on WCF if you want more on MTOM.
As for the VBA call, I'm not an expert in that area therefore I don't have any info in regards to it.

Related

Script task knows there is a variable but crashes if I attempt to use it

setting up the container
Assigning the script variables
OK so no matter what I do with variables, my script task simply will not work with them. The error thrown is generic but as I'm working with SSDT I can't step through the code or do any reasonable debugging.
I have stripped this back to the point where I have uninstalled and reinstalled SSDT, and started with a new package from scratch, adding in a line of code at a time to the automatically generated code for the ScriptTask to ensure I'm not missing anything. Everything runs fine literally until I attempt to pull anything with a package variable, the code for which I've copied from the auto-help, and verified against SO and other sources.
"dts.Variables("User::variablename").value.toString()"
should work according to everything but I'm getting nowhere.
I've tried "User::FromFile", "FromFile", "Package::FromFile", "Project::FromFile" and "variables.item(1)" and none of the variations work either.
dts.variables.count shows that the script does know that there is a single variable there, and the variable does populate because I can get a subsequent data flow task to attempt opening files based on it.
Public Sub Main()
'
' Add your code here
'
Dim currFileName As String
Dim n As Integer
Dts.Events.FireInformation(0, "Script Task", "variables count: " & CType(Dts.Variables.Count, String), "", 0, True)
For n = 1 To Dts.Variables.Count
Dts.Events.FireInformation(0, "Script Task", CType(Dts.Variables.Item(n).Name.ToString, String) & " : " & CType(Dts.Variables.Item(n).Value, String), "", 0, True)
Next
' Debugging - assiging a file path explicity here does work so the loop and the code is fine
Dim filePath As String = "\\Path\File.csv"
Dts.Events.FireInformation(0, "Script Task", "Reading in file " & filePath, "", 0, True)
Try
currFileName = CType(Dts.Variables("FromFile").Value.ToString, String)
Catch
Dts.Events.FireError(999, "Reading Variable", "Can't read filename from variable name", "", 0)
End Try
Dts.Events.FireInformation(0, "Script Task", "Reading in file " & FilePath, "", 0, True)
Dim lines() As String = System.IO.File.ReadAllLines(filePath)
Dts.TaskResult = ScriptResults.Success
End Sub
Edit: Starting from scratch with the code below worked. However I am still not clear why previous attempts where I was using ".ToString()" or "CType(Dts.Variables("name of any variation").value,string)" weren't working.
Even when I wrapped all such attempts in Try-Catch loops I still got nowhere, where I would have expected VB to at least pick up the error but nothing.
Update #2
Currently on ... iteration 40(?) of "add one line/block of code in a try/catch and deal with the error". I'm still stuck why a try/catch would not prevent these invocation errors. Surely that's the whole point?
Update #3
Adding extra FireInformation events caused failures. Commenting them out didn't change the failures. Restarting VS and the script is completing in seconds ... having done nothing. Commenting out everything apart from #billinkc's code which works ... still does nothing. Losing the will to live here.
at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj,Object[] parameters, Object[] arguments)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.RuntimeType.InvokeMember(String name, BindingFlags bindingFlags, Binder binder, Object target, Object[] providedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams)
at Microsoft.SqlServer.Dts.Tasks.ScriptTask.VSTATaskScriptingEngine.ExecuteScript()
I believe your root issue is that you're using the function ToString as a pointer and not invoking it. i.e. Value.ToString instead of Value.ToString() My VB is rusty so I could be off on that. Let's explore
I created an SSIS Variable, a package Parameter and a Project Parameter, all called FromFile and initialized them with strings to verify I am seeing expected values.
I added a Script Task, set the language to VB and added the above variables to my ReadOnlyVariables collection User::FromFile,$Package::FromFile,$Project::FromFile
I make great use of the following script within my SSIS packages. It enumerates through whatever variables it has access to and pushes the results to the OnInformation event stream. In Visual Studio, this will show in the Execution Results panel and the Output tab - which is way more useful. When running in the SSISDB, unless you've turned off Information events, you'll see it recorded in SSISDB.catalog.operation_messages
Public Sub Main()
Dim fireAgain As Boolean = False
For Each item As Variable In Dts.Variables
Dim template As String = "{0}::{1}->{2}"
Dim message As String = ""
message = String.Format(template, item.Namespace, item.Name, item.Value)
Dts.Events.FireInformation(0, "Scr Echo", message, "", 0, fireAgain)
Next
Dts.TaskResult = ScriptResults.Success
End Sub
In the Output tab, here's what I see
Information: 0x0 at SCR Echo Back, Scr Echo: $Package::FromFile->I am package parameter
Information: 0x0 at SCR Echo Back, Scr Echo: $Project::FromFile->I am project parameter
Information: 0x0 at SCR Echo Back, Scr Echo: User::FromFile->Hello world from Variable
As an added bonus, we see that the Parameters, Package:: and Project:: won't work because that's now how those parameters are addressed. The would be $Package::FromFile and $Project::FromFile
Now that I know I am correctly able to access the value of my variables or parameters, let's dig in an see if I can either reproduce your error or provide working code. I don't understand enough of what your code expects. You have two variables, filePath and currFileName. filePath is initialized to a fully qualified path, maybe? You're either going with a UNC but no share specified or a local path where you've half double escaped the slashes. We assign a value to currFileName which is from our Variable.
However, we use the filePath variable as the source for our ReadAllLines.
Public Sub Main()
Dim filePath As String = "C:\ssisdata\Input\SO_67660730.DoesNotExist.txt"
Dim fireAgain As Boolean = False
Dim lines() As String
If (False) Then
For Each item As Variable In Dts.Variables
Dim template As String = "{0}::{1}->{2}"
Dim message As String = ""
message = String.Format(template, item.Namespace, item.Name, item.Value)
Dts.Events.FireInformation(0, "Scr Echo", message, "", 0, fireAgain)
Next
End If
' Populate our filePath local variable with the value from the Variable collection
' Option A Blindly assume the first element in our collection is what we want
filePath = Dts.Variables(0).Value.ToString()
Dts.Events.FireInformation(0, "Option A", filePath, "", 0, fireAgain)
' Fully qualified named value
filePath = Dts.Variables("User::FromFile").Value.ToString()
Dts.Events.FireInformation(0, "Option B", filePath, "", 0, fireAgain)
' Either way, we have the file path available to us, let's read the data
If (System.IO.File.Exists(filePath)) Then
lines = System.IO.File.ReadAllLines(filePath)
Dts.Events.FireInformation(0, "Rows Read", lines.Length.ToString(), "", 0, fireAgain)
Else
Dts.Events.FireInformation(0, "No file found", filePath, "", 0, fireAgain)
End If
Dts.TaskResult = ScriptResults.Success
End Sub
Results being
Information: 0x0 at SCR Read File, Option A: C:\ssisdata\Input\SO_67660730.txt
Information: 0x0 at SCR Read File, Option B: C:\ssisdata\Input\SO_67660730.txt
Information: 0x0 at SCR Read File, Rows Read: 3
If you persist in using VB.NET as your language, you're going to be need to find blogs and samples that were built on 2005/2008 version of SSIS as that was the only language available to us. Most of the world has adopted C# as their primary language for SSIS Script and Tasks but Ben Weissman with Solisyon uses VB in his examples.

Read whole appsettings.json file as a string

I wish to read the entire contents of my appsettings.json file into NewtonSoft.JSON so I can parse it using NewtonSoft.
This is due to being able to use full JSON paths with NewtonSoft (filtering etc.)
I basically want to just read my entire appsettings.json file as a string.
I already have it working in regardings to a Configuration.
Private Shared Function InitConfig() As IConfigurationRoot
Try
Dim builder = New ConfigurationBuilder() _
.AddJsonFile("appsettings.json", True, True) _
.AddEnvironmentVariables()
Return builder.Build
Catch ex As Exception
Console.WriteLine(ex.Message)
Return Nothing
End Try
End Function
However I don't want to choose specifics i.e. config("test1:test:bot_token"). I wish to just read the entire string, however I don't appear to be able to get this from the ConfigurationRoot.
Cheers
Went about this all the wrong way - just used a streamreader to read the appsettings.json file.
Complete brain fart moment.
Dim tr As TextReader = New StreamReader("appsettings.json")
Dim stream = tr.ReadToEnd

64k limit for NotesJSONNavigator?

I'm currently testing the new Domino V10 LotusScript NotesJSONNavigator classes, and trying to parse long strings using this code:
dim session as New NotesSession
Dim jsonReader as NotesJSONNavigator
Dim stream as NotesStream
Set stream = session.CreateStream
stream.WriteText("*** Populate stream with long JSON here ***")
stream.Position = 0
Set jsonReader = session.CreateJSONNavigator(stream.ReadText)
Once the stream contains more than 64k of text, the final line of code produces an error as follows:
"Unable to Parse JSON string:
Missing a closing quotation mark in string, offset 65535."
If there is a 64k limit, this is a real shame, because the performance of NotesJSONNavigator appears to be very good. Is there any workaround for this if so?

Archive file in Script task using c# in ssis

I want to archive file with date using Script task.filename will be like abc_x1.csv. file will be coming monthly once next file comes like abc_x2.csv
Need to archive the file with whatever name after _.csv with date before the extension. iam using the below code this is not working. can anyone help me on this.using C# code in script task.script task used inside the for each loop container.
public void Main()
{ String sDateSuffix = DateTime.Now.ToString("yyyyMMddhhmmss");
String ArchiveDir = Dts.Variables["V_ArchiveDir"].Value.ToString();
String FileName = Dts.Variables["V_FileName"].Value.ToString()+sDateSuffix;
File.Move(Dts.Variables["V_FilePath"].Value.ToString() +FileName, ArchiveDir + FileName);
Dts.TaskResult = (int)ScriptResults.Success;
}
If I understand you correctly, you are trying to append your sDateSuffix to your file name but before the .csv extension. If so, I think you just need to use Path.GetFileNameWithoutExtension():
String sDateSuffix = DateTime.Now.ToString("yyyyMMddhhmmss");
String ArchiveDir = Dts.Variables["V_ArchiveDir"].Value.ToString();
String V_FileName = Dts.Variables["V_FileName"].Value.ToString();
//Assuming V_FileName = "abc_x1.csv", FileName will be "abc_x120150430080000.csv"
String FileName = Path.GetFileNameWithoutExtension(V_FileName) + sDateSuffix + Path.GetExtension(V_FileName);

JSON import to Excel

Is it possible to script JSON calls in a macro?
I want to get a JSON string through an API connection. It looks like the problem is Excel expects the parameters to be passed in the HTML-string, but JSON passes parameters in the HTML body. Any ideas?
Since this is VBA, I'd use COM to call xmlhttprequest but use it in synchronous manner as not to upset VBA’s single threaded execution environment, A sample class that illustrates a post and get request in this manner follows:
'BEGIN CLASS syncWebRequest
Private Const REQUEST_COMPLETE = 4
Private m_xmlhttp As Object
Private m_response As String
Private Sub Class_Initialize()
Set m_xmlhttp = CreateObject("Microsoft.XMLHTTP")
End Sub
Private Sub Class_Terminate()
Set m_xmlhttp = Nothing
End Sub
Property Get Response() As String
Response = m_response
End Property
Property Get Status() As Long
Status = m_xmlhttp.Status
End Property
Public Sub AjaxPost(Url As String, Optional postData As String = "")
m_xmlhttp.Open "POST", Url, False
m_xmlhttp.setRequestHeader "Content-type", "application/x-www-form-urlencoded"
m_xmlhttp.setRequestHeader "Content-length", Len(postData)
m_xmlhttp.setRequestHeader "Connection", "close"
m_xmlhttp.send (postData)
If m_xmlhttp.readyState = REQUEST_COMPLETE Then
m_response = m_xmlhttp.responseText
End If
End Sub
Public Sub AjaxGet(Url As String)
m_xmlhttp.Open "GET", Url, False
m_xmlhttp.setRequestHeader "Connection", "close"
m_xmlhttp.send
If m_xmlhttp.readyState = REQUEST_COMPLETE Then
m_response = m_xmlhttp.responseText
End If
End Sub
'END CLASS syncWebRequest
So now you can call the above to return you the server's response:
Dim request As New syncWebRequest
request.ajaxGet "http://localhost/ClientDB/AllClients?format=json"
Dim json as string
json = request.Response
The problem here is we want to be able to read the data returned from the server in some way, more so than manipulating the JSON string directly. What's worked for me is using the VBA-JSON (google code export here) COM type Collection to handle JSON arrays and Dictionary to handle members and their declarations, with a parser factory method Parse that basically makes creating these collections of dictionaries much simpler.
So now we can parse the JSON:
[{"Name":"test name","Surname":"test surname","Address":{"Street":"test street","Suburb":"test suburb","City":"test city"}}]
into something like the following:
Set clients = parser.parse(request.Response)
For Each client In clients
name = client("Name")
surname = client("Surname")
street = client("Address")("Street")
suburb = client("Address")("Suburb")
city = client("Address")("City")
Next
That's nice but what about stuff like being able to edit and post back the data? Well there's also a method toString to create a JSON string from the above [Collection/Dictionary] JSON data, assuming the server accepts JSON back.
I wrote a .NET Excel-Addin for this. It's a generic Excel JSON client that streams any JSON object straight into Excel via http.
Docs and installation instructions can be found here:
http://excel-requests.pathio.com/en/master/
And here's the GitHub link:
https://github.com/ZoomerAnalytics/excel-requests