webfilecache.pp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. unit webfilecache;
  2. {$mode objfpc}
  3. // Enable this to write lots of debugging info to the browser console.
  4. { $DEFINE VERBOSEWEBCACHE}
  5. interface
  6. uses
  7. Classes, SysUtils, JS, Web, fpjson, pas2jsfs, pscanner, contnrs;
  8. type
  9. TPas2jsWebFS = Class;
  10. { TWebFileContent }
  11. TWebFileContent = Class(TObject)
  12. private
  13. FContents: string;
  14. FFileName: String;
  15. FModified: Boolean;
  16. procedure SetContents(AValue: string);
  17. Public
  18. Constructor Create(const aFileName,aContents : String);
  19. Property FileName : String Read FFileName Write FFileName;
  20. Property Contents : string Read FContents Write SetContents;
  21. Property Modified : Boolean Read FModified;
  22. end;
  23. { TWebFilesCache }
  24. TWebFilesCache = Class(TObject)
  25. Private
  26. FFiles : TFPObjectHashTable;
  27. Function FindFile(aFileName : String) : TWebFileContent;
  28. Public
  29. Constructor Create;
  30. Destructor Destroy; override;
  31. Function HasFile(aFileName : String) : Boolean;
  32. Function GetFileContent(Const aFileName : String) : String;
  33. function SetFileContent(const aFileName, aContent: String): Boolean;
  34. end;
  35. { TPas2jsWebFile }
  36. TPas2jsWebFile = Class(TPas2jsFile)
  37. public
  38. function CreateLineReader(RaiseOnError: boolean): TSourceLineReader; override;
  39. function Load(RaiseOnError: boolean; Binary: boolean): boolean; override;
  40. end;
  41. { TWebSourceLineReader }
  42. TWebSourceLineReader = Class(TSourceLineReader)
  43. private
  44. FFS: TPas2jsFS;
  45. Protected
  46. Property FS : TPas2jsFS Read FFS;
  47. Procedure IncLineNumber; override;
  48. end;
  49. // aFileName is the original filename, not normalized one
  50. TLoadFileEvent = Reference to Procedure(Sender : TObject; aFileName : String; aError : string);
  51. { TLoadFileRequest }
  52. TLoadFileRequest = Class(TObject)
  53. FFS : TPas2jsWebFS;
  54. FFileName : string;
  55. FXML : TJSXMLHttpRequest;
  56. FOnLoaded : TLoadFileEvent;
  57. private
  58. procedure DoChange;
  59. Public
  60. constructor Create(aFS: TPas2jsWebFS; const aFileName : string; aOnLoaded: TLoadFileEvent);
  61. Procedure DoLoad(const aURL : String);
  62. end;
  63. { TPas2jsWebFS }
  64. TPas2jsWebFS = Class(TPas2jsFS)
  65. Private
  66. FCache : TWebFilesCache;
  67. FLoadBaseURL: String;
  68. FOnLoadedFile: TLoadFileEvent;
  69. protected
  70. // Only for names, no paths
  71. Class Function NormalizeFileName(Const aFileName : String) : String;
  72. function FindSourceFileName(const aFilename: string): String; override;
  73. public
  74. Constructor Create; override;
  75. // Overrides
  76. function CreateResolver: TPas2jsFSResolver; override;
  77. function FileExists(const aFileName: String): Boolean; override;
  78. function FindCustomJSFileName(const aFilename: string): String; override;
  79. function FindIncludeFileName(const aFilename: string): String; override;
  80. function FindUnitFileName(const aUnitname, InFilename: string; out IsForeign: boolean): String; override;
  81. function FindUnitJSFileName(const aUnitFilename: string): String; override;
  82. function LoadFile(Filename: string; Binary: boolean=false): TPas2jsFile; override;
  83. procedure SaveToFile(ms: TFPJSStream; Filename: string); override;
  84. Function SetFileContent(Const aFileName,aContents : String) : Boolean;
  85. Function GetFileContent(Const aFileName : String) : String;
  86. // Returns false if the file was already loaded. OnLoaded is called in either case.
  87. Function LoadFile(aFileName : String; OnLoaded : TLoadFileEvent = Nil) : Boolean;
  88. // Returns number of load requests. OnLoaded is called for each file in the list
  89. Function LoadFiles(aList : TStrings;OnLoaded : TLoadFileEvent = Nil) : Integer;
  90. Function LoadFiles(aList : array of String;OnLoaded : TLoadFileEvent = Nil) : integer;
  91. Property OnLoadedFile : TLoadFileEvent Read FOnLoadedFile Write FOnLoadedFile;
  92. Property LoadBaseURL : String Read FLoadBaseURL Write FLoadBaseURL;
  93. end;
  94. { TPas2jsFileResolver }
  95. { TPas2jsWebResolver }
  96. TPas2jsWebResolver = class(TPas2jsFSResolver)
  97. private
  98. function GetWebFS: TPas2jsWebFS;
  99. public
  100. Property WebFS : TPas2jsWebFS Read GetWebFS;
  101. end;
  102. implementation
  103. { TWebSourceLineReader }
  104. procedure TWebSourceLineReader.IncLineNumber;
  105. begin
  106. if (FFS<>nil) then
  107. FFS.IncReadLineCounter;
  108. inherited IncLineNumber;
  109. end;
  110. { TLoadFileRequest }
  111. procedure TLoadFileRequest.DoChange;
  112. Var
  113. Err : String;
  114. begin
  115. Case FXML.readyState of
  116. TJSXMLHttpRequest.UNSENT : ;
  117. TJSXMLHttpRequest.OPENED : ;
  118. TJSXMLHttpRequest.HEADERS_RECEIVED : ;
  119. TJSXMLHttpRequest.LOADING : ;
  120. TJSXMLHttpRequest.DONE :
  121. begin
  122. if (FXML.Status div 100)=2 then
  123. begin
  124. Err:='';
  125. // FS will normalize filename
  126. FFS.SetFileContent(FFileName,FXML.responsetext)
  127. end
  128. else
  129. Err:='Error loading file: '+FXML.StatusText;
  130. If Assigned(FOnLoaded) then
  131. FOnLoaded(FFS,FFileName,Err);
  132. if Assigned(FFS.OnLoadedFile) then
  133. FFS.OnLoadedFile(FFS,FFileName,Err);
  134. Free;
  135. end;
  136. end
  137. end;
  138. constructor TLoadFileRequest.Create(aFS: TPas2jsWebFS; const aFileName : string; aOnLoaded: TLoadFileEvent);
  139. begin
  140. FFS:=aFS;
  141. FOnLoaded:=aOnLoaded;
  142. FFileName:=aFileName;
  143. end;
  144. Procedure TLoadFileRequest.DoLoad(const aURL: String);
  145. begin
  146. FXML:=TJSXMLHttpRequest.new;
  147. FXML.onreadystatechange:=@DoChange;
  148. // Maybe one day allow do this sync, so the compiler can load files on demand.
  149. FXML.Open('GET',aURL);
  150. FXML.Send;
  151. end;
  152. { TPas2jsWebFile }
  153. function TPas2jsWebFile.CreateLineReader(RaiseOnError: boolean): TSourceLineReader;
  154. begin
  155. {$IFDEF VERBOSEWEBCACHE}
  156. Writeln(ClassName,': Creating line reader for ',FileName);
  157. {$ENDIF}
  158. if Load(RaiseOnError,False) then
  159. begin
  160. Result:=TWebSourceLineReader.Create(FileName,Source);
  161. TWebSourceLineReader(Result).FFS:=Self.FS;
  162. end
  163. else
  164. Result:=Nil;
  165. end;
  166. function TPas2jsWebFile.Load(RaiseOnError: boolean; Binary: boolean): boolean;
  167. begin
  168. Result:=False;
  169. {$IFDEF VERBOSEWEBCACHE}
  170. Writeln(ClassName,': Loading for ',FileName);
  171. {$ENDIF}
  172. With (FS as TPas2jsWebFS).FCache do
  173. if HasFile(FileName) then
  174. begin
  175. SetSource(GetFileContent(FileName));
  176. Result:=True;
  177. end;
  178. if Not Result then
  179. if RaiseOnError then
  180. Raise EFileNotFoundError.Create('File not loaded '+FileName)
  181. {$IFDEF VERBOSEWEBCACHE}
  182. else Writeln('File not loaded '+FileName);
  183. {$ENDIF}
  184. end;
  185. { TWebFilesCache }
  186. function TWebFilesCache.FindFile(aFileName: String): TWebFileContent;
  187. Var
  188. N : THTCustomNode;
  189. begin
  190. {$IFDEF VERBOSEWEBCACHE}
  191. Writeln(ClassName,': Looking for file : ',aFileName);
  192. {$ENDIF}
  193. N:=FFiles.Find(aFileName);
  194. if N=Nil then
  195. result:=Nil
  196. else
  197. Result:=TWebFileContent(THTObjectNode(N).Data);
  198. {$IFDEF VERBOSEWEBCACHE}
  199. Writeln(ClassName,': Looking for file : ',aFileName, ': ',Assigned(Result));
  200. {$ENDIF}
  201. end;
  202. constructor TWebFilesCache.Create;
  203. begin
  204. FFiles:=TFPObjectHashTable.Create(True);
  205. end;
  206. destructor TWebFilesCache.Destroy;
  207. begin
  208. FreeAndNil(FFiles);
  209. inherited Destroy;
  210. end;
  211. function TWebFilesCache.HasFile(aFileName: String): Boolean;
  212. begin
  213. Result:=FindFile(aFileName)<>Nil;
  214. {$IFDEF VERBOSEWEBCACHE}
  215. Writeln(ClassName,': HasFile(',aFileName,') : ',Result);
  216. {$ENDIF}
  217. end;
  218. function TWebFilesCache.GetFileContent(const aFileName: String): String;
  219. Var
  220. W : TWebFileContent;
  221. begin
  222. {$IFDEF VERBOSEWEBCACHE}
  223. Writeln(ClassName,': GetFileContent(',aFileName,')');
  224. {$ENDIF}
  225. W:=FindFile(aFileName);
  226. if Assigned(W) then
  227. Result:=W.Contents
  228. else
  229. Raise EFileNotFoundError.Create('No such file '+AFileName);
  230. end;
  231. function TWebFilesCache.SetFileContent(const aFileName, aContent: String) : Boolean;
  232. Var
  233. W : TWebFileContent;
  234. begin
  235. {$IFDEF VERBOSEWEBCACHE}
  236. Writeln(ClassName,': SetFileContent(',aFileName,')');
  237. {$ENDIF}
  238. W:=FindFile(aFileName);
  239. Result:=Assigned(W);
  240. if Result then
  241. W.Contents:=aContent
  242. else
  243. FFiles.Add(aFileName,TWebFileContent.Create(aFileName,aContent));
  244. end;
  245. { TWebFileContent }
  246. procedure TWebFileContent.SetContents(AValue: string);
  247. begin
  248. if FContents=AValue then Exit;
  249. FContents:=AValue;
  250. FModified:=True;
  251. end;
  252. constructor TWebFileContent.Create(const aFileName, aContents: String);
  253. begin
  254. FContents:=aContents;
  255. FFileName:=aFileName;
  256. end;
  257. { TPas2jsWebFS }
  258. function TPas2jsWebFS.FileExists(const aFileName: String): Boolean;
  259. begin
  260. {$IFDEF VERBOSEWEBCACHE}
  261. Writeln(ClassName,': FileExists(',aFileName,')');
  262. {$ENDIF}
  263. Result:=FCache.HasFile(NormalizeFileName(aFileName));
  264. {$IFDEF VERBOSEWEBCACHE}
  265. Writeln(ClassName,': FileExists(',aFileName,') : ',Result);
  266. {$ENDIF}
  267. end;
  268. function TPas2jsWebFS.FindCustomJSFileName(const aFilename: string): String;
  269. begin
  270. {$IFDEF VERBOSEWEBCACHE}
  271. Writeln(ClassName,': FindCustomJSFileName(',aFileName,')');
  272. {$ENDIF}
  273. Result:=NormalizeFileName(aFileName);
  274. If not FCache.HasFile(Result) then
  275. Result:='';
  276. {$IFDEF VERBOSEWEBCACHE}
  277. Writeln(ClassName,': FindCustomJSFileName(',aFileName,'): ',Result);
  278. {$ENDIF}
  279. end;
  280. function TPas2jsWebFS.FindIncludeFileName(const aFilename: string): String;
  281. begin
  282. {$IFDEF VERBOSEWEBCACHE}
  283. Writeln(ClassName,': FindIncludeFileName(',aFileName,')');
  284. {$ENDIF}
  285. Result:=NormalizeFileName(aFileName);
  286. If not FCache.HasFile(Result) then
  287. Result:='';
  288. {$IFDEF VERBOSEWEBCACHE}
  289. Writeln(ClassName,': FindIncludeFileName(',aFileName,') : ',Result);
  290. {$ENDIF}
  291. end;
  292. class function TPas2jsWebFS.NormalizeFileName(const aFileName: String): String;
  293. begin
  294. Result:=LowerCase(ExtractFileName(aFileName));
  295. end;
  296. function TPas2jsWebFS.FindSourceFileName(const aFilename: string): String;
  297. begin
  298. {$IFDEF VERBOSEWEBCACHE}
  299. Writeln(ClassName,': FindSourceFileName(',aFileName,')');
  300. {$ENDIF}
  301. Result:=NormalizeFileName(aFileName);
  302. If not FCache.HasFile(Result) then
  303. Result:='';
  304. {$IFDEF VERBOSEWEBCACHE}
  305. Writeln(ClassName,': FindSourceFileName(',aFileName,') : ',Result);
  306. {$ENDIF}
  307. end;
  308. constructor TPas2jsWebFS.Create;
  309. begin
  310. inherited Create;
  311. FCache:=TWebFilesCache.Create;
  312. end;
  313. function TPas2jsWebFS.CreateResolver: TPas2jsFSResolver;
  314. begin
  315. Result:=TPas2jsWebResolver.Create(Self);
  316. end;
  317. function TPas2jsWebFS.FindUnitFileName(const aUnitname, InFilename: string; out IsForeign: boolean): String;
  318. begin
  319. {$IFDEF VERBOSEWEBCACHE}
  320. Writeln(ClassName,': FindUnitFileName(',aUnitName,')');
  321. {$ENDIF}
  322. Result:=NormalizeFileName(aUnitName+'.pas');
  323. isForeign:=False;
  324. {$IFDEF VERBOSEWEBCACHE}
  325. Writeln(ClassName,': FindUnitFileName(',aUnitName,') : ',Result);
  326. {$ENDIF}
  327. end;
  328. function TPas2jsWebFS.FindUnitJSFileName(const aUnitFilename: string): String;
  329. begin
  330. {$IFDEF VERBOSEWEBCACHE}
  331. Writeln(ClassName,': FindUnitJSFileName(',aUnitFileName,')');
  332. {$ENDIF}
  333. Result:=NormalizeFileName(aUnitFileName);
  334. {$IFDEF VERBOSEWEBCACHE}
  335. Writeln(ClassName,': FindUnitJSFileName(',aUnitFileName,') : ',Result);
  336. {$ENDIF}
  337. end;
  338. function TPas2jsWebFS.LoadFile(Filename: string; Binary: boolean): TPas2jsFile;
  339. begin
  340. Result:=TPas2jsWebFile.Create(Self,FileName);
  341. Result.Load(True,False);
  342. end;
  343. (*
  344. // Check if we should not be using this instead, as the compiler outputs UTF8 ?
  345. // Found on
  346. // https://weblog.rogueamoeba.com/2017/02/27/javascript-correctly-converting-a-byte-array-to-a-utf-8-string/
  347. function stringFromUTF8Array(data)
  348. {
  349. const extraByteMap = [ 1, 1, 1, 1, 2, 2, 3, 0 ];
  350. var count = data.length;
  351. var str = "";
  352. for (var index = 0;index < count;)
  353. {
  354. var ch = data[index++];
  355. if (ch & 0x80)
  356. {
  357. var extra = extraByteMap[(ch >> 3) & 0x07];
  358. if (!(ch & 0x40) || !extra || ((index + extra) > count))
  359. return null;
  360. ch = ch & (0x3F >> extra);
  361. for (;extra > 0;extra -= 1)
  362. {
  363. var chx = data[index++];
  364. if ((chx & 0xC0) != 0x80)
  365. return null;
  366. ch = (ch << 6) | (chx & 0x3F);
  367. }
  368. }
  369. str += String.fromCharCode(ch);
  370. }
  371. return str;
  372. }
  373. *)
  374. procedure TPas2jsWebFS.SaveToFile(ms: TFPJSStream; Filename: string);
  375. Var
  376. aContent : String;
  377. i : Integer;
  378. v : JSValue;
  379. begin
  380. aContent:='';
  381. for I:=0 to MS.Length-1 do
  382. begin
  383. v:=MS[i];
  384. {AllowWriteln}
  385. Writeln('Char ',i,'(',v,') : ',TJSString.fromCharCode(v));
  386. {AllowWriteln-}
  387. aContent:=aContent+TJSString.fromCharCode(MS[i]);
  388. end;
  389. SetFileContent(FileName,aContent);
  390. end;
  391. function TPas2jsWebFS.SetFileContent(const aFileName, aContents: String): Boolean;
  392. begin
  393. Result:=FCache.SetFileContent(NormalizeFileName(aFileName),aContents);
  394. end;
  395. function TPas2jsWebFS.GetFileContent(const aFileName: String): String;
  396. begin
  397. Result:=FCache.GetFileContent(NormalizeFileName(aFileName));
  398. end;
  399. function TPas2jsWebFS.LoadFile(aFileName: String; OnLoaded: TLoadFileEvent): Boolean;
  400. Var
  401. FN : String;
  402. aURL : String;
  403. LF : TLoadFileRequest;
  404. begin
  405. FN:=NormalizeFileName(aFileName);
  406. Result:=Not FCache.HasFile(FN);
  407. if Not result then
  408. begin
  409. // It is already loaded
  410. if Assigned(OnLoaded) then
  411. OnLoaded(Self,aFileName,'')
  412. end
  413. else
  414. begin
  415. // Not yet already loaded
  416. aURL:=IncludeTrailingPathDelimiter(LoadBaseURL)+FN;
  417. LF:=TLoadFileRequest.Create(Self,aFileName,OnLoaded);
  418. LF.DoLoad(aURL);
  419. end;
  420. end;
  421. Function TPas2jsWebFS.LoadFiles(aList: TStrings; OnLoaded: TLoadFileEvent): Integer;
  422. Var
  423. i: Integer;
  424. begin
  425. Result:=0;
  426. For I:=0 to aList.Count-1 do
  427. if LoadFile(aList[i],OnLoaded) then
  428. Inc(Result);
  429. end;
  430. function TPas2jsWebFS.LoadFiles(aList: array of String; OnLoaded: TLoadFileEvent): Integer;
  431. Var
  432. i: Integer;
  433. begin
  434. Result:=0;
  435. For I:=0 to Length(aList)-1 do
  436. if LoadFile(aList[i],OnLoaded) then
  437. Inc(Result);
  438. end;
  439. { TPas2jsWebResolver }
  440. function TPas2jsWebResolver.GetWebFS: TPas2jsWebFS;
  441. begin
  442. Result:=TPas2jsWebFS(FS)
  443. end;
  444. end.