123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644 |
- {$IFNDEF FPC_DOTTEDUNITS}
- unit gitlabclient;
- {$ENDIF FPC_DOTTEDUNITS}
- {$mode ObjFPC}{$H+}
- {$modeswitch advancedrecords}
- interface
- {$IFDEF FPC_DOTTEDUNITS}
- uses
- System.Classes, System.SysUtils, System.IniFiles, FpJson.Data, FpWeb.Client, FpWeb.Client.Http, FpWeb.Http.Protocol;
- {$ELSE FPC_DOTTEDUNITS}
- uses
- Classes, SysUtils, inifiles, fpjson, fpwebclient, fphttpwebclient, httpprotocol;
- {$ENDIF FPC_DOTTEDUNITS}
- Const
- LongThrottleSleep = 60 * 1000; // One minute
- MaxSleepCount = 5; // max times to sleep when consecutive 429s
- SGitlabClient = 'Gitlab'; // Default section
- DefaultGitURL = 'https://gitlab.com/api/v4/';
- DefaultGitKey = ''; // API Key, default none
- DefaultProjectID = 0; // Gitlab Project ID, default none
- Type
- EGitLab = Class(Exception);
- { TGitlabConfig }
- TGitlabConfig = Record
- BaseURL : String;
- APIkey : String;
- ProjectID : Int64;
- Procedure Reset;
- Procedure LoadFromFile(const aFileName,aSection : String);
- Procedure LoadFromIni(aIni : TCustomInifile; const aSection : String);
- end;
- { TGitLabClient }
- TLogEvent = Procedure (Sender : TObject; Const aMessage : string) of object;
- TResourceCallback = procedure (Sender : TObject; aPage,aIndex,aCount : Integer; aObject : TJSONObject; aContinue : Boolean) of object;
- TGitLabClient = class(TObject)
- private
- FConfig: TGitlabConfig;
- FClient : TAbstractWebClient;
- FOnLog: TLogEvent;
- FSudo: String;
- procedure setconfig(AValue: TGitlabConfig);
- Protected
- procedure DoLog(const aMessage : string); overload;
- procedure DoLog(const aFmt : string; aArgs : Array of const); overload;
- function CreateRequest(aResult: TStream): TWebClientRequest;
- function CreateURL(aName: string; aParams: array of string; useSUDO : Boolean = False): String;
- procedure DoResourceRequest(aVerb, aName: String; aSrc: TStream; ADest: TStream; aContentType: String);
- procedure CreateResource(aName : String; aSrc : TStream; ADest : TStream; aContentType : String = '');
- procedure UpdateResource(aName : String; aSrc : TStream; ADest : TStream; aContentType : String = '');
- public
- Constructor Create; overload;
- Constructor Create(const aConfig : TGitlabConfig); overload;
- Destructor destroy; override;
- function GetProjectResourceURL(aResource: string): String;
- function GetResourceURL(aResource: string): String;
- // Upload file. URL is relative to baseURL, gets upload appended.
- // Return markdown
- Function UploadFile(const aURL,aLocalFileName,aRemoteFileName : String) : TJSONStringType;
- // Return JSON string
- function UploadFileRaw(const aURL,aLocalFileName,aRemoteFileName: string): TJSONStringType;
- // Return JSON Object
- Function UploadFileObject(const aURL,aLocalFileName,aRemoteFileName : String) : TJSONObject;
- // Create
- Function CreateResourceRaw(aName : String; aObj : TJSONObject) : TJSONStringType;
- Function CreateResourceObject(aName : String; aObj : TJSONObject) : TJSONObject;
- Function CreateResource(aName : String; aObj : TJSONObject) : Int64;
- // update
- Function UpdateResourceRaw(aName : String; aObj : TJSONObject) : TJSONStringType;
- Function UpdateResource(aName : String; aObj : TJSONObject) : Int64;
- Function UpdateResourceObject(aName : String; aObj : TJSONObject) : TJSONObject;
- // Get
- Function GetSingleResource(aName : String; aParams : array of string) : TJSONObject;
- Function GetResourceList(aName : String; aParams : array of string) : TJSONArray;
- Procedure GetResource(aName : String; aParams : array of string; aResult : TStream);
- Function ForEachResource(aResource : String; aParams : array of string; CallBack : TResourceCallback) : Integer;
- // Delete
- Procedure DeleteResource(aName : String);
- // Properties
- Property Config : TGitlabConfig Read FConfig write setconfig;
- Property OnLog : TLogEvent Read FOnLog Write FOnLog;
- // use SUDO
- Property Sudo : String Read FSudo Write FSudo;
- end;
- implementation
- { TGitlabConfig }
- procedure TGitlabConfig.Reset;
- begin
- BaseURL:=DefaultGitURL;
- APIKey:=DefaultGitKey;
- ProjectID:=DefaultProjectID;
- end;
- procedure TGitlabConfig.LoadFromFile(const aFileName, aSection: String);
- Var
- aIni : TMemIniFile;
- begin
- aIni:=TMemIniFile.Create(aFileName);
- try
- LoadFromIni(aIni,aSection);
- finally
- aIni.Free;
- end;
- end;
- procedure TGitlabConfig.LoadFromIni(aIni: TCustomInifile; const aSection: String);
- Var
- S : String;
- begin
- S:=aSection;
- if S='' then
- S:=SGitlabClient;
- BaseURL:=aIni.ReadString(S,'BaseURL',BaseURL);
- APIkey:=aIni.ReadString(S,'APIKey',APIKey);
- ProjectID:=aIni.ReadInt64(S,'ProjectID',ProjectID);
- end;
- constructor TGitLabClient.Create;
- begin
- FClient:=TFPHTTPWebClient.Create(Nil);
- end;
- constructor TGitLabClient.Create(const aConfig: TGitlabConfig);
- begin
- Create;
- Config:=aConfig;
- end;
- destructor TGitLabClient.destroy;
- begin
- FreeAndNil(FClient);
- inherited destroy;
- end;
- function TGitLabClient.UploadFile(const aURL, aLocalFileName,
- aRemoteFileName: String): TJSONStringType;
- Var
- Obj : TJSONObject;
- begin
- Obj:=UploadFileObject(aURL,aLocalFileName,aRemoteFilename);
- try
- Result:=Obj.Get('markdown','');
- finally
- Obj.Free;
- end;
- end;
- function TGitLabClient.UploadFileRaw(const aURL, aLocalFileName,
- aRemoteFileName: string): TJSONStringType;
- Const
- CRLF = #13#10;
- Var
- S, Sep : string;
- SS,SR : TRawByteStringStream;
- AStream : TFileStream;
- begin
- Sep:=Format('%.8x_multipart_boundary',[Random($ffffff)]);
- aStream:=Nil;
- SR:=Nil;
- SS:=TRawByteStringStream.Create('');
- try
- AStream:=TFileStream.Create(aLocalFileName,fmOpenRead);
- S:='--'+Sep+CRLF;
- s:=s+Format('Content-Disposition: form-data; name="%s"; filename="%s"'+CRLF,['file',aRemoteFileName]);
- s:=s+'Content-Type: application/octet-string'+CRLF+CRLF;
- SS.WriteBuffer(S[1],Length(S));
- AStream.Seek(0, soFromBeginning);
- SS.CopyFrom(AStream,AStream.Size);
- S:=CRLF+'--'+Sep+'--'+CRLF;
- SS.WriteBuffer(S[1],Length(S));
- SS.Position:=0;
- SR:=TRawByteStringStream.Create('');
- DoResourceRequest('POST',aURL,SS,SR,'multipart/form-data; boundary='+Sep);
- Result:=SR.DataString;
- finally
- SR.Free;
- SS.Free;
- aStream.Free;
- end;
- end;
- function TGitLabClient.UploadFileObject(const aURL, aLocalFileName,
- aRemoteFileName: String): TJSONObject;
- var
- aJSON : TJSONStringType;
- D : TJSONData;
- begin
- aJSON:=UploadFileRaw(aURL,aLocalFileName,aRemoteFileName);
- try
- D:=GetJSON(aJSON);
- Result:=D as TJSONObject;
- except
- on E : Exception do
- begin
- D.Free;
- E.Message:='Invalid JSON returned by upload of '+aLocalFileName+': '+E.Message;
- Raise;
- end;
- end;
- end;
- function TGitLabClient.CreateResourceRaw(aName: String; aObj: TJSONObject): TJSONStringType;
- Var
- Src,Dest : TStringStream;
- begin
- Dest:=nil;
- Src:=Nil;
- if Assigned(aObj) then
- Src:=TStringStream.Create(aObj.asJSON);
- try
- Dest:=TStringStream.Create('');
- CreateResource(aName,Src,Dest,'application/json');
- Result:=Dest.DataString;
- finally
- Src.Free;
- Dest.Free;
- end;
- end;
- function TGitLabClient.CreateResourceObject(aName: String; aObj: TJSONObject): TJSONObject;
- Var
- S : TJSONStringType;
- D : TJSONData;
- begin
- S:=CreateResourceRaw(aName,aObj);
- try
- D:=GetJSON(S);
- Result:=D as TJSONObject;
- except
- on E : Exception do
- begin
- D.Free;
- E.Message:='Invalid JSON returned by Create of '+aName+': '+E.Message;
- Raise;
- end;
- end;
- end;
- function TGitLabClient.CreateResource(aName: String; aObj: TJSONObject): Int64;
- Var
- Obj : TJSONObject;
- begin
- Obj:=CreateResourceObject(aName,aObj);
- try
- Result:=Obj.Get('id',Int64(-1));
- finally
- Obj.Free;
- end;
- end;
- function TGitLabClient.UpdateResourceRaw(aName: String; aObj: TJSONObject
- ): TJSONStringType;
- Var
- Src,Dest : TStringStream;
- begin
- Dest:=nil;
- Src:=TStringStream.Create(aObj.asJSON);
- try
- Dest:=TStringStream.Create('');
- UpdateResource(aName,Src,Dest,'application/json');
- Result:=Dest.DataString;
- finally
- Src.Free;
- Dest.Free;
- end;
- end;
- function TGitLabClient.UpdateResource(aName: String; aObj: TJSONObject): Int64;
- Var
- Obj : TJSONObject;
- begin
- Obj:=UpdateResourceObject(aName,aObj);
- try
- Result:=Obj.Get('id',Int64(-1));
- finally
- Obj.Free;
- end;
- end;
- function TGitLabClient.UpdateResourceObject(aName: String; aObj: TJSONObject
- ): TJSONObject;
- Var
- S : TJSONStringType;
- D : TJSONData;
- begin
- S:=UpdateResourceRaw(aName,aObj);
- try
- D:=GetJSON(S);
- Result:=D as TJSONObject;
- except
- on E : Exception do
- begin
- D.Free;
- E.Message:='Invalid JSON returned by Create of '+aName+': '+E.Message;
- Raise;
- end;
- end;
- end;
- function TGitLabClient.GetSingleResource(aName: String; aParams: array of string
- ): TJSONObject;
- Var
- S : TStream;
- D : TJSONData;
- begin
- D:=NIl;
- S:=TMemoryStream.Create;
- try
- GetResource(aName,aParams,S);
- try
- if S.Size>0 then
- D:=GetJSON(S);
- if (D<>Nil) and Not (D is TJSONObject) then
- Raise EGitlab.Create('Not a JSON object '+D.AsJSON);
- Result:=D as TJSONObject;
- except
- On E :Exception do
- E.Message:='Error getting resource'+aName+': '+E.Message;
- end;
- finally
- S.Free;
- end;
- end;
- function TGitLabClient.GetResourceList(aName: String; aParams: array of string
- ): TJSONArray;
- Var
- S : TStream;
- D : TJSONData;
- begin
- D:=NIl;
- S:=TMemoryStream.Create;
- try
- GetResource(aName,aParams,S);
- try
- D:=GetJSON(S);
- if Not (D is TJSONArray) then
- Raise EGitlab.Create('Not a JSON array '+D.AsJSON);
- Result:=D as TJSONArray;
- except
- On E :Exception do
- begin
- E.Message:='Error getting resource'+aName+': '+E.Message;
- Raise;
- end;
- end;
- finally
- S.Free;
- end;
- end;
- procedure TGitLabClient.setconfig(AValue: TGitlabConfig);
- begin
- FConfig:=AValue;
- end;
- procedure TGitLabClient.DoLog(const aMessage: string);
- begin
- If Assigned(FOnLog) then
- FOnLog(Self,aMessage);
- end;
- procedure TGitLabClient.DoLog(const aFmt: string; aArgs: array of const);
- begin
- DoLog(Format(aFmt,aArgs));
- end;
- function TGitLabClient.CreateRequest(aResult: TStream): TWebClientRequest;
- begin
- Result:=FClient.CreateRequest;
- Result.Headers.Values['Authorization']:='Bearer '+FConfig.APIkey;
- Result.ResponseContent:=aResult;
- end;
- function TGitLabClient.CreateURL(aName: string; aParams: array of string; useSUDO : Boolean = False): String;
- Var
- I : Integer;
- begin
- Result:=IncludeHTTPPathDelimiter(FConfig.BaseURL);
- Result:=Result+aName;
- if (Length(aParams) mod 2<>0) then
- Raise EGitLab.Create('URL Parameters must come in key=value pairs');
- I:=0;
- While I<Length(aParams)-1 do
- begin
- if I=0 then
- Result:=Result+'?'
- else
- Result:=Result+'&';
- Result:=Result+HTTPEncode(aParams[i])+'='+HTTPEncode(aParams[i+1]);
- inc(I,2);
- end;
- if UseSUDO and (Sudo<>'') then
- begin
- if Length(aParams)=0 then
- Result:=Result+'?'
- else
- Result:=Result+'&';
- Result:=Result+'sudo='+HTTPEncode(SUDO);
- end;
- end;
- procedure TGitLabClient.CreateResource(aName: String; aSrc: TStream;
- ADest: TStream; aContentType: String);
- begin
- DoResourceRequest('POST',aName,aSrc,aDest,aContentType);
- end;
- procedure TGitLabClient.DoResourceRequest(aVerb,aName: String; aSrc: TStream;
- ADest: TStream; aContentType: String);
- Function StreamToContent(S : TStream) : string;
- begin
- Result:='';
- if (S<>Nil) then
- With TStringStream.Create('') do
- try
- CopyFrom(S,0);
- Result:=DataString;
- S.Position:=0;
- finally
- Free;
- end;
- end;
- Var
- aRequest : TWebClientRequest;
- aResponse : TWebClientResponse;
- aContent,aMsg,aURL : String;
- aSleepTime : Integer;
- aSleepCount : integer;
- aTryCount : Integer;
- UseSUDO, ExitLoop : Boolean;
- begin
- aSleepCount:=1;
- aTryCount:=0;
- aResponse:=Nil;
- aRequest:=CreateRequest(aDest);
- try
- if (aSrc<>Nil) then
- begin
- if (aContentType='') then
- aContentType:='application/json';
- aRequest.Headers.Values['Content-Type']:=aContentType;
- aRequest.Content.CopyFrom(aSrc,0);
- end;
- repeat
- inc(aTryCount);
- ExitLoop:=True;
- UseSUDO:=False; // (Sudo<>'') and Not SameText(aVerb,'GET')
- aURL:=CreateURL(aName,[],UseSUDO);
- DoLog('URL : %s %s',[aVerb,aURL]);
- // Reset for loop
- FreeAndNil(aResponse);
- if Assigned(aSrc) then
- aRequest.Content.Position:=0;
- // Go !
- aResponse:=FClient.ExecuteRequest(aVerb,aURL,aRequest);
- // Throttle hit ?
- if aResponse.StatusCode=429 then
- begin
- aSleepTime:=LongThrottleSleep*aSleepCount;
- DoLog('API Throttle limit reached. Waiting %d seconds',[aSleepTime div 1000]);
- sleep(aSleepTime);
- Inc(aSleepCount);
- ExitLoop:=(aSleepCount>MaxSleepCount);
- end
- else if aResponse.StatusCode=409 then
- begin
- if aTryCount>1 then
- DoLog('Duplicate ID found at try %d, ignoring.',[aTryCount])
- else
- DoLog('Duplicate ID found at first try, ignoring anyway.');
- ExitLoop:=True;
- end
- else if aResponse.StatusCode=500 then
- begin
- aSleepTime:=LongThrottleSleep*aSleepCount;
- DoLog('Retry 500 error. Waiting %d seconds',[aSleepTime div 1000]);
- sleep(aSleepTime);
- Inc(aSleepCount);
- ExitLoop:=(aSleepCount>MaxSleepCount);
- end
- else if (UseSUDO and ((aResponse.StatusCode=403) or (aResponse.StatusCode=404))) then
- begin
- DoLog('SUDO request for %s failed, switching to non-sudo request',[Sudo]);
- ExitLoop:=False;
- Sudo:='';
- end;
- until ExitLoop;
- if (aResponse.StatusCode div 100)<>2 then
- begin
- aContent:=StreamToContent(aSrc);
- aMsg:=StreamToContent(aDest);
- Raise EGitLab.CreateFmt('Failed to %s URL "%s" : %d (%s):'+sLineBreak+'%s'+sLineBreak+'Request Content:'+sLineBreak+'%s',[aVerb,aURL,aResponse.StatusCode,aResponse.StatusText,aMsg,aContent]);
- end
- else
- begin
- if aSleepCount > 1 then
- DoLog('Success after %d retries', [aSleepCount-1]);
- end;
- if assigned(aDest) then
- aDest.Position:=0;
- finally
- aRequest.Free;
- aResponse.Free;
- end;
- end;
- procedure TGitLabClient.UpdateResource(aName: String; aSrc: TStream;
- ADest: TStream; aContentType: String);
- begin
- DoResourceRequest('PUT',aName,aSrc,aDest,aContentType);
- end;
- procedure TGitLabClient.GetResource(aName: String; aParams: array of string;
- aResult: TStream);
- Var
- aRequest : TWebClientRequest;
- aResponse : TWebClientResponse;
- aURL : String;
- begin
- aURL:=CreateURL(aName,aParams);
- aResponse:=Nil;
- aRequest:=CreateRequest(aResult);
- try
- aResponse:=FClient.ExecuteRequest('GET',aURL,aRequest);
- if (aResponse.StatusCode div 100)<>2 then
- Raise EGitLab.CreateFmt('Failed to get URL "%s" : %d (%s)',[aURL,aResponse.StatusCode,aResponse.StatusText]);
- aResult.Position:=0;
- finally
- aRequest.Free;
- aResponse.Free;
- end;
- end;
- function TGitLabClient.GetProjectResourceURL(aResource: string): String;
- begin
- Result:=GetResourceURL(Format('projects/%d/%s/',[FConfig.ProjectID,aResource]))
- end;
- function TGitLabClient.GetResourceURL(aResource: string): String;
- begin
- Result:= IncludeHTTPPathDelimiter(FConfig.BaseURL)+aResource;
- end;
- function TGitLabClient.ForEachResource(aResource: String; aParams: array of string;
- CallBack: TResourceCallback): Integer;
- Var
- Resources : TJSONArray;
- aLen,aTotalCount,i,aCount,aPage : Integer;
- aID : Int64;
- baseURL : String;
- tParams : Array of string;
- aContinue : Boolean;
- begin
- setLength(tParams,Length(aParams)+4);
- aLen:=Length(aParams);
- For I:=0 to Length(aParams)-1 do
- tParams[i]:=aParams[I];
- tParams[aLen]:='per_page';
- tParams[aLen+1]:='100';
- Result:=0;
- aPage:=1;
- Repeat
- tParams[aLen+2]:='page';
- tParams[aLen+3]:=IntToStr(aPage);
- Resources:=GetResourceList(aResource,tParams);
- try
- aCount:=Resources.Count;
- aContinue:=True;
- I:=0;
- While aContinue and (I<aCount) do
- begin
- CallBack(Self,aPage,I,aCount,Resources.Objects[i],aContinue);
- Inc(I);
- Inc(Result);
- end;
- finally
- Resources.Free;
- end;
- inc(aPage);
- until (aCount<100) or Not aContinue;
- end;
- procedure TGitLabClient.DeleteResource(aName: String);
- begin
- DoResourceRequest('DELETE',aName,Nil,Nil,'');
- end;
- end.
|