import 'dart:io'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:gallery/data_provider/data_provider.dart'; import 'dart:ui' as ui show Codec, ImmutableBuffer; import 'package:path_provider/path_provider.dart'; class SSHDataProvider extends DataProvider { final String host; final int port; final String username; final String password; final String initialPath; SftpClient? sftpClient; SSHClient? sshClient; SSHDataProvider( {required this.host, required this.port, required this.initialPath, required this.password, required this.username}); @override Future connect() async { if (sshClient != null && !sshClient!.isClosed) return; sshClient = SSHClient( await SSHSocket.connect(host, port), username: username, onPasswordRequest: () => password, ); sftpClient = await sshClient?.sftp(); } @override Future listOfFiles({Uri? uri}) async { await connect(); if (sftpClient == null) throw const FormatException(""); final dir = uri != null ? Directory.fromUri(uri) : Directory(initialPath); final items = await sftpClient!.listdir(dir.path); List res = []; for (final val in items) { if (val.filename == "." || val.filename == "..") continue; if (validSuffix.any((suff) => val.filename.endsWith(suff))) { res.add(Item(false, Uri.file(dir.path + val.filename), val.filename)); } else if (val.attr.isDirectory) { res.add( Item(true, Uri.directory(dir.path + val.filename), val.filename)); } } res.sort( (a, b) => b.isFolder ? 1 : -1, ); return Folder(res, dir.uri, dir.parent.uri); } @override ImageProvider getImageProvider(Uri uri) { return _SSHImageProvider(uri, sftpClient!); } } class _SSHImageProvider extends ImageProvider<_SSHImageProvider> { const _SSHImageProvider(this.uri, this.sftpClient, {this.scale = 1.0}); final Uri uri; final SftpClient sftpClient; /// The scale to place in the [ImageInfo] object of the image. final double scale; @override Future<_SSHImageProvider> obtainKey(ImageConfiguration configuration) { return SynchronousFuture<_SSHImageProvider>(this); } @override ImageStreamCompleter load(_SSHImageProvider key, DecoderCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, null, decode), scale: key.scale, debugLabel: key.uri.path, informationCollector: () => [ ErrorDescription('Path: ${uri.path}'), ], ); } @override ImageStreamCompleter loadBuffer( _SSHImageProvider key, DecoderBufferCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode, null), scale: key.scale, debugLabel: key.uri.path, informationCollector: () => [ ErrorDescription('Path: ${uri.path}'), ], ); } Future _loadAsync(_SSHImageProvider key, DecoderBufferCallback? decode, DecoderCallback? decodeDeprecated) async { assert(key == this); Directory tempDir = await getTemporaryDirectory(); String tempPath = "${tempDir.path}/gallery"; // check if temp file exists Uint8List bytes; final File tmpPic = File(tempPath + uri.toFilePath()); if (await tmpPic.exists()) { // use temp file bytes = await tmpPic.readAsBytes(); } else { final file = await sftpClient.open(uri.toFilePath()); // todo do not load whole image in ram, create tempfile instead. bytes = await file.readBytes(); await tmpPic.create(recursive: true); await tmpPic.writeAsBytes(bytes); } if (bytes.lengthInBytes == 0) { // The file may become available later. PaintingBinding.instance.imageCache.evict(key); throw StateError( 'bytes are empty is empty and cannot be loaded as an image.'); } if (decode != null) { return decode(await ui.ImmutableBuffer.fromUint8List(bytes)); } return decodeDeprecated!(bytes); } @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } return other is _SSHImageProvider && other.uri.path == uri.path && other.scale == scale; } @override int get hashCode => Object.hash(uri.path, scale); @override String toString() => '${objectRuntimeType(this, 'FileImage')}("${uri.path}", scale: $scale)'; }