mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-05-07 07:32:45 +02:00
1011 lines
40 KiB
ObjectPascal
1011 lines
40 KiB
ObjectPascal
unit uexampledata;
|
|
|
|
{
|
|
**********************************************************************
|
|
This file is part of a Lazarus Package, Examples Window.
|
|
|
|
See the file COPYING.modifiedLGPL.txt, included in this distribution,
|
|
for details about the license.
|
|
**********************************************************************
|
|
|
|
This unit is the backend that provides an List that contains details of Lazarus
|
|
Example Projects. It might get its data from one of three different places,
|
|
|
|
* The LazarusDir and the LazarusConfigDir.
|
|
* A locally cached master meta file Disabled as of Feb 2022
|
|
* A remote gitlab repository (ie, if the above is not present), Disabled as of Feb 2022
|
|
|
|
This list can be used to populate the Lazarus Examples Window or used during the
|
|
markup of existing Lazarus Projects. The unit is used by the Lazarus Package and
|
|
a simple tool used to manage the meta data files.
|
|
|
|
-- PATHS --
|
|
|
|
This only really applies in the Out of Lazarus Package usage. David Bannon, Feb 2022
|
|
|
|
Data is inserted into the list from different sources and might refer to
|
|
content stored in different places.
|
|
|
|
So, wrt FFname in the list, a path starting with a slash, / or \, is an absolute
|
|
local path. OTOH, without a slash, its remote, eg, gitlab and relative to the
|
|
top of the repository.
|
|
|
|
Special case is when we are reading the local git repository, we are doing this
|
|
to make a file to upload to the gilab repo that is an index of the remote repository,
|
|
so, no leading slash and all paths are relative to the top of the local git repo.
|
|
|
|
This unit does not interact directly with user but it does (hopefully not often)
|
|
generate some error messages that may need i18n. Only network errors have been done.
|
|
|
|
|
|
}
|
|
|
|
{$mode ObjFPC}{$H+}
|
|
|
|
interface
|
|
|
|
uses Classes, SysUtils, fpjson, jsonparser ;
|
|
|
|
const MetaFileExt = '.ex-meta';
|
|
|
|
|
|
type TExampleDataSource = ( FromGitlabTree, // Read all remote project meta files
|
|
FromLocalTree, // Read all local Git project meta files
|
|
FromCacheFile, // Load data from Local Cache File
|
|
FromLazSrcTree); // Searches the Lazarus Src Tree, eg ~/examples; ~/components
|
|
|
|
type
|
|
PExRec=^TExRec;
|
|
TExRec = record
|
|
EName : string; // CamelCase version of last part of FFName
|
|
Category : string; // eg Beginner, NoDesign (read from remote data)
|
|
Keywords : TStringList; // a list of (possibly multi-word) words
|
|
FFName : string; // Path and filename of meta file. Maybe absolute or relative
|
|
Desc : string; // 1..many lines of description
|
|
end;
|
|
|
|
type
|
|
{ TExampleList }
|
|
TExampleList = class(TFPList)
|
|
private
|
|
|
|
procedure DumpList(wherefrom: string; ShowDesc: boolean = false);
|
|
function Get(Index: integer): PExRec;
|
|
|
|
public
|
|
constructor Create();
|
|
destructor Destroy; override;
|
|
// Public - Puts new entry in List, Keys may be Nil
|
|
function InsertData(Cat, Desc, FFName, AName: string; Keys: TStringList): boolean;
|
|
function Find(const FFname: string): PExRec;
|
|
function AsJSON(Index: integer): string;
|
|
// Ret T if St is in Keywords at AnIndex, not necessarily equal to.
|
|
function IsInKeywords(St : string; AnIndex : integer) : boolean;
|
|
property Items[Index: integer]: PExRec read Get; default;
|
|
|
|
|
|
end;
|
|
|
|
{ Note - the above list is used to generate a master.ex-meta file that might be added
|
|
the the gitlab repo. So, dir seperators MUST be /. On Windows, they will be read
|
|
from a local tree as \ and a local master.ex-meta file will need to be converted.
|
|
I think we will declare they are always /, when reading local filesystems on
|
|
Windows, must convert during the insert into list stage. }
|
|
|
|
|
|
type
|
|
|
|
{ TExampleData }
|
|
|
|
TExampleData = class
|
|
private
|
|
ErrorString : String;
|
|
ExList : TExampleList;
|
|
GetListDataIndex : integer;
|
|
LazConfigDir : string; // dir (eg OPM) under which we might find more Examples
|
|
// Gets a Full URL and returns with St containing content, usually as JSON
|
|
function Downloader(URL: string; out SomeString: String): boolean;
|
|
// Does a binary safe download of a file, URL will get repositary info prepended
|
|
// and file ends up in FullDest which should be a full path and filename.
|
|
function DownLoadFile(const URL, FullDest: string): boolean;
|
|
//function EscJSON(InStr: string): string;
|
|
function ExtractArrayFromJSON(const Field: string; jItem: TJSONData; STL: TStringList): boolean;
|
|
// Passed a json block, returns the indicated field, cannot handle arrays.
|
|
// Don't rely on its base64 decoding a binary file, see DownLoadFile() instead.
|
|
function ExtractFromJSON(const Field, data: string; Base64: boolean=false) : string;
|
|
function ExtractFromJSON(const Field: string; const jItem: TJSONData; out
|
|
Res: string; Base64: boolean = false): boolean;
|
|
function GetLazDir: string;
|
|
|
|
// The returned date string down to seconds includes time zone in ISO8601
|
|
// eg 2022-01-09T11:56:51+11:00
|
|
function GetLocalTime: ANSIstring;
|
|
|
|
// Receives a pretested JSON (not just a field) containing metadata of an Example
|
|
// Returns false if data missing, drops msg to console about bad field.
|
|
// Path may be relative or absolute (ie starting with '/' or '\'). Ones without
|
|
// a leading slash are remote, ie gitlab. Ones with a slash should be resolable
|
|
// locally. Note when indexing a local git tree, relative must be used, ie top of
|
|
// git tree. In this mode of course, the entry will not be resolvable locally.
|
|
function InsertJSONData(jItem: TJSONData; FFName: string; AName: string = '' ): boolean;
|
|
function LoadCacheFile(FFName: string): boolean;
|
|
function ReadMasterJSON(FileContent: TStringList): boolean;
|
|
function ReadRemoteMetaFile(URL: string): boolean; // download and read meta file
|
|
// Gets passed a block of json, wrapped in {} containing several fields relating
|
|
// one example project. Path is ready to use in the List. Not suited to json
|
|
// With an internal Path field (ie master.ex-meta)
|
|
function ReadSingleJSON(FileContent: TStringList; PathToStore: string = ''): boolean;
|
|
function ScanLocalTree(Path: string; PathAbs: boolean): boolean;
|
|
// Will either scan and add what it finds to the List (if STL is nil) or it
|
|
// will add each full URL to the StringList if its valid and created.
|
|
function ScanRemoteTree(Path: string; STL: TstringList = nil): boolean;
|
|
function ScanOneTree(Path: string; out St: string): boolean;
|
|
procedure fSetErrorString(Er : string);
|
|
|
|
function WriteMasterMeta(FFileName: string): boolean;
|
|
|
|
public
|
|
RemoteRepo : string; // eg https://gitlab.com/api/v4/projects/32480729/repository/
|
|
|
|
ExamplesHome : string; // dir where we will save a working copy of examples too, usually LazConfigDir
|
|
LazSrcDir : string; // Laz dir where, eg ~/examples lives
|
|
GitDir : string; // where we look for a local git repo containg examples
|
|
KeyFilter : string; // A list of words, possibly grouped by " to filter Keywords
|
|
CatFilter : string; // A string that may contain 0 to n words, each word being a category as filtered by GetListData()
|
|
// A service function, tests passed St to ensure its
|
|
// a valid lump of Example Meta Data.
|
|
function TestJSON(const J: string; out Error, Cat: string): boolean;
|
|
// Public, returns with next set of data, false if no more available.
|
|
// Filters using CatFilter if CatFilter is not empty.
|
|
// If passed KeyList is not nil, filters keywords against KeyList.
|
|
function GetListData(out Proj, Cat, Path, Keys: string; GetFirst: boolean;
|
|
KeyList: TStringList = nil): boolean;
|
|
// Passed a created TStrings that it clears and fills in with all know categories
|
|
function getCategoryData(const CatList : TStrings) : boolean;
|
|
function GetDesc(const FFname: string): string;
|
|
constructor Create;
|
|
procedure LoadExData(DataSource: TExampleDataSource);
|
|
destructor Destroy; override;
|
|
procedure DumpExData();
|
|
// A service method, called by the GUI to download a project/
|
|
// Pass it a full example remote dir (eg Beginner/Laz_Hello/).
|
|
function DownLoadDir(const FExampDir: string): boolean;
|
|
function Count : integer;
|
|
function MasterMeta(DirOnly: boolean = false): string; // returns the full Master Metafile name
|
|
function ExtractFieldsFromJSON(const JStr: string; out EName, Cat, Keys, Desc,
|
|
Error: string): boolean;
|
|
property ErrorMsg : string read ErrorString write FSetErrorString;
|
|
class function EscJSON(InStr: string): string;
|
|
end;
|
|
|
|
|
|
implementation
|
|
|
|
|
|
uses LCLProc,
|
|
uConst,
|
|
httpprotocol, // for http encoding
|
|
fphttpclient, // determines a dependency on FPC 3.2.0 or later. Must for https downloads
|
|
opensslsockets,
|
|
ssockets, fpopenssl,
|
|
lazfileutils, fileutil,
|
|
jsonscanner, // these are the FPC JSON tools
|
|
base64
|
|
, laz2_DOM, laz2_XMLRead // just to get LazarusDirectory, remove if we find a better way !
|
|
{$ifdef LINUX},Unix {$endif} // We call a ReReadLocalTime();
|
|
{, IDEOptionsIntf}, LazIDEIntf;
|
|
|
|
const
|
|
LastUpDate = 'LastUpDate'; // Name of JSON item were we store last update
|
|
|
|
{ A URL starts with eg 'https://gitlab.com/api/v4/projects/32480729/repository/'
|
|
It contains a multidigit number that identifies the gitlab project. The number is a
|
|
combination of Owner (account, group..) and repository name. Its identified in Gitlab
|
|
web pages as "Project ID", group id will not work. A full URL might look like this -
|
|
https://gitlab.com/api/v4/projects/32866275/repository/files/Utility%2FExScanner%2Fproject1.ico?ref=main
|
|
}
|
|
|
|
|
|
// =============================================================================
|
|
// T E X A M P L E L I S T
|
|
//==============================================================================
|
|
|
|
function TExampleList.Get(Index: integer): PExRec;
|
|
begin
|
|
Result := PExRec(inherited get(Index));
|
|
end;
|
|
|
|
function TExampleList.InsertData(Cat, Desc, FFName, AName : string; Keys: TStringList): boolean;
|
|
var
|
|
ExRecP : PExRec;
|
|
begin
|
|
ExRecP := find(FFName);
|
|
new(ExRecP);
|
|
ExRecP^.Category := Cat;
|
|
ExRecP^.KeyWords := Keys; // Nil is acceptable
|
|
ExRecP^.Desc := Desc;
|
|
ExRecP^.FFName := FFName;
|
|
ExRecP^.EName := AName;
|
|
result := (inherited Add(ExRecP) > -1);
|
|
end;
|
|
|
|
// Returns an unquoted string being one JSON Escaped record from list.
|
|
function TExampleList.AsJSON(Index : integer) : string; // Not used, maybe remove ? Or Add in EName
|
|
begin
|
|
Result := '';
|
|
Result := Result + 'Category : ' + Items[Index]^.Category + #10;
|
|
Result := Result + 'Keywords : ' + Items[Index]^.Keywords.Text + #10#10;
|
|
Result := Result + Items[Index]^.Desc;
|
|
Result := Result.Replace('\', '\\', [rfReplaceAll] );
|
|
Result := Result.Replace('"', '\"', [rfReplaceAll] );
|
|
end;
|
|
|
|
function TExampleList.IsInKeywords(St: string; AnIndex: integer): boolean;
|
|
var KeyWord : String;
|
|
begin
|
|
result := false;
|
|
if pos(lowercase(St), lowercase(Items[AnIndex]^.EName)) > 0 then exit(true);
|
|
for KeyWord in Items[AnIndex]^.Keywords do begin
|
|
if pos(lowercase(St), lowercase(Keyword)) > 0 then exit(True);
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TExampleList.DumpList(wherefrom: string; ShowDesc : boolean = false); // ToDo : remove this, its just a debug method
|
|
var
|
|
i : integer = 0;
|
|
begin
|
|
DebugLn('-------- ExampleData Examples List ' + Wherefrom + '----------');
|
|
while i < count do begin
|
|
DebugLn('<<<< List - FFName=[' + Items[i]^.FFName +'] Cat=[' + Items[i]^.Category
|
|
+ '] EName=' + Items[i]^.EName
|
|
+ '] Key=[' + Items[i]^.Keywords.Text + ']');
|
|
if ShowDesc then
|
|
DebugLn(Items[i]^.Desc);
|
|
inc(i);
|
|
end;
|
|
end;
|
|
|
|
constructor TExampleList.Create();
|
|
begin
|
|
inherited Create;
|
|
end;
|
|
|
|
destructor TExampleList.Destroy;
|
|
var
|
|
i : integer;
|
|
begin
|
|
for I := 0 to Count-1 do begin
|
|
if Items[i]^.Keywords <> nil then
|
|
Items[i]^.Keywords.free;
|
|
dispose(Items[i]);
|
|
end;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
function TExampleList.Find(const FFname: string): PExRec;
|
|
var
|
|
i : integer = 0;
|
|
begin
|
|
while i < count do begin
|
|
if Items[i]^.FFname = FFname then
|
|
exit(Items[i]);
|
|
inc(i);
|
|
end;
|
|
Result := nil;
|
|
end;
|
|
|
|
// =============================================================================
|
|
// T E X A M P L E D A T A
|
|
// =============================================================================
|
|
|
|
|
|
function TExampleData.Count: integer;
|
|
begin
|
|
result := ExList.Count;
|
|
end;
|
|
|
|
procedure TExampleData.fSetErrorString(Er : string);
|
|
begin
|
|
ErrorString := Er;
|
|
Debugln(ErrorString);
|
|
end;
|
|
|
|
// Rets a path to where we will putting our downloaded or copied ex projects.
|
|
// At present, this is the <lazconfig>/downloaded_examples/
|
|
// if not true, returns the FFName of the master meta file, same place.
|
|
function TExampleData.MasterMeta(DirOnly : boolean = false) : string;
|
|
begin
|
|
//result := LazConfigDir + cExamplesDir + pathdelim;
|
|
result := AppendPathDelim(ExamplesHome);
|
|
if not DirOnly then
|
|
result := Result + 'master' + MetaFileExt;
|
|
end;
|
|
|
|
function TExampleData.ExtractFieldsFromJSON(const JStr: string; out EName, Cat,
|
|
Keys, Desc, Error: string): boolean;
|
|
var
|
|
jData, jItem : TJSONData;
|
|
STL : TStringList;
|
|
St : string;
|
|
begin
|
|
Error := '';
|
|
result := TestJSON(JStr, Error, Cat);
|
|
if Not Result then exit(False); // some basic tests done, so
|
|
jData := GetJSON(JStr); // we know these 2 lines are safe.
|
|
jItem := jData.Items[0];
|
|
STL := TStringList.Create;
|
|
Result := False;
|
|
try
|
|
if not ExtractFromJSON('Description', jItem, Desc) then begin
|
|
Desc := '';
|
|
end;
|
|
Keys := '';
|
|
if ExtractArrayFromJSON('Keywords', JItem, StL) then begin
|
|
for St in STL do
|
|
Keys := Keys + '"' + ST + '",';
|
|
if Keys.length > 1 then
|
|
delete(Keys, Keys.Length, 1);
|
|
end;
|
|
EName := TJSONObject(jData).Names[0];
|
|
Result := True;
|
|
finally
|
|
STL.Free;
|
|
JData.Free;
|
|
end;
|
|
end;
|
|
|
|
function TExampleData.TestJSON(const J : string; out Error, Cat : string) : boolean;
|
|
var
|
|
jData, jItem : TJSONData;
|
|
begin
|
|
Result := true;
|
|
if (J.Length = 0) or (J[1] <> '{') then begin // Ignore obvious non JSON
|
|
Error := 'Empty text or does not start with {';
|
|
exit(False)
|
|
end;
|
|
try
|
|
try
|
|
jData := GetJSON(J); // Is it valid JSON ?
|
|
jItem := jData.Items[0];
|
|
except
|
|
on E: EJSONParser do begin
|
|
Error := 'ERROR Parsing- invalid JSON ' + E.Message;
|
|
jData := Nil; // Appears nothing is allocated on error ?
|
|
exit(false);
|
|
end;
|
|
on E: EScannerError do begin
|
|
Error := 'ERROR Scanning- invalid JSON ' + E.Message;
|
|
jData := Nil; // Appears nothing is allocated on error ?
|
|
exit(false);
|
|
end;
|
|
end;
|
|
|
|
if TJSONObject(jItem).Count = 0 then begin
|
|
Error := 'WARNING - file does not contain suitable JSON : ';
|
|
exit(false);
|
|
end;
|
|
if not ExtractFromJSON('Category', jItem, Cat) then begin
|
|
Error := 'WARNING - Category Not Set ';
|
|
exit(false);
|
|
end;
|
|
finally
|
|
jData.free;
|
|
end;
|
|
end;
|
|
|
|
// jItem never contains Project Path, its either found in json Name (master)
|
|
// or derived from where we found the project (individual). So, always passed here.
|
|
function TExampleData.InsertJSONData(jItem : TJSONData; FFName : string; AName : string = ''): boolean;
|
|
var
|
|
Cat, Desc, AnotherName : String;
|
|
// index : integer;
|
|
KeyWords : TStringList;
|
|
begin
|
|
ExtractFromJSON('Category', jItem, Cat); // An empty Cat is acceptable but undesirable.
|
|
if not ExtractFromJSON('Description', jItem, Desc) then exit(False);
|
|
KeyWords := TStringList.Create;
|
|
ExtractArrayFromJSON('Keywords', jItem, Keywords);
|
|
if AName <> '' then
|
|
AnotherName := AName
|
|
else
|
|
if not ExtractFromJSON('Name', jItem, AnotherName) then
|
|
AnotherName := '';
|
|
Result := ExList.InsertData(Cat, Desc, FFName, AnotherName, KeyWords);
|
|
if not Result then KeyWords.Free; // false means its not gone into list so our responsibility go free
|
|
end;
|
|
|
|
// Scans local tree below 'Path' looking for any likely Example Metadata files.
|
|
// For each, it loads content into a StringList and passes it to an Insert method.
|
|
// If AddPath, the full path is inserted, not just the relative one, eg extra dirs
|
|
function TExampleData.ScanLocalTree(Path : string; PathAbs : boolean) : boolean;
|
|
var
|
|
STL : TStringList = nil;
|
|
FileContent : TStringList;
|
|
St : string;
|
|
begin
|
|
STL := FindAllFiles(Path, '*' + MetaFileExt, True);
|
|
try
|
|
for St in STL do begin
|
|
//debugln('TExampleData.ScanLocalTree 1 Looking at ' + St);
|
|
if pos('master' + MetaFileExt, St) > 0 then continue; // don't do master if you stumble across one
|
|
if pos(cExamplesDir, St) > 0 then continue; // thats our downloaded location
|
|
FileContent := TStringList.Create;
|
|
FileContent.LoadFromFile(St); // Thats contents of one individual metadata file
|
|
try
|
|
if PathAbs then
|
|
Result := ReadSingleJSON(FileContent, St)
|
|
else Result := ReadSingleJSON(FileContent, copy(St, Path.Length+1, 1000));
|
|
if not Result then begin
|
|
debugln('Offending file is ' + St);
|
|
debugln(ErrorMsg);
|
|
exit(False);
|
|
end;
|
|
finally
|
|
FileContent.Free;
|
|
end;
|
|
end;
|
|
finally
|
|
STL.Free;
|
|
end;
|
|
end;
|
|
|
|
|
|
function TExampleData.ReadSingleJSON(FileContent : TStringList; PathToStore : string = '') : boolean;
|
|
var
|
|
jData, jItem : TJSONData;
|
|
begin
|
|
Result := true;
|
|
if (FileContent.Count > 0) and (FileContent[0][1] = '{') then begin // Ignore obvious non JSON
|
|
try
|
|
try
|
|
jData := GetJSON(FileContent.Text); // Is it valid JSON ?
|
|
jItem := jData.Items[0];
|
|
except
|
|
on E: EJSONParser do begin
|
|
ErrorMsg := 'ERROR EJSONParser- invalid JSON ' + E.Message;
|
|
jData := Nil; // Appears nothing is allocated if error ?
|
|
exit(false);
|
|
end;
|
|
on E: EScannerError do begin // Thats in jsonscanner unit, Must doc on Wiki !!!
|
|
ErrorMsg := 'ERROR EScanner- invalid JSON ' + E.Message; // this is typically a single \
|
|
jData := Nil; // Appears nothing is allocated if error ?
|
|
exit(false);
|
|
end;
|
|
end;
|
|
if TJSONObject(jItem).Count = 0 then begin
|
|
debugln('WARNING - file does not contain suitable JSON : ');
|
|
exit(false);
|
|
end;
|
|
InsertJSONData(jItem, PathToStore, TJSONObject(jData).Names[0]);
|
|
finally
|
|
jData.free;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
destructor TExampleData.Destroy;
|
|
begin
|
|
ExList.free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TExampleData.DumpExData; // ToDo : remove this, just a debug thingo
|
|
begin
|
|
ExList.DumpList('TExampleData.Dump', True);
|
|
end;
|
|
|
|
constructor TExampleData.Create();
|
|
begin
|
|
ExList := TExampleList.Create;
|
|
LazConfigDir := appendPathDelim(LazarusIDE.GetPrimaryConfigPath);
|
|
end;
|
|
|
|
procedure TExampleData.LoadExData(DataSource: TExampleDataSource);
|
|
begin
|
|
// If we are loading the data from either the remote gitlab tree or a local
|
|
// git tree, we save the master file.
|
|
if not DirectoryExists(MasterMeta(True)) then
|
|
if not ForceDirectory(MasterMeta(True)) then exit;
|
|
case DataSource of
|
|
FromGitLabTree : begin // too slow to be useful
|
|
ScanRemoteTree('');
|
|
WriteMasterMeta('master' + MetaFileExt); // save in working dir
|
|
end;
|
|
FromLocalTree : begin // not used in Lazarus Package
|
|
if ScanLocalTree(GitDir, False) then // This should leave relative paths, suitable to upload to gitlab
|
|
WriteMasterMeta(GitDir + 'master' + MetaFileExt); // save in git tree ready to upload.
|
|
end;
|
|
FromLazSrcTree : begin
|
|
ScanLocalTree(GetLazDir(), True); // Scan the Lazarus SRC tree
|
|
ScanLocalTree(LazConfigDir, True); // Get, eg, any OPM Examples
|
|
end;
|
|
FromCacheFile : begin
|
|
if not LoadCacheFile(MasterMeta()) then begin
|
|
DownLoadFile('master' + MetaFileExt, MasterMeta());
|
|
LoadCacheFile(MasterMeta()); // ToDo : Test that worked
|
|
end;
|
|
ScanLocalTree(LazConfigDir, True); // Get, eg, any OPM Examples
|
|
end;
|
|
end;
|
|
// if ExList.Count = 0 then begin
|
|
debugln('TExampleData.LoadExData - found examples = ' + inttostr(ExList.Count));
|
|
debugln('Lazarus Dir (ie source tree) = ' + GetLazDir());
|
|
debugln('Lazarus Config Dir = ' + LazConfigDir);
|
|
debugln('Examples Home Dir = ' + ExamplesHome);
|
|
// end;
|
|
end;
|
|
|
|
|
|
// ****************** Local master meta File methods ***************************
|
|
|
|
function TExampleData.ReadMasterJSON(FileContent : TStringList) : boolean;
|
|
var
|
|
jData, jItem : TJSONData;
|
|
i : integer;
|
|
begin
|
|
Result := true;
|
|
|
|
if (FileContent.Count > 0) and (FileContent[0][1] = '{') then begin // Ignore obvious non JSON
|
|
try
|
|
try
|
|
jData := GetJSON(FileContent.Text); // Is it valid JSON ?
|
|
except
|
|
on E: EJSONParser do begin
|
|
ErrorMsg := 'ERROR EJSONParser - invalid JSON ' + E.Message;
|
|
jData := Nil; // Appears nothing is allocated if error ?
|
|
exit(false);
|
|
end;
|
|
on E: EScannerError do begin
|
|
ErrorMsg := 'ERROR EScannerError - invalid JSON ' + E.Message;
|
|
jData := Nil; // Appears nothing is allocated if error ?
|
|
exit(false);
|
|
end;
|
|
end;
|
|
for i := 0 to jData.Count-1 do begin // check its real JSON, not just a field.
|
|
jItem := jData.Items[i]; // do not free.
|
|
if TJSONObject(jItem).Count > 0 then begin // might be ...
|
|
InsertJSONData(jItem, TJSONObject(jData).Names[i]);
|
|
end;
|
|
end;
|
|
finally
|
|
freeandnil(jData);
|
|
end;
|
|
end else result := False;
|
|
end;
|
|
|
|
function TExampleData.LoadCacheFile(FFName : string) : boolean;
|
|
var
|
|
FileContent : TStringList;
|
|
begin
|
|
if not FileExists(FFName) then exit(False);
|
|
FileContent := TStringList.Create;
|
|
try
|
|
FileContent.LoadFromFile(FFname);
|
|
Result := ReadMasterJSON(FileContent);
|
|
if not Result then
|
|
debugln('Offending file is ' + FFName);
|
|
finally
|
|
FileContent.Free;
|
|
end;
|
|
Result := true;
|
|
end;
|
|
|
|
{ environmentoptions.xml
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<CONFIG>
|
|
<EnvironmentOptions>
|
|
...
|
|
<LazarusDirectory Value="/home/dbannon/bin/Lazarus/lazarus-main"> .... }
|
|
|
|
|
|
function TExampleData.GetLazDir() : string; // Todo : make direct call
|
|
var
|
|
Doc : TXMLDocument;
|
|
Node, Node1 : TDOMNode;
|
|
begin
|
|
Result := '';
|
|
ReadXMLFile(Doc, LazConfigDir + 'environmentoptions.xml');
|
|
Node1 := Doc.DocumentElement.FindNode('EnvironmentOptions');
|
|
if Node1 <> nil then begin
|
|
Node := Node1.FindNode('LazarusDirectory');
|
|
if Node <> nil then
|
|
Result := AppendPathDelim(Resolvedots(Node.Attributes.GetNamedItem('Value').NodeValue));
|
|
// Apparently sometimes Lazarus puts a relative path in envopts.xml - danger here is
|
|
// that if Lazarus now has a working dir different from when the path was written, it
|
|
// will be wrong anyway. Further research is indicated.
|
|
end;
|
|
Doc.free;
|
|
debugln('TExampleData.GetLazDir = ' + Result);
|
|
end;
|
|
|
|
function TExampleData.GetLocalTime: ANSIstring;
|
|
var
|
|
ThisMoment : TDateTime;
|
|
Res : ANSIString;
|
|
Off : longint;
|
|
begin
|
|
{$ifdef LINUX}
|
|
ReReadLocalTime(); // in case we are near daylight saving time changeover
|
|
{$endif}
|
|
ThisMoment:=Now;
|
|
Result := FormatDateTime('YYYY-MM-DD',ThisMoment) + 'T'
|
|
+ FormatDateTime('hh:mm:ss',ThisMoment);
|
|
Off := GetLocalTimeOffset();
|
|
if (Off div -60) >= 0 then Res := '+'
|
|
else Res := '-';
|
|
if abs(Off div -60) < 10 then Res := Res + '0';
|
|
Res := Res + inttostr(abs(Off div -60)) + ':';
|
|
if (Off mod 60) = 0 then
|
|
Res := res + '00'
|
|
else Res := Res + inttostr(abs(Off mod 60));
|
|
Result := Result + res;
|
|
end;
|
|
|
|
class function TExampleData.EscJSON(InStr : string) : string;
|
|
begin
|
|
Result := InStr.Replace('\', '\\', [rfReplaceAll]);
|
|
Result := Result.Replace('"', '\"', [rfReplaceAll]);
|
|
Result := Result.Replace(#10, '\n', [rfReplaceAll] ); // LF
|
|
Result := Result.Replace(#13, '', [rfReplaceAll] ); // CR
|
|
Result := Result.Replace(#09, '', [rfReplaceAll] ); // tab
|
|
end;
|
|
|
|
function TExampleData.WriteMasterMeta(FFileName : string) : boolean;
|
|
var
|
|
i : integer;
|
|
STL : TStringList;
|
|
St, StIndexed : string;
|
|
begin
|
|
STL := TStringList.Create;
|
|
StL.Add('{'#10'"' + LastUpDate + '":"' + GetLocalTime() +'",');
|
|
|
|
for i := 0 to ExList.Count-1 do begin
|
|
StL.Add('"' + EscJSON(ExList.Items[i]^.FFname) + '" : {'); // Must be unique
|
|
StL.Add(' "Name" : "' + EscJSON(ExList.Items[i]^.EName) + '",');
|
|
StL.Add(' "Category" : "' + EscJSON(ExList.Items[i]^.Category) + '",');
|
|
St := '';
|
|
for StIndexed in ExList.Items[i]^.Keywords do
|
|
St := St + '"' + StIndexed + '",';
|
|
if St.Length > 0 then delete(St, St.Length, 1); // Remove trailing comma
|
|
StL.Add(' "Keywords" : [' + St + '],');
|
|
StL.Add(' "Description" : "' + EscJSON(ExList.Items[i]^.Desc) + '"},');
|
|
end;
|
|
if STL.Count > 1 then begin
|
|
St := STL[STL.Count-1];
|
|
delete(St, St.Length, 1);
|
|
STL[STL.Count-1] := St;
|
|
end;
|
|
Stl.Add('}');
|
|
deletefile(FFileName); // ToDo : test its there first and then test delete worked
|
|
STL.SaveToFile(FFileName);
|
|
STL.Free;
|
|
Result := fileexists(FFileName);
|
|
end;
|
|
|
|
|
|
// ******************** Methods relating to using the data *******************
|
|
|
|
function TExampleData.GetListData(out Proj, Cat, Path, Keys : string;
|
|
GetFirst: boolean; KeyList : TStringList = nil): boolean;
|
|
var
|
|
St : string;
|
|
DoContinue : boolean = false;
|
|
begin
|
|
//KeyList may be Nil, if so, ignore
|
|
Result := True;
|
|
if CatFilter = '' then exit(False);
|
|
if GetFirst then
|
|
GetListDataIndex := 0;
|
|
while True do begin
|
|
if GetListDataIndex >= ExList.Count then exit(False);
|
|
if CatFilter <> '' then begin // Find an entry in one of the categories
|
|
// orig a while instead of if, needed to use DoContinue ... Why ?
|
|
if pos(ExList.Items[GetListDataIndex]^.Category, CatFilter) < 1 then begin
|
|
inc(GetListDataIndex);
|
|
continue;
|
|
end;
|
|
end;
|
|
if KeyList <> Nil then begin
|
|
for St in KeyList do
|
|
// IndexOf requires a 1:1 match, we want to know if St is in the keyword.
|
|
//if ExList.Items[GetListDataIndex]^.Keywords.IndexOf(St) = -1 then begin
|
|
if not ExList.IsInKeywords(St, GetListDataIndex) then begin
|
|
inc(GetListDataIndex);
|
|
DoContinue := True;
|
|
Break;
|
|
end;
|
|
if DoContinue then begin // Hmm, a GoTo would be easier ......
|
|
DoContinue := False;
|
|
Continue;
|
|
end;
|
|
end;
|
|
break;
|
|
end;
|
|
Proj := ExList.Items[GetListDataIndex]^.EName;
|
|
Cat := ExList.Items[GetListDataIndex]^.Category;
|
|
Path := ExtractFilePath(ExList.Items[GetListDataIndex]^.FFname);
|
|
Keys := '';
|
|
for St in ExList.Items[GetListDataIndex]^.Keywords do
|
|
Keys := Keys + St + ' ';
|
|
inc(GetListDataIndex);
|
|
end;
|
|
|
|
function TExampleData.getCategoryData(const CatList: TStrings): boolean;
|
|
var
|
|
P : PExRec;
|
|
begin
|
|
if CatList = nil then exit(false);
|
|
CatList.Clear;
|
|
for P in ExList do begin
|
|
if CatList.Indexof(P^.Category) < 0 then
|
|
CatList.Add(P^.Category);
|
|
end;
|
|
Result := True;
|
|
end;
|
|
|
|
// Passed the FFName, a combination of Path and Proj including '.ex-meta'.
|
|
function TExampleData.GetDesc(const FFname: string): string;
|
|
var
|
|
P : PExRec;
|
|
begin
|
|
for P in ExList do begin
|
|
if (lowercase(P^.FFname) = lowercase(FFname)+MetaFileExt) then begin // extension must remain lower case
|
|
exit(P^.Desc);
|
|
end;
|
|
end;
|
|
Result := '';
|
|
debugln('TExampleData.GetDesc - did not find Desc for ' + FFname);
|
|
debugln('Spelling of Name must match directory name (case insensitive)');
|
|
ExList.DumpList('TExampleData.GetDesc', True);
|
|
end;
|
|
|
|
|
|
// ************* Methods relating to getting REMOTE data *******************
|
|
|
|
function TExampleData.DownLoadDir(const FExampDir : string): boolean;
|
|
var
|
|
St : string;
|
|
STL : TStringlist;
|
|
begin
|
|
STL := TStringList.Create;
|
|
try
|
|
result := ScanRemoteTree(FExampDir, STL);
|
|
for St in STL do begin
|
|
if not DirectoryExistsUTF8(MasterMeta(True) + ExtractFileDir(St)) then
|
|
ForceDirectory(MasterMeta(True) + ExtractFileDir(St)); // ToDo : but that might fail
|
|
DownLoadFile(St, MasterMeta(True) + St);
|
|
end;
|
|
finally
|
|
STL.Free;
|
|
end;
|
|
end;
|
|
|
|
|
|
function TExampleData.DownLoadFile(const URL, FullDest : string) : boolean;
|
|
var
|
|
St, S : string;
|
|
MemBuffer : TMemoryStream;
|
|
DecodedStream : TMemoryStream;
|
|
Decoder : TBase64DecodingStream;
|
|
begin
|
|
if not Downloader(RemoteRepo + 'files/' + HTTPEncode(URL) + '?ref=main', St) then begin
|
|
ErrorMsg := 'TExampleData.ReadMetaFile - download FAILED ' + URL;
|
|
exit(false);
|
|
end;
|
|
S := ExtractFromJSON('content', St, False); // Bring it back still base64 encoded
|
|
MemBuffer := TMemoryStream.Create; // Speedups possible here. BuffStream ?
|
|
try
|
|
MemBuffer.Write(S[1], S.length);
|
|
membuffer.Position := 0;
|
|
DecodedStream := TMemoryStream.Create;
|
|
Decoder := TBase64DecodingStream.Create(MemBuffer);
|
|
try
|
|
DecodedStream.CopyFrom(Decoder, Decoder.Size);
|
|
DecodedStream.SaveToFile(FullDest); // Does not appear to benifit from TBufferedFileStream
|
|
except on E: EStreamError do
|
|
ErrorMsg := 'TExampleData.DownLoadFile - Error decoding ' + URL + ' ' + E.Message;
|
|
end;
|
|
finally
|
|
MemBuffer.Free;
|
|
DecodedStream.Free;
|
|
Decoder.Free;
|
|
end;
|
|
result := fileexists(FullDest);
|
|
end;
|
|
|
|
|
|
// Passed some json, returns the indicated field IFF its an arrays. The TStringList
|
|
// must have been created before being passed.
|
|
function TExampleData.ExtractArrayFromJSON(const Field : string; jItem : TJSONData; STL : TStringList) : boolean;
|
|
// ToDo : better to handle this with a set or array ? Once populated, it does not change
|
|
var
|
|
JObject : TJSONObject;
|
|
jArray : TJSONArray;
|
|
i : integer;
|
|
begin
|
|
result := true;
|
|
try
|
|
JObject := TJSONObject(jItem); // does not require a free
|
|
if jObject.Find(Field, JArray) then
|
|
for i := 0 to JArray.count -1 do
|
|
STL.Add(JArray.Items[i].asstring);
|
|
except
|
|
on E:Exception do begin
|
|
Result := False; // Invalid JSON or content not present
|
|
ErrorMsg := 'Exception while decoding JSON looking for ' + Field;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TExampleData.ExtractFromJSON(const Field, data : string; Base64 : boolean=false) : string;
|
|
var
|
|
JData : TJSONData;
|
|
JObject : TJSONObject;
|
|
jStr : TJSONString;
|
|
begin
|
|
result := '';
|
|
try
|
|
try
|
|
JData := GetJSON(Data); // requires a free
|
|
JObject := TJSONObject(jData); // does not require a free
|
|
if jObject.Find(Field, Jstr) then begin
|
|
if Base64 then
|
|
Result := DecodeStringBase64(jStr.AsString)
|
|
else Result := jStr.AsString;
|
|
end else ErrorMsg := 'Response has no ' + Field + ' field';
|
|
except
|
|
on E:Exception do begin
|
|
Result := ''; // Invalid JSON or content not present
|
|
ErrorMsg := 'Exception while decoding JSON looking for ' + Field;
|
|
end;
|
|
end;
|
|
finally
|
|
JData.Free;
|
|
end;
|
|
if Result = '' then debugln('ERROR, we did not find content in ' + Field);
|
|
end;
|
|
|
|
// Returns false if cannot parse passed jItem, thats not necessarily an error,
|
|
// Path will not be here if reading individual metadata files.
|
|
// If it is an error, ErrorString is set.
|
|
function TExampleData.ExtractFromJSON(const Field : string; const jItem : TJSONData;
|
|
out Res : string; Base64 : boolean=false) : boolean;
|
|
var
|
|
JObject : TJSONObject;
|
|
jStr : TJSONString;
|
|
begin
|
|
res := '';
|
|
try
|
|
JObject := TJSONObject(jItem); // does not require a free
|
|
if jObject.Find(Field, Jstr) then begin
|
|
if Base64 then
|
|
Res := DecodeStringBase64(jStr.AsString)
|
|
else Res := jStr.AsString;
|
|
end else if Field <> 'Path' then begin
|
|
ErrorMsg := 'Response has no ' + Field + ' field';
|
|
end;
|
|
except
|
|
on E:Exception do // Invalid JSON or content not present
|
|
ErrorMsg := 'Exception while decoding JSON looking for ' + Field;
|
|
end;
|
|
Result := (Res <> '');
|
|
end;
|
|
|
|
|
|
// Gets passed the RHS of URL of a metadata file, adds that content to list.
|
|
// eg Beginner/Laz_Hello/Laz_Hello.ex-meta
|
|
function TExampleData.ReadRemoteMetaFile(URL : string): boolean;
|
|
var
|
|
St : string;
|
|
StL : TStringList;
|
|
begin
|
|
if not Downloader(RemoteRepo + 'files/' + HTTPEncode(URL) + '?ref=main', St) then begin
|
|
ErrorMsg := 'TExampleData.ReadMetaFile - download FAILED';
|
|
exit(false);
|
|
end;
|
|
StL := TStringList.Create;
|
|
try
|
|
STL.Text := ExtractFromJSON('content', St, True); // get 'content' and decode base64
|
|
result := ReadSingleJSON(STL, URL);
|
|
if not Result then
|
|
debugln('Offending remote file is ' + URL);
|
|
finally
|
|
STL.Free;
|
|
end;
|
|
end;
|
|
|
|
// https://gitlab.com/api/v4/projects/32866275/repository/files/Utility/ExScanner/project1.ico?ref=main
|
|
// curl "https://gitlab.com/api/v4/projects/32866275/repository/files/Utility%2FExScanner%2Fproject1.ico?ref=main"
|
|
|
|
function TExampleData.ScanRemoteTree(Path : string; STL : TstringList = nil) : boolean;
|
|
// warning - recursive function.
|
|
var
|
|
St : string;
|
|
jData : TJSONData;
|
|
jObject : TJSONObject;
|
|
jArray : TJSONArray;
|
|
i : integer;
|
|
begin
|
|
ScanOneTree(Path, St);
|
|
jData := GetJSON(St);
|
|
jArray:=TJSONArray(jData);
|
|
for i:=0 to jArray.Count-1 do begin
|
|
jObject:= TJSONObject(jArray[i]);
|
|
if jObject.Find('type').AsString = 'tree' then // tree and blob are gitlab defines, in the download
|
|
ScanRemoteTree(jObject.Find('path').AsString, STL);
|
|
if (jObject.Find('type').AsString = 'blob') then begin // A blob is a usable file
|
|
if STL <> nil then
|
|
STL.add(jObject.Find('path').AsString)
|
|
else // OK, fill in List mode.
|
|
if (pos(MetaFileExt, jObject.Find('path').AsString) > 0) then begin
|
|
if pos('master' + MetaFileExt, jObject.Find('path').AsString) < 1 then // don't do master meta file
|
|
if STL = Nil then
|
|
ReadRemoteMetaFile(jObject.Find('path').AsString );
|
|
end;
|
|
end;
|
|
end;
|
|
jArray.Free;
|
|
Result := true;
|
|
end;
|
|
|
|
function TExampleData.ScanOneTree(Path : string; out St : string) : boolean; // needed
|
|
var
|
|
URL : string;
|
|
begin
|
|
if Path <> '' then
|
|
URL := RemoteRepo + 'tree?path=' + Path
|
|
else URL := RemoteRepo + 'tree';
|
|
Result := Downloader(URL, St);
|
|
end;
|
|
|
|
|
|
function TExampleData.Downloader(URL: string; out SomeString: String): boolean;
|
|
var
|
|
Client: TFPHTTPClient;
|
|
begin
|
|
// This is a dumb downloader, if you need auth then maybe look at transgithub in tomboy-ng
|
|
// Further, gitlab API seems quite slow, up to a second for an 80K icon file ??
|
|
// curl "https://gitlab.com/api/v4/projects/32866275/repository/files/Utility%2FExScanner%2Fproject1.ico?ref=main"
|
|
// curl does the same thing in a bit over half that time. Hmm....
|
|
Client := TFPHttpClient.Create(nil);
|
|
Client.AddHeader('User-Agent','Mozilla/5.0 (compatible; fpweb)');
|
|
Client.AddHeader('Content-Type','application/json; charset=UTF-8');
|
|
Client.AllowRedirect := true;
|
|
SomeString := '';
|
|
try
|
|
try
|
|
SomeString := Client.Get(URL);
|
|
except
|
|
on E: ESocketError do begin
|
|
ErrorMsg := rsExNetWorkError + ' ' + E.Message;
|
|
exit(false);
|
|
end;
|
|
on E: EInOutError do begin
|
|
ErrorMsg := rsExNetWorkError + ' InOut ' + E.Message;
|
|
exit(False);
|
|
end;
|
|
on E: ESSL do begin
|
|
ErrorMsg := rsExNetWorkError + ' SSL ' + E.Message;
|
|
exit(False);
|
|
end;
|
|
on E: Exception do begin // Following don't need i18n, we check they are there !
|
|
case Client.ResponseStatusCode of
|
|
401 : ErrorMsg := 'GitHub.Downloader Exception ' + E.Message
|
|
+ ' downloading ' + URL
|
|
+ ' 401 Maybe your Token has expired or password is invalid ??';
|
|
404 : ErrorMsg := 'GitHub.Downloader Exception ' + E.Message
|
|
+ ' downloading ' + URL + ' 404 File not found ' + URL;
|
|
end;
|
|
exit(false);
|
|
end;
|
|
end;
|
|
finally
|
|
Client.Free;
|
|
end;
|
|
result := true;
|
|
end;
|
|
|
|
end.
|
|
|