SFTP init.

This commit is contained in:
Junyuan Feng
2022-02-18 13:32:50 +08:00
parent 282e61afac
commit f07d33a1d6
7 changed files with 176 additions and 93 deletions

1
.gitignore vendored
View File

@@ -47,3 +47,4 @@ app.*.map.json
/android/app/fjy.androidstudio.key /android/app/fjy.androidstudio.key
/release /release
test.dart

View File

@@ -15,7 +15,7 @@ class MenuItems {
static const List<MenuItem> secondItems = [edit]; static const List<MenuItem> secondItems = [edit];
static const ssh = MenuItem(text: 'SSH', icon: Icons.link); static const ssh = MenuItem(text: 'SSH', icon: Icons.link);
static const sftp = MenuItem(text: 'SFTP', icon: Icons.file_present); static const sftp = MenuItem(text: 'SFTP', icon: Icons.insert_drive_file);
static const snippet = MenuItem(text: 'Snippet', icon: Icons.label); static const snippet = MenuItem(text: 'Snippet', icon: Icons.label);
static const apt = MenuItem(text: 'Apt', icon: Icons.system_security_update); static const apt = MenuItem(text: 'Apt', icon: Icons.system_security_update);
static const edit = MenuItem(text: 'Edit', icon: Icons.edit); static const edit = MenuItem(text: 'Edit', icon: Icons.edit);

View File

@@ -0,0 +1,29 @@
class AbsolutePath {
String path;
String? _prePath;
AbsolutePath(this.path);
void update(String newPath) {
_prePath = path;
if (newPath == '..') {
path = path.substring(0, path.lastIndexOf('/'));
if (path == '') {
path = '/';
}
return;
}
if (newPath == '/') {
path = '/';
return;
}
path = path + (path.endsWith('/') ? '' : '/') + newPath;
}
bool undo() {
if (_prePath == null) {
return false;
}
path = _prePath!;
return true;
}
}

View File

@@ -0,0 +1,39 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/sftp/absolute_path.dart';
class SFTPSideViewStatus {
bool leftSelected = false;
bool rightSelected = false;
ServerPrivateInfo? leftSpi;
ServerPrivateInfo? rightSpi;
List<SftpName>? leftFiles;
List<SftpName>? rightFiles;
AbsolutePath? leftPath;
AbsolutePath? rightPath;
SftpClient? leftClient;
SftpClient? rightClient;
SFTPSideViewStatus();
ServerPrivateInfo? spi(bool left) => left ? leftSpi : rightSpi;
void setSpi(bool left, ServerPrivateInfo nSpi) =>
left ? leftSpi = nSpi : rightSpi = nSpi;
/// Whether the Left/Right Destination is selected.
bool selected(bool left) => left ? leftSelected : rightSelected;
void setSelect(bool left, bool nSelect) =>
left ? leftSelected = nSelect : rightSelected = nSelect;
List<SftpName>? files(bool left) => left ? leftFiles : rightFiles;
void setFiles(bool left, List<SftpName>? nFiles) =>
left ? leftFiles = nFiles : rightFiles = nFiles;
AbsolutePath? path(bool left) => left ? leftPath : rightPath;
void setPath(bool left, AbsolutePath? nPath) =>
left ? leftPath = nPath : rightPath = nPath;
SftpClient? client(bool left) => left ? leftClient : rightClient;
void setClient(bool left, SftpClient? nClient) =>
left ? leftClient = nClient : rightClient = nClient;
}

View File

@@ -2,9 +2,9 @@
class BuildData { class BuildData {
static const String name = "ServerBox"; static const String name = "ServerBox";
static const int build = 97; static const int build = 98;
static const String engine = static const String engine =
"Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (8 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n"; "╔════════════════════════════════════════════════════════════════════════════╗\n║ A new version of Flutter is available! ║\n║ ║\n║ To update to the latest version, run \"flutter upgrade\". ║\n╚════════════════════════════════════════════════════════════════════════════╝\n\nFlutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (9 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n";
static const String buildAt = "2022-02-10 19:30:23.388434"; static const String buildAt = "2022-02-18 13:28:18.254386";
static const int modifications = 9; static const int modifications = 5;
} }

View File

@@ -97,7 +97,7 @@ class _MyHomePageState extends State<MyHomePage>
Widget _buildMain(BuildContext context) { Widget _buildMain(BuildContext context) {
return AdvancedDrawer( return AdvancedDrawer(
controller: _advancedDrawerController, controller: _advancedDrawerController,
animationCurve: Curves.easeInOutCirc, animationCurve: Curves.easeInOut,
animationDuration: const Duration(milliseconds: 300), animationDuration: const Duration(milliseconds: 300),
animateChildDecoration: true, animateChildDecoration: true,
rtlOpening: false, rtlOpening: false,

View File

@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart'; import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/model/server/server_connection_state.dart'; import 'package:toolbox/data/model/server/server_connection_state.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/sftp/absolute_path.dart';
import 'package:toolbox/data/model/sftp/sftp_side_status.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/fade_in.dart'; import 'package:toolbox/view/widget/fade_in.dart';
@@ -16,13 +18,7 @@ class SFTPPage extends StatefulWidget {
} }
class _SFTPPageState extends State<SFTPPage> { class _SFTPPageState extends State<SFTPPage> {
/// Whether the Left/Right Destination is selected. final SFTPSideViewStatus _status = SFTPSideViewStatus();
final List<bool> _selectedDest = List<bool>.filled(2, false);
final List<ServerPrivateInfo?> _destSpi =
List<ServerPrivateInfo?>.filled(2, null);
final List<List<SftpName>?> _files = List<List<SftpName>?>.filled(2, null);
final List<String> _paths = List<String>.filled(2, '');
final List<SftpClient?> _clients = List<SftpClient?>.filled(2, null);
final ScrollController _leftScrollController = ScrollController(); final ScrollController _leftScrollController = ScrollController();
final ScrollController _rightScrollController = ScrollController(); final ScrollController _rightScrollController = ScrollController();
@@ -39,8 +35,8 @@ class _SFTPPageState extends State<SFTPPage> {
void initState() { void initState() {
super.initState(); super.initState();
if (widget.spi != null) { if (widget.spi != null) {
_destSpi[0] = widget.spi; _status.setSpi(true, widget.spi!);
_selectedDest[0] = true; _status.setSelect(true, true);
} }
} }
@@ -49,7 +45,7 @@ class _SFTPPageState extends State<SFTPPage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: Text(_titleText), title: const Text('SFTP'),
), ),
body: Row( body: Row(
children: [ children: [
@@ -63,31 +59,10 @@ class _SFTPPageState extends State<SFTPPage> {
); );
} }
String get _titleText {
List<String> titles = [
'',
'',
];
if (_selectedDest[0]) {
titles[0] = _destSpi[0]?.name ?? '';
}
if (_selectedDest[1]) {
titles[1] = _destSpi[1]?.name ?? '';
}
return titles[0] == '' || titles[1] == '' ? 'SFTP' : titles.join(' - ');
}
Widget _buildSingleColumn(bool left) { Widget _buildSingleColumn(bool left) {
Widget child;
if (!_selectedDest[left ? 0 : 1]) {
child = _buildDestSelector(left);
} else {
child = _buildFileView(left);
}
return SizedBox( return SizedBox(
width: (_media.size.width - 2) / 2, width: (_media.size.width - 2) / 2,
child: child, child: _buildFileView(left),
); );
} }
@@ -103,7 +78,14 @@ class _SFTPPageState extends State<SFTPPage> {
); );
Widget _buildFileView(bool left) { Widget _buildFileView(bool left) {
final spi = _destSpi[left ? 0 : 1]; if (!_status.selected(left)) {
return ListView(
children: [
_buildDestSelector(left),
],
);
}
final spi = _status.spi(left);
final si = final si =
locator<ServerProvider>().servers.firstWhere((s) => s.info == spi); locator<ServerProvider>().servers.firstWhere((s) => s.info == spi);
final client = si.client; final client = si.client;
@@ -112,44 +94,45 @@ class _SFTPPageState extends State<SFTPPage> {
return centerCircleLoading; return centerCircleLoading;
} }
if (_files[left ? 0 : 1] == null) { if (_status.files(left) == null) {
updatePath('/', left); _status.setPath(left, AbsolutePath('/'));
listDir(client, '/', left); listDir('/', left, client: client);
return centerCircleLoading; return centerCircleLoading;
} else { } else {
return RefreshIndicator( return RefreshIndicator(
child: FadeIn( child: FadeIn(
child: ListView.builder( child: ListView.builder(
itemCount: _files[left ? 0 : 1]!.length, itemCount: _status.files(left)!.length + 1,
controller: left ? _leftScrollController : _rightScrollController, controller: left ? _leftScrollController : _rightScrollController,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final file = _files[left ? 0 : 1]![index]; if (index == 0) {
return _buildDestSelector(left);
}
final file = _status.files(left)![index - 1];
final isDir = file.attr.mode?.isDirectory ?? true; final isDir = file.attr.mode?.isDirectory ?? true;
return ListTile( return ListTile(
leading: leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
Icon(isDir ? Icons.folder : Icons.insert_drive_file), title: Text(file.filename),
title: Text(file.filename), subtitle:
subtitle: isDir isDir ? null : Text((convertBytes(file.attr.size ?? 0))),
? null onTap: () {
: Text((convertBytes(file.attr.size ?? 0))), if (isDir) {
onTap: () { _status.path(left)?.update(file.filename);
if (isDir) { listDir(_status.path(left)?.path ?? '/', left);
updatePath(file.filename, left); } else {
listDir(client, _paths[left ? 0 : 1], left); onItemPress(context, left, file);
} else { }
// downloadFile(client, file.name); },
} );
},
onLongPress: () => onItemLongPress(context, left, file));
}, },
), ),
key: Key(_paths[left ? 0 : 1]), key: Key(_status.spi(left)!.name + _status.path(left)!.path),
), ),
onRefresh: () => listDir(client, _paths[left ? 0 : 1], left)); onRefresh: () => listDir(_status.path(left)?.path ?? '/', left));
} }
} }
void onItemLongPress(BuildContext context, bool left, SftpName file) { void onItemPress(BuildContext context, bool left, SftpName file) {
showRoundDialog( showRoundDialog(
context, context,
'Action', 'Action',
@@ -226,8 +209,8 @@ class _SFTPPageState extends State<SFTPPage> {
]); ]);
return; return;
} }
_clients[left ? 0 : 1]! _status.client(left)!.mkdir(
.mkdir(_paths[left ? 0 : 1] + '/' + textController.text); _status.path(left)!.path + '/' + textController.text);
}, },
child: const Text( child: const Text(
'Create', 'Create',
@@ -262,7 +245,8 @@ class _SFTPPageState extends State<SFTPPage> {
]); ]);
return; return;
} }
await _clients[left ? 0 : 1]! await _status
.client(left)!
.rename(file.filename, textController.text); .rename(file.filename, textController.text);
}, },
child: const Text( child: const Text(
@@ -286,40 +270,66 @@ class _SFTPPageState extends State<SFTPPage> {
return '$finalValue ${suffix[squareTimes]}'; return '$finalValue ${suffix[squareTimes]}';
} }
void updatePath(String filename, bool left) { Future<void> listDir(String path, bool left, {SSHClient? client}) async {
if (filename == '..') { if (client != null) {
_paths[left ? 0 : 1] = _paths[left ? 0 : 1] final sftpc = await client.sftp();
.substring(0, _paths[left ? 0 : 1].lastIndexOf('/')); _status.setClient(left, sftpc);
if (_paths[left ? 0 : 1] == '') {
_paths[left ? 0 : 1] = '/';
}
return;
} }
_paths[left ? 0 : 1] = _paths[left ? 0 : 1] + final fs = await _status.client(left)!.listdir(path);
(_paths[left ? 0 : 1].endsWith('/') ? '' : '/') +
filename;
}
Future<void> listDir(SSHClient client, String path, bool left) async {
final sftpc = await client.sftp();
_clients[left ? 0 : 1] = sftpc;
final fs = await sftpc.listdir(path);
fs.sort((a, b) => a.filename.compareTo(b.filename)); fs.sort((a, b) => a.filename.compareTo(b.filename));
fs.removeAt(0); fs.removeAt(0);
if (mounted) { if (mounted) {
setState(() { setState(() {
_files[left ? 0 : 1] = fs; _status.setFiles(left, fs);
}); });
} }
} }
Widget _buildDestSelector(bool left) { Widget _buildDestSelector(bool left) {
return Column( final str = _status.path(left)?.path;
children: locator<ServerProvider>() return ExpansionTile(
.servers title: Text(_status.spi(left)?.name ?? 'Choose target'),
.map((e) => _buildDestSelectorItem(e.info, left)) subtitle: _status.selected(left)
.toList(), ? LayoutBuilder(builder: (context, size) {
); bool exceeded = false;
int len = 0;
for (; !exceeded && len < str!.length; len++) {
// Build the textspan
var span = TextSpan(
text: '...' + str.substring(str.length - len),
style: TextStyle(
fontSize:
Theme.of(context).textTheme.bodyText1?.fontSize ??
14),
);
// Use a textpainter to determine if it will exceed max lines
var tp = TextPainter(
maxLines: 1,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,
text: span,
);
// trigger it to layout
tp.layout(maxWidth: size.maxWidth);
// whether the text overflowed or not
exceeded = tp.didExceedMaxLines;
}
return Text(
(exceeded ? '...' : '') + str!.substring(str.length - len),
overflow: TextOverflow.clip,
maxLines: 1,
style: const TextStyle(color: Colors.grey),
);
})
: null,
children: locator<ServerProvider>()
.servers
.map((e) => _buildDestSelectorItem(e.info, left))
.toList());
} }
Widget _buildDestSelectorItem(ServerPrivateInfo spi, bool left) { Widget _buildDestSelectorItem(ServerPrivateInfo spi, bool left) {
@@ -327,10 +337,14 @@ class _SFTPPageState extends State<SFTPPage> {
title: Text(spi.name), title: Text(spi.name),
subtitle: Text('${spi.user}@${spi.ip}:${spi.port}'), subtitle: Text('${spi.user}@${spi.ip}:${spi.port}'),
onTap: () { onTap: () {
setState(() { _status.setSpi(left, spi);
_destSpi[left ? 0 : 1] = spi; _status.setSelect(left, true);
_selectedDest[left ? 0 : 1] = true; _status.setPath(left, AbsolutePath('/'));
}); listDir('/', left,
client: locator<ServerProvider>()
.servers
.firstWhere((s) => s.info == spi)
.client);
}, },
); );
} }