Merge pull request #20118 from owncloud/chunked-upload-dav

Initial implementation of the new chunked upload
This commit is contained in:
Thomas Müller 2016-04-13 14:37:10 +02:00
commit 3c0a1d4241
14 changed files with 809 additions and 15 deletions

View File

@ -0,0 +1,76 @@
<?php
/**
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
require '../../../../3rdparty/autoload.php';
if ($argc !== 6) {
echo "Invalid number of arguments" . PHP_EOL;
exit;
}
/**
* @param \Sabre\DAV\Client $client
* @param $uploadUrl
* @return mixed
*/
function request($client, $method, $uploadUrl, $data = null, $headers = []) {
echo "$method $uploadUrl ... ";
$t0 = microtime(true);
$result = $client->request($method, $uploadUrl, $data, $headers);
$t1 = microtime(true);
echo $result['statusCode'] . " - " . ($t1 - $t0) . ' seconds' . PHP_EOL;
if (!in_array($result['statusCode'], [200, 201])) {
echo $result['body'] . PHP_EOL;
}
return $result;
}
$baseUri = $argv[1];
$userName = $argv[2];
$password = $argv[3];
$file = $argv[4];
$chunkSize = $argv[5] * 1024 * 1024;
$client = new \Sabre\DAV\Client([
'baseUri' => $baseUri,
'userName' => $userName,
'password' => $password
]);
$transfer = uniqid('transfer', true);
$uploadUrl = "$baseUri/uploads/$userName/$transfer";
request($client, 'MKCOL', $uploadUrl);
$size = filesize($file);
$stream = fopen($file, 'r');
$index = 0;
while(!feof($stream)) {
request($client, 'PUT', "$uploadUrl/$index", fread($stream, $chunkSize));
$index++;
}
$destination = pathinfo($file, PATHINFO_BASENAME);
//echo "Moving $uploadUrl/.file to it's final destination $baseUri/files/$userName/$destination" . PHP_EOL;
request($client, 'MOVE', "$uploadUrl/.file", null, [
'Destination' => "$baseUri/files/$userName/$destination"
]);

View File

@ -143,7 +143,7 @@ class File extends Node implements IFile {
// if content length is sent by client:
// double check if the file was fully received
// compare expected and actual size
if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] !== 'LOCK') {
if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
$expected = $_SERVER['CONTENT_LENGTH'];
if ($count != $expected) {
throw new BadRequest('expected filesize ' . $expected . ' got ' . $count);

View File

@ -28,17 +28,19 @@
namespace OCA\DAV\Connector\Sabre;
use OC\Files\View;
use OCA\DAV\Upload\FutureFile;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IFile;
use \Sabre\DAV\PropFind;
use \Sabre\DAV\PropPatch;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Tree;
use \Sabre\HTTP\RequestInterface;
use \Sabre\HTTP\ResponseInterface;
use OCP\Files\StorageNotAvailableException;
class FilesPlugin extends \Sabre\DAV\ServerPlugin {
class FilesPlugin extends ServerPlugin {
// namespace
const NS_OWNCLOUD = 'http://owncloud.org/ns';
@ -146,11 +148,17 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin {
/**
* Plugin that checks if a move can actually be performed.
*
* @param string $source source path
* @param string $destination destination path
* @throws Forbidden
* @throws NotFound
*/
function checkMove($source, $destination) {
$sourceNode = $this->tree->getNodeForPath($source);
if ($sourceNode instanceof FutureFile) {
return;
}
list($sourceDir,) = \Sabre\HTTP\URLUtil::splitPath($source);
list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination);

View File

@ -89,6 +89,9 @@ class RootCollection extends SimpleCollection {
$systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, 'principals/system');
$systemAddressBookRoot->disableListing = $disableListing;
$uploadCollection = new Upload\RootCollection($userPrincipalBackend, 'principals/users');
$uploadCollection->disableListing = $disableListing;
$children = [
new SimpleCollection('principals', [
$userPrincipals,
@ -102,6 +105,7 @@ class RootCollection extends SimpleCollection {
$systemTagCollection,
$systemTagRelationsCollection,
$commentsCollection,
$uploadCollection,
];
parent::__construct('root', $children);

View File

@ -0,0 +1,234 @@
<?php
namespace OCA\DAV\Upload;
use Sabre\DAV\IFile;
/**
* Class AssemblyStream
*
* The assembly stream is a virtual stream that wraps multiple chunks.
* Reading from the stream transparently accessed the underlying chunks and
* give a representation as if they were already merged together.
*
* @package OCA\DAV\Upload
*/
class AssemblyStream implements \Icewind\Streams\File {
/** @var resource */
private $context;
/** @var IFile[] */
private $nodes;
/** @var int */
private $pos = 0;
/** @var array */
private $sortedNodes;
/** @var int */
private $size;
/**
* @param string $path
* @param string $mode
* @param int $options
* @param string &$opened_path
* @return bool
*/
public function stream_open($path, $mode, $options, &$opened_path) {
$this->loadContext('assembly');
// sort the nodes
$nodes = $this->nodes;
// http://stackoverflow.com/a/10985500
@usort($nodes, function(IFile $a, IFile $b) {
return strcmp($a->getName(), $b->getName());
});
$this->nodes = $nodes;
// build additional information
$this->sortedNodes = [];
$start = 0;
foreach($this->nodes as $node) {
$size = $node->getSize();
$name = $node->getName();
$this->sortedNodes[$name] = ['node' => $node, 'start' => $start, 'end' => $start + $size];
$start += $size;
$this->size = $start;
}
return true;
}
/**
* @param string $offset
* @param int $whence
* @return bool
*/
public function stream_seek($offset, $whence = SEEK_SET) {
return false;
}
/**
* @return int
*/
public function stream_tell() {
return $this->pos;
}
/**
* @param int $count
* @return string
*/
public function stream_read($count) {
list($node, $posInNode) = $this->getNodeForPosition($this->pos);
if (is_null($node)) {
return null;
}
$stream = $this->getStream($node);
fseek($stream, $posInNode);
$data = fread($stream, $count);
$read = strlen($data);
// update position
$this->pos += $read;
return $data;
}
/**
* @param string $data
* @return int
*/
public function stream_write($data) {
return false;
}
/**
* @param int $option
* @param int $arg1
* @param int $arg2
* @return bool
*/
public function stream_set_option($option, $arg1, $arg2) {
return false;
}
/**
* @param int $size
* @return bool
*/
public function stream_truncate($size) {
return false;
}
/**
* @return array
*/
public function stream_stat() {
return [];
}
/**
* @param int $operation
* @return bool
*/
public function stream_lock($operation) {
return false;
}
/**
* @return bool
*/
public function stream_flush() {
return false;
}
/**
* @return bool
*/
public function stream_eof() {
return $this->pos >= $this->size;
}
/**
* @return bool
*/
public function stream_close() {
return true;
}
/**
* Load the source from the stream context and return the context options
*
* @param string $name
* @return array
* @throws \Exception
*/
protected function loadContext($name) {
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
if (isset($context['nodes']) and is_array($context['nodes'])) {
$this->nodes = $context['nodes'];
} else {
throw new \BadMethodCallException('Invalid context, nodes not set');
}
return $context;
}
/**
* @param IFile[] $nodes
* @return resource
*
* @throws \BadMethodCallException
*/
public static function wrap(array $nodes) {
$context = stream_context_create([
'assembly' => [
'nodes' => $nodes]
]);
stream_wrapper_register('assembly', '\OCA\DAV\Upload\AssemblyStream');
try {
$wrapped = fopen('assembly://', 'r', null, $context);
} catch (\BadMethodCallException $e) {
stream_wrapper_unregister('assembly');
throw $e;
}
stream_wrapper_unregister('assembly');
return $wrapped;
}
/**
* @param $pos
* @return IFile | null
*/
private function getNodeForPosition($pos) {
foreach($this->sortedNodes as $node) {
if ($pos >= $node['start'] && $pos < $node['end']) {
return [$node['node'], $pos - $node['start']];
}
}
return null;
}
/**
* @param IFile $node
* @return resource
*/
private function getStream(IFile $node) {
$data = $node->get();
if (is_resource($data)) {
return $data;
}
return fopen('data://text/plain,' . $data,'r');
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace OCA\DAV\Upload;
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Upload\AssemblyStream;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\IFile;
/**
* Class FutureFile
*
* The FutureFile is a SabreDav IFile which connects the chunked upload directory
* with the AssemblyStream, who does the final assembly job
*
* @package OCA\DAV\Upload
*/
class FutureFile implements \Sabre\DAV\IFile {
/** @var Directory */
private $root;
/** @var string */
private $name;
/**
* @param Directory $root
* @param string $name
*/
function __construct(Directory $root, $name) {
$this->root = $root;
$this->name = $name;
}
/**
* @inheritdoc
*/
function put($data) {
throw new Forbidden('Permission denied to put into this file');
}
/**
* @inheritdoc
*/
function get() {
$nodes = $this->root->getChildren();
return AssemblyStream::wrap($nodes);
}
/**
* @inheritdoc
*/
function getContentType() {
return 'application/octet-stream';
}
/**
* @inheritdoc
*/
function getETag() {
return $this->root->getETag();
}
/**
* @inheritdoc
*/
function getSize() {
$children = $this->root->getChildren();
$sizes = array_map(function($node) {
/** @var IFile $node */
return $node->getSize();
}, $children);
return array_sum($sizes);
}
/**
* @inheritdoc
*/
function delete() {
$this->root->delete();
}
/**
* @inheritdoc
*/
function getName() {
return $this->name;
}
/**
* @inheritdoc
*/
function setName($name) {
throw new Forbidden('Permission denied to rename this file');
}
/**
* @inheritdoc
*/
function getLastModified() {
return $this->root->getLastModified();
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace OCA\DAV\Upload;
use Sabre\DAVACL\AbstractPrincipalCollection;
class RootCollection extends AbstractPrincipalCollection {
/**
* @inheritdoc
*/
function getChildForPrincipal(array $principalInfo) {
return new UploadHome($principalInfo);
}
/**
* @inheritdoc
*/
function getName() {
return 'uploads';
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace OCA\DAV\Upload;
use OCA\DAV\Connector\Sabre\Directory;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\ICollection;
class UploadFolder implements ICollection {
private $node;
function __construct(Directory $node) {
$this->node = $node;
}
function createFile($name, $data = null) {
// TODO: verify name - should be a simple number
$this->node->createFile($name, $data);
}
function createDirectory($name) {
throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
}
function getChild($name) {
if ($name === '.file') {
return new FutureFile($this->node, '.file');
}
return $this->node->getChild($name);
}
function getChildren() {
$children = $this->node->getChildren();
$children[] = new FutureFile($this->node, '.file');
return $children;
}
function childExists($name) {
if ($name === '.file') {
return true;
}
return $this->node->childExists($name);
}
function delete() {
$this->node->delete();
}
function getName() {
return $this->node->getName();
}
function setName($name) {
throw new Forbidden('Permission denied to rename this folder');
}
function getLastModified() {
return $this->node->getLastModified();
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace OCA\DAV\Upload;
use OC\Files\Filesystem;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Directory;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\ICollection;
class UploadHome implements ICollection {
/**
* FilesHome constructor.
*
* @param array $principalInfo
*/
public function __construct($principalInfo) {
$this->principalInfo = $principalInfo;
}
function createFile($name, $data = null) {
throw new Forbidden('Permission denied to create file (filename ' . $name . ')');
}
function createDirectory($name) {
$this->impl()->createDirectory($name);
}
function getChild($name) {
return new UploadFolder($this->impl()->getChild($name));
}
function getChildren() {
return array_map(function($node) {
return new UploadFolder($node);
}, $this->impl()->getChildren());
}
function childExists($name) {
return !is_null($this->getChild($name));
}
function delete() {
$this->impl()->delete();
}
function getName() {
return 'uploads';
}
function setName($name) {
throw new Forbidden('Permission denied to rename this folder');
}
function getLastModified() {
return $this->impl()->getLastModified();
}
/**
* @return Directory
*/
private function impl() {
$rootView = new View();
$user = \OC::$server->getUserSession()->getUser();
Filesystem::initMountPoints($user->getUID());
if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) {
$rootView->mkdir('/' . $user->getUID() . '/uploads');
}
$view = new View('/' . $user->getUID() . '/uploads');
$rootInfo = $view->getFileInfo('');
$impl = new Directory($view, $rootInfo);
return $impl;
}
}

View File

@ -23,6 +23,9 @@
namespace OCA\DAV\Tests\Unit\Connector\Sabre;
use OCP\Files\StorageNotAvailableException;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
use Test\TestCase;
/**
* Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com>
@ -30,7 +33,7 @@ use OCP\Files\StorageNotAvailableException;
* later.
* See the COPYING-README file.
*/
class FilesPlugin extends \Test\TestCase {
class FilesPlugin extends TestCase {
const GETETAG_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::GETETAG_PROPERTYNAME;
const FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::FILEID_PROPERTYNAME;
const INTERNAL_FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::INTERNAL_FILEID_PROPERTYNAME;
@ -42,12 +45,12 @@ class FilesPlugin extends \Test\TestCase {
const OWNER_DISPLAY_NAME_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME;
/**
* @var \Sabre\DAV\Server
* @var \Sabre\DAV\Server | \PHPUnit_Framework_MockObject_MockObject
*/
private $server;
/**
* @var \Sabre\DAV\Tree
* @var \Sabre\DAV\Tree | \PHPUnit_Framework_MockObject_MockObject
*/
private $tree;
@ -57,7 +60,7 @@ class FilesPlugin extends \Test\TestCase {
private $plugin;
/**
* @var \OC\Files\View
* @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject
*/
private $view;
@ -79,6 +82,7 @@ class FilesPlugin extends \Test\TestCase {
/**
* @param string $class
* @return \PHPUnit_Framework_MockObject_MockObject
*/
private function createTestNode($class) {
$node = $this->getMockBuilder($class)
@ -111,9 +115,10 @@ class FilesPlugin extends \Test\TestCase {
}
public function testGetPropertiesForFile() {
/** @var \OCA\DAV\Connector\Sabre\File | \PHPUnit_Framework_MockObject_MockObject $node */
$node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
$propFind = new \Sabre\DAV\PropFind(
$propFind = new PropFind(
'/dummyPath',
array(
self::GETETAG_PROPERTYNAME,
@ -165,11 +170,12 @@ class FilesPlugin extends \Test\TestCase {
}
public function testGetPropertiesForFileHome() {
/** @var \OCA\DAV\Files\FilesHome | \PHPUnit_Framework_MockObject_MockObject $node */
$node = $this->getMockBuilder('\OCA\DAV\Files\FilesHome')
->disableOriginalConstructor()
->getMock();
$propFind = new \Sabre\DAV\PropFind(
$propFind = new PropFind(
'/dummyPath',
array(
self::GETETAG_PROPERTYNAME,
@ -214,9 +220,10 @@ class FilesPlugin extends \Test\TestCase {
}
public function testGetPropertiesStorageNotAvailable() {
/** @var \OCA\DAV\Connector\Sabre\File | \PHPUnit_Framework_MockObject_MockObject $node */
$node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
$propFind = new \Sabre\DAV\PropFind(
$propFind = new PropFind(
'/dummyPath',
array(
self::DOWNLOADURL_PROPERTYNAME,
@ -240,7 +247,7 @@ class FilesPlugin extends \Test\TestCase {
$this->plugin = new \OCA\DAV\Connector\Sabre\FilesPlugin($this->tree, $this->view, true);
$this->plugin->initialize($this->server);
$propFind = new \Sabre\DAV\PropFind(
$propFind = new PropFind(
'/dummyPath',
[
self::PERMISSIONS_PROPERTYNAME,
@ -248,6 +255,7 @@ class FilesPlugin extends \Test\TestCase {
0
);
/** @var \OCA\DAV\Connector\Sabre\File | \PHPUnit_Framework_MockObject_MockObject $node */
$node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
$node->expects($this->any())
->method('getDavPermissions')
@ -262,9 +270,10 @@ class FilesPlugin extends \Test\TestCase {
}
public function testGetPropertiesForDirectory() {
/** @var \OCA\DAV\Connector\Sabre\Directory | \PHPUnit_Framework_MockObject_MockObject $node */
$node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory');
$propFind = new \Sabre\DAV\PropFind(
$propFind = new PropFind(
'/dummyPath',
array(
self::GETETAG_PROPERTYNAME,
@ -308,7 +317,7 @@ class FilesPlugin extends \Test\TestCase {
->will($this->returnValue(true));
// properties to set
$propPatch = new \Sabre\DAV\PropPatch(array(
$propPatch = new PropPatch(array(
self::GETETAG_PROPERTYNAME => 'newetag',
self::LASTMODIFIED_PROPERTYNAME => $testDate
));
@ -328,9 +337,7 @@ class FilesPlugin extends \Test\TestCase {
}
public function testUpdatePropsForbidden() {
$node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File');
$propPatch = new \Sabre\DAV\PropPatch(array(
$propPatch = new PropPatch(array(
self::OWNER_ID_PROPERTYNAME => 'user2',
self::OWNER_DISPLAY_NAME_PROPERTYNAME => 'User Two',
self::FILEID_PROPERTYNAME => 12345,

View File

@ -0,0 +1,47 @@
<?php
class AssemblyStreamTest extends \PHPUnit_Framework_TestCase {
/**
* @dataProvider providesNodes()
*/
public function testGetContents($expected, $nodes) {
$stream = \OCA\DAV\Upload\AssemblyStream::wrap($nodes);
$content = stream_get_contents($stream);
$this->assertEquals($expected, $content);
}
function providesNodes() {
return[
'one node only' => ['1234567890', [
$this->buildNode('0', '1234567890')
]],
'two nodes' => ['1234567890', [
$this->buildNode('1', '67890'),
$this->buildNode('0', '12345')
]]
];
}
private function buildNode($name, $data) {
$node = $this->getMockBuilder('\Sabre\DAV\File')
->setMethods(['getName', 'get', 'getSize'])
->getMockForAbstractClass();
$node->expects($this->any())
->method('getName')
->willReturn($name);
$node->expects($this->any())
->method('get')
->willReturn($data);
$node->expects($this->any())
->method('getSize')
->willReturn(strlen($data));
return $node;
}
}

View File

@ -0,0 +1,89 @@
<?php
class FutureFileTest extends \PHPUnit_Framework_TestCase {
public function testGetContentType() {
$f = $this->mockFutureFile();
$this->assertEquals('application/octet-stream', $f->getContentType());
}
public function testGetETag() {
$f = $this->mockFutureFile();
$this->assertEquals('1234567890', $f->getETag());
}
public function testGetName() {
$f = $this->mockFutureFile();
$this->assertEquals('foo.txt', $f->getName());
}
public function testGetLastModified() {
$f = $this->mockFutureFile();
$this->assertEquals(12121212, $f->getLastModified());
}
public function testGetSize() {
$f = $this->mockFutureFile();
$this->assertEquals(0, $f->getSize());
}
public function testGet() {
$f = $this->mockFutureFile();
$stream = $f->get();
$this->assertTrue(is_resource($stream));
}
public function testDelete() {
$d = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Directory')
->disableOriginalConstructor()
->setMethods(['delete'])
->getMock();
$d->expects($this->once())
->method('delete');
$f = new \OCA\DAV\Upload\FutureFile($d, 'foo.txt');
$f->delete();
}
/**
* @expectedException Sabre\DAV\Exception\Forbidden
*/
public function testPut() {
$f = $this->mockFutureFile();
$f->put('');
}
/**
* @expectedException Sabre\DAV\Exception\Forbidden
*/
public function testSetName() {
$f = $this->mockFutureFile();
$f->setName('');
}
/**
* @return \OCA\DAV\Upload\FutureFile
*/
private function mockFutureFile() {
$d = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Directory')
->disableOriginalConstructor()
->setMethods(['getETag', 'getLastModified', 'getChildren'])
->getMock();
$d->expects($this->any())
->method('getETag')
->willReturn('1234567890');
$d->expects($this->any())
->method('getLastModified')
->willReturn(12121212);
$d->expects($this->any())
->method('getChildren')
->willReturn([]);
return new \OCA\DAV\Upload\FutureFile($d, 'foo.txt');
}
}

View File

@ -373,5 +373,37 @@ trait WebDav {
$this->makeDavRequest($user, 'PUT', $file, ['OC-Chunked' => '1'], $data);
}
/**
* @Given user :user creates a new chunking upload with id :id
*/
public function userCreatesANewChunkingUploadWithId($user, $id)
{
$destination = '/uploads/'.$user.'/'.$id;
$this->makeDavRequest($user, 'MKCOL', $destination, []);
}
/**
* @Given user :user uploads new chunk file :num with :data to id :id
*/
public function userUploadsNewChunkFileOfWithToId($user, $num, $data, $id)
{
$data = \GuzzleHttp\Stream\Stream::factory($data);
$destination = '/uploads/'.$user.'/'.$id.'/'.$num;
$this->makeDavRequest($user, 'PUT', $destination, [], $data);
}
/**
* @Given user :user moves new chunk file with id :id to :dest
*/
public function userMovesNewChunkFileWithIdToMychunkedfile($user, $id, $dest)
{
$source = '/uploads/'.$user.'/'.$id.'/.file';
$destination = substr($this->baseUrl, 0, -4) . $this->davPath . '/files/'.$user.$dest;
$this->makeDavRequest($user, 'MOVE', $source, [
'Destination' => $destination
]);
}
}

View File

@ -241,3 +241,39 @@ Feature: webdav-related
| 0 |
| 1 |
| 3 |
Scenario: Upload chunked file asc with new chunking
Given using dav path "remote.php/dav"
And user "user0" exists
And user "user0" creates a new chunking upload with id "chunking-42"
And user "user0" uploads new chunk file "1" with "AAAAA" to id "chunking-42"
And user "user0" uploads new chunk file "2" with "BBBBB" to id "chunking-42"
And user "user0" uploads new chunk file "3" with "CCCCC" to id "chunking-42"
And user "user0" moves new chunk file with id "chunking-42" to "/myChunkedFile.txt"
When As an "user0"
And Downloading file "/files/user0/myChunkedFile.txt"
Then Downloaded content should be "AAAAABBBBBCCCCC"
Scenario: Upload chunked file desc with new chunking
Given using dav path "remote.php/dav"
And user "user0" exists
And user "user0" creates a new chunking upload with id "chunking-42"
And user "user0" uploads new chunk file "3" with "CCCCC" to id "chunking-42"
And user "user0" uploads new chunk file "2" with "BBBBB" to id "chunking-42"
And user "user0" uploads new chunk file "1" with "AAAAA" to id "chunking-42"
And user "user0" moves new chunk file with id "chunking-42" to "/myChunkedFile.txt"
When As an "user0"
And Downloading file "/files/user0/myChunkedFile.txt"
Then Downloaded content should be "AAAAABBBBBCCCCC"
Scenario: Upload chunked file random with new chunking
Given using dav path "remote.php/dav"
And user "user0" exists
And user "user0" creates a new chunking upload with id "chunking-42"
And user "user0" uploads new chunk file "2" with "BBBBB" to id "chunking-42"
And user "user0" uploads new chunk file "3" with "CCCCC" to id "chunking-42"
And user "user0" uploads new chunk file "1" with "AAAAA" to id "chunking-42"
And user "user0" moves new chunk file with id "chunking-42" to "/myChunkedFile.txt"
When As an "user0"
And Downloading file "/files/user0/myChunkedFile.txt"
Then Downloaded content should be "AAAAABBBBBCCCCC"