feat(mobile): SftpSessionManager 新增 readTextFile/writeTextFile 与异常类型

This commit is contained in:
chaos-zhu
2026-05-23 20:38:49 +08:00
parent a63c054c56
commit ac95ad0c25
2 changed files with 109 additions and 0 deletions
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
@@ -346,6 +347,56 @@ class SftpSessionManager extends ChangeNotifier {
return _readRemoteFile(connection.sftp, remotePath);
}
static const int defaultTextFileMaxBytes = 2 * 1024 * 1024;
Future<Uint8List> readTextFile(
String remotePath, {
int maxBytes = defaultTextFileMaxBytes,
}) async {
final state = activeSession;
if (state == null) {
throw StateError('No active SFTP session');
}
final connection = _connections[state.server.id];
if (connection == null) {
throw StateError('No active SFTP connection');
}
final sftp = connection.sftp;
final stat = await sftp.stat(remotePath);
final size = stat.size ?? 0;
if (size > maxBytes) {
throw SftpFileTooLargeException(
path: remotePath,
size: size,
limit: maxBytes,
);
}
final bytes = await _readRemoteFile(sftp, remotePath);
final probeLen = bytes.length < 8192 ? bytes.length : 8192;
for (var i = 0; i < probeLen; i++) {
if (bytes[i] == 0) {
throw SftpBinaryFileException(path: remotePath);
}
}
return bytes;
}
Future<void> writeTextFile(String remotePath, String content) async {
final state = activeSession;
if (state == null) {
throw StateError('No active SFTP session');
}
final connection = _connections[state.server.id];
if (connection == null) {
throw StateError('No active SFTP connection');
}
await _writeRemoteFile(
connection.sftp,
remotePath,
Uint8List.fromList(utf8.encode(content)),
);
}
Future<Uint8List> zipEntries(List<SftpFileEntry> entries) async {
final state = activeSession;
if (state == null || entries.isEmpty) return Uint8List(0);
@@ -787,3 +838,37 @@ extension on String {
return substring(count);
}
}
class SftpFileTooLargeException implements Exception {
SftpFileTooLargeException({
required this.path,
required this.size,
required this.limit,
});
final String path;
final int size;
final int limit;
@override
String toString() => 'SftpFileTooLargeException($path, size=$size, limit=$limit)';
}
class SftpBinaryFileException implements Exception {
SftpBinaryFileException({required this.path});
final String path;
@override
String toString() => 'SftpBinaryFileException($path)';
}
class SftpTextFileData {
const SftpTextFileData({
required this.bytes,
required this.malformedUtf8,
});
final Uint8List bytes;
final bool malformedUtf8;
}
@@ -0,0 +1,24 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile/features/shell/sftp_session_manager.dart';
void main() {
group('Sftp text exceptions', () {
test('SftpFileTooLargeException carries size info', () {
final err = SftpFileTooLargeException(
path: '/tmp/big.log',
size: 5 * 1024 * 1024,
limit: 2 * 1024 * 1024,
);
expect(err.path, '/tmp/big.log');
expect(err.size, 5 * 1024 * 1024);
expect(err.limit, 2 * 1024 * 1024);
expect(err.toString(), contains('big.log'));
});
test('SftpBinaryFileException carries path', () {
final err = SftpBinaryFileException(path: '/usr/bin/ls');
expect(err.path, '/usr/bin/ls');
expect(err.toString(), contains('ls'));
});
});
}