You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-08 23:07:06 +02:00
feat(mobile): Auto switching server URLs (#14437)
This commit is contained in:
@ -67,7 +67,7 @@ class ApiService implements Authentication {
|
||||
}
|
||||
|
||||
Future<String> resolveAndSetEndpoint(String serverUrl) async {
|
||||
final endpoint = await _resolveEndpoint(serverUrl);
|
||||
final endpoint = await resolveEndpoint(serverUrl);
|
||||
setEndpoint(endpoint);
|
||||
|
||||
// Save in local database for next startup
|
||||
@ -82,7 +82,7 @@ class ApiService implements Authentication {
|
||||
/// host - required
|
||||
/// port - optional (default: based on schema)
|
||||
/// path - optional
|
||||
Future<String> _resolveEndpoint(String serverUrl) async {
|
||||
Future<String> resolveEndpoint(String serverUrl) async {
|
||||
final url = sanitizeUrl(serverUrl);
|
||||
|
||||
if (!await _isEndpointAvailable(serverUrl)) {
|
||||
|
@ -77,6 +77,7 @@ enum AppSettingsEnum<T> {
|
||||
),
|
||||
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
|
||||
syncAlbums<bool>(StoreKey.syncAlbums, null, false),
|
||||
autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false),
|
||||
;
|
||||
|
||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||
|
@ -1,19 +1,26 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/auth.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/auth_api.interface.dart';
|
||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/auth.repository.dart';
|
||||
import 'package:immich_mobile/repositories/auth_api.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/network.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final authServiceProvider = Provider(
|
||||
(ref) => AuthService(
|
||||
ref.watch(authApiRepositoryProvider),
|
||||
ref.watch(authRepositoryProvider),
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(networkServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@ -21,6 +28,7 @@ class AuthService {
|
||||
final IAuthApiRepository _authApiRepository;
|
||||
final IAuthRepository _authRepository;
|
||||
final ApiService _apiService;
|
||||
final NetworkService _networkService;
|
||||
|
||||
final _log = Logger("AuthService");
|
||||
|
||||
@ -28,6 +36,7 @@ class AuthService {
|
||||
this._authApiRepository,
|
||||
this._authRepository,
|
||||
this._apiService,
|
||||
this._networkService,
|
||||
);
|
||||
|
||||
/// Validates the provided server URL by resolving and setting the endpoint.
|
||||
@ -46,6 +55,28 @@ class AuthService {
|
||||
return validUrl;
|
||||
}
|
||||
|
||||
Future<bool> validateAuxilaryServerUrl(String url) async {
|
||||
final httpclient = HttpClient();
|
||||
final accessToken = _authRepository.getAccessToken();
|
||||
bool isValid = false;
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('$url/users/me');
|
||||
final request = await httpclient.getUrl(uri);
|
||||
request.headers.add('x-immich-user-token', accessToken);
|
||||
final response = await request.close();
|
||||
if (response.statusCode == 200) {
|
||||
isValid = true;
|
||||
}
|
||||
} catch (error) {
|
||||
_log.severe("Error validating auxilary endpoint", error);
|
||||
} finally {
|
||||
httpclient.close();
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
Future<LoginResponse> login(String email, String password) {
|
||||
return _authApiRepository.login(email, password);
|
||||
}
|
||||
@ -84,6 +115,10 @@ class AuthService {
|
||||
Store.delete(StoreKey.currentUser),
|
||||
Store.delete(StoreKey.accessToken),
|
||||
Store.delete(StoreKey.assetETag),
|
||||
Store.delete(StoreKey.autoEndpointSwitching),
|
||||
Store.delete(StoreKey.preferredWifiName),
|
||||
Store.delete(StoreKey.localEndpoint),
|
||||
Store.delete(StoreKey.externalEndpointList),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -95,4 +130,62 @@ class AuthService {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> setOpenApiServiceEndpoint() async {
|
||||
final enable = _authRepository.getEndpointSwitchingFeature();
|
||||
if (!enable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final wifiName = await _networkService.getWifiName();
|
||||
final savedWifiName = _authRepository.getPreferredWifiName();
|
||||
String? endpoint;
|
||||
|
||||
if (wifiName == savedWifiName) {
|
||||
endpoint = await _setLocalConnection();
|
||||
}
|
||||
|
||||
endpoint ??= await _setRemoteConnection();
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
Future<String?> _setLocalConnection() async {
|
||||
try {
|
||||
final localEndpoint = _authRepository.getLocalEndpoint();
|
||||
if (localEndpoint != null) {
|
||||
await _apiService.resolveAndSetEndpoint(localEndpoint);
|
||||
return localEndpoint;
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_log.severe("Cannot set local endpoint", error, stackTrace);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String?> _setRemoteConnection() async {
|
||||
List<AuxilaryEndpoint> endpointList;
|
||||
|
||||
try {
|
||||
endpointList = _authRepository.getExternalEndpointList();
|
||||
} catch (error, stackTrace) {
|
||||
_log.severe("Cannot get external endpoint", error, stackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (final endpoint in endpointList) {
|
||||
try {
|
||||
return await _apiService.resolveAndSetEndpoint(endpoint.url);
|
||||
} on ApiException catch (error) {
|
||||
_log.severe("Cannot resolve endpoint", error);
|
||||
continue;
|
||||
} catch (_) {
|
||||
_log.severe("Auxilary server is not valid");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import 'dart:ui' show DartPluginRegistrant, IsolateNameServer, PluginUtilities;
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -17,15 +18,20 @@ import 'package:immich_mobile/repositories/album.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/auth.repository.dart';
|
||||
import 'package:immich_mobile/repositories/auth_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/etag.repository.dart';
|
||||
import 'package:immich_mobile/repositories/exif_info.repository.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/repositories/user_api.repository.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/auth.service.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
import 'package:immich_mobile/services/hash.service.dart';
|
||||
import 'package:immich_mobile/services/localization.service.dart';
|
||||
@ -36,11 +42,13 @@ import 'package:immich_mobile/services/backup.service.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/network.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart';
|
||||
import 'package:path_provider_ios/path_provider_ios.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
|
||||
@ -422,6 +430,24 @@ class BackgroundService {
|
||||
assetMediaRepository,
|
||||
);
|
||||
|
||||
AuthApiRepository authApiRepository = AuthApiRepository(apiService);
|
||||
AuthRepository authRepository = AuthRepository(db);
|
||||
NetworkRepository networkRepository = NetworkRepository(NetworkInfo());
|
||||
PermissionRepository permissionRepository = PermissionRepository();
|
||||
NetworkService networkService =
|
||||
NetworkService(networkRepository, permissionRepository);
|
||||
AuthService authService = AuthService(
|
||||
authApiRepository,
|
||||
authRepository,
|
||||
apiService,
|
||||
networkService,
|
||||
);
|
||||
|
||||
final endpoint = await authService.setOpenApiServiceEndpoint();
|
||||
if (kDebugMode) {
|
||||
debugPrint("[BG UPLOAD] Using endpoint: $endpoint");
|
||||
}
|
||||
|
||||
final selectedAlbums =
|
||||
await backupRepository.getAllBySelection(BackupSelection.select);
|
||||
final excludedAlbums =
|
||||
|
47
mobile/lib/services/network.service.dart
Normal file
47
mobile/lib/services/network.service.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/network.interface.dart';
|
||||
import 'package:immich_mobile/repositories/network.repository.dart';
|
||||
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||
|
||||
final networkServiceProvider = Provider((ref) {
|
||||
return NetworkService(
|
||||
ref.watch(networkRepositoryProvider),
|
||||
ref.watch(permissionRepositoryProvider),
|
||||
);
|
||||
});
|
||||
|
||||
class NetworkService {
|
||||
final INetworkRepository _repository;
|
||||
final IPermissionRepository _permissionRepository;
|
||||
|
||||
NetworkService(this._repository, this._permissionRepository);
|
||||
|
||||
Future<bool> getLocationWhenInUserPermission() {
|
||||
return _permissionRepository.hasLocationWhenInUsePermission();
|
||||
}
|
||||
|
||||
Future<bool> requestLocationWhenInUsePermission() {
|
||||
return _permissionRepository.requestLocationWhenInUsePermission();
|
||||
}
|
||||
|
||||
Future<bool> getLocationAlwaysPermission() {
|
||||
return _permissionRepository.hasLocationAlwaysPermission();
|
||||
}
|
||||
|
||||
Future<bool> requestLocationAlwaysPermission() {
|
||||
return _permissionRepository.requestLocationAlwaysPermission();
|
||||
}
|
||||
|
||||
Future<String?> getWifiName() async {
|
||||
final canRead = await getLocationWhenInUserPermission();
|
||||
if (!canRead) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await _repository.getWifiName();
|
||||
}
|
||||
|
||||
Future<bool> openSettings() {
|
||||
return _permissionRepository.openSettings();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user