isolaaaatesss and basic progress with it

This commit is contained in:
RafayAhmad7548 2025-07-31 11:22:12 +05:00
parent 1e0457ecd1
commit b3b2fc8895
7 changed files with 333 additions and 134 deletions

View file

@ -1,10 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:fluxcloud/connection.dart'; import 'package:fluxcloud/connection.dart';
import 'package:fluxcloud/sftp_explorer.dart'; import 'package:fluxcloud/sftp_explorer.dart';
import 'package:fluxcloud/sftp_worker.dart';
import 'package:fluxcloud/widgets/add_server_modal.dart'; import 'package:fluxcloud/widgets/add_server_modal.dart';
class SftpConnectionList extends StatefulWidget { class SftpConnectionList extends StatefulWidget {
@ -78,19 +78,9 @@ class _SftpConnectionListState extends State<SftpConnectionList> {
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: InkWell( child: InkWell(
onTap: () async { onTap: () async {
final conn = _connections[index]; final sftpWorker = await SftpWorker.spawn(_connections[index]);
final client = SSHClient(
await SSHSocket.connect(conn.host!, conn.port!),
username: conn.username!,
onPasswordRequest: () => conn.password,
identities: [
if (conn.privateKey != null)
...SSHKeyPair.fromPem(conn.privateKey!)
]
);
final sftpClient = await client.sftp();
if (context.mounted) { if (context.mounted) {
Navigator.push(context, MaterialPageRoute(builder: (context) => SftpExplorer(sftpClient: sftpClient,))); Navigator.push(context, MaterialPageRoute(builder: (context) => SftpExplorer(sftpWorker: sftpWorker,)));
} }
}, },
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),

View file

@ -1,11 +1,15 @@
import 'dart:io';
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:file_picker/file_picker.dart';
import 'package:file_selector/file_selector.dart'; import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluxcloud/sftp_worker.dart';
class SftpExplorer extends StatefulWidget { class SftpExplorer extends StatefulWidget {
const SftpExplorer({super.key, required this.sftpClient, this.path = '/'}); const SftpExplorer({super.key, required this.sftpWorker, this.path = '/'});
final SftpClient sftpClient; final SftpWorker sftpWorker;
final String path; final String path;
@override @override
@ -17,10 +21,6 @@ class _SftpExplorerState extends State<SftpExplorer> {
bool _isLoading = true; bool _isLoading = true;
late List<SftpName> _dirContents; late List<SftpName> _dirContents;
SftpFileWriter? _loader;
String _loadingFileName = '';
double _progress = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -29,7 +29,14 @@ class _SftpExplorerState extends State<SftpExplorer> {
Future<void> _listDir() async { Future<void> _listDir() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
_dirContents = await widget.sftpClient.listdir(widget.path); try {
_dirContents = await widget.sftpWorker.listdir(widget.path);
}
catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, e.toString()));
}
}
setState(() => _isLoading = false); setState(() => _isLoading = false);
} }
@ -40,7 +47,6 @@ class _SftpExplorerState extends State<SftpExplorer> {
appBar: AppBar( appBar: AppBar(
title: Text('Explorer'), title: Text('Explorer'),
), ),
bottomNavigationBar: _buildLoadingWidget(context),
floatingActionButton: _buildFABs(context), floatingActionButton: _buildFABs(context),
body: _isLoading ? Center(child: CircularProgressIndicator()) : ListView.builder( body: _isLoading ? Center(child: CircularProgressIndicator()) : ListView.builder(
itemCount: _dirContents.length, itemCount: _dirContents.length,
@ -82,19 +88,19 @@ class _SftpExplorerState extends State<SftpExplorer> {
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')), TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
try { // try {
await widget.sftpClient.rename('${widget.path}${dirEntry.filename}', '${widget.path}${newNameController.text}'); // await widget.sftpWorker.rename('${widget.path}${dirEntry.filename}', '${widget.path}${newNameController.text}');
_listDir(); // _listDir();
} // }
on SftpStatusError catch (e) { // on SftpStatusError catch (e) {
if (context.mounted) { // if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, e.message)); // ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, e.message));
} // }
} // }
if (context.mounted) { // if (context.mounted) {
Navigator.pop(context); // Navigator.pop(context);
} // }
//
}, },
child: Text('Rename') child: Text('Rename')
), ),
@ -116,30 +122,30 @@ class _SftpExplorerState extends State<SftpExplorer> {
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')), TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
if (dirEntry.attr.isDirectory) { // if (dirEntry.attr.isDirectory) {
Future<void> removeRecursively (String path) async { // Future<void> removeRecursively (String path) async {
final dirContents = await widget.sftpClient.listdir(path); // final dirContents = await widget.sftpWorker.listdir(path);
for (SftpName entry in dirContents) { // for (SftpName entry in dirContents) {
final fullPath = '$path${entry.filename}'; // final fullPath = '$path${entry.filename}';
if (entry.attr.isDirectory) { // if (entry.attr.isDirectory) {
await removeRecursively('$fullPath/'); // await removeRecursively('$fullPath/');
await widget.sftpClient.rmdir('$fullPath/'); // await widget.sftpWorker.rmdir('$fullPath/');
} // }
else { // else {
await widget.sftpClient.remove(fullPath); // await widget.sftpWorker.remove(fullPath);
} // }
} // }
await widget.sftpClient.rmdir(path); // await widget.sftpWorker.rmdir(path);
} // }
await removeRecursively('${widget.path}${dirEntry.filename}/'); // await removeRecursively('${widget.path}${dirEntry.filename}/');
} // }
else { // else {
await widget.sftpClient.remove('${widget.path}${dirEntry.filename}'); // await widget.sftpWorker.remove('${widget.path}${dirEntry.filename}');
} // }
_listDir(); // _listDir();
if (context.mounted) { // if (context.mounted) {
Navigator.pop(context); // Navigator.pop(context);
} // }
}, },
child: Text('Yes') child: Text('Yes')
), ),
@ -155,7 +161,7 @@ class _SftpExplorerState extends State<SftpExplorer> {
if (dirEntry.attr.isDirectory) { if (dirEntry.attr.isDirectory) {
Navigator.push(context, MaterialPageRoute( Navigator.push(context, MaterialPageRoute(
builder: (context) => SftpExplorer( builder: (context) => SftpExplorer(
sftpClient: widget.sftpClient, sftpWorker: widget.sftpWorker,
path: '${widget.path}${dirEntry.filename}/', path: '${widget.path}${dirEntry.filename}/',
) )
)); ));
@ -190,23 +196,23 @@ class _SftpExplorerState extends State<SftpExplorer> {
TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')), TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
try { // try {
await widget.sftpClient.mkdir('${widget.path}${nameController.text}'); // await widget.sftpWorker.mkdir('${widget.path}${nameController.text}');
_listDir(); // _listDir();
} // }
on SftpStatusError catch (e) { // on SftpStatusError catch (e) {
if (context.mounted) { // if (context.mounted) {
if (e.code == 4) { // if (e.code == 4) {
ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, 'Folder Already Exists')); // ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, 'Folder Already Exists'));
} // }
else { // else {
ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, 'Error: ${e.message}')); // ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, 'Error: ${e.message}'));
} // }
} // }
} // }
if (context.mounted) { // if (context.mounted) {
Navigator.pop(context); // Navigator.pop(context);
} // }
}, },
child: Text('Ok') child: Text('Ok')
), ),
@ -219,35 +225,17 @@ class _SftpExplorerState extends State<SftpExplorer> {
FloatingActionButton( FloatingActionButton(
heroTag: 'upload-file', heroTag: 'upload-file',
onPressed: () async { onPressed: () async {
// TODO: upload hangingig on android final List<String> filePaths;
final List<XFile> files = await openFiles(); if (Platform.isAndroid | Platform.isIOS) {
try { final res = await FilePicker.platform.pickFiles(allowMultiple: true);
for (XFile file in files) { filePaths = (res?.paths ?? []).whereType<String>().toList();
final remoteFile = await widget.sftpClient.open('${widget.path}${file.name}', mode: SftpFileOpenMode.create | SftpFileOpenMode.write | SftpFileOpenMode.exclusive);
final fileSize = await file.length();
final uploader = remoteFile.write(
file.openRead().cast(),
onProgress: (progress) => setState(() => _progress = progress/fileSize)
);
setState(() {
_loader = uploader;
_loadingFileName = file.name;
});
await uploader.done;
}
setState(() => _loader = null);
_listDir();
} }
on SftpStatusError catch (e) { else {
if (context.mounted) { final files = await openFiles();
if (e.code == 4) { filePaths = files.map((file) => file.path).toList();
ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, 'File Already Exists'));
}
else {
ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, 'Error: ${e.message}'));
}
}
} }
await widget.sftpWorker.uploadFiles(widget.path, filePaths);
_listDir();
}, },
child: Icon(Icons.upload), child: Icon(Icons.upload),
), ),
@ -268,34 +256,4 @@ class _SftpExplorerState extends State<SftpExplorer> {
) )
); );
} }
Widget _buildLoadingWidget(BuildContext context) {
return _loader != null ? Container(
color: Theme.of(context).colorScheme.secondaryContainer,
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
spacing: 10,
mainAxisSize: MainAxisSize.min,
children: [
Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Uploading file: $_loadingFileName', style: TextStyle(fontSize: 16),),
TextButton(
onPressed: () {
_loader!.abort();
widget.sftpClient.remove('${widget.path}$_loadingFileName');
},
child: Text('Cancel')
),
],
),
LinearProgressIndicator(value: _progress,)
],
),
),
) : SizedBox();
}
} }

148
lib/sftp_worker.dart Normal file
View file

@ -0,0 +1,148 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:dartssh2/dartssh2.dart';
import 'package:path/path.dart';
import 'connection.dart';
sealed class SftpCommand {}
class ListDir extends SftpCommand {
final String path;
ListDir(this.path);
}
class UploadFiles extends SftpCommand {
final String path;
final List<String> fileNames;
UploadFiles(this.path, this.fileNames);
}
class SftpWorker {
final ReceivePort _responses;
final SendPort _commands;
final Map<int, Completer<Object>> _activeRequests = {};
int _idCounter = 0;
SftpWorker._(this._responses, this._commands) {
_responses.listen(_sftpResponseHandler);
}
static Future<SftpWorker> spawn(Connection connection) async {
final initPort = RawReceivePort();
final workerReady = Completer<(ReceivePort, SendPort)>.sync();
initPort.handler = (message) {
final commandPort = message as SendPort;
workerReady.complete((
ReceivePort.fromRawReceivePort(initPort),
commandPort
));
};
try {
Isolate.spawn(_startSftpIsolate, (initPort.sendPort, connection));
} on Object {
initPort.close();
rethrow;
}
final (receivePort, sendPort) = await workerReady.future;
return SftpWorker._(receivePort, sendPort);
}
static void _startSftpIsolate((SendPort, Connection) args) async {
final sendPort = args.$1;
final receivePort = ReceivePort();
// TODO: error handling
final connection = args.$2;
final client = SSHClient(
await SSHSocket.connect(connection.host!, connection.port!),
username: connection.username!,
onPasswordRequest: () => connection.password,
identities: [
if (connection.privateKey != null)
...SSHKeyPair.fromPem(connection.privateKey!)
]
);
final sftpClient = await client.sftp();
sendPort.send(receivePort.sendPort);
_sftpCmdHandler(sendPort, receivePort, sftpClient);
}
static void _sftpCmdHandler(SendPort sendPort, ReceivePort receivePort, SftpClient sftpClient) {
receivePort.listen((message) async {
final (int id, dynamic command) = message;
switch (command) {
case ListDir(:final path):
try {
final files = await sftpClient.listdir(path);
sendPort.send((id, files));
}
on SftpStatusError catch (e) {
sendPort.send((id, RemoteError(e.message, '')));
}
case UploadFiles(:final path, fileNames:final filePaths):
for (var filePath in filePaths) {
try {
final file = File(filePath);
final fileSize = await file.length();
final remoteFile = await sftpClient.open(
'$path${basename(filePath)}',
mode: SftpFileOpenMode.create | SftpFileOpenMode.write | SftpFileOpenMode.exclusive
);
await remoteFile.write(
file.openRead().cast(),
onProgress: (progress) {
print(progress/fileSize);
}
);
}
on SftpStatusError catch (e) {
sendPort.send((id, RemoteError(e.message, '')));
}
}
sendPort.send((id, 0));
}
});
}
void _sftpResponseHandler(dynamic message) {
final (int id, Object response) = message;
final completer = _activeRequests.remove(id)!;
if (response is RemoteError) {
completer.completeError(response);
}
else {
completer.complete(response);
}
}
Future<List<SftpName>> listdir(String path) async {
final completer = Completer<Object>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
_commands.send((id, ListDir(path)));
return await completer.future as List<SftpName>;
}
Future<void> uploadFiles(String path, List<String> filePaths) async {
final completer = Completer<Object>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
_commands.send((id, UploadFiles(path, filePaths)));
await completer.future;
}
}

View file

@ -0,0 +1,83 @@
import 'package:dartssh2/dartssh2.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
class LoadingOverlay extends StatefulWidget{
const LoadingOverlay({
super.key, required this.sftpClient, required this.path, required this.files,
});
final SftpClient sftpClient;
final String path;
final List<XFile> files;
@override
State<LoadingOverlay> createState() => _LoadingOverlayState();
}
class _LoadingOverlayState extends State<LoadingOverlay> {
Future<void> uploadFiles() async {
for (final file in widget.files) {
final fileSize = await file.length();
final remoteFile = await widget.sftpClient.open(
'${widget.path}${file.name}',
mode: SftpFileOpenMode.create | SftpFileOpenMode.write | SftpFileOpenMode.exclusive
);
_loader = remoteFile.write(
file.openRead(),
onProgress: (progress) => setState(() => _progress = progress/fileSize)
);
await _loader?.done;
}
}
@override
void initState() {
super.initState();
uploadFiles();
_loadingFileName = widget.files[0].name;
}
SftpFileWriter? _loader;
late String _loadingFileName;
double _progress = 0;
@override
build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
color: Theme.of(context).colorScheme.secondaryContainer,
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
spacing: 10,
mainAxisSize: MainAxisSize.min,
children: [
Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Uploading file: $_loadingFileName', style: TextStyle(fontSize: 16),),
TextButton(
onPressed: () {
if (_loader != null) {
_loader!.abort();
widget.sftpClient.remove('${widget.path}$_loadingFileName');
}
},
child: Text('Cancel')
),
],
),
LinearProgressIndicator(value: _progress,)
],
),
),
),
);
}
}

View file

@ -5,11 +5,13 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import file_picker
import file_selector_macos import file_selector_macos
import flutter_secure_storage_macos import flutter_secure_storage_macos
import path_provider_foundation import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

View file

@ -89,6 +89,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a
url: "https://pub.dev"
source: hosted
version: "10.2.0"
file_selector: file_selector:
dependency: "direct main" dependency: "direct main"
description: description:
@ -166,6 +174,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
url: "https://pub.dev"
source: hosted
version: "2.0.28"
flutter_secure_storage: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -305,7 +321,7 @@ packages:
source: hosted source: hosted
version: "1.16.0" version: "1.16.0"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"

View file

@ -12,6 +12,8 @@ dependencies:
file_selector: ^1.0.3 file_selector: ^1.0.3
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4
dartssh2: ^2.13.0 dartssh2: ^2.13.0
path: ^1.9.1
file_picker: ^10.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: