mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
feat: sftp perm setting & path copy (#467)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
|
import 'package:server_box/view/widget/unix_perm.dart';
|
||||||
|
|
||||||
extension SftpFileX on SftpFileMode {
|
extension SftpFileX on SftpFileMode {
|
||||||
String get str {
|
String get str {
|
||||||
@@ -8,6 +9,26 @@ extension SftpFileX on SftpFileMode {
|
|||||||
|
|
||||||
return '$user$group$other';
|
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) {
|
String _getRoleMode(bool r, bool w, bool x) {
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "Einfügen",
|
"paste": "Einfügen",
|
||||||
"path": "Pfad",
|
"path": "Pfad",
|
||||||
"percentOfSize": "{percent}% von {size}",
|
"percentOfSize": "{percent}% von {size}",
|
||||||
|
"permission": "Berechtigungen",
|
||||||
"pickFile": "Datei wählen",
|
"pickFile": "Datei wählen",
|
||||||
"pingAvg": "Avg:",
|
"pingAvg": "Avg:",
|
||||||
"pingInputIP": "Bitte gib eine Ziel-IP/Domain ein.",
|
"pingInputIP": "Bitte gib eine Ziel-IP/Domain ein.",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "Paste",
|
"paste": "Paste",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
"percentOfSize": "{percent}% of {size}",
|
"percentOfSize": "{percent}% of {size}",
|
||||||
|
"permission": "Permissions",
|
||||||
"pickFile": "Pick file",
|
"pickFile": "Pick file",
|
||||||
"pingAvg": "Avg:",
|
"pingAvg": "Avg:",
|
||||||
"pingInputIP": "Please input a target IP / domain.",
|
"pingInputIP": "Please input a target IP / domain.",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "Pegar",
|
"paste": "Pegar",
|
||||||
"path": "Ruta",
|
"path": "Ruta",
|
||||||
"percentOfSize": "El {percent}% de {size}",
|
"percentOfSize": "El {percent}% de {size}",
|
||||||
|
"permission": "Permisos",
|
||||||
"pickFile": "Seleccionar archivo",
|
"pickFile": "Seleccionar archivo",
|
||||||
"pingAvg": "Promedio:",
|
"pingAvg": "Promedio:",
|
||||||
"pingInputIP": "Por favor, introduce la IP de destino o el dominio",
|
"pingInputIP": "Por favor, introduce la IP de destino o el dominio",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "Coller",
|
"paste": "Coller",
|
||||||
"path": "Chemin",
|
"path": "Chemin",
|
||||||
"percentOfSize": "{percent}% de {size}",
|
"percentOfSize": "{percent}% de {size}",
|
||||||
|
"permission": "Permissions",
|
||||||
"pickFile": "Choisir un fichier",
|
"pickFile": "Choisir un fichier",
|
||||||
"pingAvg": "Moy.:",
|
"pingAvg": "Moy.:",
|
||||||
"pingInputIP": "Veuillez saisir une adresse IP / un domaine cible.",
|
"pingInputIP": "Veuillez saisir une adresse IP / un domaine cible.",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "Tempel",
|
"paste": "Tempel",
|
||||||
"path": "Jalur",
|
"path": "Jalur",
|
||||||
"percentOfSize": "{percent}% dari {size}",
|
"percentOfSize": "{percent}% dari {size}",
|
||||||
|
"permission": "Izin",
|
||||||
"pickFile": "Pilih file",
|
"pickFile": "Pilih file",
|
||||||
"pingAvg": "Rata -rata:",
|
"pingAvg": "Rata -rata:",
|
||||||
"pingInputIP": "Harap masukkan IP / domain target.",
|
"pingInputIP": "Harap masukkan IP / domain target.",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "貼り付け",
|
"paste": "貼り付け",
|
||||||
"path": "パス",
|
"path": "パス",
|
||||||
"percentOfSize": "{size} の {percent}%",
|
"percentOfSize": "{size} の {percent}%",
|
||||||
|
"permission": "権限",
|
||||||
"pickFile": "ファイルを選択",
|
"pickFile": "ファイルを選択",
|
||||||
"pingAvg": "平均:",
|
"pingAvg": "平均:",
|
||||||
"pingInputIP": "対象のIPまたはドメインを入力してください",
|
"pingInputIP": "対象のIPまたはドメインを入力してください",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "Plakken",
|
"paste": "Plakken",
|
||||||
"path": "Pad",
|
"path": "Pad",
|
||||||
"percentOfSize": "{percent}% van {size}",
|
"percentOfSize": "{percent}% van {size}",
|
||||||
|
"permission": "Machtigingen",
|
||||||
"pickFile": "Bestand kiezen",
|
"pickFile": "Bestand kiezen",
|
||||||
"pingAvg": "Gem:",
|
"pingAvg": "Gem:",
|
||||||
"pingInputIP": "Voer een doel-IP / domein in.",
|
"pingInputIP": "Voer een doel-IP / domein in.",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "Colar",
|
"paste": "Colar",
|
||||||
"path": "Caminho",
|
"path": "Caminho",
|
||||||
"percentOfSize": "{percent}% de {size}",
|
"percentOfSize": "{percent}% de {size}",
|
||||||
|
"permission": "Permissões",
|
||||||
"pickFile": "Escolher arquivo",
|
"pickFile": "Escolher arquivo",
|
||||||
"pingAvg": "Média:",
|
"pingAvg": "Média:",
|
||||||
"pingInputIP": "Por favor, insira o IP ou domínio alvo",
|
"pingInputIP": "Por favor, insira o IP ou domínio alvo",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "вставить",
|
"paste": "вставить",
|
||||||
"path": "путь",
|
"path": "путь",
|
||||||
"percentOfSize": "{percent}% от {size}",
|
"percentOfSize": "{percent}% от {size}",
|
||||||
|
"permission": "Разрешения",
|
||||||
"pickFile": "выбрать файл",
|
"pickFile": "выбрать файл",
|
||||||
"pingAvg": "Среднее:",
|
"pingAvg": "Среднее:",
|
||||||
"pingInputIP": "Пожалуйста, введите целевой IP или доменное имя",
|
"pingInputIP": "Пожалуйста, введите целевой IP или доменное имя",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "粘贴",
|
"paste": "粘贴",
|
||||||
"path": "路径",
|
"path": "路径",
|
||||||
"percentOfSize": "{size} 的 {percent}%",
|
"percentOfSize": "{size} 的 {percent}%",
|
||||||
|
"permission": "权限",
|
||||||
"pickFile": "选择文件",
|
"pickFile": "选择文件",
|
||||||
"pingAvg": "平均:",
|
"pingAvg": "平均:",
|
||||||
"pingInputIP": "请输入目标IP或域名",
|
"pingInputIP": "请输入目标IP或域名",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"paste": "貼上",
|
"paste": "貼上",
|
||||||
"path": "路徑",
|
"path": "路徑",
|
||||||
"percentOfSize": "{size} 的 {percent}%",
|
"percentOfSize": "{size} 的 {percent}%",
|
||||||
|
"permission": "權限",
|
||||||
"pickFile": "選擇檔案",
|
"pickFile": "選擇檔案",
|
||||||
"pingAvg": "平均:",
|
"pingAvg": "平均:",
|
||||||
"pingInputIP": "請輸入目標 IP 或域名",
|
"pingInputIP": "請輸入目標 IP 或域名",
|
||||||
|
|||||||
@@ -5,18 +5,20 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/core/extension/sftpfile.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/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/misc.dart';
|
||||||
import 'package:server_box/data/res/provider.dart';
|
import 'package:server_box/data/res/provider.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/view/widget/omit_start_text.dart';
|
import 'package:server_box/view/widget/omit_start_text.dart';
|
||||||
|
|
||||||
import '../../../core/route.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
import '../../../data/model/server/server_private_info.dart';
|
import 'package:server_box/view/widget/two_line_text.dart';
|
||||||
import '../../../data/model/sftp/absolute_path.dart';
|
import 'package:server_box/view/widget/unix_perm.dart';
|
||||||
import '../../../data/model/sftp/browser_status.dart';
|
|
||||||
import '../../../data/model/sftp/req.dart';
|
|
||||||
import '../../widget/two_line_text.dart';
|
|
||||||
|
|
||||||
class SftpPage extends StatefulWidget {
|
class SftpPage extends StatefulWidget {
|
||||||
final ServerPrivateInfo spi;
|
final ServerPrivateInfo spi;
|
||||||
@@ -174,49 +176,50 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
|
|
||||||
Widget _buildUploadBtn() {
|
Widget _buildUploadBtn() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final idx = await context.showRoundDialog(
|
final idx = await context.showRoundDialog(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.open_in_new),
|
leading: const Icon(Icons.open_in_new),
|
||||||
title: Text(l10n.system),
|
title: Text(l10n.system),
|
||||||
onTap: () => context.pop(1),
|
onTap: () => context.pop(1),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.folder),
|
leading: const Icon(Icons.folder),
|
||||||
title: Text(l10n.inner),
|
title: Text(l10n.inner),
|
||||||
onTap: () => context.pop(0),
|
onTap: () => context.pop(0),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
final path = await () async {
|
final path = await () async {
|
||||||
switch (idx) {
|
switch (idx) {
|
||||||
case 0:
|
case 0:
|
||||||
return await AppRoutes.localStorage(isPickFile: true)
|
return await AppRoutes.localStorage(isPickFile: true)
|
||||||
.go<String>(context);
|
.go<String>(context);
|
||||||
case 1:
|
case 1:
|
||||||
return await Pfs.pickFilePath();
|
return await Pfs.pickFilePath();
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
}();
|
|
||||||
if (path == null) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
final remoteDir = _status.path?.path;
|
}();
|
||||||
if (remoteDir == null) {
|
if (path == null) {
|
||||||
context.showSnackBar('remote path is null');
|
return;
|
||||||
return;
|
}
|
||||||
}
|
final remoteDir = _status.path?.path;
|
||||||
final remotePath = '$remoteDir/${path.split('/').last}';
|
if (remoteDir == null) {
|
||||||
Loggers.app.info('SFTP upload local: $path, remote: $remotePath');
|
context.showSnackBar('remote path is null');
|
||||||
Pros.sftp.add(
|
return;
|
||||||
SftpReq(widget.spi, remotePath, path, SftpReqType.upload),
|
}
|
||||||
);
|
final remotePath = '$remoteDir/${path.split('/').last}';
|
||||||
},
|
Loggers.app.info('SFTP upload local: $path, remote: $remotePath');
|
||||||
icon: const Icon(Icons.upload_file));
|
Pros.sftp.add(
|
||||||
|
SftpReq(widget.spi, remotePath, path, SftpReqType.upload),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.upload_file),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAddBtn() {
|
Widget _buildAddBtn() {
|
||||||
@@ -371,6 +374,43 @@ class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
|
|||||||
title: Text(l10n.rename),
|
title: Text(l10n.rename),
|
||||||
onTap: () => _rename(file),
|
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) {
|
if (notDir) {
|
||||||
children.addAll([
|
children.addAll([
|
||||||
|
|||||||
128
lib/view/widget/unix_perm.dart
Normal file
128
lib/view/widget/unix_perm.dart
Normal file
@@ -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<UnixPermEditor> {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user