Support home dir named not as the username

This commit is contained in:
Ming Ming 2022-01-17 05:38:09 +08:00
parent 541b1d61fe
commit c8ad689a52
14 changed files with 215 additions and 22 deletions

View file

@ -3,6 +3,8 @@ import 'dart:math';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/type.dart';
@ -14,6 +16,7 @@ class Account with EquatableMixin {
String address,
this.username,
this.password,
this.altHomeDir,
List<String> roots,
) : address = address.trimRightAny("/"),
_roots = roots.map((e) => e.trimRightAny("/")).toList() {
@ -28,6 +31,7 @@ class Account with EquatableMixin {
String? address,
CiString? username,
String? password,
OrNull<CiString>? altHomeDir,
List<String>? roots,
}) {
return Account(
@ -36,6 +40,7 @@ class Account with EquatableMixin {
address ?? this.address,
username ?? this.username,
password ?? this.password,
altHomeDir == null ? this.altHomeDir : altHomeDir.obj,
roots ?? List.of(_roots),
);
}
@ -54,6 +59,7 @@ class Account with EquatableMixin {
"address: '${kDebugMode ? address : "***"}', "
"username: '${kDebugMode ? username : "***"}', "
"password: '${password.isNotEmpty == true ? (kDebugMode ? password : '***') : null}', "
"altHomeDir: '${altHomeDir == null || kDebugMode ? altHomeDir : "***"}', "
"roots: List {'${roots.join('\', \'')}'}, "
"}";
}
@ -64,6 +70,7 @@ class Account with EquatableMixin {
address = json["address"],
username = CiString(json["username"]),
password = json["password"],
altHomeDir = (json["altHomeDir"] as String?)?.run((v) => CiString(v)),
_roots = json["roots"].cast<String>();
JsonObj toJson() => {
@ -72,19 +79,26 @@ class Account with EquatableMixin {
"address": address,
"username": username.toString(),
"password": password,
"altHomeDir": altHomeDir?.toString(),
"roots": _roots,
};
@override
List<Object> get props => [id, scheme, address, username, password, _roots];
get props => [id, scheme, address, username, password, altHomeDir, _roots];
List<String> get roots => _roots;
CiString get homeDir => altHomeDir ?? username;
final String id;
final String scheme;
final String address;
final CiString username;
final String password;
/// Name of the user home dir. Normally [username] is used as the home dir
/// name, but for LDAP users, their home dir might be named differently
final CiString? altHomeDir;
final List<String> _roots;
}

View file

@ -65,10 +65,10 @@ String getFileUrlRelative(File file) {
}
String getWebdavRootUrlRelative(Account account) =>
"remote.php/dav/files/${account.username}";
"remote.php/dav/files/${account.homeDir}";
String getTrashbinPath(Account account) =>
"remote.php/dav/trashbin/${account.username}/trash";
"remote.php/dav/trashbin/${account.homeDir}/trash";
/// Return the face image URL. See [getFacePreviewUrlRelative]
String getFacePreviewUrl(

View file

@ -4,3 +4,5 @@ const sharedAlbumLimitationsUrl =
"https://gitlab.com/nkming2/nc-photos/-/wikis/help/shared-album#limitations";
const twoFactorAuthUrl =
"https://gitlab.com/nkming2/nc-photos/-/wikis/help/two-factor-authentication";
const homeFolderNotFoundUrl =
"https://gitlab.com/nkming2/nc-photos/-/wikis/help/home-folder-not-found";

View file

@ -757,6 +757,7 @@
"configButtonLabel": "CONFIG",
"useAsAlbumCoverTooltip": "Use as album cover",
"helpTooltip": "Help",
"helpButtonLabel": "HELP",
"removeFromAlbumTooltip": "Remove from album",
"@removeFromAlbumTooltip": {
"description": "Remove the opened photo from an album"
@ -1110,6 +1111,18 @@
}
}
},
"homeFolderNotFoundDialogTitle": "Home folder not found",
"@homeFolderNotFoundDialogTitle": {
"description": "The app failed to scan the user's home folder on signing in"
},
"homeFolderNotFoundDialogContent": "Please correct the WebDAV URL shown below. You can find the URL in the Nextcloud web interface.",
"@homeFolderNotFoundDialogContent": {
"description": "Ask the user to provide us the correct WebDAV URL"
},
"homeFolderInputInvalidEmpty": "Please enter the name of your home folder",
"@homeFolderInputInvalidEmpty": {
"description": "Home folder can't be left empty"
},
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
"@errorUnauthenticated": {

View file

@ -20,6 +20,7 @@
"settingsExperimentalDescription",
"settingsExperimentalPageTitle",
"sortOptionManualLabel",
"helpButtonLabel",
"collectionSharingLabel",
"fileLastSharedDescription",
"fileLastSharedByOthersDescription",
@ -58,6 +59,9 @@
"migrateDatabaseProcessingNotification",
"migrateDatabaseFailureNotification",
"memoryAlbumName",
"homeFolderNotFoundDialogTitle",
"homeFolderNotFoundDialogContent",
"homeFolderInputInvalidEmpty",
"errorAlbumDowngrade"
],
@ -83,6 +87,7 @@
"settingsExperimentalPageTitle",
"timeSecondInputHint",
"sortOptionManualLabel",
"helpButtonLabel",
"slideshowTooltip",
"slideshowSetupDialogTitle",
"slideshowSetupDialogDurationTitle",
@ -134,6 +139,9 @@
"migrateDatabaseProcessingNotification",
"migrateDatabaseFailureNotification",
"memoryAlbumName",
"homeFolderNotFoundDialogTitle",
"homeFolderNotFoundDialogContent",
"homeFolderInputInvalidEmpty",
"errorAlbumDowngrade"
],
@ -201,6 +209,7 @@
"configButtonLabel",
"useAsAlbumCoverTooltip",
"helpTooltip",
"helpButtonLabel",
"removeFromAlbumTooltip",
"fileSharedByDescription",
"emptyTrashbinTooltip",
@ -265,6 +274,9 @@
"migrateDatabaseProcessingNotification",
"migrateDatabaseFailureNotification",
"memoryAlbumName",
"homeFolderNotFoundDialogTitle",
"homeFolderNotFoundDialogContent",
"homeFolderInputInvalidEmpty",
"errorAlbumDowngrade"
],
@ -272,6 +284,7 @@
"settingsMemoriesTitle",
"settingsMemoriesSubtitle",
"settingsMapProviderTitle",
"helpButtonLabel",
"addUserInputHint",
"sharedAlbumInfoDialogTitle",
"sharedAlbumInfoDialogContent",
@ -279,6 +292,9 @@
"migrateDatabaseProcessingNotification",
"migrateDatabaseFailureNotification",
"memoryAlbumName",
"homeFolderNotFoundDialogTitle",
"homeFolderNotFoundDialogContent",
"homeFolderInputInvalidEmpty",
"errorAlbumDowngrade"
],
@ -286,6 +302,7 @@
"settingsMemoriesTitle",
"settingsMemoriesSubtitle",
"settingsMapProviderTitle",
"helpButtonLabel",
"addUserInputHint",
"sharedAlbumInfoDialogTitle",
"sharedAlbumInfoDialogContent",
@ -293,6 +310,9 @@
"migrateDatabaseProcessingNotification",
"migrateDatabaseFailureNotification",
"memoryAlbumName",
"homeFolderNotFoundDialogTitle",
"homeFolderNotFoundDialogContent",
"homeFolderInputInvalidEmpty",
"errorAlbumDowngrade"
],
@ -340,6 +360,7 @@
"sortOptionAlbumNameDescendingLabel",
"sortOptionManualLabel",
"helpTooltip",
"helpButtonLabel",
"removeFromAlbumTooltip",
"fileSharedByDescription",
"emptyTrashbinTooltip",
@ -404,6 +425,9 @@
"migrateDatabaseProcessingNotification",
"migrateDatabaseFailureNotification",
"memoryAlbumName",
"homeFolderNotFoundDialogTitle",
"homeFolderNotFoundDialogContent",
"homeFolderInputInvalidEmpty",
"errorAlbumDowngrade"
],
@ -435,6 +459,7 @@
"sortOptionAlbumNameLabel",
"sortOptionAlbumNameDescendingLabel",
"sortOptionManualLabel",
"helpButtonLabel",
"collectionPeopleLabel",
"personPhotoCountText",
"slideshowTooltip",
@ -488,6 +513,9 @@
"migrateDatabaseProcessingNotification",
"migrateDatabaseFailureNotification",
"memoryAlbumName",
"homeFolderNotFoundDialogTitle",
"homeFolderNotFoundDialogContent",
"homeFolderInputInvalidEmpty",
"errorAlbumDowngrade"
]
}

View file

@ -21,7 +21,7 @@ class ListShare {
bool? isIncludeReshare,
}) async {
try {
if (file_util.getUserDirName(file) != account.username) {
if (file_util.getUserDirName(file) != account.homeDir) {
file = await FindFile(_c)(account, file.fileId!);
}
} catch (_) {

View file

@ -17,7 +17,7 @@ class RestoreTrashbin {
await Move(_c)(
account,
file,
"remote.php/dav/trashbin/${account.username}/restore/${file.filename}",
"remote.php/dav/trashbin/${account.homeDir}/restore/${file.filename}",
shouldOverwrite: true,
);
KiwiContainer()

View file

@ -78,7 +78,7 @@ class _AlbumDirPickerState extends State<AlbumDirPicker> {
child: DirPicker(
key: _pickerKey,
account: widget.account,
rootDir: _rootDir,
strippedRootDir: _strippedRootDir,
validator: (dir) {
if (widget.account.roots.contains("")) {
return true;
@ -132,17 +132,16 @@ class _AlbumDirPickerState extends State<AlbumDirPicker> {
}
String _getPickerRoot() {
final root = api_util.getWebdavRootUrlRelative(widget.account);
if (widget.account.roots.length == 1 &&
widget.account.roots.first.isNotEmpty) {
return "$root/${widget.account.roots.first}";
return widget.account.roots.first;
} else {
return root;
return "";
}
}
final _pickerKey = GlobalKey<DirPickerState>();
late final _rootDir = _getPickerRoot();
late final _strippedRootDir = _getPickerRoot();
static final _log = Logger("widget.album_dir_picker._AlbumDirPickerState");
}

View file

@ -2,17 +2,28 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/app_password_exchange.dart';
import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/help_utils.dart' as help_util;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/mobile/self_signed_cert_manager.dart';
import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/string_extension.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/ls_single_file.dart';
import 'package:url_launcher/url_launcher.dart';
class ConnectArguments {
ConnectArguments(this.account);
@ -98,8 +109,8 @@ class _ConnectState extends State<Connect> {
BuildContext context, AppPasswordExchangeBlocState state) {
if (state is AppPasswordExchangeBlocSuccess) {
final newAccount = widget.account.copyWith(password: state.password);
_log.info("[_onStateChange] Account is good: $newAccount");
Navigator.of(context).pop(newAccount);
_log.info("[_onStateChange] Password exchanged: $newAccount");
_checkWebDavUrl(context, newAccount);
} else if (state is AppPasswordExchangeBlocFailure) {
if (features.isSupportSelfSignedCert &&
state.exception is HandshakeException) {
@ -185,7 +196,131 @@ class _ConnectState extends State<Connect> {
_bloc.add(AppPasswordExchangeBlocConnect(widget.account));
}
Future<void> _onCheckWebDavUrlFailed(
BuildContext context, Account account) async {
final altHomeDir = await _askWebDavUrl(context, account);
if (altHomeDir != null) {
final newAccount = account.copyWith(
altHomeDir: OrNull(altHomeDir.toCi()),
);
return _checkWebDavUrl(context, newAccount);
}
}
Future<void> _checkWebDavUrl(BuildContext context, Account account) async {
// check the WebDAV URL
try {
final c = KiwiContainer().resolve<DiContainer>().copyWith(
fileRepo: OrNull(const FileRepo(FileWebdavDataSource())),
);
await LsSingleFile(c)(account, file_util.unstripPath(account, ""));
_log.info("[_checkWebDavUrl] Account is good: $account");
Navigator.of(context).pop(account);
} on ApiException catch (e) {
if (e.response.statusCode == 404) {
return _onCheckWebDavUrlFailed(context, account);
}
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
duration: k.snackBarDurationNormal,
));
Navigator.of(context).pop(null);
} on StateError catch (_) {
// Nextcloud for some reason doesn't return HTTP error when listing home
// dir of other users
return _onCheckWebDavUrlFailed(context, account);
} catch (e, stackTrace) {
_log.shout("[_checkWebDavUrl] Failed", e, stackTrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
duration: k.snackBarDurationNormal,
));
Navigator.of(context).pop(null);
}
}
Future<String?> _askWebDavUrl(BuildContext context, Account account) {
return showDialog<String>(
context: context,
barrierDismissible: false,
builder: (context) => _WebDavUrlDialog(account: account),
);
}
final _bloc = AppPasswordExchangeBloc();
static final _log = Logger("widget.connect._ConnectState");
}
class _WebDavUrlDialog extends StatefulWidget {
const _WebDavUrlDialog({
Key? key,
required this.account,
}) : super(key: key);
@override
createState() => _WebDavUrlDialogState();
final Account account;
}
class _WebDavUrlDialogState extends State<_WebDavUrlDialog> {
@override
build(BuildContext context) {
return AlertDialog(
title: Text(L10n.global().homeFolderNotFoundDialogTitle),
content: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(L10n.global().homeFolderNotFoundDialogContent),
const SizedBox(height: 16),
Text("${widget.account.url}/remote.php/dav/files/"),
TextFormField(
validator: (value) {
if (value?.isNotEmpty == true) {
return null;
}
return L10n.global().homeFolderInputInvalidEmpty;
},
onSaved: (value) {
_formValue.altHomeDir = value!.trimAny("/");
},
initialValue: widget.account.homeDir.toString(),
),
],
),
),
actions: [
TextButton(
onPressed: _onHelpPressed,
child: Text(L10n.global().helpButtonLabel),
),
TextButton(
onPressed: _onOkPressed,
child: Text(MaterialLocalizations.of(context).okButtonLabel),
),
],
);
}
void _onOkPressed() {
if (_formKey.currentState?.validate() == true) {
_formKey.currentState!.save();
Navigator.of(context).pop(_formValue.altHomeDir);
}
}
void _onHelpPressed() {
launch(help_util.homeFolderNotFoundUrl);
}
final _formKey = GlobalKey<FormState>();
final _formValue = _FormValue();
}
class _FormValue {
late String altHomeDir;
}

View file

@ -17,7 +17,7 @@ class DirPicker extends StatefulWidget {
const DirPicker({
Key? key,
required this.account,
required this.rootDir,
required this.strippedRootDir,
this.initialPicks,
this.isMultipleSelections = true,
this.validator,
@ -28,7 +28,7 @@ class DirPicker extends StatefulWidget {
createState() => DirPickerState();
final Account account;
final String rootDir;
final String strippedRootDir;
final bool isMultipleSelections;
final List<File>? initialPicks;
@ -41,7 +41,7 @@ class DirPickerState extends State<DirPicker> {
@override
initState() {
super.initState();
_root = LsDirBlocItem(File(path: widget.rootDir), []);
_root = LsDirBlocItem(File(path: _rootDir), []);
_initBloc();
if (widget.initialPicks != null) {
_picks.addAll(widget.initialPicks!);
@ -67,7 +67,7 @@ class DirPickerState extends State<DirPicker> {
void _initBloc() {
_log.info("[_initBloc] Initialize bloc");
_navigateInto(File(path: widget.rootDir));
_navigateInto(File(path: _rootDir));
}
Widget _buildContent(BuildContext context, LsDirBlocState state) {
@ -89,7 +89,7 @@ class DirPickerState extends State<DirPicker> {
}
Widget _buildList(BuildContext context, LsDirBlocState state) {
final isTopLevel = _currentPath == widget.rootDir;
final isTopLevel = _currentPath == _rootDir;
return AnimatedSwitcher(
duration: k.animationDurationNormal,
// see AnimatedSwitcher.defaultLayoutBuilder
@ -384,6 +384,9 @@ class DirPickerState extends State<DirPicker> {
_bloc.add(LsDirBlocQuery(widget.account, file, depth: 2));
}
late final String _rootDir =
file_util.unstripPath(widget.account, widget.strippedRootDir);
final _bloc = LsDirBloc(const FileRepo(FileWebdavDataSource()));
late LsDirBlocItem _root;

View file

@ -3,7 +3,6 @@ import 'package:flutter/scheduler.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
@ -115,7 +114,7 @@ class _RootPickerState extends State<RootPicker> {
: DirPicker(
key: _pickerKey,
account: widget.account,
rootDir: api_util.getWebdavRootUrlRelative(widget.account),
strippedRootDir: "",
initialPicks: _initialPicks,
onConfirmed: (picks) => _onPickerConfirmed(context, picks),
),

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
@ -82,7 +81,7 @@ class _ShareFolderPickerState extends State<ShareFolderPicker> {
child: DirPicker(
key: _pickerKey,
account: widget.account,
rootDir: api_util.getWebdavRootUrlRelative(widget.account),
strippedRootDir: "",
initialPicks: [
if (widget.initialValue.isNotEmpty)
File(

View file

@ -236,6 +236,7 @@ class _SignInState extends State<SignIn> {
_formValue.address,
_formValue.username.toCi(),
_formValue.password,
null,
[""],
);
_log.info("[_connect] Try connecting with account: $account");

View file

@ -240,7 +240,7 @@ Account buildAccount({
String password = "pass",
List<String> roots = const [""],
}) =>
Account(id, scheme, address, username.toCi(), password, roots);
Account(id, scheme, address, username.toCi(), password, null, roots);
/// Build a mock [File] pointing to a album JSON file
///