mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-06 16:07:03 +03:00
Improvements to file storage engine for #151
This commit is contained in:
parent
f92810069a
commit
a729a06a6b
5 changed files with 163 additions and 52 deletions
|
|
@ -1000,8 +1000,9 @@ END;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function RecTimeDirRemove(string $sTempPath, int $iTime2Kill, int $iNow) : bool
|
public static function RecTimeDirRemove(string $sTempPath, int $iTime2Kill, int $iNow = 0) : bool
|
||||||
{
|
{
|
||||||
|
$iNow = $iNow ?: \time();
|
||||||
$iFileCount = 0;
|
$iFileCount = 0;
|
||||||
|
|
||||||
$sTempPath = rtrim($sTempPath, '\\/');
|
$sTempPath = rtrim($sTempPath, '\\/');
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ class File implements \MailSo\Cache\DriverInterface
|
||||||
{
|
{
|
||||||
if (0 < $iTimeToClearInHours)
|
if (0 < $iTimeToClearInHours)
|
||||||
{
|
{
|
||||||
\MailSo\Base\Utils::RecTimeDirRemove($this->sCacheFolder, 60 * 60 * $iTimeToClearInHours, \time());
|
\MailSo\Base\Utils::RecTimeDirRemove($this->sCacheFolder, 3600 * $iTimeToClearInHours);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ class Actions
|
||||||
switch ($sName) {
|
switch ($sName) {
|
||||||
case 'files':
|
case 'files':
|
||||||
// RainLoop\Providers\Files\IFiles
|
// RainLoop\Providers\Files\IFiles
|
||||||
$mResult = new Providers\Files\FileStorage(APP_PRIVATE_DATA . 'storage/files');
|
$mResult = new Providers\Files\FileStorage(APP_PRIVATE_DATA . 'storage');
|
||||||
break;
|
break;
|
||||||
case 'storage':
|
case 'storage':
|
||||||
case 'storage-local':
|
case 'storage-local':
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,16 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles
|
||||||
$bCreate = !!\preg_match('/[wac]/', $sOpenMode);
|
$bCreate = !!\preg_match('/[wac]/', $sOpenMode);
|
||||||
|
|
||||||
$sFileName = $this->generateFullFileName($oAccount, $sKey, $bCreate);
|
$sFileName = $this->generateFullFileName($oAccount, $sKey, $bCreate);
|
||||||
|
if (!\file_exists($sFileName)) {
|
||||||
|
$sOldFileName = $this->generateFullFileNameOld($oAccount, $sKey);
|
||||||
|
if (\file_exists($sOldFileName)
|
||||||
|
&& (
|
||||||
|
(!\is_dir(\dirname($sOldFileName)) && !\mkdir(\dirname($sOldFileName), 0700, true))
|
||||||
|
|| !\rename($sOldFileName, $sFileName)
|
||||||
|
)) {
|
||||||
|
$sFileName = $sOldFileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($bCreate || \file_exists($sFileName))
|
if ($bCreate || \file_exists($sFileName))
|
||||||
{
|
{
|
||||||
$mResult = \fopen($sFileName, $sOpenMode);
|
$mResult = \fopen($sFileName, $sOpenMode);
|
||||||
|
|
@ -68,57 +78,57 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles
|
||||||
return $mResult;
|
return $mResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function GetFileName(\RainLoop\Model\Account $oAccount, string $sKey) : string
|
public function GetFileName(\RainLoop\Model\Account $oAccount, string $sKey) /*: string|false*/
|
||||||
{
|
{
|
||||||
$mResult = false;
|
|
||||||
$sFileName = $this->generateFullFileName($oAccount, $sKey);
|
$sFileName = $this->generateFullFileName($oAccount, $sKey);
|
||||||
if (\file_exists($sFileName))
|
if (!\file_exists($sFileName)) {
|
||||||
{
|
$sFileName = $this->generateFullFileNameOld($oAccount, $sKey);
|
||||||
$mResult = $sFileName;
|
|
||||||
}
|
}
|
||||||
|
return \file_exists($sFileName) ? $sFileName : false;
|
||||||
return $mResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function Clear(\RainLoop\Model\Account $oAccount, string $sKey) : bool
|
public function Clear(\RainLoop\Model\Account $oAccount, string $sKey) : bool
|
||||||
{
|
{
|
||||||
$mResult = true;
|
|
||||||
$sFileName = $this->generateFullFileName($oAccount, $sKey);
|
$sFileName = $this->generateFullFileName($oAccount, $sKey);
|
||||||
if (\file_exists($sFileName))
|
if (!\file_exists($sFileName)) {
|
||||||
{
|
$sFileName = $this->generateFullFileNameOld($oAccount, $sKey);
|
||||||
if (isset($this->aResources[$sFileName]) && \is_resource($this->aResources[$sFileName]))
|
}
|
||||||
{
|
if (\file_exists($sFileName)) {
|
||||||
|
if (isset($this->aResources[$sFileName]) && \is_resource($this->aResources[$sFileName])) {
|
||||||
\fclose($this->aResources[$sFileName]);
|
\fclose($this->aResources[$sFileName]);
|
||||||
}
|
}
|
||||||
|
return \unlink($sFileName);
|
||||||
$mResult = \unlink($sFileName);
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
return $mResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function FileSize(\RainLoop\Model\Account $oAccount, string $sKey) : int
|
public function FileSize(\RainLoop\Model\Account $oAccount, string $sKey) /*: int|false*/
|
||||||
{
|
{
|
||||||
$mResult = false;
|
|
||||||
$sFileName = $this->generateFullFileName($oAccount, $sKey);
|
$sFileName = $this->generateFullFileName($oAccount, $sKey);
|
||||||
if (\file_exists($sFileName))
|
if (!\file_exists($sFileName)) {
|
||||||
{
|
$sFileName = $this->generateFullFileNameOld($oAccount, $sKey);
|
||||||
$mResult = \filesize($sFileName);
|
|
||||||
}
|
}
|
||||||
|
return \file_exists($sFileName) ? \filesize($sFileName) : false;
|
||||||
return $mResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function FileExists(\RainLoop\Model\Account $oAccount, string $sKey) : bool
|
public function FileExists(\RainLoop\Model\Account $oAccount, string $sKey) : bool
|
||||||
{
|
{
|
||||||
return \file_exists($this->generateFullFileName($oAccount, $sKey));
|
return \file_exists($this->generateFullFileName($oAccount, $sKey))
|
||||||
|
|| \file_exists($this->generateFullFileNameOld($oAccount, $sKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function GC(int $iTimeToClearInHours = 24) : bool
|
public function GC(int $iTimeToClearInHours = 24) : bool
|
||||||
{
|
{
|
||||||
if (0 < $iTimeToClearInHours)
|
if (0 < $iTimeToClearInHours) {
|
||||||
{
|
$iTimeToClear = 3600 * $iTimeToClearInHours;
|
||||||
\MailSo\Base\Utils::RecTimeDirRemove($this->sDataPath, 60 * 60 * $iTimeToClearInHours, \time());
|
foreach (\glob("{$this->sDataPath}/*", GLOB_ONLYDIR) as $sDomain) {
|
||||||
|
foreach (\glob("{$sDomain}/*", GLOB_ONLYDIR) as $sLocal) {
|
||||||
|
\MailSo\Base\Utils::RecTimeDirRemove("{$sLocal}/.files", $iTimeToClear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Old
|
||||||
|
\MailSo\Base\Utils::RecTimeDirRemove("{$this->sDataPath}/files", $iTimeToClear);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +151,36 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace control characters, ampersand, spaces and reserved characters (based on Win95 VFAT)
|
||||||
|
* en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
||||||
|
*/
|
||||||
|
private static function fixName($filename)
|
||||||
|
{
|
||||||
|
return \preg_replace('#[|\\\\?*<":>+\\[\\]/&\\s\\pC]#su', '-', $filename);
|
||||||
|
}
|
||||||
|
|
||||||
private function generateFullFileName(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string
|
private function generateFullFileName(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string
|
||||||
|
{
|
||||||
|
$sEmail = $oAccount->ParentEmailHelper() ?: 'nobody@unknown.tld';
|
||||||
|
$sSubEmail = $oAccount->IsAdditionalAccount() ? $oAccount->Email() : '';
|
||||||
|
|
||||||
|
$aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld');
|
||||||
|
$sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : '');
|
||||||
|
$sFilePath = $this->sDataPath
|
||||||
|
.'/'.static::fixName($sDomain ?: 'unknown.tld')
|
||||||
|
.'/'.static::fixName(\implode('@', $aEmail) ?: '.unknown')
|
||||||
|
.($sSubEmail ? '/'.static::fixName($sSubEmail) : '')
|
||||||
|
.'/.files/'.\sha1($sKey);
|
||||||
|
|
||||||
|
if ($bMkDir && !\is_dir(\dirname($sFilePath)) && !\mkdir(\dirname($sFilePath), 0700, true)) {
|
||||||
|
throw new \RainLoop\Exceptions\Exception('Can\'t make storage directory "'.$sFilePath.'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateFullFileNameOld(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string
|
||||||
{
|
{
|
||||||
$sEmail = $sSubEmail = '';
|
$sEmail = $sSubEmail = '';
|
||||||
if ($oAccount instanceof \RainLoop\Model\Account)
|
if ($oAccount instanceof \RainLoop\Model\Account)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace RainLoop\Providers\Storage;
|
namespace RainLoop\Providers\Storage;
|
||||||
|
|
||||||
|
use \RainLoop\Providers\Storage\Enumerations\StorageType;
|
||||||
|
|
||||||
class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,8 +33,8 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
||||||
*/
|
*/
|
||||||
public function Put($oAccount, int $iStorageType, string $sKey, string $sValue) : bool
|
public function Put($oAccount, int $iStorageType, string $sKey, string $sValue) : bool
|
||||||
{
|
{
|
||||||
return false !== \file_put_contents(
|
$sFileName = $this->generateFileName($oAccount, $iStorageType, $sKey, true);
|
||||||
$this->generateFileName($oAccount, $iStorageType, $sKey, true), $sValue);
|
return $sFileName && false !== \file_put_contents($sFileName, $sValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,9 +47,15 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
||||||
{
|
{
|
||||||
$mValue = false;
|
$mValue = false;
|
||||||
$sFileName = $this->generateFileName($oAccount, $iStorageType, $sKey);
|
$sFileName = $this->generateFileName($oAccount, $iStorageType, $sKey);
|
||||||
if (\file_exists($sFileName))
|
if ($sFileName && \file_exists($sFileName)) {
|
||||||
{
|
|
||||||
$mValue = \file_get_contents($sFileName);
|
$mValue = \file_get_contents($sFileName);
|
||||||
|
} else {
|
||||||
|
$sFileName = $this->generateFileNameOld($oAccount, $iStorageType, $sKey);
|
||||||
|
if ($sFileName && \file_exists($sFileName)) {
|
||||||
|
$mValue = \file_get_contents($sFileName);
|
||||||
|
$this->Put($oAccount, $iStorageType, $sKey, $mValue);
|
||||||
|
\unlink($sFileName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false === $mValue ? $mDefault : $mValue;
|
return false === $mValue ? $mDefault : $mValue;
|
||||||
|
|
@ -60,9 +68,13 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
||||||
{
|
{
|
||||||
$mResult = true;
|
$mResult = true;
|
||||||
$sFileName = $this->generateFileName($oAccount, $iStorageType, $sKey);
|
$sFileName = $this->generateFileName($oAccount, $iStorageType, $sKey);
|
||||||
if (\file_exists($sFileName))
|
if ($sFileName && \file_exists($sFileName)) {
|
||||||
{
|
|
||||||
$mResult = \unlink($sFileName);
|
$mResult = \unlink($sFileName);
|
||||||
|
} else {
|
||||||
|
$sFileName = $this->generateFileNameOld($oAccount, $iStorageType, $sKey);
|
||||||
|
if ($sFileName && \file_exists($sFileName)) {
|
||||||
|
$mResult = \unlink($sFileName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $mResult;
|
return $mResult;
|
||||||
|
|
@ -73,19 +85,18 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
||||||
*/
|
*/
|
||||||
public function DeleteStorage($oAccount) : bool
|
public function DeleteStorage($oAccount) : bool
|
||||||
{
|
{
|
||||||
$sPath = $this->generateFileName($oAccount,
|
$sPath = $this->generateFileName($oAccount, StorageType::USER, '', false, true);
|
||||||
\RainLoop\Providers\Storage\Enumerations\StorageType::USER, 'xxx', false, true);
|
if ($sPath && \is_dir($sPath)) {
|
||||||
|
|
||||||
if (!empty($sPath) && \is_dir($sPath))
|
|
||||||
{
|
|
||||||
\MailSo\Base\Utils::RecRmDir($sPath);
|
\MailSo\Base\Utils::RecRmDir($sPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
$sPath = $this->generateFileName($oAccount,
|
$sPath = $this->generateFileNameOld($oAccount, StorageType::USER, '', false, true);
|
||||||
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, 'xxx', false, true);
|
if ($sPath && \is_dir($sPath)) {
|
||||||
|
\MailSo\Base\Utils::RecRmDir($sPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($sPath) && \is_dir($sPath))
|
$sPath = $this->generateFileNameOld($oAccount, StorageType::CONFIG, '', false, true);
|
||||||
{
|
if ($sPath && \is_dir($sPath)) {
|
||||||
\MailSo\Base\Utils::RecRmDir($sPath);
|
\MailSo\Base\Utils::RecRmDir($sPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,14 +108,74 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
||||||
return $this->bLocal;
|
return $this->bLocal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace control characters, ampersand, spaces and reserved characters (based on Win95 VFAT)
|
||||||
|
* en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
||||||
|
*/
|
||||||
|
private static function fixName($filename)
|
||||||
|
{
|
||||||
|
return \preg_replace('#[|\\\\?*<":>+\\[\\]/&\\s\\pC]#su', '-', $filename);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \RainLoop\Model\Account|string|null $mAccount
|
* @param \RainLoop\Model\Account|string|null $mAccount
|
||||||
*/
|
*/
|
||||||
public function generateFileName($mAccount, int $iStorageType, string $sKey, bool $bMkDir = false, bool $bForDeleteAction = false) : string
|
private function generateFileName($mAccount, int $iStorageType, string $sKey, bool $bMkDir = false, bool $bForDeleteAction = false) : string
|
||||||
|
{
|
||||||
|
$sEmail = $sSubEmail = '';
|
||||||
|
if (null === $mAccount) {
|
||||||
|
$iStorageType = StorageType::NOBODY;
|
||||||
|
} else if ($mAccount instanceof \RainLoop\Model\Account) {
|
||||||
|
$sEmail = $mAccount->ParentEmailHelper();
|
||||||
|
if ($this->bLocal && $mAccount->IsAdditionalAccount() && !$bForDeleteAction)
|
||||||
|
{
|
||||||
|
$sSubEmail = $mAccount->Email();
|
||||||
|
}
|
||||||
|
} else if (\is_string($mAccount) && empty($sEmail)) {
|
||||||
|
$sEmail = $mAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sFilePath = '';
|
||||||
|
switch ($iStorageType)
|
||||||
|
{
|
||||||
|
case StorageType::NOBODY:
|
||||||
|
$sFilePath = $this->sDataPath.'/__nobody__/'.\sha1($sKey ?: \time());
|
||||||
|
break;
|
||||||
|
case StorageType::CONFIG:
|
||||||
|
if (empty($sEmail)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld');
|
||||||
|
$sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : '');
|
||||||
|
$sFilePath = $this->sDataPath
|
||||||
|
.'/'.static::fixName($sDomain ?: 'unknown.tld')
|
||||||
|
.'/'.static::fixName(\implode('@', $aEmail) ?: '.unknown')
|
||||||
|
.'/'.($sSubEmail ? static::fixName($sSubEmail).'/' : '')
|
||||||
|
.($sKey ? static::fixName($sKey) : '');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new \Exception("Invalid storage type {$iStorageType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bMkDir && !empty($sFilePath) && !\is_dir(\dirname($sFilePath)))
|
||||||
|
{
|
||||||
|
if (!\mkdir(\dirname($sFilePath), 0700, true))
|
||||||
|
{
|
||||||
|
throw new \RainLoop\Exceptions\Exception('Can\'t make storage directory "'.$sFilePath.'"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Old RainLoop structure
|
||||||
|
*/
|
||||||
|
private function generateFileNameOld($mAccount, int $iStorageType, string $sKey, bool $bMkDir = false, bool $bForDeleteAction = false) : string
|
||||||
{
|
{
|
||||||
if (null === $mAccount)
|
if (null === $mAccount)
|
||||||
{
|
{
|
||||||
$iStorageType = \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY;
|
$iStorageType = StorageType::NOBODY;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sEmail = $sSubEmail = '';
|
$sEmail = $sSubEmail = '';
|
||||||
|
|
@ -129,20 +200,20 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
||||||
switch ($iStorageType)
|
switch ($iStorageType)
|
||||||
{
|
{
|
||||||
default:
|
default:
|
||||||
case \RainLoop\Providers\Storage\Enumerations\StorageType::USER:
|
case StorageType::USER:
|
||||||
case \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY:
|
case StorageType::NOBODY:
|
||||||
$sTypePath = 'data';
|
$sTypePath = 'data';
|
||||||
$sKeyPath = \md5($sKey);
|
$sKeyPath = \md5($sKey);
|
||||||
$sKeyPath = \substr($sKeyPath, 0, 2).'/'.$sKeyPath;
|
$sKeyPath = \substr($sKeyPath, 0, 2).'/'.$sKeyPath;
|
||||||
break;
|
break;
|
||||||
case \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG:
|
case StorageType::CONFIG:
|
||||||
$sTypePath = 'cfg';
|
$sTypePath = 'cfg';
|
||||||
$sKeyPath = \preg_replace('/[_]+/', '_', \preg_replace('/[^a-zA-Z0-9\/]/', '_', $sKey));
|
$sKeyPath = \preg_replace('/[_]+/', '_', \preg_replace('/[^a-zA-Z0-9\/]/', '_', $sKey));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sFilePath = '';
|
$sFilePath = '';
|
||||||
if (\RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY === $iStorageType)
|
if (StorageType::NOBODY === $iStorageType)
|
||||||
{
|
{
|
||||||
$sFilePath = $this->sDataPath.'/'.$sTypePath.'/__nobody__/'.$sKeyPath;
|
$sFilePath = $this->sDataPath.'/'.$sTypePath.'/__nobody__/'.$sKeyPath;
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +221,7 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
|
||||||
{
|
{
|
||||||
$sFilePath = $this->sDataPath.'/'.$sTypePath.'/'.
|
$sFilePath = $this->sDataPath.'/'.$sTypePath.'/'.
|
||||||
\str_pad(\rtrim(\substr($sEmail, 0, 2), '@'), 2, '_').'/'.$sEmail.'/'.
|
\str_pad(\rtrim(\substr($sEmail, 0, 2), '@'), 2, '_').'/'.$sEmail.'/'.
|
||||||
(0 < \strlen($sSubEmail) ? $sSubEmail.'/' : '').
|
(\strlen($sSubEmail) ? $sSubEmail.'/' : '').
|
||||||
($bForDeleteAction ? '' : $sKeyPath);
|
($bForDeleteAction ? '' : $sKeyPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue