Merge pull request #21610 from owncloud/quota-sharedfile

Fix quota check for single shared files
This commit is contained in:
Thomas Müller 2016-02-18 14:25:32 +01:00
commit 6c4cdddba8
5 changed files with 180 additions and 24 deletions

View File

@ -95,12 +95,14 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
$req = $this->server->httpRequest;
if ($req->getHeader('OC-Chunked')) {
$info = \OC_FileChunking::decodeName($newName);
$chunkHandler = new \OC_FileChunking($info);
$chunkHandler = $this->getFileChunking($info);
// subtract the already uploaded size to see whether
// there is still enough space for the remaining chunks
$length -= $chunkHandler->getCurrentSize();
// use target file name for free space check in case of shared files
$uri = rtrim($parentUri, '/') . '/' . $info['name'];
}
$freeSpace = $this->getFreeSpace($parentUri);
$freeSpace = $this->getFreeSpace($uri);
if ($freeSpace !== \OCP\Files\FileInfo::SPACE_UNKNOWN && $length > $freeSpace) {
if (isset($chunkHandler)) {
$chunkHandler->cleanup();
@ -111,6 +113,11 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
return true;
}
public function getFileChunking($info) {
// FIXME: need a factory for better mocking support
return new \OC_FileChunking($info);
}
public function getLength() {
$req = $this->server->httpRequest;
$length = $req->getHeader('X-Expected-Entity-Length');
@ -127,12 +134,12 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
}
/**
* @param string $parentUri
* @param string $uri
* @return mixed
*/
public function getFreeSpace($parentUri) {
public function getFreeSpace($uri) {
try {
$freeSpace = $this->view->free_space($parentUri);
$freeSpace = $this->view->free_space(ltrim($uri, '/'));
return $freeSpace;
} catch (\OCP\Files\StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());

View File

@ -39,10 +39,13 @@ class QuotaPlugin extends \Test\TestCase {
*/
private $plugin;
private function init($quota) {
$view = $this->buildFileViewMock($quota);
private function init($quota, $checkedPath = '') {
$view = $this->buildFileViewMock($quota, $checkedPath);
$this->server = new \Sabre\DAV\Server();
$this->plugin = new \OCA\DAV\Connector\Sabre\QuotaPlugin($view);
$this->plugin = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\QuotaPlugin')
->setConstructorArgs([$view])
->setMethods(['getFileChunking'])
->getMock();
$this->plugin->initialize($this->server);
}
@ -51,6 +54,8 @@ class QuotaPlugin extends \Test\TestCase {
*/
public function testLength($expected, $headers) {
$this->init(0);
$this->plugin->expects($this->never())
->method('getFileChunking');
$this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
$length = $this->plugin->getLength();
$this->assertEquals($expected, $length);
@ -61,6 +66,8 @@ class QuotaPlugin extends \Test\TestCase {
*/
public function testCheckQuota($quota, $headers) {
$this->init($quota);
$this->plugin->expects($this->never())
->method('getFileChunking');
$this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
$result = $this->plugin->checkQuota('');
@ -73,11 +80,26 @@ class QuotaPlugin extends \Test\TestCase {
*/
public function testCheckExceededQuota($quota, $headers) {
$this->init($quota);
$this->plugin->expects($this->never())
->method('getFileChunking');
$this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
$this->plugin->checkQuota('');
}
/**
* @dataProvider quotaOkayProvider
*/
public function testCheckQuotaOnPath($quota, $headers) {
$this->init($quota, 'sub/test.txt');
$this->plugin->expects($this->never())
->method('getFileChunking');
$this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
$result = $this->plugin->checkQuota('/sub/test.txt');
$this->assertTrue($result);
}
public function quotaOkayProvider() {
return array(
array(1024, array()),
@ -110,12 +132,89 @@ class QuotaPlugin extends \Test\TestCase {
);
}
private function buildFileViewMock($quota) {
public function quotaChunkedOkProvider() {
return array(
array(1024, 0, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
array(1024, 0, array('CONTENT-LENGTH' => '512')),
array(1024, 0, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
// with existing chunks (allowed size = total length - chunk total size)
array(400, 128, array('X-EXPECTED-ENTITY-LENGTH' => '512')),
array(400, 128, array('CONTENT-LENGTH' => '512')),
array(400, 128, array('OC-TOTAL-LENGTH' => '512', 'CONTENT-LENGTH' => '500')),
// \OCP\Files\FileInfo::SPACE-UNKNOWN = -2
array(-2, 0, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
array(-2, 0, array('CONTENT-LENGTH' => '512')),
array(-2, 0, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
array(-2, 128, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
array(-2, 128, array('CONTENT-LENGTH' => '512')),
array(-2, 128, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
);
}
/**
* @dataProvider quotaChunkedOkProvider
*/
public function testCheckQuotaChunkedOk($quota, $chunkTotalSize, $headers) {
$this->init($quota, 'sub/test.txt');
$mockChunking = $this->getMockBuilder('\OC_FileChunking')
->disableOriginalConstructor()
->getMock();
$mockChunking->expects($this->once())
->method('getCurrentSize')
->will($this->returnValue($chunkTotalSize));
$this->plugin->expects($this->once())
->method('getFileChunking')
->will($this->returnValue($mockChunking));
$headers['OC-CHUNKED'] = 1;
$this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
$result = $this->plugin->checkQuota('/sub/test.txt-chunking-12345-3-1');
$this->assertTrue($result);
}
public function quotaChunkedFailProvider() {
return array(
array(400, 0, array('X-EXPECTED-ENTITY-LENGTH' => '1024')),
array(400, 0, array('CONTENT-LENGTH' => '512')),
array(400, 0, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')),
// with existing chunks (allowed size = total length - chunk total size)
array(380, 128, array('X-EXPECTED-ENTITY-LENGTH' => '512')),
array(380, 128, array('CONTENT-LENGTH' => '512')),
array(380, 128, array('OC-TOTAL-LENGTH' => '512', 'CONTENT-LENGTH' => '500')),
);
}
/**
* @dataProvider quotaChunkedFailProvider
* @expectedException \Sabre\DAV\Exception\InsufficientStorage
*/
public function testCheckQuotaChunkedFail($quota, $chunkTotalSize, $headers) {
$this->init($quota, 'sub/test.txt');
$mockChunking = $this->getMockBuilder('\OC_FileChunking')
->disableOriginalConstructor()
->getMock();
$mockChunking->expects($this->once())
->method('getCurrentSize')
->will($this->returnValue($chunkTotalSize));
$this->plugin->expects($this->once())
->method('getFileChunking')
->will($this->returnValue($mockChunking));
$headers['OC-CHUNKED'] = 1;
$this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers);
$this->plugin->checkQuota('/sub/test.txt-chunking-12345-3-1');
}
private function buildFileViewMock($quota, $checkedPath) {
// mock filesysten
$view = $this->getMock('\OC\Files\View', array('free_space'), array(), '', false);
$view->expects($this->any())
->method('free_space')
->with($this->identicalTo(''))
->with($this->identicalTo($checkedPath))
->will($this->returnValue($quota));
return $view;

View File

@ -136,8 +136,12 @@ $maxUploadFileSize = $storageStats['uploadMaxFilesize'];
$maxHumanFileSize = OCP\Util::humanFileSize($maxUploadFileSize);
$totalSize = 0;
foreach ($files['size'] as $size) {
$totalSize += $size;
$isReceivedShare = \OC::$server->getRequest()->getParam('isReceivedShare', false) === 'true';
// defer quota check for received shares
if (!$isReceivedShare) {
foreach ($files['size'] as $size) {
$totalSize += $size;
}
}
if ($maxUploadFileSize >= 0 and $totalSize > $maxUploadFileSize) {
OCP\JSON::error(array('data' => array('message' => $l->t('Not enough storage available'),

View File

@ -251,7 +251,26 @@ OC.Upload = {
$('#file_upload_start').trigger(new $.Event('resized'));
},
/**
* Returns whether the given file is known to be a received shared file
*
* @param {Object} file file
* @return {bool} true if the file is a shared file
*/
_isReceivedSharedFile: function(file) {
if (!window.FileList) {
return false;
}
var $tr = window.FileList.findFileEl(file.name);
if (!$tr.length) {
return false;
}
return ($tr.attr('data-mounttype') === 'shared-root' && $tr.attr('data-mime') !== 'httpd/unix-directory');
},
init: function() {
var self = this;
if ( $('#file_upload_start').exists() ) {
var file_upload_param = {
dropZone: $('#content'), // restrict dropZone to content div
@ -341,10 +360,15 @@ OC.Upload = {
}
}
// add size
selection.totalBytes += file.size;
// update size of biggest file
selection.biggestFileBytes = Math.max(selection.biggestFileBytes, file.size);
// only count if we're not overwriting an existing shared file
if (self._isReceivedSharedFile(file)) {
file.isReceivedShare = true;
} else {
// add size
selection.totalBytes += file.size;
// update size of biggest file
selection.biggestFileBytes = Math.max(selection.biggestFileBytes, file.size);
}
// check PHP upload limit against biggest file
if (selection.biggestFileBytes > $('#upload_limit').val()) {
@ -430,11 +454,16 @@ OC.Upload = {
fileDirectory = data.files[0].relativePath;
}
addFormData(data.formData, {
var params = {
requesttoken: oc_requesttoken,
dir: data.targetDir || FileList.getCurrentDirectory(),
file_directory: fileDirectory
});
file_directory: fileDirectory,
};
if (data.files[0].isReceivedShare) {
params.isReceivedShare = true;
}
addFormData(data.formData, params);
},
fail: function(e, data) {
OC.Upload.log('fail', e, data);

View File

@ -141,16 +141,33 @@ class Quota extends Wrapper {
*/
public function fopen($path, $mode) {
$source = $this->storage->fopen($path, $mode);
$free = $this->free_space('');
if ($source && $free >= 0 && $mode !== 'r' && $mode !== 'rb') {
// only apply quota for files, not metadata, trash or others
if (strpos(ltrim($path, '/'), 'files/') === 0) {
return \OC\Files\Stream\Quota::wrap($source, $free);
// don't apply quota for part files
if (!$this->isPartFile($path)) {
$free = $this->free_space('');
if ($source && $free >= 0 && $mode !== 'r' && $mode !== 'rb') {
// only apply quota for files, not metadata, trash or others
if (strpos(ltrim($path, '/'), 'files/') === 0) {
return \OC\Files\Stream\Quota::wrap($source, $free);
}
}
}
return $source;
}
/**
* Checks whether the given path is a part file
*
* @param string $path Path that may identify a .part file
* @return string File path without .part extension
* @note this is needed for reusing keys
*/
private function isPartFile($path) {
$extension = pathinfo($path, PATHINFO_EXTENSION);
return ($extension === 'part');
}
/**
* @param \OCP\Files\Storage $sourceStorage
* @param string $sourceInternalPath