mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-07 12:48:28 +08:00
feat: sftp优化
This commit is contained in:
@@ -13,6 +13,13 @@ import 'editor/editor_language.dart';
|
||||
|
||||
enum SftpConnectionStatus { disconnected, connecting, connected, error }
|
||||
|
||||
class SftpConnectTimeoutException implements Exception {
|
||||
const SftpConnectTimeoutException();
|
||||
|
||||
@override
|
||||
String toString() => 'SFTP connection timed out';
|
||||
}
|
||||
|
||||
class SftpFileEntry {
|
||||
const SftpFileEntry({
|
||||
required this.name,
|
||||
@@ -160,6 +167,7 @@ class SftpSessionManager extends ChangeNotifier {
|
||||
required ServerModel server,
|
||||
required SshConnectionConfig config,
|
||||
Future<List<SftpFavorite>> Function(String hostId)? loadFavorites,
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
final existing = _states[server.id];
|
||||
if (existing?.status == SftpConnectionStatus.connected) {
|
||||
@@ -182,47 +190,22 @@ class SftpSessionManager extends ChangeNotifier {
|
||||
|
||||
try {
|
||||
await _connections.remove(server.id)?.close();
|
||||
final transport = await _transportFactory.open(config);
|
||||
final identities = config.authType == 'privateKey'
|
||||
? SSHKeyPair.fromPem(config.privateKey, config.privateKeyPassphrase)
|
||||
: null;
|
||||
final client = SSHClient(
|
||||
transport.socket,
|
||||
username: config.username,
|
||||
onPasswordRequest: config.authType == 'password'
|
||||
? () => config.password
|
||||
: null,
|
||||
identities: identities,
|
||||
);
|
||||
state.connectPhase = 'channel';
|
||||
notifyListeners();
|
||||
final sftp = await client.sftp();
|
||||
_connections[server.id] = _SftpConnection(
|
||||
client: client,
|
||||
sftp: sftp,
|
||||
transport: transport,
|
||||
);
|
||||
|
||||
state.connectPhase = 'listing';
|
||||
notifyListeners();
|
||||
final results = await Future.wait<Object?>([
|
||||
_resolveInitialPathAndList(sftp, config.username, state.showHidden),
|
||||
_safeLoadFavorites(loadFavorites, server.id),
|
||||
]);
|
||||
final listing = results[0] as _InitialListing;
|
||||
final favorites = results[1] as List<SftpFavorite>;
|
||||
return await _runConnect(
|
||||
server: server,
|
||||
config: config,
|
||||
state: state,
|
||||
loadFavorites: loadFavorites,
|
||||
).timeout(timeout);
|
||||
} on TimeoutException {
|
||||
await _connections.remove(server.id)?.close();
|
||||
state.connectPhase = null;
|
||||
state.update(
|
||||
status: SftpConnectionStatus.connected,
|
||||
currentPath: listing.path,
|
||||
entries: listing.entries,
|
||||
favorites: favorites,
|
||||
status: SftpConnectionStatus.error,
|
||||
loadingDirectory: false,
|
||||
lastError: null,
|
||||
selectedNames: <String>{},
|
||||
lastError: 'timeout',
|
||||
);
|
||||
notifyListeners();
|
||||
return state;
|
||||
throw const SftpConnectTimeoutException();
|
||||
} catch (error) {
|
||||
await _connections.remove(server.id)?.close();
|
||||
state.connectPhase = null;
|
||||
@@ -236,6 +219,55 @@ class SftpSessionManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<SftpSessionState> _runConnect({
|
||||
required ServerModel server,
|
||||
required SshConnectionConfig config,
|
||||
required SftpSessionState state,
|
||||
required Future<List<SftpFavorite>> Function(String hostId)? loadFavorites,
|
||||
}) async {
|
||||
final transport = await _transportFactory.open(config);
|
||||
final identities = config.authType == 'privateKey'
|
||||
? SSHKeyPair.fromPem(config.privateKey, config.privateKeyPassphrase)
|
||||
: null;
|
||||
final client = SSHClient(
|
||||
transport.socket,
|
||||
username: config.username,
|
||||
onPasswordRequest: config.authType == 'password'
|
||||
? () => config.password
|
||||
: null,
|
||||
identities: identities,
|
||||
);
|
||||
state.connectPhase = 'channel';
|
||||
notifyListeners();
|
||||
final sftp = await client.sftp();
|
||||
_connections[server.id] = _SftpConnection(
|
||||
client: client,
|
||||
sftp: sftp,
|
||||
transport: transport,
|
||||
);
|
||||
|
||||
state.connectPhase = 'listing';
|
||||
notifyListeners();
|
||||
final results = await Future.wait<Object?>([
|
||||
_resolveInitialPathAndList(sftp, config.username, state.showHidden),
|
||||
_safeLoadFavorites(loadFavorites, server.id),
|
||||
]);
|
||||
final listing = results[0] as _InitialListing;
|
||||
final favorites = results[1] as List<SftpFavorite>;
|
||||
state.connectPhase = null;
|
||||
state.update(
|
||||
status: SftpConnectionStatus.connected,
|
||||
currentPath: listing.path,
|
||||
entries: listing.entries,
|
||||
favorites: favorites,
|
||||
loadingDirectory: false,
|
||||
lastError: null,
|
||||
selectedNames: <String>{},
|
||||
);
|
||||
notifyListeners();
|
||||
return state;
|
||||
}
|
||||
|
||||
void activate(String hostId) {
|
||||
if (_states[hostId] == null) return;
|
||||
_activeHostId = hostId;
|
||||
|
||||
@@ -88,7 +88,11 @@ class _SftpTabState extends ConsumerState<SftpTab> {
|
||||
await ref.read(authProvider.notifier).signOut();
|
||||
return;
|
||||
}
|
||||
_showSnack(error.toString());
|
||||
if (error is SftpConnectTimeoutException) {
|
||||
_showSnack(l.tr('sftp.connectTimeout'));
|
||||
} else {
|
||||
_showSnack(error.toString());
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _connecting = false);
|
||||
}
|
||||
@@ -114,7 +118,7 @@ class _SftpTabState extends ConsumerState<SftpTab> {
|
||||
final session = manager.activeSession;
|
||||
final showSelector = !_connecting &&
|
||||
session != null &&
|
||||
session.status == SftpConnectionStatus.connected;
|
||||
session.status != SftpConnectionStatus.connecting;
|
||||
return Column(
|
||||
children: [
|
||||
TabHeader(
|
||||
@@ -176,6 +180,18 @@ class _SftpTabState extends ConsumerState<SftpTab> {
|
||||
],
|
||||
);
|
||||
}
|
||||
if (session.status == SftpConnectionStatus.error) {
|
||||
return _SftpMessageList(
|
||||
message: session.lastError == 'timeout'
|
||||
? l.tr('sftp.connectTimeout')
|
||||
: (session.lastError ??
|
||||
l.tr('sftp.connectFailed')),
|
||||
action: TextButton(
|
||||
onPressed: () => _connectOrActivate(session.server),
|
||||
child: Text(l.tr('common.retry')),
|
||||
),
|
||||
);
|
||||
}
|
||||
return _SftpConnectedView(
|
||||
session: session,
|
||||
manager: manager,
|
||||
@@ -647,8 +663,14 @@ class _SftpHeaderSelector extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isError = session.status == SftpConnectionStatus.error;
|
||||
final dotColor = isError ? _SftpPalette.danger : _SftpPalette.success;
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
constraints: BoxConstraints(
|
||||
minWidth: screenWidth / 4,
|
||||
maxWidth: screenWidth / 2,
|
||||
),
|
||||
child: Material(
|
||||
color: _SftpPalette.card,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -663,18 +685,17 @@ class _SftpHeaderSelector extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(
|
||||
color: _SftpPalette.success,
|
||||
decoration: BoxDecoration(
|
||||
color: dotColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
Expanded(
|
||||
child: Text(
|
||||
session.server.displayName,
|
||||
maxLines: 1,
|
||||
@@ -2130,12 +2151,10 @@ class _SftpServerRow extends StatelessWidget {
|
||||
color: _SftpPalette.success,
|
||||
size: 20,
|
||||
)
|
||||
else
|
||||
else if (!connected)
|
||||
_SftpStatusPill(
|
||||
label: connected
|
||||
? l.tr('sftp.statusConnected')
|
||||
: l.tr('sftp.statusDisconnected'),
|
||||
connected: connected,
|
||||
label: l.tr('sftp.statusDisconnected'),
|
||||
connected: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -152,6 +152,8 @@ const Map<String, String> stringsEn = {
|
||||
'sftp.phase.transport': 'Connecting to server...',
|
||||
'sftp.phase.channel': 'Opening SFTP channel...',
|
||||
'sftp.phase.listing': 'Reading directory...',
|
||||
'sftp.connectTimeout': 'Connection timed out (10s)',
|
||||
'sftp.connectFailed': 'Connection failed',
|
||||
'sftp.emptyDirectory': 'This directory is empty.',
|
||||
'sftp.upload': 'Upload',
|
||||
'sftp.newItem': 'New',
|
||||
|
||||
@@ -142,6 +142,8 @@ const Map<String, String> stringsZh = {
|
||||
'sftp.phase.transport': '正在连接服务器...',
|
||||
'sftp.phase.channel': '正在打开 SFTP 通道...',
|
||||
'sftp.phase.listing': '正在读取目录...',
|
||||
'sftp.connectTimeout': '连接超时(10s)',
|
||||
'sftp.connectFailed': '连接失败',
|
||||
'sftp.emptyDirectory': '当前目录为空',
|
||||
'sftp.upload': '上传',
|
||||
'sftp.newItem': '新建',
|
||||
|
||||
Reference in New Issue
Block a user