|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* @author Bart Visscher <[email protected]> |
|
4
|
|
|
* @author Björn Schießle <[email protected]> |
|
5
|
|
|
* @author Jakob Sack <[email protected]> |
|
6
|
|
|
* @author Joas Schilling <[email protected]> |
|
7
|
|
|
* @author Jörn Friedrich Dreyer <[email protected]> |
|
8
|
|
|
* @author Lukas Reschke <[email protected]> |
|
9
|
|
|
* @author Morris Jobke <[email protected]> |
|
10
|
|
|
* @author Owen Winkler <[email protected]> |
|
11
|
|
|
* @author Robin Appelman <[email protected]> |
|
12
|
|
|
* @author Roeland Jago Douma <[email protected]> |
|
13
|
|
|
* @author Semih Serhat Karakaya <[email protected]> |
|
14
|
|
|
* @author Thomas Müller <[email protected]> |
|
15
|
|
|
* @author Vincent Petry <[email protected]> |
|
16
|
|
|
* |
|
17
|
|
|
* @copyright Copyright (c) 2017, ownCloud GmbH |
|
18
|
|
|
* @license AGPL-3.0 |
|
19
|
|
|
* |
|
20
|
|
|
* This code is free software: you can redistribute it and/or modify |
|
21
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
|
22
|
|
|
* as published by the Free Software Foundation. |
|
23
|
|
|
* |
|
24
|
|
|
* This program is distributed in the hope that it will be useful, |
|
25
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
26
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
27
|
|
|
* GNU Affero General Public License for more details. |
|
28
|
|
|
* |
|
29
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
|
30
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
|
31
|
|
|
* |
|
32
|
|
|
*/ |
|
33
|
|
|
|
|
34
|
|
|
namespace OCA\DAV\Connector\Sabre; |
|
35
|
|
|
|
|
36
|
|
|
use OC\Files\Filesystem; |
|
37
|
|
|
use OC\Files\Storage\Storage; |
|
38
|
|
|
use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge; |
|
39
|
|
|
use OCA\DAV\Connector\Sabre\Exception\FileLocked; |
|
40
|
|
|
use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException; |
|
41
|
|
|
use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType; |
|
42
|
|
|
use OCP\Encryption\Exceptions\GenericEncryptionException; |
|
43
|
|
|
use OCP\Files\EntityTooLargeException; |
|
44
|
|
|
use OCP\Files\ForbiddenException; |
|
45
|
|
|
use OCP\Files\InvalidContentException; |
|
46
|
|
|
use OCP\Files\InvalidPathException; |
|
47
|
|
|
use OCP\Files\LockNotAcquiredException; |
|
48
|
|
|
use OCP\Files\NotPermittedException; |
|
49
|
|
|
use OCP\Files\StorageNotAvailableException; |
|
50
|
|
|
use OCP\Lock\ILockingProvider; |
|
51
|
|
|
use OCP\Lock\LockedException; |
|
52
|
|
|
use Sabre\DAV\Exception; |
|
53
|
|
|
use Sabre\DAV\Exception\BadRequest; |
|
54
|
|
|
use Sabre\DAV\Exception\Forbidden; |
|
55
|
|
|
use Sabre\DAV\Exception\NotImplemented; |
|
56
|
|
|
use Sabre\DAV\Exception\ServiceUnavailable; |
|
57
|
|
|
use Sabre\DAV\IFile; |
|
58
|
|
|
use Sabre\DAV\Exception\NotFound; |
|
59
|
|
|
use OC\AppFramework\Http\Request; |
|
60
|
|
|
|
|
61
|
|
|
class File extends Node implements IFile { |
|
62
|
|
|
|
|
63
|
|
|
protected $request; |
|
64
|
|
|
|
|
65
|
|
|
/** |
|
66
|
|
|
* Sets up the node, expects a full path name |
|
67
|
|
|
* |
|
68
|
|
|
* @param \OC\Files\View $view |
|
69
|
|
|
* @param \OCP\Files\FileInfo $info |
|
70
|
|
|
* @param \OCP\Share\IManager $shareManager |
|
71
|
|
|
*/ |
|
72
|
|
|
public function __construct($view, $info, $shareManager = null, Request $request = null) { |
|
73
|
|
|
if (isset($request)) { |
|
74
|
|
|
$this->request = $request; |
|
75
|
|
|
} else { |
|
76
|
|
|
$this->request = \OC::$server->getRequest(); |
|
77
|
|
|
} |
|
78
|
|
|
parent::__construct($view, $info, $shareManager); |
|
79
|
|
|
} |
|
80
|
|
|
|
|
81
|
|
|
/** |
|
82
|
|
|
* Updates the data |
|
83
|
|
|
* |
|
84
|
|
|
* The data argument is a readable stream resource. |
|
85
|
|
|
* |
|
86
|
|
|
* After a successful put operation, you may choose to return an ETag. The |
|
87
|
|
|
* etag must always be surrounded by double-quotes. These quotes must |
|
88
|
|
|
* appear in the actual string you're returning. |
|
89
|
|
|
* |
|
90
|
|
|
* Clients may use the ETag from a PUT request to later on make sure that |
|
91
|
|
|
* when they update the file, the contents haven't changed in the mean |
|
92
|
|
|
* time. |
|
93
|
|
|
* |
|
94
|
|
|
* If you don't plan to store the file byte-by-byte, and you return a |
|
95
|
|
|
* different object on a subsequent GET you are strongly recommended to not |
|
96
|
|
|
* return an ETag, and just return null. |
|
97
|
|
|
* |
|
98
|
|
|
* @param resource|string $data |
|
99
|
|
|
* |
|
100
|
|
|
* @throws Forbidden |
|
101
|
|
|
* @throws UnsupportedMediaType |
|
102
|
|
|
* @throws BadRequest |
|
103
|
|
|
* @throws Exception |
|
104
|
|
|
* @throws EntityTooLarge |
|
105
|
|
|
* @throws ServiceUnavailable |
|
106
|
|
|
* @throws FileLocked |
|
107
|
|
|
* @return string|null |
|
108
|
|
|
*/ |
|
109
|
|
|
public function put($data) { |
|
110
|
|
|
try { |
|
111
|
|
|
$exists = $this->fileView->file_exists($this->path); |
|
112
|
|
|
if ($this->info && $exists && !$this->info->isUpdateable()) { |
|
113
|
|
|
throw new Forbidden(); |
|
114
|
|
|
} |
|
115
|
|
|
} catch (StorageNotAvailableException $e) { |
|
116
|
|
|
throw new ServiceUnavailable("File is not updatable: " . $e->getMessage()); |
|
117
|
|
|
} |
|
118
|
|
|
|
|
119
|
|
|
// verify path of the target |
|
120
|
|
|
$this->verifyPath(); |
|
121
|
|
|
|
|
122
|
|
|
// chunked handling |
|
123
|
|
|
if (isset($_SERVER['HTTP_OC_CHUNKED'])) { |
|
124
|
|
|
try { |
|
125
|
|
|
return $this->createFileChunked($data); |
|
|
|
|
|
|
126
|
|
|
} catch (\Exception $e) { |
|
127
|
|
|
$this->convertToSabreException($e); |
|
128
|
|
|
} |
|
129
|
|
|
} |
|
130
|
|
|
|
|
131
|
|
|
list($partStorage) = $this->fileView->resolvePath($this->path); |
|
132
|
|
|
$needsPartFile = $this->needsPartFile($partStorage) && (strlen($this->path) > 1); |
|
133
|
|
|
|
|
134
|
|
|
if ($needsPartFile) { |
|
135
|
|
|
// mark file as partial while uploading (ignored by the scanner) |
|
136
|
|
|
$partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part'; |
|
137
|
|
|
} else { |
|
138
|
|
|
// upload file directly as the final path |
|
139
|
|
|
$partFilePath = $this->path; |
|
140
|
|
|
} |
|
141
|
|
|
|
|
142
|
|
|
// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share) |
|
143
|
|
|
/** @var \OC\Files\Storage\Storage $partStorage */ |
|
144
|
|
|
list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath); |
|
145
|
|
|
/** @var \OC\Files\Storage\Storage $storage */ |
|
146
|
|
|
list($storage, $internalPath) = $this->fileView->resolvePath($this->path); |
|
147
|
|
|
try { |
|
148
|
|
|
$target = $partStorage->fopen($internalPartPath, 'wb'); |
|
149
|
|
|
if ($target === false) { |
|
150
|
|
|
\OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::fopen() failed', \OCP\Util::ERROR); |
|
151
|
|
|
// because we have no clue about the cause we can only throw back a 500/Internal Server Error |
|
152
|
|
|
throw new Exception('Could not write file contents'); |
|
153
|
|
|
} |
|
154
|
|
|
list($count, $result) = \OC_Helper::streamCopy($data, $target); |
|
|
|
|
|
|
155
|
|
|
fclose($target); |
|
156
|
|
|
|
|
157
|
|
|
if (!self::isChecksumValid($partStorage, $internalPartPath)) { |
|
158
|
|
|
throw new BadRequest('The computed checksum does not match the one received from the client.'); |
|
159
|
|
|
} |
|
160
|
|
|
|
|
161
|
|
|
if ($result === false) { |
|
162
|
|
|
$expected = -1; |
|
163
|
|
|
if (isset($_SERVER['CONTENT_LENGTH'])) { |
|
164
|
|
|
$expected = $_SERVER['CONTENT_LENGTH']; |
|
165
|
|
|
} |
|
166
|
|
|
throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )'); |
|
167
|
|
|
} |
|
168
|
|
|
|
|
169
|
|
|
// if content length is sent by client: |
|
170
|
|
|
// double check if the file was fully received |
|
171
|
|
|
// compare expected and actual size |
|
172
|
|
|
if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { |
|
173
|
|
|
$expected = $_SERVER['CONTENT_LENGTH']; |
|
174
|
|
|
if ($count != $expected) { |
|
175
|
|
|
throw new BadRequest('expected filesize ' . $expected . ' got ' . $count); |
|
176
|
|
|
} |
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
|
|
} catch (\Exception $e) { |
|
180
|
|
|
if ($needsPartFile) { |
|
181
|
|
|
$partStorage->unlink($internalPartPath); |
|
182
|
|
|
} |
|
183
|
|
|
$this->convertToSabreException($e); |
|
184
|
|
|
} |
|
185
|
|
|
|
|
186
|
|
|
try { |
|
187
|
|
|
$view = \OC\Files\Filesystem::getView(); |
|
188
|
|
|
if ($view) { |
|
189
|
|
|
$run = $this->emitPreHooks($exists); |
|
190
|
|
|
} else { |
|
191
|
|
|
$run = true; |
|
192
|
|
|
} |
|
193
|
|
|
|
|
194
|
|
|
try { |
|
195
|
|
|
$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
196
|
|
|
} catch (LockedException $e) { |
|
197
|
|
|
if ($needsPartFile) { |
|
198
|
|
|
$partStorage->unlink($internalPartPath); |
|
199
|
|
|
} |
|
200
|
|
|
throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
201
|
|
|
} |
|
202
|
|
|
|
|
203
|
|
|
if ($needsPartFile) { |
|
204
|
|
|
// rename to correct path |
|
205
|
|
|
try { |
|
206
|
|
|
if ($run) { |
|
207
|
|
|
$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); |
|
208
|
|
|
$fileExists = $storage->file_exists($internalPath); |
|
209
|
|
|
} |
|
210
|
|
|
if (!$run || $renameOkay === false || $fileExists === false) { |
|
|
|
|
|
|
211
|
|
|
\OCP\Util::writeLog('webdav', 'renaming part file to final file failed', \OCP\Util::ERROR); |
|
212
|
|
|
throw new Exception('Could not rename part file to final file'); |
|
213
|
|
|
} |
|
214
|
|
|
} catch (ForbiddenException $ex) { |
|
215
|
|
|
throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
216
|
|
|
} catch (\Exception $e) { |
|
217
|
|
|
$partStorage->unlink($internalPartPath); |
|
218
|
|
|
$this->convertToSabreException($e); |
|
219
|
|
|
} |
|
220
|
|
|
} |
|
221
|
|
|
|
|
222
|
|
|
// since we skipped the view we need to scan and emit the hooks ourselves |
|
223
|
|
|
$storage->getUpdater()->update($internalPath); |
|
224
|
|
|
|
|
225
|
|
|
try { |
|
226
|
|
|
$this->changeLock(ILockingProvider::LOCK_SHARED); |
|
227
|
|
|
} catch (LockedException $e) { |
|
228
|
|
|
throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
229
|
|
|
} |
|
230
|
|
|
|
|
231
|
|
|
// allow sync clients to send the mtime along in a header |
|
232
|
|
|
if (isset($this->request->server['HTTP_X_OC_MTIME'])) { |
|
|
|
|
|
|
233
|
|
|
$mtime = $this->sanitizeMtime($this->request->server ['HTTP_X_OC_MTIME']); |
|
|
|
|
|
|
234
|
|
|
if ($this->fileView->touch($this->path, $mtime)) { |
|
235
|
|
|
header('X-OC-MTime: accepted'); |
|
236
|
|
|
} |
|
237
|
|
|
} |
|
238
|
|
|
|
|
239
|
|
|
if ($view) { |
|
240
|
|
|
$this->emitPostHooks($exists); |
|
241
|
|
|
} |
|
242
|
|
|
|
|
243
|
|
|
$this->refreshInfo(); |
|
244
|
|
|
} catch (StorageNotAvailableException $e) { |
|
245
|
|
|
throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage()); |
|
246
|
|
|
} |
|
247
|
|
|
|
|
248
|
|
|
return '"' . $this->info->getEtag() . '"'; |
|
249
|
|
|
} |
|
250
|
|
|
|
|
251
|
|
|
private function getPartFileBasePath($path) { |
|
252
|
|
|
$partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true); |
|
253
|
|
|
if ($partFileInStorage) { |
|
254
|
|
|
return $path; |
|
255
|
|
|
} else { |
|
256
|
|
|
return md5($path); // will place it in the root of the view with a unique name |
|
257
|
|
|
} |
|
258
|
|
|
} |
|
259
|
|
|
|
|
260
|
|
|
/** |
|
261
|
|
|
* @param string $path |
|
262
|
|
|
*/ |
|
263
|
|
|
private function emitPreHooks($exists, $path = null) { |
|
264
|
|
|
if (is_null($path)) { |
|
265
|
|
|
$path = $this->path; |
|
266
|
|
|
} |
|
267
|
|
|
$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
|
268
|
|
|
$run = true; |
|
269
|
|
|
|
|
270
|
|
View Code Duplication |
if (!$exists) { |
|
|
|
|
|
|
271
|
|
|
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, [ |
|
272
|
|
|
\OC\Files\Filesystem::signal_param_path => $hookPath, |
|
273
|
|
|
\OC\Files\Filesystem::signal_param_run => &$run, |
|
274
|
|
|
]); |
|
275
|
|
|
} else { |
|
276
|
|
|
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, [ |
|
277
|
|
|
\OC\Files\Filesystem::signal_param_path => $hookPath, |
|
278
|
|
|
\OC\Files\Filesystem::signal_param_run => &$run, |
|
279
|
|
|
]); |
|
280
|
|
|
} |
|
281
|
|
|
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, [ |
|
282
|
|
|
\OC\Files\Filesystem::signal_param_path => $hookPath, |
|
283
|
|
|
\OC\Files\Filesystem::signal_param_run => &$run, |
|
284
|
|
|
]); |
|
285
|
|
|
return $run; |
|
286
|
|
|
} |
|
287
|
|
|
|
|
288
|
|
|
/** |
|
289
|
|
|
* @param string $path |
|
290
|
|
|
*/ |
|
291
|
|
|
private function emitPostHooks($exists, $path = null) { |
|
292
|
|
|
if (is_null($path)) { |
|
293
|
|
|
$path = $this->path; |
|
294
|
|
|
} |
|
295
|
|
|
$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
|
296
|
|
View Code Duplication |
if (!$exists) { |
|
|
|
|
|
|
297
|
|
|
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, [ |
|
298
|
|
|
\OC\Files\Filesystem::signal_param_path => $hookPath |
|
299
|
|
|
]); |
|
300
|
|
|
} else { |
|
301
|
|
|
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, [ |
|
302
|
|
|
\OC\Files\Filesystem::signal_param_path => $hookPath |
|
303
|
|
|
]); |
|
304
|
|
|
} |
|
305
|
|
|
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, [ |
|
306
|
|
|
\OC\Files\Filesystem::signal_param_path => $hookPath |
|
307
|
|
|
]); |
|
308
|
|
|
} |
|
309
|
|
|
|
|
310
|
|
|
/** |
|
311
|
|
|
* Returns the data |
|
312
|
|
|
* |
|
313
|
|
|
* @return resource |
|
314
|
|
|
* @throws Forbidden |
|
315
|
|
|
* @throws ServiceUnavailable |
|
316
|
|
|
*/ |
|
317
|
|
|
public function get() { |
|
318
|
|
|
//throw exception if encryption is disabled but files are still encrypted |
|
319
|
|
|
try { |
|
320
|
|
|
$viewPath = ltrim($this->path, '/'); |
|
321
|
|
|
if (!$this->info->isReadable() || !$this->fileView->file_exists($viewPath)) { |
|
322
|
|
|
// do a if the file did not exist |
|
323
|
|
|
throw new NotFound(); |
|
324
|
|
|
} |
|
325
|
|
|
$res = $this->fileView->fopen($viewPath, 'rb'); |
|
326
|
|
|
if ($res === false) { |
|
327
|
|
|
throw new ServiceUnavailable("Could not open file"); |
|
328
|
|
|
} |
|
329
|
|
|
return $res; |
|
330
|
|
|
} catch (GenericEncryptionException $e) { |
|
331
|
|
|
// returning 503 will allow retry of the operation at a later point in time |
|
332
|
|
|
throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage()); |
|
333
|
|
|
} catch (StorageNotAvailableException $e) { |
|
334
|
|
|
throw new ServiceUnavailable("Failed to open file: " . $e->getMessage()); |
|
335
|
|
|
} catch (ForbiddenException $ex) { |
|
336
|
|
|
throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
337
|
|
|
} catch (LockedException $e) { |
|
338
|
|
|
throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
339
|
|
|
} |
|
340
|
|
|
} |
|
341
|
|
|
|
|
342
|
|
|
/** |
|
343
|
|
|
* Delete the current file |
|
344
|
|
|
* |
|
345
|
|
|
* @throws Forbidden |
|
346
|
|
|
* @throws ServiceUnavailable |
|
347
|
|
|
*/ |
|
348
|
|
|
public function delete() { |
|
349
|
|
|
if (!$this->info->isDeletable()) { |
|
350
|
|
|
throw new Forbidden(); |
|
351
|
|
|
} |
|
352
|
|
|
|
|
353
|
|
|
try { |
|
354
|
|
|
if (!$this->fileView->unlink($this->path)) { |
|
355
|
|
|
// assume it wasn't possible to delete due to permissions |
|
356
|
|
|
throw new Forbidden(); |
|
357
|
|
|
} |
|
358
|
|
|
} catch (StorageNotAvailableException $e) { |
|
359
|
|
|
throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage()); |
|
360
|
|
|
} catch (ForbiddenException $ex) { |
|
361
|
|
|
throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
362
|
|
|
} catch (LockedException $e) { |
|
363
|
|
|
throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
364
|
|
|
} |
|
365
|
|
|
} |
|
366
|
|
|
|
|
367
|
|
|
/** |
|
368
|
|
|
* Returns the mime-type for a file |
|
369
|
|
|
* |
|
370
|
|
|
* If null is returned, we'll assume application/octet-stream |
|
371
|
|
|
* |
|
372
|
|
|
* @return string |
|
373
|
|
|
*/ |
|
374
|
|
|
public function getContentType() { |
|
375
|
|
|
$mimeType = $this->info->getMimetype(); |
|
376
|
|
|
|
|
377
|
|
|
// PROPFIND needs to return the correct mime type, for consistency with the web UI |
|
378
|
|
|
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') { |
|
379
|
|
|
return $mimeType; |
|
380
|
|
|
} |
|
381
|
|
|
return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType); |
|
382
|
|
|
} |
|
383
|
|
|
|
|
384
|
|
|
/** |
|
385
|
|
|
* @return array|false |
|
386
|
|
|
*/ |
|
387
|
|
|
public function getDirectDownload() { |
|
388
|
|
|
if (\OCP\App::isEnabled('encryption')) { |
|
389
|
|
|
return []; |
|
390
|
|
|
} |
|
391
|
|
|
/** @var \OCP\Files\Storage $storage */ |
|
392
|
|
|
list($storage, $internalPath) = $this->fileView->resolvePath($this->path); |
|
393
|
|
|
if (is_null($storage)) { |
|
394
|
|
|
return []; |
|
395
|
|
|
} |
|
396
|
|
|
|
|
397
|
|
|
return $storage->getDirectDownload($internalPath); |
|
398
|
|
|
} |
|
399
|
|
|
|
|
400
|
|
|
/** |
|
401
|
|
|
* @param resource $data |
|
402
|
|
|
* @return null|string |
|
403
|
|
|
* @throws Exception |
|
404
|
|
|
* @throws BadRequest |
|
405
|
|
|
* @throws NotImplemented |
|
406
|
|
|
* @throws ServiceUnavailable |
|
407
|
|
|
*/ |
|
408
|
|
|
private function createFileChunked($data) { |
|
409
|
|
|
list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($this->path); |
|
410
|
|
|
|
|
411
|
|
|
$info = \OC_FileChunking::decodeName($name); |
|
412
|
|
|
if (empty($info)) { |
|
413
|
|
|
throw new NotImplemented('Invalid chunk name'); |
|
414
|
|
|
} |
|
415
|
|
|
|
|
416
|
|
|
$chunk_handler = new \OC_FileChunking($info); |
|
417
|
|
|
$bytesWritten = $chunk_handler->store($info['index'], $data); |
|
418
|
|
|
|
|
419
|
|
|
//detect aborted upload |
|
420
|
|
|
if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { |
|
421
|
|
|
if (isset($_SERVER['CONTENT_LENGTH'])) { |
|
422
|
|
|
$expected = $_SERVER['CONTENT_LENGTH']; |
|
423
|
|
|
if ($bytesWritten != $expected) { |
|
424
|
|
|
$chunk_handler->remove($info['index']); |
|
425
|
|
|
throw new BadRequest( |
|
426
|
|
|
'expected filesize ' . $expected . ' got ' . $bytesWritten); |
|
427
|
|
|
} |
|
428
|
|
|
} |
|
429
|
|
|
} |
|
430
|
|
|
|
|
431
|
|
|
if ($chunk_handler->isComplete()) { |
|
432
|
|
|
list($storage,) = $this->fileView->resolvePath($path); |
|
433
|
|
|
$needsPartFile = $this->needsPartFile($storage); |
|
434
|
|
|
$partFile = null; |
|
435
|
|
|
|
|
436
|
|
|
$targetPath = $path . '/' . $info['name']; |
|
437
|
|
|
/** @var \OC\Files\Storage\Storage $targetStorage */ |
|
438
|
|
|
list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); |
|
439
|
|
|
|
|
440
|
|
|
$exists = $this->fileView->file_exists($targetPath); |
|
441
|
|
|
|
|
442
|
|
|
try { |
|
443
|
|
|
$this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED); |
|
444
|
|
|
|
|
445
|
|
|
$this->emitPreHooks($exists, $targetPath); |
|
446
|
|
|
$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE); |
|
447
|
|
|
/** @var \OC\Files\Storage\Storage $targetStorage */ |
|
448
|
|
|
list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath); |
|
449
|
|
|
|
|
450
|
|
|
if ($needsPartFile) { |
|
451
|
|
|
// we first assembly the target file as a part file |
|
452
|
|
|
$partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part'; |
|
453
|
|
|
/** @var \OC\Files\Storage\Storage $targetStorage */ |
|
454
|
|
|
list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile); |
|
455
|
|
|
|
|
456
|
|
|
|
|
457
|
|
|
$chunk_handler->file_assemble($partStorage, $partInternalPath); |
|
458
|
|
|
|
|
459
|
|
|
if (!self::isChecksumValid($partStorage, $partInternalPath)) { |
|
460
|
|
|
throw new BadRequest('The computed checksum does not match the one received from the client.'); |
|
461
|
|
|
} |
|
462
|
|
|
|
|
463
|
|
|
// here is the final atomic rename |
|
464
|
|
|
$renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath); |
|
465
|
|
|
$fileExists = $targetStorage->file_exists($targetInternalPath); |
|
466
|
|
|
if ($renameOkay === false || $fileExists === false) { |
|
467
|
|
|
\OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::rename() failed', \OCP\Util::ERROR); |
|
468
|
|
|
// only delete if an error occurred and the target file was already created |
|
469
|
|
|
if ($fileExists) { |
|
470
|
|
|
// set to null to avoid double-deletion when handling exception |
|
471
|
|
|
// stray part file |
|
472
|
|
|
$partFile = null; |
|
473
|
|
|
$targetStorage->unlink($targetInternalPath); |
|
474
|
|
|
} |
|
475
|
|
|
$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); |
|
476
|
|
|
throw new Exception('Could not rename part file assembled from chunks'); |
|
477
|
|
|
} |
|
478
|
|
|
} else { |
|
479
|
|
|
// assemble directly into the final file |
|
480
|
|
|
$chunk_handler->file_assemble($targetStorage, $targetInternalPath); |
|
481
|
|
|
} |
|
482
|
|
|
|
|
483
|
|
|
// allow sync clients to send the mtime along in a header |
|
484
|
|
|
if (isset($this->request->server['HTTP_X_OC_MTIME'])) { |
|
|
|
|
|
|
485
|
|
|
$mtime = $this->sanitizeMtime( |
|
486
|
|
|
$this->request->server ['HTTP_X_OC_MTIME'] |
|
|
|
|
|
|
487
|
|
|
); |
|
488
|
|
|
if ($targetStorage->touch($targetInternalPath, $mtime)) { |
|
489
|
|
|
header('X-OC-MTime: accepted'); |
|
490
|
|
|
} |
|
491
|
|
|
} |
|
492
|
|
|
|
|
493
|
|
|
// since we skipped the view we need to scan and emit the hooks ourselves |
|
494
|
|
|
$targetStorage->getUpdater()->update($targetInternalPath); |
|
495
|
|
|
|
|
496
|
|
|
$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); |
|
497
|
|
|
|
|
498
|
|
|
$this->emitPostHooks($exists, $targetPath); |
|
499
|
|
|
|
|
500
|
|
|
// FIXME: should call refreshInfo but can't because $this->path is not the of the final file |
|
501
|
|
|
$info = $this->fileView->getFileInfo($targetPath); |
|
502
|
|
|
|
|
503
|
|
|
|
|
504
|
|
|
if (isset($partStorage) && isset($partInternalPath)) { |
|
505
|
|
|
$checksums = $partStorage->getMetaData($partInternalPath)['checksum']; |
|
506
|
|
|
} else { |
|
507
|
|
|
$checksums = $targetStorage->getMetaData($targetInternalPath)['checksum']; |
|
508
|
|
|
} |
|
509
|
|
|
|
|
510
|
|
|
$this->fileView->putFileInfo( |
|
511
|
|
|
$targetPath, |
|
512
|
|
|
['checksum' => $checksums] |
|
513
|
|
|
); |
|
514
|
|
|
|
|
515
|
|
|
$this->refreshInfo(); |
|
516
|
|
|
|
|
517
|
|
|
$this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED); |
|
518
|
|
|
|
|
519
|
|
|
return $info->getEtag(); |
|
520
|
|
|
} catch (\Exception $e) { |
|
521
|
|
|
if ($partFile !== null) { |
|
522
|
|
|
$targetStorage->unlink($targetInternalPath); |
|
523
|
|
|
} |
|
524
|
|
|
$this->convertToSabreException($e); |
|
525
|
|
|
} |
|
526
|
|
|
} |
|
527
|
|
|
|
|
528
|
|
|
return null; |
|
529
|
|
|
} |
|
530
|
|
|
|
|
531
|
|
|
/** |
|
532
|
|
|
* will return true if checksum was not provided in request |
|
533
|
|
|
* |
|
534
|
|
|
* @param Storage $storage |
|
535
|
|
|
* @param $path |
|
536
|
|
|
* @return bool |
|
537
|
|
|
*/ |
|
538
|
|
|
private static function isChecksumValid(Storage $storage, $path) { |
|
539
|
|
|
$meta = $storage->getMetaData($path); |
|
540
|
|
|
$request = \OC::$server->getRequest(); |
|
541
|
|
|
|
|
542
|
|
|
if (!isset($request->server['HTTP_OC_CHECKSUM']) || !isset($meta['checksum'])) { |
|
|
|
|
|
|
543
|
|
|
// No comparison possible, skip the check |
|
544
|
|
|
return true; |
|
545
|
|
|
} |
|
546
|
|
|
|
|
547
|
|
|
$expectedChecksum = trim($request->server['HTTP_OC_CHECKSUM']); |
|
|
|
|
|
|
548
|
|
|
$computedChecksums = $meta['checksum']; |
|
549
|
|
|
|
|
550
|
|
|
return strpos($computedChecksums, $expectedChecksum) !== false; |
|
551
|
|
|
|
|
552
|
|
|
} |
|
553
|
|
|
|
|
554
|
|
|
/** |
|
555
|
|
|
* Returns whether a part file is needed for the given storage |
|
556
|
|
|
* or whether the file can be assembled/uploaded directly on the |
|
557
|
|
|
* target storage. |
|
558
|
|
|
* |
|
559
|
|
|
* @param \OCP\Files\Storage $storage |
|
560
|
|
|
* @return bool true if the storage needs part file handling |
|
561
|
|
|
*/ |
|
562
|
|
|
private function needsPartFile($storage) { |
|
563
|
|
|
// TODO: in the future use ChunkHandler provided by storage |
|
564
|
|
|
// and/or add method on Storage called "needsPartFile()" |
|
565
|
|
|
return !$storage->instanceOfStorage('OCA\Files_Sharing\External\Storage') && |
|
566
|
|
|
!$storage->instanceOfStorage('OC\Files\Storage\OwnCloud') && |
|
567
|
|
|
!$storage->instanceOfStorage('OC\Files\ObjectStore\ObjectStoreStorage'); |
|
568
|
|
|
} |
|
569
|
|
|
|
|
570
|
|
|
/** |
|
571
|
|
|
* Convert the given exception to a SabreException instance |
|
572
|
|
|
* |
|
573
|
|
|
* @param \Exception $e |
|
574
|
|
|
* |
|
575
|
|
|
* @throws \Sabre\DAV\Exception |
|
576
|
|
|
*/ |
|
577
|
|
|
private function convertToSabreException(\Exception $e) { |
|
578
|
|
|
if ($e instanceof \Sabre\DAV\Exception) { |
|
579
|
|
|
throw $e; |
|
580
|
|
|
} |
|
581
|
|
|
if ($e instanceof NotPermittedException) { |
|
582
|
|
|
// a more general case - due to whatever reason the content could not be written |
|
583
|
|
|
throw new Forbidden($e->getMessage(), 0, $e); |
|
584
|
|
|
} |
|
585
|
|
|
if ($e instanceof ForbiddenException) { |
|
586
|
|
|
// the path for the file was forbidden |
|
587
|
|
|
throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e); |
|
588
|
|
|
} |
|
589
|
|
|
if ($e instanceof EntityTooLargeException) { |
|
590
|
|
|
// the file is too big to be stored |
|
591
|
|
|
throw new EntityTooLarge($e->getMessage(), 0, $e); |
|
592
|
|
|
} |
|
593
|
|
|
if ($e instanceof InvalidContentException) { |
|
594
|
|
|
// the file content is not permitted |
|
595
|
|
|
throw new UnsupportedMediaType($e->getMessage(), 0, $e); |
|
596
|
|
|
} |
|
597
|
|
|
if ($e instanceof InvalidPathException) { |
|
598
|
|
|
// the path for the file was not valid |
|
599
|
|
|
// TODO: find proper http status code for this case |
|
600
|
|
|
throw new Forbidden($e->getMessage(), 0, $e); |
|
601
|
|
|
} |
|
602
|
|
|
if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) { |
|
603
|
|
|
// the file is currently being written to by another process |
|
604
|
|
|
throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
605
|
|
|
} |
|
606
|
|
|
if ($e instanceof GenericEncryptionException) { |
|
607
|
|
|
// returning 503 will allow retry of the operation at a later point in time |
|
608
|
|
|
throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e); |
|
609
|
|
|
} |
|
610
|
|
|
if ($e instanceof StorageNotAvailableException) { |
|
611
|
|
|
throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e); |
|
612
|
|
|
} |
|
613
|
|
|
|
|
614
|
|
|
throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e); |
|
615
|
|
|
} |
|
616
|
|
|
|
|
617
|
|
|
/** |
|
618
|
|
|
* Set $algo to get a specific checksum, leave null to get all checksums |
|
619
|
|
|
* (space seperated) |
|
620
|
|
|
* @param null $algo |
|
621
|
|
|
* @return string |
|
622
|
|
|
*/ |
|
623
|
|
|
public function getChecksum($algo = null) { |
|
624
|
|
|
$allChecksums = $this->info->getChecksum(); |
|
625
|
|
|
|
|
626
|
|
|
if (!$algo) { |
|
627
|
|
|
return $allChecksums; |
|
628
|
|
|
} |
|
629
|
|
|
|
|
630
|
|
|
$checksums = explode(' ', $allChecksums); |
|
631
|
|
|
$algoPrefix = strtoupper($algo) . ':'; |
|
632
|
|
|
|
|
633
|
|
|
foreach ($checksums as $checksum) { |
|
634
|
|
|
// starts with $algoPrefix |
|
635
|
|
|
if (substr($checksum, 0, strlen($algoPrefix)) === $algoPrefix) { |
|
636
|
|
|
return $checksum; |
|
637
|
|
|
} |
|
638
|
|
|
} |
|
639
|
|
|
|
|
640
|
|
|
return ''; |
|
641
|
|
|
} |
|
642
|
|
|
} |
|
643
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.