nc-photos/app/lib/api/api_util.dart

244 lines
6.9 KiB
Dart
Raw Normal View History

2021-04-10 06:28:12 +02:00
/// Helper functions working with remote Nextcloud server
import 'dart:convert';
import 'package:flutter/foundation.dart';
2021-04-10 06:28:12 +02:00
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
2021-08-01 22:46:16 +02:00
import 'package:nc_photos/entity/file_util.dart' as file_util;
2021-05-06 06:57:20 +02:00
import 'package:nc_photos/exception.dart';
2022-12-08 16:39:13 +01:00
import 'package:to_string/to_string.dart';
part 'api_util.g.dart';
2021-04-10 06:28:12 +02:00
2021-10-08 20:39:53 +02:00
/// Characters that are not allowed in filename
const reservedFilenameChars = "<>:\"/\\|?*";
2021-04-10 06:28:12 +02:00
/// Return the preview image URL for [file]. See [getFilePreviewUrlRelative]
String getFilePreviewUrl(
Account account,
FileDescriptor file, {
2021-07-23 22:05:57 +02:00
required int width,
required int height,
String? mode,
bool? a,
2021-04-10 06:28:12 +02:00
}) {
return "${account.url}/"
2021-08-01 22:46:16 +02:00
"${getFilePreviewUrlRelative(account, file, width: width, height: height, mode: mode, a: a)}";
2021-04-10 06:28:12 +02:00
}
/// Return the relative preview image URL for [file]. If [a] == true, the
/// preview will maintain the original aspect ratio, otherwise it will be
/// cropped
String getFilePreviewUrlRelative(
2021-08-01 22:46:16 +02:00
Account account,
FileDescriptor file, {
2021-07-23 22:05:57 +02:00
required int width,
required int height,
String? mode,
bool? a,
2021-04-10 06:28:12 +02:00
}) {
String url;
2021-08-01 22:46:16 +02:00
if (file_util.isTrash(account, file)) {
// trashbin does not support preview.png endpoint
url = "index.php/apps/files_trashbin/preview?fileId=${file.fdId}";
} else {
url = "index.php/core/preview?fileId=${file.fdId}";
}
2021-08-01 22:46:16 +02:00
url = "$url&x=$width&y=$height";
2021-04-10 06:28:12 +02:00
if (mode != null) {
url = "$url&mode=$mode";
2022-08-28 17:35:29 +02:00
}
if (a != null) {
url = "$url&a=${a ? 1 : 0}";
}
return url;
}
/// Return the preview image URL for [fileId]. See [getFilePreviewUrlRelative]
String getFilePreviewUrlByFileId(
Account account,
int fileId, {
required int width,
required int height,
String? mode,
bool? a,
}) {
String url = "${account.url}/index.php/core/preview?fileId=$fileId";
url = "$url&x=$width&y=$height";
if (mode != null) {
url = "$url&mode=$mode";
2021-04-10 06:28:12 +02:00
}
if (a != null) {
url = "$url&a=${a ? 1 : 0}";
}
return url;
}
String getFileUrl(Account account, FileDescriptor file) {
2021-04-10 06:28:12 +02:00
return "${account.url}/${getFileUrlRelative(file)}";
}
String getFileUrlRelative(FileDescriptor file) {
return file.fdPath;
2021-04-10 06:28:12 +02:00
}
String getWebdavRootUrlRelative(Account account) =>
"remote.php/dav/files/${account.userId}";
2021-04-10 06:28:12 +02:00
2021-08-01 22:46:16 +02:00
String getTrashbinPath(Account account) =>
"remote.php/dav/trashbin/${account.userId}/trash";
2021-08-01 22:46:16 +02:00
/// Return the face image URL. See [getFacePreviewUrlRelative]
String getFacePreviewUrl(
Account account,
2021-09-10 19:10:26 +02:00
int faceId, {
required int size,
}) {
return "${account.url}/"
2021-09-10 19:10:26 +02:00
"${getFacePreviewUrlRelative(account, faceId, size: size)}";
}
/// Return the relative URL of the face image
String getFacePreviewUrlRelative(
Account account,
2021-09-10 19:10:26 +02:00
int faceId, {
required int size,
}) {
2021-09-10 19:10:26 +02:00
return "index.php/apps/facerecognition/face/$faceId/thumb/$size";
}
/// Initiate a login with Nextclouds login flow v2: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
Future<InitiateLoginResponse> initiateLogin(Uri uri) async {
final response = await Api.fromBaseUrl(uri).request(
"POST",
"index.php/login/v2",
2021-04-10 06:28:12 +02:00
header: {
"User-Agent": "nc-photos",
2021-04-10 06:28:12 +02:00
},
);
if (response.isGood) {
return InitiateLoginResponse.fromJsonString(response.body);
2021-04-10 06:28:12 +02:00
} else {
_log.severe(
"[initiateLogin] Failed while requesting app password: $response");
throw ApiException(
response: response,
message: "Server responded with an error: HTTP ${response.statusCode}");
2021-04-10 06:28:12 +02:00
}
}
/// Retrieve App Password after successful initiation of login with Nextclouds login flow v2: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
Future<AppPasswordResponse> _getAppPassword(
InitiateLoginPollOptions options) async {
Uri baseUrl;
if (options.endpoint.scheme == "http") {
baseUrl = Uri.http(options.endpoint.authority);
} else {
baseUrl = Uri.https(options.endpoint.authority);
}
final response = await Api.fromBaseUrl(baseUrl).request(
"POST", options.endpoint.pathSegments.join("/"),
header: {
"User-Agent": "nc-photos",
"Content-Type": "application/x-www-form-urlencoded"
},
body: "token=${options.token}");
if (response.statusCode == 200) {
return AppPasswordSuccess.fromJsonString(response.body);
} else if (response.statusCode == 404) {
return AppPasswordPending();
} else {
_log.severe(
"[getAppPassword] Failed while requesting app password: $response");
throw ApiException(
response: response,
message: "Server responded with an error: HTTP ${response.statusCode}");
}
}
/// Polls the app password endpoint every 5 seconds as lang as the token is valid (currently fixed to 20 min)
/// See https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
Stream<Future<AppPasswordResponse>> pollAppPassword(
InitiateLoginPollOptions options) {
return Stream.periodic(
const Duration(seconds: 5), (_) => _getAppPassword(options))
.takeWhile((_) => options.isTokenValid());
}
2022-12-08 16:39:13 +01:00
@toString
class InitiateLoginResponse {
2022-12-08 13:56:46 +01:00
const InitiateLoginResponse({
required this.poll,
required this.login,
});
factory InitiateLoginResponse.fromJsonString(String jsonString) {
final json = jsonDecode(jsonString);
2022-12-08 13:56:46 +01:00
return InitiateLoginResponse(
poll: InitiateLoginPollOptions(
json['poll']['token'], json['poll']['endpoint']),
login: json['login'],
);
}
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
2022-12-08 13:56:46 +01:00
final InitiateLoginPollOptions poll;
final String login;
}
2022-12-08 16:39:13 +01:00
@toString
class InitiateLoginPollOptions {
InitiateLoginPollOptions(this.token, String endpoint)
: endpoint = Uri.parse(endpoint),
_validUntil = DateTime.now().add(const Duration(minutes: 20));
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
bool isTokenValid() {
return DateTime.now().isBefore(_validUntil);
}
2022-12-08 16:39:13 +01:00
@Format(r"${kDebugMode ? $? : '***'}")
final String token;
final Uri endpoint;
final DateTime _validUntil;
}
abstract class AppPasswordResponse {}
2022-12-08 16:39:13 +01:00
@toString
class AppPasswordSuccess implements AppPasswordResponse {
2022-12-08 13:56:46 +01:00
const AppPasswordSuccess({
required this.server,
required this.loginName,
required this.appPassword,
});
factory AppPasswordSuccess.fromJsonString(String jsonString) {
final json = jsonDecode(jsonString);
2022-12-08 13:56:46 +01:00
return AppPasswordSuccess(
server: Uri.parse(json['server']),
loginName: json['loginName'],
appPassword: json['appPassword'],
);
}
@override
2022-12-08 16:39:13 +01:00
String toString() => _$toString();
2022-12-08 13:56:46 +01:00
final Uri server;
final String loginName;
2022-12-08 16:39:13 +01:00
@Format(r"${kDebugMode ? appPassword : '***'}")
2022-12-08 13:56:46 +01:00
final String appPassword;
}
class AppPasswordPending implements AppPasswordResponse {}
2021-04-10 06:28:12 +02:00
final _log = Logger("api.api_util");