Completed
Push — master ( eaeaba...32c149 )
by
unknown
30:18
created
lib/private/Files/Storage/Wrapper/Availability.php 1 patch
Indentation   +261 added lines, -261 removed lines patch added patch discarded remove patch
@@ -18,265 +18,265 @@
 block discarded – undo
18 18
  * Throws a StorageNotAvailableException for storages with known failures
19 19
  */
20 20
 class Availability extends Wrapper {
21
-	public const RECHECK_TTL_SEC = 600; // 10 minutes
22
-
23
-	/** @var IConfig */
24
-	protected $config;
25
-	protected ?bool $available = null;
26
-
27
-	public function __construct(array $parameters) {
28
-		$this->config = $parameters['config'] ?? \OCP\Server::get(IConfig::class);
29
-		parent::__construct($parameters);
30
-	}
31
-
32
-	public static function shouldRecheck($availability): bool {
33
-		if (!$availability['available']) {
34
-			// trigger a recheck if TTL reached
35
-			if ((time() - $availability['last_checked']) > self::RECHECK_TTL_SEC) {
36
-				return true;
37
-			}
38
-		}
39
-		return false;
40
-	}
41
-
42
-	/**
43
-	 * Only called if availability === false
44
-	 */
45
-	private function updateAvailability(): bool {
46
-		// reset availability to false so that multiple requests don't recheck concurrently
47
-		$this->setAvailability(false);
48
-		try {
49
-			$result = $this->test();
50
-		} catch (\Exception $e) {
51
-			$result = false;
52
-		}
53
-		$this->setAvailability($result);
54
-		return $result;
55
-	}
56
-
57
-	private function isAvailable(): bool {
58
-		if (is_null($this->available)) {
59
-			$availability = $this->getAvailability();
60
-			if (self::shouldRecheck($availability)) {
61
-				return $this->updateAvailability();
62
-			}
63
-			$this->available = $availability['available'];
64
-		}
65
-		return $this->available;
66
-	}
67
-
68
-	/**
69
-	 * @throws StorageNotAvailableException
70
-	 */
71
-	private function checkAvailability(): void {
72
-		if (!$this->isAvailable()) {
73
-			throw new StorageNotAvailableException();
74
-		}
75
-	}
76
-
77
-	/**
78
-	 * Handles availability checks and delegates method calls dynamically
79
-	 */
80
-	private function handleAvailability(string $method, mixed ...$args): mixed {
81
-		$this->checkAvailability();
82
-		try {
83
-			return call_user_func_array([parent::class, $method], $args);
84
-		} catch (StorageNotAvailableException $e) {
85
-			$this->setUnavailable($e);
86
-			return false;
87
-		}
88
-	}
89
-
90
-	public function mkdir(string $path): bool {
91
-		return $this->handleAvailability('mkdir', $path);
92
-	}
93
-
94
-	public function rmdir(string $path): bool {
95
-		return $this->handleAvailability('rmdir', $path);
96
-	}
97
-
98
-	public function opendir(string $path) {
99
-		return $this->handleAvailability('opendir', $path);
100
-	}
101
-
102
-	public function is_dir(string $path): bool {
103
-		return $this->handleAvailability('is_dir', $path);
104
-	}
105
-
106
-	public function is_file(string $path): bool {
107
-		return $this->handleAvailability('is_file', $path);
108
-	}
109
-
110
-	public function stat(string $path): array|false {
111
-		return $this->handleAvailability('stat', $path);
112
-	}
113
-
114
-	public function filetype(string $path): string|false {
115
-		return $this->handleAvailability('filetype', $path);
116
-	}
117
-
118
-	public function filesize(string $path): int|float|false {
119
-		return $this->handleAvailability('filesize', $path);
120
-	}
121
-
122
-	public function isCreatable(string $path): bool {
123
-		return $this->handleAvailability('isCreatable', $path);
124
-	}
125
-
126
-	public function isReadable(string $path): bool {
127
-		return $this->handleAvailability('isReadable', $path);
128
-	}
129
-
130
-	public function isUpdatable(string $path): bool {
131
-		return $this->handleAvailability('isUpdatable', $path);
132
-	}
133
-
134
-	public function isDeletable(string $path): bool {
135
-		return $this->handleAvailability('isDeletable', $path);
136
-	}
137
-
138
-	public function isSharable(string $path): bool {
139
-		return $this->handleAvailability('isSharable', $path);
140
-	}
141
-
142
-	public function getPermissions(string $path): int {
143
-		return $this->handleAvailability('getPermissions', $path);
144
-	}
145
-
146
-	public function file_exists(string $path): bool {
147
-		if ($path === '') {
148
-			return true;
149
-		}
150
-		return $this->handleAvailability('file_exists', $path);
151
-	}
152
-
153
-	public function filemtime(string $path): int|false {
154
-		return $this->handleAvailability('filemtime', $path);
155
-	}
156
-
157
-	public function file_get_contents(string $path): string|false {
158
-		return $this->handleAvailability('file_get_contents', $path);
159
-	}
160
-
161
-	public function file_put_contents(string $path, mixed $data): int|float|false {
162
-		return $this->handleAvailability('file_put_contents', $path, $data);
163
-	}
164
-
165
-	public function unlink(string $path): bool {
166
-		return $this->handleAvailability('unlink', $path);
167
-	}
168
-
169
-	public function rename(string $source, string $target): bool {
170
-		return $this->handleAvailability('rename', $source, $target);
171
-	}
172
-
173
-	public function copy(string $source, string $target): bool {
174
-		return $this->handleAvailability('copy', $source, $target);
175
-	}
176
-
177
-	public function fopen(string $path, string $mode) {
178
-		return $this->handleAvailability('fopen', $path, $mode);
179
-	}
180
-
181
-	public function getMimeType(string $path): string|false {
182
-		return $this->handleAvailability('getMimeType', $path);
183
-	}
184
-
185
-	public function hash(string $type, string $path, bool $raw = false): string|false {
186
-		return $this->handleAvailability('hash', $type, $path, $raw);
187
-	}
188
-
189
-	public function free_space(string $path): int|float|false {
190
-		return $this->handleAvailability('free_space', $path);
191
-	}
192
-
193
-	public function touch(string $path, ?int $mtime = null): bool {
194
-		return $this->handleAvailability('touch', $path, $mtime);
195
-	}
196
-
197
-	public function getLocalFile(string $path): string|false {
198
-		return $this->handleAvailability('getLocalFile', $path);
199
-	}
200
-
201
-	public function hasUpdated(string $path, int $time): bool {
202
-		if (!$this->isAvailable()) {
203
-			return false;
204
-		}
205
-		try {
206
-			return parent::hasUpdated($path, $time);
207
-		} catch (StorageNotAvailableException $e) {
208
-			// set unavailable but don't rethrow
209
-			$this->setUnavailable(null);
210
-			return false;
211
-		}
212
-	}
213
-
214
-	public function getOwner(string $path): string|false {
215
-		try {
216
-			return parent::getOwner($path);
217
-		} catch (StorageNotAvailableException $e) {
218
-			$this->setUnavailable($e);
219
-			return false;
220
-		}
221
-	}
222
-
223
-	public function getETag(string $path): string|false {
224
-		return $this->handleAvailability('getETag', $path);
225
-	}
226
-
227
-	public function getDirectDownload(string $path): array|false {
228
-		return $this->handleAvailability('getDirectDownload', $path);
229
-	}
230
-
231
-	public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
232
-		return $this->handleAvailability('copyFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath);
233
-	}
234
-
235
-	public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
236
-		return $this->handleAvailability('moveFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath);
237
-	}
238
-
239
-	public function getMetaData(string $path): ?array {
240
-		$this->checkAvailability();
241
-		try {
242
-			return parent::getMetaData($path);
243
-		} catch (StorageNotAvailableException $e) {
244
-			$this->setUnavailable($e);
245
-			return null;
246
-		}
247
-	}
248
-
249
-	/**
250
-	 * @template T of StorageNotAvailableException|null
251
-	 * @param T $e
252
-	 * @psalm-return (T is null ? void : never)
253
-	 * @throws StorageNotAvailableException
254
-	 */
255
-	protected function setUnavailable(?StorageNotAvailableException $e): void {
256
-		$delay = self::RECHECK_TTL_SEC;
257
-		if ($e instanceof StorageAuthException) {
258
-			$delay = max(
259
-				// 30min
260
-				$this->config->getSystemValueInt('external_storage.auth_availability_delay', 1800),
261
-				self::RECHECK_TTL_SEC
262
-			);
263
-		}
264
-		$this->available = false;
265
-		$this->getStorageCache()->setAvailability(false, $delay);
266
-		if ($e !== null) {
267
-			throw $e;
268
-		}
269
-	}
270
-
271
-
272
-
273
-	public function getDirectoryContent(string $directory): \Traversable {
274
-		$this->checkAvailability();
275
-		try {
276
-			return parent::getDirectoryContent($directory);
277
-		} catch (StorageNotAvailableException $e) {
278
-			$this->setUnavailable($e);
279
-			return new \EmptyIterator();
280
-		}
281
-	}
21
+    public const RECHECK_TTL_SEC = 600; // 10 minutes
22
+
23
+    /** @var IConfig */
24
+    protected $config;
25
+    protected ?bool $available = null;
26
+
27
+    public function __construct(array $parameters) {
28
+        $this->config = $parameters['config'] ?? \OCP\Server::get(IConfig::class);
29
+        parent::__construct($parameters);
30
+    }
31
+
32
+    public static function shouldRecheck($availability): bool {
33
+        if (!$availability['available']) {
34
+            // trigger a recheck if TTL reached
35
+            if ((time() - $availability['last_checked']) > self::RECHECK_TTL_SEC) {
36
+                return true;
37
+            }
38
+        }
39
+        return false;
40
+    }
41
+
42
+    /**
43
+     * Only called if availability === false
44
+     */
45
+    private function updateAvailability(): bool {
46
+        // reset availability to false so that multiple requests don't recheck concurrently
47
+        $this->setAvailability(false);
48
+        try {
49
+            $result = $this->test();
50
+        } catch (\Exception $e) {
51
+            $result = false;
52
+        }
53
+        $this->setAvailability($result);
54
+        return $result;
55
+    }
56
+
57
+    private function isAvailable(): bool {
58
+        if (is_null($this->available)) {
59
+            $availability = $this->getAvailability();
60
+            if (self::shouldRecheck($availability)) {
61
+                return $this->updateAvailability();
62
+            }
63
+            $this->available = $availability['available'];
64
+        }
65
+        return $this->available;
66
+    }
67
+
68
+    /**
69
+     * @throws StorageNotAvailableException
70
+     */
71
+    private function checkAvailability(): void {
72
+        if (!$this->isAvailable()) {
73
+            throw new StorageNotAvailableException();
74
+        }
75
+    }
76
+
77
+    /**
78
+     * Handles availability checks and delegates method calls dynamically
79
+     */
80
+    private function handleAvailability(string $method, mixed ...$args): mixed {
81
+        $this->checkAvailability();
82
+        try {
83
+            return call_user_func_array([parent::class, $method], $args);
84
+        } catch (StorageNotAvailableException $e) {
85
+            $this->setUnavailable($e);
86
+            return false;
87
+        }
88
+    }
89
+
90
+    public function mkdir(string $path): bool {
91
+        return $this->handleAvailability('mkdir', $path);
92
+    }
93
+
94
+    public function rmdir(string $path): bool {
95
+        return $this->handleAvailability('rmdir', $path);
96
+    }
97
+
98
+    public function opendir(string $path) {
99
+        return $this->handleAvailability('opendir', $path);
100
+    }
101
+
102
+    public function is_dir(string $path): bool {
103
+        return $this->handleAvailability('is_dir', $path);
104
+    }
105
+
106
+    public function is_file(string $path): bool {
107
+        return $this->handleAvailability('is_file', $path);
108
+    }
109
+
110
+    public function stat(string $path): array|false {
111
+        return $this->handleAvailability('stat', $path);
112
+    }
113
+
114
+    public function filetype(string $path): string|false {
115
+        return $this->handleAvailability('filetype', $path);
116
+    }
117
+
118
+    public function filesize(string $path): int|float|false {
119
+        return $this->handleAvailability('filesize', $path);
120
+    }
121
+
122
+    public function isCreatable(string $path): bool {
123
+        return $this->handleAvailability('isCreatable', $path);
124
+    }
125
+
126
+    public function isReadable(string $path): bool {
127
+        return $this->handleAvailability('isReadable', $path);
128
+    }
129
+
130
+    public function isUpdatable(string $path): bool {
131
+        return $this->handleAvailability('isUpdatable', $path);
132
+    }
133
+
134
+    public function isDeletable(string $path): bool {
135
+        return $this->handleAvailability('isDeletable', $path);
136
+    }
137
+
138
+    public function isSharable(string $path): bool {
139
+        return $this->handleAvailability('isSharable', $path);
140
+    }
141
+
142
+    public function getPermissions(string $path): int {
143
+        return $this->handleAvailability('getPermissions', $path);
144
+    }
145
+
146
+    public function file_exists(string $path): bool {
147
+        if ($path === '') {
148
+            return true;
149
+        }
150
+        return $this->handleAvailability('file_exists', $path);
151
+    }
152
+
153
+    public function filemtime(string $path): int|false {
154
+        return $this->handleAvailability('filemtime', $path);
155
+    }
156
+
157
+    public function file_get_contents(string $path): string|false {
158
+        return $this->handleAvailability('file_get_contents', $path);
159
+    }
160
+
161
+    public function file_put_contents(string $path, mixed $data): int|float|false {
162
+        return $this->handleAvailability('file_put_contents', $path, $data);
163
+    }
164
+
165
+    public function unlink(string $path): bool {
166
+        return $this->handleAvailability('unlink', $path);
167
+    }
168
+
169
+    public function rename(string $source, string $target): bool {
170
+        return $this->handleAvailability('rename', $source, $target);
171
+    }
172
+
173
+    public function copy(string $source, string $target): bool {
174
+        return $this->handleAvailability('copy', $source, $target);
175
+    }
176
+
177
+    public function fopen(string $path, string $mode) {
178
+        return $this->handleAvailability('fopen', $path, $mode);
179
+    }
180
+
181
+    public function getMimeType(string $path): string|false {
182
+        return $this->handleAvailability('getMimeType', $path);
183
+    }
184
+
185
+    public function hash(string $type, string $path, bool $raw = false): string|false {
186
+        return $this->handleAvailability('hash', $type, $path, $raw);
187
+    }
188
+
189
+    public function free_space(string $path): int|float|false {
190
+        return $this->handleAvailability('free_space', $path);
191
+    }
192
+
193
+    public function touch(string $path, ?int $mtime = null): bool {
194
+        return $this->handleAvailability('touch', $path, $mtime);
195
+    }
196
+
197
+    public function getLocalFile(string $path): string|false {
198
+        return $this->handleAvailability('getLocalFile', $path);
199
+    }
200
+
201
+    public function hasUpdated(string $path, int $time): bool {
202
+        if (!$this->isAvailable()) {
203
+            return false;
204
+        }
205
+        try {
206
+            return parent::hasUpdated($path, $time);
207
+        } catch (StorageNotAvailableException $e) {
208
+            // set unavailable but don't rethrow
209
+            $this->setUnavailable(null);
210
+            return false;
211
+        }
212
+    }
213
+
214
+    public function getOwner(string $path): string|false {
215
+        try {
216
+            return parent::getOwner($path);
217
+        } catch (StorageNotAvailableException $e) {
218
+            $this->setUnavailable($e);
219
+            return false;
220
+        }
221
+    }
222
+
223
+    public function getETag(string $path): string|false {
224
+        return $this->handleAvailability('getETag', $path);
225
+    }
226
+
227
+    public function getDirectDownload(string $path): array|false {
228
+        return $this->handleAvailability('getDirectDownload', $path);
229
+    }
230
+
231
+    public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
232
+        return $this->handleAvailability('copyFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath);
233
+    }
234
+
235
+    public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
236
+        return $this->handleAvailability('moveFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath);
237
+    }
238
+
239
+    public function getMetaData(string $path): ?array {
240
+        $this->checkAvailability();
241
+        try {
242
+            return parent::getMetaData($path);
243
+        } catch (StorageNotAvailableException $e) {
244
+            $this->setUnavailable($e);
245
+            return null;
246
+        }
247
+    }
248
+
249
+    /**
250
+     * @template T of StorageNotAvailableException|null
251
+     * @param T $e
252
+     * @psalm-return (T is null ? void : never)
253
+     * @throws StorageNotAvailableException
254
+     */
255
+    protected function setUnavailable(?StorageNotAvailableException $e): void {
256
+        $delay = self::RECHECK_TTL_SEC;
257
+        if ($e instanceof StorageAuthException) {
258
+            $delay = max(
259
+                // 30min
260
+                $this->config->getSystemValueInt('external_storage.auth_availability_delay', 1800),
261
+                self::RECHECK_TTL_SEC
262
+            );
263
+        }
264
+        $this->available = false;
265
+        $this->getStorageCache()->setAvailability(false, $delay);
266
+        if ($e !== null) {
267
+            throw $e;
268
+        }
269
+    }
270
+
271
+
272
+
273
+    public function getDirectoryContent(string $directory): \Traversable {
274
+        $this->checkAvailability();
275
+        try {
276
+            return parent::getDirectoryContent($directory);
277
+        } catch (StorageNotAvailableException $e) {
278
+            $this->setUnavailable($e);
279
+            return new \EmptyIterator();
280
+        }
281
+    }
282 282
 }
Please login to merge, or discard this patch.
lib/private/Files/Storage/Common.php 1 patch
Indentation   +714 added lines, -714 removed lines patch added patch discarded remove patch
@@ -52,718 +52,718 @@
 block discarded – undo
52 52
  * in classes which extend it, e.g. $this->stat() .
53 53
  */
54 54
 abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage, IConstructableStorage {
55
-	use LocalTempFileTrait;
56
-
57
-	protected ?Cache $cache = null;
58
-	protected ?Scanner $scanner = null;
59
-	protected ?Watcher $watcher = null;
60
-	protected ?Propagator $propagator = null;
61
-	protected $storageCache;
62
-	protected ?Updater $updater = null;
63
-
64
-	protected array $mountOptions = [];
65
-	protected $owner = null;
66
-
67
-	private ?bool $shouldLogLocks = null;
68
-	private ?LoggerInterface $logger = null;
69
-	private ?IFilenameValidator $filenameValidator = null;
70
-
71
-	public function __construct(array $parameters) {
72
-	}
73
-
74
-	protected function remove(string $path): bool {
75
-		if ($this->file_exists($path)) {
76
-			if ($this->is_dir($path)) {
77
-				return $this->rmdir($path);
78
-			} elseif ($this->is_file($path)) {
79
-				return $this->unlink($path);
80
-			}
81
-		}
82
-		return false;
83
-	}
84
-
85
-	public function is_dir(string $path): bool {
86
-		return $this->filetype($path) === 'dir';
87
-	}
88
-
89
-	public function is_file(string $path): bool {
90
-		return $this->filetype($path) === 'file';
91
-	}
92
-
93
-	public function filesize(string $path): int|float|false {
94
-		if ($this->is_dir($path)) {
95
-			return 0; //by definition
96
-		} else {
97
-			$stat = $this->stat($path);
98
-			return isset($stat['size']) ? $stat['size'] : 0;
99
-		}
100
-	}
101
-
102
-	public function isReadable(string $path): bool {
103
-		// at least check whether it exists
104
-		// subclasses might want to implement this more thoroughly
105
-		return $this->file_exists($path);
106
-	}
107
-
108
-	public function isUpdatable(string $path): bool {
109
-		// at least check whether it exists
110
-		// subclasses might want to implement this more thoroughly
111
-		// a non-existing file/folder isn't updatable
112
-		return $this->file_exists($path);
113
-	}
114
-
115
-	public function isCreatable(string $path): bool {
116
-		if ($this->is_dir($path) && $this->isUpdatable($path)) {
117
-			return true;
118
-		}
119
-		return false;
120
-	}
121
-
122
-	public function isDeletable(string $path): bool {
123
-		if ($path === '' || $path === '/') {
124
-			return $this->isUpdatable($path);
125
-		}
126
-		$parent = dirname($path);
127
-		return $this->isUpdatable($parent) && $this->isUpdatable($path);
128
-	}
129
-
130
-	public function isSharable(string $path): bool {
131
-		return $this->isReadable($path);
132
-	}
133
-
134
-	public function getPermissions(string $path): int {
135
-		$permissions = 0;
136
-		if ($this->isCreatable($path)) {
137
-			$permissions |= \OCP\Constants::PERMISSION_CREATE;
138
-		}
139
-		if ($this->isReadable($path)) {
140
-			$permissions |= \OCP\Constants::PERMISSION_READ;
141
-		}
142
-		if ($this->isUpdatable($path)) {
143
-			$permissions |= \OCP\Constants::PERMISSION_UPDATE;
144
-		}
145
-		if ($this->isDeletable($path)) {
146
-			$permissions |= \OCP\Constants::PERMISSION_DELETE;
147
-		}
148
-		if ($this->isSharable($path)) {
149
-			$permissions |= \OCP\Constants::PERMISSION_SHARE;
150
-		}
151
-		return $permissions;
152
-	}
153
-
154
-	public function filemtime(string $path): int|false {
155
-		$stat = $this->stat($path);
156
-		if (isset($stat['mtime']) && $stat['mtime'] > 0) {
157
-			return $stat['mtime'];
158
-		} else {
159
-			return 0;
160
-		}
161
-	}
162
-
163
-	public function file_get_contents(string $path): string|false {
164
-		$handle = $this->fopen($path, 'r');
165
-		if (!$handle) {
166
-			return false;
167
-		}
168
-		$data = stream_get_contents($handle);
169
-		fclose($handle);
170
-		return $data;
171
-	}
172
-
173
-	public function file_put_contents(string $path, mixed $data): int|float|false {
174
-		$handle = $this->fopen($path, 'w');
175
-		if (!$handle) {
176
-			return false;
177
-		}
178
-		$this->removeCachedFile($path);
179
-		$count = fwrite($handle, $data);
180
-		fclose($handle);
181
-		return $count;
182
-	}
183
-
184
-	public function rename(string $source, string $target): bool {
185
-		$this->remove($target);
186
-
187
-		$this->removeCachedFile($source);
188
-		return $this->copy($source, $target) && $this->remove($source);
189
-	}
190
-
191
-	public function copy(string $source, string $target): bool {
192
-		if ($this->is_dir($source)) {
193
-			$this->remove($target);
194
-			$dir = $this->opendir($source);
195
-			$this->mkdir($target);
196
-			while (($file = readdir($dir)) !== false) {
197
-				if (!Filesystem::isIgnoredDir($file)) {
198
-					if (!$this->copy($source . '/' . $file, $target . '/' . $file)) {
199
-						closedir($dir);
200
-						return false;
201
-					}
202
-				}
203
-			}
204
-			closedir($dir);
205
-			return true;
206
-		} else {
207
-			$sourceStream = $this->fopen($source, 'r');
208
-			$targetStream = $this->fopen($target, 'w');
209
-			[, $result] = Files::streamCopy($sourceStream, $targetStream, true);
210
-			if (!$result) {
211
-				Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target");
212
-			}
213
-			$this->removeCachedFile($target);
214
-			return $result;
215
-		}
216
-	}
217
-
218
-	public function getMimeType(string $path): string|false {
219
-		if ($this->is_dir($path)) {
220
-			return 'httpd/unix-directory';
221
-		} elseif ($this->file_exists($path)) {
222
-			return \OC::$server->getMimeTypeDetector()->detectPath($path);
223
-		} else {
224
-			return false;
225
-		}
226
-	}
227
-
228
-	public function hash(string $type, string $path, bool $raw = false): string|false {
229
-		$fh = $this->fopen($path, 'rb');
230
-		if (!$fh) {
231
-			return false;
232
-		}
233
-		$ctx = hash_init($type);
234
-		hash_update_stream($ctx, $fh);
235
-		fclose($fh);
236
-		return hash_final($ctx, $raw);
237
-	}
238
-
239
-	public function getLocalFile(string $path): string|false {
240
-		return $this->getCachedFile($path);
241
-	}
242
-
243
-	private function addLocalFolder(string $path, string $target): void {
244
-		$dh = $this->opendir($path);
245
-		if (is_resource($dh)) {
246
-			while (($file = readdir($dh)) !== false) {
247
-				if (!Filesystem::isIgnoredDir($file)) {
248
-					if ($this->is_dir($path . '/' . $file)) {
249
-						mkdir($target . '/' . $file);
250
-						$this->addLocalFolder($path . '/' . $file, $target . '/' . $file);
251
-					} else {
252
-						$tmp = $this->toTmpFile($path . '/' . $file);
253
-						rename($tmp, $target . '/' . $file);
254
-					}
255
-				}
256
-			}
257
-		}
258
-	}
259
-
260
-	protected function searchInDir(string $query, string $dir = ''): array {
261
-		$files = [];
262
-		$dh = $this->opendir($dir);
263
-		if (is_resource($dh)) {
264
-			while (($item = readdir($dh)) !== false) {
265
-				if (Filesystem::isIgnoredDir($item)) {
266
-					continue;
267
-				}
268
-				if (strstr(strtolower($item), strtolower($query)) !== false) {
269
-					$files[] = $dir . '/' . $item;
270
-				}
271
-				if ($this->is_dir($dir . '/' . $item)) {
272
-					$files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
273
-				}
274
-			}
275
-		}
276
-		closedir($dh);
277
-		return $files;
278
-	}
279
-
280
-	/**
281
-	 * @inheritDoc
282
-	 * Check if a file or folder has been updated since $time
283
-	 *
284
-	 * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking
285
-	 * the mtime should always return false here. As a result storage implementations that always return false expect
286
-	 * exclusive access to the backend and will not pick up files that have been added in a way that circumvents
287
-	 * Nextcloud filesystem.
288
-	 */
289
-	public function hasUpdated(string $path, int $time): bool {
290
-		return $this->filemtime($path) > $time;
291
-	}
292
-
293
-	protected function getCacheDependencies(): CacheDependencies {
294
-		static $dependencies = null;
295
-		if (!$dependencies) {
296
-			$dependencies = Server::get(CacheDependencies::class);
297
-		}
298
-		return $dependencies;
299
-	}
300
-
301
-	public function getCache(string $path = '', ?IStorage $storage = null): ICache {
302
-		if (!$storage) {
303
-			$storage = $this;
304
-		}
305
-		/** @var self $storage */
306
-		if (!isset($storage->cache)) {
307
-			$storage->cache = new Cache($storage, $this->getCacheDependencies());
308
-		}
309
-		return $storage->cache;
310
-	}
311
-
312
-	public function getScanner(string $path = '', ?IStorage $storage = null): IScanner {
313
-		if (!$storage) {
314
-			$storage = $this;
315
-		}
316
-		if (!$storage->instanceOfStorage(self::class)) {
317
-			throw new \InvalidArgumentException('Storage is not of the correct class');
318
-		}
319
-		if (!isset($storage->scanner)) {
320
-			$storage->scanner = new Scanner($storage);
321
-		}
322
-		return $storage->scanner;
323
-	}
324
-
325
-	public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher {
326
-		if (!$storage) {
327
-			$storage = $this;
328
-		}
329
-		if (!isset($this->watcher)) {
330
-			$this->watcher = new Watcher($storage);
331
-			$globalPolicy = Server::get(IConfig::class)->getSystemValueInt('filesystem_check_changes', Watcher::CHECK_NEVER);
332
-			$this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy));
333
-		}
334
-		return $this->watcher;
335
-	}
336
-
337
-	public function getPropagator(?IStorage $storage = null): IPropagator {
338
-		if (!$storage) {
339
-			$storage = $this;
340
-		}
341
-		if (!$storage->instanceOfStorage(self::class)) {
342
-			throw new \InvalidArgumentException('Storage is not of the correct class');
343
-		}
344
-		/** @var self $storage */
345
-		if (!isset($storage->propagator)) {
346
-			$config = Server::get(IConfig::class);
347
-			$storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getSystemValueString('instanceid')]);
348
-		}
349
-		return $storage->propagator;
350
-	}
351
-
352
-	public function getUpdater(?IStorage $storage = null): IUpdater {
353
-		if (!$storage) {
354
-			$storage = $this;
355
-		}
356
-		if (!$storage->instanceOfStorage(self::class)) {
357
-			throw new \InvalidArgumentException('Storage is not of the correct class');
358
-		}
359
-		/** @var self $storage */
360
-		if (!isset($storage->updater)) {
361
-			$storage->updater = new Updater($storage);
362
-		}
363
-		return $storage->updater;
364
-	}
365
-
366
-	public function getStorageCache(?IStorage $storage = null): \OC\Files\Cache\Storage {
367
-		/** @var Cache $cache */
368
-		$cache = $this->getCache(storage: $storage);
369
-		return $cache->getStorageCache();
370
-	}
371
-
372
-	public function getOwner(string $path): string|false {
373
-		if ($this->owner === null) {
374
-			$this->owner = \OC_User::getUser();
375
-		}
376
-
377
-		return $this->owner;
378
-	}
379
-
380
-	public function getETag(string $path): string|false {
381
-		return uniqid();
382
-	}
383
-
384
-	/**
385
-	 * clean a path, i.e. remove all redundant '.' and '..'
386
-	 * making sure that it can't point to higher than '/'
387
-	 *
388
-	 * @param string $path The path to clean
389
-	 * @return string cleaned path
390
-	 */
391
-	public function cleanPath(string $path): string {
392
-		if (strlen($path) == 0 || $path[0] != '/') {
393
-			$path = '/' . $path;
394
-		}
395
-
396
-		$output = [];
397
-		foreach (explode('/', $path) as $chunk) {
398
-			if ($chunk == '..') {
399
-				array_pop($output);
400
-			} elseif ($chunk == '.') {
401
-			} else {
402
-				$output[] = $chunk;
403
-			}
404
-		}
405
-		return implode('/', $output);
406
-	}
407
-
408
-	/**
409
-	 * Test a storage for availability
410
-	 */
411
-	public function test(): bool {
412
-		try {
413
-			if ($this->stat('')) {
414
-				return true;
415
-			}
416
-			Server::get(LoggerInterface::class)->info('External storage not available: stat() failed');
417
-			return false;
418
-		} catch (\Exception $e) {
419
-			Server::get(LoggerInterface::class)->warning(
420
-				'External storage not available: ' . $e->getMessage(),
421
-				['exception' => $e]
422
-			);
423
-			return false;
424
-		}
425
-	}
426
-
427
-	public function free_space(string $path): int|float|false {
428
-		return \OCP\Files\FileInfo::SPACE_UNKNOWN;
429
-	}
430
-
431
-	public function isLocal(): bool {
432
-		// the common implementation returns a temporary file by
433
-		// default, which is not local
434
-		return false;
435
-	}
436
-
437
-	/**
438
-	 * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class
439
-	 */
440
-	public function instanceOfStorage(string $class): bool {
441
-		if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') {
442
-			// FIXME Temporary fix to keep existing checks working
443
-			$class = '\OCA\Files_Sharing\SharedStorage';
444
-		}
445
-		return is_a($this, $class);
446
-	}
447
-
448
-	/**
449
-	 * A custom storage implementation can return an url for direct download of a give file.
450
-	 *
451
-	 * For now the returned array can hold the parameter url - in future more attributes might follow.
452
-	 */
453
-	public function getDirectDownload(string $path): array|false {
454
-		return [];
455
-	}
456
-
457
-	public function verifyPath(string $path, string $fileName): void {
458
-		$this->getFilenameValidator()
459
-			->validateFilename($fileName);
460
-
461
-		// verify also the path is valid
462
-		if ($path && $path !== '/' && $path !== '.') {
463
-			try {
464
-				$this->verifyPath(dirname($path), basename($path));
465
-			} catch (InvalidPathException $e) {
466
-				// Ignore invalid file type exceptions on directories
467
-				if ($e->getCode() !== FilenameValidator::INVALID_FILE_TYPE) {
468
-					$l = \OCP\Util::getL10N('lib');
469
-					throw new InvalidPathException($l->t('Invalid parent path'), previous: $e);
470
-				}
471
-			}
472
-		}
473
-	}
474
-
475
-	/**
476
-	 * Get the filename validator
477
-	 * (cached for performance)
478
-	 */
479
-	protected function getFilenameValidator(): IFilenameValidator {
480
-		if ($this->filenameValidator === null) {
481
-			$this->filenameValidator = Server::get(IFilenameValidator::class);
482
-		}
483
-		return $this->filenameValidator;
484
-	}
485
-
486
-	public function setMountOptions(array $options): void {
487
-		$this->mountOptions = $options;
488
-	}
489
-
490
-	public function getMountOption(string $name, mixed $default = null): mixed {
491
-		return $this->mountOptions[$name] ?? $default;
492
-	}
493
-
494
-	public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool {
495
-		if ($sourceStorage === $this) {
496
-			return $this->copy($sourceInternalPath, $targetInternalPath);
497
-		}
498
-
499
-		if ($sourceStorage->is_dir($sourceInternalPath)) {
500
-			$dh = $sourceStorage->opendir($sourceInternalPath);
501
-			$result = $this->mkdir($targetInternalPath);
502
-			if (is_resource($dh)) {
503
-				$result = true;
504
-				while ($result && ($file = readdir($dh)) !== false) {
505
-					if (!Filesystem::isIgnoredDir($file)) {
506
-						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file);
507
-					}
508
-				}
509
-			}
510
-		} else {
511
-			$source = $sourceStorage->fopen($sourceInternalPath, 'r');
512
-			$result = false;
513
-			if ($source) {
514
-				try {
515
-					$this->writeStream($targetInternalPath, $source);
516
-					$result = true;
517
-				} catch (\Exception $e) {
518
-					Server::get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]);
519
-				}
520
-			}
521
-
522
-			if ($result && $preserveMtime) {
523
-				$mtime = $sourceStorage->filemtime($sourceInternalPath);
524
-				$this->touch($targetInternalPath, is_int($mtime) ? $mtime : null);
525
-			}
526
-
527
-			if (!$result) {
528
-				// delete partially written target file
529
-				$this->unlink($targetInternalPath);
530
-				// delete cache entry that was created by fopen
531
-				$this->getCache()->remove($targetInternalPath);
532
-			}
533
-		}
534
-		return (bool)$result;
535
-	}
536
-
537
-	/**
538
-	 * Check if a storage is the same as the current one, including wrapped storages
539
-	 */
540
-	private function isSameStorage(IStorage $storage): bool {
541
-		while ($storage->instanceOfStorage(Wrapper::class)) {
542
-			/**
543
-			 * @var Wrapper $storage
544
-			 */
545
-			$storage = $storage->getWrapperStorage();
546
-		}
547
-
548
-		return $storage === $this;
549
-	}
550
-
551
-	public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
552
-		if (
553
-			!$sourceStorage->instanceOfStorage(Encryption::class)
554
-			&& $this->isSameStorage($sourceStorage)
555
-		) {
556
-			// resolve any jailed paths
557
-			while ($sourceStorage->instanceOfStorage(Jail::class)) {
558
-				/**
559
-				 * @var Jail $sourceStorage
560
-				 */
561
-				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
562
-				$sourceStorage = $sourceStorage->getUnjailedStorage();
563
-			}
564
-
565
-			return $this->rename($sourceInternalPath, $targetInternalPath);
566
-		}
567
-
568
-		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
569
-			return false;
570
-		}
571
-
572
-		$result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true);
573
-		if ($result) {
574
-			if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
575
-				/** @var ObjectStoreStorage $sourceStorage */
576
-				$sourceStorage->setPreserveCacheOnDelete(true);
577
-			}
578
-			try {
579
-				if ($sourceStorage->is_dir($sourceInternalPath)) {
580
-					$result = $sourceStorage->rmdir($sourceInternalPath);
581
-				} else {
582
-					$result = $sourceStorage->unlink($sourceInternalPath);
583
-				}
584
-			} finally {
585
-				if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
586
-					/** @var ObjectStoreStorage $sourceStorage */
587
-					$sourceStorage->setPreserveCacheOnDelete(false);
588
-				}
589
-			}
590
-		}
591
-		return $result;
592
-	}
593
-
594
-	public function getMetaData(string $path): ?array {
595
-		if (Filesystem::isFileBlacklisted($path)) {
596
-			throw new ForbiddenException('Invalid path: ' . $path, false);
597
-		}
598
-
599
-		$permissions = $this->getPermissions($path);
600
-		if (!$permissions & \OCP\Constants::PERMISSION_READ) {
601
-			//can't read, nothing we can do
602
-			return null;
603
-		}
604
-
605
-		$data = [];
606
-		$data['mimetype'] = $this->getMimeType($path);
607
-		$data['mtime'] = $this->filemtime($path);
608
-		if ($data['mtime'] === false) {
609
-			$data['mtime'] = time();
610
-		}
611
-		if ($data['mimetype'] == 'httpd/unix-directory') {
612
-			$data['size'] = -1; //unknown
613
-		} else {
614
-			$data['size'] = $this->filesize($path);
615
-		}
616
-		$data['etag'] = $this->getETag($path);
617
-		$data['storage_mtime'] = $data['mtime'];
618
-		$data['permissions'] = $permissions;
619
-		$data['name'] = basename($path);
620
-
621
-		return $data;
622
-	}
623
-
624
-	public function acquireLock(string $path, int $type, ILockingProvider $provider): void {
625
-		$logger = $this->getLockLogger();
626
-		if ($logger) {
627
-			$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
628
-			$logger->info(
629
-				sprintf(
630
-					'acquire %s lock on "%s" on storage "%s"',
631
-					$typeString,
632
-					$path,
633
-					$this->getId()
634
-				),
635
-				[
636
-					'app' => 'locking',
637
-				]
638
-			);
639
-		}
640
-		try {
641
-			$provider->acquireLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type, $this->getId() . '::' . $path);
642
-		} catch (LockedException $e) {
643
-			$e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
644
-			if ($logger) {
645
-				$logger->info($e->getMessage(), ['exception' => $e]);
646
-			}
647
-			throw $e;
648
-		}
649
-	}
650
-
651
-	public function releaseLock(string $path, int $type, ILockingProvider $provider): void {
652
-		$logger = $this->getLockLogger();
653
-		if ($logger) {
654
-			$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
655
-			$logger->info(
656
-				sprintf(
657
-					'release %s lock on "%s" on storage "%s"',
658
-					$typeString,
659
-					$path,
660
-					$this->getId()
661
-				),
662
-				[
663
-					'app' => 'locking',
664
-				]
665
-			);
666
-		}
667
-		try {
668
-			$provider->releaseLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
669
-		} catch (LockedException $e) {
670
-			$e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
671
-			if ($logger) {
672
-				$logger->info($e->getMessage(), ['exception' => $e]);
673
-			}
674
-			throw $e;
675
-		}
676
-	}
677
-
678
-	public function changeLock(string $path, int $type, ILockingProvider $provider): void {
679
-		$logger = $this->getLockLogger();
680
-		if ($logger) {
681
-			$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
682
-			$logger->info(
683
-				sprintf(
684
-					'change lock on "%s" to %s on storage "%s"',
685
-					$path,
686
-					$typeString,
687
-					$this->getId()
688
-				),
689
-				[
690
-					'app' => 'locking',
691
-				]
692
-			);
693
-		}
694
-		try {
695
-			$provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
696
-		} catch (LockedException $e) {
697
-			$e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
698
-			if ($logger) {
699
-				$logger->info($e->getMessage(), ['exception' => $e]);
700
-			}
701
-			throw $e;
702
-		}
703
-	}
704
-
705
-	private function getLockLogger(): ?LoggerInterface {
706
-		if (is_null($this->shouldLogLocks)) {
707
-			$this->shouldLogLocks = Server::get(IConfig::class)->getSystemValueBool('filelocking.debug', false);
708
-			$this->logger = $this->shouldLogLocks ? Server::get(LoggerInterface::class) : null;
709
-		}
710
-		return $this->logger;
711
-	}
712
-
713
-	/**
714
-	 * @return array{available: bool, last_checked: int}
715
-	 */
716
-	public function getAvailability(): array {
717
-		return $this->getStorageCache()->getAvailability();
718
-	}
719
-
720
-	public function setAvailability(bool $isAvailable): void {
721
-		$this->getStorageCache()->setAvailability($isAvailable);
722
-	}
723
-
724
-	public function setOwner(?string $user): void {
725
-		$this->owner = $user;
726
-	}
727
-
728
-	public function needsPartFile(): bool {
729
-		return true;
730
-	}
731
-
732
-	public function writeStream(string $path, $stream, ?int $size = null): int {
733
-		$target = $this->fopen($path, 'w');
734
-		if (!$target) {
735
-			throw new GenericFileException("Failed to open $path for writing");
736
-		}
737
-		try {
738
-			[$count, $result] = Files::streamCopy($stream, $target, true);
739
-			if (!$result) {
740
-				throw new GenericFileException('Failed to copy stream');
741
-			}
742
-		} finally {
743
-			fclose($target);
744
-			fclose($stream);
745
-		}
746
-		return $count;
747
-	}
748
-
749
-	public function getDirectoryContent(string $directory): \Traversable {
750
-		$dh = $this->opendir($directory);
751
-
752
-		if ($dh === false) {
753
-			throw new StorageNotAvailableException('Directory listing failed');
754
-		}
755
-
756
-		if (is_resource($dh)) {
757
-			$basePath = rtrim($directory, '/');
758
-			while (($file = readdir($dh)) !== false) {
759
-				if (!Filesystem::isIgnoredDir($file)) {
760
-					$childPath = $basePath . '/' . trim($file, '/');
761
-					$metadata = $this->getMetaData($childPath);
762
-					if ($metadata !== null) {
763
-						yield $metadata;
764
-					}
765
-				}
766
-			}
767
-		}
768
-	}
55
+    use LocalTempFileTrait;
56
+
57
+    protected ?Cache $cache = null;
58
+    protected ?Scanner $scanner = null;
59
+    protected ?Watcher $watcher = null;
60
+    protected ?Propagator $propagator = null;
61
+    protected $storageCache;
62
+    protected ?Updater $updater = null;
63
+
64
+    protected array $mountOptions = [];
65
+    protected $owner = null;
66
+
67
+    private ?bool $shouldLogLocks = null;
68
+    private ?LoggerInterface $logger = null;
69
+    private ?IFilenameValidator $filenameValidator = null;
70
+
71
+    public function __construct(array $parameters) {
72
+    }
73
+
74
+    protected function remove(string $path): bool {
75
+        if ($this->file_exists($path)) {
76
+            if ($this->is_dir($path)) {
77
+                return $this->rmdir($path);
78
+            } elseif ($this->is_file($path)) {
79
+                return $this->unlink($path);
80
+            }
81
+        }
82
+        return false;
83
+    }
84
+
85
+    public function is_dir(string $path): bool {
86
+        return $this->filetype($path) === 'dir';
87
+    }
88
+
89
+    public function is_file(string $path): bool {
90
+        return $this->filetype($path) === 'file';
91
+    }
92
+
93
+    public function filesize(string $path): int|float|false {
94
+        if ($this->is_dir($path)) {
95
+            return 0; //by definition
96
+        } else {
97
+            $stat = $this->stat($path);
98
+            return isset($stat['size']) ? $stat['size'] : 0;
99
+        }
100
+    }
101
+
102
+    public function isReadable(string $path): bool {
103
+        // at least check whether it exists
104
+        // subclasses might want to implement this more thoroughly
105
+        return $this->file_exists($path);
106
+    }
107
+
108
+    public function isUpdatable(string $path): bool {
109
+        // at least check whether it exists
110
+        // subclasses might want to implement this more thoroughly
111
+        // a non-existing file/folder isn't updatable
112
+        return $this->file_exists($path);
113
+    }
114
+
115
+    public function isCreatable(string $path): bool {
116
+        if ($this->is_dir($path) && $this->isUpdatable($path)) {
117
+            return true;
118
+        }
119
+        return false;
120
+    }
121
+
122
+    public function isDeletable(string $path): bool {
123
+        if ($path === '' || $path === '/') {
124
+            return $this->isUpdatable($path);
125
+        }
126
+        $parent = dirname($path);
127
+        return $this->isUpdatable($parent) && $this->isUpdatable($path);
128
+    }
129
+
130
+    public function isSharable(string $path): bool {
131
+        return $this->isReadable($path);
132
+    }
133
+
134
+    public function getPermissions(string $path): int {
135
+        $permissions = 0;
136
+        if ($this->isCreatable($path)) {
137
+            $permissions |= \OCP\Constants::PERMISSION_CREATE;
138
+        }
139
+        if ($this->isReadable($path)) {
140
+            $permissions |= \OCP\Constants::PERMISSION_READ;
141
+        }
142
+        if ($this->isUpdatable($path)) {
143
+            $permissions |= \OCP\Constants::PERMISSION_UPDATE;
144
+        }
145
+        if ($this->isDeletable($path)) {
146
+            $permissions |= \OCP\Constants::PERMISSION_DELETE;
147
+        }
148
+        if ($this->isSharable($path)) {
149
+            $permissions |= \OCP\Constants::PERMISSION_SHARE;
150
+        }
151
+        return $permissions;
152
+    }
153
+
154
+    public function filemtime(string $path): int|false {
155
+        $stat = $this->stat($path);
156
+        if (isset($stat['mtime']) && $stat['mtime'] > 0) {
157
+            return $stat['mtime'];
158
+        } else {
159
+            return 0;
160
+        }
161
+    }
162
+
163
+    public function file_get_contents(string $path): string|false {
164
+        $handle = $this->fopen($path, 'r');
165
+        if (!$handle) {
166
+            return false;
167
+        }
168
+        $data = stream_get_contents($handle);
169
+        fclose($handle);
170
+        return $data;
171
+    }
172
+
173
+    public function file_put_contents(string $path, mixed $data): int|float|false {
174
+        $handle = $this->fopen($path, 'w');
175
+        if (!$handle) {
176
+            return false;
177
+        }
178
+        $this->removeCachedFile($path);
179
+        $count = fwrite($handle, $data);
180
+        fclose($handle);
181
+        return $count;
182
+    }
183
+
184
+    public function rename(string $source, string $target): bool {
185
+        $this->remove($target);
186
+
187
+        $this->removeCachedFile($source);
188
+        return $this->copy($source, $target) && $this->remove($source);
189
+    }
190
+
191
+    public function copy(string $source, string $target): bool {
192
+        if ($this->is_dir($source)) {
193
+            $this->remove($target);
194
+            $dir = $this->opendir($source);
195
+            $this->mkdir($target);
196
+            while (($file = readdir($dir)) !== false) {
197
+                if (!Filesystem::isIgnoredDir($file)) {
198
+                    if (!$this->copy($source . '/' . $file, $target . '/' . $file)) {
199
+                        closedir($dir);
200
+                        return false;
201
+                    }
202
+                }
203
+            }
204
+            closedir($dir);
205
+            return true;
206
+        } else {
207
+            $sourceStream = $this->fopen($source, 'r');
208
+            $targetStream = $this->fopen($target, 'w');
209
+            [, $result] = Files::streamCopy($sourceStream, $targetStream, true);
210
+            if (!$result) {
211
+                Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target");
212
+            }
213
+            $this->removeCachedFile($target);
214
+            return $result;
215
+        }
216
+    }
217
+
218
+    public function getMimeType(string $path): string|false {
219
+        if ($this->is_dir($path)) {
220
+            return 'httpd/unix-directory';
221
+        } elseif ($this->file_exists($path)) {
222
+            return \OC::$server->getMimeTypeDetector()->detectPath($path);
223
+        } else {
224
+            return false;
225
+        }
226
+    }
227
+
228
+    public function hash(string $type, string $path, bool $raw = false): string|false {
229
+        $fh = $this->fopen($path, 'rb');
230
+        if (!$fh) {
231
+            return false;
232
+        }
233
+        $ctx = hash_init($type);
234
+        hash_update_stream($ctx, $fh);
235
+        fclose($fh);
236
+        return hash_final($ctx, $raw);
237
+    }
238
+
239
+    public function getLocalFile(string $path): string|false {
240
+        return $this->getCachedFile($path);
241
+    }
242
+
243
+    private function addLocalFolder(string $path, string $target): void {
244
+        $dh = $this->opendir($path);
245
+        if (is_resource($dh)) {
246
+            while (($file = readdir($dh)) !== false) {
247
+                if (!Filesystem::isIgnoredDir($file)) {
248
+                    if ($this->is_dir($path . '/' . $file)) {
249
+                        mkdir($target . '/' . $file);
250
+                        $this->addLocalFolder($path . '/' . $file, $target . '/' . $file);
251
+                    } else {
252
+                        $tmp = $this->toTmpFile($path . '/' . $file);
253
+                        rename($tmp, $target . '/' . $file);
254
+                    }
255
+                }
256
+            }
257
+        }
258
+    }
259
+
260
+    protected function searchInDir(string $query, string $dir = ''): array {
261
+        $files = [];
262
+        $dh = $this->opendir($dir);
263
+        if (is_resource($dh)) {
264
+            while (($item = readdir($dh)) !== false) {
265
+                if (Filesystem::isIgnoredDir($item)) {
266
+                    continue;
267
+                }
268
+                if (strstr(strtolower($item), strtolower($query)) !== false) {
269
+                    $files[] = $dir . '/' . $item;
270
+                }
271
+                if ($this->is_dir($dir . '/' . $item)) {
272
+                    $files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
273
+                }
274
+            }
275
+        }
276
+        closedir($dh);
277
+        return $files;
278
+    }
279
+
280
+    /**
281
+     * @inheritDoc
282
+     * Check if a file or folder has been updated since $time
283
+     *
284
+     * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking
285
+     * the mtime should always return false here. As a result storage implementations that always return false expect
286
+     * exclusive access to the backend and will not pick up files that have been added in a way that circumvents
287
+     * Nextcloud filesystem.
288
+     */
289
+    public function hasUpdated(string $path, int $time): bool {
290
+        return $this->filemtime($path) > $time;
291
+    }
292
+
293
+    protected function getCacheDependencies(): CacheDependencies {
294
+        static $dependencies = null;
295
+        if (!$dependencies) {
296
+            $dependencies = Server::get(CacheDependencies::class);
297
+        }
298
+        return $dependencies;
299
+    }
300
+
301
+    public function getCache(string $path = '', ?IStorage $storage = null): ICache {
302
+        if (!$storage) {
303
+            $storage = $this;
304
+        }
305
+        /** @var self $storage */
306
+        if (!isset($storage->cache)) {
307
+            $storage->cache = new Cache($storage, $this->getCacheDependencies());
308
+        }
309
+        return $storage->cache;
310
+    }
311
+
312
+    public function getScanner(string $path = '', ?IStorage $storage = null): IScanner {
313
+        if (!$storage) {
314
+            $storage = $this;
315
+        }
316
+        if (!$storage->instanceOfStorage(self::class)) {
317
+            throw new \InvalidArgumentException('Storage is not of the correct class');
318
+        }
319
+        if (!isset($storage->scanner)) {
320
+            $storage->scanner = new Scanner($storage);
321
+        }
322
+        return $storage->scanner;
323
+    }
324
+
325
+    public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher {
326
+        if (!$storage) {
327
+            $storage = $this;
328
+        }
329
+        if (!isset($this->watcher)) {
330
+            $this->watcher = new Watcher($storage);
331
+            $globalPolicy = Server::get(IConfig::class)->getSystemValueInt('filesystem_check_changes', Watcher::CHECK_NEVER);
332
+            $this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy));
333
+        }
334
+        return $this->watcher;
335
+    }
336
+
337
+    public function getPropagator(?IStorage $storage = null): IPropagator {
338
+        if (!$storage) {
339
+            $storage = $this;
340
+        }
341
+        if (!$storage->instanceOfStorage(self::class)) {
342
+            throw new \InvalidArgumentException('Storage is not of the correct class');
343
+        }
344
+        /** @var self $storage */
345
+        if (!isset($storage->propagator)) {
346
+            $config = Server::get(IConfig::class);
347
+            $storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getSystemValueString('instanceid')]);
348
+        }
349
+        return $storage->propagator;
350
+    }
351
+
352
+    public function getUpdater(?IStorage $storage = null): IUpdater {
353
+        if (!$storage) {
354
+            $storage = $this;
355
+        }
356
+        if (!$storage->instanceOfStorage(self::class)) {
357
+            throw new \InvalidArgumentException('Storage is not of the correct class');
358
+        }
359
+        /** @var self $storage */
360
+        if (!isset($storage->updater)) {
361
+            $storage->updater = new Updater($storage);
362
+        }
363
+        return $storage->updater;
364
+    }
365
+
366
+    public function getStorageCache(?IStorage $storage = null): \OC\Files\Cache\Storage {
367
+        /** @var Cache $cache */
368
+        $cache = $this->getCache(storage: $storage);
369
+        return $cache->getStorageCache();
370
+    }
371
+
372
+    public function getOwner(string $path): string|false {
373
+        if ($this->owner === null) {
374
+            $this->owner = \OC_User::getUser();
375
+        }
376
+
377
+        return $this->owner;
378
+    }
379
+
380
+    public function getETag(string $path): string|false {
381
+        return uniqid();
382
+    }
383
+
384
+    /**
385
+     * clean a path, i.e. remove all redundant '.' and '..'
386
+     * making sure that it can't point to higher than '/'
387
+     *
388
+     * @param string $path The path to clean
389
+     * @return string cleaned path
390
+     */
391
+    public function cleanPath(string $path): string {
392
+        if (strlen($path) == 0 || $path[0] != '/') {
393
+            $path = '/' . $path;
394
+        }
395
+
396
+        $output = [];
397
+        foreach (explode('/', $path) as $chunk) {
398
+            if ($chunk == '..') {
399
+                array_pop($output);
400
+            } elseif ($chunk == '.') {
401
+            } else {
402
+                $output[] = $chunk;
403
+            }
404
+        }
405
+        return implode('/', $output);
406
+    }
407
+
408
+    /**
409
+     * Test a storage for availability
410
+     */
411
+    public function test(): bool {
412
+        try {
413
+            if ($this->stat('')) {
414
+                return true;
415
+            }
416
+            Server::get(LoggerInterface::class)->info('External storage not available: stat() failed');
417
+            return false;
418
+        } catch (\Exception $e) {
419
+            Server::get(LoggerInterface::class)->warning(
420
+                'External storage not available: ' . $e->getMessage(),
421
+                ['exception' => $e]
422
+            );
423
+            return false;
424
+        }
425
+    }
426
+
427
+    public function free_space(string $path): int|float|false {
428
+        return \OCP\Files\FileInfo::SPACE_UNKNOWN;
429
+    }
430
+
431
+    public function isLocal(): bool {
432
+        // the common implementation returns a temporary file by
433
+        // default, which is not local
434
+        return false;
435
+    }
436
+
437
+    /**
438
+     * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class
439
+     */
440
+    public function instanceOfStorage(string $class): bool {
441
+        if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') {
442
+            // FIXME Temporary fix to keep existing checks working
443
+            $class = '\OCA\Files_Sharing\SharedStorage';
444
+        }
445
+        return is_a($this, $class);
446
+    }
447
+
448
+    /**
449
+     * A custom storage implementation can return an url for direct download of a give file.
450
+     *
451
+     * For now the returned array can hold the parameter url - in future more attributes might follow.
452
+     */
453
+    public function getDirectDownload(string $path): array|false {
454
+        return [];
455
+    }
456
+
457
+    public function verifyPath(string $path, string $fileName): void {
458
+        $this->getFilenameValidator()
459
+            ->validateFilename($fileName);
460
+
461
+        // verify also the path is valid
462
+        if ($path && $path !== '/' && $path !== '.') {
463
+            try {
464
+                $this->verifyPath(dirname($path), basename($path));
465
+            } catch (InvalidPathException $e) {
466
+                // Ignore invalid file type exceptions on directories
467
+                if ($e->getCode() !== FilenameValidator::INVALID_FILE_TYPE) {
468
+                    $l = \OCP\Util::getL10N('lib');
469
+                    throw new InvalidPathException($l->t('Invalid parent path'), previous: $e);
470
+                }
471
+            }
472
+        }
473
+    }
474
+
475
+    /**
476
+     * Get the filename validator
477
+     * (cached for performance)
478
+     */
479
+    protected function getFilenameValidator(): IFilenameValidator {
480
+        if ($this->filenameValidator === null) {
481
+            $this->filenameValidator = Server::get(IFilenameValidator::class);
482
+        }
483
+        return $this->filenameValidator;
484
+    }
485
+
486
+    public function setMountOptions(array $options): void {
487
+        $this->mountOptions = $options;
488
+    }
489
+
490
+    public function getMountOption(string $name, mixed $default = null): mixed {
491
+        return $this->mountOptions[$name] ?? $default;
492
+    }
493
+
494
+    public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool {
495
+        if ($sourceStorage === $this) {
496
+            return $this->copy($sourceInternalPath, $targetInternalPath);
497
+        }
498
+
499
+        if ($sourceStorage->is_dir($sourceInternalPath)) {
500
+            $dh = $sourceStorage->opendir($sourceInternalPath);
501
+            $result = $this->mkdir($targetInternalPath);
502
+            if (is_resource($dh)) {
503
+                $result = true;
504
+                while ($result && ($file = readdir($dh)) !== false) {
505
+                    if (!Filesystem::isIgnoredDir($file)) {
506
+                        $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file);
507
+                    }
508
+                }
509
+            }
510
+        } else {
511
+            $source = $sourceStorage->fopen($sourceInternalPath, 'r');
512
+            $result = false;
513
+            if ($source) {
514
+                try {
515
+                    $this->writeStream($targetInternalPath, $source);
516
+                    $result = true;
517
+                } catch (\Exception $e) {
518
+                    Server::get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]);
519
+                }
520
+            }
521
+
522
+            if ($result && $preserveMtime) {
523
+                $mtime = $sourceStorage->filemtime($sourceInternalPath);
524
+                $this->touch($targetInternalPath, is_int($mtime) ? $mtime : null);
525
+            }
526
+
527
+            if (!$result) {
528
+                // delete partially written target file
529
+                $this->unlink($targetInternalPath);
530
+                // delete cache entry that was created by fopen
531
+                $this->getCache()->remove($targetInternalPath);
532
+            }
533
+        }
534
+        return (bool)$result;
535
+    }
536
+
537
+    /**
538
+     * Check if a storage is the same as the current one, including wrapped storages
539
+     */
540
+    private function isSameStorage(IStorage $storage): bool {
541
+        while ($storage->instanceOfStorage(Wrapper::class)) {
542
+            /**
543
+             * @var Wrapper $storage
544
+             */
545
+            $storage = $storage->getWrapperStorage();
546
+        }
547
+
548
+        return $storage === $this;
549
+    }
550
+
551
+    public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
552
+        if (
553
+            !$sourceStorage->instanceOfStorage(Encryption::class)
554
+            && $this->isSameStorage($sourceStorage)
555
+        ) {
556
+            // resolve any jailed paths
557
+            while ($sourceStorage->instanceOfStorage(Jail::class)) {
558
+                /**
559
+                 * @var Jail $sourceStorage
560
+                 */
561
+                $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
562
+                $sourceStorage = $sourceStorage->getUnjailedStorage();
563
+            }
564
+
565
+            return $this->rename($sourceInternalPath, $targetInternalPath);
566
+        }
567
+
568
+        if (!$sourceStorage->isDeletable($sourceInternalPath)) {
569
+            return false;
570
+        }
571
+
572
+        $result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true);
573
+        if ($result) {
574
+            if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
575
+                /** @var ObjectStoreStorage $sourceStorage */
576
+                $sourceStorage->setPreserveCacheOnDelete(true);
577
+            }
578
+            try {
579
+                if ($sourceStorage->is_dir($sourceInternalPath)) {
580
+                    $result = $sourceStorage->rmdir($sourceInternalPath);
581
+                } else {
582
+                    $result = $sourceStorage->unlink($sourceInternalPath);
583
+                }
584
+            } finally {
585
+                if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
586
+                    /** @var ObjectStoreStorage $sourceStorage */
587
+                    $sourceStorage->setPreserveCacheOnDelete(false);
588
+                }
589
+            }
590
+        }
591
+        return $result;
592
+    }
593
+
594
+    public function getMetaData(string $path): ?array {
595
+        if (Filesystem::isFileBlacklisted($path)) {
596
+            throw new ForbiddenException('Invalid path: ' . $path, false);
597
+        }
598
+
599
+        $permissions = $this->getPermissions($path);
600
+        if (!$permissions & \OCP\Constants::PERMISSION_READ) {
601
+            //can't read, nothing we can do
602
+            return null;
603
+        }
604
+
605
+        $data = [];
606
+        $data['mimetype'] = $this->getMimeType($path);
607
+        $data['mtime'] = $this->filemtime($path);
608
+        if ($data['mtime'] === false) {
609
+            $data['mtime'] = time();
610
+        }
611
+        if ($data['mimetype'] == 'httpd/unix-directory') {
612
+            $data['size'] = -1; //unknown
613
+        } else {
614
+            $data['size'] = $this->filesize($path);
615
+        }
616
+        $data['etag'] = $this->getETag($path);
617
+        $data['storage_mtime'] = $data['mtime'];
618
+        $data['permissions'] = $permissions;
619
+        $data['name'] = basename($path);
620
+
621
+        return $data;
622
+    }
623
+
624
+    public function acquireLock(string $path, int $type, ILockingProvider $provider): void {
625
+        $logger = $this->getLockLogger();
626
+        if ($logger) {
627
+            $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
628
+            $logger->info(
629
+                sprintf(
630
+                    'acquire %s lock on "%s" on storage "%s"',
631
+                    $typeString,
632
+                    $path,
633
+                    $this->getId()
634
+                ),
635
+                [
636
+                    'app' => 'locking',
637
+                ]
638
+            );
639
+        }
640
+        try {
641
+            $provider->acquireLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type, $this->getId() . '::' . $path);
642
+        } catch (LockedException $e) {
643
+            $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
644
+            if ($logger) {
645
+                $logger->info($e->getMessage(), ['exception' => $e]);
646
+            }
647
+            throw $e;
648
+        }
649
+    }
650
+
651
+    public function releaseLock(string $path, int $type, ILockingProvider $provider): void {
652
+        $logger = $this->getLockLogger();
653
+        if ($logger) {
654
+            $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
655
+            $logger->info(
656
+                sprintf(
657
+                    'release %s lock on "%s" on storage "%s"',
658
+                    $typeString,
659
+                    $path,
660
+                    $this->getId()
661
+                ),
662
+                [
663
+                    'app' => 'locking',
664
+                ]
665
+            );
666
+        }
667
+        try {
668
+            $provider->releaseLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
669
+        } catch (LockedException $e) {
670
+            $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
671
+            if ($logger) {
672
+                $logger->info($e->getMessage(), ['exception' => $e]);
673
+            }
674
+            throw $e;
675
+        }
676
+    }
677
+
678
+    public function changeLock(string $path, int $type, ILockingProvider $provider): void {
679
+        $logger = $this->getLockLogger();
680
+        if ($logger) {
681
+            $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
682
+            $logger->info(
683
+                sprintf(
684
+                    'change lock on "%s" to %s on storage "%s"',
685
+                    $path,
686
+                    $typeString,
687
+                    $this->getId()
688
+                ),
689
+                [
690
+                    'app' => 'locking',
691
+                ]
692
+            );
693
+        }
694
+        try {
695
+            $provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
696
+        } catch (LockedException $e) {
697
+            $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path);
698
+            if ($logger) {
699
+                $logger->info($e->getMessage(), ['exception' => $e]);
700
+            }
701
+            throw $e;
702
+        }
703
+    }
704
+
705
+    private function getLockLogger(): ?LoggerInterface {
706
+        if (is_null($this->shouldLogLocks)) {
707
+            $this->shouldLogLocks = Server::get(IConfig::class)->getSystemValueBool('filelocking.debug', false);
708
+            $this->logger = $this->shouldLogLocks ? Server::get(LoggerInterface::class) : null;
709
+        }
710
+        return $this->logger;
711
+    }
712
+
713
+    /**
714
+     * @return array{available: bool, last_checked: int}
715
+     */
716
+    public function getAvailability(): array {
717
+        return $this->getStorageCache()->getAvailability();
718
+    }
719
+
720
+    public function setAvailability(bool $isAvailable): void {
721
+        $this->getStorageCache()->setAvailability($isAvailable);
722
+    }
723
+
724
+    public function setOwner(?string $user): void {
725
+        $this->owner = $user;
726
+    }
727
+
728
+    public function needsPartFile(): bool {
729
+        return true;
730
+    }
731
+
732
+    public function writeStream(string $path, $stream, ?int $size = null): int {
733
+        $target = $this->fopen($path, 'w');
734
+        if (!$target) {
735
+            throw new GenericFileException("Failed to open $path for writing");
736
+        }
737
+        try {
738
+            [$count, $result] = Files::streamCopy($stream, $target, true);
739
+            if (!$result) {
740
+                throw new GenericFileException('Failed to copy stream');
741
+            }
742
+        } finally {
743
+            fclose($target);
744
+            fclose($stream);
745
+        }
746
+        return $count;
747
+    }
748
+
749
+    public function getDirectoryContent(string $directory): \Traversable {
750
+        $dh = $this->opendir($directory);
751
+
752
+        if ($dh === false) {
753
+            throw new StorageNotAvailableException('Directory listing failed');
754
+        }
755
+
756
+        if (is_resource($dh)) {
757
+            $basePath = rtrim($directory, '/');
758
+            while (($file = readdir($dh)) !== false) {
759
+                if (!Filesystem::isIgnoredDir($file)) {
760
+                    $childPath = $basePath . '/' . trim($file, '/');
761
+                    $metadata = $this->getMetaData($childPath);
762
+                    if ($metadata !== null) {
763
+                        yield $metadata;
764
+                    }
765
+                }
766
+            }
767
+        }
768
+    }
769 769
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/Storage.php 1 patch
Indentation   +208 added lines, -208 removed lines patch added patch discarded remove patch
@@ -24,212 +24,212 @@
 block discarded – undo
24 24
  * @package OC\Files\Cache
25 25
  */
26 26
 class Storage {
27
-	/** @var StorageGlobal|null */
28
-	private static $globalCache = null;
29
-	private $storageId;
30
-	private $numericId;
31
-
32
-	/**
33
-	 * @return StorageGlobal
34
-	 */
35
-	public static function getGlobalCache() {
36
-		if (is_null(self::$globalCache)) {
37
-			self::$globalCache = new StorageGlobal(\OC::$server->getDatabaseConnection());
38
-		}
39
-		return self::$globalCache;
40
-	}
41
-
42
-	/**
43
-	 * @param \OC\Files\Storage\Storage|string $storage
44
-	 * @param bool $isAvailable
45
-	 * @throws \RuntimeException
46
-	 */
47
-	public function __construct($storage, $isAvailable, IDBConnection $connection) {
48
-		if ($storage instanceof IStorage) {
49
-			$this->storageId = $storage->getId();
50
-		} else {
51
-			$this->storageId = $storage;
52
-		}
53
-		$this->storageId = self::adjustStorageId($this->storageId);
54
-
55
-		if ($row = self::getStorageById($this->storageId)) {
56
-			$this->numericId = (int)$row['numeric_id'];
57
-		} else {
58
-			$available = $isAvailable ? 1 : 0;
59
-			if ($connection->insertIfNotExist('*PREFIX*storages', ['id' => $this->storageId, 'available' => $available])) {
60
-				$this->numericId = $connection->lastInsertId('*PREFIX*storages');
61
-			} else {
62
-				if ($row = self::getStorageById($this->storageId)) {
63
-					$this->numericId = (int)$row['numeric_id'];
64
-				} else {
65
-					throw new \RuntimeException('Storage could neither be inserted nor be selected from the database: ' . $this->storageId);
66
-				}
67
-			}
68
-		}
69
-	}
70
-
71
-	/**
72
-	 * @param string $storageId
73
-	 * @return array
74
-	 */
75
-	public static function getStorageById($storageId) {
76
-		return self::getGlobalCache()->getStorageInfo($storageId);
77
-	}
78
-
79
-	/**
80
-	 * Adjusts the storage id to use md5 if too long
81
-	 * @param string $storageId storage id
82
-	 * @return string unchanged $storageId if its length is less than 64 characters,
83
-	 *                else returns the md5 of $storageId
84
-	 */
85
-	public static function adjustStorageId($storageId) {
86
-		if (strlen($storageId) > 64) {
87
-			return md5($storageId);
88
-		}
89
-		return $storageId;
90
-	}
91
-
92
-	/**
93
-	 * Get the numeric id for the storage
94
-	 *
95
-	 * @return int
96
-	 */
97
-	public function getNumericId() {
98
-		return $this->numericId;
99
-	}
100
-
101
-	/**
102
-	 * Get the string id for the storage
103
-	 *
104
-	 * @param int $numericId
105
-	 * @return string|null either the storage id string or null if the numeric id is not known
106
-	 */
107
-	public static function getStorageId(int $numericId): ?string {
108
-		$storage = self::getGlobalCache()->getStorageInfoByNumericId($numericId);
109
-		return $storage['id'] ?? null;
110
-	}
111
-
112
-	/**
113
-	 * Get the numeric of the storage with the provided string id
114
-	 *
115
-	 * @param $storageId
116
-	 * @return int|null either the numeric storage id or null if the storage id is not known
117
-	 */
118
-	public static function getNumericStorageId($storageId) {
119
-		$storageId = self::adjustStorageId($storageId);
120
-
121
-		if ($row = self::getStorageById($storageId)) {
122
-			return (int)$row['numeric_id'];
123
-		} else {
124
-			return null;
125
-		}
126
-	}
127
-
128
-	/**
129
-	 * @return array{available: bool, last_checked: int}
130
-	 */
131
-	public function getAvailability() {
132
-		if ($row = self::getStorageById($this->storageId)) {
133
-			return [
134
-				'available' => (int)$row['available'] === 1,
135
-				'last_checked' => $row['last_checked']
136
-			];
137
-		} else {
138
-			return [
139
-				'available' => true,
140
-				'last_checked' => time(),
141
-			];
142
-		}
143
-	}
144
-
145
-	/**
146
-	 * @param bool $isAvailable
147
-	 * @param int $delay amount of seconds to delay reconsidering that storage further
148
-	 */
149
-	public function setAvailability($isAvailable, int $delay = 0) {
150
-		$available = $isAvailable ? 1 : 0;
151
-		if (!$isAvailable) {
152
-			\OCP\Server::get(LoggerInterface::class)->info('Storage with ' . $this->storageId . ' marked as unavailable', ['app' => 'lib']);
153
-		}
154
-
155
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
156
-		$query->update('storages')
157
-			->set('available', $query->createNamedParameter($available))
158
-			->set('last_checked', $query->createNamedParameter(time() + $delay))
159
-			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
160
-		$query->executeStatement();
161
-	}
162
-
163
-	/**
164
-	 * Check if a string storage id is known
165
-	 *
166
-	 * @param string $storageId
167
-	 * @return bool
168
-	 */
169
-	public static function exists($storageId) {
170
-		return !is_null(self::getNumericStorageId($storageId));
171
-	}
172
-
173
-	/**
174
-	 * remove the entry for the storage
175
-	 *
176
-	 * @param string $storageId
177
-	 */
178
-	public static function remove($storageId) {
179
-		$storageId = self::adjustStorageId($storageId);
180
-		$numericId = self::getNumericStorageId($storageId);
181
-
182
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
183
-		$query->delete('storages')
184
-			->where($query->expr()->eq('id', $query->createNamedParameter($storageId)));
185
-		$query->executeStatement();
186
-
187
-		if (!is_null($numericId)) {
188
-			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
189
-			$query->delete('filecache')
190
-				->where($query->expr()->eq('storage', $query->createNamedParameter($numericId)));
191
-			$query->executeStatement();
192
-		}
193
-	}
194
-
195
-	/**
196
-	 * remove the entry for the storage by the mount id
197
-	 *
198
-	 * @param int $mountId
199
-	 */
200
-	public static function cleanByMountId(int $mountId) {
201
-		$db = \OC::$server->getDatabaseConnection();
202
-
203
-		try {
204
-			$db->beginTransaction();
205
-
206
-			$query = $db->getQueryBuilder();
207
-			$query->select('storage_id')
208
-				->from('mounts')
209
-				->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
210
-			$storageIds = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
211
-			$storageIds = array_unique($storageIds);
212
-
213
-			$query = $db->getQueryBuilder();
214
-			$query->delete('filecache')
215
-				->where($query->expr()->in('storage', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)));
216
-			$query->runAcrossAllShards();
217
-			$query->executeStatement();
218
-
219
-			$query = $db->getQueryBuilder();
220
-			$query->delete('storages')
221
-				->where($query->expr()->in('numeric_id', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)));
222
-			$query->executeStatement();
223
-
224
-			$query = $db->getQueryBuilder();
225
-			$query->delete('mounts')
226
-				->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
227
-			$query->executeStatement();
228
-
229
-			$db->commit();
230
-		} catch (\Exception $e) {
231
-			$db->rollBack();
232
-			throw $e;
233
-		}
234
-	}
27
+    /** @var StorageGlobal|null */
28
+    private static $globalCache = null;
29
+    private $storageId;
30
+    private $numericId;
31
+
32
+    /**
33
+     * @return StorageGlobal
34
+     */
35
+    public static function getGlobalCache() {
36
+        if (is_null(self::$globalCache)) {
37
+            self::$globalCache = new StorageGlobal(\OC::$server->getDatabaseConnection());
38
+        }
39
+        return self::$globalCache;
40
+    }
41
+
42
+    /**
43
+     * @param \OC\Files\Storage\Storage|string $storage
44
+     * @param bool $isAvailable
45
+     * @throws \RuntimeException
46
+     */
47
+    public function __construct($storage, $isAvailable, IDBConnection $connection) {
48
+        if ($storage instanceof IStorage) {
49
+            $this->storageId = $storage->getId();
50
+        } else {
51
+            $this->storageId = $storage;
52
+        }
53
+        $this->storageId = self::adjustStorageId($this->storageId);
54
+
55
+        if ($row = self::getStorageById($this->storageId)) {
56
+            $this->numericId = (int)$row['numeric_id'];
57
+        } else {
58
+            $available = $isAvailable ? 1 : 0;
59
+            if ($connection->insertIfNotExist('*PREFIX*storages', ['id' => $this->storageId, 'available' => $available])) {
60
+                $this->numericId = $connection->lastInsertId('*PREFIX*storages');
61
+            } else {
62
+                if ($row = self::getStorageById($this->storageId)) {
63
+                    $this->numericId = (int)$row['numeric_id'];
64
+                } else {
65
+                    throw new \RuntimeException('Storage could neither be inserted nor be selected from the database: ' . $this->storageId);
66
+                }
67
+            }
68
+        }
69
+    }
70
+
71
+    /**
72
+     * @param string $storageId
73
+     * @return array
74
+     */
75
+    public static function getStorageById($storageId) {
76
+        return self::getGlobalCache()->getStorageInfo($storageId);
77
+    }
78
+
79
+    /**
80
+     * Adjusts the storage id to use md5 if too long
81
+     * @param string $storageId storage id
82
+     * @return string unchanged $storageId if its length is less than 64 characters,
83
+     *                else returns the md5 of $storageId
84
+     */
85
+    public static function adjustStorageId($storageId) {
86
+        if (strlen($storageId) > 64) {
87
+            return md5($storageId);
88
+        }
89
+        return $storageId;
90
+    }
91
+
92
+    /**
93
+     * Get the numeric id for the storage
94
+     *
95
+     * @return int
96
+     */
97
+    public function getNumericId() {
98
+        return $this->numericId;
99
+    }
100
+
101
+    /**
102
+     * Get the string id for the storage
103
+     *
104
+     * @param int $numericId
105
+     * @return string|null either the storage id string or null if the numeric id is not known
106
+     */
107
+    public static function getStorageId(int $numericId): ?string {
108
+        $storage = self::getGlobalCache()->getStorageInfoByNumericId($numericId);
109
+        return $storage['id'] ?? null;
110
+    }
111
+
112
+    /**
113
+     * Get the numeric of the storage with the provided string id
114
+     *
115
+     * @param $storageId
116
+     * @return int|null either the numeric storage id or null if the storage id is not known
117
+     */
118
+    public static function getNumericStorageId($storageId) {
119
+        $storageId = self::adjustStorageId($storageId);
120
+
121
+        if ($row = self::getStorageById($storageId)) {
122
+            return (int)$row['numeric_id'];
123
+        } else {
124
+            return null;
125
+        }
126
+    }
127
+
128
+    /**
129
+     * @return array{available: bool, last_checked: int}
130
+     */
131
+    public function getAvailability() {
132
+        if ($row = self::getStorageById($this->storageId)) {
133
+            return [
134
+                'available' => (int)$row['available'] === 1,
135
+                'last_checked' => $row['last_checked']
136
+            ];
137
+        } else {
138
+            return [
139
+                'available' => true,
140
+                'last_checked' => time(),
141
+            ];
142
+        }
143
+    }
144
+
145
+    /**
146
+     * @param bool $isAvailable
147
+     * @param int $delay amount of seconds to delay reconsidering that storage further
148
+     */
149
+    public function setAvailability($isAvailable, int $delay = 0) {
150
+        $available = $isAvailable ? 1 : 0;
151
+        if (!$isAvailable) {
152
+            \OCP\Server::get(LoggerInterface::class)->info('Storage with ' . $this->storageId . ' marked as unavailable', ['app' => 'lib']);
153
+        }
154
+
155
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
156
+        $query->update('storages')
157
+            ->set('available', $query->createNamedParameter($available))
158
+            ->set('last_checked', $query->createNamedParameter(time() + $delay))
159
+            ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
160
+        $query->executeStatement();
161
+    }
162
+
163
+    /**
164
+     * Check if a string storage id is known
165
+     *
166
+     * @param string $storageId
167
+     * @return bool
168
+     */
169
+    public static function exists($storageId) {
170
+        return !is_null(self::getNumericStorageId($storageId));
171
+    }
172
+
173
+    /**
174
+     * remove the entry for the storage
175
+     *
176
+     * @param string $storageId
177
+     */
178
+    public static function remove($storageId) {
179
+        $storageId = self::adjustStorageId($storageId);
180
+        $numericId = self::getNumericStorageId($storageId);
181
+
182
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
183
+        $query->delete('storages')
184
+            ->where($query->expr()->eq('id', $query->createNamedParameter($storageId)));
185
+        $query->executeStatement();
186
+
187
+        if (!is_null($numericId)) {
188
+            $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
189
+            $query->delete('filecache')
190
+                ->where($query->expr()->eq('storage', $query->createNamedParameter($numericId)));
191
+            $query->executeStatement();
192
+        }
193
+    }
194
+
195
+    /**
196
+     * remove the entry for the storage by the mount id
197
+     *
198
+     * @param int $mountId
199
+     */
200
+    public static function cleanByMountId(int $mountId) {
201
+        $db = \OC::$server->getDatabaseConnection();
202
+
203
+        try {
204
+            $db->beginTransaction();
205
+
206
+            $query = $db->getQueryBuilder();
207
+            $query->select('storage_id')
208
+                ->from('mounts')
209
+                ->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
210
+            $storageIds = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
211
+            $storageIds = array_unique($storageIds);
212
+
213
+            $query = $db->getQueryBuilder();
214
+            $query->delete('filecache')
215
+                ->where($query->expr()->in('storage', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)));
216
+            $query->runAcrossAllShards();
217
+            $query->executeStatement();
218
+
219
+            $query = $db->getQueryBuilder();
220
+            $query->delete('storages')
221
+                ->where($query->expr()->in('numeric_id', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)));
222
+            $query->executeStatement();
223
+
224
+            $query = $db->getQueryBuilder();
225
+            $query->delete('mounts')
226
+                ->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
227
+            $query->executeStatement();
228
+
229
+            $db->commit();
230
+        } catch (\Exception $e) {
231
+            $db->rollBack();
232
+            throw $e;
233
+        }
234
+    }
235 235
 }
Please login to merge, or discard this patch.
tests/lib/Files/Storage/Wrapper/AvailabilityTest.php 1 patch
Indentation   +167 added lines, -167 removed lines patch added patch discarded remove patch
@@ -14,171 +14,171 @@
 block discarded – undo
14 14
 use OCP\Files\StorageNotAvailableException;
15 15
 
16 16
 class AvailabilityTest extends \Test\TestCase {
17
-	/** @var \PHPUnit\Framework\MockObject\MockObject|StorageCache */
18
-	protected $storageCache;
19
-	/** @var \PHPUnit\Framework\MockObject\MockObject|Temporary */
20
-	protected $storage;
21
-	/** @var Availability */
22
-	protected $wrapper;
23
-
24
-	protected function setUp(): void {
25
-		parent::setUp();
26
-
27
-		$this->storageCache = $this->createMock(StorageCache::class);
28
-
29
-		$this->storage = $this->createMock(Temporary::class);
30
-		$this->storage->expects($this->any())
31
-			->method('getStorageCache')
32
-			->willReturn($this->storageCache);
33
-
34
-		$this->wrapper = new Availability(['storage' => $this->storage]);
35
-	}
36
-
37
-	/**
38
-	 * Storage is available
39
-	 */
40
-	public function testAvailable(): void {
41
-		$this->storage->expects($this->once())
42
-			->method('getAvailability')
43
-			->willReturn(['available' => true, 'last_checked' => 0]);
44
-		$this->storage->expects($this->never())
45
-			->method('test');
46
-		$this->storage->expects($this->once())
47
-			->method('mkdir');
48
-
49
-		$this->wrapper->mkdir('foobar');
50
-	}
51
-
52
-	/**
53
-	 * Storage marked unavailable, TTL not expired
54
-	 *
55
-	 */
56
-	public function testUnavailable(): void {
57
-		$this->expectException(StorageNotAvailableException::class);
58
-
59
-		$this->storage->expects($this->once())
60
-			->method('getAvailability')
61
-			->willReturn(['available' => false, 'last_checked' => time()]);
62
-		$this->storage->expects($this->never())
63
-			->method('test');
64
-		$this->storage->expects($this->never())
65
-			->method('mkdir');
66
-
67
-		$this->wrapper->mkdir('foobar');
68
-	}
69
-
70
-	/**
71
-	 * Storage marked unavailable, TTL expired
72
-	 */
73
-	public function testUnavailableRecheck(): void {
74
-		$this->storage->expects($this->once())
75
-			->method('getAvailability')
76
-			->willReturn(['available' => false, 'last_checked' => 0]);
77
-		$this->storage->expects($this->once())
78
-			->method('test')
79
-			->willReturn(true);
80
-		$calls = [
81
-			false, // prevents concurrent rechecks
82
-			true, // sets correct availability
83
-		];
84
-		$this->storage->expects($this->exactly(2))
85
-			->method('setAvailability')
86
-			->willReturnCallback(function ($value) use (&$calls): void {
87
-				$expected = array_shift($calls);
88
-				$this->assertEquals($expected, $value);
89
-			});
90
-		$this->storage->expects($this->once())
91
-			->method('mkdir');
92
-
93
-		$this->wrapper->mkdir('foobar');
94
-	}
95
-
96
-	/**
97
-	 * Storage marked available, but throws StorageNotAvailableException
98
-	 *
99
-	 */
100
-	public function testAvailableThrowStorageNotAvailable(): void {
101
-		$this->expectException(StorageNotAvailableException::class);
102
-
103
-		$this->storage->expects($this->once())
104
-			->method('getAvailability')
105
-			->willReturn(['available' => true, 'last_checked' => 0]);
106
-		$this->storage->expects($this->never())
107
-			->method('test');
108
-		$this->storage->expects($this->once())
109
-			->method('mkdir')
110
-			->willThrowException(new StorageNotAvailableException());
111
-		$this->storageCache->expects($this->once())
112
-			->method('setAvailability')
113
-			->with($this->equalTo(false));
114
-
115
-		$this->wrapper->mkdir('foobar');
116
-	}
117
-
118
-	/**
119
-	 * Storage available, but call fails
120
-	 * Method failure does not indicate storage unavailability
121
-	 */
122
-	public function testAvailableFailure(): void {
123
-		$this->storage->expects($this->once())
124
-			->method('getAvailability')
125
-			->willReturn(['available' => true, 'last_checked' => 0]);
126
-		$this->storage->expects($this->never())
127
-			->method('test');
128
-		$this->storage->expects($this->once())
129
-			->method('mkdir')
130
-			->willReturn(false);
131
-		$this->storage->expects($this->never())
132
-			->method('setAvailability');
133
-
134
-		$this->wrapper->mkdir('foobar');
135
-	}
136
-
137
-	/**
138
-	 * Storage available, but throws exception
139
-	 * Standard exception does not indicate storage unavailability
140
-	 *
141
-	 */
142
-	public function testAvailableThrow(): void {
143
-		$this->expectException(\Exception::class);
144
-
145
-		$this->storage->expects($this->once())
146
-			->method('getAvailability')
147
-			->willReturn(['available' => true, 'last_checked' => 0]);
148
-		$this->storage->expects($this->never())
149
-			->method('test');
150
-		$this->storage->expects($this->once())
151
-			->method('mkdir')
152
-			->willThrowException(new \Exception());
153
-		$this->storage->expects($this->never())
154
-			->method('setAvailability');
155
-
156
-		$this->wrapper->mkdir('foobar');
157
-	}
158
-
159
-	public function testUnavailableMultiple(): void {
160
-		$this->storage->expects($this->once())
161
-			->method('getAvailability')
162
-			->willReturn(['available' => true, 'last_checked' => 0]);
163
-		$this->storage->expects($this->never())
164
-			->method('test');
165
-		$this->storage
166
-			->expects($this->once()) // load-bearing `once`
167
-			->method('mkdir')
168
-			->willThrowException(new StorageNotAvailableException());
169
-
170
-		try {
171
-			$this->wrapper->mkdir('foobar');
172
-			$this->fail();
173
-		} catch (StorageNotAvailableException) {
174
-		}
175
-
176
-		$this->storage->expects($this->never())->method('file_exists');
177
-
178
-		try {
179
-			$this->wrapper->mkdir('foobar');
180
-			$this->fail();
181
-		} catch (StorageNotAvailableException) {
182
-		}
183
-	}
17
+    /** @var \PHPUnit\Framework\MockObject\MockObject|StorageCache */
18
+    protected $storageCache;
19
+    /** @var \PHPUnit\Framework\MockObject\MockObject|Temporary */
20
+    protected $storage;
21
+    /** @var Availability */
22
+    protected $wrapper;
23
+
24
+    protected function setUp(): void {
25
+        parent::setUp();
26
+
27
+        $this->storageCache = $this->createMock(StorageCache::class);
28
+
29
+        $this->storage = $this->createMock(Temporary::class);
30
+        $this->storage->expects($this->any())
31
+            ->method('getStorageCache')
32
+            ->willReturn($this->storageCache);
33
+
34
+        $this->wrapper = new Availability(['storage' => $this->storage]);
35
+    }
36
+
37
+    /**
38
+     * Storage is available
39
+     */
40
+    public function testAvailable(): void {
41
+        $this->storage->expects($this->once())
42
+            ->method('getAvailability')
43
+            ->willReturn(['available' => true, 'last_checked' => 0]);
44
+        $this->storage->expects($this->never())
45
+            ->method('test');
46
+        $this->storage->expects($this->once())
47
+            ->method('mkdir');
48
+
49
+        $this->wrapper->mkdir('foobar');
50
+    }
51
+
52
+    /**
53
+     * Storage marked unavailable, TTL not expired
54
+     *
55
+     */
56
+    public function testUnavailable(): void {
57
+        $this->expectException(StorageNotAvailableException::class);
58
+
59
+        $this->storage->expects($this->once())
60
+            ->method('getAvailability')
61
+            ->willReturn(['available' => false, 'last_checked' => time()]);
62
+        $this->storage->expects($this->never())
63
+            ->method('test');
64
+        $this->storage->expects($this->never())
65
+            ->method('mkdir');
66
+
67
+        $this->wrapper->mkdir('foobar');
68
+    }
69
+
70
+    /**
71
+     * Storage marked unavailable, TTL expired
72
+     */
73
+    public function testUnavailableRecheck(): void {
74
+        $this->storage->expects($this->once())
75
+            ->method('getAvailability')
76
+            ->willReturn(['available' => false, 'last_checked' => 0]);
77
+        $this->storage->expects($this->once())
78
+            ->method('test')
79
+            ->willReturn(true);
80
+        $calls = [
81
+            false, // prevents concurrent rechecks
82
+            true, // sets correct availability
83
+        ];
84
+        $this->storage->expects($this->exactly(2))
85
+            ->method('setAvailability')
86
+            ->willReturnCallback(function ($value) use (&$calls): void {
87
+                $expected = array_shift($calls);
88
+                $this->assertEquals($expected, $value);
89
+            });
90
+        $this->storage->expects($this->once())
91
+            ->method('mkdir');
92
+
93
+        $this->wrapper->mkdir('foobar');
94
+    }
95
+
96
+    /**
97
+     * Storage marked available, but throws StorageNotAvailableException
98
+     *
99
+     */
100
+    public function testAvailableThrowStorageNotAvailable(): void {
101
+        $this->expectException(StorageNotAvailableException::class);
102
+
103
+        $this->storage->expects($this->once())
104
+            ->method('getAvailability')
105
+            ->willReturn(['available' => true, 'last_checked' => 0]);
106
+        $this->storage->expects($this->never())
107
+            ->method('test');
108
+        $this->storage->expects($this->once())
109
+            ->method('mkdir')
110
+            ->willThrowException(new StorageNotAvailableException());
111
+        $this->storageCache->expects($this->once())
112
+            ->method('setAvailability')
113
+            ->with($this->equalTo(false));
114
+
115
+        $this->wrapper->mkdir('foobar');
116
+    }
117
+
118
+    /**
119
+     * Storage available, but call fails
120
+     * Method failure does not indicate storage unavailability
121
+     */
122
+    public function testAvailableFailure(): void {
123
+        $this->storage->expects($this->once())
124
+            ->method('getAvailability')
125
+            ->willReturn(['available' => true, 'last_checked' => 0]);
126
+        $this->storage->expects($this->never())
127
+            ->method('test');
128
+        $this->storage->expects($this->once())
129
+            ->method('mkdir')
130
+            ->willReturn(false);
131
+        $this->storage->expects($this->never())
132
+            ->method('setAvailability');
133
+
134
+        $this->wrapper->mkdir('foobar');
135
+    }
136
+
137
+    /**
138
+     * Storage available, but throws exception
139
+     * Standard exception does not indicate storage unavailability
140
+     *
141
+     */
142
+    public function testAvailableThrow(): void {
143
+        $this->expectException(\Exception::class);
144
+
145
+        $this->storage->expects($this->once())
146
+            ->method('getAvailability')
147
+            ->willReturn(['available' => true, 'last_checked' => 0]);
148
+        $this->storage->expects($this->never())
149
+            ->method('test');
150
+        $this->storage->expects($this->once())
151
+            ->method('mkdir')
152
+            ->willThrowException(new \Exception());
153
+        $this->storage->expects($this->never())
154
+            ->method('setAvailability');
155
+
156
+        $this->wrapper->mkdir('foobar');
157
+    }
158
+
159
+    public function testUnavailableMultiple(): void {
160
+        $this->storage->expects($this->once())
161
+            ->method('getAvailability')
162
+            ->willReturn(['available' => true, 'last_checked' => 0]);
163
+        $this->storage->expects($this->never())
164
+            ->method('test');
165
+        $this->storage
166
+            ->expects($this->once()) // load-bearing `once`
167
+            ->method('mkdir')
168
+            ->willThrowException(new StorageNotAvailableException());
169
+
170
+        try {
171
+            $this->wrapper->mkdir('foobar');
172
+            $this->fail();
173
+        } catch (StorageNotAvailableException) {
174
+        }
175
+
176
+        $this->storage->expects($this->never())->method('file_exists');
177
+
178
+        try {
179
+            $this->wrapper->mkdir('foobar');
180
+            $this->fail();
181
+        } catch (StorageNotAvailableException) {
182
+        }
183
+    }
184 184
 }
Please login to merge, or discard this patch.