Compare commits

...

1 Commits

Author SHA1 Message Date
Git'Fellow 8bb31e0fec fix(files/cache): uniqueness guarantees in the file cache layer
Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
2026-04-11 12:23:04 +02:00
3 changed files with 46 additions and 38 deletions
+30 -28
View File
@@ -1015,7 +1015,6 @@ class Cache implements ICache {
/**
* inner function because we can't add new params to the public function without breaking any child classes
*
* @param string $path
* @param array|null|ICacheEntry $entry (optional) meta data of the folder
* @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
* @return int|float
@@ -1029,7 +1028,19 @@ class Cache implements ICache {
$id = $entry['fileid'];
$query = $this->getQueryBuilder();
$query->select('size', 'unencrypted_size')
$sizeCol = $query->getColumnName('size');
$unencryptedSizeCol = $query->getColumnName('unencrypted_size');
$query->selectAlias($query->func()->sum('size'), 'size_sum')
->selectAlias($query->func()->min('size'), 'size_min')
->selectAlias($query->func()->max('unencrypted_size'), 'unencrypted_max')
->selectAlias(
$query->createFunction("SUM(CASE WHEN $unencryptedSizeCol > 0 THEN $unencryptedSizeCol ELSE $sizeCol END)"),
'unencrypted_sum'
)
->selectAlias(
$query->createFunction("MIN(CASE WHEN $unencryptedSizeCol > 0 THEN $unencryptedSizeCol ELSE $sizeCol END)"),
'unencrypted_min'
)
->from('filecache')
->whereStorageId($this->getNumericStorageId())
->whereParent($id);
@@ -1038,34 +1049,24 @@ class Cache implements ICache {
}
$result = $query->executeQuery();
$rows = $result->fetchAll();
$agg = $result->fetch();
$result->closeCursor();
if ($rows) {
$sizes = array_map(function (array $row) {
return Util::numericToNumber($row['size']);
}, $rows);
$unencryptedOnlySizes = array_map(function (array $row) {
return Util::numericToNumber($row['unencrypted_size']);
}, $rows);
$unencryptedSizes = array_map(function (array $row) {
return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
}, $rows);
$sum = array_sum($sizes);
$min = min($sizes);
$unencryptedSum = array_sum($unencryptedSizes);
$unencryptedMin = min($unencryptedSizes);
$unencryptedMax = max($unencryptedOnlySizes);
// SUM() returns NULL on empty set
if ($agg && $agg['size_sum'] !== null) {
$sum = Util::numericToNumber($agg['size_sum']);
$min = Util::numericToNumber($agg['size_min']);
$unencryptedMax = Util::numericToNumber($agg['unencrypted_max'] ?? 0);
$unencryptedSum = Util::numericToNumber($agg['unencrypted_sum'] ?? 0);
$unencryptedMin = Util::numericToNumber($agg['unencrypted_min'] ?? 0);
$sum = 0 + $sum;
$min = 0 + $min;
if ($min === -1) {
$totalSize = $min;
} else {
$totalSize = $sum;
}
$unencryptedSum = 0 + $unencryptedSum;
$unencryptedMin = 0 + $unencryptedMin;
$totalSize = ($min === -1) ? $min : $sum;
if ($unencryptedMin === -1 || $min === -1) {
$unencryptedTotal = $unencryptedMin;
} else {
@@ -1077,15 +1078,16 @@ class Cache implements ICache {
$unencryptedMax = 0;
}
// only set unencrypted size for a folder if any child entries have it set, or the folder is empty
// only set unencrypted size for a folder if any child entries have it set
// or if the folder is empty
$shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || ($entry['unencrypted_size'] ?? 0) > 0;
if ($entry['size'] !== $totalSize || (($entry['unencrypted_size'] ?? 0) !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
if ($shouldWriteUnEncryptedSize) {
// if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
// if all children have an unencrypted size of 0
// just set the folder unencrypted size to 0 instead of summing the sizes
if ($unencryptedMax === 0) {
$unencryptedTotal = 0;
}
$this->update($id, [
'size' => $totalSize,
'unencrypted_size' => $unencryptedTotal,
+15 -9
View File
@@ -75,7 +75,7 @@ class Propagator implements IPropagator {
$parentHashes = array_map('md5', $parents);
sort($parentHashes); // Ensure rows are always locked in the same order
$etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag
$etag = bin2hex(random_bytes(8)); // since we give all folders the same etag we don't ask the storage for the etag
$builder = $this->connection->getQueryBuilder();
$hashParams = array_map(static fn (string $hash): ILiteral => $builder->expr()->literal($hash), $parentHashes);
@@ -218,29 +218,32 @@ class Propagator implements IPropagator {
$query = $this->connection->getQueryBuilder();
$query->update('filecache')
->set('mtime', $query->func()->greatest('mtime', $query->createParameter('time')))
->set('etag', $query->expr()->literal(uniqid()))
->set('etag', $query->createParameter('etag'))
->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('fileid', $query->createParameter('fileid')));
$queryWithSize = $this->connection->getQueryBuilder();
$queryWithSize->update('filecache')
->set('mtime', $queryWithSize->func()->greatest('mtime', $queryWithSize->createParameter('time')))
->set('etag', $queryWithSize->expr()->literal(uniqid()))
->set('etag', $queryWithSize->createParameter('etag'))
->set('size', $queryWithSize->func()->add('size', $queryWithSize->createParameter('size')))
->where($queryWithSize->expr()->eq('storage', $queryWithSize->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
->andWhere($queryWithSize->expr()->eq('fileid', $queryWithSize->createParameter('fileid')));
while ($row = $result->fetchAssociative()) {
$item = $this->batch[$row['path']];
$newEtag = bin2hex(random_bytes(8));
if ($item['size'] && $row['size'] > -1) {
$queryWithSize->setParameter('fileid', $row['fileid'], IQueryBuilder::PARAM_INT)
->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT)
->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT)
->executeStatement();
->setParameter('etag', $newEtag, IQueryBuilder::PARAM_STR);
$queryWithSize->executeStatement();
} else {
$query->setParameter('fileid', $row['fileid'], IQueryBuilder::PARAM_INT)
->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT)
->executeStatement();
->setParameter('etag', $newEtag, IQueryBuilder::PARAM_STR);
$query->executeStatement();
}
}
}
@@ -249,28 +252,31 @@ class Propagator implements IPropagator {
$query = $this->connection->getQueryBuilder();
$query->update('filecache')
->set('mtime', $query->func()->greatest('mtime', $query->createParameter('time')))
->set('etag', $query->expr()->literal(uniqid()))
->set('etag', $query->createParameter('etag'))
->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')));
$queryWithSize = $this->connection->getQueryBuilder();
$queryWithSize->update('filecache')
->set('mtime', $queryWithSize->func()->greatest('mtime', $queryWithSize->createParameter('time')))
->set('etag', $queryWithSize->expr()->literal(uniqid()))
->set('etag', $queryWithSize->createParameter('etag'))
->set('size', $queryWithSize->func()->add('size', $queryWithSize->createParameter('size')))
->where($queryWithSize->expr()->eq('storage', $queryWithSize->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
->andWhere($queryWithSize->expr()->eq('path_hash', $queryWithSize->createParameter('hash')));
foreach ($this->batch as $item) {
$newEtag = bin2hex(random_bytes(8));
if ($item['size']) {
$queryWithSize->setParameter('hash', $item['hash'], IQueryBuilder::PARAM_STR)
->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT)
->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT)
->executeStatement();
->setParameter('etag', $newEtag, IQueryBuilder::PARAM_STR);
$queryWithSize->executeStatement();
} else {
$query->setParameter('hash', $item['hash'], IQueryBuilder::PARAM_STR)
->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT)
->executeStatement();
->setParameter('etag', $newEtag, IQueryBuilder::PARAM_STR);
$query->executeStatement();
}
}
}
+1 -1
View File
@@ -423,7 +423,7 @@ class Scanner extends BasicEmitter implements IScanner {
$updatedData['size'] = $size;
}
if ($etagChanged) {
$updatedData['etag'] = uniqid();
$updatedData['etag'] = bin2hex(random_bytes(8));
}
if ($updatedData) {
$this->cache->update($folderId, $updatedData);