From f0081e058709b3ac8c3a7459dbc27d988db4e700 Mon Sep 17 00:00:00 2001 From: Junyuan Feng Date: Thu, 10 Mar 2022 15:25:14 +0800 Subject: [PATCH] Support APT/Docker --- lib/core/extension/uint8list.dart | 6 ++- lib/data/provider/apt.dart | 60 +++++++++++++++------- lib/data/provider/docker.dart | 24 ++++++--- lib/data/provider/server.dart | 14 +++--- lib/data/res/build_data.dart | 8 +-- lib/view/page/apt.dart | 83 ++++++++++++++++++------------- lib/view/page/docker.dart | 6 +-- lib/view/page/server/tab.dart | 7 +-- lib/view/page/sftp.dart | 19 ++++--- pubspec.lock | 9 +--- 10 files changed, 143 insertions(+), 93 deletions(-) diff --git a/lib/core/extension/uint8list.dart b/lib/core/extension/uint8list.dart index 222c67db..ee330f55 100644 --- a/lib/core/extension/uint8list.dart +++ b/lib/core/extension/uint8list.dart @@ -1,6 +1,10 @@ import 'dart:convert'; import 'dart:typed_data'; -extension Uint8ListX on Future { +extension FutureUint8ListX on Future { Future get string async => utf8.decode(await this); } + +extension Uint8ListX on Uint8List { + String get string => utf8.decode(this); +} diff --git a/lib/data/provider/apt.dart b/lib/data/provider/apt.dart index 262186c6..5d7ad58e 100644 --- a/lib/data/provider/apt.dart +++ b/lib/data/provider/apt.dart @@ -1,6 +1,5 @@ -import 'dart:convert'; - import 'package:dartssh2/dartssh2.dart'; +import 'package:toolbox/core/extension/uint8list.dart'; import 'package:toolbox/core/provider_base.dart'; import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart'; import 'package:toolbox/data/model/distribution.dart'; @@ -8,41 +7,68 @@ import 'package:toolbox/data/model/distribution.dart'; class AptProvider extends BusyProvider { SSHClient? client; Distribution? dist; + String? whoami; List? upgradeable; + String? error; + String? updateLog; AptProvider(); - void init(SSHClient client, Distribution dist) { + Future init(SSHClient client, Distribution dist) async { this.client = client; this.dist = dist; + whoami = (await client.run('whoami').string).trim(); } + bool get isSU => whoami == 'root'; + void clear() { client = null; dist = null; upgradeable = null; - } - - bool get isReady { - return upgradeable != null && !isBusy; + error = null; + updateLog = null; + whoami = null; } Future refreshInstalled() async { + if (client == null) { + error = 'No client'; + return; + } await update(); - final result = utf8.decode(await client!.run('apt list --upgradeable')); - final list = result.split('\n').sublist(4); - list.removeWhere((element) => element.isEmpty); - upgradeable = list.map((e) => AptUpgradePkgInfo(e, dist!)).toList(); - notifyListeners(); + final result = await client!.run('apt list --upgradeable').string; + try { + final list = result.split('\n').sublist(4); + list.removeWhere((element) => element.isEmpty); + upgradeable = list.map((e) => AptUpgradePkgInfo(e, dist!)).toList(); + } catch (e) { + error = e.toString(); + } finally { + notifyListeners(); + } } Future update() async { + if (client == null) { + error = 'No client'; + return; + } await client!.run('apt update'); } - // Future upgrade() async { - // setBusyState(); - // await client!.run('apt upgrade -y'); - // refreshInstalled(); - // } + Future upgrade() async { + if (client == null) { + error = 'No client'; + return; + } + updateLog = null; + + final session = await client!.execute('apt upgrade -y'); + session.stdout.listen((data) { + updateLog = (updateLog ?? '') + data.string; + notifyListeners(); + }); + refreshInstalled(); + } } diff --git a/lib/data/provider/docker.dart b/lib/data/provider/docker.dart index 2df355b6..46aaf09a 100644 --- a/lib/data/provider/docker.dart +++ b/lib/data/provider/docker.dart @@ -33,8 +33,12 @@ class DockerProvider extends BusyProvider { error = 'invalid version'; notifyListeners(); } else { - version = verSplit[1].split(' ').last; - edition = verSplit[0].split(': ')[1]; + try { + version = verSplit[1].split(' ').last; + edition = verSplit[0].split(': ')[1]; + } catch (e) { + error = e.toString(); + } } final raw = await client!.run('docker ps -a').string; @@ -43,11 +47,17 @@ class DockerProvider extends BusyProvider { notifyListeners(); return; } - final lines = raw.split('\n'); - lines.removeAt(0); - lines.removeWhere((element) => element.isEmpty); - running = lines.map((e) => DockerPsItem.fromRawString(e)).toList(); - notifyListeners(); + + try { + final lines = raw.split('\n'); + lines.removeAt(0); + lines.removeWhere((element) => element.isEmpty); + running = lines.map((e) => DockerPsItem.fromRawString(e)).toList(); + } catch (e) { + error = e.toString(); + } finally { + notifyListeners(); + } } Future stop(String id) async { diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index 8327e2ae..7a767d28 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'dart:convert'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:toolbox/core/extension/stringx.dart'; +import 'package:toolbox/core/extension/uint8list.dart'; import 'package:toolbox/core/provider_base.dart'; import 'package:toolbox/data/model/server/cpu_2_status.dart'; import 'package:toolbox/data/model/server/cpu_status.dart'; @@ -178,8 +178,10 @@ class ServerProvider extends BusyProvider { final si = _servers[idx]; try { if (si.client == null) return; - final raw = utf8.decode(await si.client!.run( - r"cat /proc/net/dev && date +%s && echo 'A====A' && cat /etc/os-release | grep PRETTY_NAME && echo 'A====A' && cat /proc/stat | grep cpu && echo 'A====A' && paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed 's/\(.\)..$/.\1°C/' && echo 'A====A' && uptime && echo 'A====A' && cat /proc/net/snmp && echo 'A====A' && df -h && echo 'A====A' && free -m")); + final raw = await si.client! + .run( + r"cat /proc/net/dev && date +%s && echo 'A====A' && cat /etc/os-release | grep PRETTY_NAME && echo 'A====A' && cat /proc/stat | grep cpu && echo 'A====A' && paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed 's/\(.\)..$/.\1°C/' && echo 'A====A' && uptime && echo 'A====A' && cat /proc/net/snmp && echo 'A====A' && df -h && echo 'A====A' && free -m") + .string; final lines = raw.split('A====A').map((e) => e.trim()).toList(); _getCPU(spi, lines[2], lines[3]); _getMem(spi, lines[7]); @@ -321,10 +323,10 @@ class ServerProvider extends BusyProvider { } Future runSnippet(ServerPrivateInfo spi, Snippet snippet) async { - final result = await _servers + return await _servers .firstWhere((element) => element.info == spi) .client! - .run(snippet.script); - return utf8.decode(result); + .run(snippet.script) + .string; } } diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index 213eafe8..4bcc21b2 100644 --- a/lib/data/res/build_data.dart +++ b/lib/data/res/build_data.dart @@ -2,9 +2,9 @@ class BuildData { static const String name = "ServerBox"; - static const int build = 106; + static const int build = 107; static const String engine = - "Flutter 2.10.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 7e9793dee1 (6 days ago) • 2022-03-02 11:23:12 -0600\nEngine • revision bd539267b4\nTools • Dart 2.16.1 • DevTools 2.9.2\n"; - static const String buildAt = "2022-03-08 18:06:40.014600"; - static const int modifications = 8; + "Flutter 2.10.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 7e9793dee1 (8 days ago) • 2022-03-02 11:23:12 -0600\nEngine • revision bd539267b4\nTools • Dart 2.16.1 • DevTools 2.9.2\n"; + static const String buildAt = "2022-03-10 13:25:24.362670"; + static const int modifications = 11; } diff --git a/lib/view/page/apt.dart b/lib/view/page/apt.dart index 643034e6..a3a8ca23 100644 --- a/lib/view/page/apt.dart +++ b/lib/view/page/apt.dart @@ -7,6 +7,7 @@ import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/provider/apt.dart'; import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/locator.dart'; +import 'package:toolbox/view/widget/center_loading.dart'; import 'package:toolbox/view/widget/round_rect_card.dart'; import 'package:toolbox/view/widget/two_line_text.dart'; @@ -55,61 +56,75 @@ class _AptManagePageState extends State return Scaffold( appBar: AppBar( centerTitle: true, - title: TwoLineText(up: 'Apt', down: widget.spi.ip), - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () { - locator().refreshInstalled(); - }, - ), - ], + title: TwoLineText(up: 'Apt', down: widget.spi.name), ), body: Consumer(builder: (_, apt, __) { if (apt.upgradeable == null) { apt.refreshInstalled(); + return centerLoading; + } + if (!apt.isSU) { + return Center( + child: Text( + 'Only supported as root. Not "${apt.whoami}".', + style: greyStyle, + ), + ); } return ListView( padding: const EdgeInsets.all(13), - children: [_buildUpdatePanel(apt)], + children: + [_buildUpdatePanel(apt)].map((e) => RoundRectCard(e)).toList(), ); }), ); } Widget _buildUpdatePanel(AptProvider apt) { + if (apt.upgradeable!.isEmpty) { + return const ListTile( + title: Text( + 'No update available', + textAlign: TextAlign.center, + ), + subtitle: Text('>_<', textAlign: TextAlign.center), + ); + } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RoundRectCard(ExpansionTile( - title: Text(!apt.isReady - ? 'Loading...' - : 'Found ${apt.upgradeable!.length} update'), - subtitle: !apt.isReady - ? null - : Text( - apt.upgradeable!.map((e) => e.package).join(', '), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: greyStyle, - ), - children: [ - // TextButton( - // child: Text('Update all'), - // onPressed: () { - // apt.upgrade(); - // }), - !apt.isReady - ? const SizedBox() - : SizedBox( - height: _media.size.height * 0.23, + ExpansionTile( + title: Text('Found ${apt.upgradeable!.length} update'), + subtitle: Text( + apt.upgradeable!.map((e) => e.package).join(', '), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: greyStyle, + ), + children: apt.updateLog == null + ? [ + TextButton( + child: const Text('Update all'), + onPressed: () { + apt.upgrade(); + }), + SizedBox( + height: _media.size.height * 0.73, child: ListView( children: apt.upgradeable! .map((e) => _buildUpdateItem(e, apt)) .toList()), ) - ], - )) + ] + : [ + SizedBox( + height: _media.size.height * 0.73, + child: ListView( + padding: const EdgeInsets.all(18), + children: [Text(apt.updateLog!)], + )) + ], + ) ], ); } diff --git a/lib/view/page/docker.dart b/lib/view/page/docker.dart index c6a9e102..a1c2e9ff 100644 --- a/lib/view/page/docker.dart +++ b/lib/view/page/docker.dart @@ -58,7 +58,7 @@ class _DockerManagePageState extends State { return Scaffold( appBar: AppBar( centerTitle: true, - title: TwoLineText(up: 'Docker', down: widget.spi.ip), + title: TwoLineText(up: 'Docker', down: widget.spi.name), ), body: _buildMain(), ); @@ -110,7 +110,7 @@ class _DockerManagePageState extends State { Widget _buildVersion(String edition, String version) { return Padding( - padding: const EdgeInsets.all(13), + padding: const EdgeInsets.all(17), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text(edition), Text(version)], @@ -172,7 +172,7 @@ class _DockerManagePageState extends State { }, itemHeight: 37, itemPadding: const EdgeInsets.only(left: 17, right: 17), - dropdownWidth: 160, + dropdownWidth: 133, dropdownDecoration: BoxDecoration( borderRadius: BorderRadius.circular(7), ), diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index 07142f65..efca2a99 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -245,12 +245,7 @@ class _ServerPageState extends State AppRoute(AptManagePage(spi), 'apt manage page').go(context); break; case ServerTabMenuItems.sftp: - AppRoute( - SFTPPage( - spi: spi, - ), - 'SFTP') - .go(context); + AppRoute(SFTPPage(spi), 'SFTP').go(context); break; case ServerTabMenuItems.snippet: AppRoute( diff --git a/lib/view/page/sftp.dart b/lib/view/page/sftp.dart index fdc69163..31eee464 100644 --- a/lib/view/page/sftp.dart +++ b/lib/view/page/sftp.dart @@ -11,8 +11,8 @@ import 'package:toolbox/view/widget/fade_in.dart'; import 'package:toolbox/view/widget/two_line_text.dart'; class SFTPPage extends StatefulWidget { - final ServerPrivateInfo? spi; - const SFTPPage({this.spi, Key? key}) : super(key: key); + final ServerPrivateInfo spi; + const SFTPPage(this.spi, {Key? key}) : super(key: key); @override _SFTPPageState createState() => _SFTPPageState(); @@ -34,10 +34,8 @@ class _SFTPPageState extends State { @override void initState() { super.initState(); - if (widget.spi != null) { - _status.spi = widget.spi!; - _status.selected = true; - } + _status.spi = widget.spi; + _status.selected = true; } @override @@ -45,7 +43,7 @@ class _SFTPPageState extends State { return Scaffold( appBar: AppBar( centerTitle: true, - title: TwoLineText(up: 'SFTP', down: _status.spi?.ip ?? '...'), + title: TwoLineText(up: 'SFTP', down: widget.spi.name), ), body: _buildFileView(), ); @@ -98,6 +96,13 @@ class _SFTPPageState extends State { return ListTile( leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file), title: Text(file.filename), + trailing: Text( + DateTime.fromMillisecondsSinceEpoch( + (file.attr.modifyTime ?? 0) * 1000) + .toString() + .replaceFirst('.000', ''), + style: const TextStyle(color: Colors.grey), + ), subtitle: isDir ? null : Text(convertBytes(file.attr.size ?? 0)), onTap: () { diff --git a/pubspec.lock b/pubspec.lock index cdb53bc2..2716724a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -335,13 +335,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.3" meta: dependency: transitive description: @@ -528,7 +521,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.3" typed_data: dependency: transitive description: