From 076082c94524f52a369242024aaa9cd3439e68d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:40:40 +0800 Subject: [PATCH] feat: sftp perm setting & path copy (#467) --- lib/core/extension/sftpfile.dart | 21 +++++ lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_id.arb | 1 + lib/l10n/app_ja.arb | 1 + lib/l10n/app_nl.arb | 1 + lib/l10n/app_pt.arb | 1 + lib/l10n/app_ru.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/l10n/app_zh_tw.arb | 1 + lib/view/page/storage/sftp.dart | 136 ++++++++++++++++++++----------- lib/view/widget/unix_perm.dart | 128 +++++++++++++++++++++++++++++ 14 files changed, 248 insertions(+), 48 deletions(-) create mode 100644 lib/view/widget/unix_perm.dart diff --git a/lib/core/extension/sftpfile.dart b/lib/core/extension/sftpfile.dart index 9db3bb56..de92eb82 100644 --- a/lib/core/extension/sftpfile.dart +++ b/lib/core/extension/sftpfile.dart @@ -1,4 +1,5 @@ import 'package:dartssh2/dartssh2.dart'; +import 'package:server_box/view/widget/unix_perm.dart'; extension SftpFileX on SftpFileMode { String get str { @@ -8,6 +9,26 @@ extension SftpFileX on SftpFileMode { return '$user$group$other'; } + + UnixPerm toUnixPerm() { + return UnixPerm( + user: RWX( + r: userRead, + w: userWrite, + x: userExecute, + ), + group: RWX( + r: groupRead, + w: groupWrite, + x: groupExecute, + ), + other: RWX( + r: otherRead, + w: otherWrite, + x: otherExecute, + ), + ); + } } String _getRoleMode(bool r, bool w, bool x) { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8598afc8..670a0430 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -209,6 +209,7 @@ "paste": "Einfügen", "path": "Pfad", "percentOfSize": "{percent}% von {size}", + "permission": "Berechtigungen", "pickFile": "Datei wählen", "pingAvg": "Avg:", "pingInputIP": "Bitte gib eine Ziel-IP/Domain ein.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a6bb2e1a..204f6481 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -209,6 +209,7 @@ "paste": "Paste", "path": "Path", "percentOfSize": "{percent}% of {size}", + "permission": "Permissions", "pickFile": "Pick file", "pingAvg": "Avg:", "pingInputIP": "Please input a target IP / domain.", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 2dc15eae..6b97b0bb 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -209,6 +209,7 @@ "paste": "Pegar", "path": "Ruta", "percentOfSize": "El {percent}% de {size}", + "permission": "Permisos", "pickFile": "Seleccionar archivo", "pingAvg": "Promedio:", "pingInputIP": "Por favor, introduce la IP de destino o el dominio", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c81882ae..ebe5dae6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -209,6 +209,7 @@ "paste": "Coller", "path": "Chemin", "percentOfSize": "{percent}% de {size}", + "permission": "Permissions", "pickFile": "Choisir un fichier", "pingAvg": "Moy.:", "pingInputIP": "Veuillez saisir une adresse IP / un domaine cible.", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 2e2af510..bd6dd99a 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -209,6 +209,7 @@ "paste": "Tempel", "path": "Jalur", "percentOfSize": "{percent}% dari {size}", + "permission": "Izin", "pickFile": "Pilih file", "pingAvg": "Rata -rata:", "pingInputIP": "Harap masukkan IP / domain target.", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 4c27b836..a7db9c99 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -209,6 +209,7 @@ "paste": "貼り付け", "path": "パス", "percentOfSize": "{size} の {percent}%", + "permission": "権限", "pickFile": "ファイルを選択", "pingAvg": "平均:", "pingInputIP": "対象のIPまたはドメインを入力してください", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 8fe3c944..ca23701e 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -209,6 +209,7 @@ "paste": "Plakken", "path": "Pad", "percentOfSize": "{percent}% van {size}", + "permission": "Machtigingen", "pickFile": "Bestand kiezen", "pingAvg": "Gem:", "pingInputIP": "Voer een doel-IP / domein in.", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 9dcc6db5..cf8ba845 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -209,6 +209,7 @@ "paste": "Colar", "path": "Caminho", "percentOfSize": "{percent}% de {size}", + "permission": "Permissões", "pickFile": "Escolher arquivo", "pingAvg": "Média:", "pingInputIP": "Por favor, insira o IP ou domínio alvo", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 048ff62d..c69ddb4a 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -209,6 +209,7 @@ "paste": "вставить", "path": "путь", "percentOfSize": "{percent}% от {size}", + "permission": "Разрешения", "pickFile": "выбрать файл", "pingAvg": "Среднее:", "pingInputIP": "Пожалуйста, введите целевой IP или доменное имя", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 9a19abda..875d1986 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -209,6 +209,7 @@ "paste": "粘贴", "path": "路径", "percentOfSize": "{size} 的 {percent}%", + "permission": "权限", "pickFile": "选择文件", "pingAvg": "平均:", "pingInputIP": "请输入目标IP或域名", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index a61428ef..5b0a05d1 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -209,6 +209,7 @@ "paste": "貼上", "path": "路徑", "percentOfSize": "{size} 的 {percent}%", + "permission": "權限", "pickFile": "選擇檔案", "pingAvg": "平均:", "pingInputIP": "請輸入目標 IP 或域名", diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart index 5bc75936..7dee61c4 100644 --- a/lib/view/page/storage/sftp.dart +++ b/lib/view/page/storage/sftp.dart @@ -5,18 +5,20 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/sftpfile.dart'; +import 'package:server_box/core/route.dart'; import 'package:server_box/core/utils/comparator.dart'; +import 'package:server_box/data/model/server/server_private_info.dart'; +import 'package:server_box/data/model/sftp/absolute_path.dart'; +import 'package:server_box/data/model/sftp/browser_status.dart'; +import 'package:server_box/data/model/sftp/req.dart'; import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/provider.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/view/widget/omit_start_text.dart'; -import '../../../core/route.dart'; -import '../../../data/model/server/server_private_info.dart'; -import '../../../data/model/sftp/absolute_path.dart'; -import '../../../data/model/sftp/browser_status.dart'; -import '../../../data/model/sftp/req.dart'; -import '../../widget/two_line_text.dart'; +import 'package:icons_plus/icons_plus.dart'; +import 'package:server_box/view/widget/two_line_text.dart'; +import 'package:server_box/view/widget/unix_perm.dart'; class SftpPage extends StatefulWidget { final ServerPrivateInfo spi; @@ -174,49 +176,50 @@ class _SftpPageState extends State with AfterLayoutMixin { Widget _buildUploadBtn() { return IconButton( - onPressed: () async { - final idx = await context.showRoundDialog( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.open_in_new), - title: Text(l10n.system), - onTap: () => context.pop(1), - ), - ListTile( - leading: const Icon(Icons.folder), - title: Text(l10n.inner), - onTap: () => context.pop(0), - ), - ], - )); - final path = await () async { - switch (idx) { - case 0: - return await AppRoutes.localStorage(isPickFile: true) - .go(context); - case 1: - return await Pfs.pickFilePath(); - default: - return null; - } - }(); - if (path == null) { - return; + onPressed: () async { + final idx = await context.showRoundDialog( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.open_in_new), + title: Text(l10n.system), + onTap: () => context.pop(1), + ), + ListTile( + leading: const Icon(Icons.folder), + title: Text(l10n.inner), + onTap: () => context.pop(0), + ), + ], + )); + final path = await () async { + switch (idx) { + case 0: + return await AppRoutes.localStorage(isPickFile: true) + .go(context); + case 1: + return await Pfs.pickFilePath(); + default: + return null; } - final remoteDir = _status.path?.path; - if (remoteDir == null) { - context.showSnackBar('remote path is null'); - return; - } - final remotePath = '$remoteDir/${path.split('/').last}'; - Loggers.app.info('SFTP upload local: $path, remote: $remotePath'); - Pros.sftp.add( - SftpReq(widget.spi, remotePath, path, SftpReqType.upload), - ); - }, - icon: const Icon(Icons.upload_file)); + }(); + if (path == null) { + return; + } + final remoteDir = _status.path?.path; + if (remoteDir == null) { + context.showSnackBar('remote path is null'); + return; + } + final remotePath = '$remoteDir/${path.split('/').last}'; + Loggers.app.info('SFTP upload local: $path, remote: $remotePath'); + Pros.sftp.add( + SftpReq(widget.spi, remotePath, path, SftpReqType.upload), + ); + }, + icon: const Icon(Icons.upload_file), + ); } Widget _buildAddBtn() { @@ -371,6 +374,43 @@ class _SftpPageState extends State with AfterLayoutMixin { title: Text(l10n.rename), onTap: () => _rename(file), ), + ListTile( + leading: const Icon(MingCute.copy_line), + title: Text(l10n.copyPath), + onTap: () { + Pfs.copy(_getRemotePath(file)); + context.pop(); + context.showSnackBar(l10n.success); + }, + ), + ListTile( + leading: const Icon(Icons.security), + title: Text(l10n.permission), + onTap: () async { + context.pop(); + + final perm = file.attr.mode?.toUnixPerm() ?? UnixPerm.empty; + var newPerm = perm.copyWith(); + final ok = await context.showRoundDialog( + child: UnixPermEditor(perm: perm, onChanged: (p) => newPerm = p), + actions: Btns.oks(onTap: () => context.pop(true)), + ); + + final permStr = newPerm.perm; + if (ok == true && permStr != perm.perm) { + print('${perm.perm} -> $permStr'); + await context.showLoadingDialog( + fn: () async { + await _client!.run('chmod $permStr "${_getRemotePath(file)}"'); + await _listDir(); + }, + onErr: (e, s) { + context.showErrDialog(e: e, s: s, operation: l10n.permission); + }, + ); + } + }, + ), ]; if (notDir) { children.addAll([ diff --git a/lib/view/widget/unix_perm.dart b/lib/view/widget/unix_perm.dart new file mode 100644 index 00000000..b7c2e2a5 --- /dev/null +++ b/lib/view/widget/unix_perm.dart @@ -0,0 +1,128 @@ +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter/material.dart'; + +final class RWX { + final bool r; + final bool w; + final bool x; + + const RWX({ + required this.r, + required this.w, + required this.x, + }); + + RWX copyWith({bool? r, bool? w, bool? x}) { + return RWX(r: r ?? this.r, w: w ?? this.w, x: x ?? this.x); + } + + int get value { + return (r ? 4 : 0) + (w ? 2 : 0) + (x ? 1 : 0); + } +} + +final class UnixPerm { + final RWX user; + final RWX group; + final RWX other; + + const UnixPerm({ + required this.user, + required this.group, + required this.other, + }); + + UnixPerm copyWith({RWX? user, RWX? group, RWX? other}) { + return UnixPerm( + user: user ?? this.user, + group: group ?? this.group, + other: other ?? this.other, + ); + } + + /// eg.: 744 + String get perm { + return '${user.value}${group.value}${other.value}'; + } + + static UnixPerm get empty => const UnixPerm( + user: RWX(r: false, w: false, x: false), + group: RWX(r: false, w: false, x: false), + other: RWX(r: false, w: false, x: false), + ); +} + +final class UnixPermEditor extends StatefulWidget { + final UnixPerm perm; + final void Function(UnixPerm) onChanged; + const UnixPermEditor( + {super.key, required this.perm, required this.onChanged}); + + @override + _UnixPermEditorState createState() => _UnixPermEditorState(); +} + +final class _UnixPermEditorState extends State { + late UnixPerm perm; + + @override + void initState() { + super.initState(); + perm = widget.perm; + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text('R'), + Text('W'), + Text('X'), + ], + ).paddingOnly(left: 13), + UIs.height7, + _buildRow('U', perm.user), + _buildRow('G', perm.group), + _buildRow('O', perm.other), + ], + ); + } + + Widget _buildRow(String title, RWX rwx) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 7, child: Text(title)), + _buildSwitch(rwx.r, (v) { + setState(() { + perm = perm.copyWith(user: rwx.copyWith(r: v)); + }); + widget.onChanged(perm); + }), + _buildSwitch(rwx.w, (v) { + setState(() { + perm = perm.copyWith(user: rwx.copyWith(w: v)); + }); + widget.onChanged(perm); + }), + _buildSwitch(rwx.x, (v) { + setState(() { + perm = perm.copyWith(user: rwx.copyWith(x: v)); + }); + widget.onChanged(perm); + }), + ], + ); + } + + Widget _buildSwitch(bool value, void Function(bool) onChanged) { + return Switch( + value: value, + onChanged: onChanged, + ); + } +}