| // Copyright 2016 The Chromium 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:async'; |
| import 'dart:convert' show BASE64, UTF8; |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as path; |
| |
| import 'dart/package_map.dart'; |
| import 'asset.dart'; |
| import 'globals.dart'; |
| import 'observatory.dart'; |
| |
| // A file that has been added to a DevFS. |
| class DevFSEntry { |
| DevFSEntry(this.devicePath, this.file) |
| : bundleEntry = null; |
| |
| DevFSEntry.bundle(this.devicePath, AssetBundleEntry bundleEntry) |
| : bundleEntry = bundleEntry, |
| file = bundleEntry.file; |
| |
| final String devicePath; |
| final AssetBundleEntry bundleEntry; |
| |
| final File file; |
| FileStat _fileStat; |
| // When we updated the DevFS, did we see this entry? |
| bool _wasSeen = false; |
| DateTime get lastModified => _fileStat?.modified; |
| bool get stillExists { |
| if (_isSourceEntry) |
| return true; |
| _stat(); |
| return _fileStat.type != FileSystemEntityType.NOT_FOUND; |
| } |
| bool get isModified { |
| if (_isSourceEntry) |
| return true; |
| |
| if (_fileStat == null) { |
| _stat(); |
| return true; |
| } |
| FileStat _oldFileStat = _fileStat; |
| _stat(); |
| return _fileStat.modified.isAfter(_oldFileStat.modified); |
| } |
| |
| int get size { |
| if (_isSourceEntry) { |
| return bundleEntry.contentsLength; |
| } else { |
| if (_fileStat == null) { |
| _stat(); |
| } |
| return _fileStat.size; |
| } |
| } |
| |
| void _stat() { |
| if (_isSourceEntry) |
| return; |
| _fileStat = file.statSync(); |
| } |
| |
| bool get _isSourceEntry => file == null; |
| |
| Future<List<int>> contentsAsBytes() async { |
| if (_isSourceEntry) |
| return bundleEntry.contentsAsBytes(); |
| return file.readAsBytes(); |
| } |
| } |
| |
| |
| /// Abstract DevFS operations interface. |
| abstract class DevFSOperations { |
| Future<Uri> create(String fsName); |
| Future<dynamic> destroy(String fsName); |
| Future<dynamic> writeFile(String fsName, DevFSEntry entry); |
| Future<dynamic> deleteFile(String fsName, DevFSEntry entry); |
| Future<dynamic> writeSource(String fsName, |
| String devicePath, |
| String contents); |
| } |
| |
| /// An implementation of [DevFSOperations] that speaks to the |
| /// service protocol. |
| class ServiceProtocolDevFSOperations implements DevFSOperations { |
| final Observatory serviceProtocol; |
| |
| ServiceProtocolDevFSOperations(this.serviceProtocol); |
| |
| @override |
| Future<Uri> create(String fsName) async { |
| Response response = await serviceProtocol.createDevFS(fsName); |
| return Uri.parse(response['uri']); |
| } |
| |
| @override |
| Future<dynamic> destroy(String fsName) async { |
| await serviceProtocol.sendRequest('_deleteDevFS', |
| <String, dynamic> { 'fsName': fsName }); |
| } |
| |
| @override |
| Future<dynamic> writeFile(String fsName, DevFSEntry entry) async { |
| List<int> bytes; |
| try { |
| bytes = await entry.contentsAsBytes(); |
| } catch (e) { |
| return e; |
| } |
| String fileContents = BASE64.encode(bytes); |
| try { |
| return await serviceProtocol.sendRequest('_writeDevFSFile', |
| <String, dynamic> { |
| 'fsName': fsName, |
| 'path': entry.devicePath, |
| 'fileContents': fileContents |
| }); |
| } catch (e) { |
| printTrace('DevFS: Failed to write ${entry.devicePath}: $e'); |
| } |
| } |
| |
| @override |
| Future<dynamic> deleteFile(String fsName, DevFSEntry entry) async { |
| // TODO(johnmccutchan): Add file deletion to the devFS protocol. |
| } |
| |
| @override |
| Future<dynamic> writeSource(String fsName, |
| String devicePath, |
| String contents) async { |
| String fileContents = BASE64.encode(UTF8.encode(contents)); |
| return await serviceProtocol.sendRequest('_writeDevFSFile', |
| <String, dynamic> { |
| 'fsName': fsName, |
| 'path': devicePath, |
| 'fileContents': fileContents |
| }); |
| } |
| } |
| |
| class DevFS { |
| /// Create a [DevFS] named [fsName] for the local files in [directory]. |
| DevFS(Observatory serviceProtocol, |
| this.fsName, |
| this.rootDirectory) |
| : _operations = new ServiceProtocolDevFSOperations(serviceProtocol); |
| |
| DevFS.operations(this._operations, |
| this.fsName, |
| this.rootDirectory); |
| |
| final DevFSOperations _operations; |
| final String fsName; |
| final Directory rootDirectory; |
| final Map<String, DevFSEntry> _entries = <String, DevFSEntry>{}; |
| final List<Future<Response>> _pendingWrites = new List<Future<Response>>(); |
| int _bytes = 0; |
| int get bytes => _bytes; |
| Uri _baseUri; |
| Uri get baseUri => _baseUri; |
| |
| Future<Uri> create() async { |
| _baseUri = await _operations.create(fsName); |
| printTrace('DevFS: Created new filesystem on the device ($_baseUri)'); |
| return _baseUri; |
| } |
| |
| Future<dynamic> destroy() async { |
| printTrace('DevFS: Deleted filesystem on the device ($_baseUri)'); |
| return await _operations.destroy(fsName); |
| } |
| |
| Future<dynamic> update([AssetBundle bundle = null]) async { |
| _bytes = 0; |
| // Mark all entries as not seen. |
| _entries.forEach((String path, DevFSEntry entry) { |
| entry._wasSeen = false; |
| }); |
| printTrace('DevFS: Starting sync from $rootDirectory'); |
| // Send the root and lib directories. |
| Directory directory = rootDirectory; |
| _syncDirectory(directory, recursive: true); |
| String packagesFilePath = path.join(rootDirectory.path, kPackagesFileName); |
| StringBuffer sb; |
| // Send the packages. |
| if (FileSystemEntity.isFileSync(packagesFilePath)) { |
| PackageMap packageMap = new PackageMap(kPackagesFileName); |
| |
| for (String packageName in packageMap.map.keys) { |
| Uri uri = packageMap.map[packageName]; |
| // Ignore self-references. |
| if (uri.toString() == 'lib/') |
| continue; |
| Directory directory = new Directory.fromUri(uri); |
| if (_syncDirectory(directory, |
| directoryName: 'packages/$packageName', |
| recursive: true)) { |
| if (sb == null) { |
| sb = new StringBuffer(); |
| } |
| sb.writeln('$packageName:packages/$packageName'); |
| } |
| } |
| } |
| if (bundle != null) { |
| // Synchronize asset bundle. |
| for (AssetBundleEntry entry in bundle.entries) { |
| // We write the assets into 'build/flx' so that they are in the |
| // same location in DevFS and the iOS simulator. |
| final String devicePath = path.join('build/flx', entry.archivePath); |
| _syncBundleEntry(devicePath, entry); |
| } |
| } |
| // Handle deletions. |
| final List<String> toRemove = new List<String>(); |
| _entries.forEach((String path, DevFSEntry entry) { |
| if (!entry._wasSeen) { |
| _deleteEntry(path, entry); |
| toRemove.add(path); |
| } |
| }); |
| for (int i = 0; i < toRemove.length; i++) { |
| _entries.remove(toRemove[i]); |
| } |
| // Send the assets. |
| printTrace('DevFS: Waiting for sync of ${_pendingWrites.length} files ' |
| 'to finish'); |
| await Future.wait(_pendingWrites); |
| _pendingWrites.clear(); |
| if (sb != null) { |
| await _operations.writeSource(fsName, '.packages', sb.toString()); |
| } |
| printTrace('DevFS: Sync finished'); |
| // NB: You must call flush after a printTrace if you want to be printed |
| // immediately. |
| logger.flush(); |
| } |
| |
| void _deleteEntry(String path, DevFSEntry entry) { |
| _pendingWrites.add(_operations.deleteFile(fsName, entry)); |
| } |
| |
| void _syncFile(String devicePath, File file) { |
| DevFSEntry entry = _entries[devicePath]; |
| if (entry == null) { |
| // New file. |
| entry = new DevFSEntry(devicePath, file); |
| _entries[devicePath] = entry; |
| } |
| entry._wasSeen = true; |
| bool needsWrite = entry.isModified; |
| if (needsWrite) { |
| _bytes += entry.size; |
| Future<dynamic> pendingWrite = _operations.writeFile(fsName, entry); |
| if (pendingWrite != null) { |
| _pendingWrites.add(pendingWrite); |
| } else { |
| printTrace('DevFS: Failed to sync "$devicePath"'); |
| } |
| } |
| } |
| |
| void _syncBundleEntry(String devicePath, AssetBundleEntry assetBundleEntry) { |
| DevFSEntry entry = _entries[devicePath]; |
| if (entry == null) { |
| // New file. |
| entry = new DevFSEntry.bundle(devicePath, assetBundleEntry); |
| _entries[devicePath] = entry; |
| } |
| entry._wasSeen = true; |
| bool needsWrite = entry.isModified; |
| if (needsWrite) { |
| _bytes += entry.size; |
| Future<dynamic> pendingWrite = _operations.writeFile(fsName, entry); |
| if (pendingWrite != null) { |
| _pendingWrites.add(pendingWrite); |
| } else { |
| printTrace('DevFS: Failed to sync "$devicePath"'); |
| } |
| } |
| } |
| |
| bool _shouldIgnore(String devicePath) { |
| List<String> ignoredPrefixes = <String>['android/', |
| 'build/', |
| 'ios/', |
| 'packages/analyzer']; |
| for (String ignoredPrefix in ignoredPrefixes) { |
| if (devicePath.startsWith(ignoredPrefix)) |
| return true; |
| } |
| return false; |
| } |
| |
| bool _syncDirectory(Directory directory, |
| {String directoryName, |
| bool recursive: false, |
| bool ignoreDotFiles: true}) { |
| String prefix = directoryName; |
| if (prefix == null) { |
| prefix = path.relative(directory.path, from: rootDirectory.path); |
| if (prefix == '.') |
| prefix = ''; |
| } |
| try { |
| List<FileSystemEntity> files = |
| directory.listSync(recursive: recursive, followLinks: false); |
| for (FileSystemEntity file in files) { |
| if (file is! File) { |
| // Skip non-files. |
| continue; |
| } |
| if (ignoreDotFiles && path.basename(file.path).startsWith('.')) { |
| // Skip dot files. |
| continue; |
| } |
| final String devicePath = |
| path.join(prefix, path.relative(file.path, from: directory.path)); |
| if (!_shouldIgnore(devicePath)) |
| _syncFile(devicePath, file); |
| } |
| } catch (e) { |
| // Ignore directory and error. |
| return false; |
| } |
| return true; |
| } |
| } |