Enables MediaWiki to use Windows Azure as a file backend.
[lhc/web/wiklou.git] / extensions / WindowsAzureStorage / includes / filerepo / backend / WindowsAzureFileBackend.php
1 <?php
2 /**
3 * @file
4 * @ingroup FileBackend
5 * @author Markus Glaser
6 * @author Robert Vogel
7 * @author Hallo Welt! - Medienwerkstatt GmbH for Microsoft Corp.
8 */
9
10 /**
11 * Copied and modified from Swift FileBackend:
12 *
13 * Class for a Windows Azure Blob Storage based file backend.
14 * Status messages should avoid mentioning the Azure account name
15 * Likewise, error suppression should be used to avoid path disclosure.
16 *
17 * This requires the PHPAzure library to be present,
18 * which is available at http://phpazure.codeplex.com/.
19 * All of the library classes must be registed in $wgAutoloadClasses.
20 * You may use the WindowsAzureSDK MediaWiki extension to fulfill this
21 * requirement.
22 *
23 * @ingroup FileBackend
24 */
25 class WindowsAzureFileBackend extends FileBackend {
26
27 function doStore( array $p ) {
28 return $this->doStoreInternal( $p );
29 }
30
31 function doCopy( array $p ) {
32 return $this->doCopyInternal( $p );
33 }
34
35 function doDelete( array $p ) {
36 return $this->doDeleteInternal( $p );
37 }
38
39 function doConcatenate( array $p ) {
40 return $this->dodoConcatenateInternal( $p );
41 }
42
43 function doCreate( array $p ) {
44 return $this->doCreateInternal( $p );
45 }
46
47 /**
48 * @see FileBackend::move()
49 */
50 protected function doMove( array $params ) {
51 // Copy source to dest
52 // TODO: Remove backend. I assume, this function does not need to be overridden.
53 $status = $this->backend->copy( $params );
54 if ( !$status->isOK() ) {
55 return $status;
56 }
57 // Delete source (only fails due to races or medium going down)
58 // TODO: Remoce backend
59 $status->merge( $this->backend->delete( array( 'src' => $params['src'] ) ) );
60 $status->setResult( true, $status->value ); // ignore delete() errors
61 return $status;
62 }
63
64 /** @var Microsoft_WindowsAzure_Storage_Blob */
65 protected $storageClient = null;
66
67 /** @var Array Map of container names to Azure container names */
68 protected $containerPaths = array();
69
70 /**
71 * @see FileBackend::__construct()
72 * Additional $config params include:
73 * azureHost : Windows Azure server URL
74 * azureAccount : Windows Azure user used by MediaWiki
75 * azureKey : Authentication key for the above user (used to get sessions)
76 * //azureContainer : Identifier of the container. (Optional. If not provided wikiId will be used as container name)
77 * containerPaths : Map of container names to Azure container names
78 */
79 public function __construct( array $config ) {
80 parent::__construct( $config );
81 $this->storageClient = new Microsoft_WindowsAzure_Storage_Blob(
82 $config['azureHost'],
83 $config['azureAccount'],
84 $config['azureKey']
85 );
86
87 $this->containerPaths = (array)$config['containerPaths'];
88 }
89
90 /**
91 * @see FileBackend::resolveContainerPath()
92 */
93 protected function resolveContainerPath( $container, $relStoragePath ) {
94 //Azure container naming conventions; http://msdn.microsoft.com/en-us/library/dd135715.aspx
95
96 if ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
97 return null;
98 }
99 // TODO: Should storagepath not be urlencoded?
100 return $relStoragePath;
101 }
102
103 /**
104 * @see FileBackend::doStoreInternal()
105 */
106 function doStoreInternal( array $params ) {
107 $status = Status::newGood();
108 // TODO: Use more telling names
109 list( $c, $dir ) = $this->resolveStoragePath( $params['dst'] );
110 try {
111 $result = $this->storageClient->putBlob( $c, $dir, $params['src']);
112 }
113 catch ( Exception $e ) {
114 // TODO: Read exception. Are there different ones?
115 $status->fatal( 'backend-fail-put' );
116 }
117 //error_log( __METHOD__.'::putBlob - result: '.print_r( $result, true ) );
118 return $status;
119 }
120
121 /**
122 * @see FileBackend::doCopyInternal()
123 */
124 function doCopyInternal( array $params ) {
125 $status = Status::newGood();
126 list( $srcContainer, $srcDir ) = $this->resolveStoragePath( $params['src'] );
127 list( $dstContainer, $dstDir ) = $this->resolveStoragePath( $params['dst'] );
128 // TODO: check for null
129 try {
130 $result = $this->storageClient->copyBlob( $srcContainer, $srcDir, $dstContainer, $dstDir);
131 }
132 catch ( Exception $e ) {
133 $status->fatal( 'backend-fail-copy', $e->getMessage() );
134 }
135 //error_log( __METHOD__.'::copyBlob - result: '.print_r( $result, true ) );
136 return $status;
137 }
138
139 /**
140 * @see FileBackend::doDeleteInternal()
141 */
142 function doDeleteInternal( array $params ) {
143 $status = Status::newGood();
144
145 list( $srcCont, $srcRel ) = $this->resolveStoragePath( $params['src'] );
146 if ( $srcRel === null ) {
147 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
148 return $status;
149 }
150
151 // (a) Check the source container
152 try { //TODO: Unnecessary --> remove
153 $container = $this->storageClient->getContainer( $srcCont );
154 }
155 catch ( Exception $e ) {
156 // TODO: remove error_log
157 error_log( __METHOD__.':'.__LINE__.' '.$e->getMessage() );
158 $status->fatal( 'backend-fail-internal' );
159 return $status;
160 }
161
162 // (b) Actually delete the object
163 try {
164 $this->storageClient->deleteBlob( $srcCont, $srcRel );
165 }
166 catch ( Exception $e ) {
167 error_log( __METHOD__.':'.__LINE__.' '.$e->getMessage() );
168 $status->fatal( 'backend-fail-internal' );
169 }
170
171 return $status;
172 }
173
174 /**
175 * @see FileBackend::doCreateInternal()
176 */
177 function doCreateInternal( array $params ) {
178 $status = Status::newGood();
179
180 list( $dstCont, $dstRel ) = $this->resolveStoragePath( $params['dst'] );
181 if ( $dstRel === null ) {
182 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
183 return $status;
184 }
185
186 // (a) Check if the destination object already exists
187 $blobExists = $this->storageClient->blobExists( $dstCont, $dstRel );
188 if ( $blobExists && empty( $params['overwriteDest'] ) ) { //Blob exists _and_ should not be overridden
189 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
190 return $status;
191 }
192
193 // (b) Actually create the object
194 try {
195 // TODO: how do I know the container exists? Should we call prepare?
196 $this->storageClient->putBlobData( $dstCont, $dstRel, $params['content'] );
197 }
198 catch ( Exception $e ) {
199 error_log( __METHOD__.':'.__LINE__.' '.$e->getMessage() );
200 $status->fatal( 'backend-fail-internal' );
201 }
202
203 return $status;
204 }
205
206 /**
207 * @see FileBackend::prepare()
208 */
209 function prepare( array $params ) {
210 $status = Status::newGood();
211
212 list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
213 if ( $dir === null ) {
214 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
215 return $status; // invalid storage path
216 }
217 try {
218 $this->storageClient->createContainerIfNotExists( $c );
219 $this->storageClient->setContainerAcl( $c, Microsoft_WindowsAzure_Storage_Blob::ACL_PUBLIC );//TODO: Really set public?
220
221 //TODO: check if readable and writeable
222 //$container = $this->storageClient->getContainer( $c );
223 //$status->fatal( 'directoryreadonlyerror', $params['dir'] );
224 //$status->fatal( 'directorynotreadableerror', $params['dir'] );
225 }
226 catch (Exception $e ) {
227 $status->fatal( 'directorycreateerror', $params['dir'] );
228 return $status;
229 }
230 return $status;
231 }
232
233 /**
234 * @see FileBackend::resolveContainerName()
235 */
236 protected function resolveContainerName( $container ) {
237 //Azure container naming conventions; http://msdn.microsoft.com/en-us/library/dd135715.aspx
238 $container = strtolower($container);
239 $container = preg_replace( '#[^a-z0-9\-]#', '', $container );
240 // TODO: -test und test- geht auch nicht
241 $container = preg_replace( '#-{2,}#', '-', $container );
242
243 return $container;
244 }
245
246 /**
247 * @see FileBackend::secure()
248 */
249 function secure( array $params ) {
250 $status = Status::newGood();
251 // @TODO: restrict container from $this->swiftProxyUser
252 return $status; // badgers? We don't need no steenking badgers!
253 }
254
255 /**
256 * @see FileBackend::fileExists()
257 */
258 function fileExists( array $params ) {
259 list( $c, $dir ) = $this->resolveStoragePath( $params['src'] );
260 // TODO: null? Telling names
261 $exists = $this->storageClient->blobExists( $c, $dir );
262 //error_log( __METHOD__.'::blobExists - result: '.$exists );
263 return $exists;
264 }
265
266 /**
267 * @see FileBackend::getFileTimestamp()
268 */
269 function getFileTimestamp( array $params ) {
270 list( $srcCont, $srcRel ) = $this->resolveStoragePath( $params['src'] );
271 if ( $srcRel === null ) {
272 return false; // invalid storage path
273 }
274
275 $timestamp= false;
276 try {
277 //TODO Maybe use getBlobData()?
278 $blob = $this->storageClient->getBlobInstance( $srcCont, $srcRel );
279 $timestamp = wfTimestamp( TS_MW, $blob->lastmodified ); //TODO: Timezone?
280 } catch ( Exception $e ) { // some other exception?
281 error_log( __METHOD__.':'.__LINE__.' '.$e->getMessage() );
282 }
283 return $timestamp;
284 }
285
286 /**
287 * @see FileBackend::getFileList()
288 */
289 function getFileList( array $params ) {
290 $files = array();
291 list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
292 try {
293 if ( $dir === null ) {
294 $blobs = $this->storageClient->listBlobs($c);
295 }
296 else {
297 $blobs = $this->storageClient->listBlobs( $c, $dir );//TODO:Check if $dir really is a startsequence of the blob name
298 }
299 foreach( $blobs as $blob ) {
300 $files[] = $blob->name;
301 }
302 }
303 catch( Exception $e ) {
304 error_log( __METHOD__.':'.__LINE__.' '.$e->getMessage() );
305 return null;
306 }
307
308 // if there are no files matching the prefix, return empty array
309 return $files;
310 }
311
312 /**
313 * @see FileBackend::getLocalCopy()
314 */
315 function getLocalCopy( array $params ) {
316 list( $srcCont, $srcRel ) = $this->resolveStoragePath( $params['src'] );
317 if ( $srcRel === null ) {
318 return null;
319 }
320
321 // Get source file extension
322 $ext = FileBackend::extensionFromPath( $srcRel );
323 // Create a new temporary file...
324 // TODO: Caution: tempfile should not write a local file.
325 $tmpFile = TempFSFile::factory( wfBaseName( $srcRel ) . '_', $ext );
326 if ( !$tmpFile ) {
327 return null;
328 }
329 $tmpPath = $tmpFile->getPath();
330
331 try {
332 $this->storageClient->getBlob( $srcCont, $srcRel, $tmpPath );
333 }
334 catch ( Exception $e ) {
335 error_log( __METHOD__.':'.__LINE__.' '.$e->getMessage() );
336 $tmpFile = null;
337 }
338
339 return $tmpFile;
340 }
341 }