-
Notifications
You must be signed in to change notification settings - Fork 0
/
CloudStorageRepo.php
executable file
·382 lines (343 loc) · 11.1 KB
/
CloudStorageRepo.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
<?php
/**
* A repository for files accessible via the Cloud Storage filesystem. Does not support
* database access or registration.
* @ingroup FileRepo
*/
class CloudStorageRepo extends FileRepo {
protected $fileFactory = array( 'CloudStorageFile', 'newFromTitle' );
protected $oldFileFactory = false;
protected $bucketName;
var $directory, $deletedDir, $deletedHashLevels, $fileMode;
var $urlbase;
var $CSS_ACCESS_KEY, $CSS_SECRET_KEY, $CSS_PUBLIC, $CSS_SSL;
var $pathDisclosureProtection = 'simple';
function __construct( $info ) {
parent::__construct( $info );
global $wgCss;
$this->directory = isset( $info['directory'] ) ? $info['directory'] : $wgCss->getBucketUrl();
$this->bucketName = isset( $info['bucket'] ) ? $info['bucket'] : '';
if ( isset( $info['deletedDir'] ) ) {
$this->deletedDir = $info['deletedDir'];
} else {
$this->deletedDir = "{$this->directory}/archive";
}
if ( isset( $info['thumbDir'] ) ) {
$this->thumbDir = $info['thumbDir'];
} else {
$this->thumbDir = "{$this->directory}/thumb";
}
if ( isset( $info['thumbUrl'] ) ) {
$this->thumbUrl = $info['thumbUrl'];
} else {
$this->thumbUrl = "{$this->url}/thumb";
}
$this->fileMode = isset( $info['fileMode'] ) ? $info['fileMode'] : 0644;
}
/**
* need to override the parent method
* @see also FileRepo::initDirectory()
*/
protected function initDirectory( $dir ) {
$status = $this->newGood();
if (!file_exists($dir)) {
$result = mkdir($dir, 0755, true);
if ($result == false) {
$status->error( 'directorycreateerror', $dir);
$status->failCount++;
}
else
$status->successCount++;
}
return $status;
}
/**
* the local reference of a $url is its $url
* @see FileRepo::getLocalReference()
*/
public function getLocalReference( $url ) {
return new FSFile($url);
}
/**
* Get the directory corresponding to one of the three basic zones
*/
function getZonePath( $zone ) {
switch ( $zone ) {
case 'public':
return $this->directory;
case 'temp':
return "{$this->directory}/temp";
case 'deleted':
return $this->deletedDir;
case 'thumb':
return $this->thumbDir;
default:
return false;
}
}
public function setBucketName($bucketName) {
$this->bucketName = $bucketName;
}
/**
* Get the public (upload) root directory of the repository.
*/
function getRootDirectory() {
return $this->directory;
}
/**
* Get the public root URL of the repository (not authenticated, will not work in general)
*/
function getRootUrl() {
return $this->url;
}
/**
* Get the base URL of the repository (not authenticated, will not work in general)
*/
function getUrlBase() {
return $this->urlbase;
}
/**
* Returns true if the repository uses a multi-level directory structure
*/
function isHashed() {
return (bool)$this->hashLevels;
}
/**
* Store a batch of files from local (i.e. Windows or Linux) filesystem to Cloud Storage
*
* @param $triplets Array: (src,zone,dest) triplets as per store()
* @param $flags Integer: bitwise combination of the following flags:
* self::DELETE_SOURCE Delete the source file after upload
* self::OVERWRITE Overwrite an existing destination file instead of failing
* self::OVERWRITE_SAME Overwrite the file if the destination exists and has the
* same contents as the source (not implemented in Cloud Storage)
*/
function storeBatch( $triplets, $flags = 0 ) {
return parent::storeBatch($triplets, $flags);
}
/**
* Append a file from from local (i.e. Windows or Linux) filesystem to existing file
*
* @param $srcPath - file on filesystem
* @param $toAppendPath - file from local filesystem
* @param $flags Integer: bitwise combination of the following flags:
* self::DELETE_SOURCE Delete the toAppend file after append
*/
function append( $srcPath, $toAppendPath, $flags = 0 ) {
// virtual
return false;
}
/**
* Take all available measures to prevent web accessibility of new deleted
* directories, in case the user has not configured offline storage
* Not applicable in S3
*/
protected function initDeletedDir( $dir ) {
return;
}
/**
* Pick a random name in the temp zone and store a file to it.
* @param $originalName String: the base name of the file as specified
* by the user. The file extension will be maintained.
* @param $srcPath String: the current location of the file.
* @return FileRepoStatus object with the URL in the value.
*/
function storeTemp( $originalName, $srcPath ) {
wfDebug(__METHOD__.": ".print_r($originalName,true)."--> $srcPath \n");
$date = gmdate( "YmdHis" );
$hashPath = $this->getHashPath( $originalName );
$dstRel = "$hashPath$date!$originalName";
$dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
$result = $this->store( $srcPath, 'temp', $dstRel );
$result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
return $result;
}
/**
* Remove a temporary file or mark it for garbage collection
* @param $virtualUrl String: the virtual URL returned by storeTemp
* @return Boolean: true on success, false on failure
*/
function freeTemp( $virtualUrl ) {
wfDebug(__METHOD__.": ".print_r($virtualUrl,true)."\n");
global $wgCss;
$path = $virtualUrl;
$infoS3 = $wgCss->getObjectInfo($this->bucketName, $path); // see if on S3
wfDebug(__METHOD__." path: $path, infoS3:".print_r($infoS3,true)."\n");
$success = $wgCss->deleteObject($this->bucketName, $path);
return $success;
}
/**
* Publish a batch of files
* @param $triplets Array: (source,dest,archive) triplets as per publish()
* source can be on local machine or on S3, dest must be on S3
* @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate
* that the source files should be deleted if possible
*/
function publishBatch( $triplets, $flags = 0 ) {
return parent::publishBatch($triplets, $flags);
}
/**
* Move a group of files to the deletion archive.
* If no valid deletion archive is configured, this may either delete the
* file or throw an exception, depending on the preference of the repository.
*
* @param $sourceDestPairs Array of source/destination pairs. Each element
* is a two-element array containing the source file path relative to the
* public root in the first element, and the archive file path relative
* to the deleted zone root in the second element.
* @return FileRepoStatus
*/
function deleteBatch( $sourceDestPairs ) {
wfDebug(__METHOD__.": ".print_r($sourceDestPairs,true)."\n");
global $wgCss;
$status = $this->newGood();
if ( !$this->deletedDir ) {
throw new MWException( __METHOD__.': no valid deletion archive directory' );
}
/**
* Validate filenames and create archive directories
*/
foreach ( $sourceDestPairs as $pair ) {
list( $srcRel, $archiveRel ) = $pair;
if ( !$this->validateFilename( $srcRel ) ) {
throw new MWException( __METHOD__.':Validation error in $srcRel' );
}
if ( !$this->validateFilename( $archiveRel ) ) {
throw new MWException( __METHOD__.':Validation error in $archiveRel' );
}
$archivePath = "{$this->deletedDir}/$archiveRel";
// $archiveDir = dirname( $archivePath );
// if ( !is_dir( $archiveDir ) ) {
// if ( !wfMkdirParents( $archiveDir ) ) {
// $status->fatal( 'directorycreateerror', $archiveDir );
// continue;
// }
// $this->initDeletedDir( $archiveDir );
// }
// // Check if the archive directory is writable
// // This doesn't appear to work on NTFS
// if ( !is_writable( $archiveDir ) ) {
// $status->fatal( 'filedelete-archive-read-only', $archiveDir );
// }
}
if ( !$status->ok ) {
// Abort early
return $status;
}
/**
* Move the files
* We're now committed to returning an OK result, which will lead to
* the files being moved in the DB also.
*/
foreach ( $sourceDestPairs as $pair ) {
list( $srcRel, $archiveRel ) = $pair;
$srcPath = "{$this->directory}/$srcRel";
$archivePath = "{$this->deletedDir}/$archiveRel";
wfDebug(__METHOD__.": src: $srcPath, dest: $archivePath \n");
$good = true;
$info = $wgCss->getObjectInfo($this->bucketName, $archivePath);
wfDebug(__METHOD__." :$archivePath\ninfo:".print_r($info,true)."\n");
if ( $info ) {
# A file with this content hash is already archived
if ( !$wgCss->deleteObject($this->bucketName, $srcPath) ) {
$status->error( 'filedeleteerror', $srcPath );
$good = false;
}
} else{
if(! (
$wgCss->copyObject($this->bucketName, $srcPath,
$this->bucketName, $archivePath,
($this->CSS_PUBLIC ? S3::ACL_PUBLIC_READ : S3::ACL_PRIVATE)) &&
$wgCss->deleteObject($this->bucketName, $srcPath))
) {
wfDebug(__METHOD__.": FAILED moving file $dstPath to $archivePath\n");
$status->error( 'filerenameerror', $srcPath, $archivePath );
$good = false;
}
}
if ( $good ) {
$status->successCount++;
} else {
$status->failCount++;
}
}
return $status;
}
/**
* Get a relative path for a deletion archive key,
* e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg
*/
function getDeletedHashPath( $key ) {
$path = '';
for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
$path .= $key[$i] . '/';
}
return $path;
}
/**
* Call a callback function for every file in the repository.
* Uses the filesystem even in child classes.
*/
function enumFilesInFS( $callback ) {
global $wgCss;
$contents = $wgCss->getBucket($this->bucketName, $this->directory."/");
wfDebug(__METHOD__." :".print_r($contents,true)."\n");
foreach( $contents as $path ) {
call_user_func( $callback, $path->name );
}
}
/**
* Call a callback function for every file in the repository
* May use either the database or the filesystem
*/
function enumFiles( $callback ) {
$this->enumFilesInFS( $callback );
}
/**
* Get properties of a file with a given virtual URL
* The virtual URL must refer to this repo
*/
function getFileProps( $virtualUrl ) {
$path = $this->resolveVirtualUrl( $virtualUrl );
return File::getPropsFromPath( $path );
}
/**
* Path disclosure protection functions
*
* Get a callback function to use for cleaning error message parameters
*/
function getErrorCleanupFunction() {
switch ( $this->pathDisclosureProtection ) {
case 'simple':
$callback = array( $this, 'simpleClean' );
break;
default:
$callback = parent::getErrorCleanupFunction();
}
return $callback;
}
function simpleClean( $param ) {
if ( !isset( $this->simpleCleanPairs ) ) {
global $IP;
$this->simpleCleanPairs = array(
$this->directory => 'public',
"{$this->directory}/temp" => 'temp',
$IP => '$IP',
dirname( __FILE__ ) => '$IP/extensions/WebStore',
);
if ( $this->deletedDir ) {
$this->simpleCleanPairs[$this->deletedDir] = 'deleted';
}
}
return strtr( $param, $this->simpleCleanPairs );
}
/**
* Chmod a file, supressing the warnings.
* @param $path String: the path to change
*/
protected function chmod( $path ) {
wfSuppressWarnings();
chmod( $path, $this->fileMode );
wfRestoreWarnings();
}
}