Revamp the FullNameHash system for better readable urls

And reduce the folders caching footprint.
And it reduces server load.
This commit is contained in:
djmaze 2021-12-07 14:03:30 +01:00
parent 76627ae2f6
commit 3a61bb3e5a
10 changed files with 90 additions and 78 deletions

View file

@ -657,20 +657,20 @@ class AppUser extends AbstractApp {
} }
/** /**
* @param {string} sFullNameHash * @param {string} sFullName
* @param {boolean} bExpanded * @param {boolean} bExpanded
*/ */
setExpandedFolder(sFullNameHash, bExpanded) { setExpandedFolder(sFullName, bExpanded) {
let aExpandedList = Local.get(ClientSideKeyName.ExpandedFolders); let aExpandedList = Local.get(ClientSideKeyName.ExpandedFolders);
if (!isArray(aExpandedList)) { if (!isArray(aExpandedList)) {
aExpandedList = []; aExpandedList = [];
} }
if (bExpanded) { if (bExpanded) {
if (!aExpandedList.includes(sFullNameHash)) if (!aExpandedList.includes(sFullName))
aExpandedList.push(sFullNameHash); aExpandedList.push(sFullName);
} else { } else {
aExpandedList = aExpandedList.filter(value => value !== sFullNameHash); aExpandedList = aExpandedList.filter(value => value !== sFullName);
} }
Local.set(ClientSideKeyName.ExpandedFolders, aExpandedList); Local.set(ClientSideKeyName.ExpandedFolders, aExpandedList);

View file

@ -3,8 +3,6 @@ import { isArray } from 'Common/Utils';
let FOLDERS_CACHE = {}, let FOLDERS_CACHE = {},
FOLDERS_NAME_CACHE = {}, FOLDERS_NAME_CACHE = {},
FOLDERS_HASH_CACHE = {},
FOLDERS_UID_NEXT_CACHE = {},
MESSAGE_FLAGS_CACHE = {}, MESSAGE_FLAGS_CACHE = {},
NEW_MESSAGE_CACHE = {}, NEW_MESSAGE_CACHE = {},
REQUESTED_MESSAGE_CACHE = {}, REQUESTED_MESSAGE_CACHE = {},
@ -17,8 +15,6 @@ export const
clearCache = () => { clearCache = () => {
FOLDERS_CACHE = {}; FOLDERS_CACHE = {};
FOLDERS_NAME_CACHE = {}; FOLDERS_NAME_CACHE = {};
FOLDERS_HASH_CACHE = {};
FOLDERS_UID_NEXT_CACHE = {};
MESSAGE_FLAGS_CACHE = {}; MESSAGE_FLAGS_CACHE = {};
NEW_MESSAGE_CACHE = {}; NEW_MESSAGE_CACHE = {};
REQUESTED_MESSAGE_CACHE = {}; REQUESTED_MESSAGE_CACHE = {};
@ -89,9 +85,10 @@ export const
* @param {string} folderFullName * @param {string} folderFullName
* @param {?FolderModel} folder * @param {?FolderModel} folder
*/ */
setFolder = (folderHash, folderFullName, folder) => { setFolder = folder => {
FOLDERS_CACHE[folderFullName] = folder; folder.hash = '';
FOLDERS_NAME_CACHE[folderHash] = folderFullName; FOLDERS_CACHE[folder.fullName] = folder;
FOLDERS_NAME_CACHE[folder.fullNameHash] = folder.fullName;
}, },
/** /**
@ -99,37 +96,35 @@ export const
* @returns {string} * @returns {string}
*/ */
getFolderHash = folderFullName => getFolderHash = folderFullName =>
folderFullName && FOLDERS_HASH_CACHE[folderFullName] ? FOLDERS_HASH_CACHE[folderFullName] : '', FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].hash : '',
/** /**
* @param {string} folderFullName * @param {string} folderFullName
* @param {string} folderHash * @param {string} folderHash
*/ */
setFolderHash = (folderFullName, folderHash) => setFolderHash = (folderFullName, folderHash) =>
folderFullName && (FOLDERS_HASH_CACHE[folderFullName] = folderHash), FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].hash = folderHash),
/** /**
* @param {string} folderFullName * @param {string} folderFullName
* @returns {string} * @returns {string}
*/ */
getFolderUidNext = folderFullName => getFolderUidNext = folderFullName =>
folderFullName && FOLDERS_UID_NEXT_CACHE[folderFullName] FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName].uidNext : 0,
? FOLDERS_UID_NEXT_CACHE[folderFullName]
: '',
/** /**
* @param {string} folderFullName * @param {string} folderFullName
* @param {string} uidNext * @param {string} uidNext
*/ */
setFolderUidNext = (folderFullName, uidNext) => setFolderUidNext = (folderFullName, uidNext) =>
FOLDERS_UID_NEXT_CACHE[folderFullName] = uidNext, FOLDERS_CACHE[folderFullName] && (FOLDERS_CACHE[folderFullName].uidNext = uidNext),
/** /**
* @param {string} folderFullName * @param {string} folderFullName
* @returns {?FolderModel} * @returns {?FolderModel}
*/ */
getFolderFromCacheList = folderFullName => getFolderFromCacheList = folderFullName =>
folderFullName && FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null, FOLDERS_CACHE[folderFullName] ? FOLDERS_CACHE[folderFullName] : null,
/** /**
* @param {string} folderFullName * @param {string} folderFullName
@ -156,9 +151,7 @@ export class MessageFlagsCache
* @returns {?Array} * @returns {?Array}
*/ */
static getFor(folderFullName, uid) { static getFor(folderFullName, uid) {
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid] return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid];
? MESSAGE_FLAGS_CACHE[folderFullName][uid]
: null;
} }
/** /**

View file

@ -132,7 +132,7 @@ ko.bindingHandlers.dropmessages = {
if (folder && folder.collapsed()) { if (folder && folder.collapsed()) {
dragTimer.start(() => { dragTimer.start(() => {
folder.collapsed(false); folder.collapsed(false);
rl.app.setExpandedFolder(folder.fullNameHash, true); rl.app.setExpandedFolder(folder.fullName, true);
}, 500); }, 500);
} }
} }

View file

@ -1,7 +1,7 @@
import { AbstractCollectionModel } from 'Model/AbstractCollection'; import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { UNUSED_OPTION_VALUE } from 'Common/Consts'; import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { isArray, getKeyByValue, forEachObjectEntry } from 'Common/Utils'; import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { ClientSideKeyName, FolderType, FolderMetadataKeys } from 'Common/EnumsUser'; import { ClientSideKeyName, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
import * as Cache from 'Common/Cache'; import * as Cache from 'Common/Cache';
import { Settings, SettingsGet } from 'Common/Globals'; import { Settings, SettingsGet } from 'Common/Globals';
@ -79,7 +79,7 @@ export class FolderCollectionModel extends AbstractCollectionModel
oCacheFolder.type(FolderType.Inbox); oCacheFolder.type(FolderType.Inbox);
Cache.setFolderInboxName(oFolder.FullName); Cache.setFolderInboxName(oFolder.FullName);
} }
Cache.setFolder(oCacheFolder.fullNameHash, oFolder.FullName, oCacheFolder); Cache.setFolder(oCacheFolder);
} }
if (1 < type) { if (1 < type) {
@ -88,7 +88,7 @@ export class FolderCollectionModel extends AbstractCollectionModel
oCacheFolder.collapsed(!expandedFolders oCacheFolder.collapsed(!expandedFolders
|| !isArray(expandedFolders) || !isArray(expandedFolders)
|| !expandedFolders.includes(oCacheFolder.fullNameHash)); || !expandedFolders.includes(oCacheFolder.fullName));
if (oFolder.Extended) { if (oFolder.Extended) {
if (oFolder.Extended.Hash) { if (oFolder.Extended.Hash) {
@ -180,7 +180,6 @@ export class FolderModel extends AbstractModel {
super(); super();
this.fullName = ''; this.fullName = '';
this.fullNameHash = '';
this.delimiter = ''; this.delimiter = '';
this.deep = 0; this.deep = 0;
this.expires = 0; this.expires = 0;
@ -188,6 +187,9 @@ export class FolderModel extends AbstractModel {
this.exists = true; this.exists = true;
// this.hash = '';
// this.uidNext = 0;
this.addObservables({ this.addObservables({
name: '', name: '',
type: FolderType.User, type: FolderType.User,
@ -218,6 +220,14 @@ export class FolderModel extends AbstractModel {
this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 }); this.actionBlink = ko.observable(false).extend({ falseTimeout: 1000 });
} }
/**
* For url safe '/#/mailbox/...' path
*/
get fullNameHash() {
return this.fullName.replace(/[^a-z0-9._-]+/giu, b64EncodeJSONSafe);
// return /^[a-z0-9._-]+$/iu.test(this.fullName) ? this.fullName : b64EncodeJSONSafe(this.fullName);
}
/** /**
* @static * @static
* @param {FetchJsonFolder} json * @param {FetchJsonFolder} json

View file

@ -135,10 +135,11 @@ export class MailBoxUserScreen extends AbstractScreen {
return [ return [
[/^([^/]*)$/, { normalize_: fNormS }], [/^([^/]*)$/, { normalize_: fNormS }],
[/^([a-zA-Z0-9~]+)\/(.+)\/?$/, { normalize_: (request, vals) => // Regex the fullNameHash
[/^([a-zA-Z0-9.~_-]+)\/(.+)\/?$/, { normalize_: (request, vals) =>
[folder(request, vals), 1, decodeURI(pString(vals[1]))] [folder(request, vals), 1, decodeURI(pString(vals[1]))]
}], }],
[/^([a-zA-Z0-9~]+)\/p([1-9][0-9]*)(?:\/(.+)\/?)?$/, { normalize_: fNormS }] [/^([a-zA-Z0-9.~_-]+)\/p([1-9][0-9]*)(?:\/(.+)\/?)?$/, { normalize_: fNormS }]
]; ];
} }
} }

View file

@ -5,7 +5,7 @@ import { ClientSideKeyName, FolderMetadataKeys } from 'Common/EnumsUser';
import { Settings } from 'Common/Globals'; import { Settings } from 'Common/Globals';
import { getNotification } from 'Common/Translator'; import { getNotification } from 'Common/Translator';
import { removeFolderFromCacheList } from 'Common/Cache'; import { setFolder, removeFolderFromCacheList } from 'Common/Cache';
import { Capa } from 'Common/Enums'; import { Capa } from 'Common/Enums';
import { defaultOptionsAfterRender } from 'Common/Utils'; import { defaultOptionsAfterRender } from 'Common/Utils';
import { initOnStartOrLangChange, i18n } from 'Common/Translator'; import { initOnStartOrLangChange, i18n } from 'Common/Translator';
@ -71,18 +71,28 @@ export class FoldersUserSettings /*extends AbstractViewSettings*/ {
if (nameToEdit && folder.name() !== nameToEdit) { if (nameToEdit && folder.name() !== nameToEdit) {
Local.set(ClientSideKeyName.FoldersLashHash, ''); Local.set(ClientSideKeyName.FoldersLashHash, '');
Remote
rl.app.foldersPromisesActionHelper( .post('FolderRename', FolderUserStore.foldersRenaming, {
Remote.post('FolderRename', FolderUserStore.foldersRenaming, {
Folder: folder.fullName, Folder: folder.fullName,
NewFolderName: nameToEdit NewFolderName: nameToEdit
}), })
Notification.CantRenameFolder .then(data => {
); folder.name(nameToEdit/*data.Name*/);
if (folder.subFolders.length) {
Remote.foldersReloadWithTimeout();
// rename all subfolders folder.delimiter
} else {
removeFolderFromCacheList(folder.fullName); removeFolderFromCacheList(folder.fullName);
data = data.Result;
folder.name(nameToEdit); folder.fullName = data.FullName;
setFolder(folder);
}
})
.catch(error => {
FolderUserStore.folderListError(
getNotification(error.code, '', Notification.CantRenameFolder)
+ '.\n' + error.message);
});
} }
folder.edited(false); folder.edited(false);

View file

@ -69,7 +69,7 @@ export class MailFolderList extends AbstractViewLeft {
const folder = ko.dataFor(el); const folder = ko.dataFor(el);
if (folder) { if (folder) {
const collapsed = folder.collapsed(); const collapsed = folder.collapsed();
rl.app.setExpandedFolder(folder.fullNameHash, collapsed); rl.app.setExpandedFolder(folder.fullName, collapsed);
folder.collapsed(!collapsed); folder.collapsed(!collapsed);
event.preventDefault(); event.preventDefault();
@ -151,7 +151,7 @@ export class MailFolderList extends AbstractViewLeft {
folder = item && ko.dataFor(item); folder = item && ko.dataFor(item);
if (folder) { if (folder) {
const collapsed = folder.collapsed(); const collapsed = folder.collapsed();
rl.app.setExpandedFolder(folder.fullNameHash, collapsed); rl.app.setExpandedFolder(folder.fullName, collapsed);
folder.collapsed(!collapsed); folder.collapsed(!collapsed);
} }

View file

@ -1361,22 +1361,43 @@ class MailClient
*/ */
public function FolderMove(string $sPrevFolderFullName, string $sNextFolderFullNameInUtf, bool $bSubscribeOnMove = true) : self public function FolderMove(string $sPrevFolderFullName, string $sNextFolderFullNameInUtf, bool $bSubscribeOnMove = true) : self
{ {
return $this->folderModify($sPrevFolderFullName, $sNextFolderFullNameInUtf, false, $bSubscribeOnMove); if (!$this->oImapClient->FolderHierarchyDelimiter($sPrevFolderFullName)) {
// TODO: Translate
throw new Exceptions\RuntimeException('Cannot move non-existent folder.');
}
return $this->folderModify($sPrevFolderFullName, $sNextFolderFullNameInUtf, $bSubscribeOnMove);
} }
/** /**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Base\Exceptions\InvalidArgumentException
*/ */
public function FolderRename(string $sPrevFolderFullName, string $sNewTopFolderNameInUtf, bool $bSubscribeOnRename = true) : self public function FolderRename(string $sPrevFolderFullName, string $sNewTopFolderNameInUtf, bool $bSubscribeOnRename = true) : string
{ {
return $this->folderModify($sPrevFolderFullName, $sNewTopFolderNameInUtf, true, $bSubscribeOnRename); $sDelimiter = $this->oImapClient->FolderHierarchyDelimiter($sPrevFolderFullName);
if (!$sDelimiter) {
// TODO: Translate
throw new Exceptions\RuntimeException('Cannot rename non-existent folder.');
}
if (\strlen($sDelimiter) && false !== \strpos($sNewTopFolderNameInUtf, $sDelimiter)) {
// TODO: Translate
throw new Exceptions\RuntimeException('New folder name contains delimiter.');
}
$iLast = \strrpos($sPrevFolderFullName, $sDelimiter);
$sNewFolderFullName = (false === $iLast ? '' : \substr($sPrevFolderFullName, 0, $iLast + 1))
. $sNewTopFolderNameInUtf;
$this->folderModify($sPrevFolderFullName, $sNewFolderFullName, $bSubscribeOnRename);
return $sNewFolderFullName;
} }
/** /**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Base\Exceptions\RuntimeException * @throws \MailSo\Base\Exceptions\RuntimeException
*/ */
protected function folderModify(string $sPrevFolderFullName, string $sNewFolderFullName, bool $bRename, bool $bSubscribeOnModify) : self protected function folderModify(string $sPrevFolderFullName, string $sNewFolderFullName, bool $bSubscribe) : self
{ {
if (!\strlen($sPrevFolderFullName) || !\strlen($sNewFolderFullName)) if (!\strlen($sPrevFolderFullName) || !\strlen($sNewFolderFullName))
{ {
@ -1384,7 +1405,7 @@ class MailClient
} }
$aSubscribeFolders = array(); $aSubscribeFolders = array();
if ($bSubscribeOnModify) if ($bSubscribe)
{ {
$aSubscribeFolders = $this->oImapClient->FolderSubscribeList($sPrevFolderFullName, '*'); $aSubscribeFolders = $this->oImapClient->FolderSubscribeList($sPrevFolderFullName, '*');
foreach ($aSubscribeFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) foreach ($aSubscribeFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder)
@ -1393,27 +1414,6 @@ class MailClient
} }
} }
if ($bRename)
{
$sDelimiter = $this->oImapClient->FolderHierarchyDelimiter($sPrevFolderFullName);
if (!$sDelimiter)
{
// TODO: Translate
throw new Exceptions\RuntimeException('Cannot '.($bRename?'rename':'move').' non-existent folder.');
}
$iLast = \strrpos($sPrevFolderFullName, $sDelimiter);
if (\strlen($sDelimiter) && false !== \strpos($sNewFolderFullName, $sDelimiter))
{
// TODO: Translate
throw new Exceptions\RuntimeException('New folder name contains delimiter.');
}
$sNewFolderFullName = (false === $iLast ? '' : \substr($sPrevFolderFullName, 0, $iLast + 1))
. $sNewFolderFullName;
}
$this->oImapClient->FolderRename($sPrevFolderFullName, $sNewFolderFullName); $this->oImapClient->FolderRename($sPrevFolderFullName, $sNewFolderFullName);
foreach ($aSubscribeFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder) foreach ($aSubscribeFolders as /* @var $oFolder \MailSo\Imap\Folder */ $oFolder)

View file

@ -320,11 +320,12 @@ trait Folders
{ {
$this->initMailClientConnection(); $this->initMailClientConnection();
$sName = $this->GetActionParam('NewFolderName', '');
try try
{ {
$this->MailClient()->FolderRename( $sFullName = $this->MailClient()->FolderRename(
$this->GetActionParam('Folder', ''), $this->GetActionParam('Folder', ''),
$this->GetActionParam('NewFolderName', ''), $sName,
!!$this->Config()->Get('labs', 'use_imap_list_subscribe', true) !!$this->Config()->Get('labs', 'use_imap_list_subscribe', true)
); );
} }
@ -333,7 +334,11 @@ trait Folders
throw new ClientException(Notifications::CantRenameFolder, $oException); throw new ClientException(Notifications::CantRenameFolder, $oException);
} }
return $this->TrueResponse(__FUNCTION__); // FolderInformation(string $sFolderName, int $iPrevUidNext = 0, array $aUids = array())
return $this->DefaultResponse(__FUNCTION__, array(
'Name' => $sName,
'FullName' => $sFullName,
));
} }
/** /**

View file

@ -139,12 +139,6 @@ trait Response
return $bResult; return $bResult;
} }
private function hashFolderFullName(string $sFolderFullName) : string
{
// return \strspn(\mb_strtolower($sFolderFullName), ':/#?') ? \md5($sFolderFullName) : $sFolderFullName;
return \preg_match('/^[a-z0-9]+$/iu', $sFolderFullName) ? $sFolderFullName : \md5($sFolderFullName);
}
/** /**
* @param mixed $mResponse * @param mixed $mResponse
* *
@ -382,7 +376,6 @@ trait Response
return \array_merge( return \array_merge(
$mResponse->jsonSerialize(), $mResponse->jsonSerialize(),
array( array(
'FullNameHash' => $this->hashFolderFullName($mResponse->FullName()),
'Checkable' => \in_array($mResponse->FullName(), $this->aCheckableFolder), 'Checkable' => \in_array($mResponse->FullName(), $this->aCheckableFolder),
'Extended' => $aExtended, 'Extended' => $aExtended,
'SubFolders' => $this->responseObject($mResponse->SubFolders(), $sParent, $aParameters) 'SubFolders' => $this->responseObject($mResponse->SubFolders(), $sParent, $aParameters)