فهرست منبع

* location management API

Michaël Van Canneyt 3 سال پیش
والد
کامیت
886baf4cb9
1فایلهای تغییر یافته به همراه371 افزوده شده و 22 حذف شده
  1. 371 22
      packages/fcl-web/src/base/fpwebfile.pp

+ 371 - 22
packages/fcl-web/src/base/fpwebfile.pp

@@ -14,6 +14,7 @@
  **********************************************************************}
  **********************************************************************}
 {$mode objfpc}
 {$mode objfpc}
 {$h+}
 {$h+}
+{$modeswitch advancedrecords}
 
 
 unit fpwebfile;
 unit fpwebfile;
 
 
@@ -22,6 +23,7 @@ interface
 uses SysUtils, Classes, httpdefs, fphttp, httproute;
 uses SysUtils, Classes, httpdefs, fphttp, httproute;
 
 
 Type
 Type
+  EFileLocation = class(EHTTP);
 
 
   { TFPCustomFileModule }
   { TFPCustomFileModule }
 
 
@@ -45,10 +47,44 @@ Type
   end;
   end;
   TFPCustomFileModuleClass = Class of TFPCustomFileModule;
   TFPCustomFileModuleClass = Class of TFPCustomFileModule;
 
 
+  TFPWebFileLocationAPIModule = Class;
+  TFPWebFileLocationAPIModuleClass = Class of TFPWebFileLocationAPIModule;
+
+  { TFPWebFileLocationAPIModule }
+
+  TFPWebFileLocationAPIModule = class(TCustomHTTPModule)
+  Private
+    Class Var APIPassword : String;
+  Private
+    FCors: TCORSSupport;
+    procedure SetCors(AValue: TCORSSupport);
+  Protected
+    procedure CreateLocation(ARequest: TRequest; AResponse: TResponse); virtual;
+    procedure DeleteLocation(ARequest: TRequest; AResponse: TResponse); virtual;
+    procedure GetLocations(ARequest: TRequest; AResponse: TResponse); virtual;
+    procedure UpdateLocation(ARequest: TRequest; AResponse: TResponse); virtual;
+    Function IsRequestAuthenticated(aRequest : TRequest): Boolean; virtual;
+    class procedure HandleFileLocationAPIModuleRequest(ARequest: TRequest; AResponse: TResponse); static;
+  Public
+    Class var LocationAPIModuleClass : TFPWebFileLocationAPIModuleClass;
+  Public
+    Constructor CreateNew(aOwner : TComponent; CreateMode: Integer); override;
+    Destructor Destroy; override;
+    Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse); override;
+    Class procedure RegisterFileLocationAPI(Const aPath,aPassword : String);
+    Property CORS : TCORSSupport Read FCors Write SetCors;
+  end;
+
+  TSimpleFileModule = Class;
+  TSimpleFileModuleClass = class of TSimpleFileModule;
+
   { TSimpleFileModule }
   { TSimpleFileModule }
 
 
   TSimpleFileLog = Procedure (EventType : TEventType; Const Msg : String) of object;
   TSimpleFileLog = Procedure (EventType : TEventType; Const Msg : String) of object;
   TSimpleFileModule = class(TFPCustomFileModule,IRouteInterface)
   TSimpleFileModule = class(TFPCustomFileModule,IRouteInterface)
+  Private
+    class var
+      FPrivateRoute : THTTPRoute;
   private
   private
     FRequestedFileName,
     FRequestedFileName,
     FMappedFileName : String;
     FMappedFileName : String;
@@ -67,7 +103,9 @@ Type
     IndexPageName : String;
     IndexPageName : String;
     // If you want some logging, set this.
     // If you want some logging, set this.
     OnLog : TSimpleFileLog;
     OnLog : TSimpleFileLog;
-    Class Procedure RegisterDefaultRoute;
+    DefaultSimpleFileModuleClass: TSimpleFileModuleClass;
+    Class Procedure RegisterDefaultRoute(OverAllDefault : Boolean = True);
+    Class function DefaultRouteActive : Boolean;
   end;
   end;
 
 
 Var
 Var
@@ -81,15 +119,20 @@ Var
 // use this to map locations (relative to BaseURL of the application) to physical directories.
 // use this to map locations (relative to BaseURL of the application) to physical directories.
 // More than one location can be registered. Directory must exist, location must not have / or \
 // More than one location can be registered. Directory must exist, location must not have / or \
 Procedure RegisterFileLocation(Const ALocation,ADirectory : String);
 Procedure RegisterFileLocation(Const ALocation,ADirectory : String);
+Procedure UnRegisterFileLocation(Const ALocation: String);
 
 
 implementation
 implementation
 
 
-uses fpmimetypes;
+uses strutils, fpmimetypes, httpprotocol, fpjson, httpjson;
 
 
 Resourcestring
 Resourcestring
   SErrNoLocation = 'Cannot register an empty location.';
   SErrNoLocation = 'Cannot register an empty location.';
-  SErrInvalidLocation = 'Location contains invalid characters.';
+  SErrInvalidLocation = 'Location %s contains invalid characters.';
   SErrInvalidDirectory = 'Directory "%s" does not exist';
   SErrInvalidDirectory = 'Directory "%s" does not exist';
+  SErrNeedAPath = 'Need a path for the API';
+  SErrRequiredField = 'Field "%s" is required';
+
+
 
 
 Var
 Var
   Locations : TStrings;
   Locations : TStrings;
@@ -103,7 +146,13 @@ begin
     MimeLoaded:=true;
     MimeLoaded:=true;
     end;
     end;
 end;
 end;
-                  
+
+Procedure SetFileLocationPath(Const ALocation,ADirectory : String);
+
+begin
+  Locations.Values[IncludeHTTPPathDelimiter(ALocation)]:=aDirectory
+end;
+
 Procedure RegisterFileLocation(Const ALocation,ADirectory : String);
 Procedure RegisterFileLocation(Const ALocation,ADirectory : String);
 
 
 Var
 Var
@@ -113,14 +162,14 @@ begin
   if (ALocation='') then
   if (ALocation='') then
     Raise HTTPError.Create(SErrNoLocation); 
     Raise HTTPError.Create(SErrNoLocation); 
   if Pos('/',ALocation)<>0 then
   if Pos('/',ALocation)<>0 then
-    Raise HTTPError.Create(SErrInvalidLocation);
+    Raise HTTPError.CreateFmt(SErrInvalidLocation,[aLocation]);
   if (Locations=Nil) then
   if (Locations=Nil) then
     Locations:=TStringList.Create;
     Locations:=TStringList.Create;
   if DefaultFileModuleClass=Nil then
   if DefaultFileModuleClass=Nil then
     DefaultFileModuleClass:=TFPCustomFileModule;
     DefaultFileModuleClass:=TFPCustomFileModule;
   BaseDir:=ExtractFilePath(ParamStr(0));
   BaseDir:=ExtractFilePath(ParamStr(0));
   if (ADirectory='') then
   if (ADirectory='') then
-    Locations.Values[IncludeHTTPPathDelimiter(ALocation)]:=BaseDir
+    setFileLocationPath(aLocation,BaseDir)
   else
   else
     begin
     begin
     D:=ADirectory;
     D:=ADirectory;
@@ -128,17 +177,49 @@ begin
       D:=BaseDir+D;
       D:=BaseDir+D;
     if not DirectoryExists(D) then
     if not DirectoryExists(D) then
       Raise HTTPError.CreateFmt(SErrInvalidDirectory,[D]);
       Raise HTTPError.CreateFmt(SErrInvalidDirectory,[D]);
-    Locations.Values[IncludeHTTPPathDelimiter(ALocation)]:=IncludeTrailingPathDelimiter(D);
+    SetFileLocationPath(ALocation,IncludeTrailingPathDelimiter(D));
     end;
     end;
   RegisterHTTPModule(ALocation,DefaultFileModuleClass,true);
   RegisterHTTPModule(ALocation,DefaultFileModuleClass,true);
+  ModuleFactory.MoveModuleBeforeDefault(ALocation);
+end;
+
+Function IndexOfFileLocation(const ALocation: String) : Integer;
+
+begin
+  if Not Assigned(Locations) then
+    Result:=-1
+  else
+    Result:=Locations.IndexOfName(IncludeHTTPPathDelimiter(ALocation))
+end;
+
+procedure UnRegisterFileLocation(const ALocation: String);
+
+Var
+  Idx : Integer;
+
+begin
+  if Not Assigned(Locations) then
+    Exit;
+  Idx:=IndexOfFileLocation(aLocation);
+  if Idx<>-1 then
+    begin
+    Locations.Delete(Idx);
+    ModuleFactory.RemoveModule(aLocation);
+    end;
 end;
 end;
 
 
 { TSimpleFileModule }
 { TSimpleFileModule }
 
 
 Class Procedure TSimpleFileModule.HandleSimpleFileRequest(ARequest : TRequest; AResponse : TResponse); static;
 Class Procedure TSimpleFileModule.HandleSimpleFileRequest(ARequest : TRequest; AResponse : TResponse); static;
 
 
+Var
+  aClass : TSimpleFileModuleClass;
+
 begin
 begin
-  With TSimpleFileModule.CreateNew(Nil) do
+  aClass:=DefaultSimpleFileModuleClass;
+  if aClass=Nil then
+    aClass:=TSimpleFileModule;
+  With aClass.CreateNew(Nil) do
     try
     try
       HandleRequest(ARequest,AResponse);
       HandleRequest(ARequest,AResponse);
     finally
     finally
@@ -146,6 +227,7 @@ begin
     end;
     end;
 end;
 end;
 
 
+
 function TSimpleFileModule.AllowFile(const AFileName: String): Boolean;
 function TSimpleFileModule.AllowFile(const AFileName: String): Boolean;
 
 
 Var
 Var
@@ -182,11 +264,18 @@ begin
     OnLog(etInfo,Format('%d serving "%s" -> "%s"',[AResponse.Code,FRequestedFileName,FMappedFileName]));
     OnLog(etInfo,Format('%d serving "%s" -> "%s"',[AResponse.Code,FRequestedFileName,FMappedFileName]));
 end;
 end;
 
 
-class procedure TSimpleFileModule.RegisterDefaultRoute;
+class procedure TSimpleFileModule.RegisterDefaultRoute(OverAllDefault : Boolean = True);
 begin
 begin
   if BaseDir='' then
   if BaseDir='' then
     BaseDir:=IncludeTrailingPathDelimiter(GetCurrentDir);
     BaseDir:=IncludeTrailingPathDelimiter(GetCurrentDir);
-  httprouter.RegisterRoute('/*',@HandleSimpleFileRequest);
+  FPrivateRoute:=httprouter.RegisterRoute('/*',@HandleSimpleFileRequest);
+  if OverallDefault then
+    FPrivateRoute.Default:=True;
+end;
+
+class function TSimpleFileModule.DefaultRouteActive: Boolean;
+begin
+  Result:=FPrivateRoute<>Nil;
 end;
 end;
 
 
 Function TFPCustomFileModule.GetRequestFileName(Const ARequest : TRequest) : String;
 Function TFPCustomFileModule.GetRequestFileName(Const ARequest : TRequest) : String;
@@ -287,37 +376,297 @@ Var
 begin
 begin
   If CompareText(ARequest.Method,'GET')<>0 then
   If CompareText(ARequest.Method,'GET')<>0 then
     begin
     begin
-    AResponse.Code:=405;
-    AResponse.CodeText:='Method not allowed';
-    AResponse.SendContent;
+    AResponse.SetStatus(405,True);
     Exit;
     Exit;
     end;
     end;
   RFN:=GetRequestFileName(ARequest);
   RFN:=GetRequestFileName(ARequest);
   if (RFN='') then
   if (RFN='') then
     begin
     begin
-    AResponse.Code:=400;
-    AResponse.CodeText:='Bad request';
-    AResponse.SendContent;
+    AResponse.SetStatus(400,True);
     exit;
     exit;
     end;
     end;
   FN:=MapFileName(RFN);
   FN:=MapFileName(RFN);
   if (FN='') or not AllowFile(FN) then  
   if (FN='') or not AllowFile(FN) then  
     begin
     begin
-    AResponse.Code:=403;
-    AResponse.CodeText:='Forbidden';
-    AResponse.SendContent;
+    AResponse.SetStatus(403,True);
     exit;
     exit;
     end;
     end;
   if  not FileExists(FN) then
   if  not FileExists(FN) then
     begin
     begin
-    AResponse.Code:=404;
-    AResponse.CodeText:='Not found';
-    AResponse.SendContent;
+    AResponse.SetStatus(404,True);
     exit;
     exit;
     end;
     end;
   SendFile(FN,AResponse);
   SendFile(FN,AResponse);
 end;
 end;
 
 
+procedure TFPWebFileLocationAPIModule.SetCors(AValue: TCORSSupport);
+begin
+  if FCors=AValue then Exit;
+  FCors.Assign(AValue);
+end;
+
+class procedure TFPWebFileLocationAPIModule.HandleFileLocationAPIModuleRequest(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  aClass : TFPWebFileLocationAPIModuleClass;
+
+begin
+  aClass:=LocationAPIModuleClass;
+  if aClass=Nil then
+    aClass:=TFPWebFileLocationAPIModule;
+  With aClass.CreateNew(Nil) do
+    try
+      HandleRequest(aRequest,aResponse);
+    finally
+      Free
+    end;
+end;
+
+constructor TFPWebFileLocationAPIModule.CreateNew(aOwner: TComponent; CreateMode: Integer);
+begin
+  inherited CreateNew(aOwner,CreateMode);
+  FCors:=TCORSSupport.Create;
+  FCors.Enabled:=True;
+  FCors.Options:=[coAllowCredentials];
+end;
+
+destructor TFPWebFileLocationAPIModule.Destroy;
+begin
+  FreeAndNil(FCors);
+  inherited Destroy;
+end;
+
+Type
+
+  { TLocationData }
+
+  TLocationData = Record
+    Location : String;
+    Path : String;
+    Procedure Verify;
+    Procedure FromJSON(aJSON : TJSONObject);
+    Procedure ToJSON(aJSON : TJSONObject);
+  end;
+
+{ TLocationData }
+
+procedure TLocationData.Verify;
+
+  Procedure DoError(aFmt, aField : String);
+
+  Var
+    E : EFileLocation;
+
+  begin
+    E:=EFileLocation.CreateFmt(aFmt,[aField]);
+    E.StatusText:='BAD REQUEST';
+    E.StatusCode:=400;
+    Raise E;
+  end;
+
+begin
+  if Location='' then
+    DoError(SErrRequiredField,'location');
+  if Path='' then
+    DoError(SErrRequiredField,'path');
+  if Pos('/',Location)<>0 then
+    DoError(SErrInvalidLocation,Location);
+  if not DirectoryExists(Path) then
+    DoError(SErrInvalidDirectory,Location);
+end;
+
+procedure TLocationData.FromJSON(aJSON: TJSONObject);
+
+begin
+  Location:=aJSON.Get('location','');
+  Path:=aJSON.Get('path','');
+end;
+
+procedure TLocationData.ToJSON(aJSON: TJSONObject);
+begin
+  aJSON.Add('location',Location);
+  aJSON.Add('path',path);
+end;
+
+Procedure TFPWebFileLocationAPIModule.CreateLocation(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  aJSON : TJSONObject;
+  aLoc : TLocationData;
+
+begin
+  aJSON:=aRequest.ContentAsJSONObject;
+  try
+    ALoc.FromJSON(aJSON);
+    ALoc.Verify;
+    RegisterFileLocation(aLoc.Location,Aloc.Path);
+    aJSON.Clear;
+    aLoc.ToJSON(aJSON);
+    aResponse.ContentAsJSON:=aJSON;
+    aResponse.SetStatus(201,True);
+  finally
+    aJSON.Free;
+  end;
+end;
+
+Procedure TFPWebFileLocationAPIModule.UpdateLocation(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  aJSON : TJSONObject;
+  aLoc : TLocationData;
+  aOldLoc : String;
+  Idx: Integer;
+
+begin
+  aJSON:=aRequest.ContentAsJSONObject;
+  try
+    ALoc.FromJSON(aJSON);
+    aOldLoc:=aRequest.RouteParams['location'];
+    if aOldLoc='' then // Location name in payload
+      aOldLoc:=aLoc.Location;
+    if aLoc.Location='' then // Only path in payload
+      aLoc.Location:=aOldLoc;
+    ALoc.Verify;
+    Idx:=IndexOfFileLocation(aOldLoc);
+    if Idx=-1 then
+      aResponse.SetStatus(404,True)
+    else  if not SameText(aOldLoc,aLoc.Location) then
+      begin
+      UnRegisterFileLocation(aOldLoc);
+      RegisterFileLocation(aLoc.Location,Aloc.Path);
+      end
+    else
+      SetFileLocationPath(aLoc.Location,aLoc.Path);
+    aJSON.Clear;
+    aLoc.ToJSON(aJSON);
+    aResponse.ContentAsJSON:=aJSON;
+    aResponse.SendContent;
+  finally
+    aJSON.Free;
+  end;
+end;
+
+function TFPWebFileLocationAPIModule.IsRequestAuthenticated(aRequest: TRequest): Boolean;
+
+Var
+  aAuth : String;
+
+begin
+  Result:=(APIPassword='');
+  if Result then exit;
+  aAuth:=aRequest.Authorization;
+  if (aAuth<>'') and SameText(ExtractWord(1,aAuth,[' ']),'Bearer') then
+    aAuth:=ExtractWord(2,aAuth,[' '])
+  else
+    aAuth:=aRequest.QueryFields.Values['APIKey'];
+  Result:=(aAuth=APIPassword);
+end;
+
+Procedure TFPWebFileLocationAPIModule.DeleteLocation(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  aOldLoc : String;
+  Idx: Integer;
+
+begin
+  aOldLoc:=aRequest.RouteParams['location'];
+  if aOldLoc='' then
+    aResponse.SetStatus(400,True)
+  else
+    begin
+    Idx:=IndexOfFileLocation(aOldLoc);
+    if Idx=-1 then
+      aResponse.SetStatus(404,True)
+    else
+      begin
+      UnRegisterFileLocation(aOldLoc);
+      aResponse.SetStatus(204,True);
+      end;
+    end;
+end;
+
+Procedure TFPWebFileLocationAPIModule.GetLocations(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  Res,Loc : TJSONObject;
+  Arr: TJSONArray;
+  I : Integer;
+  aLoc : TLocationData;
+
+begin
+  Res:=TJSONObject.Create;
+  try
+    Arr:=TJSONArray.Create;
+    Res.Add('data',Arr);
+    if Assigned(Locations) then
+      For I:=0 to Locations.Count-1 do
+        begin
+        Loc:=TJSONObject.Create;
+        Arr.Add(Loc);
+        with aLoc do
+          begin
+          Locations.GetNameValue(I,Location,Path);
+          aLoc.Location:=ExcludeHTTPPathDelimiter(aLoc.Location);
+          aLoc.ToJSON(Loc);
+          end;
+        end;
+    if TSimpleFileModule.DefaultRouteActive then
+      begin
+      Loc:=TJSONObject.Create;
+      Arr.Add(Loc);
+      aLoc.Location:='*';
+      aLoc.Path:=TSimpleFileModule.BaseDir;
+      aLoc.ToJSON(Loc);
+      end;
+    aResponse.SetContentFromJSON(Res,aRequest.QueryFields.Values['fmt']='1');
+    aResponse.SendContent;
+  finally
+    Res.Free;
+  end;
+end;
+
+procedure TFPWebFileLocationAPIModule.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  if Not IsRequestAuthenticated(aRequest) then
+    begin
+    aResponse.SetStatus(401,True);
+    exit;
+    end
+  else
+    Cors.HandleRequest(aRequest,aResponse,[]);
+  try
+    Case UpperCase(ARequest.Method) of
+      'GET'    : GetLocations(ARequest,aResponse);
+      'POST'   : CreateLocation(ARequest,aResponse);
+      'PUT'    : UpdateLocation(aRequest,aResponse);
+      'DELETE' : DeleteLocation(aRequest,aResponse);
+      'OPTIONS' :  Cors.HandleRequest(aRequest,aResponse,[hcSend]);
+    else
+      aResponse.SetStatus(405);
+    end;
+  except
+    on E : Exception do
+      aResponse.SendExceptionJSON(E);
+  end;
+end;
+
+class procedure TFPWebFileLocationAPIModule.RegisterFileLocationAPI(const aPath, aPassword: String);
+
+Var
+  P : String;
+
+begin
+  APIPassword:=aPassword;
+  if aPath='' then
+    Raise EFileLocation.Create(SErrNeedAPath);
+  P:=aPath;
+  if P[1]<>'/' then
+    P:='/'+P;
+  httprouter.RegisterRoute(P,@HandleFileLocationAPIModuleRequest); // Get and post
+  httprouter.RegisterRoute(IncludeHTTPPathDelimiter(P)+':Location',@HandleFileLocationAPIModuleRequest); // Put & Delete
+end;
+
+
 initialization
 initialization
 
 
 finalization
 finalization