FileBackendDBRepoWrapper::translateSrcParams()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 4
nop 2
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Proxy backend that manages file layout rewriting for FileRepo.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup FileRepo
22
 * @ingroup FileBackend
23
 * @author Aaron Schulz
24
 */
25
26
/**
27
 * @brief Proxy backend that manages file layout rewriting for FileRepo.
28
 *
29
 * LocalRepo may be configured to store files under their title names or by SHA-1.
30
 * This acts as a shim in the latter case, providing backwards compatability for
31
 * most callers. All "public"/"deleted" zone files actually go in an "original"
32
 * container and are never changed.
33
 *
34
 * This requires something like thumb_handler.php and img_auth.php for client viewing of files.
35
 *
36
 * @ingroup FileRepo
37
 * @ingroup FileBackend
38
 * @since 1.25
39
 */
40
class FileBackendDBRepoWrapper extends FileBackend {
41
	/** @var FileBackend */
42
	protected $backend;
43
	/** @var string */
44
	protected $repoName;
45
	/** @var Closure */
46
	protected $dbHandleFunc;
47
	/** @var ProcessCacheLRU */
48
	protected $resolvedPathCache;
49
	/** @var DBConnRef[] */
50
	protected $dbs;
51
52
	public function __construct( array $config ) {
53
		/** @var FileBackend $backend */
54
		$backend = $config['backend'];
55
		$config['name'] = $backend->getName();
56
		$config['wikiId'] = $backend->getWikiId();
57
		parent::__construct( $config );
58
		$this->backend = $config['backend'];
59
		$this->repoName = $config['repoName'];
60
		$this->dbHandleFunc = $config['dbHandleFactory'];
61
		$this->resolvedPathCache = new ProcessCacheLRU( 100 );
62
	}
63
64
	/**
65
	 * Get the underlying FileBackend that is being wrapped
66
	 *
67
	 * @return FileBackend
68
	 */
69
	public function getInternalBackend() {
70
		return $this->backend;
71
	}
72
73
	/**
74
	 * Translate a legacy "title" path to it's "sha1" counterpart
75
	 *
76
	 * E.g. mwstore://local-backend/local-public/a/ab/<name>.jpg
77
	 * => mwstore://local-backend/local-original/x/y/z/<sha1>.jpg
78
	 *
79
	 * @param string $path
80
	 * @param bool $latest
81
	 * @return string
82
	 */
83
	public function getBackendPath( $path, $latest = true ) {
84
		$paths = $this->getBackendPaths( [ $path ], $latest );
85
		return current( $paths );
86
	}
87
88
	/**
89
	 * Translate legacy "title" paths to their "sha1" counterparts
90
	 *
91
	 * E.g. mwstore://local-backend/local-public/a/ab/<name>.jpg
92
	 * => mwstore://local-backend/local-original/x/y/z/<sha1>.jpg
93
	 *
94
	 * @param array $paths
95
	 * @param bool $latest
96
	 * @return array Translated paths in same order
97
	 */
98
	public function getBackendPaths( array $paths, $latest = true ) {
99
		$db = $this->getDB( $latest ? DB_MASTER : DB_REPLICA );
100
101
		// @TODO: batching
102
		$resolved = [];
103
		foreach ( $paths as $i => $path ) {
104
			if ( !$latest && $this->resolvedPathCache->has( $path, 'target', 10 ) ) {
105
				$resolved[$i] = $this->resolvedPathCache->get( $path, 'target' );
106
				continue;
107
			}
108
109
			list( , $container ) = FileBackend::splitStoragePath( $path );
110
111
			if ( $container === "{$this->repoName}-public" ) {
112
				$name = basename( $path );
113
				if ( strpos( $path, '!' ) !== false ) {
114
					$sha1 = $db->selectField( 'oldimage', 'oi_sha1',
115
						[ 'oi_archive_name' => $name ],
116
						__METHOD__
117
					);
118
				} else {
119
					$sha1 = $db->selectField( 'image', 'img_sha1',
120
						[ 'img_name' => $name ],
121
						__METHOD__
122
					);
123
				}
124
				if ( !strlen( $sha1 ) ) {
125
					$resolved[$i] = $path; // give up
126
					continue;
127
				}
128
				$resolved[$i] = $this->getPathForSHA1( $sha1 );
129
				$this->resolvedPathCache->set( $path, 'target', $resolved[$i] );
130
			} elseif ( $container === "{$this->repoName}-deleted" ) {
131
				$name = basename( $path ); // <hash>.<ext>
132
				$sha1 = substr( $name, 0, strpos( $name, '.' ) ); // ignore extension
133
				$resolved[$i] = $this->getPathForSHA1( $sha1 );
134
				$this->resolvedPathCache->set( $path, 'target', $resolved[$i] );
135
			} else {
136
				$resolved[$i] = $path;
137
			}
138
		}
139
140
		$res = [];
141
		foreach ( $paths as $i => $path ) {
142
			$res[$i] = $resolved[$i];
143
		}
144
145
		return $res;
146
	}
147
148
	protected function doOperationsInternal( array $ops, array $opts ) {
149
		return $this->backend->doOperationsInternal( $this->mungeOpPaths( $ops ), $opts );
150
	}
151
152
	protected function doQuickOperationsInternal( array $ops ) {
153
		return $this->backend->doQuickOperationsInternal( $this->mungeOpPaths( $ops ) );
154
	}
155
156
	protected function doPrepare( array $params ) {
157
		return $this->backend->doPrepare( $params );
158
	}
159
160
	protected function doSecure( array $params ) {
161
		return $this->backend->doSecure( $params );
162
	}
163
164
	protected function doPublish( array $params ) {
165
		return $this->backend->doPublish( $params );
166
	}
167
168
	protected function doClean( array $params ) {
169
		return $this->backend->doClean( $params );
170
	}
171
172
	public function concatenate( array $params ) {
173
		return $this->translateSrcParams( __FUNCTION__, $params );
174
	}
175
176
	public function fileExists( array $params ) {
177
		return $this->translateSrcParams( __FUNCTION__, $params );
178
	}
179
180
	public function getFileTimestamp( array $params ) {
181
		return $this->translateSrcParams( __FUNCTION__, $params );
182
	}
183
184
	public function getFileSize( array $params ) {
185
		return $this->translateSrcParams( __FUNCTION__, $params );
186
	}
187
188
	public function getFileStat( array $params ) {
189
		return $this->translateSrcParams( __FUNCTION__, $params );
190
	}
191
192
	public function getFileXAttributes( array $params ) {
193
		return $this->translateSrcParams( __FUNCTION__, $params );
194
	}
195
196
	public function getFileSha1Base36( array $params ) {
197
		return $this->translateSrcParams( __FUNCTION__, $params );
198
	}
199
200
	public function getFileProps( array $params ) {
201
		return $this->translateSrcParams( __FUNCTION__, $params );
202
	}
203
204
	public function streamFile( array $params ) {
205
		// The stream methods use the file extension to determine the
206
		// Content-Type (as MediaWiki should already validate it on upload).
207
		// The translated SHA1 path has no extension, so this needs to use
208
		// the untranslated path extension.
209
		$type = StreamFile::contentTypeFromPath( $params['src'] );
210
		if ( $type && $type != 'unknown/unknown' ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
211
			$params['headers'][] = "Content-type: $type";
212
		}
213
		return $this->translateSrcParams( __FUNCTION__, $params );
214
	}
215
216
	public function getFileContentsMulti( array $params ) {
217
		return $this->translateArrayResults( __FUNCTION__, $params );
218
	}
219
220
	public function getLocalReferenceMulti( array $params ) {
221
		return $this->translateArrayResults( __FUNCTION__, $params );
222
	}
223
224
	public function getLocalCopyMulti( array $params ) {
225
		return $this->translateArrayResults( __FUNCTION__, $params );
226
	}
227
228
	public function getFileHttpUrl( array $params ) {
229
		return $this->translateSrcParams( __FUNCTION__, $params );
230
	}
231
232
	public function directoryExists( array $params ) {
233
		return $this->backend->directoryExists( $params );
234
	}
235
236
	public function getDirectoryList( array $params ) {
237
		return $this->backend->getDirectoryList( $params );
238
	}
239
240
	public function getFileList( array $params ) {
241
		return $this->backend->getFileList( $params );
242
	}
243
244
	public function getFeatures() {
245
		return $this->backend->getFeatures();
246
	}
247
248
	public function clearCache( array $paths = null ) {
249
		$this->backend->clearCache( null ); // clear all
250
	}
251
252
	public function preloadCache( array $paths ) {
253
		$paths = $this->getBackendPaths( $paths );
254
		$this->backend->preloadCache( $paths );
255
	}
256
257
	public function preloadFileStat( array $params ) {
258
		return $this->translateSrcParams( __FUNCTION__, $params );
259
	}
260
261
	public function getScopedLocksForOps( array $ops, StatusValue $status ) {
262
		return $this->backend->getScopedLocksForOps( $ops, $status );
263
	}
264
265
	/**
266
	 * Get the ultimate original storage path for a file
267
	 *
268
	 * Use this when putting a new file into the system
269
	 *
270
	 * @param string $sha1 File SHA-1 base36
271
	 * @return string
272
	 */
273
	public function getPathForSHA1( $sha1 ) {
274
		if ( strlen( $sha1 ) < 3 ) {
275
			throw new InvalidArgumentException( "Invalid file SHA-1." );
276
		}
277
		return $this->backend->getContainerStoragePath( "{$this->repoName}-original" ) .
278
			"/{$sha1[0]}/{$sha1[1]}/{$sha1[2]}/{$sha1}";
279
	}
280
281
	/**
282
	 * Get a connection to the repo file registry DB
283
	 *
284
	 * @param integer $index
285
	 * @return DBConnRef
286
	 */
287
	protected function getDB( $index ) {
288
		if ( !isset( $this->dbs[$index] ) ) {
289
			$func = $this->dbHandleFunc;
290
			$this->dbs[$index] = $func( $index );
291
		}
292
		return $this->dbs[$index];
293
	}
294
295
	/**
296
	 * Translates paths found in the "src" or "srcs" keys of a params array
297
	 *
298
	 * @param string $function
299
	 * @param array $params
300
	 */
301
	protected function translateSrcParams( $function, array $params ) {
302
		$latest = !empty( $params['latest'] );
303
304
		if ( isset( $params['src'] ) ) {
305
			$params['src'] = $this->getBackendPath( $params['src'], $latest );
306
		}
307
308
		if ( isset( $params['srcs'] ) ) {
309
			$params['srcs'] = $this->getBackendPaths( $params['srcs'], $latest );
310
		}
311
312
		return $this->backend->$function( $params );
313
	}
314
315
	/**
316
	 * Translates paths when the backend function returns results keyed by paths
317
	 *
318
	 * @param string $function
319
	 * @param array $params
320
	 * @return array
321
	 */
322
	protected function translateArrayResults( $function, array $params ) {
323
		$origPaths = $params['srcs'];
324
		$params['srcs'] = $this->getBackendPaths( $params['srcs'], !empty( $params['latest'] ) );
325
		$pathMap = array_combine( $params['srcs'], $origPaths );
326
327
		$results = $this->backend->$function( $params );
328
329
		$contents = [];
330
		foreach ( $results as $path => $result ) {
331
			$contents[$pathMap[$path]] = $result;
332
		}
333
334
		return $contents;
335
	}
336
337
	/**
338
	 * Translate legacy "title" source paths to their "sha1" counterparts
339
	 *
340
	 * This leaves destination paths alone since we don't want those to mutate
341
	 *
342
	 * @param array $ops
343
	 * @return array
344
	 */
345
	protected function mungeOpPaths( array $ops ) {
346
		// Ops that use 'src' and do not mutate core file data there
347
		static $srcRefOps = [ 'store', 'copy', 'describe' ];
348
		foreach ( $ops as &$op ) {
349
			if ( isset( $op['src'] ) && in_array( $op['op'], $srcRefOps ) ) {
350
				$op['src'] = $this->getBackendPath( $op['src'], true );
351
			}
352
			if ( isset( $op['srcs'] ) ) {
353
				$op['srcs'] = $this->getBackendPaths( $op['srcs'], true );
354
			}
355
		}
356
		return $ops;
357
	}
358
}
359