Skip to content

Commit 5484183

Browse files
authored
Enhancing SFTP timeout handling (#1216)
* Improve SFTP code quality * Harden SFTP transfer handling * Refactor SFTP transfer models
1 parent c74175a commit 5484183

9 files changed

Lines changed: 270 additions & 232 deletions

File tree

lib/core/utils/sftp_timeout.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import 'dart:async';
2+
3+
import 'package:dartssh2/dartssh2.dart';
4+
import 'package:fl_lib/fl_lib.dart';
5+
6+
Duration sftpOperationTimeout(int seconds) {
7+
return Duration(seconds: seconds <= 0 ? 5 : seconds);
8+
}
9+
10+
Future<T> withSftpOpTimeout<T>(
11+
String operation,
12+
Future<T> future,
13+
Duration timeout,
14+
) async {
15+
try {
16+
return await future.timeout(timeout);
17+
} on TimeoutException catch (e, s) {
18+
final error = TimeoutException('SFTP $operation timed out', timeout);
19+
Loggers.app.warning(error.message, e, s);
20+
throw error;
21+
}
22+
}
23+
24+
Future<SftpClient> withSftpSessionOpenTimeout(
25+
String operation,
26+
Future<SftpClient> future,
27+
Duration timeout,
28+
) async {
29+
try {
30+
return await future.timeout(timeout);
31+
} on TimeoutException catch (e, s) {
32+
unawaited(
33+
future.then((client) => client.close()).catchError((Object closeError) {
34+
Loggers.app.warning('Failed to close timed out SFTP session', closeError);
35+
}),
36+
);
37+
final error = TimeoutException('SFTP $operation timed out', timeout);
38+
Loggers.app.warning(error.message, e, s);
39+
throw error;
40+
}
41+
}

lib/data/model/sftp/req.dart

Lines changed: 7 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
part of 'worker.dart';
1+
import 'package:fl_lib/fl_lib.dart';
2+
import 'package:server_box/core/utils/jump_chain.dart';
3+
import 'package:server_box/core/utils/refresh_interval.dart';
4+
import 'package:server_box/core/utils/server.dart';
5+
import 'package:server_box/data/model/server/server_private_info.dart';
6+
import 'package:server_box/data/res/default.dart';
7+
import 'package:server_box/data/res/store.dart';
28

39
class SftpReq {
410
final Spi spi;
@@ -87,106 +93,4 @@ class SftpTransferProgress {
8793
});
8894
}
8995

90-
class SftpReqStatus {
91-
final int id;
92-
final SftpReq req;
93-
final void Function() notifyListeners;
94-
late SftpWorker worker;
95-
final Completer? completer;
96-
97-
String get fileName => req.localPath.split(Pfs.seperator).last;
98-
99-
// status of the download
100-
double? progress;
101-
double? speedBytesPerSecond;
102-
int? transferredBytes;
103-
DateTime? _speedSampleTime;
104-
int _speedSampleBytes = 0;
105-
SftpWorkerStatus? status;
106-
int? size;
107-
Exception? error;
108-
Duration? spentTime;
109-
110-
SftpReqStatus({
111-
required this.req,
112-
required this.notifyListeners,
113-
this.completer,
114-
}) : id = DateTime.now().microsecondsSinceEpoch {
115-
worker = SftpWorker(onNotify: onNotify, req: req)..init();
116-
}
117-
118-
@override
119-
bool operator ==(Object other) => other is SftpReqStatus && id == other.id;
120-
121-
@override
122-
int get hashCode => id ^ super.hashCode;
123-
124-
void dispose() {
125-
worker._dispose();
126-
completer?.complete(true);
127-
}
128-
129-
void onNotify(dynamic event) {
130-
var shouldDispose = false;
131-
switch (event) {
132-
case final SftpWorkerStatus val:
133-
status = val;
134-
if (status == SftpWorkerStatus.finished) {
135-
dispose();
136-
}
137-
break;
138-
case final double val:
139-
progress = val;
140-
break;
141-
case final SftpTransferProgress val:
142-
progress = val.percent;
143-
transferredBytes = val.transferredBytes;
144-
_initSpeedSampleIfNeeded(val.transferredBytes);
145-
break;
146-
case final int val:
147-
size = val;
148-
break;
149-
case final Duration d:
150-
spentTime = d;
151-
break;
152-
default:
153-
error = Exception('sftp worker event: $event');
154-
Loggers.app.warning(error);
155-
shouldDispose = true;
156-
}
157-
notifyListeners();
158-
if (shouldDispose) dispose();
159-
}
160-
161-
void _initSpeedSampleIfNeeded(int transferredBytes) {
162-
if (_speedSampleTime != null) return;
163-
_speedSampleTime = DateTime.now();
164-
_speedSampleBytes = transferredBytes;
165-
}
166-
167-
bool refreshSpeed([DateTime? now]) {
168-
if (status != SftpWorkerStatus.loading) return false;
169-
170-
final sampleTime = now ?? DateTime.now();
171-
final sampleBytes = transferredBytes ?? 0;
172-
final lastSampleTime = _speedSampleTime;
173-
if (lastSampleTime == null) {
174-
_speedSampleTime = sampleTime;
175-
_speedSampleBytes = sampleBytes;
176-
return false;
177-
}
178-
179-
final elapsedMs = sampleTime.difference(lastSampleTime).inMilliseconds;
180-
if (elapsedMs <= 0) return false;
181-
182-
final speed = (sampleBytes - _speedSampleBytes) * 1000 / elapsedMs;
183-
_speedSampleTime = sampleTime;
184-
_speedSampleBytes = sampleBytes;
185-
186-
if (speedBytesPerSecond == speed) return false;
187-
speedBytesPerSecond = speed;
188-
return true;
189-
}
190-
}
191-
19296
enum SftpWorkerStatus { preparing, sshConnectted, loading, finished }

lib/data/model/sftp/status.dart

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import 'dart:async';
2+
3+
import 'package:fl_lib/fl_lib.dart';
4+
import 'package:server_box/data/model/sftp/req.dart';
5+
import 'package:server_box/data/model/sftp/worker.dart';
6+
7+
class SftpReqStatus {
8+
final int id;
9+
final SftpReq req;
10+
final void Function() notifyListeners;
11+
late SftpWorker worker;
12+
final Completer? completer;
13+
14+
String get fileName => req.localPath.split(Pfs.seperator).last;
15+
16+
// status of the download
17+
double? progress;
18+
double? speedBytesPerSecond;
19+
int? transferredBytes;
20+
DateTime? _speedSampleTime;
21+
int _speedSampleBytes = 0;
22+
SftpWorkerStatus? status;
23+
int? size;
24+
Exception? error;
25+
Duration? spentTime;
26+
bool _disposed = false;
27+
28+
SftpReqStatus({
29+
required this.req,
30+
required this.notifyListeners,
31+
this.completer,
32+
}) : id = DateTime.now().microsecondsSinceEpoch {
33+
worker = SftpWorker(onNotify: onNotify, req: req);
34+
unawaited(_initWorker());
35+
}
36+
37+
@override
38+
bool operator ==(Object other) => other is SftpReqStatus && id == other.id;
39+
40+
@override
41+
int get hashCode => id.hashCode;
42+
43+
void dispose() {
44+
if (_disposed) return;
45+
_disposed = true;
46+
worker.dispose();
47+
if (completer?.isCompleted == false) {
48+
completer?.complete(true);
49+
}
50+
}
51+
52+
Future<void> _initWorker() async {
53+
try {
54+
await worker.init();
55+
} catch (e, s) {
56+
Loggers.app.warning('Failed to initialize SFTP worker', e, s);
57+
onNotify(e);
58+
}
59+
}
60+
61+
void onNotify(dynamic event) {
62+
var shouldDispose = false;
63+
switch (event) {
64+
case final SftpWorkerStatus val:
65+
status = val;
66+
if (status == SftpWorkerStatus.finished) {
67+
dispose();
68+
}
69+
break;
70+
case final double val:
71+
progress = val;
72+
break;
73+
case final SftpTransferProgress val:
74+
progress = val.percent;
75+
transferredBytes = val.transferredBytes;
76+
_initSpeedSampleIfNeeded(val.transferredBytes);
77+
break;
78+
case final int val:
79+
size = val;
80+
break;
81+
case final Duration d:
82+
spentTime = d;
83+
break;
84+
default:
85+
error = Exception('sftp worker event: $event');
86+
Loggers.app.warning(error);
87+
shouldDispose = true;
88+
}
89+
notifyListeners();
90+
if (shouldDispose) dispose();
91+
}
92+
93+
void _initSpeedSampleIfNeeded(int transferredBytes) {
94+
if (_speedSampleTime != null) return;
95+
_speedSampleTime = DateTime.now();
96+
_speedSampleBytes = transferredBytes;
97+
}
98+
99+
bool refreshSpeed([DateTime? now]) {
100+
if (status != SftpWorkerStatus.loading) return false;
101+
102+
final sampleTime = now ?? DateTime.now();
103+
final sampleBytes = transferredBytes ?? 0;
104+
final lastSampleTime = _speedSampleTime;
105+
if (lastSampleTime == null) {
106+
_speedSampleTime = sampleTime;
107+
_speedSampleBytes = sampleBytes;
108+
return false;
109+
}
110+
111+
final elapsedMs = sampleTime.difference(lastSampleTime).inMilliseconds;
112+
if (elapsedMs <= 0) return false;
113+
114+
final speed = (sampleBytes - _speedSampleBytes) * 1000 / elapsedMs;
115+
_speedSampleTime = sampleTime;
116+
_speedSampleBytes = sampleBytes;
117+
118+
if (speedBytesPerSecond == speed) return false;
119+
speedBytesPerSecond = speed;
120+
return true;
121+
}
122+
}

0 commit comments

Comments
 (0)