feat(protocol): add FTP, FTPS and FTPES downloads (#1569)

This commit is contained in:
Silas Mariusz authored and GitHub committed 2026-10-30 17:01:46 +08:00
1 parent e0951de270
commit c0b70d319d
54 files changed
+3850 -38

No files matched your search

@@ -64,6 +64,10 @@ class ProtocolConfig {
Ed2kConfig ed2k = Ed2kConfig();
HlsConfig hls = HlsConfig();
/// A server without FTP support sends no `ftp` key; the defaults keep the rest of the config readable.
@JsonKey(fromJson: _ftpConfigFromJson)
FtpConfig ftp = FtpConfig();
ProtocolConfig();
factory ProtocolConfig.fromJson(Map<String, dynamic>? json) =>
@@ -91,6 +95,20 @@ class HlsConfig {
Map<String, dynamic> toJson() => _$HlsConfigToJson(this);
}
FtpConfig _ftpConfigFromJson(Map<String, dynamic>? json) => json == null ? FtpConfig() : FtpConfig.fromJson(json);
/// Global FTP defaults. It mirrors the `ftp` protocol config of the server.
@JsonSerializable()
class FtpConfig {
int connections;
FtpConfig({this.connections = 0});
factory FtpConfig.fromJson(Map<String, dynamic> json) => _$FtpConfigFromJson(json);
Map<String, dynamic> toJson() => _$FtpConfigToJson(this);
}
@JsonSerializable()
class HttpConfig {
String userAgent;
@@ -67,13 +67,15 @@ ProtocolConfig _$ProtocolConfigFromJson(Map<String, dynamic> json) => ProtocolCo
..http = HttpConfig.fromJson(json['http'] as Map<String, dynamic>)
..bt = BtConfig.fromJson(json['bt'] as Map<String, dynamic>)
..ed2k = Ed2kConfig.fromJson(json['ed2k'] as Map<String, dynamic>)
..hls = HlsConfig.fromJson(json['hls'] as Map<String, dynamic>);
..hls = HlsConfig.fromJson(json['hls'] as Map<String, dynamic>)
..ftp = _ftpConfigFromJson(json['ftp'] as Map<String, dynamic>?);
Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) => <String, dynamic>{
'http': instance.http.toJson(),
'bt': instance.bt.toJson(),
'ed2k': instance.ed2k.toJson(),
'hls': instance.hls.toJson(),
'ftp': instance.ftp.toJson(),
};
HlsConfig _$HlsConfigFromJson(Map<String, dynamic> json) => HlsConfig(
@@ -90,6 +92,11 @@ Map<String, dynamic> _$HlsConfigToJson(HlsConfig instance) => <String, dynamic>{
'prefetchContentLength': instance.prefetchContentLength,
};
FtpConfig _$FtpConfigFromJson(Map<String, dynamic> json) =>
FtpConfig(connections: (json['connections'] as num?)?.toInt() ?? 0);
Map<String, dynamic> _$FtpConfigToJson(FtpConfig instance) => <String, dynamic>{'connections': instance.connections};
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
userAgent: json['userAgent'] as String? ?? '',
connections: (json['connections'] as num?)?.toInt() ?? 0,
+25
View File
@@ -67,3 +67,28 @@ class OptsExtraHttp {
Map<String, dynamic> toJson() => _$OptsExtraHttpToJson(this);
}
/// Options for an FTP task. It mirrors `pkg/protocol/ftp.OptsExtra`. The TLS
/// mode of an `ftp://` link is on the request, see `ReqExtraFtp`.
@JsonSerializable()
class OptsExtraFtp {
int connections;
bool? autoTorrent;
bool? deleteTorrentAfterDownload;
bool? autoExtract;
String archivePassword;
bool deleteAfterExtract;
OptsExtraFtp({
this.connections = 0,
this.autoTorrent,
this.deleteTorrentAfterDownload,
this.autoExtract,
this.archivePassword = '',
this.deleteAfterExtract = false,
});
factory OptsExtraFtp.fromJson(Map<String, dynamic> json) => _$OptsExtraFtpFromJson(json);
Map<String, dynamic> toJson() => _$OptsExtraFtpToJson(this);
}
+18
View File
@@ -65,3 +65,21 @@ Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
'deleteAfterExtract': instance.deleteAfterExtract,
'checksum': ?instance.checksum?.toJson(),
};
OptsExtraFtp _$OptsExtraFtpFromJson(Map<String, dynamic> json) => OptsExtraFtp(
connections: (json['connections'] as num?)?.toInt() ?? 0,
autoTorrent: json['autoTorrent'] as bool?,
deleteTorrentAfterDownload: json['deleteTorrentAfterDownload'] as bool?,
autoExtract: json['autoExtract'] as bool?,
archivePassword: json['archivePassword'] as String? ?? '',
deleteAfterExtract: json['deleteAfterExtract'] as bool? ?? false,
);
Map<String, dynamic> _$OptsExtraFtpToJson(OptsExtraFtp instance) => <String, dynamic>{
'connections': instance.connections,
'autoTorrent': ?instance.autoTorrent,
'deleteTorrentAfterDownload': ?instance.deleteTorrentAfterDownload,
'autoExtract': ?instance.autoExtract,
'archivePassword': instance.archivePassword,
'deleteAfterExtract': instance.deleteAfterExtract,
};
+14
View File
@@ -42,6 +42,20 @@ class ReqExtraBt {
Map<String, dynamic> toJson() => _$ReqExtraBtToJson(this);
}
/// The request extra of an FTP task. It mirrors `pkg/protocol/ftp.ReqExtra`;
/// [tls] only applies to `ftp://` URLs, because `ftps://` always uses
/// implicit TLS and `ftpes://` always uses explicit TLS.
@JsonSerializable()
class ReqExtraFtp {
String tls;
ReqExtraFtp({this.tls = 'none'});
factory ReqExtraFtp.fromJson(Map<String, dynamic> json) => _$ReqExtraFtpFromJson(json);
Map<String, dynamic> toJson() => _$ReqExtraFtpToJson(this);
}
enum RequestProxyMode { follow, none, custom }
@JsonSerializable()
+4
View File
@@ -41,6 +41,10 @@ ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) => <String, dynamic>{'trackers': instance.trackers};
ReqExtraFtp _$ReqExtraFtpFromJson(Map<String, dynamic> json) => ReqExtraFtp(tls: json['tls'] as String? ?? 'none');
Map<String, dynamic> _$ReqExtraFtpToJson(ReqExtraFtp instance) => <String, dynamic>{'tls': instance.tls};
RequestProxy _$RequestProxyFromJson(Map<String, dynamic> json) => RequestProxy(
mode: $enumDecodeNullable(_$RequestProxyModeEnumMap, json['mode']) ?? RequestProxyMode.follow,
scheme: json['scheme'] as String? ?? 'http',
+1 -1
View File
@@ -6,7 +6,7 @@ part 'task.g.dart';
enum Status { ready, running, pause, wait, error, done }
enum Protocol { http, bt, ed2k, hls }
enum Protocol { http, bt, ed2k, hls, ftp }
// ExtractStatus enum matching Go backend
enum ExtractStatus {
+7 -1
View File
@@ -40,7 +40,13 @@ const _$StatusEnumMap = {
Status.done: 'done',
};
const _$ProtocolEnumMap = {Protocol.http: 'http', Protocol.bt: 'bt', Protocol.ed2k: 'ed2k', Protocol.hls: 'hls'};
const _$ProtocolEnumMap = {
Protocol.http: 'http',
Protocol.bt: 'bt',
Protocol.ed2k: 'ed2k',
Protocol.hls: 'hls',
Protocol.ftp: 'ftp',
};
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
used: (json['used'] as num).toInt(),
+2 -1
View File
@@ -52,7 +52,8 @@ sealed class TaskStats {
final runtime = _map(json['runtime']);
if (snapshot.isEmpty && runtime.isEmpty) return null;
return switch (protocol) {
Protocol.http => HttpTaskStats.fromJson(snapshot),
// FTP stats have the same shape as HTTP stats: one entry per connection.
Protocol.http || Protocol.ftp => HttpTaskStats.fromJson(snapshot),
Protocol.bt => BtTaskStats.fromJson(snapshot: snapshot, runtime: runtime),
Protocol.ed2k => Ed2kTaskStats.fromJson(snapshot: snapshot, runtime: runtime),
Protocol.hls || null => null,