| // Copyright 2023 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'package:github/github.dart'; | |
| import 'utils.dart'; | |
| // Returns the data from the network, in the form used in the cache. | |
| // | |
| // It's a bit inefficient to have the data be serialized to string and then | |
| // immediately reparsed, but it avoids any issues where the cache is interpreted | |
| // differently than the original data. Since this code is not performance sensitive, | |
| // the sanity is more important. | |
| typedef PopulateCacheCallback = Future<String> Function(); | |
| const String cacheSeparator = ':'; | |
| File cacheFileFor(final Directory cacheDirectory, final List<String> key) { | |
| for (final k in key) { | |
| verifyStringSanity(k, const <String>{'\x00', '/', cacheSeparator}); | |
| } | |
| final cacheName = key.join(cacheSeparator); | |
| return File('${cacheDirectory.path}/$cacheName'); | |
| } | |
| typedef Parser<T> = T? Function(String); | |
| Future<T?> readFromFile<T>(final File file, final Parser<T> parser) async { | |
| try { | |
| return parser(await file.readAsString()); | |
| } on FileSystemException { | |
| if (await file.exists()) { | |
| rethrow; | |
| } | |
| } | |
| return null; | |
| } | |
| Future<String> loadFromCache( | |
| final Directory cacheDirectory, | |
| final GitHub github, | |
| final List<String> key, | |
| final DateTime? cacheEpoch, | |
| final PopulateCacheCallback callback, | |
| ) async { | |
| final cacheFile = cacheFileFor(cacheDirectory, key); | |
| final cacheFileContents = await cacheFile.open(mode: FileMode.append); | |
| var firstWait = true; | |
| while (true) { | |
| try { | |
| await cacheFileContents.lock(); | |
| break; | |
| } on FileSystemException catch (e) { | |
| if (e.osError?.errorCode == 11) { | |
| if (firstWait) { | |
| print('\x1B[KWaiting for lock on ${cacheFile.path}'); | |
| firstWait = false; | |
| } | |
| await Future<void>.delayed(const Duration(seconds: 1)); | |
| continue; | |
| } | |
| rethrow; | |
| } | |
| } | |
| try { | |
| await cacheFileContents.setPosition(0); | |
| final cacheData = utf8.decode( | |
| await cacheFileContents.read(await cacheFileContents.length()), | |
| ); | |
| final firstLineBreak = cacheData.indexOf('\n'); | |
| var needsReplacing = true; | |
| if (cacheEpoch != null) { | |
| if (firstLineBreak > 0) { | |
| final cacheTimeInMilliseconds = int.tryParse( | |
| cacheData.substring(0, firstLineBreak), | |
| radix: 10, | |
| ); | |
| if (cacheTimeInMilliseconds != null) { | |
| final cacheTime = DateTime.fromMillisecondsSinceEpoch( | |
| cacheTimeInMilliseconds, | |
| ); | |
| if (cacheTime.isAfter(cacheEpoch)) { | |
| needsReplacing = false; | |
| } | |
| } | |
| } | |
| } | |
| if (needsReplacing) { | |
| final String data; | |
| try { | |
| data = await callback(); | |
| } on Exception { | |
| await cacheFile.delete(); | |
| rethrow; | |
| } | |
| await cacheFileContents.truncate(0); | |
| await cacheFileContents.setPosition(0); | |
| await cacheFileContents.writeString( | |
| '${DateTime.now().millisecondsSinceEpoch}\n', | |
| ); | |
| await cacheFileContents.writeString(data); | |
| return data; | |
| } | |
| return cacheData.substring(firstLineBreak + 1); | |
| } finally { | |
| await cacheFileContents.unlock(); | |
| await cacheFileContents.close(); | |
| } | |
| } |