| // Copyright 2013 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:async'; |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as p; |
| |
| import '../logger.dart'; |
| import 'command.dart'; |
| import 'flags.dart'; |
| |
| /// The `cleanup` command for removing unnecessary on-disk artifacts. |
| final class CleanupCommand extends CommandBase { |
| /// Constructs the `cleanup` command. |
| CleanupCommand({ |
| required super.environment, |
| super.help = false, |
| super.usageLineLength, |
| }) { |
| argParser.addFlag( |
| dryRunFlag, |
| abbr: 'd', |
| help: 'Write changes to stdout without modifying the file system.', |
| negatable: false, |
| ); |
| |
| argParser.addOption( |
| 'untouched-since', |
| defaultsTo: () { |
| const thirtyDays = Duration(days: 30); |
| final dateTime = environment.now().subtract(thirtyDays); |
| return _toDateString(dateTime); |
| }(), |
| help: 'What date to consider artifacts old enough to safely remove.', |
| valueHelp: 'YYYY-MM-DD', |
| ); |
| } |
| |
| @override |
| String get name => 'cleanup'; |
| |
| @override |
| String get description => 'Removes stale or unnecessary on-disk artifacts.'; |
| |
| @override |
| List<String> get aliases => const ['gc']; |
| |
| static final _dateString = RegExp(r'^(\d{4})-(\d{2})-(\d{2})$'); |
| |
| @override |
| Future<int> run() async { |
| final dryRun = argResults!.flag('dry-run'); |
| final since = () { |
| final yyyyMmDd = argResults!.option('untouched-since')!; |
| final dateMatch = _dateString.matchAsPrefix(yyyyMmDd); |
| if (dateMatch == null) { |
| throw FatalError('Invalid --untouched-since: $yyyyMmDd'); |
| } |
| return DateTime( |
| int.parse(dateMatch.group(1)!), |
| int.parse(dateMatch.group(2)!), |
| int.parse(dateMatch.group(3)!), |
| ); |
| }(); |
| |
| // Look at the directories in "out" for ones older than "since". |
| environment.logger.status('Checking ${environment.engine.outDir.path}...'); |
| final toDelete = [ |
| await for (final entity in environment.engine.outDir.list()) |
| if (entity is Directory && |
| _shouldDelete(entity, ifAccessedLaterThan: since)) |
| entity |
| ]..sort((a, b) => a.path.compareTo(b.path)); |
| |
| if (toDelete.isEmpty) { |
| environment.logger.status( |
| 'No directories were accessed later than ${_toDateString(since)}.', |
| ); |
| return 0; |
| } |
| |
| final totalSize = toDelete.fold(0, (p, n) => p + _getSizeRecursive(n)); |
| |
| if (dryRun) { |
| environment.logger.status( |
| 'The following directories were accessed later than ${_toDateString(since)}:', |
| ); |
| for (final e in toDelete) { |
| environment.logger.status(' ${p.basename(e.path)}'); |
| } |
| environment.logger.status( |
| 'Run without --dry-run to reclaim ' |
| '${_toReadableBytes(totalSize.toDouble())}.', |
| ); |
| return 0; |
| } |
| |
| final spinner = environment.logger.startSpinner(); |
| for (final e in toDelete) { |
| try { |
| await e.delete(recursive: true); |
| } on FileSystemException catch (_) { |
| environment.logger.warning('Failed to delete ${p.basename(e.path)}'); |
| } |
| } |
| spinner.finish(); |
| |
| environment.logger.status( |
| 'Deleted ${toDelete.length} output directories and reclaimed ' |
| '${_toReadableBytes(totalSize.toDouble())}.', |
| ); |
| |
| return 0; |
| } |
| |
| static bool _shouldDelete( |
| Directory entity, { |
| required DateTime ifAccessedLaterThan, |
| }) { |
| final accessed = entity.statSync().accessed; |
| return accessed.isBefore(ifAccessedLaterThan); |
| } |
| } |
| |
| int _getSizeRecursive(Directory dir) { |
| return dir.listSync().fold(0, (p, n) => p + n.statSync().size); |
| } |
| |
| String _toDateString(DateTime dateTime) { |
| final output = StringBuffer('${dateTime.year}-'); |
| output.write(dateTime.month.toString().padLeft(2, '0')); |
| output.write('-'); |
| output.write(dateTime.day.toString().padLeft(2, '0')); |
| return output.toString(); |
| } |
| |
| String _toReadableBytes(double bytes) { |
| var type = _FileSize.bytes; |
| if (bytes >= 1024) { |
| type = _FileSize.kilobytes; |
| bytes = bytes / 1024; |
| } |
| if (bytes >= 1024) { |
| type = _FileSize.megabytes; |
| bytes = bytes / 1024; |
| } |
| if (bytes >= 1024) { |
| type = _FileSize.gigabytes; |
| bytes = bytes / 1024; |
| } |
| return '${bytes.toStringAsFixed(2)}${type.suffix}'; |
| } |
| |
| enum _FileSize { |
| bytes('bytes'), |
| kilobytes('KB'), |
| megabytes('MB'), |
| gigabytes('GB'); |
| |
| const _FileSize(this.suffix); |
| final String suffix; |
| } |