diff --git a/lib/logic/models/hive/server_domain.dart b/lib/logic/models/hive/server_domain.dart index 1649be2a..bd755bbc 100644 --- a/lib/logic/models/hive/server_domain.dart +++ b/lib/logic/models/hive/server_domain.dart @@ -47,4 +47,11 @@ enum DnsProviderType { return unknown; } } + + String toInfectName() => switch (this) { + digitalOcean => 'DIGITALOCEAN', + cloudflare => 'CLOUDFLARE', + desec => 'DESEC', + unknown => 'UNKNOWN', + }; } diff --git a/lib/logic/providers/server_providers/digital_ocean.dart b/lib/logic/providers/server_providers/digital_ocean.dart index 93371405..2f8e11c7 100644 --- a/lib/logic/providers/server_providers/digital_ocean.dart +++ b/lib/logic/providers/server_providers/digital_ocean.dart @@ -5,7 +5,6 @@ import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_oc import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart'; import 'package:selfprivacy/logic/models/disk_size.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/digital_ocean_server_info.dart'; import 'package:selfprivacy/logic/models/metrics.dart'; import 'package:selfprivacy/logic/models/price.dart'; @@ -52,86 +51,41 @@ class DigitalOceanServerProvider extends ServerProvider { ServerProviderType get type => ServerProviderType.digitalOcean; @override - Future> trySetServerLocation( - final String location, - ) async { - final bool apiInitialized = _adapter.api().isWithToken; - if (!apiInitialized) { + Future>> getServers() async { + List servers = []; + final result = await _adapter.api().getServers(); + if (result.data.isEmpty || !result.success) { return GenericResult( - success: true, - data: false, - message: 'Not authorized!', + success: result.success, + data: servers, + code: result.code, + message: result.message, ); } - _adapter = ApiAdapter( - isWithToken: true, - region: location, - ); - return success; - } + final List rawServers = result.data; + servers = rawServers.map( + (final server) { + String ipv4 = '0.0.0.0'; + if (server['networks']['v4'].isNotEmpty) { + for (final v4 in server['networks']['v4']) { + if (v4['type'].toString() == 'public') { + ipv4 = v4['ip_address'].toString(); + } + } + } - @override - Future> tryInitApiByToken(final String token) async { - final api = _adapter.api(getInitialized: false); - final result = await api.isApiTokenValid(token); - if (!result.data || !result.success) { - return result; - } + return ServerBasicInfo( + id: server['id'], + reverseDns: server['name'], + created: DateTime.now(), + ip: ipv4, + name: server['name'], + ); + }, + ).toList(); - _adapter = ApiAdapter(region: api.region, isWithToken: true); - return result; - } - - String? getEmojiFlag(final String query) { - String? emoji; - - switch (query.toLowerCase().substring(0, 3)) { - case 'fra': - emoji = '🇩🇪'; - break; - - case 'ams': - emoji = '🇳🇱'; - break; - - case 'sgp': - emoji = '🇸🇬'; - break; - - case 'lon': - emoji = '🇬🇧'; - break; - - case 'tor': - emoji = '🇨🇦'; - break; - - case 'blr': - emoji = '🇮🇳'; - break; - - case 'nyc': - case 'sfo': - emoji = '🇺🇸'; - break; - } - - return emoji; - } - - String dnsProviderToInfectName(final DnsProviderType dnsProvider) { - String dnsProviderType; - switch (dnsProvider) { - case DnsProviderType.digitalOcean: - dnsProviderType = 'DIGITALOCEAN'; - break; - case DnsProviderType.cloudflare: - default: - dnsProviderType = 'CLOUDFLARE'; - break; - } - return dnsProviderType; + return GenericResult(success: true, data: servers); } @override @@ -148,8 +102,7 @@ class DigitalOceanServerProvider extends ServerProvider { rootUser: installationData.rootUser, domainName: installationData.serverDomain.domainName, serverType: installationData.serverTypeId, - dnsProviderType: - dnsProviderToInfectName(installationData.dnsProviderType), + dnsProviderType: installationData.dnsProviderType.toInfectName(), hostName: hostname, base64Password: base64.encode( utf8.encode(installationData.rootUser.password ?? 'PASS'), @@ -244,6 +197,139 @@ class DigitalOceanServerProvider extends ServerProvider { return GenericResult(success: true, data: null); } + @override + Future> deleteServer( + final String hostname, + ) async { + final String deletionName = getHostnameFromDomain(hostname); + final serversResult = await getServers(); + try { + final servers = serversResult.data; + ServerBasicInfo? foundServer; + for (final server in servers) { + if (server.name == deletionName) { + foundServer = server; + break; + } + } + + final volumes = await getVolumes(); + final ServerVolume volumeToRemove; + volumeToRemove = volumes.data.firstWhere( + (final el) => el.serverId == foundServer!.id, + ); + + await _adapter.api().detachVolume( + volumeToRemove.name, + volumeToRemove.serverId!, + ); + + await Future.delayed(const Duration(seconds: 10)); + final List laterFutures = []; + laterFutures.add(_adapter.api().deleteVolume(volumeToRemove.uuid!)); + laterFutures.add(_adapter.api().deleteServer(foundServer!.id)); + + await Future.wait(laterFutures); + } catch (e) { + print(e); + return GenericResult( + success: false, + data: CallbackDialogueBranching( + choices: [ + CallbackDialogueChoice( + title: 'basis.cancel'.tr(), + callback: null, + ), + CallbackDialogueChoice( + title: 'modals.try_again'.tr(), + callback: () async { + await Future.delayed(const Duration(seconds: 5)); + return deleteServer(hostname); + }, + ), + ], + description: 'modals.try_again'.tr(), + title: 'modals.server_deletion_error'.tr(), + ), + message: e.toString(), + ); + } + + return GenericResult( + success: true, + data: null, + ); + } + + @override + Future> tryInitApiByToken(final String token) async { + final api = _adapter.api(getInitialized: false); + final result = await api.isApiTokenValid(token); + if (!result.data || !result.success) { + return result; + } + + _adapter = ApiAdapter(region: api.region, isWithToken: true); + return result; + } + + @override + Future> trySetServerLocation( + final String location, + ) async { + final bool apiInitialized = _adapter.api().isWithToken; + if (!apiInitialized) { + return GenericResult( + success: true, + data: false, + message: 'Not authorized!', + ); + } + + _adapter = ApiAdapter( + isWithToken: true, + region: location, + ); + return success; + } + + String? getEmojiFlag(final String query) { + String? emoji; + + switch (query.toLowerCase().substring(0, 3)) { + case 'fra': + emoji = '🇩🇪'; + break; + + case 'ams': + emoji = '🇳🇱'; + break; + + case 'sgp': + emoji = '🇸🇬'; + break; + + case 'lon': + emoji = '🇬🇧'; + break; + + case 'tor': + emoji = '🇨🇦'; + break; + + case 'blr': + emoji = '🇮🇳'; + break; + + case 'nyc': + case 'sfo': + emoji = '🇺🇸'; + break; + } + + return emoji; + } + @override Future>> getAvailableLocations() async { @@ -318,43 +404,214 @@ class DigitalOceanServerProvider extends ServerProvider { } @override - Future>> getServers() async { - List servers = []; - final result = await _adapter.api().getServers(); - if (result.data.isEmpty || !result.success) { + Future> powerOn(final int serverId) async { + DateTime? timestamp; + final result = await _adapter.api().powerOn(serverId); + if (!result.success) { return GenericResult( - success: result.success, - data: servers, + success: false, + data: timestamp, code: result.code, message: result.message, ); } - final List rawServers = result.data; - servers = rawServers.map( - (final server) { - String ipv4 = '0.0.0.0'; - if (server['networks']['v4'].isNotEmpty) { - for (final v4 in server['networks']['v4']) { - if (v4['type'].toString() == 'public') { - ipv4 = v4['ip_address'].toString(); - } - } - } + timestamp = DateTime.now(); - return ServerBasicInfo( - id: server['id'], - reverseDns: server['name'], - created: DateTime.now(), - ip: ipv4, - name: server['name'], - ); - }, - ).toList(); - - return GenericResult(success: true, data: servers); + return GenericResult( + success: true, + data: timestamp, + ); } + @override + Future> restart(final int serverId) async { + DateTime? timestamp; + final result = await _adapter.api().restart(serverId); + if (!result.success) { + return GenericResult( + success: false, + data: timestamp, + code: result.code, + message: result.message, + ); + } + + timestamp = DateTime.now(); + + return GenericResult( + success: true, + data: timestamp, + ); + } + + /// Hardcoded on their documentation and there is no pricing API at all + /// Probably we should scrap the doc page manually + @override + Future> getPricePerGb() async => GenericResult( + success: true, + data: Price( + value: 0.10, + currency: currency, + ), + ); + + @override + Future>> getVolumes({ + final String? status, + }) async { + final List volumes = []; + + final result = await _adapter.api().getVolumes(); + + if (!result.success || result.data.isEmpty) { + return GenericResult( + data: [], + success: false, + code: result.code, + message: result.message, + ); + } + + try { + int id = 0; + for (final rawVolume in result.data) { + final String volumeName = rawVolume.name; + final volume = ServerVolume( + id: id++, + name: volumeName, + sizeByte: rawVolume.sizeGigabytes * 1024 * 1024 * 1024, + serverId: + (rawVolume.dropletIds != null && rawVolume.dropletIds!.isNotEmpty) + ? rawVolume.dropletIds![0] + : null, + linuxDevice: 'scsi-0DO_Volume_$volumeName', + uuid: rawVolume.id, + ); + volumes.add(volume); + } + } catch (e) { + print(e); + return GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } + + return GenericResult( + data: volumes, + success: true, + ); + } + + @override + Future> createVolume() async { + ServerVolume? volume; + + final result = await _adapter.api().createVolume(); + + if (!result.success || result.data == null) { + return GenericResult( + data: null, + success: false, + code: result.code, + message: result.message, + ); + } + + final getVolumesResult = await _adapter.api().getVolumes(); + + if (!getVolumesResult.success || getVolumesResult.data.isEmpty) { + return GenericResult( + data: null, + success: false, + code: result.code, + message: result.message, + ); + } + + final String volumeName = result.data!.name; + volume = ServerVolume( + id: getVolumesResult.data.length, + name: volumeName, + sizeByte: result.data!.sizeGigabytes, + serverId: null, + linuxDevice: '/dev/disk/by-id/scsi-0DO_Volume_$volumeName', + uuid: result.data!.id, + ); + + return GenericResult( + data: volume, + success: true, + ); + } + + Future> getVolume( + final String volumeUuid, + ) async { + ServerVolume? requestedVolume; + + final result = await getVolumes(); + + if (!result.success || result.data.isEmpty) { + return GenericResult( + data: null, + success: false, + code: result.code, + message: result.message, + ); + } + + for (final volume in result.data) { + if (volume.uuid == volumeUuid) { + requestedVolume = volume; + } + } + + return GenericResult( + data: requestedVolume, + success: true, + ); + } + + @override + Future> attachVolume( + final ServerVolume volume, + final int serverId, + ) async => + _adapter.api().attachVolume( + volume.name, + serverId, + ); + + @override + Future> detachVolume( + final ServerVolume volume, + ) async => + _adapter.api().detachVolume( + volume.name, + volume.serverId!, + ); + + @override + Future> deleteVolume( + final ServerVolume volume, + ) async => + _adapter.api().deleteVolume( + volume.uuid!, + ); + + @override + Future> resizeVolume( + final ServerVolume volume, + final DiskSize size, + ) async => + _adapter.api().resizeVolume( + volume.name, + size, + ); + @override Future>> getMetadata( final int serverId, @@ -536,277 +793,4 @@ class DigitalOceanServerProvider extends ServerProvider { return GenericResult(success: true, data: metrics); } - - @override - Future> restart(final int serverId) async { - DateTime? timestamp; - final result = await _adapter.api().restart(serverId); - if (!result.success) { - return GenericResult( - success: false, - data: timestamp, - code: result.code, - message: result.message, - ); - } - - timestamp = DateTime.now(); - - return GenericResult( - success: true, - data: timestamp, - ); - } - - @override - Future> deleteServer( - final String hostname, - ) async { - final String deletionName = getHostnameFromDomain(hostname); - final serversResult = await getServers(); - try { - final servers = serversResult.data; - ServerBasicInfo? foundServer; - for (final server in servers) { - if (server.name == deletionName) { - foundServer = server; - break; - } - } - - final volumes = await getVolumes(); - final ServerVolume volumeToRemove; - volumeToRemove = volumes.data.firstWhere( - (final el) => el.serverId == foundServer!.id, - ); - - await _adapter.api().detachVolume( - volumeToRemove.name, - volumeToRemove.serverId!, - ); - - await Future.delayed(const Duration(seconds: 10)); - final List laterFutures = []; - laterFutures.add(_adapter.api().deleteVolume(volumeToRemove.uuid!)); - laterFutures.add(_adapter.api().deleteServer(foundServer!.id)); - - await Future.wait(laterFutures); - } catch (e) { - print(e); - return GenericResult( - success: false, - data: CallbackDialogueBranching( - choices: [ - CallbackDialogueChoice( - title: 'basis.cancel'.tr(), - callback: null, - ), - CallbackDialogueChoice( - title: 'modals.try_again'.tr(), - callback: () async { - await Future.delayed(const Duration(seconds: 5)); - return deleteServer(hostname); - }, - ), - ], - description: 'modals.try_again'.tr(), - title: 'modals.server_deletion_error'.tr(), - ), - message: e.toString(), - ); - } - - return GenericResult( - success: true, - data: null, - ); - } - - @override - Future>> getVolumes({ - final String? status, - }) async { - final List volumes = []; - - final result = await _adapter.api().getVolumes(); - - if (!result.success || result.data.isEmpty) { - return GenericResult( - data: [], - success: false, - code: result.code, - message: result.message, - ); - } - - try { - int id = 0; - for (final rawVolume in result.data) { - final String volumeName = rawVolume.name; - final volume = ServerVolume( - id: id++, - name: volumeName, - sizeByte: rawVolume.sizeGigabytes * 1024 * 1024 * 1024, - serverId: - (rawVolume.dropletIds != null && rawVolume.dropletIds!.isNotEmpty) - ? rawVolume.dropletIds![0] - : null, - linuxDevice: 'scsi-0DO_Volume_$volumeName', - uuid: rawVolume.id, - ); - volumes.add(volume); - } - } catch (e) { - print(e); - return GenericResult( - data: [], - success: false, - message: e.toString(), - ); - } - - return GenericResult( - data: volumes, - success: true, - ); - } - - @override - Future> createVolume() async { - ServerVolume? volume; - - final result = await _adapter.api().createVolume(); - - if (!result.success || result.data == null) { - return GenericResult( - data: null, - success: false, - code: result.code, - message: result.message, - ); - } - - final getVolumesResult = await _adapter.api().getVolumes(); - - if (!getVolumesResult.success || getVolumesResult.data.isEmpty) { - return GenericResult( - data: null, - success: false, - code: result.code, - message: result.message, - ); - } - - final String volumeName = result.data!.name; - volume = ServerVolume( - id: getVolumesResult.data.length, - name: volumeName, - sizeByte: result.data!.sizeGigabytes, - serverId: null, - linuxDevice: '/dev/disk/by-id/scsi-0DO_Volume_$volumeName', - uuid: result.data!.id, - ); - - return GenericResult( - data: volume, - success: true, - ); - } - - Future> getVolume( - final String volumeUuid, - ) async { - ServerVolume? requestedVolume; - - final result = await getVolumes(); - - if (!result.success || result.data.isEmpty) { - return GenericResult( - data: null, - success: false, - code: result.code, - message: result.message, - ); - } - - for (final volume in result.data) { - if (volume.uuid == volumeUuid) { - requestedVolume = volume; - } - } - - return GenericResult( - data: requestedVolume, - success: true, - ); - } - - @override - Future> deleteVolume( - final ServerVolume volume, - ) async => - _adapter.api().deleteVolume( - volume.uuid!, - ); - - @override - Future> attachVolume( - final ServerVolume volume, - final int serverId, - ) async => - _adapter.api().attachVolume( - volume.name, - serverId, - ); - - @override - Future> detachVolume( - final ServerVolume volume, - ) async => - _adapter.api().detachVolume( - volume.name, - volume.serverId!, - ); - - @override - Future> resizeVolume( - final ServerVolume volume, - final DiskSize size, - ) async => - _adapter.api().resizeVolume( - volume.name, - size, - ); - - /// Hardcoded on their documentation and there is no pricing API at all - /// Probably we should scrap the doc page manually - @override - Future> getPricePerGb() async => GenericResult( - success: true, - data: Price( - value: 0.10, - currency: currency, - ), - ); - - @override - Future> powerOn(final int serverId) async { - DateTime? timestamp; - final result = await _adapter.api().powerOn(serverId); - if (!result.success) { - return GenericResult( - success: false, - data: timestamp, - code: result.code, - message: result.message, - ); - } - - timestamp = DateTime.now(); - - return GenericResult( - success: true, - data: timestamp, - ); - } } diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart index f7afeb3c..b61e458f 100644 --- a/lib/logic/providers/server_providers/hetzner.dart +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -5,7 +5,6 @@ import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/he import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart'; import 'package:selfprivacy/logic/models/disk_size.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; -import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; import 'package:selfprivacy/logic/models/metrics.dart'; import 'package:selfprivacy/logic/models/price.dart'; @@ -51,131 +50,6 @@ class HetznerServerProvider extends ServerProvider { @override ServerProviderType get type => ServerProviderType.hetzner; - @override - Future> trySetServerLocation( - final String location, - ) async { - final bool apiInitialized = _adapter.api().isWithToken; - if (!apiInitialized) { - return GenericResult( - success: true, - data: false, - message: 'Not authorized!', - ); - } - - _adapter = ApiAdapter( - isWithToken: true, - region: location, - ); - return success; - } - - @override - Future> tryInitApiByToken(final String token) async { - final api = _adapter.api(getInitialized: false); - final result = await api.isApiTokenValid(token); - if (!result.data || !result.success) { - return result; - } - - _adapter = ApiAdapter(region: api.region, isWithToken: true); - return result; - } - - String? getEmojiFlag(final String query) { - String? emoji; - - switch (query.toLowerCase()) { - case 'de': - emoji = '🇩🇪'; - break; - - case 'fi': - emoji = '🇫🇮'; - break; - - case 'us': - emoji = '🇺🇸'; - break; - } - - return emoji; - } - - @override - Future>> - getAvailableLocations() async { - final List locations = []; - final result = await _adapter.api().getAvailableLocations(); - if (result.data.isEmpty || !result.success) { - return GenericResult( - success: result.success, - data: locations, - code: result.code, - message: result.message, - ); - } - - final List rawLocations = result.data; - for (final rawLocation in rawLocations) { - ServerProviderLocation? location; - try { - location = ServerProviderLocation( - title: rawLocation.city, - description: rawLocation.description, - flag: getEmojiFlag(rawLocation.country), - identifier: rawLocation.name, - ); - } catch (e) { - continue; - } - locations.add(location); - } - - return GenericResult(success: true, data: locations); - } - - @override - Future>> getServerTypes({ - required final ServerProviderLocation location, - }) async { - final List types = []; - final result = await _adapter.api().getAvailableServerTypes(); - if (result.data.isEmpty || !result.success) { - return GenericResult( - success: result.success, - data: types, - code: result.code, - message: result.message, - ); - } - - final rawTypes = result.data; - for (final rawType in rawTypes) { - for (final rawPrice in rawType.prices) { - if (rawPrice.location == location.identifier) { - types.add( - ServerType( - title: rawType.description, - identifier: rawType.name, - ram: rawType.memory.toDouble(), - cores: rawType.cores, - disk: DiskSize(byte: rawType.disk * 1024 * 1024 * 1024), - price: Price( - value: rawPrice.monthly, - currency: currency, - ), - location: location, - ), - ); - } - } - } - - return GenericResult(success: true, data: types); - } - @override Future>> getServers() async { final List servers = []; @@ -214,206 +88,6 @@ class HetznerServerProvider extends ServerProvider { return GenericResult(success: true, data: servers); } - @override - Future>> getMetadata( - final int serverId, - ) async { - List metadata = []; - final result = await _adapter.api().getServers(); - if (result.data.isEmpty || !result.success) { - return GenericResult( - success: false, - data: metadata, - code: result.code, - message: result.message, - ); - } - - final List servers = result.data; - try { - final HetznerServerInfo server = servers.firstWhere( - (final server) => server.id == serverId, - ); - - metadata = [ - ServerMetadataEntity( - type: MetadataType.id, - trId: 'server.server_id', - value: server.id.toString(), - ), - ServerMetadataEntity( - type: MetadataType.status, - trId: 'server.status', - value: server.status.toString().split('.')[1].capitalize(), - ), - ServerMetadataEntity( - type: MetadataType.cpu, - trId: 'server.cpu', - value: server.serverType.cores.toString(), - ), - ServerMetadataEntity( - type: MetadataType.ram, - trId: 'server.ram', - value: '${server.serverType.memory.toString()} GB', - ), - ServerMetadataEntity( - type: MetadataType.cost, - trId: 'server.monthly_cost', - value: - '${server.serverType.prices[1].monthly.toStringAsFixed(2)} ${currency.shortcode}', - ), - ServerMetadataEntity( - type: MetadataType.location, - trId: 'server.location', - value: '${server.location.city}, ${server.location.country}', - ), - ServerMetadataEntity( - type: MetadataType.other, - trId: 'server.provider', - value: _adapter.api().displayProviderName, - ), - ]; - } catch (e) { - return GenericResult( - success: false, - data: [], - message: e.toString(), - ); - } - - return GenericResult(success: true, data: metadata); - } - - @override - Future> getMetrics( - final int serverId, - final DateTime start, - final DateTime end, - ) async { - ServerMetrics? metrics; - - List serializeTimeSeries( - final Map json, - final String type, - ) { - final List list = json['time_series'][type]['values']; - return list - .map((final el) => TimeSeriesData(el[0], double.parse(el[1]))) - .toList(); - } - - final cpuResult = await _adapter.api().getMetrics( - serverId, - start, - end, - 'cpu', - ); - - if (cpuResult.data.isEmpty || !cpuResult.success) { - return GenericResult( - success: false, - data: metrics, - code: cpuResult.code, - message: cpuResult.message, - ); - } - - final netResult = await _adapter.api().getMetrics( - serverId, - start, - end, - 'network', - ); - - if (cpuResult.data.isEmpty || !netResult.success) { - return GenericResult( - success: false, - data: metrics, - code: netResult.code, - message: netResult.message, - ); - } - - metrics = ServerMetrics( - cpu: serializeTimeSeries( - cpuResult.data, - 'cpu', - ), - bandwidthIn: serializeTimeSeries( - netResult.data, - 'network.0.bandwidth.in', - ), - bandwidthOut: serializeTimeSeries( - netResult.data, - 'network.0.bandwidth.out', - ), - end: end, - start: start, - stepsInSecond: cpuResult.data['step'], - ); - - return GenericResult(data: metrics, success: true); - } - - @override - Future> restart(final int serverId) async { - DateTime? timestamp; - final result = await _adapter.api().restart(serverId); - if (!result.success) { - return GenericResult( - success: false, - data: timestamp, - code: result.code, - message: result.message, - ); - } - - timestamp = DateTime.now(); - - return GenericResult( - success: true, - data: timestamp, - ); - } - - @override - Future> powerOn(final int serverId) async { - DateTime? timestamp; - final result = await _adapter.api().powerOn(serverId); - if (!result.success) { - return GenericResult( - success: false, - data: timestamp, - code: result.code, - message: result.message, - ); - } - - timestamp = DateTime.now(); - - return GenericResult( - success: true, - data: timestamp, - ); - } - - String dnsProviderToInfectName(final DnsProviderType dnsProvider) { - String dnsProviderType; - switch (dnsProvider) { - case DnsProviderType.digitalOcean: - dnsProviderType = 'DIGITALOCEAN'; - break; - case DnsProviderType.desec: - dnsProviderType = 'DESEC'; - break; - case DnsProviderType.cloudflare: - default: - dnsProviderType = 'CLOUDFLARE'; - break; - } - return dnsProviderType; - } - @override Future> launchInstallation( final LaunchInstallationData installationData, @@ -454,8 +128,7 @@ class HetznerServerProvider extends ServerProvider { rootUser: installationData.rootUser, domainName: installationData.serverDomain.domainName, serverType: installationData.serverTypeId, - dnsProviderType: - dnsProviderToInfectName(installationData.dnsProviderType), + dnsProviderType: installationData.dnsProviderType.toInfectName(), hostName: hostname, volumeId: volume.id, base64Password: base64.encode( @@ -651,10 +324,175 @@ class HetznerServerProvider extends ServerProvider { } @override - Future> createVolume() async { - ServerVolume? volume; + Future> tryInitApiByToken(final String token) async { + final api = _adapter.api(getInitialized: false); + final result = await api.isApiTokenValid(token); + if (!result.data || !result.success) { + return result; + } - final result = await _adapter.api().createVolume(); + _adapter = ApiAdapter(region: api.region, isWithToken: true); + return result; + } + + String? getEmojiFlag(final String query) { + String? emoji; + + switch (query.toLowerCase()) { + case 'de': + emoji = '🇩🇪'; + break; + + case 'fi': + emoji = '🇫🇮'; + break; + + case 'us': + emoji = '🇺🇸'; + break; + } + + return emoji; + } + + @override + Future> trySetServerLocation( + final String location, + ) async { + final bool apiInitialized = _adapter.api().isWithToken; + if (!apiInitialized) { + return GenericResult( + success: true, + data: false, + message: 'Not authorized!', + ); + } + + _adapter = ApiAdapter( + isWithToken: true, + region: location, + ); + return success; + } + + @override + Future>> + getAvailableLocations() async { + final List locations = []; + final result = await _adapter.api().getAvailableLocations(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: locations, + code: result.code, + message: result.message, + ); + } + + final List rawLocations = result.data; + for (final rawLocation in rawLocations) { + ServerProviderLocation? location; + try { + location = ServerProviderLocation( + title: rawLocation.city, + description: rawLocation.description, + flag: getEmojiFlag(rawLocation.country), + identifier: rawLocation.name, + ); + } catch (e) { + continue; + } + locations.add(location); + } + + return GenericResult(success: true, data: locations); + } + + @override + Future>> getServerTypes({ + required final ServerProviderLocation location, + }) async { + final List types = []; + final result = await _adapter.api().getAvailableServerTypes(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: types, + code: result.code, + message: result.message, + ); + } + + final rawTypes = result.data; + for (final rawType in rawTypes) { + for (final rawPrice in rawType.prices) { + if (rawPrice.location == location.identifier) { + types.add( + ServerType( + title: rawType.description, + identifier: rawType.name, + ram: rawType.memory.toDouble(), + cores: rawType.cores, + disk: DiskSize(byte: rawType.disk * 1024 * 1024 * 1024), + price: Price( + value: rawPrice.monthly, + currency: currency, + ), + location: location, + ), + ); + } + } + } + + return GenericResult(success: true, data: types); + } + + @override + Future> powerOn(final int serverId) async { + DateTime? timestamp; + final result = await _adapter.api().powerOn(serverId); + if (!result.success) { + return GenericResult( + success: false, + data: timestamp, + code: result.code, + message: result.message, + ); + } + + timestamp = DateTime.now(); + + return GenericResult( + success: true, + data: timestamp, + ); + } + + @override + Future> restart(final int serverId) async { + DateTime? timestamp; + final result = await _adapter.api().restart(serverId); + if (!result.success) { + return GenericResult( + success: false, + data: timestamp, + code: result.code, + message: result.message, + ); + } + + timestamp = DateTime.now(); + + return GenericResult( + success: true, + data: timestamp, + ); + } + + @override + Future> getPricePerGb() async { + final result = await _adapter.api().getPricePerGb(); if (!result.success || result.data == null) { return GenericResult( @@ -665,28 +503,12 @@ class HetznerServerProvider extends ServerProvider { ); } - try { - volume = ServerVolume( - id: result.data!.id, - name: result.data!.name, - sizeByte: result.data!.size * 1024 * 1024 * 1024, - serverId: result.data!.serverId, - linuxDevice: result.data!.linuxDevice, - ); - } catch (e) { - print(e); - return GenericResult( - data: null, - success: false, - message: e.toString(), - ); - } - return GenericResult( - data: volume, success: true, - code: result.code, - message: result.message, + data: Price( + value: result.data!, + currency: currency, + ), ); } @@ -739,10 +561,66 @@ class HetznerServerProvider extends ServerProvider { ); } + @override + Future> createVolume() async { + ServerVolume? volume; + + final result = await _adapter.api().createVolume(); + + if (!result.success || result.data == null) { + return GenericResult( + data: null, + success: false, + message: result.message, + code: result.code, + ); + } + + try { + volume = ServerVolume( + id: result.data!.id, + name: result.data!.name, + sizeByte: result.data!.size * 1024 * 1024 * 1024, + serverId: result.data!.serverId, + linuxDevice: result.data!.linuxDevice, + ); + } catch (e) { + print(e); + return GenericResult( + data: null, + success: false, + message: e.toString(), + ); + } + + return GenericResult( + data: volume, + success: true, + code: result.code, + message: result.message, + ); + } + @override Future> deleteVolume(final ServerVolume volume) async => _adapter.api().deleteVolume(volume.id); + @override + Future> resizeVolume( + final ServerVolume volume, + final DiskSize size, + ) async => + _adapter.api().resizeVolume( + HetznerVolume( + volume.id, + volume.sizeByte, + volume.serverId, + volume.name, + volume.linuxDevice, + ), + size, + ); + @override Future> attachVolume( final ServerVolume volume, @@ -768,40 +646,143 @@ class HetznerServerProvider extends ServerProvider { ); @override - Future> resizeVolume( - final ServerVolume volume, - final DiskSize size, - ) async => - _adapter.api().resizeVolume( - HetznerVolume( - volume.id, - volume.sizeByte, - volume.serverId, - volume.name, - volume.linuxDevice, - ), - size, - ); - - @override - Future> getPricePerGb() async { - final result = await _adapter.api().getPricePerGb(); - - if (!result.success || result.data == null) { + Future>> getMetadata( + final int serverId, + ) async { + List metadata = []; + final result = await _adapter.api().getServers(); + if (result.data.isEmpty || !result.success) { return GenericResult( - data: null, success: false, - message: result.message, + data: metadata, code: result.code, + message: result.message, ); } - return GenericResult( - success: true, - data: Price( - value: result.data!, - currency: currency, + final List servers = result.data; + try { + final HetznerServerInfo server = servers.firstWhere( + (final server) => server.id == serverId, + ); + + metadata = [ + ServerMetadataEntity( + type: MetadataType.id, + trId: 'server.server_id', + value: server.id.toString(), + ), + ServerMetadataEntity( + type: MetadataType.status, + trId: 'server.status', + value: server.status.toString().split('.')[1].capitalize(), + ), + ServerMetadataEntity( + type: MetadataType.cpu, + trId: 'server.cpu', + value: server.serverType.cores.toString(), + ), + ServerMetadataEntity( + type: MetadataType.ram, + trId: 'server.ram', + value: '${server.serverType.memory.toString()} GB', + ), + ServerMetadataEntity( + type: MetadataType.cost, + trId: 'server.monthly_cost', + value: + '${server.serverType.prices[1].monthly.toStringAsFixed(2)} ${currency.shortcode}', + ), + ServerMetadataEntity( + type: MetadataType.location, + trId: 'server.location', + value: '${server.location.city}, ${server.location.country}', + ), + ServerMetadataEntity( + type: MetadataType.other, + trId: 'server.provider', + value: _adapter.api().displayProviderName, + ), + ]; + } catch (e) { + return GenericResult( + success: false, + data: [], + message: e.toString(), + ); + } + + return GenericResult(success: true, data: metadata); + } + + @override + Future> getMetrics( + final int serverId, + final DateTime start, + final DateTime end, + ) async { + ServerMetrics? metrics; + + List serializeTimeSeries( + final Map json, + final String type, + ) { + final List list = json['time_series'][type]['values']; + return list + .map((final el) => TimeSeriesData(el[0], double.parse(el[1]))) + .toList(); + } + + final cpuResult = await _adapter.api().getMetrics( + serverId, + start, + end, + 'cpu', + ); + + if (cpuResult.data.isEmpty || !cpuResult.success) { + return GenericResult( + success: false, + data: metrics, + code: cpuResult.code, + message: cpuResult.message, + ); + } + + final netResult = await _adapter.api().getMetrics( + serverId, + start, + end, + 'network', + ); + + if (cpuResult.data.isEmpty || !netResult.success) { + return GenericResult( + success: false, + data: metrics, + code: netResult.code, + message: netResult.message, + ); + } + + metrics = ServerMetrics( + cpu: serializeTimeSeries( + cpuResult.data, + 'cpu', ), + bandwidthIn: serializeTimeSeries( + netResult.data, + 'network.0.bandwidth.in', + ), + bandwidthOut: serializeTimeSeries( + netResult.data, + 'network.0.bandwidth.out', + ), + end: end, + start: start, + stepsInSecond: cpuResult.data['step'], ); + + return GenericResult(data: metrics, success: true); } } diff --git a/lib/logic/providers/server_providers/server_provider.dart b/lib/logic/providers/server_providers/server_provider.dart index b48662bf..f71ce09a 100644 --- a/lib/logic/providers/server_providers/server_provider.dart +++ b/lib/logic/providers/server_providers/server_provider.dart @@ -14,44 +14,124 @@ export 'package:selfprivacy/logic/api_maps/generic_result.dart'; export 'package:selfprivacy/logic/models/launch_installation_data.dart'; abstract class ServerProvider { + /// Returns an assigned enum value, respectively to which + /// provider implements [ServerProvider] interface. ServerProviderType get type; + + /// Returns [ServerBasicInfo] of all available machines + /// assigned to the authorized user. + /// + /// Only with public IPv4 addresses. Future>> getServers(); - Future> trySetServerLocation(final String location); + + /// Tries to launch installation of SelfPrivacy on + /// the requested server entry for the authorized account. + /// Depending on a server provider, the algorithm + /// and instructions vary drastically. + /// + /// If successly launched, stores new server information. + /// + /// If failure, returns error dialogue information to + /// write in ServerInstallationState. + Future> launchInstallation( + final LaunchInstallationData installationData, + ); + + /// Tries to delete the requested server entry + /// from the authorized account, including all assigned + /// storage extensions. + /// + /// If failure, returns error dialogue information to + /// write in ServerInstallationState. + Future> deleteServer( + final String hostname, + ); + + /// Tries to access an account linked to the provided token. + /// + /// To generate a token for your account follow instructions of your + /// server provider respectfully. + /// + /// If success, saves it for future usage. Future> tryInitApiByToken(final String token); + + /// Tries to assign the location shortcode for future usage. + /// + /// If API wasn't initialized with token by [tryInitApiByToken] beforehand, + /// returns 'Not authorized!' error. + Future> trySetServerLocation(final String location); + + /// Returns all available server locations + /// of the authorized user's server provider. Future>> getAvailableLocations(); + + /// Returns [ServerType] of all available + /// machine configurations available to the authorized account + /// within the requested provider location, + /// accessed from [getAvailableLocations]. Future>> getServerTypes({ required final ServerProviderLocation location, }); - Future> deleteServer( - final String hostname, - ); - Future> launchInstallation( - final LaunchInstallationData installationData, - ); + /// Tries to power on the requested accessible machine. + /// + /// If success, returns [DateTime] of when the server + /// answered the request. Future> powerOn(final int serverId); + + /// Tries to restart the requested accessible machine. + /// + /// If success, returns [DateTime] of when the server + /// answered the request. Future> restart(final int serverId); + + /// Returns [Price] information per one gigabyte of storage extension for + /// the requested accessible machine. + Future> getPricePerGb(); + + /// Returns [ServerVolume] of all available volumes + /// assigned to the authorized user and attached to active machine. + Future>> getVolumes({final String? status}); + + /// Tries to create an empty unattached [ServerVolume]. + /// + /// If success, returns this volume information. + Future> createVolume(); + + /// Tries to delete the requested accessible [ServerVolume]. + Future> deleteVolume(final ServerVolume volume); + + /// Tries to resize the requested accessible [ServerVolume] + /// to the provided size **(not by!)**, must be greater than current size. + Future> resizeVolume( + final ServerVolume volume, + final DiskSize size, + ); + + /// Tries to attach the requested accessible [ServerVolume] + /// to an accessible machine by the provided identificator. + Future> attachVolume( + final ServerVolume volume, + final int serverId, + ); + + /// Tries to attach the requested accessible [ServerVolume] + /// from any machine. + Future> detachVolume(final ServerVolume volume); + + /// Returns metedata of an accessible machine by the provided identificator + /// to show on ServerDetailsScreen. + Future>> getMetadata( + final int serverId, + ); + + /// Returns information about cpu and bandwidth load within the provided + /// time period of the requested accessible machine. Future> getMetrics( final int serverId, final DateTime start, final DateTime end, ); - Future> getPricePerGb(); - Future>> getVolumes({final String? status}); - Future> createVolume(); - Future> deleteVolume(final ServerVolume volume); - Future> resizeVolume( - final ServerVolume volume, - final DiskSize size, - ); - Future> attachVolume( - final ServerVolume volume, - final int serverId, - ); - Future> detachVolume(final ServerVolume volume); - Future>> getMetadata( - final int serverId, - ); GenericResult get success => GenericResult(success: true, data: true); }