264 lines
10 KiB
Dart
264 lines
10 KiB
Dart
// lib/services/retry_service.dart
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import 'package:environment_monitoring_app/models/in_situ_sampling_data.dart';
|
|
import 'package:environment_monitoring_app/services/marine_in_situ_sampling_service.dart';
|
|
import 'package:environment_monitoring_app/models/river_in_situ_sampling_data.dart'; // ADDED
|
|
import 'package:environment_monitoring_app/services/river_in_situ_sampling_service.dart'; // ADDED
|
|
import 'package:environment_monitoring_app/services/api_service.dart';
|
|
import 'package:environment_monitoring_app/services/base_api_service.dart';
|
|
import 'package:environment_monitoring_app/services/ftp_service.dart';
|
|
import 'package:environment_monitoring_app/services/server_config_service.dart';
|
|
import 'package:environment_monitoring_app/auth_provider.dart';
|
|
|
|
/// A dedicated service to manage the queue of failed API, FTP, and complex submission tasks.
|
|
class RetryService {
|
|
final DatabaseHelper _dbHelper = DatabaseHelper();
|
|
final BaseApiService _baseApiService = BaseApiService();
|
|
final FtpService _ftpService = FtpService();
|
|
final ServerConfigService _serverConfigService = ServerConfigService();
|
|
bool _isProcessing = false;
|
|
|
|
// --- START: MODIFICATION FOR HANDLING COMPLEX TASKS ---
|
|
// These services will be provided after the RetryService is created.
|
|
MarineInSituSamplingService? _marineInSituService;
|
|
RiverInSituSamplingService? _riverInSituService; // ADDED
|
|
AuthProvider? _authProvider;
|
|
|
|
// Call this method from your main app setup to provide the necessary services.
|
|
void initialize({
|
|
required MarineInSituSamplingService marineInSituService,
|
|
required RiverInSituSamplingService riverInSituService, // ADDED
|
|
required AuthProvider authProvider,
|
|
}) {
|
|
_marineInSituService = marineInSituService;
|
|
_riverInSituService = riverInSituService; // ADDED
|
|
_authProvider = authProvider;
|
|
}
|
|
// --- END: MODIFICATION FOR HANDLING COMPLEX TASKS ---
|
|
|
|
|
|
/// Adds a generic, complex task to the queue, to be handled by a background processor.
|
|
Future<void> queueTask({
|
|
required String type,
|
|
required Map<String, dynamic> payload,
|
|
}) async {
|
|
await _dbHelper.queueFailedRequest({
|
|
'type': type,
|
|
'endpoint_or_path': 'N/A',
|
|
'payload': jsonEncode(payload),
|
|
'timestamp': DateTime.now().toIso8601String(),
|
|
'status': 'pending',
|
|
});
|
|
debugPrint("Task of type '$type' has been queued for background processing.");
|
|
}
|
|
|
|
/// Adds a failed API request to the local database queue.
|
|
Future<void> addApiToQueue({
|
|
required String endpoint,
|
|
required String method,
|
|
Map<String, dynamic>? body,
|
|
Map<String, String>? fields,
|
|
Map<String, File>? files,
|
|
}) async {
|
|
final serializableFiles = files?.map((key, value) => MapEntry(key, value.path));
|
|
final payload = {
|
|
'method': method,
|
|
'body': body,
|
|
'fields': fields,
|
|
'files': serializableFiles,
|
|
};
|
|
await _dbHelper.queueFailedRequest({
|
|
'type': 'api',
|
|
'endpoint_or_path': endpoint,
|
|
'payload': jsonEncode(payload),
|
|
'timestamp': DateTime.now().toIso8601String(),
|
|
'status': 'pending',
|
|
});
|
|
debugPrint("API request for endpoint '$endpoint' has been queued for retry.");
|
|
}
|
|
|
|
/// Adds a failed FTP upload to the local database queue.
|
|
Future<void> addFtpToQueue({
|
|
required String localFilePath,
|
|
required String remotePath,
|
|
}) async {
|
|
final payload = {'localFilePath': localFilePath};
|
|
await _dbHelper.queueFailedRequest({
|
|
'type': 'ftp',
|
|
'endpoint_or_path': remotePath,
|
|
'payload': jsonEncode(payload),
|
|
'timestamp': DateTime.now().toIso8601String(),
|
|
'status': 'pending',
|
|
});
|
|
debugPrint("FTP upload for file '$localFilePath' has been queued for retry.");
|
|
}
|
|
|
|
/// Retrieves all tasks currently in the 'pending' state from the queue.
|
|
Future<List<Map<String, dynamic>>> getPendingTasks() {
|
|
return _dbHelper.getPendingRequests();
|
|
}
|
|
|
|
/// Processes the entire queue of pending tasks.
|
|
Future<void> processRetryQueue() async {
|
|
if (_isProcessing) {
|
|
debugPrint("[RetryService] ⏳ Queue is already being processed. Skipping.");
|
|
return;
|
|
}
|
|
_isProcessing = true;
|
|
debugPrint("[RetryService] ▶️ Starting to process main retry queue...");
|
|
|
|
final pendingTasks = await getPendingTasks();
|
|
if (pendingTasks.isEmpty) {
|
|
debugPrint("[RetryService] ⏹️ Queue is empty. Nothing to process.");
|
|
_isProcessing = false;
|
|
return;
|
|
}
|
|
|
|
if (_authProvider == null || !await _authProvider!.isConnected()) {
|
|
debugPrint("[RetryService] ❌ No internet connection. Aborting queue processing.");
|
|
_isProcessing = false;
|
|
return;
|
|
}
|
|
|
|
debugPrint("[RetryService] 🔎 Found ${pendingTasks.length} pending tasks.");
|
|
for (final task in pendingTasks) {
|
|
await retryTask(task['id'] as int);
|
|
}
|
|
|
|
debugPrint("[RetryService] ⏹️ Finished processing retry queue.");
|
|
_isProcessing = false;
|
|
}
|
|
|
|
/// Attempts to re-execute a single failed task from the queue.
|
|
/// Returns `true` on success, `false` on failure.
|
|
Future<bool> retryTask(int taskId) async {
|
|
final task = await _dbHelper.getRequestById(taskId);
|
|
if (task == null) {
|
|
debugPrint("Retry failed: Task with ID $taskId not found in the queue.");
|
|
return false;
|
|
}
|
|
|
|
bool success = false;
|
|
final payload = jsonDecode(task['payload'] as String);
|
|
|
|
try {
|
|
if (_authProvider == null) {
|
|
debugPrint("RetryService has not been initialized. Cannot process task.");
|
|
return false;
|
|
}
|
|
|
|
if (task['type'] == 'insitu_submission') {
|
|
debugPrint("Retrying complex task 'insitu_submission' with ID $taskId.");
|
|
if (_marineInSituService == null) return false;
|
|
|
|
final String logFilePath = payload['localLogPath'];
|
|
final file = File(logFilePath);
|
|
|
|
if (!await file.exists()) {
|
|
debugPrint("Retry failed: Source log file no longer exists at $logFilePath");
|
|
await _dbHelper.deleteRequestFromQueue(taskId); // Remove invalid task
|
|
return false;
|
|
}
|
|
|
|
final content = await file.readAsString();
|
|
final jsonData = jsonDecode(content) as Map<String, dynamic>;
|
|
final InSituSamplingData dataToResubmit = InSituSamplingData.fromJson(jsonData);
|
|
final String logDirectoryPath = p.dirname(logFilePath);
|
|
|
|
final result = await _marineInSituService!.submitInSituSample(
|
|
data: dataToResubmit,
|
|
appSettings: _authProvider!.appSettings,
|
|
authProvider: _authProvider!,
|
|
logDirectory: logDirectoryPath,
|
|
);
|
|
success = result['success'];
|
|
|
|
// --- START: ADDED LOGIC FOR RIVER IN-SITU SUBMISSION ---
|
|
} else if (task['type'] == 'river_insitu_submission') {
|
|
debugPrint("Retrying complex task 'river_insitu_submission' with ID $taskId.");
|
|
if (_riverInSituService == null) return false;
|
|
|
|
final String logFilePath = payload['localLogPath'];
|
|
final file = File(logFilePath);
|
|
|
|
if (!await file.exists()) {
|
|
debugPrint("Retry failed: Source log file no longer exists at $logFilePath");
|
|
await _dbHelper.deleteRequestFromQueue(taskId); // Remove invalid task
|
|
return false;
|
|
}
|
|
|
|
final content = await file.readAsString();
|
|
final jsonData = jsonDecode(content) as Map<String, dynamic>;
|
|
final RiverInSituSamplingData dataToResubmit = RiverInSituSamplingData.fromJson(jsonData);
|
|
final String logDirectoryPath = p.dirname(logFilePath);
|
|
|
|
final result = await _riverInSituService!.submitData(
|
|
data: dataToResubmit,
|
|
appSettings: _authProvider!.appSettings,
|
|
authProvider: _authProvider!,
|
|
logDirectory: logDirectoryPath,
|
|
);
|
|
success = result['success'];
|
|
// --- END: ADDED LOGIC FOR RIVER IN-SITU SUBMISSION ---
|
|
|
|
} else if (task['type'] == 'api') {
|
|
final endpoint = task['endpoint_or_path'] as String;
|
|
final method = payload['method'] as String;
|
|
final baseUrl = await _serverConfigService.getActiveApiUrl();
|
|
debugPrint("Retrying API task $taskId: $method to $baseUrl/$endpoint");
|
|
Map<String, dynamic> result;
|
|
|
|
if (method == 'POST_MULTIPART') {
|
|
final Map<String, String> fields = Map<String, String>.from(payload['fields'] ?? {});
|
|
final Map<String, File> files = (payload['files'] as Map<String, dynamic>?)
|
|
?.map((key, value) => MapEntry(key, File(value as String))) ?? {};
|
|
result = await _baseApiService.postMultipart(baseUrl: baseUrl, endpoint: endpoint, fields: fields, files: files);
|
|
} else {
|
|
final Map<String, dynamic> body = Map<String, dynamic>.from(payload['body'] ?? {});
|
|
result = await _baseApiService.post(baseUrl, endpoint, body);
|
|
}
|
|
success = result['success'];
|
|
|
|
} else if (task['type'] == 'ftp') {
|
|
final remotePath = task['endpoint_or_path'] as String;
|
|
final localFile = File(payload['localFilePath'] as String);
|
|
debugPrint("Retrying FTP task $taskId: Uploading ${localFile.path} to $remotePath");
|
|
|
|
if (await localFile.exists()) {
|
|
final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];
|
|
if (ftpConfigs.isEmpty) {
|
|
debugPrint("Retry failed for FTP task $taskId: No FTP configurations found.");
|
|
return false;
|
|
}
|
|
for (final config in ftpConfigs) {
|
|
final result = await _ftpService.uploadFile(config: config, fileToUpload: localFile, remotePath: remotePath);
|
|
if (result['success']) {
|
|
success = true;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
debugPrint("Retry failed for FTP task $taskId: Source file no longer exists at ${localFile.path}");
|
|
success = false;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint("A critical error occurred while retrying task $taskId: $e");
|
|
success = false;
|
|
}
|
|
|
|
if (success) {
|
|
debugPrint("Task $taskId completed successfully. Removing from queue.");
|
|
await _dbHelper.deleteRequestFromQueue(taskId);
|
|
} else {
|
|
debugPrint("Retry attempt for task $taskId failed. It will remain in the queue.");
|
|
}
|
|
|
|
return success;
|
|
}
|
|
} |