webfilecache.pp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  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, SrcDir, ModuleDir: string; Mode: TModeSwitch): String; override;
  80. function FindUnitFileName(const aUnitname, InFilename, ModuleDir: 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, SrcDir, ModuleDir: string; Mode: TModeSwitch
  281. ): String;
  282. begin
  283. {$IFDEF VERBOSEWEBCACHE}
  284. Writeln(ClassName,': FindIncludeFileName(',aFileName,',',ModuleDir,')');
  285. {$ENDIF}
  286. Result:=NormalizeFileName(aFileName);
  287. If not FCache.HasFile(Result) then
  288. Result:='';
  289. {$IFDEF VERBOSEWEBCACHE}
  290. Writeln(ClassName,': FindIncludeFileName(',aFileName,') : ',Result);
  291. {$ENDIF}
  292. end;
  293. class function TPas2jsWebFS.NormalizeFileName(const aFileName: String): String;
  294. begin
  295. Result:=LowerCase(ExtractFileName(aFileName));
  296. end;
  297. function TPas2jsWebFS.FindSourceFileName(const aFilename: string): String;
  298. begin
  299. {$IFDEF VERBOSEWEBCACHE}
  300. Writeln(ClassName,': FindSourceFileName(',aFileName,')');
  301. {$ENDIF}
  302. Result:=NormalizeFileName(aFileName);
  303. If not FCache.HasFile(Result) then
  304. Result:='';
  305. {$IFDEF VERBOSEWEBCACHE}
  306. Writeln(ClassName,': FindSourceFileName(',aFileName,') : ',Result);
  307. {$ENDIF}
  308. end;
  309. constructor TPas2jsWebFS.Create;
  310. begin
  311. inherited Create;
  312. FCache:=TWebFilesCache.Create;
  313. end;
  314. function TPas2jsWebFS.CreateResolver: TPas2jsFSResolver;
  315. begin
  316. Result:=TPas2jsWebResolver.Create(Self);
  317. end;
  318. function TPas2jsWebFS.FindUnitFileName(const aUnitname, InFilename, ModuleDir: string; out IsForeign: boolean): String;
  319. begin
  320. {$IFDEF VERBOSEWEBCACHE}
  321. Writeln(ClassName,': FindUnitFileName(',aUnitName,',',InFilename,',',ModuleDir,')');
  322. {$ENDIF}
  323. Result:=NormalizeFileName(aUnitName+'.pas');
  324. isForeign:=False;
  325. {$IFDEF VERBOSEWEBCACHE}
  326. Writeln(ClassName,': FindUnitFileName(',aUnitName,') : ',Result);
  327. {$ENDIF}
  328. end;
  329. function TPas2jsWebFS.FindUnitJSFileName(const aUnitFilename: string): String;
  330. begin
  331. {$IFDEF VERBOSEWEBCACHE}
  332. Writeln(ClassName,': FindUnitJSFileName(',aUnitFileName,')');
  333. {$ENDIF}
  334. Result:=NormalizeFileName(aUnitFileName);
  335. {$IFDEF VERBOSEWEBCACHE}
  336. Writeln(ClassName,': FindUnitJSFileName(',aUnitFileName,') : ',Result);
  337. {$ENDIF}
  338. end;
  339. function TPas2jsWebFS.LoadFile(Filename: string; Binary: boolean): TPas2jsFile;
  340. begin
  341. Result:=TPas2jsWebFile.Create(Self,FileName);
  342. Result.Load(True,False);
  343. end;
  344. (*
  345. // Check if we should not be using this instead, as the compiler outputs UTF8 ?
  346. // Found on
  347. // https://weblog.rogueamoeba.com/2017/02/27/javascript-correctly-converting-a-byte-array-to-a-utf-8-string/
  348. function stringFromUTF8Array(data)
  349. {
  350. const extraByteMap = [ 1, 1, 1, 1, 2, 2, 3, 0 ];
  351. var count = data.length;
  352. var str = "";
  353. for (var index = 0;index < count;)
  354. {
  355. var ch = data[index++];
  356. if (ch & 0x80)
  357. {
  358. var extra = extraByteMap[(ch >> 3) & 0x07];
  359. if (!(ch & 0x40) || !extra || ((index + extra) > count))
  360. return null;
  361. ch = ch & (0x3F >> extra);
  362. for (;extra > 0;extra -= 1)
  363. {
  364. var chx = data[index++];
  365. if ((chx & 0xC0) != 0x80)
  366. return null;
  367. ch = (ch << 6) | (chx & 0x3F);
  368. }
  369. }
  370. str += String.fromCharCode(ch);
  371. }
  372. return str;
  373. }
  374. *)
  375. procedure TPas2jsWebFS.SaveToFile(ms: TFPJSStream; Filename: string);
  376. Var
  377. aContent : String;
  378. i : Integer;
  379. v : JSValue;
  380. begin
  381. aContent:='';
  382. for I:=0 to MS.Length-1 do
  383. begin
  384. v:=MS[i];
  385. {AllowWriteln}
  386. Writeln('Char ',i,'(',v,') : ',TJSString.fromCharCode(v));
  387. {AllowWriteln-}
  388. aContent:=aContent+TJSString.fromCharCode(MS[i]);
  389. end;
  390. SetFileContent(FileName,aContent);
  391. end;
  392. function TPas2jsWebFS.SetFileContent(const aFileName, aContents: String): Boolean;
  393. begin
  394. Result:=FCache.SetFileContent(NormalizeFileName(aFileName),aContents);
  395. end;
  396. function TPas2jsWebFS.GetFileContent(const aFileName: String): String;
  397. begin
  398. Result:=FCache.GetFileContent(NormalizeFileName(aFileName));
  399. end;
  400. function TPas2jsWebFS.LoadFile(aFileName: String; OnLoaded: TLoadFileEvent): Boolean;
  401. Var
  402. FN : String;
  403. aURL : String;
  404. LF : TLoadFileRequest;
  405. begin
  406. FN:=NormalizeFileName(aFileName);
  407. Result:=Not FCache.HasFile(FN);
  408. if Not result then
  409. begin
  410. // It is already loaded
  411. if Assigned(OnLoaded) then
  412. OnLoaded(Self,aFileName,'')
  413. end
  414. else
  415. begin
  416. // Not yet already loaded
  417. aURL:=IncludeTrailingPathDelimiter(LoadBaseURL)+FN;
  418. LF:=TLoadFileRequest.Create(Self,aFileName,OnLoaded);
  419. LF.DoLoad(aURL);
  420. end;
  421. end;
  422. function TPas2jsWebFS.LoadFiles(aList: TStrings; OnLoaded: TLoadFileEvent
  423. ): Integer;
  424. Var
  425. i: Integer;
  426. begin
  427. Result:=0;
  428. For I:=0 to aList.Count-1 do
  429. if LoadFile(aList[i],OnLoaded) then
  430. Inc(Result);
  431. end;
  432. function TPas2jsWebFS.LoadFiles(aList: array of String; OnLoaded: TLoadFileEvent
  433. ): integer;
  434. Var
  435. i: Integer;
  436. begin
  437. Result:=0;
  438. For I:=0 to Length(aList)-1 do
  439. if LoadFile(aList[i],OnLoaded) then
  440. Inc(Result);
  441. end;
  442. { TPas2jsWebResolver }
  443. function TPas2jsWebResolver.GetWebFS: TPas2jsWebFS;
  444. begin
  445. Result:=TPas2jsWebFS(FS)
  446. end;
  447. end.