From 842da3f18364c50ca907f4ac984ded1ecafd35fe Mon Sep 17 00:00:00 2001
From: Robin Appelman <robin@icewind.nl>
Date: Fri, 25 Oct 2019 18:21:57 +0200
Subject: [PATCH] store filecache extension fields

Signed-off-by: Robin Appelman <robin@icewind.nl>
---
 .../Version17000Date20190514105811.php        |  2 +-
 lib/private/Files/Cache/Cache.php             | 94 ++++++++++++++-----
 lib/private/Files/Cache/CacheEntry.php        | 12 +++
 lib/private/Files/Cache/CacheQueryBuilder.php | 31 ++++--
 lib/public/Files/Cache/ICacheEntry.php        | 24 +++++
 tests/lib/Files/Cache/CacheTest.php           | 61 ++++++++++++
 6 files changed, 196 insertions(+), 28 deletions(-)

diff --git a/core/Migrations/Version17000Date20190514105811.php b/core/Migrations/Version17000Date20190514105811.php
index 7f8084f9832..1cf4c9f5a37 100644
--- a/core/Migrations/Version17000Date20190514105811.php
+++ b/core/Migrations/Version17000Date20190514105811.php
@@ -44,7 +44,7 @@ class Version17000Date20190514105811 extends SimpleMigrationStep {
 		$schema = $schemaClosure();
 		if(!$schema->hasTable('filecache_extended')) {
 			$table = $schema->createTable('filecache_extended');
-			$table->addColumn('fileid', Type::INTEGER, [
+			$table->addColumn('fileid', Type::BIGINT, [
 				'notnull' => true,
 				'length' => 4,
 				'unsigned' => true,
diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php
index 2c63190eca0..cc4acd82927 100644
--- a/lib/private/Files/Cache/Cache.php
+++ b/lib/private/Files/Cache/Cache.php
@@ -187,6 +187,12 @@ class Cache implements ICache {
 			$data['storage_mtime'] = $data['mtime'];
 		}
 		$data['permissions'] = (int)$data['permissions'];
+		if (isset($data['creation_time'])) {
+			$data['creation_time'] = (int) $data['creation_time'];
+		}
+		if (isset($data['upload_time'])) {
+			$data['upload_time'] = (int) $data['upload_time'];
+		}
 		return new CacheEntry($data);
 	}
 
@@ -272,7 +278,7 @@ class Cache implements ICache {
 		$data['parent'] = $this->getParentId($file);
 		$data['name'] = basename($file);
 
-		$values = $this->normalizeData($data);
+		[$values, $extensionValues] = $this->normalizeData($data);
 		$values['storage'] = $this->getNumericStorageId();
 
 		try {
@@ -284,7 +290,17 @@ class Cache implements ICache {
 			}
 
 			if ($builder->execute()) {
-				$fileId = (int)$this->connection->lastInsertId('*PREFIX*filecache');
+				$fileId = $builder->getLastInsertId();
+
+				$query = $this->getQueryBuilder();
+				$query->insert('filecache_extended');
+
+				$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
+				foreach ($extensionValues as $column => $value) {
+					$query->setValue($column, $query->createNamedParameter($value));
+				}
+				$query->execute();
+
 				$this->eventDispatcher->dispatch(CacheInsertEvent::class, new CacheInsertEvent($this->storage, $file, $fileId));
 				return $fileId;
 			}
@@ -319,24 +335,44 @@ class Cache implements ICache {
 			$data['name'] = $this->normalize($data['name']);
 		}
 
-		$values = $this->normalizeData($data);
+		[$values, $extensionValues] = $this->normalizeData($data);
 
-		$query = $this->getQueryBuilder();
+		if (count($values)) {
+			$query = $this->getQueryBuilder();
 
-		$query->update('filecache')
-			->whereFileId($id)
-			->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
-				return $query->expr()->orX(
-					$query->expr()->neq($key, $query->createNamedParameter($value)),
-					$query->expr()->isNull($key)
-				);
-			}, array_keys($values), array_values($values))));
+			$query->update('filecache')
+				->whereFileId($id)
+				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
+					return $query->expr()->orX(
+						$query->expr()->neq($key, $query->createNamedParameter($value)),
+						$query->expr()->isNull($key)
+					);
+				}, array_keys($values), array_values($values))));
+
+			foreach ($values as $key => $value) {
+				$query->set($key, $query->createNamedParameter($value));
+			}
 
-		foreach ($values as $key => $value) {
-			$query->set($key, $query->createNamedParameter($value));
+			$query->execute();
 		}
 
-		$query->execute();
+		if (count($extensionValues)) {
+			$query = $this->getQueryBuilder();
+			$query->update('filecache_extended')
+				->whereFileId($id)
+				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
+					return $query->expr()->orX(
+						$query->expr()->neq($key, $query->createNamedParameter($value)),
+						$query->expr()->isNull($key)
+					);
+				}, array_keys($extensionValues), array_values($extensionValues))));
+
+			foreach ($extensionValues as $key => $value) {
+				$query->set($key, $query->createNamedParameter($value));
+			}
+
+			$query->execute();
+		}
 
 		$path = $this->getPathById($id);
 		// path can still be null if the file doesn't exist
@@ -355,6 +391,7 @@ class Cache implements ICache {
 		$fields = [
 			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
 			'etag', 'permissions', 'checksum', 'storage'];
+		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
 
 		$doNotCopyStorageMTime = false;
 		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
@@ -364,6 +401,7 @@ class Cache implements ICache {
 		}
 
 		$params = [];
+		$extensionParams = [];
 		foreach ($data as $name => $value) {
 			if (array_search($name, $fields) !== false) {
 				if ($name === 'path') {
@@ -385,8 +423,11 @@ class Cache implements ICache {
 				}
 				$params[$name] = $value;
 			}
+			if (array_search($name, $extensionFields) !== false) {
+				$extensionParams[$name] = $value;
+			}
 		}
-		return $params;
+		return [$params, $extensionParams];
 	}
 
 	/**
@@ -461,6 +502,12 @@ class Cache implements ICache {
 			$query->delete('filecache')
 				->whereFileId($entry->getId());
 			$query->execute();
+
+			$query = $this->getQueryBuilder();
+			$query->delete('filecache_extended')
+				->whereFileId($entry->getId());
+			$query->execute();
+
 			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
 				$this->removeChildren($entry);
 			}
@@ -496,6 +543,11 @@ class Cache implements ICache {
 		$query->delete('filecache')
 			->whereParent($entry->getId());
 		$query->execute();
+
+		$query = $this->getQueryBuilder();
+		$query->delete('filecache_extended')
+			->whereParent($entry->getId());
+		$query->execute();
 	}
 
 	/**
@@ -574,15 +626,16 @@ class Cache implements ICache {
 				}
 			}
 
-			$query = $this->connection->getQueryBuilder();
+			$query = $this->getQueryBuilder();
 			$query->update('filecache')
 				->set('storage', $query->createNamedParameter($targetStorageId))
 				->set('path', $query->createNamedParameter($targetPath))
 				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
 				->set('name', $query->createNamedParameter(basename($targetPath)))
 				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
-				->where($query->expr()->eq('fileid', $query->createNamedParameter($sourceId)));
+				->whereFileId($sourceId);
 			$query->execute();
+
 			$this->connection->commit();
 		} else {
 			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
@@ -595,7 +648,7 @@ class Cache implements ICache {
 	public function clear() {
 		$query = $this->getQueryBuilder();
 		$query->delete('filecache')
-			->whereStorageId();;
+			->whereStorageId();
 		$query->execute();
 
 		$query = $this->connection->getQueryBuilder();
@@ -705,8 +758,7 @@ class Cache implements ICache {
 	public function searchQuery(ISearchQuery $searchQuery) {
 		$builder = $this->getQueryBuilder();
 
-		$query = $builder->select(['fileid', 'storage', 'path', 'parent', 'name', 'mimetype', 'mimepart', 'size', 'mtime', 'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum'])
-			->from('filecache', 'file');
+		$query = $builder->selectFileCache('file');
 
 		$query->whereStorageId();
 
diff --git a/lib/private/Files/Cache/CacheEntry.php b/lib/private/Files/Cache/CacheEntry.php
index 4a2579a88f8..176a0bf27ed 100644
--- a/lib/private/Files/Cache/CacheEntry.php
+++ b/lib/private/Files/Cache/CacheEntry.php
@@ -109,6 +109,18 @@ class CacheEntry implements ICacheEntry, \ArrayAccess {
 		return isset($this->data['encrypted']) && $this->data['encrypted'];
 	}
 
+	public function getMetadataEtag(): ?string {
+		return $this->data['metadata_etag'];
+	}
+
+	public function getCreationTime(): ?int {
+		return $this->data['creation_time'];
+	}
+
+	public function getUploadTime(): ?int {
+		return $this->data['upload_time'];
+	}
+
 	public function getData() {
 		return $this->data;
 	}
diff --git a/lib/private/Files/Cache/CacheQueryBuilder.php b/lib/private/Files/Cache/CacheQueryBuilder.php
index 3d5fa453301..a5ff2129de8 100644
--- a/lib/private/Files/Cache/CacheQueryBuilder.php
+++ b/lib/private/Files/Cache/CacheQueryBuilder.php
@@ -32,6 +32,7 @@ use OCP\ILogger;
  */
 class CacheQueryBuilder extends QueryBuilder {
 	private $cache;
+	private $alias = null;
 
 	public function __construct(IDBConnection $connection, SystemConfig $systemConfig, ILogger $logger, Cache $cache) {
 		parent::__construct($connection, $systemConfig, $logger);
@@ -39,10 +40,14 @@ class CacheQueryBuilder extends QueryBuilder {
 		$this->cache = $cache;
 	}
 
-	public function selectFileCache() {
-		$this->select('fileid', 'storage', 'path', 'path_hash', 'parent', 'name', 'mimetype', 'mimepart', 'size', 'mtime',
-			'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum')
-			->from('filecache');
+	public function selectFileCache(string $alias = null) {
+		$name = $alias ? $alias : 'filecache';
+		$this->select("$name.fileid", 'storage', 'path', 'path_hash', "$name.parent", 'name', 'mimetype', 'mimepart', 'size', 'mtime',
+			'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum', 'metadata_etag', 'creation_time', 'upload_time')
+			->from('filecache', $name)
+			->leftJoin($name, 'filecache_extended', 'fe', $this->expr()->eq("$name.fileid", 'fe.fileid'));
+
+		$this->alias = $name;
 
 		return $this;
 	}
@@ -54,7 +59,14 @@ class CacheQueryBuilder extends QueryBuilder {
 	}
 
 	public function whereFileId(int $fileId) {
-		$this->andWhere($this->expr()->eq('fileid', $this->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
+		$alias = $this->alias;
+		if ($alias) {
+			$alias .= '.';
+		} else {
+			$alias = '';
+		}
+
+		$this->andWhere($this->expr()->eq("{$alias}fileid", $this->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
 
 		return $this;
 	}
@@ -66,7 +78,14 @@ class CacheQueryBuilder extends QueryBuilder {
 	}
 
 	public function whereParent(int $parent) {
-		$this->andWhere($this->expr()->eq('parent', $this->createNamedParameter($parent, IQueryBuilder::PARAM_INT)));
+		$alias = $this->alias;
+		if ($alias) {
+			$alias .= '.';
+		} else {
+			$alias = '';
+		}
+
+		$this->andWhere($this->expr()->eq("{$alias}parent", $this->createNamedParameter($parent, IQueryBuilder::PARAM_INT)));
 
 		return $this;
 	}
diff --git a/lib/public/Files/Cache/ICacheEntry.php b/lib/public/Files/Cache/ICacheEntry.php
index bbc9982935e..5223720b006 100644
--- a/lib/public/Files/Cache/ICacheEntry.php
+++ b/lib/public/Files/Cache/ICacheEntry.php
@@ -132,4 +132,28 @@ interface ICacheEntry {
 	 * @since 9.0.0
 	 */
 	public function isEncrypted();
+
+	/**
+	 * Get the metadata etag for the file
+	 *
+	 * @return string | null
+	 * @since 18.0.0
+	 */
+	public function getMetadataEtag(): ?string;
+
+	/**
+	 * Get the last modified date as unix timestamp
+	 *
+	 * @return int | null
+	 * @since 18.0.0
+	 */
+	public function getCreationTime(): ?int;
+
+	/**
+	 * Get the last modified date as unix timestamp
+	 *
+	 * @return int | null
+	 * @since 18.0.0
+	 */
+	public function getUploadTime(): ?int;
 }
diff --git a/tests/lib/Files/Cache/CacheTest.php b/tests/lib/Files/Cache/CacheTest.php
index eaea1692fe2..4ecccf2210d 100644
--- a/tests/lib/Files/Cache/CacheTest.php
+++ b/tests/lib/Files/Cache/CacheTest.php
@@ -707,6 +707,67 @@ class CacheTest extends \Test\TestCase {
 		}
 	}
 
+	public function testExtended() {
+		$folderData = ['size' => 100, 'mtime' => 50, 'mimetype' => 'httpd/unix-directory'];
+		$this->cache->put("", $folderData);
+
+		$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'creation_time' => 20];
+		$id1 = $this->cache->put("foo1", $data);
+		$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'upload_time' => 30];
+		$this->cache->put("foo2", $data);
+		$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain', 'metadata_etag' => 'foo'];
+		$this->cache->put("foo3", $data);
+		$data = ['size' => 100, 'mtime' => 50, 'mimetype' => 'text/plain'];
+		$this->cache->put("foo4", $data);
+
+		$entry = $this->cache->get($id1);
+		$this->assertEquals(20, $entry->getCreationTime());
+		$this->assertEquals(0, $entry->getUploadTime());
+		$this->assertEquals(null, $entry->getMetadataEtag());
+
+		$entries = $this->cache->getFolderContents("");
+		$this->assertCount(4, $entries);
+
+		$this->assertEquals("foo1", $entries[0]->getName());
+		$this->assertEquals("foo2", $entries[1]->getName());
+		$this->assertEquals("foo3", $entries[2]->getName());
+		$this->assertEquals("foo4", $entries[3]->getName());
+
+		$this->assertEquals(20, $entries[0]->getCreationTime());
+		$this->assertEquals(0, $entries[0]->getUploadTime());
+		$this->assertEquals(null, $entries[0]->getMetadataEtag());
+
+		$this->assertEquals(0, $entries[1]->getCreationTime());
+		$this->assertEquals(30, $entries[1]->getUploadTime());
+		$this->assertEquals(null, $entries[1]->getMetadataEtag());
+
+		$this->assertEquals(0, $entries[2]->getCreationTime());
+		$this->assertEquals(0, $entries[2]->getUploadTime());
+		$this->assertEquals('foo', $entries[2]->getMetadataEtag());
+
+		$this->assertEquals(0, $entries[3]->getCreationTime());
+		$this->assertEquals(0, $entries[3]->getUploadTime());
+		$this->assertEquals(null, $entries[3]->getMetadataEtag());
+
+		$this->cache->update($id1, ['upload_time' => 25]);
+
+		$entry = $this->cache->get($id1);
+		$this->assertEquals(20, $entry->getCreationTime());
+		$this->assertEquals(25, $entry->getUploadTime());
+		$this->assertEquals(null, $entry->getMetadataEtag());
+
+		$this->cache->put("sub", $folderData);
+
+		$this->cache->move("foo1", "sub/foo1");
+
+		$entries = $this->cache->getFolderContents("sub");
+		$this->assertCount(1, $entries);
+
+		$this->assertEquals(20, $entries[0]->getCreationTime());
+		$this->assertEquals(25, $entries[0]->getUploadTime());
+		$this->assertEquals(null, $entries[0]->getMetadataEtag());
+	}
+
 	protected function tearDown() {
 		if ($this->cache) {
 			$this->cache->clear();
-- 
GitLab