- SFTP download
- open downloaded files in other apps
This commit is contained in:
Junyuan Feng
2022-05-07 22:15:09 +08:00
parent 74a933eb6e
commit b824e06736
33 changed files with 1223 additions and 198 deletions

View File

@@ -0,0 +1,38 @@
class PathWithPrefix {
late String _prefixPath;
String _path = '/';
String? _prePath;
String get path => _prefixPath + _path;
PathWithPrefix(String prefixPath) {
if (prefixPath.endsWith('/')) {
_prefixPath = prefixPath.substring(0, prefixPath.length - 1);
} else {
_prefixPath = prefixPath;
}
}
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 || _path == _prePath) {
return false;
}
_path = _prePath!;
return true;
}
}

View File

@@ -20,7 +20,7 @@ class AbsolutePath {
}
bool undo() {
if (_prePath == null) {
if (_prePath == null || _prePath == path) {
return false;
}
path = _prePath!;

View File

@@ -2,7 +2,7 @@ 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 {
class SftpBrowserStatus {
bool selected = false;
ServerPrivateInfo? spi;
List<SftpName>? files;
@@ -10,5 +10,5 @@ class SFTPSideViewStatus {
SftpClient? client;
bool isBusy = false;
SFTPSideViewStatus();
SftpBrowserStatus();
}

View File

@@ -0,0 +1,55 @@
import 'package:toolbox/data/model/sftp/download_worker.dart';
class SftpDownloadStatus {
final int id;
final DownloadItem item;
final void Function() notifyListeners;
late SftpDownloadWorker worker;
String get fileName => item.localPath.split('/').last;
// status of the download
double? progress;
SftpWorkerStatus? status;
int? size;
Exception? error;
Duration? spentTime;
SftpDownloadStatus(this.item, this.notifyListeners, {String? key})
: id = DateTime.now().microsecondsSinceEpoch {
worker =
SftpDownloadWorker(onNotify: onNotify, item: item, privateKey: key);
worker.init();
}
@override
bool operator ==(Object other) =>
other is SftpDownloadStatus && id == other.id;
@override
int get hashCode => id ^ super.hashCode;
void onNotify(dynamic event) {
switch (event.runtimeType) {
case SftpWorkerStatus:
status = event;
break;
case double:
progress = event;
break;
case int:
size = event;
break;
case Exception:
error = event;
break;
case Duration:
spentTime = event;
break;
default:
}
notifyListeners();
}
}
enum SftpWorkerStatus { preparing, sshConnectted, downloading, finished }

View File

@@ -0,0 +1,107 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:dartssh2/dartssh2.dart';
import 'package:easy_isolate/easy_isolate.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/sftp/download_status.dart';
class DownloadItem {
DownloadItem(this.spi, this.remotePath, this.localPath);
final ServerPrivateInfo spi;
final String remotePath;
final String localPath;
}
class SftpDownloadWorker {
SftpDownloadWorker(
{required this.onNotify, required this.item, this.privateKey});
final Function(Object event) onNotify;
final DownloadItem item;
final worker = Worker();
final String? privateKey;
/// Initiate the worker (new thread) and start listen from messages between
/// the threads
Future<void> init() async {
if (worker.isInitialized) worker.dispose();
await worker.init(
mainMessageHandler,
isolateMessageHandler,
errorHandler: print,
);
worker.sendMessage(DownloadItemEvent(item, privateKey));
}
/// Handle the messages coming from the isolate
void mainMessageHandler(dynamic data, SendPort isolateSendPort) {
onNotify(data);
}
/// Handle the messages coming from the main
static isolateMessageHandler(
dynamic data, SendPort mainSendPort, SendErrorFunction sendError) async {
if (data is DownloadItemEvent) {
try {
mainSendPort.send(SftpWorkerStatus.preparing);
final watch = Stopwatch()..start();
final item = data.item;
final spi = item.spi;
final socket = await SSHSocket.connect(spi.ip, spi.port);
SSHClient client;
if (spi.pubKeyId == null) {
client = SSHClient(socket,
username: spi.user,
onPasswordRequest: () => spi.authorization as String);
} else {
client = SSHClient(socket,
username: spi.user,
identities: SSHKeyPair.fromPem(data.privateKey!));
}
mainSendPort.send(SftpWorkerStatus.sshConnectted);
final remotePath = item.remotePath;
final localPath = item.localPath;
await Directory(localPath.substring(0, item.localPath.lastIndexOf('/')))
.create(recursive: true);
final local = File(localPath);
if (await local.exists()) {
await local.delete();
}
final localFile = local.openWrite(mode: FileMode.append);
final file = await (await client.sftp()).open(remotePath);
final size = (await file.stat()).size;
if (size == null) {
mainSendPort.send(Exception('can not get file size'));
return;
}
const defaultChunkSize = 1024 * 512;
final chunkSize = size > defaultChunkSize ? defaultChunkSize : size;
mainSendPort.send(size);
mainSendPort.send(SftpWorkerStatus.downloading);
for (var i = 0; i < size; i += chunkSize) {
final fileData = file.read(length: chunkSize);
await for (var form in fileData) {
localFile.add(form);
mainSendPort.send((i + form.length) / size * 100);
}
}
mainSendPort.send(SftpWorkerStatus.finished);
localFile.close();
mainSendPort.send(watch.elapsed);
} catch (e) {
mainSendPort.send(e);
}
}
}
}
class DownloadItemEvent {
DownloadItemEvent(this.item, this.privateKey);
final DownloadItem item;
final String? privateKey;
}

View File

@@ -0,0 +1,31 @@
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/sftp/download_status.dart';
import 'package:toolbox/data/model/sftp/download_worker.dart';
class SftpDownloadProvider extends ProviderBase {
final List<SftpDownloadStatus> _status = [];
List<SftpDownloadStatus> get status => _status;
List<SftpDownloadStatus> gets({int? id, String? fileName}) {
var found = <SftpDownloadStatus>[];
if (id != null) {
found = _status.where((e) => e.id == id).toList();
}
if (fileName != null) {
found = found
.where((e) => e.item.localPath.split('/').last == fileName)
.toList();
}
return found;
}
SftpDownloadStatus? get({int? id, String? name}) {
final found = gets(id: id, fileName: name);
if (found.isEmpty) return null;
return found.first;
}
void add(DownloadItem item, {String? key}) {
_status.add(SftpDownloadStatus(item, notifyListeners, key: key));
}
}

View File

@@ -2,9 +2,9 @@
class BuildData {
static const String name = "ServerBox";
static const int build = 125;
static const int build = 126;
static const String engine =
"Flutter 2.10.5 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 5464c5bac7 (2 weeks ago) • 2022-04-18 09:55:37 -0700\nEngine • revision 57d3bac3dd\nTools • Dart 2.16.2 • DevTools 2.9.2\n";
static const String buildAt = "2022-05-05 16:11:07.575227";
static const int modifications = 2;
"Flutter 2.10.5 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 5464c5bac7 (3 weeks ago) • 2022-04-18 09:55:37 -0700\nEngine • revision 57d3bac3dd\nTools • Dart 2.16.2 • DevTools 2.9.2\n";
static const String buildAt = "2022-05-07 21:49:09.473227";
static const int modifications = 11;
}

View File

@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
const TextStyle size18 = TextStyle(fontSize: 18);
const TextStyle grey = TextStyle(color: Colors.grey);

10
lib/data/res/path.dart Normal file
View File

@@ -0,0 +1,10 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
Future<Directory> get docDir async => await getApplicationDocumentsDirectory();
Future<Directory> get sftpDownloadDir async {
final dir = Directory('${(await docDir).path}/sftp');
return dir.create(recursive: true);
}