WIP - Replaced get_it + injectable with Provider

This commit is contained in:
Anton Stubenbord
2022-12-21 01:14:06 +01:00
parent 10149fb7c1
commit 60aecb549d
59 changed files with 1099 additions and 1362 deletions

View File

@@ -1,11 +0,0 @@
// Fix for accepting self signed certificates.
import 'dart:io';
class X509HttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
}

View File

@@ -2,11 +2,8 @@ import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
@prod
@injectable
class AuthenticationInterceptor implements InterceptorContract {
final LocalVault _localVault;
AuthenticationInterceptor(this._localVault);

View File

@@ -1,9 +1,6 @@
import 'package:http_interceptor/http_interceptor.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
@prod
@injectable
class BaseUrlInterceptor implements InterceptorContract {
final LocalVault _localVault;

View File

@@ -1,8 +1,6 @@
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:injectable/injectable.dart';
@injectable
class LanguageHeaderInterceptor implements InterceptorContract {
final ApplicationSettingsCubit appSettingsCubit;

View File

@@ -1,10 +1,7 @@
import 'package:http_interceptor/http_interceptor.dart';
import 'package:injectable/injectable.dart';
const interceptedRoutes = ['thumb/'];
@injectable
@prod
class ResponseConversionInterceptor implements InterceptorContract {
@override
Future<BaseRequest> interceptRequest({required BaseRequest request}) async =>

View File

@@ -1,195 +0,0 @@
import 'dart:typed_data';
import 'dart:convert';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:http/http.dart';
import 'package:injectable/injectable.dart';
///
/// Convenience class which handles timeout errors.
///
@prod
@Named("timeoutClient")
@Injectable(as: BaseClient)
class TimeoutClient implements BaseClient {
final ConnectivityStatusService connectivityStatusService;
static const Duration requestTimeout = Duration(seconds: 25);
TimeoutClient(this.connectivityStatusService);
@override
Future<StreamedResponse> send(BaseRequest request) async {
return getIt<BaseClient>().send(request).timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
);
}
@override
void close() {
getIt<BaseClient>().close();
}
@override
Future<Response> delete(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>()
.delete(url, headers: headers, body: body, encoding: encoding)
.timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<Response> get(
Uri url, {
Map<String, String>? headers,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>().get(url, headers: headers).timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<Response> head(
Uri url, {
Map<String, String>? headers,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>().head(url, headers: headers).timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<Response> patch(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>()
.patch(url, headers: headers, body: body, encoding: encoding)
.timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<Response> post(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>()
.post(url, headers: headers, body: body, encoding: encoding)
.timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<Response> put(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>()
.put(url, headers: headers, body: body, encoding: encoding)
.timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<String> read(
Uri url, {
Map<String, String>? headers,
}) async {
await _handleOfflineState();
return getIt<BaseClient>().read(url, headers: headers).timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
);
}
@override
Future<Uint8List> readBytes(
Uri url, {
Map<String, String>? headers,
}) async {
await _handleOfflineState();
return getIt<BaseClient>().readBytes(url, headers: headers).timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
);
}
Response _handle400Error(Response response) {
if (response.statusCode == 400) {
// try to parse contained error message, otherwise return response
final JSON json = jsonDecode(utf8.decode(response.bodyBytes));
final PaperlessValidationErrors errorMessages = {};
for (final entry in json.entries) {
if (entry.value is List) {
errorMessages.putIfAbsent(
entry.key, () => (entry.value as List).cast<String>().first);
} else if (entry.value is String) {
errorMessages.putIfAbsent(entry.key, () => entry.value);
} else {
errorMessages.putIfAbsent(entry.key, () => entry.value.toString());
}
}
throw errorMessages;
}
return response;
}
Future<void> _handleOfflineState() async {
if (!(await connectivityStatusService.isConnectedToInternet())) {
throw const PaperlessServerException(ErrorCode.deviceOffline);
}
}
}

View File

@@ -11,17 +11,21 @@ class LabelRepositoriesProvider extends StatelessWidget {
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: [
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Correspondent>>(context),
RepositoryProvider(
create: (context) =>
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(context),
RepositoryProvider(
create: (context) =>
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<StoragePath>>(context),
RepositoryProvider(
create: (context) =>
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Tag>>(context),
RepositoryProvider(
create: (context) =>
RepositoryProvider.of<LabelRepository<Tag>>(context),
),
],
child: child,

View File

@@ -0,0 +1,100 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
///
/// Convenience http client handling timeouts.
///
class SecurityContextAwareDioManager {
final Dio _dio;
// Some dependencies require an [HttpClient], therefore this is also maintained here.
final HttpClient _httpClient;
SecurityContextAwareDioManager()
: _dio = _initDio(),
_httpClient = HttpClient();
Dio get client => _dio;
Stream<SecurityContext> get securityContextChanges =>
_securityContextStreamController.stream.asBroadcastStream();
final StreamController<SecurityContext> _securityContextStreamController =
StreamController.broadcast();
static Dio _initDio() {
//en- and decoded by utf8 by default
final Dio dio = Dio(BaseOptions());
dio.options.receiveTimeout = const Duration(seconds: 25).inMilliseconds;
dio.options.responseType = ResponseType.json;
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) => client
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
dio.interceptors.add(
//TODO: Refactor, create own class...
InterceptorsWrapper(
onError: (e, handler) {
//TODO: Implement and debug how error handling works, or if request has to be resolved.
if (e.response?.statusCode == 400) {
// try to parse contained error message, otherwise return response
final JSON json = jsonDecode(e.response?.data);
final PaperlessValidationErrors errorMessages = {};
for (final entry in json.entries) {
if (entry.value is List) {
errorMessages.putIfAbsent(entry.key,
() => (entry.value as List).cast<String>().first);
} else if (entry.value is String) {
errorMessages.putIfAbsent(entry.key, () => entry.value);
} else {
errorMessages.putIfAbsent(
entry.key, () => entry.value.toString());
}
}
throw errorMessages;
}
handler.next(e);
},
),
);
return dio;
}
void updateSettings({
String? baseUrl,
String? authToken,
ClientCertificate? clientCertificate,
}) {
if (clientCertificate != null) {
final sc = SecurityContext()
..usePrivateKeyBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
)
..useCertificateChainBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
)
..setTrustedCertificatesBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
);
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) => HttpClient(
context: sc,
)..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
_securityContextStreamController.add(sc);
}
if (baseUrl != null) {
_dio.options.baseUrl = baseUrl;
}
if (authToken != null) {
_dio.options.headers.addAll({'Authorization': 'Token $authToken'});
}
}
}

View File

@@ -0,0 +1,26 @@
import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:http/io_client.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:rxdart/rxdart.dart';
extension SecurityContextAwareBaseClientSubjectExtension
on BehaviorSubject<BaseClient> {
///
/// Registers new security context in a new [HttpClient].
///
BaseClient _createSecurityContextAwareHttpClient(
SecurityContext context, {
List<InterceptorContract> interceptors = const [],
}) {
Dio(BaseOptions());
return InterceptedClient.build(
client: IOClient(HttpClient(context: context)),
interceptors: interceptors,
);
}
}

View File

View File

@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:injectable/injectable.dart';
abstract class ConnectivityStatusService {
Future<bool> isConnectedToInternet();
@@ -9,8 +8,6 @@ abstract class ConnectivityStatusService {
Stream<bool> connectivityChanges();
}
@prod
@Injectable(as: ConnectivityStatusService)
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
final Connectivity connectivity;

View File

@@ -82,8 +82,8 @@ class FileService {
static Future<void> clearUserData() async {
final scanDir = await scanDirectory;
final tempDir = await temporaryDirectory;
scanDir?.delete(recursive: true);
tempDir.delete(recursive: true);
await scanDir?.delete(recursive: true);
await tempDir.delete(recursive: true);
}
}

View File

@@ -3,11 +3,9 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
import 'package:paperless_mobile/core/model/document_processing_status.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/util.dart';
import 'package:web_socket_channel/io.dart';
@@ -17,8 +15,6 @@ abstract class StatusService {
AuthenticationInformation credentials, String documentFileName);
}
@Singleton(as: StatusService)
@Named("webSocketStatusService")
class WebSocketStatusService implements StatusService {
late WebSocket? socket;
late IOWebSocketChannel? _channel;
@@ -31,35 +27,33 @@ class WebSocketStatusService implements StatusService {
AuthenticationInformation credentials,
String documentFileName,
) async {
socket = await WebSocket.connect(
httpUrl.replaceFirst("http", "ws") + "/ws/status/",
customClient: getIt<HttpClient>(),
headers: {
'Authorization': 'Token ${credentials.token}',
},
).catchError((_) {
// Use long polling if connection could not be established
});
// socket = await WebSocket.connect(
// httpUrl.replaceFirst("http", "ws") + "/ws/status/",
// customClient: getIt<HttpClient>(),
// headers: {
// 'Authorization': 'Token ${credentials.token}',
// },
// ).catchError((_) {
// // Use long polling if connection could not be established
// });
if (socket != null) {
socket!.where(isNotNull).listen((event) {
final status = DocumentProcessingStatus.fromJson(event);
getIt<DocumentStatusCubit>().updateStatus(status);
if (status.currentProgress == 100) {
socket!.close();
}
});
}
// if (socket != null) {
// socket!.where(isNotNull).listen((event) {
// final status = DocumentProcessingStatus.fromJson(event);
// getIt<DocumentStatusCubit>().updateStatus(status);
// if (status.currentProgress == 100) {
// socket!.close();
// }
// });
// }
}
}
@Injectable(as: StatusService)
@Named("longPollingStatusService")
class LongPollingStatusService implements StatusService {
static const maxRetries = 60;
final BaseClient httpClient;
LongPollingStatusService(@Named("timeoutClient") this.httpClient);
LongPollingStatusService(this.httpClient);
@override
Future<void> startListeningBeforeDocumentUpload(
@@ -67,51 +61,51 @@ class LongPollingStatusService implements StatusService {
AuthenticationInformation credentials,
String documentFileName,
) async {
final today = DateTime.now();
bool consumptionFinished = false;
int retryCount = 0;
// final today = DateTime.now();
// bool consumptionFinished = false;
// int retryCount = 0;
getIt<DocumentStatusCubit>().updateStatus(
DocumentProcessingStatus(
currentProgress: 0,
filename: documentFileName,
maxProgress: 100,
message: ProcessingMessage.new_file,
status: ProcessingStatus.working,
taskId: DocumentProcessingStatus.unknownTaskId,
documentId: null,
isApproximated: true,
),
);
// getIt<DocumentStatusCubit>().updateStatus(
// DocumentProcessingStatus(
// currentProgress: 0,
// filename: documentFileName,
// maxProgress: 100,
// message: ProcessingMessage.new_file,
// status: ProcessingStatus.working,
// taskId: DocumentProcessingStatus.unknownTaskId,
// documentId: null,
// isApproximated: true,
// ),
// );
do {
final response = await httpClient.get(
Uri.parse(
'$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
);
final data = await compute(
PagedSearchResult.fromJson,
PagedSearchResultJsonSerializer(
jsonDecode(response.body), DocumentModel.fromJson),
);
if (data.count > 0) {
consumptionFinished = true;
final docId = data.results[0].id;
getIt<DocumentStatusCubit>().updateStatus(
DocumentProcessingStatus(
currentProgress: 100,
filename: documentFileName,
maxProgress: 100,
message: ProcessingMessage.finished,
status: ProcessingStatus.success,
taskId: DocumentProcessingStatus.unknownTaskId,
documentId: docId,
isApproximated: true,
),
);
return;
}
sleep(const Duration(seconds: 1));
} while (!consumptionFinished && retryCount < maxRetries);
// do {
// final response = await httpClient.get(
// Uri.parse(
// '$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
// );
// final data = await compute(
// PagedSearchResult.fromJson,
// PagedSearchResultJsonSerializer(
// jsonDecode(response.body), DocumentModel.fromJson),
// );
// if (data.count > 0) {
// consumptionFinished = true;
// final docId = data.results[0].id;
// getIt<DocumentStatusCubit>().updateStatus(
// DocumentProcessingStatus(
// currentProgress: 100,
// filename: documentFileName,
// maxProgress: 100,
// message: ProcessingMessage.finished,
// status: ProcessingStatus.success,
// taskId: DocumentProcessingStatus.unknownTaskId,
// documentId: docId,
// isApproximated: true,
// ),
// );
// return;
// }
// sleep(const Duration(seconds: 1));
// } while (!consumptionFinished && retryCount < maxRetries);
}
}

View File

@@ -6,7 +6,6 @@ import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:injectable/injectable.dart';
abstract class LocalVault {
Future<void> storeAuthenticationInformation(AuthenticationInformation auth);
@@ -17,8 +16,6 @@ abstract class LocalVault {
Future<void> clear();
}
@prod
@Injectable(as: LocalVault)
class LocalVaultImpl implements LocalVault {
static const applicationSettingsKey = "applicationSettings";
static const authenticationKey = "authentication";