diff --git a/lib/core/remote_stream.dart b/lib/core/remote_stream.dart index 2fcba34..59180ae 100644 --- a/lib/core/remote_stream.dart +++ b/lib/core/remote_stream.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:path/path.dart' as p; +import 'ftp.dart' show FtpRemote; import 'sftp.dart' show SftpRemote; import 'smb.dart' show SmbRemote; import 'vfs.dart'; @@ -61,6 +62,8 @@ class RemoteStreamBridge { final client = d.client; if (client is SftpRemote) return client.openReadFrom(d.rel, offset: start, length: length); if (client is SmbRemote) return client.openReadFrom(d.rel, start: start, length: length); + // l FTP: REST 로 그 자리부터 (15 가 알린 것 - 합친 뒤 넣음) + if (client is FtpRemote) return client.openReadFrom(d.rel, offset: start, length: length); // 그 밖 (WebDAV 는 다리를 쓰지 않지만 혹시 오면): 처음부터 읽으며 건너뛴다 return _skip(await client.openRead(d.rel), start, length); } diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index dac93a8..0cdf633 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -794,6 +794,15 @@ class _ExplorerPageState extends State { final base = s.root.isEmpty ? '/~' : (s.root.startsWith('/') ? s.root : '/${s.root}'); return 'sftp://${s.user.isEmpty ? '' : '${s.user}@'}${s.host}${s.port == 22 ? '' : ':${s.port}'}$base${d.isRoot ? '' : d.rel}'; } + // l: FTP 는 ftp://아이디@서버:포트/경로 (암호화 990 은 ftps://, 비밀번호 없음) + if (d.isFtp) { + final s = FtpRegistry.server(d.server); + if (s == null) return _displayPath(path); + final implicit = s.security == 'implicit'; + final r = s.root.trim().replaceAll(RegExp(r'^/+|/+$'), ''); + return '${implicit ? 'ftps' : 'ftp'}://${s.user.isEmpty ? '' : '${Uri.encodeComponent(s.user)}@'}${s.host}' + '${s.port == (implicit ? 990 : 21) ? '' : ':${s.port}'}${r.isEmpty ? '' : '/$r'}${d.isRoot ? '' : d.rel}'; + } // 88: SMB 는 smb://서버/공유/경로 (아이디 · 비밀번호 없음) if (d.isSmb) { final s = SmbRegistry.server(d.server); diff --git a/test/ftp_test.dart b/test/ftp_test.dart index 5ef20c7..23dcaad 100644 --- a/test/ftp_test.dart +++ b/test/ftp_test.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/core/ftp.dart'; +import 'package:jj_mkvmaker/core/remote_stream.dart'; import 'package:jj_mkvmaker/services/secret_store.dart'; import 'package:path/path.dart' as p; @@ -142,6 +143,16 @@ void main() { expect(bad, isA().having((x) => x.changed, 'changed', isTrue)); }, skip: hasPy ? false : '시험용 파이썬 없음'); + test('동영상 스트리밍 다리: ftp:// 경로의 부분 읽기 (FtpRemote.openReadFrom - REST 로 그 자리부터)', () async { + await start('plain'); + FtpRegistry.configure([srv(security: 'none')]); + await FtpRegistry.client('f1').write('/v.mkv', Stream.value(utf8.encode('0123456789'))); + Future range(int start, int? length) async => + utf8.decode(await (await RemoteStreamBridge.openRange('ftp://f1/v.mkv', start, length)).expand((x) => x).toList()); + expect(await range(4, 3), '456'); + expect(await range(7, null), '789'); + }, skip: hasPy ? false : '시험용 파이썬 없음'); + test('FTPS (암시, 990 꼴)', () async { await start('implicit'); final c = FtpRemote(srv(security: 'implicit', cert: fp));