| // 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 String k in key) { |
| verifyStringSanity(k, const <String>{'\x00', '/', cacheSeparator}); |
| } |
| final String 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 File cacheFile = cacheFileFor(cacheDirectory, key); |
| final RandomAccessFile cacheFileContents = await cacheFile.open(mode: FileMode.append); |
| bool 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 String cacheData = utf8.decode(await cacheFileContents.read(await cacheFileContents.length())); |
| final int firstLineBreak = cacheData.indexOf('\n'); |
| bool needsReplacing = true; |
| if (cacheEpoch != null) { |
| if (firstLineBreak > 0) { |
| final int? cacheTimeInMilliseconds = int.tryParse(cacheData.substring(0, firstLineBreak), radix: 10); |
| if (cacheTimeInMilliseconds != null) { |
| final DateTime 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(); |
| } |
| } |