Merge pull request #4467 from owncloud/storage-wrapper-quota

Move quota logic from filesystem proxy to storage wrapper
This commit is contained in:
icewind1991 2013-08-19 03:38:55 -07:00
commit d7dde3cfbc
19 changed files with 454 additions and 147 deletions

View File

@ -284,7 +284,7 @@ class Google extends \OC\Files\Storage\Common {
// Check if this is a Google Doc
if ($this->getMimeType($path) !== $file->getMimeType()) {
// Return unknown file size
$stat['size'] = \OC\Files\FREE_SPACE_UNKNOWN;
$stat['size'] = \OC\Files\SPACE_UNKNOWN;
} else {
$stat['size'] = $file->getFileSize();
}

View File

@ -225,7 +225,7 @@ class DAV extends \OC\Files\Storage\Common{
return 0;
}
} catch(\Exception $e) {
return \OC\Files\FREE_SPACE_UNKNOWN;
return \OC\Files\SPACE_UNKNOWN;
}
}

View File

@ -391,7 +391,7 @@ class Shared extends \OC\Files\Storage\Common {
public function free_space($path) {
if ($path == '') {
return \OC\Files\FREE_SPACE_UNKNOWN;
return \OC\Files\SPACE_UNKNOWN;
}
$source = $this->getSourcePath($path);
if ($source) {

View File

@ -91,7 +91,7 @@ class OC {
// ensure we can find OC_Config
set_include_path(
OC::$SERVERROOT . '/lib' . PATH_SEPARATOR .
get_include_path()
get_include_path()
);
OC::$SUBURI = str_replace("\\", "/", substr(realpath($_SERVER["SCRIPT_FILENAME"]), strlen(OC::$SERVERROOT)));
@ -160,11 +160,11 @@ class OC {
// set the right include path
set_include_path(
OC::$SERVERROOT . '/lib' . PATH_SEPARATOR .
OC::$SERVERROOT . '/config' . PATH_SEPARATOR .
OC::$THIRDPARTYROOT . '/3rdparty' . PATH_SEPARATOR .
implode($paths, PATH_SEPARATOR) . PATH_SEPARATOR .
get_include_path() . PATH_SEPARATOR .
OC::$SERVERROOT
OC::$SERVERROOT . '/config' . PATH_SEPARATOR .
OC::$THIRDPARTYROOT . '/3rdparty' . PATH_SEPARATOR .
implode($paths, PATH_SEPARATOR) . PATH_SEPARATOR .
get_include_path() . PATH_SEPARATOR .
OC::$SERVERROOT
);
}
@ -278,17 +278,17 @@ class OC {
ini_set('session.cookie_httponly', '1;');
// set the cookie path to the ownCloud directory
$cookie_path = OC::$WEBROOT ?: '/';
$cookie_path = OC::$WEBROOT ? : '/';
ini_set('session.cookie_path', $cookie_path);
//set the session object to a dummy session so code relying on the session existing still works
self::$session = new \OC\Session\Memory('');
try{
try {
// set the session name to the instance id - which is unique
self::$session = new \OC\Session\Internal(OC_Util::getInstanceId());
// if session cant be started break with http 500 error
}catch (Exception $e){
} catch (Exception $e) {
OC_Log::write('core', 'Session could not be initialized',
OC_Log::ERROR);
@ -352,7 +352,7 @@ class OC {
public static function init() {
// register autoloader
require_once __DIR__ . '/autoloader.php';
self::$loader=new \OC\Autoloader();
self::$loader = new \OC\Autoloader();
self::$loader->registerPrefix('Doctrine\\Common', 'doctrine/common/lib');
self::$loader->registerPrefix('Doctrine\\DBAL', 'doctrine/dbal/lib');
self::$loader->registerPrefix('Symfony\\Component\\Routing', 'symfony/routing');
@ -373,7 +373,7 @@ class OC {
ini_set('arg_separator.output', '&');
// try to switch magic quotes off.
if (get_magic_quotes_gpc()==1) {
if (get_magic_quotes_gpc() == 1) {
ini_set('magic_quotes_runtime', 0);
}
@ -398,7 +398,8 @@ class OC {
//set http auth headers for apache+php-cgi work around
if (isset($_SERVER['HTTP_AUTHORIZATION'])
&& preg_match('/Basic\s+(.*)$/i', $_SERVER['HTTP_AUTHORIZATION'], $matches)) {
&& preg_match('/Basic\s+(.*)$/i', $_SERVER['HTTP_AUTHORIZATION'], $matches)
) {
list($name, $password) = explode(':', base64_decode($matches[1]), 2);
$_SERVER['PHP_AUTH_USER'] = strip_tags($name);
$_SERVER['PHP_AUTH_PW'] = strip_tags($password);
@ -406,7 +407,8 @@ class OC {
//set http auth headers for apache+php-cgi work around if variable gets renamed by apache
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])
&& preg_match('/Basic\s+(.*)$/i', $_SERVER['REDIRECT_HTTP_AUTHORIZATION'], $matches)) {
&& preg_match('/Basic\s+(.*)$/i', $_SERVER['REDIRECT_HTTP_AUTHORIZATION'], $matches)
) {
list($name, $password) = explode(':', base64_decode($matches[1]), 2);
$_SERVER['PHP_AUTH_USER'] = strip_tags($name);
$_SERVER['PHP_AUTH_PW'] = strip_tags($password);
@ -435,10 +437,11 @@ class OC {
stream_wrapper_register('fakedir', 'OC\Files\Stream\Dir');
stream_wrapper_register('static', 'OC\Files\Stream\StaticStream');
stream_wrapper_register('close', 'OC\Files\Stream\Close');
stream_wrapper_register('quota', 'OC\Files\Stream\Quota');
stream_wrapper_register('oc', 'OC\Files\Stream\OC');
self::initTemplateEngine();
if ( !self::$CLI ) {
if (!self::$CLI) {
self::initSession();
} else {
self::$session = new \OC\Session\Memory('');
@ -459,7 +462,7 @@ class OC {
// User and Groups
if (!OC_Config::getValue("installed", false)) {
self::$session->set('user_id','');
self::$session->set('user_id', '');
}
OC_User::useBackend(new OC_User_Database());

View File

@ -51,7 +51,7 @@ class OC_Connector_Sabre_QuotaPlugin extends Sabre_DAV_ServerPlugin {
}
list($parentUri, $newName) = Sabre_DAV_URLUtil::splitPath($uri);
$freeSpace = \OC\Files\Filesystem::free_space($parentUri);
if ($freeSpace !== \OC\Files\FREE_SPACE_UNKNOWN && $length > $freeSpace) {
if ($freeSpace !== \OC\Files\SPACE_UNKNOWN && $length > $freeSpace) {
throw new Sabre_DAV_Exception_InsufficientStorage();
}
}

View File

@ -1,114 +0,0 @@
<?php
/**
* ownCloud
*
* @author Robin Appelman
* @copyright 2011 Robin Appelman icewind1991@gmail.com
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* user quota management
*/
class OC_FileProxy_Quota extends OC_FileProxy{
static $rootView;
private $userQuota=array();
/**
* get the quota for the user
* @param user
* @return int
*/
private function getQuota($user) {
if(in_array($user, $this->userQuota)) {
return $this->userQuota[$user];
}
$userQuota=OC_Preferences::getValue($user, 'files', 'quota', 'default');
if($userQuota=='default') {
$userQuota=OC_AppConfig::getValue('files', 'default_quota', 'none');
}
if($userQuota=='none') {
$this->userQuota[$user]=-1;
}else{
$this->userQuota[$user]=OC_Helper::computerFileSize($userQuota);
}
return $this->userQuota[$user];
}
/**
* get the free space in the path's owner home folder
* @param path
* @return int
*/
private function getFreeSpace($path) {
/**
* @var \OC\Files\Storage\Storage $storage
* @var string $internalPath
*/
list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath($path);
$owner = $storage->getOwner($internalPath);
if (!$owner) {
return -1;
}
$totalSpace = $this->getQuota($owner);
if($totalSpace == -1) {
return -1;
}
$view = new \OC\Files\View("/".$owner."/files");
$rootInfo = $view->getFileInfo('/');
$usedSpace = isset($rootInfo['size'])?$rootInfo['size']:0;
return $totalSpace - $usedSpace;
}
public function postFree_space($path, $space) {
$free=$this->getFreeSpace($path);
if($free==-1) {
return $space;
}
if ($space < 0){
return $free;
}
return min($free, $space);
}
public function preFile_put_contents($path, $data) {
if (is_resource($data)) {
$data = '';//TODO: find a way to get the length of the stream without emptying it
}
return (strlen($data)<$this->getFreeSpace($path) or $this->getFreeSpace($path)==-1);
}
public function preCopy($path1, $path2) {
if(!self::$rootView) {
self::$rootView = new \OC\Files\View('');
}
return (self::$rootView->filesize($path1)<$this->getFreeSpace($path2) or $this->getFreeSpace($path2)==-1);
}
public function preFromTmpFile($tmpfile, $path) {
return (filesize($tmpfile)<$this->getFreeSpace($path) or $this->getFreeSpace($path)==-1);
}
public function preFromUploadedFile($tmpfile, $path) {
return (filesize($tmpfile)<$this->getFreeSpace($path) or $this->getFreeSpace($path)==-1);
}
}

View File

@ -31,8 +31,9 @@
namespace OC\Files;
use OC\Files\Storage\Loader;
const FREE_SPACE_UNKNOWN = -2;
const FREE_SPACE_UNLIMITED = -3;
const SPACE_NOT_COMPUTED = -1;
const SPACE_UNKNOWN = -2;
const SPACE_UNLIMITED = -3;
class Filesystem {
/**
@ -148,6 +149,18 @@ class Filesystem {
*/
private static $loader;
/**
* @param callable $wrapper
*/
public static function addStorageWrapper($wrapper) {
self::getLoader()->addStorageWrapper($wrapper);
$mounts = self::getMountManager()->getAll();
foreach ($mounts as $mount) {
$mount->wrapStorage($wrapper);
}
}
public static function getLoader() {
if (!self::$loader) {
self::$loader = new Loader();

View File

@ -95,6 +95,13 @@ class Manager {
return $result;
}
/**
* @return Mount[]
*/
public function getAll() {
return $this->mounts;
}
/**
* Find mounts by numeric storage id
*

View File

@ -138,4 +138,11 @@ class Mount {
}
return $path;
}
/**
* @param callable $wrapper
*/
public function wrapStorage($wrapper) {
$this->storage = $wrapper($this->mountPoint, $this->storage);
}
}

View File

@ -366,6 +366,6 @@ abstract class Common implements \OC\Files\Storage\Storage {
* @return int
*/
public function free_space($path) {
return \OC\Files\FREE_SPACE_UNKNOWN;
return \OC\Files\SPACE_UNKNOWN;
}
}

View File

@ -265,7 +265,7 @@ if (\OC_Util::runningOnWindows()) {
public function free_space($path) {
$space = @disk_free_space($this->datadir . $path);
if ($space === false) {
return \OC\Files\FREE_SPACE_UNKNOWN;
return \OC\Files\SPACE_UNKNOWN;
}
return $space;
}

View File

@ -0,0 +1,104 @@
<?php
/**
* Copyright (c) 2013 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace OC\Files\Storage\Wrapper;
class Quota extends Wrapper {
/**
* @var int $quota
*/
protected $quota;
/**
* @param array $parameters
*/
public function __construct($parameters) {
$this->storage = $parameters['storage'];
$this->quota = $parameters['quota'];
}
protected function getSize($path) {
$cache = $this->getCache();
$data = $cache->get($path);
if (is_array($data) and isset($data['size'])) {
return $data['size'];
} else {
return \OC\Files\SPACE_NOT_COMPUTED;
}
}
/**
* Get free space as limited by the quota
*
* @param string $path
* @return int
*/
public function free_space($path) {
if ($this->quota < 0) {
return $this->storage->free_space($path);
} else {
$used = $this->getSize('');
if ($used < 0) {
return \OC\Files\SPACE_NOT_COMPUTED;
} else {
$free = $this->storage->free_space($path);
return min($free, (max($this->quota - $used, 0)));
}
}
}
/**
* see http://php.net/manual/en/function.file_put_contents.php
*
* @param string $path
* @param string $data
* @return bool
*/
public function file_put_contents($path, $data) {
$free = $this->free_space('');
if ($free < 0 or strlen($data) < $free) {
return $this->storage->file_put_contents($path, $data);
} else {
return false;
}
}
/**
* see http://php.net/manual/en/function.copy.php
*
* @param string $source
* @param string $target
* @return bool
*/
public function copy($source, $target) {
$free = $this->free_space('');
if ($free < 0 or $this->getSize($source) < $free) {
return $this->storage->copy($source, $target);
} else {
return false;
}
}
/**
* see http://php.net/manual/en/function.fopen.php
*
* @param string $path
* @param string $mode
* @return resource
*/
public function fopen($path, $mode) {
$source = $this->storage->fopen($path, $mode);
$free = $this->free_space('');
if ($free >= 0) {
return \OC\Files\Stream\Quota::wrap($source, $free);
} else {
return $source;
}
}
}

View File

@ -395,7 +395,7 @@ class Wrapper implements \OC\Files\Storage\Storage {
* @return \OC\Files\Cache\Permissions
*/
public function getPermissionsCache($path = '') {
return $this->storage->getPermissions($path);
return $this->storage->getPermissionsCache($path);
}
/**

128
lib/files/stream/quota.php Normal file
View File

@ -0,0 +1,128 @@
<?php
/**
* Copyright (c) 2013 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace OC\Files\Stream;
/**
* stream wrapper limits the amount of data that can be written to a stream
*
* usage: void \OC\Files\Stream\Quota::register($id, $stream, $limit)
* or: resource \OC\Files\Stream\Quota::wrap($stream, $limit)
*/
class Quota {
private static $streams = array();
/**
* @var resource $source
*/
private $source;
/**
* @var int $limit
*/
private $limit;
/**
* @param string $id
* @param resource $stream
* @param int $limit
*/
public static function register($id, $stream, $limit) {
self::$streams[$id] = array($stream, $limit);
}
/**
* remove all registered streams
*/
public static function clear() {
self::$streams = array();
}
/**
* @param resource $stream
* @param int $limit
* @return resource
*/
static public function wrap($stream, $limit) {
$id = uniqid();
self::register($id, $stream, $limit);
$meta = stream_get_meta_data($stream);
return fopen('quota://' . $id, $meta['mode']);
}
public function stream_open($path, $mode, $options, &$opened_path) {
$id = substr($path, strlen('quota://'));
if (isset(self::$streams[$id])) {
list($this->source, $this->limit) = self::$streams[$id];
return true;
} else {
return false;
}
}
public function stream_seek($offset, $whence = SEEK_SET) {
if ($whence === SEEK_SET) {
$this->limit += $this->stream_tell() - $offset;
} else {
$this->limit -= $offset;
}
fseek($this->source, $offset, $whence);
}
public function stream_tell() {
return ftell($this->source);
}
public function stream_read($count) {
$this->limit -= $count;
return fread($this->source, $count);
}
public function stream_write($data) {
$size = strlen($data);
if ($size > $this->limit) {
$data = substr($data, 0, $this->limit);
$size = $this->limit;
}
$this->limit -= $size;
return fwrite($this->source, $data);
}
public function stream_set_option($option, $arg1, $arg2) {
switch ($option) {
case STREAM_OPTION_BLOCKING:
stream_set_blocking($this->source, $arg1);
break;
case STREAM_OPTION_READ_TIMEOUT:
stream_set_timeout($this->source, $arg1, $arg2);
break;
case STREAM_OPTION_WRITE_BUFFER:
stream_set_write_buffer($this->source, $arg1, $arg2);
}
}
public function stream_stat() {
return fstat($this->source);
}
public function stream_lock($mode) {
flock($this->source, $mode);
}
public function stream_flush() {
return fflush($this->source);
}
public function stream_eof() {
return feof($this->source);
}
public function stream_close() {
fclose($this->source);
}
}

View File

@ -55,8 +55,8 @@ class OC_Helper {
*
* Returns a url to the given app and file.
*/
public static function linkTo($app, $file, $args = array()) {
if ($app != '') {
public static function linkTo( $app, $file, $args = array() ) {
if( $app != '' ) {
$app_path = OC_App::getAppPath($app);
// Check if the app is in the app folder
if ($app_path && file_exists($app_path . '/' . $file)) {
@ -786,14 +786,14 @@ class OC_Helper {
$post_max_size = OCP\Util::computerFileSize(ini_get('post_max_size'));
$freeSpace = \OC\Files\Filesystem::free_space($dir);
if ((int)$upload_max_filesize === 0 and (int)$post_max_size === 0) {
$maxUploadFilesize = \OC\Files\FREE_SPACE_UNLIMITED;
$maxUploadFilesize = \OC\Files\SPACE_UNLIMITED;
} elseif ((int)$upload_max_filesize === 0 or (int)$post_max_size === 0) {
$maxUploadFilesize = max($upload_max_filesize, $post_max_size); //only the non 0 value counts
} else {
$maxUploadFilesize = min($upload_max_filesize, $post_max_size);
}
if ($freeSpace !== \OC\Files\FREE_SPACE_UNKNOWN) {
if ($freeSpace !== \OC\Files\SPACE_UNKNOWN) {
$freeSpace = max($freeSpace, 0);
return min($maxUploadFilesize, $freeSpace);

View File

@ -46,6 +46,16 @@ class OC_Util {
}
if( $user != "" ) { //if we aren't logged in, there is no use to set up the filesystem
$quota = self::getUserQuota($user);
if ($quota !== \OC\Files\SPACE_UNLIMITED) {
\OC\Files\Filesystem::addStorageWrapper(function($mountPoint, $storage) use ($quota, $user) {
if ($mountPoint === '/' . $user . '/'){
return new \OC\Files\Storage\Wrapper\Quota(array('storage' => $storage, 'quota' => $quota));
} else {
return $storage;
}
});
}
$user_dir = '/'.$user.'/files';
$user_root = OC_User::getHome($user);
$userdirectory = $user_root . '/files';
@ -55,9 +65,7 @@ class OC_Util {
//jail the user into his "home" directory
\OC\Files\Filesystem::init($user, $user_dir);
$quotaProxy=new OC_FileProxy_Quota();
$fileOperationProxy = new OC_FileProxy_FileOperations();
OC_FileProxy::register($quotaProxy);
OC_FileProxy::register($fileOperationProxy);
OC_Hook::emit('OC_Filesystem', 'setup', array('user' => $user, 'user_dir' => $user_dir));
@ -65,6 +73,18 @@ class OC_Util {
return true;
}
public static function getUserQuota($user){
$userQuota = OC_Preferences::getValue($user, 'files', 'quota', 'default');
if($userQuota === 'default') {
$userQuota = OC_AppConfig::getValue('files', 'default_quota', 'none');
}
if($userQuota === 'none') {
return \OC\Files\SPACE_UNLIMITED;
}else{
return OC_Helper::computerFileSize($userQuota);
}
}
public static function tearDownFS() {
\OC\Files\Filesystem::tearDown();
self::$fsSetup=false;

View File

@ -0,0 +1,61 @@
<?php
/**
* Copyright (c) 2013 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace Test\Files\Storage\Wrapper;
//ensure the constants are loaded
\OC::$loader->load('\OC\Files\Filesystem');
class Quota extends \Test\Files\Storage\Storage {
/**
* @var string tmpDir
*/
private $tmpDir;
public function setUp() {
$this->tmpDir = \OC_Helper::tmpFolder();
$storage = new \OC\Files\Storage\Local(array('datadir' => $this->tmpDir));
$this->instance = new \OC\Files\Storage\Wrapper\Quota(array('storage' => $storage, 'quota' => 10000000));
}
public function tearDown() {
\OC_Helper::rmdirr($this->tmpDir);
}
protected function getLimitedStorage($limit) {
$storage = new \OC\Files\Storage\Local(array('datadir' => $this->tmpDir));
$storage->getScanner()->scan('');
return new \OC\Files\Storage\Wrapper\Quota(array('storage' => $storage, 'quota' => $limit));
}
public function testFilePutContentsNotEnoughSpace() {
$instance = $this->getLimitedStorage(3);
$this->assertFalse($instance->file_put_contents('foo', 'foobar'));
}
public function testCopyNotEnoughSpace() {
$instance = $this->getLimitedStorage(9);
$this->assertEquals(6, $instance->file_put_contents('foo', 'foobar'));
$instance->getScanner()->scan('');
$this->assertFalse($instance->copy('foo', 'bar'));
}
public function testFreeSpace() {
$instance = $this->getLimitedStorage(9);
$this->assertEquals(9, $instance->free_space(''));
}
public function testFWriteNotEnoughSpace() {
$instance = $this->getLimitedStorage(9);
$stream = $instance->fopen('foo', 'w+');
$this->assertEquals(6, fwrite($stream, 'foobar'));
$this->assertEquals(3, fwrite($stream, 'qwerty'));
fclose($stream);
$this->assertEquals('foobarqwe', $instance->file_get_contents('foo'));
}
}

View File

@ -6,9 +6,9 @@
* See the COPYING-README file.
*/
namespace Test\Files\Storage;
namespace Test\Files\Storage\Wrapper;
class Wrapper extends Storage {
class Wrapper extends \Test\Files\Storage\Storage {
/**
* @var string tmpDir
*/

View File

@ -0,0 +1,78 @@
<?php
/**
* Copyright (c) 2013 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace Test\Files\Stream;
class Quota extends \PHPUnit_Framework_TestCase {
public function tearDown() {
\OC\Files\Stream\Quota::clear();
}
protected function getStream($mode, $limit) {
$source = fopen('php://temp', $mode);
return \OC\Files\Stream\Quota::wrap($source, $limit);
}
public function testWriteEnoughSpace() {
$stream = $this->getStream('w+', 100);
$this->assertEquals(6, fwrite($stream, 'foobar'));
rewind($stream);
$this->assertEquals('foobar', fread($stream, 100));
}
public function testWriteNotEnoughSpace() {
$stream = $this->getStream('w+', 3);
$this->assertEquals(3, fwrite($stream, 'foobar'));
rewind($stream);
$this->assertEquals('foo', fread($stream, 100));
}
public function testWriteNotEnoughSpaceSecondTime() {
$stream = $this->getStream('w+', 9);
$this->assertEquals(6, fwrite($stream, 'foobar'));
$this->assertEquals(3, fwrite($stream, 'qwerty'));
rewind($stream);
$this->assertEquals('foobarqwe', fread($stream, 100));
}
public function testWriteEnoughSpaceRewind() {
$stream = $this->getStream('w+', 6);
$this->assertEquals(6, fwrite($stream, 'foobar'));
rewind($stream);
$this->assertEquals(3, fwrite($stream, 'qwe'));
rewind($stream);
$this->assertEquals('qwebar', fread($stream, 100));
}
public function testWriteNotEnoughSpaceRead() {
$stream = $this->getStream('w+', 6);
$this->assertEquals(6, fwrite($stream, 'foobar'));
rewind($stream);
$this->assertEquals('foobar', fread($stream, 6));
$this->assertEquals(0, fwrite($stream, 'qwe'));
}
public function testWriteNotEnoughSpaceExistingStream() {
$source = fopen('php://temp', 'w+');
fwrite($source, 'foobar');
$stream = \OC\Files\Stream\Quota::wrap($source, 3);
$this->assertEquals(3, fwrite($stream, 'foobar'));
rewind($stream);
$this->assertEquals('foobarfoo', fread($stream, 100));
}
public function testWriteNotEnoughSpaceExistingStreamRewind() {
$source = fopen('php://temp', 'w+');
fwrite($source, 'foobar');
$stream = \OC\Files\Stream\Quota::wrap($source, 3);
rewind($stream);
$this->assertEquals(6, fwrite($stream, 'qwerty'));
rewind($stream);
$this->assertEquals('qwerty', fread($stream, 100));
}
}