Support APT/Docker
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
extension Uint8ListX on Future<Uint8List> {
|
extension FutureUint8ListX on Future<Uint8List> {
|
||||||
Future<String> get string async => utf8.decode(await this);
|
Future<String> get string async => utf8.decode(await this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Uint8ListX on Uint8List {
|
||||||
|
String get string => utf8.decode(this);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
|
import 'package:toolbox/core/extension/uint8list.dart';
|
||||||
import 'package:toolbox/core/provider_base.dart';
|
import 'package:toolbox/core/provider_base.dart';
|
||||||
import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart';
|
import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart';
|
||||||
import 'package:toolbox/data/model/distribution.dart';
|
import 'package:toolbox/data/model/distribution.dart';
|
||||||
@@ -8,41 +7,68 @@ import 'package:toolbox/data/model/distribution.dart';
|
|||||||
class AptProvider extends BusyProvider {
|
class AptProvider extends BusyProvider {
|
||||||
SSHClient? client;
|
SSHClient? client;
|
||||||
Distribution? dist;
|
Distribution? dist;
|
||||||
|
String? whoami;
|
||||||
List<AptUpgradePkgInfo>? upgradeable;
|
List<AptUpgradePkgInfo>? upgradeable;
|
||||||
|
String? error;
|
||||||
|
String? updateLog;
|
||||||
|
|
||||||
AptProvider();
|
AptProvider();
|
||||||
|
|
||||||
void init(SSHClient client, Distribution dist) {
|
Future<void> init(SSHClient client, Distribution dist) async {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.dist = dist;
|
this.dist = dist;
|
||||||
|
whoami = (await client.run('whoami').string).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isSU => whoami == 'root';
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
client = null;
|
client = null;
|
||||||
dist = null;
|
dist = null;
|
||||||
upgradeable = null;
|
upgradeable = null;
|
||||||
}
|
error = null;
|
||||||
|
updateLog = null;
|
||||||
bool get isReady {
|
whoami = null;
|
||||||
return upgradeable != null && !isBusy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshInstalled() async {
|
Future<void> refreshInstalled() async {
|
||||||
|
if (client == null) {
|
||||||
|
error = 'No client';
|
||||||
|
return;
|
||||||
|
}
|
||||||
await update();
|
await update();
|
||||||
final result = utf8.decode(await client!.run('apt list --upgradeable'));
|
final result = await client!.run('apt list --upgradeable').string;
|
||||||
final list = result.split('\n').sublist(4);
|
try {
|
||||||
list.removeWhere((element) => element.isEmpty);
|
final list = result.split('\n').sublist(4);
|
||||||
upgradeable = list.map((e) => AptUpgradePkgInfo(e, dist!)).toList();
|
list.removeWhere((element) => element.isEmpty);
|
||||||
notifyListeners();
|
upgradeable = list.map((e) => AptUpgradePkgInfo(e, dist!)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.toString();
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> update() async {
|
Future<void> update() async {
|
||||||
|
if (client == null) {
|
||||||
|
error = 'No client';
|
||||||
|
return;
|
||||||
|
}
|
||||||
await client!.run('apt update');
|
await client!.run('apt update');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<void> upgrade() async {
|
Future<void> upgrade() async {
|
||||||
// setBusyState();
|
if (client == null) {
|
||||||
// await client!.run('apt upgrade -y');
|
error = 'No client';
|
||||||
// refreshInstalled();
|
return;
|
||||||
// }
|
}
|
||||||
|
updateLog = null;
|
||||||
|
|
||||||
|
final session = await client!.execute('apt upgrade -y');
|
||||||
|
session.stdout.listen((data) {
|
||||||
|
updateLog = (updateLog ?? '') + data.string;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
refreshInstalled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,12 @@ class DockerProvider extends BusyProvider {
|
|||||||
error = 'invalid version';
|
error = 'invalid version';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} else {
|
} else {
|
||||||
version = verSplit[1].split(' ').last;
|
try {
|
||||||
edition = verSplit[0].split(': ')[1];
|
version = verSplit[1].split(' ').last;
|
||||||
|
edition = verSplit[0].split(': ')[1];
|
||||||
|
} catch (e) {
|
||||||
|
error = e.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final raw = await client!.run('docker ps -a').string;
|
final raw = await client!.run('docker ps -a').string;
|
||||||
@@ -43,11 +47,17 @@ class DockerProvider extends BusyProvider {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final lines = raw.split('\n');
|
|
||||||
lines.removeAt(0);
|
try {
|
||||||
lines.removeWhere((element) => element.isEmpty);
|
final lines = raw.split('\n');
|
||||||
running = lines.map((e) => DockerPsItem.fromRawString(e)).toList();
|
lines.removeAt(0);
|
||||||
notifyListeners();
|
lines.removeWhere((element) => element.isEmpty);
|
||||||
|
running = lines.map((e) => DockerPsItem.fromRawString(e)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
error = e.toString();
|
||||||
|
} finally {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> stop(String id) async {
|
Future<bool> stop(String id) async {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:toolbox/core/extension/stringx.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/core/provider_base.dart';
|
||||||
import 'package:toolbox/data/model/server/cpu_2_status.dart';
|
import 'package:toolbox/data/model/server/cpu_2_status.dart';
|
||||||
import 'package:toolbox/data/model/server/cpu_status.dart';
|
import 'package:toolbox/data/model/server/cpu_status.dart';
|
||||||
@@ -178,8 +178,10 @@ class ServerProvider extends BusyProvider {
|
|||||||
final si = _servers[idx];
|
final si = _servers[idx];
|
||||||
try {
|
try {
|
||||||
if (si.client == null) return;
|
if (si.client == null) return;
|
||||||
final raw = utf8.decode(await si.client!.run(
|
final raw = await si.client!
|
||||||
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"));
|
.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();
|
final lines = raw.split('A====A').map((e) => e.trim()).toList();
|
||||||
_getCPU(spi, lines[2], lines[3]);
|
_getCPU(spi, lines[2], lines[3]);
|
||||||
_getMem(spi, lines[7]);
|
_getMem(spi, lines[7]);
|
||||||
@@ -321,10 +323,10 @@ class ServerProvider extends BusyProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async {
|
Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async {
|
||||||
final result = await _servers
|
return await _servers
|
||||||
.firstWhere((element) => element.info == spi)
|
.firstWhere((element) => element.info == spi)
|
||||||
.client!
|
.client!
|
||||||
.run(snippet.script);
|
.run(snippet.script)
|
||||||
return utf8.decode(result);
|
.string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
class BuildData {
|
class BuildData {
|
||||||
static const String name = "ServerBox";
|
static const String name = "ServerBox";
|
||||||
static const int build = 106;
|
static const int build = 107;
|
||||||
static const String engine =
|
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";
|
"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-08 18:06:40.014600";
|
static const String buildAt = "2022-03-10 13:25:24.362670";
|
||||||
static const int modifications = 8;
|
static const int modifications = 11;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/apt.dart';
|
||||||
import 'package:toolbox/data/provider/server.dart';
|
import 'package:toolbox/data/provider/server.dart';
|
||||||
import 'package:toolbox/locator.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/round_rect_card.dart';
|
||||||
import 'package:toolbox/view/widget/two_line_text.dart';
|
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||||
|
|
||||||
@@ -55,61 +56,75 @@ class _AptManagePageState extends State<AptManagePage>
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: TwoLineText(up: 'Apt', down: widget.spi.ip),
|
title: TwoLineText(up: 'Apt', down: widget.spi.name),
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
onPressed: () {
|
|
||||||
locator<AptProvider>().refreshInstalled();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: Consumer<AptProvider>(builder: (_, apt, __) {
|
body: Consumer<AptProvider>(builder: (_, apt, __) {
|
||||||
if (apt.upgradeable == null) {
|
if (apt.upgradeable == null) {
|
||||||
apt.refreshInstalled();
|
apt.refreshInstalled();
|
||||||
|
return centerLoading;
|
||||||
|
}
|
||||||
|
if (!apt.isSU) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'Only supported as root. Not "${apt.whoami}".',
|
||||||
|
style: greyStyle,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(13),
|
padding: const EdgeInsets.all(13),
|
||||||
children: [_buildUpdatePanel(apt)],
|
children:
|
||||||
|
[_buildUpdatePanel(apt)].map((e) => RoundRectCard(e)).toList(),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUpdatePanel(AptProvider apt) {
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
RoundRectCard(ExpansionTile(
|
ExpansionTile(
|
||||||
title: Text(!apt.isReady
|
title: Text('Found ${apt.upgradeable!.length} update'),
|
||||||
? 'Loading...'
|
subtitle: Text(
|
||||||
: 'Found ${apt.upgradeable!.length} update'),
|
apt.upgradeable!.map((e) => e.package).join(', '),
|
||||||
subtitle: !apt.isReady
|
maxLines: 1,
|
||||||
? null
|
overflow: TextOverflow.ellipsis,
|
||||||
: Text(
|
style: greyStyle,
|
||||||
apt.upgradeable!.map((e) => e.package).join(', '),
|
),
|
||||||
maxLines: 1,
|
children: apt.updateLog == null
|
||||||
overflow: TextOverflow.ellipsis,
|
? [
|
||||||
style: greyStyle,
|
TextButton(
|
||||||
),
|
child: const Text('Update all'),
|
||||||
children: [
|
onPressed: () {
|
||||||
// TextButton(
|
apt.upgrade();
|
||||||
// child: Text('Update all'),
|
}),
|
||||||
// onPressed: () {
|
SizedBox(
|
||||||
// apt.upgrade();
|
height: _media.size.height * 0.73,
|
||||||
// }),
|
|
||||||
!apt.isReady
|
|
||||||
? const SizedBox()
|
|
||||||
: SizedBox(
|
|
||||||
height: _media.size.height * 0.23,
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: apt.upgradeable!
|
children: apt.upgradeable!
|
||||||
.map((e) => _buildUpdateItem(e, apt))
|
.map((e) => _buildUpdateItem(e, apt))
|
||||||
.toList()),
|
.toList()),
|
||||||
)
|
)
|
||||||
],
|
]
|
||||||
))
|
: [
|
||||||
|
SizedBox(
|
||||||
|
height: _media.size.height * 0.73,
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
children: [Text(apt.updateLog!)],
|
||||||
|
))
|
||||||
|
],
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: TwoLineText(up: 'Docker', down: widget.spi.ip),
|
title: TwoLineText(up: 'Docker', down: widget.spi.name),
|
||||||
),
|
),
|
||||||
body: _buildMain(),
|
body: _buildMain(),
|
||||||
);
|
);
|
||||||
@@ -110,7 +110,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
|||||||
|
|
||||||
Widget _buildVersion(String edition, String version) {
|
Widget _buildVersion(String edition, String version) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(13),
|
padding: const EdgeInsets.all(17),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [Text(edition), Text(version)],
|
children: [Text(edition), Text(version)],
|
||||||
@@ -172,7 +172,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
|
|||||||
},
|
},
|
||||||
itemHeight: 37,
|
itemHeight: 37,
|
||||||
itemPadding: const EdgeInsets.only(left: 17, right: 17),
|
itemPadding: const EdgeInsets.only(left: 17, right: 17),
|
||||||
dropdownWidth: 160,
|
dropdownWidth: 133,
|
||||||
dropdownDecoration: BoxDecoration(
|
dropdownDecoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(7),
|
borderRadius: BorderRadius.circular(7),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -245,12 +245,7 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
AppRoute(AptManagePage(spi), 'apt manage page').go(context);
|
AppRoute(AptManagePage(spi), 'apt manage page').go(context);
|
||||||
break;
|
break;
|
||||||
case ServerTabMenuItems.sftp:
|
case ServerTabMenuItems.sftp:
|
||||||
AppRoute(
|
AppRoute(SFTPPage(spi), 'SFTP').go(context);
|
||||||
SFTPPage(
|
|
||||||
spi: spi,
|
|
||||||
),
|
|
||||||
'SFTP')
|
|
||||||
.go(context);
|
|
||||||
break;
|
break;
|
||||||
case ServerTabMenuItems.snippet:
|
case ServerTabMenuItems.snippet:
|
||||||
AppRoute(
|
AppRoute(
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import 'package:toolbox/view/widget/fade_in.dart';
|
|||||||
import 'package:toolbox/view/widget/two_line_text.dart';
|
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||||
|
|
||||||
class SFTPPage extends StatefulWidget {
|
class SFTPPage extends StatefulWidget {
|
||||||
final ServerPrivateInfo? spi;
|
final ServerPrivateInfo spi;
|
||||||
const SFTPPage({this.spi, Key? key}) : super(key: key);
|
const SFTPPage(this.spi, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_SFTPPageState createState() => _SFTPPageState();
|
_SFTPPageState createState() => _SFTPPageState();
|
||||||
@@ -34,10 +34,8 @@ class _SFTPPageState extends State<SFTPPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.spi != null) {
|
_status.spi = widget.spi;
|
||||||
_status.spi = widget.spi!;
|
_status.selected = true;
|
||||||
_status.selected = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -45,7 +43,7 @@ class _SFTPPageState extends State<SFTPPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: TwoLineText(up: 'SFTP', down: _status.spi?.ip ?? '...'),
|
title: TwoLineText(up: 'SFTP', down: widget.spi.name),
|
||||||
),
|
),
|
||||||
body: _buildFileView(),
|
body: _buildFileView(),
|
||||||
);
|
);
|
||||||
@@ -98,6 +96,13 @@ class _SFTPPageState extends State<SFTPPage> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
|
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
|
||||||
title: Text(file.filename),
|
title: Text(file.filename),
|
||||||
|
trailing: Text(
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
(file.attr.modifyTime ?? 0) * 1000)
|
||||||
|
.toString()
|
||||||
|
.replaceFirst('.000', ''),
|
||||||
|
style: const TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
subtitle:
|
subtitle:
|
||||||
isDir ? null : Text(convertBytes(file.attr.size ?? 0)),
|
isDir ? null : Text(convertBytes(file.attr.size ?? 0)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -335,13 +335,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.11"
|
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:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -528,7 +521,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.8"
|
version: "0.4.3"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user