mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-09-17 17:07:50 +00:00
NaiJi
bd33b8d679
Now it's 'false' when api token is invalid and null response if couldn't connect at all, to show different kinds of errors to the user
796 lines
22 KiB
Dart
796 lines
22 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:selfprivacy/config/get_it_config.dart';
|
|
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/volume_provider.dart';
|
|
import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart';
|
|
import 'package:selfprivacy/logic/api_maps/staging_options.dart';
|
|
import 'package:selfprivacy/logic/models/disk_size.dart';
|
|
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
|
|
import 'package:selfprivacy/logic/models/hive/server_details.dart';
|
|
import 'package:selfprivacy/logic/models/hive/user.dart';
|
|
import 'package:selfprivacy/logic/models/metrics.dart';
|
|
import 'package:selfprivacy/logic/models/price.dart';
|
|
import 'package:selfprivacy/logic/models/server_basic_info.dart';
|
|
import 'package:selfprivacy/logic/models/server_metadata.dart';
|
|
import 'package:selfprivacy/logic/models/server_provider_location.dart';
|
|
import 'package:selfprivacy/logic/models/server_type.dart';
|
|
import 'package:selfprivacy/utils/extensions/string_extensions.dart';
|
|
import 'package:selfprivacy/utils/password_generator.dart';
|
|
|
|
class DigitalOceanApi extends ServerProviderApi with VolumeProviderApi {
|
|
DigitalOceanApi({
|
|
required this.region,
|
|
this.hasLogger = false,
|
|
this.isWithToken = true,
|
|
});
|
|
@override
|
|
bool hasLogger;
|
|
@override
|
|
bool isWithToken;
|
|
|
|
final String? region;
|
|
|
|
@override
|
|
BaseOptions get options {
|
|
final BaseOptions options = BaseOptions(baseUrl: rootAddress);
|
|
if (isWithToken) {
|
|
final String? token = getIt<ApiConfigModel>().serverProviderKey;
|
|
assert(token != null);
|
|
options.headers = {'Authorization': 'Bearer $token'};
|
|
}
|
|
|
|
if (validateStatus != null) {
|
|
options.validateStatus = validateStatus!;
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
@override
|
|
String get rootAddress => 'https://api.digitalocean.com/v2';
|
|
|
|
@override
|
|
String get infectProviderName => 'digitalocean';
|
|
|
|
@override
|
|
String get displayProviderName => 'Digital Ocean';
|
|
|
|
@override
|
|
Future<APIGenericResult<bool>> isApiTokenValid(final String token) async {
|
|
bool isValid = false;
|
|
Response? response;
|
|
String message = '';
|
|
final Dio client = await getClient();
|
|
try {
|
|
response = await client.get(
|
|
'/account',
|
|
options: Options(
|
|
followRedirects: false,
|
|
validateStatus: (final status) =>
|
|
status != null && (status >= 200 || status == 401),
|
|
headers: {'Authorization': 'Bearer $token'},
|
|
),
|
|
);
|
|
} catch (e) {
|
|
print(e);
|
|
isValid = false;
|
|
message = e.toString();
|
|
} finally {
|
|
close(client);
|
|
}
|
|
|
|
if (response == null) {
|
|
return APIGenericResult(
|
|
data: isValid,
|
|
success: false,
|
|
message: message,
|
|
);
|
|
}
|
|
|
|
if (response.statusCode == HttpStatus.ok) {
|
|
isValid = true;
|
|
} else if (response.statusCode == HttpStatus.unauthorized) {
|
|
isValid = false;
|
|
} else {
|
|
throw Exception('code: ${response.statusCode}');
|
|
}
|
|
|
|
return APIGenericResult(
|
|
data: isValid,
|
|
success: true,
|
|
message: response.statusMessage,
|
|
);
|
|
}
|
|
|
|
/// Hardcoded on their documentation and there is no pricing API at all
|
|
/// Probably we should scrap the doc page manually
|
|
@override
|
|
Future<Price?> getPricePerGb() async => Price(
|
|
value: 0.10,
|
|
currency: 'USD',
|
|
);
|
|
|
|
@override
|
|
Future<ServerVolume?> createVolume() async {
|
|
ServerVolume? volume;
|
|
|
|
final Response createVolumeResponse;
|
|
final Dio client = await getClient();
|
|
try {
|
|
final List<ServerVolume> volumes = await getVolumes();
|
|
await Future.delayed(const Duration(seconds: 6));
|
|
|
|
createVolumeResponse = await client.post(
|
|
'/volumes',
|
|
data: {
|
|
'size_gigabytes': 10,
|
|
'name': 'volume${StringGenerators.storageName()}',
|
|
'labels': {'labelkey': 'value'},
|
|
'region': region,
|
|
'filesystem_type': 'ext4',
|
|
},
|
|
);
|
|
final volumeId = createVolumeResponse.data['volume']['id'];
|
|
final volumeSize = createVolumeResponse.data['volume']['size_gigabytes'];
|
|
final volumeName = createVolumeResponse.data['volume']['name'];
|
|
volume = ServerVolume(
|
|
id: volumes.length,
|
|
name: volumeName,
|
|
sizeByte: volumeSize,
|
|
serverId: null,
|
|
linuxDevice: '/dev/disk/by-id/scsi-0DO_Volume_$volumeName',
|
|
uuid: volumeId,
|
|
);
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
|
|
return volume;
|
|
}
|
|
|
|
@override
|
|
Future<List<ServerVolume>> getVolumes({final String? status}) async {
|
|
final List<ServerVolume> volumes = [];
|
|
|
|
final Response getVolumesResponse;
|
|
final Dio client = await getClient();
|
|
try {
|
|
getVolumesResponse = await client.get(
|
|
'/volumes',
|
|
queryParameters: {
|
|
'status': status,
|
|
},
|
|
);
|
|
final List<dynamic> rawVolumes = getVolumesResponse.data['volumes'];
|
|
int id = 0;
|
|
for (final rawVolume in rawVolumes) {
|
|
final volumeId = rawVolume['id'];
|
|
final int volumeSize = rawVolume['size_gigabytes'] * 1024 * 1024 * 1024;
|
|
final volumeDropletIds = rawVolume['droplet_ids'];
|
|
final String volumeName = rawVolume['name'];
|
|
final volume = ServerVolume(
|
|
id: id++,
|
|
name: volumeName,
|
|
sizeByte: volumeSize,
|
|
serverId: volumeDropletIds.isNotEmpty ? volumeDropletIds[0] : null,
|
|
linuxDevice: 'scsi-0DO_Volume_$volumeName',
|
|
uuid: volumeId,
|
|
);
|
|
volumes.add(volume);
|
|
}
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
|
|
return volumes;
|
|
}
|
|
|
|
Future<ServerVolume?> getVolume(final String volumeUuid) async {
|
|
ServerVolume? requestedVolume;
|
|
|
|
final List<ServerVolume> volumes = await getVolumes();
|
|
|
|
for (final volume in volumes) {
|
|
if (volume.uuid == volumeUuid) {
|
|
requestedVolume = volume;
|
|
}
|
|
}
|
|
|
|
return requestedVolume;
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteVolume(final ServerVolume volume) async {
|
|
final Dio client = await getClient();
|
|
try {
|
|
await client.delete('/volumes/${volume.uuid}');
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> attachVolume(
|
|
final ServerVolume volume,
|
|
final int serverId,
|
|
) async {
|
|
bool success = false;
|
|
|
|
final Response attachVolumeResponse;
|
|
final Dio client = await getClient();
|
|
try {
|
|
attachVolumeResponse = await client.post(
|
|
'/volumes/actions',
|
|
data: {
|
|
'type': 'attach',
|
|
'volume_name': volume.name,
|
|
'region': region,
|
|
'droplet_id': serverId,
|
|
},
|
|
);
|
|
success =
|
|
attachVolumeResponse.data['action']['status'].toString() != 'error';
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
close(client);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
@override
|
|
Future<bool> detachVolume(final ServerVolume volume) async {
|
|
bool success = false;
|
|
|
|
final Response detachVolumeResponse;
|
|
final Dio client = await getClient();
|
|
try {
|
|
detachVolumeResponse = await client.post(
|
|
'/volumes/actions',
|
|
data: {
|
|
'type': 'detach',
|
|
'volume_name': volume.name,
|
|
'droplet_id': volume.serverId,
|
|
'region': region,
|
|
},
|
|
);
|
|
success =
|
|
detachVolumeResponse.data['action']['status'].toString() != 'error';
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
@override
|
|
Future<bool> resizeVolume(
|
|
final ServerVolume volume,
|
|
final DiskSize size,
|
|
) async {
|
|
bool success = false;
|
|
|
|
final Response resizeVolumeResponse;
|
|
final Dio client = await getClient();
|
|
try {
|
|
resizeVolumeResponse = await client.post(
|
|
'/volumes/actions',
|
|
data: {
|
|
'type': 'resize',
|
|
'volume_name': volume.name,
|
|
'size_gigabytes': size.gibibyte,
|
|
'region': region,
|
|
},
|
|
);
|
|
success =
|
|
resizeVolumeResponse.data['action']['status'].toString() != 'error';
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
static String getHostnameFromDomain(final String domain) {
|
|
// Replace all non-alphanumeric characters with an underscore
|
|
String hostname =
|
|
domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
|
|
if (hostname.endsWith('-')) {
|
|
hostname = hostname.substring(0, hostname.length - 1);
|
|
}
|
|
if (hostname.startsWith('-')) {
|
|
hostname = hostname.substring(1);
|
|
}
|
|
if (hostname.isEmpty) {
|
|
hostname = 'selfprivacy-server';
|
|
}
|
|
|
|
return hostname;
|
|
}
|
|
|
|
@override
|
|
Future<ServerHostingDetails?> createServer({
|
|
required final String dnsApiToken,
|
|
required final User rootUser,
|
|
required final String domainName,
|
|
required final String serverType,
|
|
}) async {
|
|
ServerHostingDetails? serverDetails;
|
|
|
|
final String dbPassword = StringGenerators.dbPassword();
|
|
final String apiToken = StringGenerators.apiToken();
|
|
|
|
final String base64Password =
|
|
base64.encode(utf8.encode(rootUser.password ?? 'PASS'));
|
|
|
|
final String formattedHostname = getHostnameFromDomain(domainName);
|
|
const String infectBranch = 'providers/digital-ocean';
|
|
final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false';
|
|
|
|
final String userdataString =
|
|
"#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/$infectBranch/nixos-infect | PROVIDER=$infectProviderName STAGING_ACME='$stagingAcme' DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$dnsApiToken DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$formattedHostname bash 2>&1 | tee /tmp/infect.log";
|
|
print(userdataString);
|
|
|
|
final Dio client = await getClient();
|
|
try {
|
|
final Map<String, Object> data = {
|
|
'name': formattedHostname,
|
|
'size': serverType,
|
|
'image': 'ubuntu-20-04-x64',
|
|
'user_data': userdataString,
|
|
'region': region!,
|
|
};
|
|
print('Decoded data: $data');
|
|
|
|
final Response serverCreateResponse = await client.post(
|
|
'/droplets',
|
|
data: data,
|
|
);
|
|
|
|
final int serverId = serverCreateResponse.data['droplet']['id'];
|
|
final ServerVolume? newVolume = await createVolume();
|
|
final bool attachedVolume = await attachVolume(newVolume!, serverId);
|
|
|
|
String? ipv4;
|
|
int attempts = 0;
|
|
while (attempts < 5 && ipv4 == null) {
|
|
await Future.delayed(const Duration(seconds: 20));
|
|
final List<ServerBasicInfo> servers = await getServers();
|
|
for (final server in servers) {
|
|
if (server.name == formattedHostname && server.ip != '0.0.0.0') {
|
|
ipv4 = server.ip;
|
|
break;
|
|
}
|
|
}
|
|
++attempts;
|
|
}
|
|
|
|
if (attachedVolume && ipv4 != null) {
|
|
serverDetails = ServerHostingDetails(
|
|
id: serverId,
|
|
ip4: ipv4,
|
|
createTime: DateTime.now(),
|
|
volume: newVolume,
|
|
apiToken: apiToken,
|
|
provider: ServerProvider.digitalOcean,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
close(client);
|
|
}
|
|
|
|
return serverDetails;
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteServer({
|
|
required final String domainName,
|
|
}) async {
|
|
final Dio client = await getClient();
|
|
|
|
final ServerBasicInfo serverToRemove = (await getServers()).firstWhere(
|
|
(final el) => el.name == domainName,
|
|
);
|
|
final ServerVolume volumeToRemove = (await getVolumes()).firstWhere(
|
|
(final el) => el.serverId == serverToRemove.id,
|
|
);
|
|
final List<Future> laterFutures = <Future>[];
|
|
|
|
await detachVolume(volumeToRemove);
|
|
await Future.delayed(const Duration(seconds: 10));
|
|
|
|
try {
|
|
laterFutures.add(deleteVolume(volumeToRemove));
|
|
laterFutures.add(client.delete('/droplets/$serverToRemove.id'));
|
|
await Future.wait(laterFutures);
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
close(client);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<ServerHostingDetails> restart() async {
|
|
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
|
|
|
|
final Dio client = await getClient();
|
|
try {
|
|
await client.post(
|
|
'/droplets/${server.id}/actions',
|
|
data: {
|
|
'type': 'reboot',
|
|
},
|
|
);
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
close(client);
|
|
}
|
|
|
|
return server.copyWith(startTime: DateTime.now());
|
|
}
|
|
|
|
@override
|
|
Future<ServerHostingDetails> powerOn() async {
|
|
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
|
|
|
|
final Dio client = await getClient();
|
|
try {
|
|
await client.post(
|
|
'/droplets/${server.id}/actions',
|
|
data: {
|
|
'type': 'power_on',
|
|
},
|
|
);
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
close(client);
|
|
}
|
|
|
|
return server.copyWith(startTime: DateTime.now());
|
|
}
|
|
|
|
/// Digital Ocean returns a map of lists of /proc/stat values,
|
|
/// so here we are trying to implement average CPU
|
|
/// load calculation for each point in time on a given interval.
|
|
///
|
|
/// For each point of time:
|
|
///
|
|
/// `Average Load = 100 * (1 - (Idle Load / Total Load))`
|
|
///
|
|
/// For more info please proceed to read:
|
|
/// https://rosettacode.org/wiki/Linux_CPU_utilization
|
|
List<TimeSeriesData> calculateCpuLoadMetrics(final List rawProcStatMetrics) {
|
|
final List<TimeSeriesData> cpuLoads = [];
|
|
|
|
final int pointsInTime = (rawProcStatMetrics[0]['values'] as List).length;
|
|
for (int i = 0; i < pointsInTime; ++i) {
|
|
double currentMetricLoad = 0.0;
|
|
double? currentMetricIdle;
|
|
for (final rawProcStat in rawProcStatMetrics) {
|
|
final String rawProcValue = rawProcStat['values'][i][1];
|
|
// Converting MBit into bit
|
|
final double procValue = double.parse(rawProcValue) * 1000000;
|
|
currentMetricLoad += procValue;
|
|
if (currentMetricIdle == null &&
|
|
rawProcStat['metric']['mode'] == 'idle') {
|
|
currentMetricIdle = procValue;
|
|
}
|
|
}
|
|
currentMetricIdle ??= 0.0;
|
|
currentMetricLoad = 100.0 * (1 - (currentMetricIdle / currentMetricLoad));
|
|
cpuLoads.add(
|
|
TimeSeriesData(
|
|
rawProcStatMetrics[0]['values'][i][0],
|
|
currentMetricLoad,
|
|
),
|
|
);
|
|
}
|
|
|
|
return cpuLoads;
|
|
}
|
|
|
|
@override
|
|
Future<ServerMetrics?> getMetrics(
|
|
final int serverId,
|
|
final DateTime start,
|
|
final DateTime end,
|
|
) async {
|
|
ServerMetrics? metrics;
|
|
|
|
const int step = 15;
|
|
final Dio client = await getClient();
|
|
try {
|
|
Response response = await client.get(
|
|
'/monitoring/metrics/droplet/bandwidth',
|
|
queryParameters: {
|
|
'start': '${(start.microsecondsSinceEpoch / 1000000).round()}',
|
|
'end': '${(end.microsecondsSinceEpoch / 1000000).round()}',
|
|
'host_id': '$serverId',
|
|
'interface': 'public',
|
|
'direction': 'inbound',
|
|
},
|
|
);
|
|
|
|
final List inbound = response.data['data']['result'][0]['values'];
|
|
|
|
response = await client.get(
|
|
'/monitoring/metrics/droplet/bandwidth',
|
|
queryParameters: {
|
|
'start': '${(start.microsecondsSinceEpoch / 1000000).round()}',
|
|
'end': '${(end.microsecondsSinceEpoch / 1000000).round()}',
|
|
'host_id': '$serverId',
|
|
'interface': 'public',
|
|
'direction': 'outbound',
|
|
},
|
|
);
|
|
|
|
final List outbound = response.data['data']['result'][0]['values'];
|
|
|
|
response = await client.get(
|
|
'/monitoring/metrics/droplet/cpu',
|
|
queryParameters: {
|
|
'start': '${(start.microsecondsSinceEpoch / 1000000).round()}',
|
|
'end': '${(end.microsecondsSinceEpoch / 1000000).round()}',
|
|
'host_id': '$serverId',
|
|
},
|
|
);
|
|
|
|
metrics = ServerMetrics(
|
|
bandwidthIn: inbound
|
|
.map(
|
|
(final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000),
|
|
)
|
|
.toList(),
|
|
bandwidthOut: outbound
|
|
.map(
|
|
(final el) => TimeSeriesData(el[0], double.parse(el[1]) * 100000),
|
|
)
|
|
.toList(),
|
|
cpu: calculateCpuLoadMetrics(response.data['data']['result']),
|
|
start: start,
|
|
end: end,
|
|
stepsInSecond: step,
|
|
);
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
close(client);
|
|
}
|
|
|
|
return metrics;
|
|
}
|
|
|
|
@override
|
|
Future<List<ServerMetadataEntity>> getMetadata(final int serverId) async {
|
|
List<ServerMetadataEntity> metadata = [];
|
|
|
|
final Dio client = await getClient();
|
|
try {
|
|
final Response response = await client.get('/droplets/$serverId');
|
|
final droplet = response.data!['droplet'];
|
|
metadata = [
|
|
ServerMetadataEntity(
|
|
type: MetadataType.id,
|
|
name: 'server.server_id'.tr(),
|
|
value: droplet['id'].toString(),
|
|
),
|
|
ServerMetadataEntity(
|
|
type: MetadataType.status,
|
|
name: 'server.status'.tr(),
|
|
value: droplet['status'].toString().capitalize(),
|
|
),
|
|
ServerMetadataEntity(
|
|
type: MetadataType.cpu,
|
|
name: 'server.cpu'.tr(),
|
|
value: 'server.core_count'.plural(droplet['vcpus']),
|
|
),
|
|
ServerMetadataEntity(
|
|
type: MetadataType.ram,
|
|
name: 'server.ram'.tr(),
|
|
value: "${droplet['memory'].toString()} MB",
|
|
),
|
|
ServerMetadataEntity(
|
|
type: MetadataType.cost,
|
|
name: 'server.monthly_cost'.tr(),
|
|
value: droplet['size']['price_monthly'].toString(),
|
|
),
|
|
ServerMetadataEntity(
|
|
type: MetadataType.location,
|
|
name: 'server.location'.tr(),
|
|
value:
|
|
'${droplet['region']['name']} ${getEmojiFlag(droplet['region']['slug'].toString()) ?? ''}',
|
|
),
|
|
ServerMetadataEntity(
|
|
type: MetadataType.other,
|
|
name: 'server.provider'.tr(),
|
|
value: displayProviderName,
|
|
),
|
|
];
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
close(client);
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
|
|
@override
|
|
Future<List<ServerBasicInfo>> getServers() async {
|
|
List<ServerBasicInfo> servers = [];
|
|
|
|
final Dio client = await getClient();
|
|
try {
|
|
final Response response = await client.get('/droplets');
|
|
servers = response.data!['droplets'].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();
|
|
}
|
|
}
|
|
}
|
|
|
|
return ServerBasicInfo(
|
|
id: server['id'],
|
|
reverseDns: server['name'],
|
|
created: DateTime.now(),
|
|
ip: ipv4,
|
|
name: server['name'],
|
|
);
|
|
},
|
|
).toList();
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
close(client);
|
|
}
|
|
|
|
print(servers);
|
|
return servers;
|
|
}
|
|
|
|
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<List<ServerProviderLocation>> getAvailableLocations() async {
|
|
List<ServerProviderLocation> locations = [];
|
|
|
|
final Dio client = await getClient();
|
|
try {
|
|
final Response response = await client.get(
|
|
'/regions',
|
|
);
|
|
|
|
locations = response.data!['regions']
|
|
.map<ServerProviderLocation>(
|
|
(final location) => ServerProviderLocation(
|
|
title: location['slug'],
|
|
description: location['name'],
|
|
flag: getEmojiFlag(location['slug']),
|
|
identifier: location['slug'],
|
|
),
|
|
)
|
|
.toList();
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
close(client);
|
|
}
|
|
|
|
return locations;
|
|
}
|
|
|
|
@override
|
|
Future<List<ServerType>> getServerTypesByLocation({
|
|
required final ServerProviderLocation location,
|
|
}) async {
|
|
final List<ServerType> types = [];
|
|
|
|
final Dio client = await getClient();
|
|
try {
|
|
final Response response = await client.get(
|
|
'/sizes',
|
|
);
|
|
final rawSizes = response.data!['sizes'];
|
|
for (final rawSize in rawSizes) {
|
|
for (final rawRegion in rawSize['regions']) {
|
|
if (rawRegion.toString() == location.identifier) {
|
|
types.add(
|
|
ServerType(
|
|
title: rawSize['description'],
|
|
identifier: rawSize['slug'],
|
|
ram: rawSize['memory'].toDouble(),
|
|
cores: rawSize['vcpus'],
|
|
disk: DiskSize(byte: rawSize['disk'] * 1024 * 1024 * 1024),
|
|
price: Price(
|
|
value: rawSize['price_monthly'],
|
|
currency: 'USD',
|
|
),
|
|
location: location,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print(e);
|
|
} finally {
|
|
close(client);
|
|
}
|
|
|
|
return types;
|
|
}
|
|
|
|
@override
|
|
Future<void> createReverseDns({
|
|
required final ServerHostingDetails serverDetails,
|
|
required final ServerDomain domain,
|
|
}) async {
|
|
/// TODO remove from provider interface
|
|
}
|
|
|
|
@override
|
|
ProviderApiTokenValidation getApiTokenValidation() =>
|
|
ProviderApiTokenValidation(
|
|
regexp: RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'),
|
|
length: 71,
|
|
);
|
|
}
|