This commit is contained in:
kodjomoustapha 2024-05-14 12:25:51 +01:00
parent 281e621da3
commit e2c79c829b
12 changed files with 468 additions and 48 deletions

View file

@ -3,19 +3,17 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_anime.dart';
import 'database.dart';
import 'exceptions.dart';
import 'models.dart';
import 'persistent_storage.dart';
import 'queue/task_queue.dart';
import 'task.dart';
import 'desktop/desktop_downloader.dart';
/// Common download functionality
///
@ -94,7 +92,8 @@ abstract base class BaseDownloader {
final canResumeTask = <Task, Completer<bool>>{};
/// Flag indicating we have retrieved missed data
var _retrievedLocallyStoredData = false;
@visibleForTesting
var retrievedLocallyStoredData = false;
/// Connected TaskQueues that will receive a signal upon task completion
final taskQueues = <TaskQueue>[];
@ -171,7 +170,7 @@ abstract base class BaseDownloader {
/// Retrieve data that was stored locally because it could not be
/// delivered to the downloader
Future<void> retrieveLocallyStoredData() async {
if (!_retrievedLocallyStoredData) {
if (!retrievedLocallyStoredData) {
final resumeDataMap = await popUndeliveredData(Undelivered.resumeData);
for (var jsonString in resumeDataMap.values) {
final resumeData = ResumeData.fromJsonString(jsonString);
@ -188,7 +187,7 @@ abstract base class BaseDownloader {
for (var jsonString in progressUpdateMap.values) {
processProgressUpdate(TaskProgressUpdate.fromJsonString(jsonString));
}
_retrievedLocallyStoredData = true;
retrievedLocallyStoredData = true;
}
}
@ -196,7 +195,7 @@ abstract base class BaseDownloader {
///
/// Matches on task, then on group, then on default
TaskNotificationConfig? notificationConfigForTask(Task task) {
if (task.group == chunkGroup) {
if (task.group == chunkGroup || task is DataTask) {
return null;
}
return notificationConfigs

View file

@ -0,0 +1,53 @@
import 'dart:async';
import 'dart:isolate';
import 'package:http/http.dart' as http;
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
import 'desktop_downloader.dart';
import 'download_isolate.dart';
import 'isolate.dart';
/// Do the data task
///
/// Sends updates via the [sendPort] and can be commanded to cancel via
/// the [messagesToIsolate] queue
Future<void> doDataTask(DataTask task, SendPort sendPort) async {
final client = DesktopDownloader.httpClient;
var request = http.Request(task.httpRequestMethod, Uri.parse(task.url));
request.headers.addAll(task.headers);
if (task.post is String) {
request.body = task.post!;
}
var resultStatus = TaskStatus.failed;
try {
final response = await client.send(request);
if (!isCanceled) {
responseHeaders = response.headers;
responseStatusCode = response.statusCode;
extractContentType(response.headers);
responseBody = await responseContent(response);
if (okResponses.contains(response.statusCode)) {
resultStatus = TaskStatus.complete;
} else {
if (response.statusCode == 404) {
resultStatus = TaskStatus.notFound;
} else {
taskException = TaskHttpException(
responseBody?.isNotEmpty == true
? responseBody!
: response.reasonPhrase ?? 'Invalid HTTP Request',
response.statusCode);
}
}
}
} catch (e) {
logError(task, e.toString());
setTaskError(e);
}
if (isCanceled) {
// cancellation overrides other results
resultStatus = TaskStatus.canceled;
}
processStatusUpdateInIsolate(task, resultStatus, sendPort);
}

View file

@ -4,7 +4,6 @@ import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:isolate';
import 'package:async/async.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
@ -14,7 +13,6 @@ import 'package:logging/logging.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import '../base_downloader.dart';
import '../chunk.dart';
import '../exceptions.dart';
@ -34,7 +32,7 @@ const okResponses = [200, 201, 202, 203, 204, 205, 206];
final class DesktopDownloader extends BaseDownloader {
static final _log = Logger('DesktopDownloader');
static const unlimited = 1 << 20;
var maxConcurrent = 20;
var maxConcurrent = 10;
var maxConcurrentByHost = unlimited;
var maxConcurrentByGroup = unlimited;
static final DesktopDownloader _singleton = DesktopDownloader._internal();
@ -565,12 +563,12 @@ final class DesktopDownloader extends BaseDownloader {
///
/// This is a convenience method, bundling the [requestTimeout],
/// [proxy] and [bypassTLSCertificateValidation]
static Future<void> setHttpClient(Duration? requestTimeout,
Map<String, dynamic> proxy, bool bypassTLSCertificateValidation) async {
static void setHttpClient(Duration? requestTimeout,
Map<String, dynamic> proxy, bool bypassTLSCertificateValidation) {
_requestTimeout = requestTimeout;
_proxy = proxy;
_bypassTLSCertificateValidation = bypassTLSCertificateValidation;
await _recreateClient();
_recreateClient();
}
/// Recreates the [httpClient] used for Requests and isolate downloads/uploads

View file

@ -4,7 +4,6 @@ import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:isolate';
import 'package:async/async.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
@ -13,7 +12,6 @@ import 'package:logging/logging.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import '../base_downloader.dart';
import '../chunk.dart';
import '../exceptions.dart';
@ -33,11 +31,10 @@ const okResponses = [200, 201, 202, 203, 204, 205, 206];
final class DesktopDownloaderAnime extends BaseDownloader {
static final _log = Logger('DesktopDownloaderAnime');
static const unlimited = 1 << 20;
var maxConcurrent = 20;
var maxConcurrent = 10;
var maxConcurrentByHost = unlimited;
var maxConcurrentByGroup = unlimited;
static final DesktopDownloaderAnime _singleton =
DesktopDownloaderAnime._internal();
static final DesktopDownloaderAnime _singleton = DesktopDownloaderAnime._internal();
final _queue = PriorityQueue<Task>();
final _running = Queue<Task>(); // subset that is running
final _resume = <Task>{};
@ -565,12 +562,12 @@ final class DesktopDownloaderAnime extends BaseDownloader {
///
/// This is a convenience method, bundling the [requestTimeout],
/// [proxy] and [bypassTLSCertificateValidation]
static Future<void> setHttpClient(Duration? requestTimeout,
Map<String, dynamic> proxy, bool bypassTLSCertificateValidation) async {
static void setHttpClient(Duration? requestTimeout,
Map<String, dynamic> proxy, bool bypassTLSCertificateValidation) {
_requestTimeout = requestTimeout;
_proxy = proxy;
_bypassTLSCertificateValidation = bypassTLSCertificateValidation;
await _recreateClient();
_recreateClient();
}
/// Recreates the [httpClient] used for Requests and isolate downloads/uploads

View file

@ -7,12 +7,15 @@ import 'dart:isolate';
import 'dart:math';
import 'package:async/async.dart';
import 'package:mangayomi/services/background_downloader/src/exceptions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
import '../models.dart';
import '../task.dart';
import 'data_isolate.dart';
import 'desktop_downloader.dart';
import 'download_isolate.dart';
import 'parallel_download_isolate.dart';
@ -62,7 +65,7 @@ Future<void> doTask((RootIsolateToken, SendPort) isolateArguments) async {
bool isResume,
Duration? requestTimeout,
Map<String, dynamic> proxy,
bool bypassTLSCertificateValidation,
bool bypassTLSCertificateValidation
) = await messagesToIsolate.next;
DesktopDownloader.setHttpClient(
requestTimeout, proxy, bypassTLSCertificateValidation);
@ -95,7 +98,8 @@ Future<void> doTask((RootIsolateToken, SendPort) isolateArguments) async {
sendPort),
DownloadTask() => doDownloadTask(task, filePath, resumeData, isResume,
requestTimeout ?? const Duration(seconds: 60), sendPort),
UploadTask() => doUploadTask(task, filePath, sendPort)
UploadTask() => doUploadTask(task, filePath, sendPort),
DataTask() => doDataTask(task, sendPort)
};
}
receivePort.close();

View file

@ -5,8 +5,8 @@ import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:http/http.dart' as http;
import 'package:mangayomi/services/background_downloader/src/exceptions.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as p;
import '../models.dart';
@ -43,7 +43,6 @@ Future<TaskStatus> binaryUpload(
return TaskStatus.failed;
}
final fileSize = inFile.lengthSync();
// determine the content length of the multi-part data
var resultStatus = TaskStatus.failed;
try {
final client = DesktopDownloader.httpClient;
@ -52,6 +51,8 @@ Future<TaskStatus> binaryUpload(
request.headers.addAll(task.headers);
request.contentLength = fileSize;
request.headers['Content-Type'] = task.mimeType;
request.headers['Content-Disposition'] =
'attachment; filename="${task.filename}"';
// initiate the request and handle completion async
final requestCompleter = Completer();
var transferBytesResult = TaskStatus.failed;

View file

@ -1,20 +1,17 @@
// ignore_for_file: depend_on_referenced_packages
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
import 'base_downloader.dart';
import 'localstore/localstore.dart';
import 'desktop/desktop_downloader.dart';
/// Provides access to all functions of the plugin in a single place.
interface class FileDownloader {
static FileDownloader? _singleton;
/// If no group is specified the default group name will be used
static const defaultGroup = 'default';
@ -36,8 +33,13 @@ interface class FileDownloader {
factory FileDownloader(
{PersistentStorage? persistentStorage, bool isAnime = false}) {
return FileDownloader._internal(
assert(
_singleton == null || persistentStorage == null,
'You can only supply a persistentStorage on the very first call to '
'FileDownloader()');
_singleton ??= FileDownloader._internal(
persistentStorage ?? LocalStorePersistentStorage(), isAnime);
return _singleton!;
}
FileDownloader._internal(PersistentStorage persistentStorage, bool isAnime) {
@ -271,6 +273,32 @@ interface class FileDownloader {
onElapsedTime: onElapsedTime,
elapsedTimeInterval: elapsedTimeInterval);
/// Transmit data in the [DataTask] and receive the response
///
/// Different from [enqueue], this method returns a [Future] that completes
/// when the [DataTask] has completed, or an error has occurred.
/// While it uses the same mechanism as [enqueue],
/// and will execute the task also when
/// the app moves to the background, it is meant for data tasks that are
/// awaited while the app is in the foreground.
///
/// [onStatus] is an optional callback for status updates
///
/// An optional callback [onElapsedTime] will be called at regular intervals
/// (defined by [elapsedTimeInterval], which defaults to 5 seconds) with a
/// single argument that is the elapsed time since the call to [transmit].
/// This can be used to trigger UI warnings (e.g. 'this is taking rather long')
/// For performance reasons the [elapsedTimeInterval] should not be set to
/// a value less than one second.
Future<TaskStatusUpdate> transmit(DataTask task,
{void Function(TaskStatus)? onStatus,
void Function(Duration)? onElapsedTime,
Duration? elapsedTimeInterval}) =>
_downloader.enqueueAndAwait(task,
onStatus: onStatus,
onElapsedTime: onElapsedTime,
elapsedTimeInterval: elapsedTimeInterval);
/// Enqueues a list of files to download and returns when all downloads
/// have finished (successfully or otherwise). The returned value is a
/// [Batch] object that contains the original [tasks], the
@ -864,13 +892,7 @@ Future<http.Response> _doRequest(
(Request, Duration?, Map<String, dynamic>, bool) params) async {
final (request, requestTimeout, proxy, bypassTLSCertificateValidation) =
params;
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((LogRecord rec) {
if (kDebugMode) {
print('${rec.loggerName}>${rec.level.name}: ${rec.time}: ${rec.message}');
}
});
final log = Logger('FileDownloader.request');
DesktopDownloader.setHttpClient(
requestTimeout, proxy, bypassTLSCertificateValidation);
final client = DesktopDownloader.httpClient;
@ -895,7 +917,6 @@ Future<http.Response> _doRequest(
return response;
}
} catch (e) {
log.warning(e);
response = http.Response('', 499, reasonPhrase: e.toString());
}
// error, retry if allowed

View file

@ -9,7 +9,7 @@ import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'dart:math';
import 'utils/io.dart';
import 'utils/html.dart' if (dart.library.io) 'utils/io.dart';
part 'collection_ref.dart';
part 'collection_ref_impl.dart';

View file

@ -0,0 +1,223 @@
import 'dart:async';
import 'dart:convert';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
import 'utils_impl.dart';
/// Utils class
class Utils implements UtilsImpl {
Utils._();
static final Utils _utils = Utils._();
static Utils get instance => _utils;
@override
void clearCache() {
// no cache on web
}
@override
Future<Map<String, dynamic>?> get(String path,
[bool? isCollection = false, List<List>? conditions]) async {
// Fetch the documents for this collection
if (isCollection != null && isCollection == true) {
var dataCol = html.window.localStorage.entries.singleWhere(
(e) => e.key == path,
orElse: () => const MapEntry('', ''),
);
if (dataCol.key != '') {
if (conditions != null && conditions.first.isNotEmpty) {
return _getAll(dataCol);
/*
final ck = conditions.first[0] as String;
final co = conditions.first[1];
final cv = conditions.first[2];
// With conditions
try {
final mapCol = json.decode(dataCol.value) as Map<String, dynamic>;
final its = SplayTreeMap.of(mapCol);
its.removeWhere((key, value) {
if (value is Map<String, dynamic>) {
final key = value.keys.contains(ck);
final check = value[ck] as bool;
return !(key == true && check == cv);
}
return false;
});
its.forEach((key, value) {
final data = value as Map<String, dynamic>;
_data[key] = data;
});
return _data;
} catch (error) {
throw error;
}
*/
} else {
return _getAll(dataCol);
}
}
} else {
final data = await _readFromStorage(path);
final id = path.substring(path.lastIndexOf('/') + 1, path.length);
if (data is Map<String, dynamic>) {
if (data.containsKey(id)) return data[id];
return null;
}
}
return null;
}
@override
Future<dynamic>? set(Map<String, dynamic> data, String path) {
return _writeToStorage(data, path);
}
@override
Future delete(String path) async {
_deleteFromStorage(path);
}
@override
Stream<Map<String, dynamic>> stream(String path, [List<List>? conditions]) {
// ignore: close_sinks
final storage = _storageCache[path] ??
_storageCache.putIfAbsent(
path, () => StreamController<Map<String, dynamic>>.broadcast());
_initStream(storage, path);
return storage.stream;
}
Map<String, dynamic>? _getAll(MapEntry<String, String> dataCol) {
final items = <String, dynamic>{};
try {
final mapCol = json.decode(dataCol.value) as Map<String, dynamic>;
mapCol.forEach((key, value) {
final data = value as Map<String, dynamic>;
items[key] = data;
});
if (items.isEmpty) return null;
return items;
} catch (error) {
rethrow;
}
}
void _initStream(
StreamController<Map<String, dynamic>> storage, String path) {
var dataCol = html.window.localStorage.entries.singleWhere(
(e) => e.key == path,
orElse: () => const MapEntry('', ''),
);
try {
if (dataCol.key != '') {
final mapCol = json.decode(dataCol.value) as Map<String, dynamic>;
mapCol.forEach((key, value) {
final data = value as Map<String, dynamic>;
storage.add(data);
});
}
} catch (error) {
rethrow;
}
}
final _storageCache = <String, StreamController<Map<String, dynamic>>>{};
Future<dynamic> _readFromStorage(String path) async {
final key = path.replaceAll(RegExp(r'[^\/]+\/?$'), '');
final data = html.window.localStorage.entries.firstWhere(
(i) => i.key == key,
orElse: () => const MapEntry('', ''),
);
if (data != const MapEntry('', '')) {
try {
return json.decode(data.value) as Map<String, dynamic>;
} catch (e) {
return e;
}
}
}
Future<dynamic> _writeToStorage(
Map<String, dynamic> data,
String path,
) async {
final key = path.replaceAll(RegExp(r'[^\/]+\/?$'), '');
final uri = Uri.parse(path);
final id = uri.pathSegments.last;
var dataCol = html.window.localStorage.entries.singleWhere(
(e) => e.key == key,
orElse: () => const MapEntry('', ''),
);
try {
if (dataCol.key != '') {
final mapCol = json.decode(dataCol.value) as Map<String, dynamic>;
mapCol[id] = data;
dataCol = MapEntry(id, json.encode(mapCol));
html.window.localStorage.update(
key,
(value) => dataCol.value,
ifAbsent: () => dataCol.value,
);
} else {
html.window.localStorage.update(
key,
(value) => json.encode({id: data}),
ifAbsent: () => json.encode({id: data}),
);
}
// ignore: close_sinks
final storage = _storageCache[key] ??
_storageCache.putIfAbsent(
key, () => StreamController<Map<String, dynamic>>.broadcast());
storage.sink.add(data);
} catch (error) {
rethrow;
}
}
Future<dynamic> _deleteFromStorage(String path) async {
if (path.endsWith('/')) {
// If path is a directory path
final dataCol = html.window.localStorage.entries.singleWhere(
(element) => element.key == path,
orElse: () => const MapEntry('', ''),
);
try {
if (dataCol.key != '') {
html.window.localStorage.remove(dataCol.key);
}
} catch (error) {
rethrow;
}
} else {
// If path is a file path
final uri = Uri.parse(path);
final key = path.replaceAll(RegExp(r'[^\/]+\/?$'), '');
final id = uri.pathSegments.last;
var dataCol = html.window.localStorage.entries.singleWhere(
(e) => e.key == key,
orElse: () => const MapEntry('', ''),
);
try {
if (dataCol.key != '') {
final mapCol = json.decode(dataCol.value) as Map<String, dynamic>;
mapCol.remove(id);
html.window.localStorage.update(
key,
(value) => json.encode(mapCol),
ifAbsent: () => dataCol.value,
);
}
} catch (error) {
rethrow;
}
}
}
}

View file

@ -197,7 +197,7 @@ sealed class TaskUpdate {
/// [responseHeaders], [responseStatusCode], [mimeType] and [charSet].
/// Note: header names in [responseHeaders] are converted to lowercase
class TaskStatusUpdate extends TaskUpdate {
final TaskStatus status;
final TaskStatus status; // note: serialized as 'taskStatus'
final TaskException? exception;
final String? responseBody;
final int? responseStatusCode;
@ -220,8 +220,10 @@ class TaskStatusUpdate extends TaskUpdate {
? TaskException.fromJson(json['exception'])
: null,
responseBody = json['responseBody'],
responseHeaders = json['responseHeaders'],
responseStatusCode = json['responseStatusCode'],
responseHeaders = json['responseHeaders'] != null
? Map.from(json['responseHeaders'])
: null,
responseStatusCode = (json['responseStatusCode'] as num?)?.toInt(),
mimeType = json['mimeType'],
charSet = json['charSet'],
super.fromJson();

View file

@ -3,6 +3,7 @@
import 'dart:async';
import 'dart:io';
import 'package:mangayomi/services/background_downloader/src/base_downloader.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';

View file

@ -8,6 +8,7 @@ import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
@ -15,7 +16,6 @@ import 'package:path_provider/path_provider.dart';
import 'file_downloader.dart';
import 'models.dart';
import 'utils.dart';
import 'desktop/desktop_downloader.dart';
final _log = Logger('FileDownloader');
@ -67,12 +67,13 @@ base class Request {
Request(
{required String url,
Map<String, String>? urlQueryParameters,
this.headers = const {},
Map<String, String>? headers,
String? httpRequestMethod,
post,
this.retries = 0,
DateTime? creationTime})
: url = urlWithQueryParameters(url, urlQueryParameters),
headers = headers ?? {},
httpRequestMethod =
httpRequestMethod?.toUpperCase() ?? (post == null ? 'GET' : 'POST'),
post = post is Uint8List ? String.fromCharCodes(post) : post,
@ -319,8 +320,9 @@ sealed class Task extends Request implements Comparable {
'UploadTask' => UploadTask.fromJson(json),
'MultiUploadTask' => MultiUploadTask.fromJson(json),
'ParallelDownloadTask' => ParallelDownloadTask.fromJson(json),
'DataTask' => DataTask.fromJson(json),
_ => throw ArgumentError(
'taskType not in [DownloadTask, UploadTask, MultiUploadTask, ParallelDownloadTask]')
'taskType not in [DownloadTask, UploadTask, MultiUploadTask, ParallelDownloadTask, DataTask]')
};
/// Create a new [Task] subclass from provided [jsonString]
@ -1227,3 +1229,122 @@ final class ParallelDownloadTask extends DownloadTask {
creationTime: creationTime ?? this.creationTime)
..retriesRemaining = retriesRemaining ?? this.retriesRemaining;
}
/// Class for background requests that do not involve a file
///
/// Closely resembles a Task, with fewer fields available during construction
final class DataTask extends Task {
/// Creates a [DataTask] that runs in the background, but does not involve a
/// file
///
/// [taskId] must be unique. A unique id will be generated if omitted
/// [url] properly encoded if necessary, can include query parameters
/// [urlQueryParameters] may be added and will be appended to the [url], must
/// be properly encoded if necessary
/// [headers] an optional map of HTTP request headers
/// [httpRequestMethod] the HTTP request method used (e.g. GET, POST)
/// [post] String post body, encoded in utf8
/// [json] if given will encode [json] to string and use as the [post] data
/// [contentType] sets the Content-Type header to this value. If omitted and
/// [post] is given, it will be set to 'text-plain; charset=utf-8' and if
/// [json] is given, it will be set to 'application/json]
/// [group] if set allows different callbacks or processing for different
/// groups
/// [updates] the kind of progress updates requested (only .status or none)
/// [requiresWiFi] if set, will not start download until WiFi is available.
/// If not set may start download over cellular network
/// [retries] if >0 will retry a failed download this many times
/// [priority] in range 0 <= priority <= 10 with 0 highest, defaults to 5
/// [metaData] user data
/// [displayName] human readable name for this task
/// [creationTime] time of task creation, 'now' by default.
DataTask(
{String? taskId,
required super.url,
super.urlQueryParameters,
super.headers,
super.httpRequestMethod,
String? post,
Map<String, dynamic>? json,
String? contentType,
super.group,
super.updates,
super.requiresWiFi,
super.retries,
super.metaData,
super.displayName,
super.priority,
super.creationTime})
: assert(const [Updates.status, Updates.none].contains(updates),
'DataTasks can only provide status updates'),
super(
post: json != null ? jsonEncode(json) : post,
baseDirectory: BaseDirectory.temporary,
allowPause: false) {
// if no content-type header set, it is set to [contentType] or
// (if post or json is given) to text/plain or application/json
if (!headers.containsKey('Content-Type') &&
!headers.containsKey('content-type')) {
try {
if (contentType != null) {
headers['Content-Type'] = contentType;
} else if ((post != null || json != null)) {
assert((post != null) ^ (json != null),
'Only post or json can be set, not both');
headers['Content-Type'] =
json != null ? 'application/json' : 'text/plain; charset=utf-8';
}
} on UnsupportedError {
_log.warning(
'Could not add Content-Type header as supplied header is const');
}
}
}
@override
Task copyWith(
{String? taskId,
String? url,
String? filename,
Map<String, String>? headers,
String? httpRequestMethod,
Object? post,
String? directory,
BaseDirectory? baseDirectory,
String? group,
Updates? updates,
bool? requiresWiFi,
int? retries,
int? retriesRemaining,
bool? allowPause,
int? priority,
String? metaData,
String? displayName,
DateTime? creationTime}) =>
DataTask(
taskId: taskId ?? this.taskId,
url: url ?? this.url,
headers: headers ?? this.headers,
httpRequestMethod: httpRequestMethod ?? this.httpRequestMethod,
post: post as String? ?? this.post,
group: group ?? this.group,
updates: updates ?? this.updates,
requiresWiFi: requiresWiFi ?? this.requiresWiFi,
retries: retries ?? this.retries,
priority: priority ?? this.priority,
metaData: metaData ?? this.metaData,
displayName: displayName ?? this.displayName,
creationTime: creationTime ?? this.creationTime)
..retriesRemaining = retriesRemaining ?? this.retriesRemaining;
/// Creates [DataTask] object from [json]
DataTask.fromJson(super.json)
: assert(
json['taskType'] == 'DataTask',
'The provided JSON map is not a DataTask, '
'because key "taskType" is not "DataTask".'),
super.fromJson();
@override
String get taskType => 'DataTask';
}