| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // @dart = 2.8 |
| |
| library symbols_cache; |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| |
| import '../build.dart'; |
| |
| /// Caches symbols in a ".build-id" directory. The directory layout is |
| /// |
| /// ```txt |
| /// .build-id/12/3456.debug |
| /// => for an ELF with a BuildId of "123456" |
| /// ``` |
| /// |
| /// The cache will place in-progress downloads in |
| /// |
| /// ```txt |
| /// .build-id/123456.part |
| /// => for an ELF with a BuildId of "123456" |
| /// ``` |
| /// |
| /// before atomically moving it into `12/3456.debug` when finished. |
| class Cache { |
| /// Creates a cache at the specified directory. |
| /// The directory name is typically ".build-id". |
| Cache(this.directory); |
| |
| /// If there is an entry with the specified BuildId in the cache, |
| /// returns its absolute path. If not, returns `null`. |
| Future<String> getEntry(String buildId) async { |
| final file = pathForBuildId(buildId); |
| if (file.existsSync()) { |
| return file.absolute.path; |
| } |
| return null; |
| } |
| |
| /// Downloads contents from the stream, then atomically adds it to the cache |
| /// upon successs. |
| Future<String> addEntry(String buildId, Stream<List<int>> contents) async { |
| // Download to a temporary location first. |
| final tempFile = await _tempFile(buildId); |
| final sink = tempFile.file.openWrite(); |
| Object anyError; |
| // Here and below we are being intentionally catch-all in exception |
| // handling, to ensure that the temporary files always get cleaned up. |
| try { |
| await sink.addStream(contents); |
| await sink.flush(); |
| // ignore: avoid_catches_without_on_clauses |
| } catch (err) { |
| anyError = err; |
| } |
| // Regardless of error, always close the stream |
| try { |
| await sink.close(); |
| // ignore: avoid_catches_without_on_clauses |
| } catch (err) { |
| // ignore: empty_catches |
| } |
| if (anyError != null) { |
| try { |
| await tempFile.delete(); |
| // ignore: avoid_catches_without_on_clauses |
| } catch (err) { |
| // ignore: empty_catches |
| } |
| // ignore: only_throw_errors |
| throw anyError; |
| } |
| |
| // Then atomically move to the cache folder. |
| final file = pathForBuildId(buildId); |
| await file.parent.create(recursive: true); |
| await tempFile.move(file); |
| return file.absolute.path; |
| } |
| |
| File pathForBuildId(String buildId) { |
| final prefix = buildId.substring(0, 2); |
| final remaining = buildId.substring(2); |
| // ignore: avoid_as |
| return ((directory / Directory(prefix)) as Directory) / |
| File('$remaining.debug'); |
| } |
| |
| File pathForTempBuildId(String buildId) => directory / File('$buildId.part'); |
| |
| Future<TempFile> _tempFile(String buildId) async { |
| final file = pathForTempBuildId(buildId); |
| // Create parent directory. |
| await file.parent.create(recursive: true); |
| return TempFile(file); |
| } |
| |
| Directory directory; |
| } |
| |
| /// A temporary file. |
| class TempFile { |
| TempFile(this.file); |
| |
| Future<void> move(File destination) async { |
| await file.rename(destination.absolute.path); |
| } |
| |
| Future<void> delete() async { |
| await file.delete(recursive: true); |
| } |
| |
| File file; |
| } |