]> Git Repo - pico-vscode.git/blob - src/utils/githubApiCache.mts
Test system gdb for arm_any support
[pico-vscode.git] / src / utils / githubApiCache.mts
1 // TODO: put defaults into json file, to prevent need for these disables
2 import type { ExtensionContext, Memento } from "vscode";
3 import type { GithubReleaseResponse, GithubRepository } from "./githubREST.mjs";
4 import Logger from "../logger.mjs";
5 import { getDataRoot } from "./downloadHelpers.mjs";
6 import { get } from "https";
7 import {
8   isInternetConnected,
9   CURRENT_DATA_VERSION,
10 } from "./downloadHelpers.mjs";
11 import { join as joinPosix } from "path/posix";
12 import { readFileSync } from "fs";
13
14 /**
15  * Tells if the stored data is a GithubReleaseResponse (data of a specific release)
16  * or a string array (release tags).
17  */
18 export enum GithubApiCacheEntryDataType {
19   releases = 0,
20   tag = 1,
21 }
22
23 /// The data structure containing the cached data of a Github API request.
24 export interface GithubApiCacheEntry {
25   /// Identity of entry is a combination of repository and dataType.
26   repository: GithubRepository;
27   /// Identity of entry is a combination of repository and dataType.
28   dataType: GithubApiCacheEntryDataType;
29
30   /// The cached data.
31   data: GithubReleaseResponse | string[];
32
33   /// Use to check if the cached data is still up to date.
34   etag: string;
35 }
36
37 const CACHE_JSON_URL =
38   "https://raspberrypi.github.io/pico-vscode/" +
39   `${CURRENT_DATA_VERSION}/github-cache.json`;
40
41 function parseCacheJson(data: string): {
42   [id: string]: GithubReleaseResponse | string[];
43 } {
44   try {
45     const cache = JSON.parse(data.toString()) as {
46       [id: string]: GithubReleaseResponse | string[];
47     };
48
49     return cache;
50   } catch {
51     Logger.log("Failed to parse github-cache.json");
52
53     throw new Error(
54       "Error while downloading github cache. " + "Parsing Failed"
55     );
56   }
57 }
58
59 export async function defaultCacheOfRepository(
60   repository: GithubRepository,
61   dataType: GithubApiCacheEntryDataType,
62   tag?: string
63 ): Promise<GithubApiCacheEntry | undefined> {
64   const ret: GithubApiCacheEntry = {
65     repository: repository,
66     dataType: dataType,
67     data: [],
68     etag: "",
69   };
70   try {
71     if (!(await isInternetConnected())) {
72       throw new Error(
73         "Error while downloading github cache. " + "No internet connection"
74       );
75     }
76     const result = await new Promise<{
77       [id: string]: GithubReleaseResponse | string[];
78     }>((resolve, reject) => {
79       // Download the JSON file
80       get(CACHE_JSON_URL, response => {
81         if (response.statusCode !== 200) {
82           reject(
83             new Error(
84               "Error while downloading github cache list. " +
85                 `Status code: ${response.statusCode}`
86             )
87           );
88         }
89         let data = "";
90
91         // Append data as it arrives
92         response.on("data", chunk => {
93           data += chunk;
94         });
95
96         // Parse the JSON data when the download is complete
97         response.on("end", () => {
98           // Resolve with the array of SupportedToolchainVersion
99           const ret = parseCacheJson(data);
100           if (ret !== undefined) {
101             resolve(ret);
102           } else {
103             reject(
104               new Error(
105                 "Error while downloading github cache list. " +
106                   "Parsing data failed"
107               )
108             );
109           }
110         });
111
112         // Handle errors
113         response.on("error", error => {
114           reject(error);
115         });
116       });
117     });
118
119     // TODO: Logger.debug
120     Logger.log(`Successfully downloaded github cache from the internet.`);
121
122     ret.data =
123       result[
124         `githubApiCache-${repository}-${dataType}` +
125           `${tag !== undefined ? "-" + tag : ""}`
126       ];
127
128     return ret;
129   } catch (error) {
130     Logger.log(error instanceof Error ? error.message : (error as string));
131
132     Logger.log("Failed to load github-cache.json");
133
134     try {
135       const cacheFile = readFileSync(
136         joinPosix(getDataRoot(), "github-cache.json")
137       );
138       const parsed = parseCacheJson(cacheFile.toString("utf-8"));
139       ret.data =
140         parsed[
141           `githubApiCache-${repository}-${dataType}` +
142             `${tag !== undefined ? "-" + tag : ""}`
143         ];
144
145       return ret;
146     } catch {
147       Logger.log("Failed to load local github-cache.json");
148
149       return undefined;
150     }
151   }
152 }
153
154 /**
155  * A simple cache for Github API responses to avoid hittign the rate limit,
156  * and to avoid putting unnecessary load on the Github servers.
157  */
158 export default class GithubApiCache {
159   private static instance?: GithubApiCache;
160   private globalState: Memento;
161
162   private constructor(context: ExtensionContext) {
163     this.globalState = context.globalState;
164   }
165
166   /**
167    * Creates a new instance of the GithubApiCache as singleton.
168    *
169    * @param context The extension context.
170    * @returns A reference to the created singleton instance.
171    */
172   public static createInstance(context: ExtensionContext): GithubApiCache {
173     GithubApiCache.instance = new GithubApiCache(context);
174
175     return GithubApiCache.instance;
176   }
177
178   /**
179    * Returns the singleton instance of the GithubApiCache if available.
180    *
181    * @throws An error if the singleton instance is not available. It can be created
182    * by calling createInstance() with the extension context.
183    * @returns The requested singleton instance or throws an error if the instance is not available.
184    */
185   public static getInstance(): GithubApiCache {
186     if (GithubApiCache.instance === undefined) {
187       throw new Error("GithubApiCache not initialized.");
188     }
189
190     return GithubApiCache.instance;
191   }
192
193   /**
194    * Saves the given response in the cache together with its etag.
195    *
196    * @param repository The repository the response is for.
197    * @param dataType The type of the request (for this repository) the response is from.
198    * @param data The response data to save.
199    * @param etag The etag of the response.
200    * @returns A promise that resolves when the response is saved.
201    */
202   public async saveResponse(
203     repository: GithubRepository,
204     dataType: GithubApiCacheEntryDataType,
205     data: GithubReleaseResponse | string[],
206     tag?: string,
207     etag?: string
208   ): Promise<void> {
209     if (etag === undefined) {
210       // TODO: Logger.warn
211       Logger.log("GithubApiCache.saveResponse: response without etag.");
212
213       return;
214     }
215
216     await this.globalState.update(
217       `githubApiCache-${repository}-${dataType}` +
218         `${tag !== undefined ? "-" + tag : ""}`,
219       {
220         repository,
221         dataType,
222         data,
223         etag,
224       } as GithubApiCacheEntry
225     );
226   }
227
228   public async getResponse(
229     repository: GithubRepository,
230     dataType: GithubApiCacheEntryDataType,
231     tag?: string
232   ): Promise<GithubApiCacheEntry | undefined> {
233     return this.globalState.get(
234       `githubApiCache-${repository}-${dataType}` +
235         `${tag !== undefined ? "-" + tag : ""}`
236     );
237   }
238
239   /**
240    * Returns the last etag of the given repository and data type.
241    *
242    * @param repository The repository to get the last etag for.
243    * @param dataType The type of request (for this repository) to get the last etag for.
244    * @returns The last etag of the given repository and data type combination or
245    * undefined if no etag is stored.
246    */
247   public async getLastEtag(
248     repository: GithubRepository,
249     dataType: GithubApiCacheEntryDataType,
250     tag?: string
251   ): Promise<string | undefined> {
252     const response = await this.getResponse(repository, dataType, tag);
253
254     return response?.etag;
255   }
256
257   public async getDefaultResponse(
258     repository: GithubRepository,
259     dataType: GithubApiCacheEntryDataType,
260     tag?: string
261   ): Promise<GithubApiCacheEntry | undefined> {
262     const lastEtag = await GithubApiCache.getInstance().getLastEtag(
263       repository,
264       dataType
265     );
266     if (lastEtag) {
267       return this.globalState.get(
268         `githubApiCache-${repository}-${dataType}` +
269           `${tag !== undefined ? "-" + tag : ""}`
270       );
271     } else {
272       return defaultCacheOfRepository(repository, dataType, tag);
273     }
274   }
275
276   /**
277    * Clears the github api cache.
278    */
279   public async clear(): Promise<void> {
280     // for all keys in global state starting with githubApiCache- delete
281     for (const key of this.globalState.keys()) {
282       if (key.startsWith("githubApiCache-")) {
283         await this.globalState.update(key, undefined);
284       }
285     }
286   }
287 }
This page took 0.039487 seconds and 4 git commands to generate.