Download file using service xdata and webcore save how binary file

Hello, I can't solve this, I have a rest api with xdata with which I am returning a file in service, my function that returns is type tstream, the problem I have is that when receiving the response.result of an xdatawebclient I don't know how can I pass it to a Tbyte to later make use of application.downloadbinaryfile(), could someone help me or give me an example of how I could perform this task?, thanks.

1 Like

I'm sure there are other examples, but you can check this one out: https://support.tmssoftware.com/t/web-core-xdata-service-query-example-v3/15107. You'll have to wade through some unrelated stuff but there is a PDF that is generated in XData that is then transferred to the client and handled as a regular file. So you can see how the Base64 conversions worked on both ends. Lots of benefits to Base64 encoding, particularly when trying to debug things.

1 Like

thank you very much for your answer, maybe you can help me, the issue is my endpoint if it is returning a file, if I enter http://localhost:2001/tms/xdata/CustomService/GetFile from the browser, I can get the file, the The problem is that I want to get it through an xdatawebclient.rawinvoke('ICustomService.GetFile',[]) and in the on load method I get a Response.Result, so I want to convert that response to Bytes or TJSUint8Array to be able to use the application.downloadbinaryfile( ) from webcore, do you have any idea how to do it?

Well. I got around that by just encoding the file (binary > Base64) in the XData service, and then decoding it in the client (Base64 > binary) so no need to worry about any kind of Bytes or TJSUnit8Array issues. However, if what you're working with is a binary TStream, then let's figure out what is needed to make that work :slight_smile:

Disclaimer... I don't know if this will work or if this is the best way. Just trying to help...

In this thread they convert a Base64 string to a TJSUnit8Array so it's likely the same idea, just that your source is your Response.Result value. So maybe it looks a little bit like this?

 function ResponseToArrayBuffer(str: string): TJSArrayBuffer;
  var
    BufView: TJSUInt8Array;
    I: Integer;
  begin
    Result := TJSArrayBuffer.new(Length(str));
    BufView := TJSUInt8Array(Result);
    for I := 0 to Length(str) - 1 do
      BufView := TJSString(str).charCodeAt(I);
  end;

And you'd use it like this?

buf := ResponseToArrayBuffer(String(Response.Result));

Someone who's actually done this might be of more help, but I think that's the gist of it.

2 Likes

Again, thank you very much for your answer, and how would you convert from TJSArrayBuffer to TJSUInt8Array, nuevamente muchas gracias

I think you can just convert it directly? No expert on JS data types but the compiler isn't complaining about it...

var
  binfile: TJSUnit8Array;
  arrfile: TJSArrayBuffer;

binfile := TJSUint8Array(arrfile)
1 Like

Could you explain me a little more about how to convert from base64 > binary and from binary > base64. thanks for your help

Ok :slight_smile:

On the XData server, an endpoint can convert a file into a Base64 string using the following approach. Normally you wouldn't necessarily pass it the file you want but rather something else and the server will figure out where the file is, but hopefully the idea is simple enough.

uses
  ...
  System.NetEncoding;

function TSomeFileService.GetSomeFile(ServerFileName: String): TStream;
var
  ms: TMemoryStream;
begin
  Result := TMemoryStream.Create;
  ms := TMemoryStream.Create;

  try
    ms.LoadFromFile(ServerFileName);
    ms.Position := 0;
    TNetEncoding.Base64.Encode(ms, Result);
  finally
    ms.Free;
  end;

end;

On the client side (assuming TMS Web Core here) you can retrieve the file like this.

function TForm1.GetSomeFile(SomeFileName: String; var FileData: WideString): Integer;
var
  Client:    TXDataWebClient;
  Response:  TXDataClientResponse;

begin

  Result := -1;
  Client            := TXDataWebClient.Create(nil);
  Client.Connection := ServerConn; // whaterver connection you are using

  // Get File
  try
    Response := await(Client.RawInvokeAsync('ISomeFileService.GetSomeFile', [ SomeFileName]  );
  except
    on Error: Exception do
    begin
      Client.Free;
      console.log('ERROR: '+Error.Classname+' ('+Error.Message+')');
      exit;
    end;
  end;

  // Convert file
  try
    FileData := window.atob(String(Response.Result));
    Result := Length(FileData);
  except on E: Exception do
    begin
      console.log('ERROR: (Base64) File could not be decoded');
    end;
  end;

  if (Result <> -1) then
  begin
    console.log('Retrieved (Base64) File: '+IntToStr(length(String(Response.Result)))+' bytes');
    console.log('Decoded (Base64) File: '+IntToStr(Result)+' bytes');
  end;

  Client.Free;
end;

This gets you a FileData value populated with a binary string containing your file. The next question is sure to be about what to do with that. I suppose if you are set on using Application.DownloadBinaryFile you can try this approach.

procedure TForm1.Button1Click(Sender: TObject);
var
  somefile: WideString;
  filesize: Integer;
  AB: TJSArrayBuffer;
  AV: TJSUInt8Array;
begin
  filesize := await(GetSomeFile('serverfilename', somefile));
  if (filesize <> -1) then
  begin
    AB := TJSArrayBuffer.new(filesize);
    AV := TJSUInt8Array.new(AB);
    for i := 0 to filesize - 1 do
      AV[i] := TJSString(somefile).charCodeAt(i);
    Application.DownloadBinaryFile(AV, 'clientfilename');
  end
  else
  begin
    // what/how do you want to tell the user about a failure here
  end;
end;

Hopefully that helps you work out whatever your situation calls for. Should work for any file, no messing about with content types or anything.

I don't particularly like Application.DownloadBinaryFile as I'd rather prompt the user to decide where to put the file at that moment rather than beforehand, but my situation might very well be different than yours. FileSaver.js is something that is mentioned in the thread I referenced originally so check that out if you'd like an alnterative. There's also a printjs library you can use which will take a base64-encoded pdf file directly if that's of any interest.

2 Likes

Thanks for the support, I have the following error from webcore side

"Uncaught DOMException: Failed to execute 'atob' on 'Window': The string to be decoded contains characters outside of the Latin1 range ".

In the case of using FileSaver.js, how would the implementation be done?

I've run into this before, but on the encoding end. I ended up using a different Base64 library on the client end, so you could try this:

<script src="https://cdn.jsdelivr.net/npm/js-base64@latest/base64.js"></script>

And instead of

FileData := window.atob(String(Response.Result));

Maybe try

FileData := String(Response.Result);
asm FileData = Base64.Decode(FileData); end;

I haven't tried it though. Not sure what is messing up the encoding to cause the problem though? The library also has other functions that may simplify the conversion to a UInt8Array if that helps any.

2 Likes

The posting I mentioned originally has the FileSaver.js example at the end of it.

This topic was automatically closed 60 minutes after the last reply. New replies are no longer allowed.