refactor(server-provider): Rearrange Server Provider interface

- Move all implement functions accordingly to their position in interface
- Get rid of duplicate toInfect() functions, move them to ServerDomain
pull/227/head
NaiJi ✨ 2023-06-26 14:15:53 -03:00
parent b8009cde71
commit a56f525060
4 changed files with 831 additions and 779 deletions

View File

@ -47,4 +47,11 @@ enum DnsProviderType {
return unknown;
}
}
String toInfectName() => switch (this) {
digitalOcean => 'DIGITALOCEAN',
cloudflare => 'CLOUDFLARE',
desec => 'DESEC',
unknown => 'UNKNOWN',
};
}

View File

@ -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<GenericResult<bool>> trySetServerLocation(
final String location,
) async {
final bool apiInitialized = _adapter.api().isWithToken;
if (!apiInitialized) {
Future<GenericResult<List<ServerBasicInfo>>> getServers() async {
List<ServerBasicInfo> 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<ServerBasicInfo>(
(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<GenericResult<bool>> 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<GenericResult<CallbackDialogueBranching?>> 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<Future> laterFutures = <Future>[];
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<GenericResult<bool>> 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<GenericResult<bool>> 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<GenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
@ -318,43 +404,214 @@ class DigitalOceanServerProvider extends ServerProvider {
}
@override
Future<GenericResult<List<ServerBasicInfo>>> getServers() async {
List<ServerBasicInfo> servers = [];
final result = await _adapter.api().getServers();
if (result.data.isEmpty || !result.success) {
Future<GenericResult<DateTime?>> 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<ServerBasicInfo>(
(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<GenericResult<DateTime?>> 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<GenericResult<Price?>> getPricePerGb() async => GenericResult(
success: true,
data: Price(
value: 0.10,
currency: currency,
),
);
@override
Future<GenericResult<List<ServerVolume>>> getVolumes({
final String? status,
}) async {
final List<ServerVolume> 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<GenericResult<ServerVolume?>> 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<GenericResult<ServerVolume?>> 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<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
) async =>
_adapter.api().attachVolume(
volume.name,
serverId,
);
@override
Future<GenericResult<bool>> detachVolume(
final ServerVolume volume,
) async =>
_adapter.api().detachVolume(
volume.name,
volume.serverId!,
);
@override
Future<GenericResult<void>> deleteVolume(
final ServerVolume volume,
) async =>
_adapter.api().deleteVolume(
volume.uuid!,
);
@override
Future<GenericResult<bool>> resizeVolume(
final ServerVolume volume,
final DiskSize size,
) async =>
_adapter.api().resizeVolume(
volume.name,
size,
);
@override
Future<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
@ -536,277 +793,4 @@ class DigitalOceanServerProvider extends ServerProvider {
return GenericResult(success: true, data: metrics);
}
@override
Future<GenericResult<DateTime?>> 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<GenericResult<CallbackDialogueBranching?>> 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<Future> laterFutures = <Future>[];
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<GenericResult<List<ServerVolume>>> getVolumes({
final String? status,
}) async {
final List<ServerVolume> 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<GenericResult<ServerVolume?>> 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<GenericResult<ServerVolume?>> 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<GenericResult<void>> deleteVolume(
final ServerVolume volume,
) async =>
_adapter.api().deleteVolume(
volume.uuid!,
);
@override
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
) async =>
_adapter.api().attachVolume(
volume.name,
serverId,
);
@override
Future<GenericResult<bool>> detachVolume(
final ServerVolume volume,
) async =>
_adapter.api().detachVolume(
volume.name,
volume.serverId!,
);
@override
Future<GenericResult<bool>> 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<GenericResult<Price?>> getPricePerGb() async => GenericResult(
success: true,
data: Price(
value: 0.10,
currency: currency,
),
);
@override
Future<GenericResult<DateTime?>> 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,
);
}
}

View File

@ -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<GenericResult<bool>> 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<GenericResult<bool>> 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<GenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
final List<ServerProviderLocation> 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<HetznerLocation> 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<GenericResult<List<ServerType>>> getServerTypes({
required final ServerProviderLocation location,
}) async {
final List<ServerType> 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<GenericResult<List<ServerBasicInfo>>> getServers() async {
final List<ServerBasicInfo> servers = [];
@ -214,206 +88,6 @@ class HetznerServerProvider extends ServerProvider {
return GenericResult(success: true, data: servers);
}
@override
Future<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
) async {
List<ServerMetadataEntity> 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<HetznerServerInfo> 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<GenericResult<ServerMetrics?>> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
) async {
ServerMetrics? metrics;
List<TimeSeriesData> serializeTimeSeries(
final Map<String, dynamic> 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<GenericResult<DateTime?>> 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<GenericResult<DateTime?>> 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<GenericResult<CallbackDialogueBranching?>> 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<GenericResult<ServerVolume?>> createVolume() async {
ServerVolume? volume;
Future<GenericResult<bool>> 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<GenericResult<bool>> 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<GenericResult<List<ServerProviderLocation>>>
getAvailableLocations() async {
final List<ServerProviderLocation> 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<HetznerLocation> 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<GenericResult<List<ServerType>>> getServerTypes({
required final ServerProviderLocation location,
}) async {
final List<ServerType> 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<GenericResult<DateTime?>> 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<GenericResult<DateTime?>> 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<GenericResult<Price?>> 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<GenericResult<ServerVolume?>> 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<GenericResult<void>> deleteVolume(final ServerVolume volume) async =>
_adapter.api().deleteVolume(volume.id);
@override
Future<GenericResult<bool>> 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<GenericResult<bool>> attachVolume(
final ServerVolume volume,
@ -768,40 +646,143 @@ class HetznerServerProvider extends ServerProvider {
);
@override
Future<GenericResult<bool>> 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<GenericResult<Price?>> getPricePerGb() async {
final result = await _adapter.api().getPricePerGb();
if (!result.success || result.data == null) {
Future<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
) async {
List<ServerMetadataEntity> 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<HetznerServerInfo> 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<GenericResult<ServerMetrics?>> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
) async {
ServerMetrics? metrics;
List<TimeSeriesData> serializeTimeSeries(
final Map<String, dynamic> 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);
}
}

View File

@ -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<GenericResult<List<ServerBasicInfo>>> getServers();
Future<GenericResult<bool>> 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<GenericResult<CallbackDialogueBranching?>> 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<GenericResult<CallbackDialogueBranching?>> 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<GenericResult<bool>> 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<GenericResult<bool>> trySetServerLocation(final String location);
/// Returns all available server locations
/// of the authorized user's server provider.
Future<GenericResult<List<ServerProviderLocation>>> getAvailableLocations();
/// Returns [ServerType] of all available
/// machine configurations available to the authorized account
/// within the requested provider location,
/// accessed from [getAvailableLocations].
Future<GenericResult<List<ServerType>>> getServerTypes({
required final ServerProviderLocation location,
});
Future<GenericResult<CallbackDialogueBranching?>> deleteServer(
final String hostname,
);
Future<GenericResult<CallbackDialogueBranching?>> launchInstallation(
final LaunchInstallationData installationData,
);
/// Tries to power on the requested accessible machine.
///
/// If success, returns [DateTime] of when the server
/// answered the request.
Future<GenericResult<DateTime?>> powerOn(final int serverId);
/// Tries to restart the requested accessible machine.
///
/// If success, returns [DateTime] of when the server
/// answered the request.
Future<GenericResult<DateTime?>> restart(final int serverId);
/// Returns [Price] information per one gigabyte of storage extension for
/// the requested accessible machine.
Future<GenericResult<Price?>> getPricePerGb();
/// Returns [ServerVolume] of all available volumes
/// assigned to the authorized user and attached to active machine.
Future<GenericResult<List<ServerVolume>>> getVolumes({final String? status});
/// Tries to create an empty unattached [ServerVolume].
///
/// If success, returns this volume information.
Future<GenericResult<ServerVolume?>> createVolume();
/// Tries to delete the requested accessible [ServerVolume].
Future<GenericResult<void>> deleteVolume(final ServerVolume volume);
/// Tries to resize the requested accessible [ServerVolume]
/// to the provided size **(not by!)**, must be greater than current size.
Future<GenericResult<bool>> resizeVolume(
final ServerVolume volume,
final DiskSize size,
);
/// Tries to attach the requested accessible [ServerVolume]
/// to an accessible machine by the provided identificator.
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
);
/// Tries to attach the requested accessible [ServerVolume]
/// from any machine.
Future<GenericResult<bool>> detachVolume(final ServerVolume volume);
/// Returns metedata of an accessible machine by the provided identificator
/// to show on ServerDetailsScreen.
Future<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
);
/// Returns information about cpu and bandwidth load within the provided
/// time period of the requested accessible machine.
Future<GenericResult<ServerMetrics?>> getMetrics(
final int serverId,
final DateTime start,
final DateTime end,
);
Future<GenericResult<Price?>> getPricePerGb();
Future<GenericResult<List<ServerVolume>>> getVolumes({final String? status});
Future<GenericResult<ServerVolume?>> createVolume();
Future<GenericResult<void>> deleteVolume(final ServerVolume volume);
Future<GenericResult<bool>> resizeVolume(
final ServerVolume volume,
final DiskSize size,
);
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final int serverId,
);
Future<GenericResult<bool>> detachVolume(final ServerVolume volume);
Future<GenericResult<List<ServerMetadataEntity>>> getMetadata(
final int serverId,
);
GenericResult<bool> get success => GenericResult(success: true, data: true);
}