Passed
Push — master ( 402b51...60681d )
by Morris
11:03
created

Propagator::getParents()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Lukas Reschke <[email protected]>
6
 * @author Robin Appelman <[email protected]>
7
 *
8
 * @license AGPL-3.0
9
 *
10
 * This code is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License, version 3,
12
 * as published by the Free Software Foundation.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License, version 3,
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21
 *
22
 */
23
24
namespace OC\Files\Cache;
25
26
use OCP\DB\QueryBuilder\IQueryBuilder;
27
use OCP\Files\Cache\IPropagator;
28
use OCP\IDBConnection;
29
30
/**
31
 * Propagate etags and mtimes within the storage
32
 */
33
class Propagator implements IPropagator {
34
	private $inBatch = false;
35
36
	private $batch = [];
37
38
	/**
39
	 * @var \OC\Files\Storage\Storage
40
	 */
41
	protected $storage;
42
43
	/**
44
	 * @var IDBConnection
45
	 */
46
	private $connection;
47
48
	/**
49
	 * @var array
50
	 */
51
	private $ignore = [];
52
53
	public function __construct(\OC\Files\Storage\Storage $storage, IDBConnection $connection, array $ignore = []) {
54
		$this->storage = $storage;
55
		$this->connection = $connection;
56
		$this->ignore = $ignore;
57
	}
58
59
60
	/**
61
	 * @param string $internalPath
62
	 * @param int $time
63
	 * @param int $sizeDifference number of bytes the file has grown
64
	 * @suppress SqlInjectionChecker
65
	 */
66
	public function propagateChange($internalPath, $time, $sizeDifference = 0) {
67
		// Do not propogate changes in ignored paths
68
		foreach ($this->ignore as $ignore) {
69
			if (strpos($internalPath, $ignore) === 0) {
70
				return;
71
			}
72
		}
73
74
		$storageId = (int)$this->storage->getStorageCache()->getNumericId();
75
76
		$parents = $this->getParents($internalPath);
77
78
		if ($this->inBatch) {
79
			foreach ($parents as $parent) {
80
				$this->addToBatch($parent, $time, $sizeDifference);
81
			}
82
			return;
83
		}
84
85
		$parentHashes = array_map('md5', $parents);
86
		$etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag
87
88
		$builder = $this->connection->getQueryBuilder();
89
		$hashParams = array_map(function ($hash) use ($builder) {
90
			return $builder->expr()->literal($hash);
91
		}, $parentHashes);
92
93
		$builder->update('filecache')
94
			->set('mtime', $builder->createFunction('GREATEST(' . $builder->getColumnName('mtime') . ', ' . $builder->createNamedParameter((int)$time, IQueryBuilder::PARAM_INT) . ')'))
95
			->set('etag', $builder->createNamedParameter($etag, IQueryBuilder::PARAM_STR))
96
			->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
97
			->andWhere($builder->expr()->in('path_hash', $hashParams));
98
99
		$builder->execute();
100
101
		if ($sizeDifference !== 0) {
102
			// we need to do size separably so we can ignore entries with uncalculated size
103
			$builder = $this->connection->getQueryBuilder();
104
			$builder->update('filecache')
105
				->set('size', $builder->func()->add('size', $builder->createNamedParameter($sizeDifference)))
106
				->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
107
				->andWhere($builder->expr()->in('path_hash', $hashParams))
108
				->andWhere($builder->expr()->gt('size', $builder->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
109
110
			$builder->execute();
111
		}
112
	}
113
114
	protected function getParents($path) {
115
		$parts = explode('/', $path);
116
		$parent = '';
117
		$parents = [];
118
		foreach ($parts as $part) {
119
			$parents[] = $parent;
120
			$parent = trim($parent . '/' . $part, '/');
121
		}
122
		return $parents;
123
	}
124
125
	/**
126
	 * Mark the beginning of a propagation batch
127
	 *
128
	 * Note that not all cache setups support propagation in which case this will be a noop
129
	 *
130
	 * Batching for cache setups that do support it has to be explicit since the cache state is not fully consistent
131
	 * before the batch is committed.
132
	 */
133
	public function beginBatch() {
134
		$this->inBatch = true;
135
	}
136
137
	private function addToBatch($internalPath, $time, $sizeDifference) {
138
		if (!isset($this->batch[$internalPath])) {
139
			$this->batch[$internalPath] = [
140
				'hash' => md5($internalPath),
141
				'time' => $time,
142
				'size' => $sizeDifference
143
			];
144
		} else {
145
			$this->batch[$internalPath]['size'] += $sizeDifference;
146
			if ($time > $this->batch[$internalPath]['time']) {
147
				$this->batch[$internalPath]['time'] = $time;
148
			}
149
		}
150
	}
151
152
	/**
153
	 * Commit the active propagation batch
154
	 * @suppress SqlInjectionChecker
155
	 */
156
	public function commitBatch() {
157
		if (!$this->inBatch) {
158
			throw new \BadMethodCallException('Not in batch');
159
		}
160
		$this->inBatch = false;
161
162
		$this->connection->beginTransaction();
163
164
		$query = $this->connection->getQueryBuilder();
165
		$storageId = (int)$this->storage->getStorageCache()->getNumericId();
166
167
		$query->update('filecache')
168
			->set('mtime', $query->createFunction('GREATEST(' . $query->getColumnName('mtime') . ', ' . $query->createParameter('time') . ')'))
169
			->set('etag', $query->expr()->literal(uniqid()))
170
			->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
171
			->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')));
172
173
		$sizeQuery = $this->connection->getQueryBuilder();
174
		$sizeQuery->update('filecache')
175
			->set('size', $sizeQuery->func()->add('size', $sizeQuery->createParameter('size')))
176
			->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
177
			->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')))
178
			->andWhere($sizeQuery->expr()->gt('size', $sizeQuery->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
179
180
		foreach ($this->batch as $item) {
181
			$query->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT);
182
			$query->setParameter('hash', $item['hash']);
183
184
			$query->execute();
185
186
			if ($item['size']) {
187
				$sizeQuery->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT);
188
				$sizeQuery->setParameter('hash', $item['hash']);
189
190
				$sizeQuery->execute();
191
			}
192
		}
193
194
		$this->batch = [];
195
196
		$this->connection->commit();
197
	}
198
199
200
}
201