lessto CloudToFile

Demo of Business Central 365 spring edition (14) where data is transferred to and from a FTP site. A simple cloud to file function.

Code for Business Central 365 Wave 2 (15)
Simpel demo of the four FTP commands
The full wordpress code from the demo.

The code works both in local installation, server onprem, docker and cloud version.

The first page extenstions are for demo purpose, use the code to make your own AL code.

The codeunit is what makes the magic happens, so do not make changes.

Feel free to use the code if you have projects with http request, JSON conversion and BASE64.

The keypoints is to change the APIKEY and ftp information, this can be done in the AL code. In customer code it should be saved in a table.

The next keypoint is to make the call to the codeunit.

lesstoFTPmanagement.lesstoFTPsend(apikey, ftpuser, ftppassword, ftpserver, ftpdir, ftpfilename, base64, errorno, errortext, status, serverfile);

You can make this call in pages and codeunits and batchjobs.

Here is the code. You could make the codeunit as one extension and the page extensions as another. After installation the actions are on the customer list->processing.

pageextension 50000 lesstoFTPhttps extends "Customer List"
{
actions
{
addlast(Processing)
{
Action(FTPsendFile)
{
ApplicationArea = Basic, Suite;
image = GetEntries;
Caption = 'FTPsendfile';
Promoted = true;
PromotedCategory = Process;
PromotedIsBig = true;
trigger OnAction();
var
apikey: Text[100];
ftpuser: Text[20];
ftppassword: Text[20];
ftpserver: Text[20];
ftpdir: Text[100];
ftpfilename: Text[50];
base64: text;
lesstoFTPmanagement: codeunit "lesstoFTPmanagement";

Customer: Record Customer;
FileBlob: Record Tempblob temporary;
DataString: Text;
FileOutStream: OutStream;
FileInStream: InStream;
CRLF: text[2];
Errorno: Text[2];
Errortext: Text[100];
Status: Text[100];
Serverfile: Text[200];
begin

apikey := '<Your API key from lessto Cloud To File>';
ftpuser := '<FTP server user name>';
ftppassword := '<FTP server user password>';
ftpserver := '<FTP server name>';
ftpdir := '<FTP server directory>';
ftpfilename := 'testcsvfile.csv'; // filename in this demo

CRLF := ' ';
CRLF[1] := 13;
CRLF[2] := 10;

FileBlob.Blob.CreateOutStream(FileOutStream, TextEncoding::UTF8);
if customer.findfirst then
repeat
datastring := '"' + customer."No." + '","' + customer.Name + '"' + CRLF;
FileOutStream.WriteText(datastring);
until customer.next = 0;

base64 := fileblob.ToBase64String();

lesstoFTPmanagement.lesstoFTPsend(apikey, ftpuser, ftppassword, ftpserver, ftpdir, ftpfilename, base64, errorno, errortext, status, serverfile);

if errorno <> '' then begin
message('Errorno = ' + errorno + ' Errortext = ' + errortext);
exit;
end;

message(status);

end;
}

Action(FTPgetdir)
{
ApplicationArea = Basic, Suite;
image = GetEntries;
Caption = 'FTPgetdir';
Promoted = true;
PromotedCategory = Process;
PromotedIsBig = true;
trigger OnAction();
var
apikey: Text[100];
ftpuser: Text[20];
ftppassword: Text[20];
ftpserver: Text[20];
ftpdir: Text[100];
lesstoFTPmanagement: codeunit "lesstoFTPmanagement";
Errorno: Text[2];
Errortext: Text[100];
Status: Text[100];
dirlist: array[1000] of text[100];
i: integer;
direntry: text;
CRLF: text[2];
Serverfile: Text[200];
begin

apikey := '<Your API key from lessto Cloud To File>';
ftpuser := '<FTP server user name>';
ftppassword := '<FTP server user password>';
ftpserver := '<FTP server name>';
ftpdir := '<FTP server directory>';

CRLF := ' ';
CRLF[1] := 13;
CRLF[2] := 10;

lesstoFTPmanagement.lesstoFTPgetdir(apikey, ftpuser, ftppassword, ftpserver, ftpdir, errorno, errortext, status, dirlist, serverfile);

if errorno <> '' then begin
message('Errorno = ' + errorno + ' Errortext = ' + errortext);
exit;
end;

for i := 1 to arraylen(dirlist) do begin
if dirlist[i] <> '' then
direntry := direntry + dirlist[i] + CRLF;
end;

message(direntry);

end;
}

Action(FTPgetfile)
{
ApplicationArea = Basic, Suite;
image = GetEntries;
Caption = 'FTPgetfile';
Promoted = true;
PromotedCategory = Process;
PromotedIsBig = true;
trigger OnAction();
var
apikey: Text[100];
ftpuser: Text[20];
ftppassword: Text[20];
ftpserver: Text[20];
ftpdir: Text[100];
ftpfilename: Text[100];
Textfile: Text;
lesstoFTPmanagement: codeunit "lesstoFTPmanagement";
Errorno: Text[2];
Errortext: Text[100];
Status: Text[100];
dirlist: array[1000] of text[100];
i: integer;
direntry: text;
CRLF: text[2];
Serverfile: Text[200];
begin

apikey := '<Your API key from lessto Cloud To File>';
ftpuser := '<FTP server user name>';
ftppassword := '<FTP server user password>';
ftpserver := '<FTP server name>';
ftpdir := '<FTP server directory>';
ftpfilename := 'testcsvfile.csv'; // filename in this demo

CRLF := ' ';
CRLF[1] := 13;
CRLF[2] := 10;

lesstoFTPmanagement.lesstoFTPget(apikey, ftpuser, ftppassword, ftpserver, ftpdir, ftpfilename, textfile, errorno, errortext, status, Serverfile);

if errorno <> '' then begin
message('Errorno = ' + errorno + ' Errortext = ' + errortext);
exit;
end;

message(textfile);

end;
}

Action(FTPdeletefile)
{
ApplicationArea = Basic, Suite;
image = GetEntries;
Caption = 'FTPdeletefile';
Promoted = true;
PromotedCategory = Process;
PromotedIsBig = true;
trigger OnAction();
var
apikey: Text[100];
ftpuser: Text[20];
ftppassword: Text[20];
ftpserver: Text[20];
ftpdir: Text[100];
ftpfilename: Text[100];
Textfile: Text;
lesstoFTPmanagement: codeunit "lesstoFTPmanagement";
Errorno: Text[2];
Errortext: Text[100];
Status: Text[100];
Serverfile: Text[200];

begin

apikey := '<Your API key from lessto Cloud To File>';
ftpuser := '<FTP server user name>';
ftppassword := '<FTP server user password>';
ftpserver := '<FTP server name>';
ftpdir := '<FTP server directory>';
ftpfilename := 'testcsvfile.csv'; // filename in this demo

lesstoFTPmanagement.lesstoFTPdelete(apikey, ftpuser, ftppassword, ftpserver, ftpdir, ftpfilename, errorno, errortext, status, serverfile);

if errorno <> '' then begin
message('Errorno = ' + errorno + ' Errortext = ' + errortext);
exit;
end;

message(status);

end;
}
}
}
}
codeunit 50000 "lesstoFTPmanagement"

{
trigger OnRun()
var

begin

end;

procedure lesstoFTPgetdir(apikey: Text[100]; ftpuser: Text[20]; ftppassword: Text[20]; ftpserver: Text[20]; ftpdir: Text[100]; var errorno: text[2]; var errortext: text[100]; var status: text[100]; var dirlist: array[1000] of text[100]; var serverfile: text[200]);

var
TempBlob: Record TempBlob temporary;
JsonResArray: JsonArray;
JsonResToken: JsonToken;
JsonResObject: JsonObject;
httploadOutStream: OutStream;
httploadInStream: InStream;
Content: HttpContent;
ContentHeaders: HttpHeaders;
Client: HttpClient;
Request: HttpRequestMessage;
Response: HttpResponseMessage;
ResponseText: text;
PostString: Text;
i: Integer;
j: Integer;
maxi: integer;
Keystring: text;

begin

Content.GetHeaders(ContentHeaders);
ContentHeaders.Clear();
ContentHeaders.Add('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
TempBlob.Blob.CreateOutStream(httploadOutStream, TextEncoding::UTF8);

poststring := '&action=list' +
'&apikey=' + apikey +
'&user=' + ftpuser +
'&pass=' + ftppassword +
'&ftpserver=' + ftpserver +
'&ftpdir=' + ftpdir;

httploadOutStream.WriteText(poststring);

TempBlob.Blob.CreateInStream(httploadInStream, TextEncoding::UTF8);
Content.WriteFrom(httploadInStream);
Request.Content := Content;
Request.SetRequestUri('https://lessto.dk/cloudtofile/lessto-ftp-connect');
Request.Method := 'POST';

Client.Send(Request, Response);

Response.Content().ReadAs(ResponseText);

ResponseText := '[' + ResponseText + ']';
if not JsonResArray.ReadFrom((ResponseText)) then begin
errorno := '99';
errortext := 'Connection error';
exit;
end;

For i := 0 to jsonresarray.count - 1 do begin
jsonResarray.get(i, jsonrestoken);
JsonResObject := jsonrestoken.asobject;
end;

if jsonresobject.get('errorno', JsonResToken) then
errorno := jsonrestoken.asvalue.astext;

if jsonresobject.get('errortext', JsonResToken) then
errortext := jsonrestoken.asvalue.astext;

if jsonresobject.get('status', JsonResToken) then
status := jsonrestoken.asvalue.astext;

if jsonresobject.get('serverfile', JsonResToken) then
serverfile := jsonrestoken.asvalue.astext;

maxi := 1000;

j := 0;
For i := 0 to maxi do begin
Keystring := 'file' + format(i);
if jsonresobject.get(Keystring, JsonResToken) then begin
j := j + 1;
dirlist[j] := jsonrestoken.asvalue.astext;
end;
end;
end;

procedure lesstoFTPsend(apikey: Text[100]; ftpuser: Text[20]; ftppassword: Text[20]; ftpserver: Text[20]; ftpdir: Text[100]; ftpfilename: Text[50]; base64: text; var errorno: text[2]; var errortext: text[100]; var status: text[100]; var serverfile: text[200]);
var
TempBlob: Record TempBlob temporary;
JsonResArray: JsonArray;
JsonResToken: JsonToken;
JsonResObject: JsonObject;
httploadOutStream: OutStream;
httploadInStream: InStream;
Content: HttpContent;
ContentHeaders: HttpHeaders;
Client: HttpClient;
Request: HttpRequestMessage;
Response: HttpResponseMessage;
ResponseText: text;
PostString: Text;
i: Integer;

begin

Content.GetHeaders(ContentHeaders);
ContentHeaders.Clear();
ContentHeaders.Add('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
TempBlob.Blob.CreateOutStream(httploadOutStream, TextEncoding::UTF8);

poststring := '&action=send' +
'&apikey=' + apikey +
'&user=' + ftpuser +
'&pass=' + ftppassword +
'&ftpserver=' + ftpserver +
'&ftpdir=' + ftpdir +
'&ftpfilename=' + ftpfilename +
'&base64=' + base64;

httploadOutStream.WriteText(poststring);

TempBlob.Blob.CreateInStream(httploadInStream, TextEncoding::UTF8);
Content.WriteFrom(httploadInStream);
Request.Content := Content;
Request.SetRequestUri('https://lessto.dk/cloudtofile/lessto-ftp-connect');
Request.Method := 'POST';

Client.Send(Request, Response);

Response.Content().ReadAs(ResponseText);

ResponseText := '[' + ResponseText + ']';
if not JsonResArray.ReadFrom((ResponseText)) then begin
errorno := '99';
errortext := 'Connection error';
exit;
end;

For i := 0 to jsonresarray.count - 1 do begin
jsonResarray.get(i, jsonrestoken);
JsonResObject := jsonrestoken.asobject;
end;

if jsonresobject.get('errorno', JsonResToken) then
errorno := jsonrestoken.asvalue.astext;

if jsonresobject.get('errortext', JsonResToken) then
errortext := jsonrestoken.asvalue.astext;

if jsonresobject.get('status', JsonResToken) then
status := jsonrestoken.asvalue.astext;

if jsonresobject.get('serverfile', JsonResToken) then
serverfile := jsonrestoken.asvalue.astext;

end;

procedure lesstoFTPget(apikey: Text[100]; ftpuser: Text[20]; ftppassword: Text[20]; ftpserver: Text[20]; ftpdir: Text[100]; ftpfilename: Text[50]; var textfile: text; var errorno: text[2]; var errortext: text[100]; var status: text[100]; var serverfile: text[200]);
var
TempBlob: Record TempBlob temporary;
JsonResArray: JsonArray;
JsonResToken: JsonToken;
JsonResObject: JsonObject;
httploadOutStream: OutStream;
httploadInStream: InStream;
Content: HttpContent;
ContentHeaders: HttpHeaders;
Client: HttpClient;
Request: HttpRequestMessage;
Response: HttpResponseMessage;
ResponseText: text;
PostString: Text;
i: Integer;
Base64: text;

begin

Content.GetHeaders(ContentHeaders);
ContentHeaders.Clear();
ContentHeaders.Add('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
TempBlob.Blob.CreateOutStream(httploadOutStream, TextEncoding::UTF8);

poststring := '&action=get' +
'&apikey=' + apikey +
'&user=' + ftpuser +
'&pass=' + ftppassword +
'&ftpserver=' + ftpserver +
'&ftpdir=' + ftpdir +
'&ftpfilename=' + ftpfilename;

httploadOutStream.WriteText(poststring);

TempBlob.Blob.CreateInStream(httploadInStream, TextEncoding::UTF8);
Content.WriteFrom(httploadInStream);
Request.Content := Content;
Request.SetRequestUri('https://lessto.dk/cloudtofile/lessto-ftp-connect');
Request.Method := 'POST';

Client.Send(Request, Response);

Response.Content().ReadAs(ResponseText);

ResponseText := '[' + ResponseText + ']';
if not JsonResArray.ReadFrom((ResponseText)) then begin
errorno := '99';
errortext := 'Connection error';
exit;
end;

For i := 0 to jsonresarray.count - 1 do begin
jsonResarray.get(i, jsonrestoken);
JsonResObject := jsonrestoken.asobject;
end;

if jsonresobject.get('errorno', JsonResToken) then
errorno := jsonrestoken.asvalue.astext;

if jsonresobject.get('errortext', JsonResToken) then
errortext := jsonrestoken.asvalue.astext;

if jsonresobject.get('status', JsonResToken) then
status := jsonrestoken.asvalue.astext;

if jsonresobject.get('base64', JsonResToken) then begin
base64 := jsonrestoken.asvalue.astext;
textfile := FromBase64StringToText(base64);
end;

if jsonresobject.get('serverfile', JsonResToken) then
serverfile := jsonrestoken.asvalue.astext;

end;

procedure lesstoFTPdelete(apikey: Text[100]; ftpuser: Text[20]; ftppassword: Text[20]; ftpserver: Text[20]; ftpdir: Text[100]; ftpfilename: Text[50]; var errorno: text[2]; var errortext: text[100]; var status: text[100]; var serverfile: text[200]);
var
TempBlob: Record TempBlob temporary;
JsonResArray: JsonArray;
JsonResToken: JsonToken;
JsonResObject: JsonObject;
httploadOutStream: OutStream;
httploadInStream: InStream;
Content: HttpContent;
ContentHeaders: HttpHeaders;
Client: HttpClient;
Request: HttpRequestMessage;
Response: HttpResponseMessage;
ResponseText: text;
PostString: Text;
i: integer;

begin

Content.GetHeaders(ContentHeaders);
ContentHeaders.Clear();
ContentHeaders.Add('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
TempBlob.Blob.CreateOutStream(httploadOutStream, TextEncoding::UTF8);

poststring := '&action=delete' +
'&apikey=' + apikey +
'&user=' + ftpuser +
'&pass=' + ftppassword +
'&ftpserver=' + ftpserver +
'&ftpdir=' + ftpdir +
'&ftpfilename=' + ftpfilename;

httploadOutStream.WriteText(poststring);

TempBlob.Blob.CreateInStream(httploadInStream, TextEncoding::UTF8);
Content.WriteFrom(httploadInStream);
Request.Content := Content;
Request.SetRequestUri('https://lessto.dk/cloudtofile/lessto-ftp-connect');
Request.Method := 'POST';

Client.Send(Request, Response);

Response.Content().ReadAs(ResponseText);

ResponseText := '[' + ResponseText + ']';
if not JsonResArray.ReadFrom((ResponseText)) then begin
errorno := '99';
errortext := 'Connection error';
exit;
end;

For i := 0 to jsonresarray.count - 1 do begin
jsonResarray.get(i, jsonrestoken);
JsonResObject := jsonrestoken.asobject;
end;

if jsonresobject.get('errorno', JsonResToken) then
errorno := jsonrestoken.asvalue.astext;

if jsonresobject.get('errortext', JsonResToken) then
errortext := jsonrestoken.asvalue.astext;

if jsonresobject.get('status', JsonResToken) then
status := jsonrestoken.asvalue.astext;

if jsonresobject.get('serverfile', JsonResToken) then
serverfile := jsonrestoken.asvalue.astext;

end;

procedure TextToBase64String(Value: Text) ReturnValue: Text;
var
BinaryValue: text;
Length: Integer;
begin
// Divide value into blocks of 3 bytes
Length := StrLen(Value);
BinaryValue := TextToBinary(Value, 8);
ReturnValue := ConvertBinaryValueToBase64String(BinaryValue, Length);
end;

procedure StreamToBase64String(Value: InStream) ReturnValue: Text;
var
SingleByte: Byte;
Length: Integer;
BinaryValue: Text;
begin
while not Value.EOS do begin
Value.Read(SingleByte, 1);
Length += 1;
BinaryValue += ByteToBinary(SingleByte, 8);
end;

ReturnValue := ConvertBinaryValueToBase64String(BinaryValue, Length);
end;

procedure FromBase64StringToText(Value: Text) ReturnValue: Text;
var
BinaryValue: Text;
begin
BinaryValue := ConvertBase64StringToBinaryValue(Value);
ReturnValue := BinaryToText(BinaryValue);
end;

procedure FromBase64StringToStream(Value: Text; var ReturnValue: OutStream);
var
BinaryValue: Text;
begin
BinaryValue := ConvertBase64StringToBinaryValue(Value);
BinaryToStream(BinaryValue, ReturnValue);
end;

local procedure ConvertBinaryValueToBase64String(Value: Text; Length: Integer) ReturnValue: Text;
var
Length2: Integer;
PaddingCount: Integer;
BlockCount: Integer;
Pos: Integer;
CurrentByte: text;
i: Integer;
begin
if Length MOD 3 = 0 then begin
PaddingCount := 0;
BlockCount := Length / 3;
end else begin
PaddingCount := 3 - (Length MOD 3);
BlockCount := (Length + PaddingCount) / 3;
end;

Length2 := Length + PaddingCount;
Value := PadStr(Value, Length2 * 8, '0');

// Loop through bytes in groups of 6 bits
Pos := 1;
while Pos < Length2 * 8 do begin
CurrentByte := CopyStr(Value, Pos, 6);
ReturnValue += GetBase64Char(BinaryToInt(CurrentByte));
pos += 6;
end;

// Replace last characters with '='
for i := 1 to PaddingCount do begin
Pos := StrLen(ReturnValue) - i + 1;
ReturnValue[Pos] := '=';
end;

end;

local procedure ConvertBase64StringToBinaryValue(Value: Text) ReturnValue: Text;
var
BinaryValue: Text;
i: Integer;
IntValue: Integer;
PaddingCount: Integer;
begin
for i := 1 to StrLen(Value) do begin
if Value[i] = '=' then
PaddingCount += 1;

IntValue := GetBase64Number(Value[i]);
BinaryValue += IncreaseStringLength(IntToBinary(IntValue), 6);
end;

for i := 1 to PaddingCount do
BinaryValue := CopyStr(BinaryValue, 1, StrLen(BinaryValue) - 8);

ReturnValue := BinaryValue;
end;

local procedure TextToBinary(Value: text; ByteLength: Integer) ReturnValue: text;
var
IntValue: Integer;
i: Integer;
BinaryValue: text;
begin
for i := 1 to StrLen(value) do begin
IntValue := value[i];
BinaryValue := IntToBinary(IntValue);
BinaryValue := IncreaseStringLength(BinaryValue, ByteLength);
ReturnValue += BinaryValue;
end;
end;

local procedure BinaryToText(Value: Text) ReturnValue: Text;
var
Buffer: BigText;
Pos: Integer;
SingleByte: Text;
CharValue: Text;
begin
Buffer.AddText(Value);

Pos := 1;
while Pos < Buffer.Length do begin
Buffer.GetSubText(SingleByte, Pos, 8);
CharValue[1] := BinaryToInt(SingleByte);
ReturnValue += CharValue;
Pos += 8;
end;
end;

local procedure BinaryToStream(Value: Text; var ReturnValue: OutStream);
var
Buffer: BigText;
Pos: Integer;
SingleByte: Text;
ByteValue: Byte;
begin
Buffer.AddText(Value);

Pos := 1;
while Pos < Buffer.Length do begin
Buffer.GetSubText(SingleByte, Pos, 8);
ByteValue := BinaryToInt(SingleByte);
ReturnValue.Write(ByteValue, 1);
Pos += 8;
end;
end;

local procedure ByteToBinary(Value: Byte; ByteLenght: Integer) ReturnValue: Text;
var
BinaryValue: Text;
begin
BinaryValue := IntToBinary(Value);
BinaryValue := IncreaseStringLength(BinaryValue, ByteLenght);
ReturnValue := BinaryValue;
end;

local procedure IntToBinary(Value: integer) ReturnValue: text;
begin
while Value >= 1 do begin
ReturnValue := Format(Value MOD 2) + ReturnValue;
Value := Value DIV 2;
end;
end;

local procedure BinaryToInt(Value: Text) ReturnValue: Integer;
var
Multiplier: BigInteger;
IntValue: Integer;
i: Integer;
begin
Multiplier := 1;
for i := StrLen(Value) downto 1 do begin
Evaluate(IntValue, CopyStr(Value, i, 1));
ReturnValue += IntValue * Multiplier;
Multiplier *= 2;
end;
end;

local procedure IncreaseStringLength(Value: Text; ToLength: Integer) ReturnValue: Text;
var
ExtraLength: Integer;
ExtraText: Text;
begin
ExtraLength := ToLength - StrLen(Value);

if ExtraLength < 0 then
exit;

ExtraText := PadStr(ExtraText, ExtraLength, '0');
ReturnValue := ExtraText + Value;
end;

local procedure GetBase64Char(Value: Integer): text;
var
chars: text;
i: Integer;
begin
chars := Base64Chars;
exit(chars[Value + 1]);
end;

local procedure GetBase64Number(Value: text): Integer;
var
chars: text;
begin
if Value = '=' then
exit(0);

chars := Base64Chars;
exit(StrPos(chars, Value) - 1);
end;

local procedure Base64Chars(): text;
begin
exit('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/');
end;

}