new: podman
This commit is contained in:
@@ -20,7 +20,7 @@ class Backup {
|
||||
final List<ServerPrivateInfo> spis;
|
||||
final List<Snippet> snippets;
|
||||
final List<PrivateKeyInfo> keys;
|
||||
final Map<String, dynamic> dockerHosts;
|
||||
final Map<String, dynamic> container;
|
||||
final Map<String, dynamic> settings;
|
||||
final Map<String, dynamic> history;
|
||||
final int? lastModTime;
|
||||
@@ -31,7 +31,7 @@ class Backup {
|
||||
required this.spis,
|
||||
required this.snippets,
|
||||
required this.keys,
|
||||
required this.dockerHosts,
|
||||
required this.container,
|
||||
required this.settings,
|
||||
required this.history,
|
||||
this.lastModTime,
|
||||
@@ -48,7 +48,7 @@ class Backup {
|
||||
keys = (json['keys'] as List)
|
||||
.map((e) => PrivateKeyInfo.fromJson(e))
|
||||
.toList(),
|
||||
dockerHosts = json['dockerHosts'] ?? {},
|
||||
container = json['container'] ?? {},
|
||||
settings = json['settings'] ?? {},
|
||||
lastModTime = json['lastModTime'],
|
||||
history = json['history'] ?? {};
|
||||
@@ -59,7 +59,7 @@ class Backup {
|
||||
'spis': spis,
|
||||
'snippets': snippets,
|
||||
'keys': keys,
|
||||
'dockerHosts': dockerHosts,
|
||||
'container': container,
|
||||
'settings': settings,
|
||||
'lastModTime': lastModTime,
|
||||
'history': history,
|
||||
@@ -71,7 +71,7 @@ class Backup {
|
||||
spis = Stores.server.fetch(),
|
||||
snippets = Stores.snippet.fetch(),
|
||||
keys = Stores.key.fetch(),
|
||||
dockerHosts = Stores.docker.box.toJson(),
|
||||
container = Stores.docker.box.toJson(),
|
||||
settings = Stores.setting.box.toJson(),
|
||||
lastModTime = Stores.lastModTime,
|
||||
history = Stores.history.box.toJson();
|
||||
@@ -110,8 +110,8 @@ class Backup {
|
||||
for (final s in history.keys) {
|
||||
Stores.history.box.put(s, history[s]);
|
||||
}
|
||||
for (final k in dockerHosts.keys) {
|
||||
final val = dockerHosts[k];
|
||||
for (final k in container.keys) {
|
||||
final val = container[k];
|
||||
if (val != null && val is String && val.isNotEmpty) {
|
||||
Stores.docker.put(k, val);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class SSHErr extends Err<SSHErrType> {
|
||||
}
|
||||
}
|
||||
|
||||
enum DockerErrType {
|
||||
enum ContainerErrType {
|
||||
unknown,
|
||||
noClient,
|
||||
notInstalled,
|
||||
@@ -45,12 +45,12 @@ enum DockerErrType {
|
||||
parseStats,
|
||||
}
|
||||
|
||||
class DockerErr extends Err<DockerErrType> {
|
||||
DockerErr({required super.type, super.message}) : super(from: ErrFrom.docker);
|
||||
class ContainerErr extends Err<ContainerErrType> {
|
||||
ContainerErr({required super.type, super.message}) : super(from: ErrFrom.docker);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DockerErr<$type>: $message';
|
||||
return 'ContainerErr<$type>: $message';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
|
||||
enum ServerTabMenuType {
|
||||
enum ServerTabMenu {
|
||||
terminal,
|
||||
sftp,
|
||||
docker,
|
||||
container,
|
||||
process,
|
||||
pkg,
|
||||
//snippet,
|
||||
@@ -12,40 +12,40 @@ enum ServerTabMenuType {
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case ServerTabMenuType.sftp:
|
||||
case ServerTabMenu.sftp:
|
||||
return Icons.insert_drive_file;
|
||||
//case ServerTabMenuType.snippet:
|
||||
//return Icons.code;
|
||||
case ServerTabMenuType.pkg:
|
||||
case ServerTabMenu.pkg:
|
||||
return Icons.system_security_update;
|
||||
case ServerTabMenuType.docker:
|
||||
case ServerTabMenu.container:
|
||||
return Icons.view_agenda;
|
||||
case ServerTabMenuType.process:
|
||||
case ServerTabMenu.process:
|
||||
return Icons.list_alt_outlined;
|
||||
case ServerTabMenuType.terminal:
|
||||
case ServerTabMenu.terminal:
|
||||
return Icons.terminal;
|
||||
}
|
||||
}
|
||||
|
||||
String get toStr {
|
||||
switch (this) {
|
||||
case ServerTabMenuType.sftp:
|
||||
case ServerTabMenu.sftp:
|
||||
return 'SFTP';
|
||||
//case ServerTabMenuType.snippet:
|
||||
//return l10n.snippet;
|
||||
case ServerTabMenuType.pkg:
|
||||
case ServerTabMenu.pkg:
|
||||
return l10n.pkg;
|
||||
case ServerTabMenuType.docker:
|
||||
return 'Docker';
|
||||
case ServerTabMenuType.process:
|
||||
case ServerTabMenu.container:
|
||||
return l10n.container;
|
||||
case ServerTabMenu.process:
|
||||
return l10n.process;
|
||||
case ServerTabMenuType.terminal:
|
||||
case ServerTabMenu.terminal:
|
||||
return l10n.terminal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DockerMenuType {
|
||||
enum ContainerMenu {
|
||||
start,
|
||||
stop,
|
||||
restart,
|
||||
@@ -55,7 +55,7 @@ enum DockerMenuType {
|
||||
//stats,
|
||||
;
|
||||
|
||||
static List<DockerMenuType> items(bool running) {
|
||||
static List<ContainerMenu> items(bool running) {
|
||||
if (running) {
|
||||
return [
|
||||
stop,
|
||||
@@ -72,17 +72,17 @@ enum DockerMenuType {
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case DockerMenuType.start:
|
||||
case ContainerMenu.start:
|
||||
return Icons.play_arrow;
|
||||
case DockerMenuType.stop:
|
||||
case ContainerMenu.stop:
|
||||
return Icons.stop;
|
||||
case DockerMenuType.restart:
|
||||
case ContainerMenu.restart:
|
||||
return Icons.restart_alt;
|
||||
case DockerMenuType.rm:
|
||||
case ContainerMenu.rm:
|
||||
return Icons.delete;
|
||||
case DockerMenuType.logs:
|
||||
case ContainerMenu.logs:
|
||||
return Icons.logo_dev;
|
||||
case DockerMenuType.terminal:
|
||||
case ContainerMenu.terminal:
|
||||
return Icons.terminal;
|
||||
// case DockerMenuType.stats:
|
||||
// return Icons.bar_chart;
|
||||
@@ -91,24 +91,24 @@ enum DockerMenuType {
|
||||
|
||||
String get toStr {
|
||||
switch (this) {
|
||||
case DockerMenuType.start:
|
||||
case ContainerMenu.start:
|
||||
return l10n.start;
|
||||
case DockerMenuType.stop:
|
||||
case ContainerMenu.stop:
|
||||
return l10n.stop;
|
||||
case DockerMenuType.restart:
|
||||
case ContainerMenu.restart:
|
||||
return l10n.restart;
|
||||
case DockerMenuType.rm:
|
||||
case ContainerMenu.rm:
|
||||
return l10n.delete;
|
||||
case DockerMenuType.logs:
|
||||
case ContainerMenu.logs:
|
||||
return l10n.log;
|
||||
case DockerMenuType.terminal:
|
||||
case ContainerMenu.terminal:
|
||||
return l10n.terminal;
|
||||
// case DockerMenuType.stats:
|
||||
// return s.stats;
|
||||
}
|
||||
}
|
||||
|
||||
PopupMenuItem<DockerMenuType> get widget => _build(this, icon, toStr);
|
||||
PopupMenuItem<ContainerMenu> get widget => _build(this, icon, toStr);
|
||||
}
|
||||
|
||||
PopupMenuItem<T> _build<T>(T t, IconData icon, String text) {
|
||||
|
||||
@@ -224,30 +224,6 @@ const _statusCmds = [
|
||||
'nvidia-smi -q -x',
|
||||
];
|
||||
|
||||
enum DockerCmdType {
|
||||
version,
|
||||
ps,
|
||||
//stats,
|
||||
images,
|
||||
;
|
||||
|
||||
String get exec {
|
||||
switch (this) {
|
||||
case DockerCmdType.version:
|
||||
return 'docker version';
|
||||
case DockerCmdType.ps:
|
||||
return 'docker ps -a';
|
||||
// case DockerCmdType.stats:
|
||||
// return 'docker stats --no-stream';
|
||||
case DockerCmdType.images:
|
||||
return 'docker image ls';
|
||||
}
|
||||
}
|
||||
|
||||
static final execAll =
|
||||
values.map((e) => e.exec).join(' && echo $seperator && ');
|
||||
}
|
||||
|
||||
enum BSDStatusCmdType {
|
||||
echo,
|
||||
time,
|
||||
|
||||
115
lib/data/model/container/image.dart
Normal file
115
lib/data/model/container/image.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'package:toolbox/data/model/container/type.dart';
|
||||
|
||||
abstract final class ContainerImg {
|
||||
final String? repository = null;
|
||||
final String? tag = null;
|
||||
final String? id = null;
|
||||
String? get sizeMB;
|
||||
int? get containersCount;
|
||||
|
||||
factory ContainerImg.fromRawJson(String s, ContainerType typ) => typ.img(s);
|
||||
}
|
||||
|
||||
final class PodmanImg implements ContainerImg {
|
||||
@override
|
||||
final String? repository;
|
||||
@override
|
||||
final String? tag;
|
||||
@override
|
||||
final String? id;
|
||||
final int? created;
|
||||
final int? size;
|
||||
final int? containers;
|
||||
|
||||
PodmanImg({
|
||||
this.repository,
|
||||
this.tag,
|
||||
this.id,
|
||||
this.created,
|
||||
this.size,
|
||||
this.containers,
|
||||
});
|
||||
|
||||
@override
|
||||
String? get sizeMB => size?.convertBytes;
|
||||
|
||||
@override
|
||||
int? get containersCount => containers;
|
||||
|
||||
factory PodmanImg.fromRawJson(String str) =>
|
||||
PodmanImg.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory PodmanImg.fromJson(Map<String, dynamic> json) => PodmanImg(
|
||||
repository: json["repository"],
|
||||
tag: json["tag"],
|
||||
id: json["Id"],
|
||||
created: json["Created"],
|
||||
size: json["Size"],
|
||||
containers: json["Containers"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"repository": repository,
|
||||
"tag": tag,
|
||||
"Id": id,
|
||||
"Created": created,
|
||||
"Size": size,
|
||||
"Containers": containers,
|
||||
};
|
||||
}
|
||||
|
||||
final class DockerImg implements ContainerImg {
|
||||
final String containers;
|
||||
final String createdAt;
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final String repository;
|
||||
final String size;
|
||||
@override
|
||||
final String tag;
|
||||
|
||||
DockerImg({
|
||||
required this.containers,
|
||||
required this.createdAt,
|
||||
required this.id,
|
||||
required this.repository,
|
||||
required this.size,
|
||||
required this.tag,
|
||||
});
|
||||
|
||||
@override
|
||||
String? get sizeMB => size;
|
||||
|
||||
@override
|
||||
int? get containersCount =>
|
||||
containers == 'N/A' ? 0 : int.tryParse(containers);
|
||||
|
||||
factory DockerImg.fromRawJson(String str) =>
|
||||
DockerImg.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory DockerImg.fromJson(Map<String, dynamic> json) => DockerImg(
|
||||
containers: json["Containers"],
|
||||
createdAt: json["CreatedAt"],
|
||||
id: json["ID"],
|
||||
repository: json["Repository"],
|
||||
size: json["Size"],
|
||||
tag: json["Tag"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"Containers": containers,
|
||||
"CreatedAt": createdAt,
|
||||
"ID": id,
|
||||
"Repository": repository,
|
||||
"Size": size,
|
||||
"Tag": tag,
|
||||
};
|
||||
}
|
||||
127
lib/data/model/container/ps.dart
Normal file
127
lib/data/model/container/ps.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:toolbox/data/model/container/type.dart';
|
||||
|
||||
abstract final class ContainerPs {
|
||||
final String? id = null;
|
||||
final String? image = null;
|
||||
String? get name;
|
||||
String? get cmd;
|
||||
bool get running;
|
||||
|
||||
factory ContainerPs.fromRawJson(String s, ContainerType typ) => typ.ps(s);
|
||||
}
|
||||
|
||||
final class PodmanPs implements ContainerPs {
|
||||
final List<String>? command;
|
||||
final DateTime? created;
|
||||
final bool? exited;
|
||||
@override
|
||||
final String? id;
|
||||
@override
|
||||
final String? image;
|
||||
final List<String>? names;
|
||||
final int? startedAt;
|
||||
|
||||
PodmanPs({
|
||||
this.command,
|
||||
this.created,
|
||||
this.exited,
|
||||
this.id,
|
||||
this.image,
|
||||
this.names,
|
||||
this.startedAt,
|
||||
});
|
||||
|
||||
@override
|
||||
String? get name => names?.firstOrNull;
|
||||
|
||||
@override
|
||||
String? get cmd => command?.firstOrNull;
|
||||
|
||||
@override
|
||||
bool get running => exited != true;
|
||||
|
||||
factory PodmanPs.fromRawJson(String str) =>
|
||||
PodmanPs.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
|
||||
command: json["Command"] == null
|
||||
? []
|
||||
: List<String>.from(json["Command"]!.map((x) => x)),
|
||||
created:
|
||||
json["Created"] == null ? null : DateTime.parse(json["Created"]),
|
||||
exited: json["Exited"],
|
||||
id: json["Id"],
|
||||
image: json["Image"],
|
||||
names: json["Names"] == null
|
||||
? []
|
||||
: List<String>.from(json["Names"]!.map((x) => x)),
|
||||
startedAt: json["StartedAt"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"Command":
|
||||
command == null ? [] : List<dynamic>.from(command!.map((x) => x)),
|
||||
"Created": created?.toIso8601String(),
|
||||
"Exited": exited,
|
||||
"Id": id,
|
||||
"Image": image,
|
||||
"Names": names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
|
||||
"StartedAt": startedAt,
|
||||
};
|
||||
}
|
||||
|
||||
final class DockerPs implements ContainerPs {
|
||||
final String? command;
|
||||
final String? createdAt;
|
||||
@override
|
||||
final String? id;
|
||||
@override
|
||||
final String? image;
|
||||
final String? names;
|
||||
final String? state;
|
||||
|
||||
DockerPs({
|
||||
this.command,
|
||||
this.createdAt,
|
||||
this.id,
|
||||
this.image,
|
||||
this.names,
|
||||
this.state,
|
||||
});
|
||||
|
||||
@override
|
||||
String? get name => names;
|
||||
|
||||
@override
|
||||
String? get cmd => command;
|
||||
|
||||
@override
|
||||
bool get running => state == 'running';
|
||||
|
||||
factory DockerPs.fromRawJson(String str) =>
|
||||
DockerPs.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory DockerPs.fromJson(Map<String, dynamic> json) => DockerPs(
|
||||
command: json["Command"],
|
||||
createdAt: json["CreatedAt"],
|
||||
id: json["ID"],
|
||||
image: json["Image"],
|
||||
names: json["Names"],
|
||||
state: json["State"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"Command": command,
|
||||
"CreatedAt": createdAt,
|
||||
"ID": id,
|
||||
"Image": image,
|
||||
"Names": names,
|
||||
"State": state,
|
||||
};
|
||||
}
|
||||
18
lib/data/model/container/type.dart
Normal file
18
lib/data/model/container/type.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:toolbox/data/model/container/image.dart';
|
||||
import 'package:toolbox/data/model/container/ps.dart';
|
||||
|
||||
enum ContainerType {
|
||||
docker,
|
||||
podman,
|
||||
;
|
||||
|
||||
ContainerPs Function(String str) get ps => switch (this) {
|
||||
ContainerType.docker => DockerPs.fromRawJson,
|
||||
ContainerType.podman => PodmanPs.fromRawJson,
|
||||
};
|
||||
|
||||
ContainerImg Function(String str) get img => switch (this) {
|
||||
ContainerType.docker => DockerImg.fromRawJson,
|
||||
ContainerType.podman => PodmanImg.fromRawJson,
|
||||
};
|
||||
}
|
||||
69
lib/data/model/container/version.dart
Normal file
69
lib/data/model/container/version.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class Containerd {
|
||||
final ContainerdClient client;
|
||||
|
||||
Containerd({
|
||||
required this.client,
|
||||
});
|
||||
|
||||
factory Containerd.fromRawJson(String str) => Containerd.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory Containerd.fromJson(Map<String, dynamic> json) => Containerd(
|
||||
client: ContainerdClient.fromJson(json["Client"]),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"Client": client.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
class ContainerdClient {
|
||||
final String apiVersion;
|
||||
final String version;
|
||||
final String goVersion;
|
||||
final String gitCommit;
|
||||
final String builtTime;
|
||||
final int built;
|
||||
final String osArch;
|
||||
final String os;
|
||||
|
||||
ContainerdClient({
|
||||
required this.apiVersion,
|
||||
required this.version,
|
||||
required this.goVersion,
|
||||
required this.gitCommit,
|
||||
required this.builtTime,
|
||||
required this.built,
|
||||
required this.osArch,
|
||||
required this.os,
|
||||
});
|
||||
|
||||
factory ContainerdClient.fromRawJson(String str) => ContainerdClient.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory ContainerdClient.fromJson(Map<String, dynamic> json) => ContainerdClient(
|
||||
apiVersion: json["APIVersion"],
|
||||
version: json["Version"],
|
||||
goVersion: json["GoVersion"],
|
||||
gitCommit: json["GitCommit"],
|
||||
builtTime: json["BuiltTime"],
|
||||
built: json["Built"],
|
||||
osArch: json["OsArch"],
|
||||
os: json["Os"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"APIVersion": apiVersion,
|
||||
"Version": version,
|
||||
"GoVersion": goVersion,
|
||||
"GitCommit": gitCommit,
|
||||
"BuiltTime": builtTime,
|
||||
"Built": built,
|
||||
"OsArch": osArch,
|
||||
"Os": os,
|
||||
};
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
final _dockerImageReg = RegExp(r'(\S+) +(\S+) +(\S+) +(.+) +(\S+)');
|
||||
|
||||
class DockerImage {
|
||||
final String repo;
|
||||
final String tag;
|
||||
final String id;
|
||||
final String created;
|
||||
final String size;
|
||||
|
||||
static final Map<String, DockerImage> _cache = <String, DockerImage>{};
|
||||
|
||||
DockerImage({
|
||||
required this.repo,
|
||||
required this.tag,
|
||||
required this.id,
|
||||
required this.created,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'repo': repo,
|
||||
'tag': tag,
|
||||
'id': id,
|
||||
'created': created,
|
||||
'size': size,
|
||||
};
|
||||
}
|
||||
|
||||
factory DockerImage.fromRawStr(String raw) {
|
||||
return _cache.putIfAbsent(raw, () => _parse(raw));
|
||||
}
|
||||
|
||||
static DockerImage _parse(String raw) {
|
||||
final match = _dockerImageReg.firstMatch(raw);
|
||||
if (match == null) {
|
||||
throw Exception('Invalid docker image: $raw');
|
||||
}
|
||||
return DockerImage(
|
||||
repo: match.group(1)!,
|
||||
tag: match.group(2)!,
|
||||
id: match.group(3)!,
|
||||
created: match.group(4)!,
|
||||
size: match.group(5)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
final _seperator = RegExp(' +');
|
||||
|
||||
class DockerPsItem {
|
||||
late String containerId;
|
||||
late String image;
|
||||
late String command;
|
||||
late String created;
|
||||
late String status;
|
||||
late String ports;
|
||||
late String name;
|
||||
// String? cpu;
|
||||
// String? mem;
|
||||
// String? net;
|
||||
// String? disk;
|
||||
|
||||
DockerPsItem(
|
||||
this.containerId,
|
||||
this.image,
|
||||
this.command,
|
||||
this.created,
|
||||
this.status,
|
||||
this.ports,
|
||||
this.name,
|
||||
);
|
||||
|
||||
DockerPsItem.fromRawString(String rawString) {
|
||||
List<String> parts = rawString.split(_seperator);
|
||||
parts = parts.map((e) => e.trim()).toList();
|
||||
|
||||
containerId = parts[0];
|
||||
image = parts[1];
|
||||
command = parts[2].trim();
|
||||
created = parts[3];
|
||||
status = parts[4];
|
||||
if (running && parts.length > 6) {
|
||||
ports = parts[5];
|
||||
name = parts[6];
|
||||
} else {
|
||||
ports = '';
|
||||
name = parts[5];
|
||||
}
|
||||
}
|
||||
|
||||
// void parseStats(String rawString) {
|
||||
// if (rawString.isEmpty) {
|
||||
// return;
|
||||
// }
|
||||
// final parts = rawString.split(_seperator);
|
||||
// if (parts.length != 8) {
|
||||
// return;
|
||||
// }
|
||||
// cpu = parts[2];
|
||||
// mem = parts[3];
|
||||
// net = parts[5];
|
||||
// disk = parts[6];
|
||||
// }
|
||||
|
||||
bool get running => status.contains('Up ');
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DockerPsItem<$containerId@$name>';
|
||||
}
|
||||
}
|
||||
213
lib/data/provider/container.dart
Normal file
213
lib/data/provider/container.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/ssh_client.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/container/image.dart';
|
||||
import 'package:toolbox/data/model/container/ps.dart';
|
||||
import 'package:toolbox/data/model/app/error.dart';
|
||||
import 'package:toolbox/data/model/container/type.dart';
|
||||
import 'package:toolbox/data/model/container/version.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
|
||||
final _dockerNotFound = RegExp(r'command not found|Unknown command');
|
||||
|
||||
class ContainerProvider extends ChangeNotifier {
|
||||
SSHClient? client;
|
||||
String? userName;
|
||||
List<ContainerPs>? items;
|
||||
List<ContainerImg>? images;
|
||||
String? version;
|
||||
ContainerErr? error;
|
||||
String? hostId;
|
||||
String? runLog;
|
||||
BuildContext? context;
|
||||
ContainerType type;
|
||||
|
||||
ContainerProvider({
|
||||
this.client,
|
||||
this.userName,
|
||||
this.hostId,
|
||||
this.context,
|
||||
}) : type = Stores.docker.getType(hostId) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
Future<void> setType(ContainerType type) async {
|
||||
this.type = type;
|
||||
Stores.docker.setType(hostId, type);
|
||||
error = runLog = items = images = version = null;
|
||||
notifyListeners();
|
||||
await refresh();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
var raw = '';
|
||||
await client?.execWithPwd(
|
||||
_wrap(ContainerCmdType.execAll(type)),
|
||||
context: context,
|
||||
onStdout: (data, _) => raw = '$raw$data',
|
||||
);
|
||||
|
||||
if (raw.contains(_dockerNotFound)) {
|
||||
error = ContainerErr(type: ContainerErrType.notInstalled);
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check result segments count
|
||||
final segments = raw.split(seperator);
|
||||
if (segments.length != ContainerCmdType.values.length) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.segmentsNotMatch,
|
||||
message: 'Container segments: ${segments.length}',
|
||||
);
|
||||
Loggers.parse.warning('Container segments: ${segments.length}\n$raw');
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse docker version
|
||||
final verRaw = ContainerCmdType.version.find(segments);
|
||||
try {
|
||||
final containerVersion = Containerd.fromRawJson(verRaw);
|
||||
version = containerVersion.client.version;
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.invalidVersion,
|
||||
message: '$e',
|
||||
);
|
||||
Loggers.parse.warning('Container version failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse docker ps
|
||||
final psRaw = ContainerCmdType.ps.find(segments);
|
||||
|
||||
final lines = psRaw.split('\n');
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
if (lines.isNotEmpty) lines.removeAt(0);
|
||||
items = lines.map((e) => ContainerPs.fromRawJson(e, type)).toList();
|
||||
|
||||
// Parse docker images
|
||||
final imageRaw = ContainerCmdType.images.find(segments);
|
||||
try {
|
||||
final imgLines = imageRaw.split('\n');
|
||||
imgLines.removeWhere((element) => element.isEmpty);
|
||||
if (imgLines.isNotEmpty) imgLines.removeAt(0);
|
||||
images = imgLines.map((e) => ContainerImg.fromRawJson(e, type)).toList();
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.parseImages,
|
||||
message: '$e',
|
||||
);
|
||||
Loggers.parse.warning('Container images failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse docker stats
|
||||
// final statsRaw = DockerCmdType.stats.find(segments);
|
||||
// try {
|
||||
// final statsLines = statsRaw.split('\n');
|
||||
// statsLines.removeWhere((element) => element.isEmpty);
|
||||
// if (statsLines.isNotEmpty) statsLines.removeAt(0);
|
||||
// for (var item in items!) {
|
||||
// final statsLine = statsLines.firstWhere(
|
||||
// (element) => element.contains(item.containerId),
|
||||
// orElse: () => '',
|
||||
// );
|
||||
// if (statsLine.isEmpty) continue;
|
||||
// item.parseStats(statsLine);
|
||||
// }
|
||||
// } catch (e, trace) {
|
||||
// error = DockerErr(
|
||||
// type: DockerErrType.parseStats,
|
||||
// message: '$e',
|
||||
// );
|
||||
// _logger.warning('Parse docker stats: $statsRaw', e, trace);
|
||||
// } finally {
|
||||
// notifyListeners();
|
||||
// }
|
||||
}
|
||||
|
||||
Future<ContainerErr?> stop(String id) async => await run('stop $id');
|
||||
|
||||
Future<ContainerErr?> start(String id) async => await run('start $id');
|
||||
|
||||
Future<ContainerErr?> delete(String id, bool force) async {
|
||||
if (force) {
|
||||
return await run('rm -f $id');
|
||||
}
|
||||
return await run('rm $id');
|
||||
}
|
||||
|
||||
Future<ContainerErr?> restart(String id) async => await run('restart $id');
|
||||
|
||||
Future<ContainerErr?> run(String cmd) async {
|
||||
cmd = switch (type) {
|
||||
ContainerType.docker => 'docker $cmd',
|
||||
ContainerType.podman => 'podman $cmd',
|
||||
};
|
||||
|
||||
runLog = '';
|
||||
final errs = <String>[];
|
||||
final code = await client?.execWithPwd(
|
||||
_wrap(cmd),
|
||||
context: context,
|
||||
onStdout: (data, _) {
|
||||
runLog = '$runLog$data';
|
||||
notifyListeners();
|
||||
},
|
||||
onStderr: (data, _) => errs.add(data),
|
||||
);
|
||||
runLog = null;
|
||||
notifyListeners();
|
||||
|
||||
if (code != 0) {
|
||||
return ContainerErr(
|
||||
type: ContainerErrType.unknown,
|
||||
message: errs.join('\n').trim(),
|
||||
);
|
||||
}
|
||||
await refresh();
|
||||
return null;
|
||||
}
|
||||
|
||||
/// wrap cmd with `docker host`
|
||||
String _wrap(String cmd) {
|
||||
final dockerHost = Stores.docker.fetch(hostId);
|
||||
cmd = 'export LANG=en_US.UTF-8 && $cmd';
|
||||
final noDockerHost = dockerHost?.isEmpty ?? true;
|
||||
if (!noDockerHost) {
|
||||
cmd = 'export DOCKER_HOST=$dockerHost && $cmd';
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
|
||||
const _jsonFmt = '--format "{{json .}}"';
|
||||
|
||||
enum ContainerCmdType {
|
||||
version,
|
||||
ps,
|
||||
//stats,
|
||||
images,
|
||||
;
|
||||
|
||||
String exec(ContainerType type) {
|
||||
final prefix = type.name;
|
||||
return switch (this) {
|
||||
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
||||
ContainerCmdType.ps => '$prefix ps -a $_jsonFmt',
|
||||
// DockerCmdType.stats => '$prefix stats --no-stream';
|
||||
ContainerCmdType.images => '$prefix image ls $_jsonFmt',
|
||||
};
|
||||
}
|
||||
|
||||
static String execAll(ContainerType type) =>
|
||||
values.map((e) => e.exec(type)).join(' && echo $seperator && ');
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/ssh_client.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/docker/image.dart';
|
||||
import 'package:toolbox/data/model/docker/ps.dart';
|
||||
import 'package:toolbox/data/model/app/error.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
|
||||
final _dockerNotFound = RegExp(r'command not found|Unknown command');
|
||||
final _versionReg = RegExp(r'(Version:)\s+([0-9]+\.[0-9]+\.[0-9]+)');
|
||||
// eg: `Docker Engine - Community`
|
||||
final _editionReg = RegExp(r'Docker Engine - [a-zA-Z]+');
|
||||
final _dockerPrefixReg = RegExp(r'(sudo )?docker ');
|
||||
|
||||
class DockerProvider extends ChangeNotifier {
|
||||
SSHClient? client;
|
||||
String? userName;
|
||||
List<DockerPsItem>? items;
|
||||
List<DockerImage>? images;
|
||||
String? version;
|
||||
String? edition;
|
||||
DockerErr? error;
|
||||
String? hostId;
|
||||
String? runLog;
|
||||
BuildContext? context;
|
||||
|
||||
void init(
|
||||
SSHClient client,
|
||||
String userName,
|
||||
PwdRequestFunc onPwdReq,
|
||||
String hostId,
|
||||
BuildContext context,
|
||||
) {
|
||||
this.client = client;
|
||||
this.userName = userName;
|
||||
this.context = context;
|
||||
this.hostId = hostId;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
client = userName = error = items = version = edition = context = null;
|
||||
hostId = runLog = images = null;
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
var raw = '';
|
||||
await client?.execWithPwd(
|
||||
_wrap(DockerCmdType.execAll),
|
||||
context: context,
|
||||
onStdout: (data, _) => raw = '$raw$data',
|
||||
);
|
||||
|
||||
if (raw.contains(_dockerNotFound)) {
|
||||
error = DockerErr(type: DockerErrType.notInstalled);
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check result segments count
|
||||
final segments = raw.split(seperator);
|
||||
if (segments.length != DockerCmdType.values.length) {
|
||||
error = DockerErr(type: DockerErrType.segmentsNotMatch);
|
||||
Loggers.parse.warning('Docker segments: ${segments.length}\n$raw');
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse docker version
|
||||
final verRaw = DockerCmdType.version.find(segments);
|
||||
version = _versionReg.firstMatch(verRaw)?.group(2);
|
||||
edition = _editionReg.firstMatch(verRaw)?.group(0);
|
||||
|
||||
// Parse docker ps
|
||||
final psRaw = DockerCmdType.ps.find(segments);
|
||||
try {
|
||||
final lines = psRaw.split('\n');
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
if (lines.isNotEmpty) lines.removeAt(0);
|
||||
items = lines.map((e) => DockerPsItem.fromRawString(e)).toList();
|
||||
} catch (e, trace) {
|
||||
error = DockerErr(
|
||||
type: DockerErrType.parsePsItem,
|
||||
message: '$psRaw\n-\n$e',
|
||||
);
|
||||
Loggers.parse.warning('Docker ps failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse docker images
|
||||
final imageRaw = DockerCmdType.images.find(segments);
|
||||
try {
|
||||
final imageLines = imageRaw.split('\n');
|
||||
imageLines.removeWhere((element) => element.isEmpty);
|
||||
if (imageLines.isNotEmpty) imageLines.removeAt(0);
|
||||
images = imageLines.map((e) => DockerImage.fromRawStr(e)).toList();
|
||||
} catch (e, trace) {
|
||||
error = DockerErr(
|
||||
type: DockerErrType.parseImages,
|
||||
message: '$imageRaw\n-\n$e',
|
||||
);
|
||||
Loggers.parse.warning('Docker images failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse docker stats
|
||||
// final statsRaw = DockerCmdType.stats.find(segments);
|
||||
// try {
|
||||
// final statsLines = statsRaw.split('\n');
|
||||
// statsLines.removeWhere((element) => element.isEmpty);
|
||||
// if (statsLines.isNotEmpty) statsLines.removeAt(0);
|
||||
// for (var item in items!) {
|
||||
// final statsLine = statsLines.firstWhere(
|
||||
// (element) => element.contains(item.containerId),
|
||||
// orElse: () => '',
|
||||
// );
|
||||
// if (statsLine.isEmpty) continue;
|
||||
// item.parseStats(statsLine);
|
||||
// }
|
||||
// } catch (e, trace) {
|
||||
// error = DockerErr(
|
||||
// type: DockerErrType.parseStats,
|
||||
// message: '$statsRaw\n-\n$e',
|
||||
// );
|
||||
// _logger.warning('Parse docker stats: $statsRaw', e, trace);
|
||||
// } finally {
|
||||
// notifyListeners();
|
||||
// }
|
||||
}
|
||||
|
||||
Future<DockerErr?> stop(String id) async => await run('docker stop $id');
|
||||
|
||||
Future<DockerErr?> start(String id) async => await run('docker start $id');
|
||||
|
||||
Future<DockerErr?> delete(String id, bool force) async {
|
||||
if (force) {
|
||||
return await run('docker rm -f $id');
|
||||
}
|
||||
return await run('docker rm $id');
|
||||
}
|
||||
|
||||
Future<DockerErr?> restart(String id) async =>
|
||||
await run('docker restart $id');
|
||||
|
||||
Future<DockerErr?> run(String cmd) async {
|
||||
if (!cmd.startsWith(_dockerPrefixReg)) {
|
||||
return DockerErr(type: DockerErrType.cmdNoPrefix);
|
||||
}
|
||||
|
||||
runLog = '';
|
||||
final errs = <String>[];
|
||||
final code = await client?.execWithPwd(
|
||||
_wrap(cmd),
|
||||
context: context,
|
||||
onStdout: (data, _) {
|
||||
runLog = '$runLog$data';
|
||||
notifyListeners();
|
||||
},
|
||||
onStderr: (data, _) => errs.add(data),
|
||||
);
|
||||
runLog = null;
|
||||
notifyListeners();
|
||||
|
||||
if (code != 0) {
|
||||
return DockerErr(
|
||||
type: DockerErrType.unknown,
|
||||
message: errs.join('\n').trim(),
|
||||
);
|
||||
}
|
||||
await refresh();
|
||||
return null;
|
||||
}
|
||||
|
||||
/// wrap cmd with `docker host`
|
||||
String _wrap(String cmd) {
|
||||
final dockerHost = Stores.docker.fetch(hostId);
|
||||
cmd = 'export LANG=en_US.UTF-8 && $cmd';
|
||||
final noDockerHost = dockerHost?.isEmpty ?? true;
|
||||
if (!noDockerHost) {
|
||||
cmd = 'export DOCKER_HOST=$dockerHost && $cmd';
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 707;
|
||||
static const int build = 709;
|
||||
static const String engine = "3.16.7";
|
||||
static const String buildAt = "2024-01-16 12:17:21";
|
||||
static const int modifications = 1;
|
||||
static const String buildAt = "2024-01-19 17:32:15";
|
||||
static const int modifications = 2;
|
||||
static const int script = 34;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/provider/debug.dart';
|
||||
import 'package:toolbox/data/provider/docker.dart';
|
||||
import 'package:toolbox/data/provider/private_key.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/provider/sftp.dart';
|
||||
@@ -10,7 +9,6 @@ import 'package:toolbox/locator.dart';
|
||||
abstract final class Pros {
|
||||
static final app = locator<AppProvider>();
|
||||
static final debug = locator<DebugProvider>();
|
||||
static final docker = locator<DockerProvider>();
|
||||
static final key = locator<PrivateKeyProvider>();
|
||||
static final server = locator<ServerProvider>();
|
||||
static final sftp = locator<SftpProvider>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:toolbox/core/persistant_store.dart';
|
||||
import 'package:toolbox/data/store/docker.dart';
|
||||
import 'package:toolbox/data/store/container.dart';
|
||||
import 'package:toolbox/data/store/history.dart';
|
||||
import 'package:toolbox/data/store/private_key.dart';
|
||||
import 'package:toolbox/data/store/server.dart';
|
||||
|
||||
30
lib/data/store/container.dart
Normal file
30
lib/data/store/container.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:toolbox/data/model/container/type.dart';
|
||||
|
||||
import '../../core/persistant_store.dart';
|
||||
|
||||
const _keyConfig = 'providerConfig';
|
||||
|
||||
class DockerStore extends PersistentStore {
|
||||
DockerStore() : super('docker');
|
||||
|
||||
String? fetch(String? id) {
|
||||
return box.get(id);
|
||||
}
|
||||
|
||||
void put(String id, String host) {
|
||||
box.put(id, host);
|
||||
}
|
||||
|
||||
ContainerType getType([String? id]) {
|
||||
final cfg = box.get(_keyConfig + (id ?? ''));
|
||||
if (cfg == null) {
|
||||
return ContainerType.docker;
|
||||
} else {
|
||||
return ContainerType.values.firstWhere((e) => e.toString() == cfg);
|
||||
}
|
||||
}
|
||||
|
||||
void setType(String? id, ContainerType type) {
|
||||
box.put(_keyConfig + (id ?? ''), type.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import '../../core/persistant_store.dart';
|
||||
|
||||
class DockerStore extends PersistentStore {
|
||||
DockerStore() : super('docker');
|
||||
|
||||
String? fetch(String? id) {
|
||||
return box.get(id);
|
||||
}
|
||||
|
||||
void put(String id, String host) {
|
||||
box.put(id, host);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user