zmodem.dart 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:typed_data';
  4. import 'package:dartssh2/dartssh2.dart';
  5. import 'package:file_picker/file_picker.dart';
  6. import 'package:flutter/cupertino.dart';
  7. import 'package:path/path.dart' as path;
  8. import 'package:xterm/xterm.dart';
  9. const host = 'localhost';
  10. const port = 22;
  11. const username = '<your username>';
  12. const password = '<your password>';
  13. void main() {
  14. runApp(MyApp());
  15. }
  16. class MyApp extends StatelessWidget {
  17. @override
  18. Widget build(BuildContext context) {
  19. return CupertinoApp(
  20. title: 'xterm.dart demo',
  21. home: MyHomePage(),
  22. );
  23. }
  24. }
  25. class MyHomePage extends StatefulWidget {
  26. MyHomePage({Key? key}) : super(key: key);
  27. @override
  28. // ignore: library_private_types_in_public_api
  29. _MyHomePageState createState() => _MyHomePageState();
  30. }
  31. class _MyHomePageState extends State<MyHomePage> {
  32. late final terminal = Terminal();
  33. var title = host;
  34. @override
  35. void initState() {
  36. super.initState();
  37. initTerminal();
  38. }
  39. Future<void> initTerminal() async {
  40. terminal.write('Connecting...\r\n');
  41. final client = SSHClient(
  42. await SSHSocket.connect(host, port),
  43. username: username,
  44. onPasswordRequest: () => password,
  45. printTrace: print,
  46. );
  47. terminal.write('Connected\r\n');
  48. final session = await client.shell(
  49. pty: SSHPtyConfig(
  50. width: terminal.viewWidth,
  51. height: terminal.viewHeight,
  52. ),
  53. );
  54. terminal.buffer.clear();
  55. terminal.buffer.setCursor(0, 0);
  56. terminal.onTitleChange = (title) {
  57. setState(() => this.title = title);
  58. };
  59. terminal.onResize = (width, height, pixelWidth, pixelHeight) {
  60. session.resizeTerminal(width, height, pixelWidth, pixelHeight);
  61. };
  62. final mux = ZModemMux(
  63. stdin: session.stdin,
  64. stdout: session.stdout,
  65. );
  66. mux.onTerminalInput = terminal.write;
  67. mux.onFileOffer = _handleFileOffer;
  68. mux.onFileRequest = _handleFileRequest;
  69. terminal.onOutput = mux.terminalWrite;
  70. }
  71. void _handleFileOffer(ZModemOffer offer) async {
  72. print(offer.info);
  73. final outputDir = await FilePicker.platform.getDirectoryPath();
  74. if (outputDir == null) {
  75. offer.skip();
  76. return;
  77. }
  78. final file = File(path.join(outputDir, offer.info.pathname));
  79. void updateProgress(int received) {
  80. final length = offer.info.length;
  81. if (length != null) {
  82. terminal.write('\r');
  83. terminal.write('\x1b[K');
  84. terminal.write('${offer.info.pathname}: ');
  85. terminal.write((received / length * 100).toStringAsFixed(1));
  86. terminal.write('%');
  87. }
  88. }
  89. await offer
  90. .accept(0)
  91. .cast<List<int>>()
  92. .transform(WithProgress(onProgress: updateProgress))
  93. .pipe(file.openWrite());
  94. terminal.write('\r\n');
  95. terminal.write('Received ${offer.info.pathname}');
  96. }
  97. Future<Iterable<ZModemOffer>> _handleFileRequest() async {
  98. final result = await FilePicker.platform.pickFiles(withReadStream: true);
  99. if (result == null) {
  100. return [];
  101. }
  102. void updateProgress(PlatformFile file, int received) {
  103. terminal.write('\r');
  104. terminal.write('\x1b[K');
  105. terminal.write('${file.name}: ');
  106. terminal.write((received / file.size * 100).toStringAsFixed(1));
  107. terminal.write('%');
  108. }
  109. return result.files.map(
  110. (file) => ZModemCallbackOffer(
  111. ZModemFileInfo(
  112. pathname: path.basename(file.path!),
  113. length: file.size,
  114. mode: '100644',
  115. filesRemaining: 1,
  116. bytesRemaining: file.size,
  117. ),
  118. onAccept: (offset) => file.readStream!
  119. .skip(offset)
  120. .transform(
  121. WithProgress(onProgress: (bytes) => updateProgress(file, bytes)),
  122. )
  123. .cast<Uint8List>(),
  124. onSkip: () {
  125. terminal.write('\r\n');
  126. terminal.write('Rejected ${file.name}');
  127. },
  128. ),
  129. );
  130. }
  131. @override
  132. Widget build(BuildContext context) {
  133. return CupertinoPageScaffold(
  134. navigationBar: CupertinoNavigationBar(
  135. middle: Text(title),
  136. backgroundColor:
  137. CupertinoTheme.of(context).barBackgroundColor.withOpacity(0.5),
  138. ),
  139. child: TerminalView(terminal),
  140. );
  141. }
  142. }
  143. class WithProgress<T> extends StreamTransformerBase<List<T>, List<T>> {
  144. WithProgress({this.onProgress});
  145. void Function(int progress)? onProgress;
  146. var _progress = 0;
  147. int get progress => _progress;
  148. @override
  149. Stream<List<T>> bind(Stream<List<T>> stream) {
  150. return stream.transform(StreamTransformer<List<T>, List<T>>.fromHandlers(
  151. handleData: (List<T> data, EventSink<List<T>> sink) {
  152. _progress += data.length;
  153. onProgress?.call(_progress);
  154. sink.add(data);
  155. },
  156. ));
  157. }
  158. }