// Copyright 2014 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. // @dart = 2.8 import 'dart:collection'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:meta/meta.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/utils.dart'; import '../convert.dart'; import 'hash.dart'; /// An encoded representation of all file hashes. class FileStorage { FileStorage(this.version, this.files); factory FileStorage.fromBuffer(Uint8List buffer) { final Map<String, dynamic> json = castStringKeyedMap(jsonDecode(utf8.decode(buffer))); final int version = json['version'] as int; final List<Map<String, Object>> rawCachedFiles = (json['files'] as List<dynamic>).cast<Map<String, Object>>(); final List<FileHash> cachedFiles = <FileHash>[ for (final Map<String, Object> rawFile in rawCachedFiles) FileHash.fromJson(rawFile), ]; return FileStorage(version, cachedFiles); } final int version; final List<FileHash> files; List<int> toBuffer() { final Map<String, Object> json = <String, Object>{ 'version': version, 'files': <Object>[ for (final FileHash file in files) file.toJson(), ], }; return utf8.encode(jsonEncode(json)); } } /// A stored file hash and path. class FileHash { FileHash(this.path, this.hash); factory FileHash.fromJson(Map<String, Object> json) { return FileHash(json['path'] as String, json['hash'] as String); } final String path; final String hash; Object toJson() { return <String, Object>{ 'path': path, 'hash': hash, }; } } /// The strategy used by [FileStore] to determine if a file has been /// invalidated. enum FileStoreStrategy { /// The [FileStore] will compute an md5 hash of the file contents. hash, /// The [FileStore] will check for differences in the file's last modified /// timestamp. timestamp, } /// A globally accessible cache of files. /// /// In cases where multiple targets read the same source files as inputs, we /// avoid recomputing or storing multiple copies of hashes by delegating /// through this class. /// /// This class uses either timestamps or file hashes depending on the /// provided [FileStoreStrategy]. All information is held in memory during /// a build operation, and may be persisted to cache in the root build /// directory. /// /// The format of the file store is subject to change and not part of its API. class FileStore { FileStore({ @required File cacheFile, @required Logger logger, FileStoreStrategy strategy = FileStoreStrategy.hash, }) : _logger = logger, _strategy = strategy, _cacheFile = cacheFile; final File _cacheFile; final Logger _logger; final FileStoreStrategy _strategy; final HashMap<String, String> previousAssetKeys = HashMap<String, String>(); final HashMap<String, String> currentAssetKeys = HashMap<String, String>(); // The name of the file which stores the file hashes. static const String kFileCache = '.filecache'; // The current version of the file cache storage format. static const int _kVersion = 2; /// Read file hashes from disk. void initialize() { _logger.printTrace('Initializing file store'); if (!_cacheFile.existsSync()) { return; } Uint8List data; try { data = _cacheFile.readAsBytesSync(); } on FileSystemException catch (err) { _logger.printError( 'Failed to read file store at ${_cacheFile.path} due to $err.\n' 'Build artifacts will not be cached. Try clearing the cache directories ' 'with "flutter clean"', ); return; } FileStorage fileStorage; try { fileStorage = FileStorage.fromBuffer(data); } on Exception catch (err) { _logger.printTrace('Filestorage format changed: $err'); _cacheFile.deleteSync(); return; } if (fileStorage.version != _kVersion) { _logger.printTrace('file cache format updating, clearing old hashes.'); _cacheFile.deleteSync(); return; } for (final FileHash fileHash in fileStorage.files) { previousAssetKeys[fileHash.path] = fileHash.hash; } _logger.printTrace('Done initializing file store'); } /// Persist file marks to disk for a non-incremental build. void persist() { _logger.printTrace('Persisting file store'); if (!_cacheFile.existsSync()) { _cacheFile.createSync(recursive: true); } final List<FileHash> fileHashes = <FileHash>[]; for (final MapEntry<String, String> entry in currentAssetKeys.entries) { fileHashes.add(FileHash(entry.key, entry.value)); } final FileStorage fileStorage = FileStorage( _kVersion, fileHashes, ); final List<int> buffer = fileStorage.toBuffer(); try { _cacheFile.writeAsBytesSync(buffer); } on FileSystemException catch (err) { _logger.printError( 'Failed to persist file store at ${_cacheFile.path} due to $err.\n' 'Build artifacts will not be cached. Try clearing the cache directories ' 'with "flutter clean"', ); } _logger.printTrace('Done persisting file store'); } /// Reset `previousMarks` for an incremental build. void persistIncremental() { previousAssetKeys.clear(); previousAssetKeys.addAll(currentAssetKeys); currentAssetKeys.clear(); } /// Computes a diff of the provided files and returns a list of files /// that were dirty. List<File> diffFileList(List<File> files) { final List<File> dirty = <File>[]; switch (_strategy) { case FileStoreStrategy.hash: for (final File file in files) { _hashFile(file, dirty); } break; case FileStoreStrategy.timestamp: for (final File file in files) { _checkModification(file, dirty); } break; } return dirty; } void _checkModification(File file, List<File> dirty) { final String absolutePath = file.path; final String previousTime = previousAssetKeys[absolutePath]; // If the file is missing it is assumed to be dirty. if (!file.existsSync()) { currentAssetKeys.remove(absolutePath); previousAssetKeys.remove(absolutePath); dirty.add(file); return; } final String modifiedTime = file.lastModifiedSync().toString(); if (modifiedTime != previousTime) { dirty.add(file); } currentAssetKeys[absolutePath] = modifiedTime; } // 64k is the same sized buffer used by dart:io for `File.openRead`. static final Uint8List _readBuffer = Uint8List(64 * 1024); void _hashFile(File file, List<File> dirty) { final String absolutePath = file.path; final String previousHash = previousAssetKeys[absolutePath]; // If the file is missing it is assumed to be dirty. if (!file.existsSync()) { currentAssetKeys.remove(absolutePath); previousAssetKeys.remove(absolutePath); dirty.add(file); return; } final int fileBytes = file.lengthSync(); final Md5Hash hash = Md5Hash(); RandomAccessFile openFile; try { openFile = file.openSync(mode: FileMode.read); int bytes = 0; while (bytes < fileBytes) { final int bytesRead = openFile.readIntoSync(_readBuffer); hash.addChunk(_readBuffer, bytesRead); bytes += bytesRead; } } finally { openFile?.closeSync(); } final Digest digest = Digest(hash.finalize().buffer.asUint8List()); final String currentHash = digest.toString(); if (currentHash != previousHash) { dirty.add(file); } currentAssetKeys[absolutePath] = currentHash; } }