2021-04-10 06:28:12 +02:00
/// Helper functions working with remote Nextcloud server
2022-11-20 16:35:53 +01:00
import ' dart:convert ' ;
2023-03-13 12:55:39 +01:00
import ' package:clock/clock.dart ' ;
2022-11-20 16:35:53 +01:00
import ' package:flutter/foundation.dart ' ;
2021-04-10 06:28:12 +02:00
import ' package:logging/logging.dart ' ;
import ' package:nc_photos/account.dart ' ;
2022-10-15 16:29:18 +02:00
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 ' ;
2023-05-06 19:44:35 +02:00
import ' package:np_api/np_api.dart ' hide NcAlbumItem ;
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 ,
2022-10-15 16:29:18 +02:00
FileDescriptor file , {
2021-07-23 22:05:57 +02:00
required int width ,
required int height ,
String ? mode ,
2022-12-18 07:35:54 +01:00
required bool isKeepAspectRatio ,
2021-04-10 06:28:12 +02:00
} ) {
return " ${ account . url } / "
2022-12-18 07:35:54 +01:00
" ${ getFilePreviewUrlRelative ( account , file , width: width , height: height , mode: mode , isKeepAspectRatio: isKeepAspectRatio ) } " ;
2021-04-10 06:28:12 +02:00
}
2022-12-18 07:35:54 +01:00
/// Return the relative preview image URL for [file]
///
/// If [isKeepAspectRatio] == true, the preview will maintain the original
/// aspect ratio, otherwise it will be cropped
2021-04-10 06:28:12 +02:00
String getFilePreviewUrlRelative (
2021-08-01 22:46:16 +02:00
Account account ,
2022-10-15 16:29:18 +02:00
FileDescriptor file , {
2021-07-23 22:05:57 +02:00
required int width ,
required int height ,
String ? mode ,
2022-12-18 07:35:54 +01:00
required bool isKeepAspectRatio ,
2021-04-10 06:28:12 +02:00
} ) {
2021-06-14 12:50:54 +02:00
String url ;
2023-05-28 15:19:54 +02:00
if ( file_util . isNcAlbumFile ( account , file ) ) {
2023-05-07 16:00:33 +02:00
// We can't use the generic file preview url because collaborative albums do
// not create a file share for photos not belonging to you, that means you
// can only access the file view the Photos API
url = " apps/photos/api/v1/preview/ ${ file . fdId } ?x= $ width &y= $ height " ;
2021-06-14 12:50:54 +02:00
} else {
2023-05-28 15:19:54 +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 } " ;
}
url = " $ url &x= $ width &y= $ height " ;
if ( mode ! = null ) {
url = " $ url &mode= $ mode " ;
}
// keep this here to use cache from older version
url = " $ url &a= ${ isKeepAspectRatio ? 1 : 0 } " ;
2021-06-14 12:50:54 +02:00
}
2022-08-28 17:35:29 +02:00
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 ,
2022-12-18 07:35:54 +01:00
required bool isKeepAspectRatio ,
2022-08-28 17:35:29 +02:00
} ) {
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
}
2022-12-18 07:35:54 +01:00
url = " $ url &a= ${ isKeepAspectRatio ? 1 : 0 } " ;
2021-04-10 06:28:12 +02:00
return url ;
}
2023-05-07 19:33:11 +02:00
/// Return the preview image URL for [fileId], using the new Photos API in
/// Nextcloud 25
String getPhotosApiFilePreviewUrlByFileId (
Account account ,
int fileId , {
required int width ,
required int height ,
} ) = >
" ${ account . url } /apps/photos/api/v1/preview/ $ fileId ?x= $ width &y= $ height " ;
2022-10-15 16:29:18 +02:00
String getFileUrl ( Account account , FileDescriptor file ) {
2021-04-10 06:28:12 +02:00
return " ${ account . url } / ${ getFileUrlRelative ( file ) } " ;
}
2022-10-15 16:29:18 +02:00
String getFileUrlRelative ( FileDescriptor file ) {
return file . fdPath ;
2021-04-10 06:28:12 +02:00
}
String getWebdavRootUrlRelative ( Account account ) = >
2022-07-11 20:14:42 +02:00
" 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 ) = >
2022-07-11 20:14:42 +02:00
" remote.php/dav/trashbin/ ${ account . userId } /trash " ;
2021-08-01 22:46:16 +02:00
2021-09-08 12:44:14 +02:00
/// Return the face image URL. See [getFacePreviewUrlRelative]
String getFacePreviewUrl (
Account account ,
2021-09-10 19:10:26 +02:00
int faceId , {
2021-09-08 12:44:14 +02:00
required int size ,
} ) {
return " ${ account . url } / "
2021-09-10 19:10:26 +02:00
" ${ getFacePreviewUrlRelative ( account , faceId , size: size ) } " ;
2021-09-08 12:44:14 +02:00
}
/// Return the relative URL of the face image
String getFacePreviewUrlRelative (
Account account ,
2021-09-10 19:10:26 +02:00
int faceId , {
2021-09-08 12:44:14 +02:00
required int size ,
} ) {
2021-09-10 19:10:26 +02:00
return " index.php/apps/facerecognition/face/ $ faceId /thumb/ $ size " ;
2021-09-08 12:44:14 +02:00
}
2023-03-19 16:51:06 +01:00
String getAccountAvatarUrl ( Account account , int size ) = >
" ${ account . url } / ${ getAccountAvatarUrlRelative ( account , size ) } " ;
String getAccountAvatarUrlRelative ( Account account , int size ) = >
" avatar/ ${ account . userId } / $ size " ;
2022-11-20 16:35:53 +01:00
/// 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: {
2022-11-20 16:35:53 +01:00
" User-Agent " : " nc-photos " ,
2021-04-10 06:28:12 +02:00
} ,
) ;
if ( response . isGood ) {
2022-11-20 16:35:53 +01:00
return InitiateLoginResponse . fromJsonString ( response . body ) ;
2021-04-10 06:28:12 +02:00
} else {
_log . severe (
2022-11-20 16:35:53 +01:00
" [initiateLogin] Failed while requesting app password: $ response " ) ;
2021-05-07 21:22:21 +02:00
throw ApiException (
response: response ,
2022-11-20 16:35:53 +01:00
message: " Server responded with an error: HTTP ${ response . statusCode } " ) ;
2021-04-10 06:28:12 +02:00
}
}
2022-11-20 16:35:53 +01: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
2022-11-20 16:35:53 +01:00
class InitiateLoginResponse {
2022-12-08 13:56:46 +01:00
const InitiateLoginResponse ( {
required this . poll ,
required this . login ,
} ) ;
factory InitiateLoginResponse . fromJsonString ( String jsonString ) {
2022-11-20 16:35:53 +01:00
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 ' ] ,
) ;
2022-11-20 16:35:53 +01:00
}
@ override
2022-12-08 16:39:13 +01:00
String toString ( ) = > _ $toString ( ) ;
2022-11-20 16:35:53 +01:00
2022-12-08 13:56:46 +01:00
final InitiateLoginPollOptions poll ;
final String login ;
2022-11-20 16:35:53 +01:00
}
2022-12-08 16:39:13 +01:00
@ toString
2022-11-20 16:35:53 +01:00
class InitiateLoginPollOptions {
InitiateLoginPollOptions ( this . token , String endpoint )
: endpoint = Uri . parse ( endpoint ) ,
2023-03-13 12:55:39 +01:00
_validUntil = clock . now ( ) . add ( const Duration ( minutes: 20 ) ) ;
2022-11-20 16:35:53 +01:00
@ override
2022-12-08 16:39:13 +01:00
String toString ( ) = > _ $toString ( ) ;
2022-11-20 16:35:53 +01:00
bool isTokenValid ( ) {
2023-03-13 12:55:39 +01:00
return clock . now ( ) . isBefore ( _validUntil ) ;
2022-11-20 16:35:53 +01:00
}
2022-12-08 16:39:13 +01:00
@ Format ( r"${kDebugMode ? $? : '***'}" )
2022-11-20 16:35:53 +01:00
final String token ;
final Uri endpoint ;
final DateTime _validUntil ;
}
abstract class AppPasswordResponse { }
2022-12-08 16:39:13 +01:00
@ toString
2022-11-20 16:35:53 +01:00
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 ) {
2022-11-20 16:35:53 +01:00
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 ' ] ,
) ;
2022-11-20 16:35:53 +01:00
}
@ override
2022-12-08 16:39:13 +01:00
String toString ( ) = > _ $toString ( ) ;
2022-11-20 16:35:53 +01:00
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 ;
2022-11-20 16:35:53 +01:00
}
class AppPasswordPending implements AppPasswordResponse { }
2021-04-10 06:28:12 +02:00
final _log = Logger ( " api.api_util " ) ;