diff options
author | Lukas Reschke <lukas@statuscode.ch> | 2016-10-25 10:31:03 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-10-25 10:31:03 +0200 |
commit | 79706e0ddc6ab970d5709e89b8d0caec4d34662b (patch) | |
tree | 168f9bc806e7eed287bce63e7f6d277eb5adb956 /apps | |
parent | 5926da3dd6535e0eea7fe7871d2347f8b33bb337 (diff) | |
parent | c8a13f644ebbc5840d0e632cf86e5ae46856f7f0 (diff) | |
download | nextcloud-server-79706e0ddc6ab970d5709e89b8d0caec4d34662b.tar.gz nextcloud-server-79706e0ddc6ab970d5709e89b8d0caec4d34662b.zip |
Merge pull request #1283 from nextcloud/us_files-ui-webdav-upload
Use Webdav PUT for uploads
Diffstat (limited to 'apps')
-rw-r--r-- | apps/dav/appinfo/v1/publicwebdav.php | 17 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/FilesPlugin.php | 51 | ||||
-rw-r--r-- | apps/dav/lib/Files/Sharing/FilesDropPlugin.php | 94 | ||||
-rw-r--r-- | apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php | 85 | ||||
-rw-r--r-- | apps/files/ajax/upload.php | 283 | ||||
-rw-r--r-- | apps/files/appinfo/routes.php | 12 | ||||
-rw-r--r-- | apps/files/js/app.js | 1 | ||||
-rw-r--r-- | apps/files/js/file-upload.js | 901 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 338 | ||||
-rw-r--r-- | apps/files/js/files.js | 11 | ||||
-rw-r--r-- | apps/files/templates/list.php | 3 | ||||
-rw-r--r-- | apps/files/tests/js/fileUploadSpec.js | 141 | ||||
-rw-r--r-- | apps/files/tests/js/filelistSpec.js | 242 | ||||
-rw-r--r-- | apps/files_sharing/js/files_drop.js | 64 | ||||
-rw-r--r-- | apps/files_sharing/js/public.js | 43 | ||||
-rw-r--r-- | apps/files_sharing/tests/js/publicAppSpec.js | 23 |
16 files changed, 1437 insertions, 872 deletions
diff --git a/apps/dav/appinfo/v1/publicwebdav.php b/apps/dav/appinfo/v1/publicwebdav.php index b88c5847ab5..670eadd5ea9 100644 --- a/apps/dav/appinfo/v1/publicwebdav.php +++ b/apps/dav/appinfo/v1/publicwebdav.php @@ -57,8 +57,9 @@ $serverFactory = new OCA\DAV\Connector\Sabre\ServerFactory( $requestUri = \OC::$server->getRequest()->getRequestUri(); $linkCheckPlugin = new \OCA\DAV\Files\Sharing\PublicLinkCheckPlugin(); +$filesDropPlugin = new \OCA\DAV\Files\Sharing\FilesDropPlugin(); -$server = $serverFactory->createServer($baseuri, $requestUri, $authBackend, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin) { +$server = $serverFactory->createServer($baseuri, $requestUri, $authBackend, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) { $isAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest'); $federatedSharingApp = new \OCA\FederatedFileSharing\AppInfo\Application(); $federatedShareProvider = $federatedSharingApp->getFederatedShareProvider(); @@ -72,9 +73,10 @@ $server = $serverFactory->createServer($baseuri, $requestUri, $authBackend, func $isReadable = $share->getPermissions() & \OCP\Constants::PERMISSION_READ; $fileId = $share->getNodeId(); + /* if (!$isReadable) { return false; - } + }*/ \OC\Files\Filesystem::addStorageWrapper('sharePermissions', function ($mountPoint, $storage) use ($share) { return new \OC\Files\Storage\Wrapper\PermissionsMask(array('storage' => $storage, 'mask' => $share->getPermissions() | \OCP\Constants::PERMISSION_SHARE)); @@ -86,10 +88,19 @@ $server = $serverFactory->createServer($baseuri, $requestUri, $authBackend, func $fileInfo = $ownerView->getFileInfo($path); $linkCheckPlugin->setFileInfo($fileInfo); - return new \OC\Files\View($ownerView->getAbsolutePath($path)); + // If not readble (files_drop) enable the filesdrop plugin + if (!$isReadable) { + $filesDropPlugin->enable(); + } + + $view = new \OC\Files\View($ownerView->getAbsolutePath($path)); + $filesDropPlugin->setView($view); + + return $view; }); $server->addPlugin($linkCheckPlugin); +$server->addPlugin($filesDropPlugin); // And off we go! $server->exec(); diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index aa5bacea5bb..39d15e0c6e9 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -46,6 +46,8 @@ use \Sabre\HTTP\ResponseInterface; use OCP\Files\StorageNotAvailableException; use OCP\IConfig; use OCP\IRequest; +use Sabre\DAV\Exception\BadRequest; +use OCA\DAV\Connector\Sabre\Directory; class FilesPlugin extends ServerPlugin { @@ -170,6 +172,8 @@ class FilesPlugin extends ServerPlugin { $this->server = $server; $this->server->on('propFind', array($this, 'handleGetProperties')); $this->server->on('propPatch', array($this, 'handleUpdateProperties')); + // RFC5995 to add file to the collection with a suggested name + $this->server->on('method:POST', [$this, 'httpPost']); $this->server->on('afterBind', array($this, 'sendFileIdHeader')); $this->server->on('afterWriteContent', array($this, 'sendFileIdHeader')); $this->server->on('afterMethod:GET', [$this,'httpGet']); @@ -432,4 +436,51 @@ class FilesPlugin extends ServerPlugin { } } + /** + * POST operation on directories to create a new file + * with suggested name + * + * @param RequestInterface $request request object + * @param ResponseInterface $response response object + * @return null|false + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) { + // TODO: move this to another plugin ? + if (!\OC::$CLI && !\OC::$server->getRequest()->passesCSRFCheck()) { + throw new BadRequest('Invalid CSRF token'); + } + + list($parentPath, $name) = \Sabre\HTTP\URLUtil::splitPath($request->getPath()); + + // Making sure the parent node exists and is a directory + $node = $this->tree->getNodeForPath($parentPath); + + if ($node instanceof Directory) { + // no Add-Member found + if (empty($name) || $name[0] !== '&') { + // suggested name required + throw new BadRequest('Missing suggested file name'); + } + + $name = substr($name, 1); + + if (empty($name)) { + // suggested name required + throw new BadRequest('Missing suggested file name'); + } + + // make sure the name is unique + $name = basename(\OC_Helper::buildNotExistingFileNameForView($parentPath, $name, $this->fileView)); + + $node->createFile($name, $request->getBodyAsStream()); + + list($parentUrl, ) = \Sabre\HTTP\URLUtil::splitPath($request->getUrl()); + + $response->setHeader('Content-Location', $parentUrl . '/' . rawurlencode($name)); + + // created + $response->setStatus(201); + return false; + } + } } diff --git a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php new file mode 100644 index 00000000000..ccfa452cb68 --- /dev/null +++ b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php @@ -0,0 +1,94 @@ +<?php +/** + * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\DAV\Files\Sharing; + +use OC\Files\View; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * Make sure that the destination is writable + */ +class FilesDropPlugin extends ServerPlugin { + + /** @var View */ + private $view; + + /** @var bool */ + private $enabled = false; + + /** + * @param View $view + */ + public function setView($view) { + $this->view = $view; + } + + public function enable() { + $this->enabled = true; + } + + + /** + * This initializes the plugin. + * + * @param \Sabre\DAV\Server $server Sabre server + * + * @return void + */ + public function initialize(\Sabre\DAV\Server $server) { + $server->on('beforeMethod:PUT', [$this, 'beforeMethod']); + $this->enabled = false; + } + + public function beforeMethod(RequestInterface $request, ResponseInterface $response){ + + if (!$this->enabled) { + return; + } + + $path = $request->getPath(); + + if ($this->view->file_exists($path)) { + $newName = \OC_Helper::buildNotExistingFileNameForView('/', $path, $this->view); + + $url = $request->getBaseUrl() . $newName . '?'; + $parms = $request->getQueryParameters(); + $first = true; + foreach ($parms as $k => $v) { + if ($first) { + $url .= '?'; + $first = false; + } else { + $url .= '&'; + } + $url .= $k . '=' . $v; + } + + $request->setUrl($url); + } + + + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php index 282a5b2f626..43ca119abff 100644 --- a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php @@ -123,7 +123,7 @@ class FilesPluginTest extends TestCase { * @param string $class * @return \PHPUnit_Framework_MockObject_MockObject */ - private function createTestNode($class) { + private function createTestNode($class, $path = '/dummypath') { $node = $this->getMockBuilder($class) ->disableOriginalConstructor() ->getMock(); @@ -134,7 +134,7 @@ class FilesPluginTest extends TestCase { $this->tree->expects($this->any()) ->method('getNodeForPath') - ->with('/dummypath') + ->with($path) ->will($this->returnValue($node)); $node->expects($this->any()) @@ -547,4 +547,85 @@ class FilesPluginTest extends TestCase { $this->assertEquals("false", $propFind->get(self::HAS_PREVIEW_PROPERTYNAME)); } + + public function postCreateFileProvider() { + $baseUrl = 'http://example.com/owncloud/remote.php/webdav/subdir/'; + return [ + ['test.txt', 'some file.txt', 'some file.txt', $baseUrl . 'some%20file.txt'], + ['some file.txt', 'some file.txt', 'some file (2).txt', $baseUrl . 'some%20file%20%282%29.txt'], + ]; + } + + /** + * @dataProvider postCreateFileProvider + */ + public function testPostWithAddMember($existingFile, $wantedName, $deduplicatedName, $expectedLocation) { + $request = $this->getMock('Sabre\HTTP\RequestInterface'); + $response = $this->getMock('Sabre\HTTP\ResponseInterface'); + + $request->expects($this->any()) + ->method('getUrl') + ->will($this->returnValue('http://example.com/owncloud/remote.php/webdav/subdir/&' . $wantedName)); + + $request->expects($this->any()) + ->method('getPath') + ->will($this->returnValue('/subdir/&' . $wantedName)); + + $request->expects($this->once()) + ->method('getBodyAsStream') + ->will($this->returnValue(fopen('data://text/plain,hello', 'r'))); + + $this->view->expects($this->any()) + ->method('file_exists') + ->will($this->returnCallback(function($path) use ($existingFile) { + return ($path === '/subdir/' . $existingFile); + })); + + $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory', '/subdir'); + + $node->expects($this->once()) + ->method('createFile') + ->with($deduplicatedName, $this->isType('resource')); + + $response->expects($this->once()) + ->method('setStatus') + ->with(201); + $response->expects($this->once()) + ->method('setHeader') + ->with('Content-Location', $expectedLocation); + + $this->assertFalse($this->plugin->httpPost($request, $response)); + } + + public function testPostOnNonDirectory() { + $request = $this->getMock('Sabre\HTTP\RequestInterface'); + $response = $this->getMock('Sabre\HTTP\ResponseInterface'); + + $request->expects($this->any()) + ->method('getPath') + ->will($this->returnValue('/subdir/test.txt/&abc')); + + $this->createTestNode('\OCA\DAV\Connector\Sabre\File', '/subdir/test.txt'); + + $this->assertNull($this->plugin->httpPost($request, $response)); + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + */ + public function testPostWithoutAddMember() { + $request = $this->getMock('Sabre\HTTP\RequestInterface'); + $response = $this->getMock('Sabre\HTTP\ResponseInterface'); + + $request->expects($this->any()) + ->method('getPath') + ->will($this->returnValue('/subdir/&')); + + $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory', '/subdir'); + + $node->expects($this->never()) + ->method('createFile'); + + $this->plugin->httpPost($request, $response); + } } diff --git a/apps/files/ajax/upload.php b/apps/files/ajax/upload.php deleted file mode 100644 index 9de5c0bce39..00000000000 --- a/apps/files/ajax/upload.php +++ /dev/null @@ -1,283 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Clark Tomlinson <fallen013@gmail.com> - * @author Florian Pritz <bluewind@xinu.at> - * @author Frank Karlitschek <frank@karlitschek.de> - * @author Individual IT Services <info@individual-it.net> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Luke Policinski <lpolicinski@gmail.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Roman Geber <rgeber@owncloudapps.com> - * @author TheSFReader <TheSFReader@gmail.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @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/> - * - */ -\OC::$server->getSession()->close(); - -// Firefox and Konqueror tries to download application/json for me. --Arthur -OCP\JSON::setContentTypeHeader('text/plain'); - -// If a directory token is sent along check if public upload is permitted. -// If not, check the login. -// If no token is sent along, rely on login only - -$errorCode = null; -$errorFileName = null; - -$l = \OC::$server->getL10N('files'); -if (empty($_POST['dirToken'])) { - // The standard case, files are uploaded through logged in users :) - OCP\JSON::checkLoggedIn(); - $dir = isset($_POST['dir']) ? (string)$_POST['dir'] : ''; - if (!$dir || empty($dir) || $dir === false) { - OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('Unable to set upload directory.'))))); - die(); - } -} else { - // TODO: ideally this code should be in files_sharing/ajax/upload.php - // and the upload/file transfer code needs to be refactored into a utility method - // that could be used there - - \OC_User::setIncognitoMode(true); - - $publicDirectory = !empty($_POST['subdir']) ? (string)$_POST['subdir'] : '/'; - - $linkItem = OCP\Share::getShareByToken((string)$_POST['dirToken']); - if ($linkItem === false) { - OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('Invalid Token'))))); - die(); - } - - if (!($linkItem['permissions'] & \OCP\Constants::PERMISSION_CREATE)) { - OCP\JSON::checkLoggedIn(); - } else { - // resolve reshares - $rootLinkItem = OCP\Share::resolveReShare($linkItem); - - OCP\JSON::checkUserExists($rootLinkItem['uid_owner']); - // Setup FS with owner - OC_Util::tearDownFS(); - OC_Util::setupFS($rootLinkItem['uid_owner']); - - // The token defines the target directory (security reasons) - $path = \OC\Files\Filesystem::getPath($linkItem['file_source']); - if($path === null) { - OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('Unable to set upload directory.'))))); - die(); - } - $dir = sprintf( - "/%s/%s", - $path, - $publicDirectory - ); - - if (!$dir || empty($dir) || $dir === false) { - OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('Unable to set upload directory.'))))); - die(); - } - - $dir = rtrim($dir, '/'); - } -} - -OCP\JSON::callCheck(); - -// get array with current storage stats (e.g. max file size) -$storageStats = \OCA\Files\Helper::buildFileStorageStatistics($dir); - -if (!isset($_FILES['files'])) { - OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('No file was uploaded. Unknown error')), $storageStats))); - exit(); -} - -foreach ($_FILES['files']['error'] as $error) { - if ($error != 0) { - $errors = array( - UPLOAD_ERR_OK => $l->t('There is no error, the file uploaded with success'), - UPLOAD_ERR_INI_SIZE => $l->t('The uploaded file exceeds the upload_max_filesize directive in php.ini: ') - . OC::$server->getIniWrapper()->getNumeric('upload_max_filesize'), - UPLOAD_ERR_FORM_SIZE => $l->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'), - UPLOAD_ERR_PARTIAL => $l->t('The uploaded file was only partially uploaded'), - UPLOAD_ERR_NO_FILE => $l->t('No file was uploaded'), - UPLOAD_ERR_NO_TMP_DIR => $l->t('Missing a temporary folder'), - UPLOAD_ERR_CANT_WRITE => $l->t('Failed to write to disk'), - ); - $errorMessage = $errors[$error]; - \OC::$server->getLogger()->alert("Upload error: $error - $errorMessage", array('app' => 'files')); - OCP\JSON::error(array('data' => array_merge(array('message' => $errorMessage), $storageStats))); - exit(); - } -} -$files = $_FILES['files']; - -$error = false; - -$maxUploadFileSize = $storageStats['uploadMaxFilesize']; -$maxHumanFileSize = OCP\Util::humanFileSize($maxUploadFileSize); - -$totalSize = 0; -$isReceivedShare = \OC::$server->getRequest()->getParam('isReceivedShare', false) === 'true'; -// defer quota check for received shares -if (!$isReceivedShare && $storageStats['freeSpace'] >= 0) { - 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'), - 'uploadMaxFilesize' => $maxUploadFileSize, - 'maxHumanFilesize' => $maxHumanFileSize))); - exit(); -} - -$result = array(); -if (\OC\Files\Filesystem::isValidPath($dir) === true) { - $fileCount = count($files['name']); - for ($i = 0; $i < $fileCount; $i++) { - - if (isset($_POST['resolution'])) { - $resolution = $_POST['resolution']; - } else { - $resolution = null; - } - - if(isset($_POST['dirToken'])) { - // If it is a read only share the resolution will always be autorename - $shareManager = \OC::$server->getShareManager(); - $share = $shareManager->getShareByToken((string)$_POST['dirToken']); - if (!($share->getPermissions() & \OCP\Constants::PERMISSION_READ)) { - $resolution = 'autorename'; - } - } - - // target directory for when uploading folders - $relativePath = ''; - if(!empty($_POST['file_directory'])) { - $relativePath = '/'.$_POST['file_directory']; - } - - // $path needs to be normalized - this failed within drag'n'drop upload to a sub-folder - if ($resolution === 'autorename') { - // append a number in brackets like 'filename (2).ext' - $target = OCP\Files::buildNotExistingFileName($dir . $relativePath, $files['name'][$i]); - } else { - $target = \OC\Files\Filesystem::normalizePath($dir . $relativePath.'/'.$files['name'][$i]); - } - - // relative dir to return to the client - if (isset($publicDirectory)) { - // path relative to the public root - $returnedDir = $publicDirectory . $relativePath; - } else { - // full path - $returnedDir = $dir . $relativePath; - } - $returnedDir = \OC\Files\Filesystem::normalizePath($returnedDir); - - - $exists = \OC\Files\Filesystem::file_exists($target); - if ($exists) { - $updatable = \OC\Files\Filesystem::isUpdatable($target); - } - if ( ! $exists || ($updatable && $resolution === 'replace' ) ) { - // upload and overwrite file - try - { - if (is_uploaded_file($files['tmp_name'][$i]) and \OC\Files\Filesystem::fromTmpFile($files['tmp_name'][$i], $target)) { - - // updated max file size after upload - $storageStats = \OCA\Files\Helper::buildFileStorageStatistics($dir); - - $meta = \OC\Files\Filesystem::getFileInfo($target); - if ($meta === false) { - $error = $l->t('The target folder has been moved or deleted.'); - $errorCode = 'targetnotfound'; - } else { - $data = \OCA\Files\Helper::formatFileInfo($meta); - $data['status'] = 'success'; - $data['originalname'] = $files['name'][$i]; - $data['uploadMaxFilesize'] = $maxUploadFileSize; - $data['maxHumanFilesize'] = $maxHumanFileSize; - $data['permissions'] = $meta['permissions']; - $data['directory'] = $returnedDir; - $result[] = $data; - } - - } else { - $error = $l->t('Upload failed. Could not find uploaded file'); - $errorFileName = $files['name'][$i]; - } - } catch(Exception $ex) { - $error = $ex->getMessage(); - } - - } else { - // file already exists - $meta = \OC\Files\Filesystem::getFileInfo($target); - if ($meta === false) { - $error = $l->t('Upload failed. Could not get file info.'); - } else { - $data = \OCA\Files\Helper::formatFileInfo($meta); - if ($updatable) { - $data['status'] = 'existserror'; - } else { - $data['status'] = 'readonly'; - } - $data['originalname'] = $files['name'][$i]; - $data['uploadMaxFilesize'] = $maxUploadFileSize; - $data['maxHumanFilesize'] = $maxHumanFileSize; - $data['permissions'] = $meta['permissions']; - $data['directory'] = $returnedDir; - $result[] = $data; - } - } - } -} else { - $error = $l->t('Invalid directory.'); -} - -if ($error === false) { - // Do not leak file information if it is a read-only share - if(isset($_POST['dirToken'])) { - $shareManager = \OC::$server->getShareManager(); - $share = $shareManager->getShareByToken((string)$_POST['dirToken']); - if (!($share->getPermissions() & \OCP\Constants::PERMISSION_READ)) { - $newResults = []; - foreach($result as $singleResult) { - $fileName = $singleResult['originalname']; - $newResults['filename'] = $fileName; - $newResults['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileName); - } - $result = $newResults; - } - } - OCP\JSON::encodedPrint($result); -} else { - OCP\JSON::error(array(array('data' => array_merge(array( - 'message' => $error, - 'code' => $errorCode, - 'filename' => $errorFileName - ), $storageStats)))); -} diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index 49dbe553435..06d8d39585f 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -75,24 +75,12 @@ $application->registerRoutes( /** @var $this \OC\Route\Router */ -$this->create('files_ajax_delete', 'ajax/delete.php') - ->actionInclude('files/ajax/delete.php'); $this->create('files_ajax_download', 'ajax/download.php') ->actionInclude('files/ajax/download.php'); $this->create('files_ajax_getstoragestats', 'ajax/getstoragestats.php') ->actionInclude('files/ajax/getstoragestats.php'); $this->create('files_ajax_list', 'ajax/list.php') ->actionInclude('files/ajax/list.php'); -$this->create('files_ajax_move', 'ajax/move.php') - ->actionInclude('files/ajax/move.php'); -$this->create('files_ajax_newfile', 'ajax/newfile.php') - ->actionInclude('files/ajax/newfile.php'); -$this->create('files_ajax_newfolder', 'ajax/newfolder.php') - ->actionInclude('files/ajax/newfolder.php'); -$this->create('files_ajax_rename', 'ajax/rename.php') - ->actionInclude('files/ajax/rename.php'); -$this->create('files_ajax_upload', 'ajax/upload.php') - ->actionInclude('files/ajax/upload.php'); $this->create('download', 'download{file}') ->requirements(array('file' => '.*')) diff --git a/apps/files/js/app.js b/apps/files/js/app.js index fbfa510e07e..17e92de90dd 100644 --- a/apps/files/js/app.js +++ b/apps/files/js/app.js @@ -93,6 +93,7 @@ direction: $('#defaultFileSortingDirection').val() }, config: this._filesConfig, + enableUpload: true } ); this.files.initialize(); diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 56ea384c9e0..30784528700 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -18,83 +18,518 @@ * - TODO music upload button */ -/* global jQuery, oc_requesttoken, humanFileSize, FileList */ +/* global jQuery, humanFileSize, md5 */ /** - * Function that will allow us to know if Ajax uploads are supported - * @link https://github.com/New-Bamboo/example-ajax-upload/blob/master/public/index.html - * also see article @link http://blog.new-bamboo.co.uk/2012/01/10/ridiculously-simple-ajax-uploads-with-formdata + * File upload object + * + * @class OC.FileUpload + * @classdesc + * + * Represents a file upload + * + * @param {OC.Uploader} uploader uploader + * @param {Object} data blueimp data */ -function supportAjaxUploadWithProgress() { - return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData(); - - // Is the File API supported? - function supportFileAPI() { - var fi = document.createElement('INPUT'); - fi.type = 'file'; - return 'files' in fi; +OC.FileUpload = function(uploader, data) { + this.uploader = uploader; + this.data = data; + var path = ''; + if (this.uploader.fileList) { + path = OC.joinPaths(this.uploader.fileList.getCurrentDirectory(), this.getFile().name); + } else { + path = this.getFile().name; } + this.id = 'web-file-upload-' + md5(path) + '-' + (new Date()).getTime(); +}; +OC.FileUpload.CONFLICT_MODE_DETECT = 0; +OC.FileUpload.CONFLICT_MODE_OVERWRITE = 1; +OC.FileUpload.CONFLICT_MODE_AUTORENAME = 2; +OC.FileUpload.prototype = { - // Are progress events supported? - function supportAjaxUploadProgressEvents() { - var xhr = new XMLHttpRequest(); - return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload)); - } + /** + * Unique upload id + * + * @type string + */ + id: null, - // Is FormData supported? - function supportFormData() { - return !! window.FormData; - } -} + /** + * Upload element + * + * @type Object + */ + $uploadEl: null, -/** - * Add form data into the given form data - * - * @param {Array|Object} formData form data which can either be an array or an object - * @param {Object} newData key-values to add to the form data - * - * @return updated form data - */ -function addFormData(formData, newData) { - // in IE8, formData is an array instead of object - if (_.isArray(formData)) { - _.each(newData, function(value, key) { - formData.push({name: key, value: value}); + /** + * Target folder + * + * @type string + */ + _targetFolder: '', + + /** + * @type int + */ + _conflictMode: OC.FileUpload.CONFLICT_MODE_DETECT, + + /** + * New name from server after autorename + * + * @type String + */ + _newName: null, + + /** + * Returns the unique upload id + * + * @return string + */ + getId: function() { + return this.id; + }, + + /** + * Returns the file to be uploaded + * + * @return {File} file + */ + getFile: function() { + return this.data.files[0]; + }, + + /** + * Return the final filename. + * Either this is the original file name or the file name + * after an autorename. + * + * @return {String} file name + */ + getFileName: function() { + // in case of autorename + if (this._newName) { + return this._newName; + } + + if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) { + + var locationUrl = this.getResponseHeader('Content-Location'); + if (locationUrl) { + this._newName = decodeURIComponent(OC.basename(locationUrl)); + return this._newName; + } + } + + return this.getFile().name; + }, + + setTargetFolder: function(targetFolder) { + this._targetFolder = targetFolder; + }, + + getTargetFolder: function() { + return this._targetFolder; + }, + + /** + * Get full path for the target file, including relative path, + * without the file name. + * + * @return {String} full path + */ + getFullPath: function() { + return OC.joinPaths(this._targetFolder, this.getFile().relativePath || ''); + }, + + /** + * Set conflict resolution mode. + * See CONFLICT_MODE_* constants. + */ + setConflictMode: function(mode) { + this._conflictMode = mode; + }, + + /** + * Returns whether the upload is in progress + * + * @return {bool} + */ + isPending: function() { + return this.data.state() === 'pending'; + }, + + deleteUpload: function() { + delete this.data.jqXHR; + }, + + /** + * Submit the upload + */ + submit: function() { + var self = this; + var data = this.data; + var file = this.getFile(); + + // it was a folder upload, so make sure the parent directory exists alrady + var folderPromise; + if (file.relativePath) { + folderPromise = this.uploader.ensureFolderExists(this.getFullPath()); + } else { + folderPromise = $.Deferred().resolve().promise(); + } + + if (this.uploader.fileList) { + this.data.url = this.uploader.fileList.getUploadUrl(file.name, this.getFullPath()); + } + + if (!this.data.headers) { + this.data.headers = {}; + } + + // webdav without multipart + this.data.multipart = false; + this.data.type = 'PUT'; + + delete this.data.headers['If-None-Match']; + if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_DETECT) { + this.data.headers['If-None-Match'] = '*'; + } else if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) { + // POST to parent folder, with slug + this.data.type = 'POST'; + this.data.url = this.uploader.fileList.getUploadUrl('&' + file.name, this.getFullPath()); + } + + if (file.lastModified) { + // preserve timestamp + this.data.headers['X-OC-Mtime'] = file.lastModified / 1000; + } + + var userName = this.uploader.filesClient.getUserName(); + var password = this.uploader.filesClient.getPassword(); + if (userName) { + // copy username/password from DAV client + this.data.headers['Authorization'] = + 'Basic ' + btoa(userName + ':' + (password || '')); + } + + if (!this.uploader.isXHRUpload()) { + data.formData = []; + + // pass headers as parameters + data.formData.push({name: 'headers', value: JSON.stringify(this.data.headers)}); + data.formData.push({name: 'requesttoken', value: OC.requestToken}); + if (data.type === 'POST') { + // still add the method to the URL + data.url += '?_method=POST'; + } + } + + var chunkFolderPromise; + if ($.support.blobSlice + && this.uploader.fileUploadParam.maxChunkSize + && this.getFile().size > this.uploader.fileUploadParam.maxChunkSize + ) { + data.isChunked = true; + chunkFolderPromise = this.uploader.filesClient.createDirectory( + 'uploads/' + encodeURIComponent(OC.getCurrentUser().uid) + '/' + encodeURIComponent(this.getId()) + ); + // TODO: if fails, it means same id already existed, need to retry + } else { + chunkFolderPromise = $.Deferred().resolve().promise(); + } + + // wait for creation of the required directory before uploading + $.when(folderPromise, chunkFolderPromise).then(function() { + data.submit(); + }, function() { + self.abort(); }); - } else { - formData = _.extend(formData, newData); + + }, + + /** + * Process end of transfer + */ + done: function() { + if (!this.data.isChunked) { + return $.Deferred().resolve().promise(); + } + + var uid = OC.getCurrentUser().uid; + return this.uploader.filesClient.move( + 'uploads/' + encodeURIComponent(uid) + '/' + encodeURIComponent(this.getId()) + '/.file', + 'files/' + encodeURIComponent(uid) + '/' + OC.joinPaths(this.getFullPath(), this.getFileName()) + ); + }, + + /** + * Abort the upload + */ + abort: function() { + if (this.data.isChunked) { + // delete transfer directory for this upload + this.uploader.filesClient.remove( + 'uploads/' + encodeURIComponent(OC.getCurrentUser().uid) + '/' + encodeURIComponent(this.getId()) + ); + } + this.data.abort(); + }, + + /** + * Returns the server response + * + * @return {Object} response + */ + getResponse: function() { + var response = this.data.response(); + if (typeof response.result !== 'string') { + //fetch response from iframe + response = $.parseJSON(response.result[0].body.innerText); + if (!response) { + // likely due to internal server error + response = {status: 500}; + } + } else { + response = response.result; + } + return response; + }, + + /** + * Returns the status code from the response + * + * @return {int} status code + */ + getResponseStatus: function() { + if (this.uploader.isXHRUpload()) { + var xhr = this.data.response().jqXHR; + if (xhr) { + return xhr.status; + } + return null; + } + return this.getResponse().status; + }, + + /** + * Returns the response header by name + * + * @param {String} headerName header name + * @return {Array|String} response header value(s) + */ + getResponseHeader: function(headerName) { + headerName = headerName.toLowerCase(); + if (this.uploader.isXHRUpload()) { + return this.data.response().jqXHR.getResponseHeader(headerName); + } + + var headers = this.getResponse().headers; + if (!headers) { + return null; + } + + var value = _.find(headers, function(value, key) { + return key.toLowerCase() === headerName; + }); + if (_.isArray(value) && value.length === 1) { + return value[0]; + } + return value; } - return formData; -} +}; /** * keeps track of uploads in progress and implements callbacks for the conflicts dialog * @namespace */ -OC.Upload = { - _uploads: [], + +OC.Uploader = function() { + this.init.apply(this, arguments); +}; + +OC.Uploader.prototype = _.extend({ + /** + * @type Array<OC.FileUpload> + */ + _uploads: {}, + + /** + * List of directories known to exist. + * + * Key is the fullpath and value is boolean, true meaning that the directory + * was already created so no need to create it again. + */ + _knownDirs: {}, + + /** + * @type OCA.Files.FileList + */ + fileList: null, + + /** + * @type OC.Files.Client + */ + filesClient: null, + + /** + * Function that will allow us to know if Ajax uploads are supported + * @link https://github.com/New-Bamboo/example-ajax-upload/blob/master/public/index.html + * also see article @link http://blog.new-bamboo.co.uk/2012/01/10/ridiculously-simple-ajax-uploads-with-formdata + */ + _supportAjaxUploadWithProgress: function() { + if (window.TESTING) { + return true; + } + return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData(); + + // Is the File API supported? + function supportFileAPI() { + var fi = document.createElement('INPUT'); + fi.type = 'file'; + return 'files' in fi; + } + + // Are progress events supported? + function supportAjaxUploadProgressEvents() { + var xhr = new XMLHttpRequest(); + return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload)); + } + + // Is FormData supported? + function supportFormData() { + return !! window.FormData; + } + }, + + /** + * Returns whether an XHR upload will be used + * + * @return {bool} true if XHR upload will be used, + * false for iframe upload + */ + isXHRUpload: function () { + return !this.fileUploadParam.forceIframeTransport && + ((!this.fileUploadParam.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload); + }, + /** - * deletes the jqHXR object from a data selection - * @param {object} data + * Makes sure that the upload folder and its parents exists + * + * @param {String} fullPath full path + * @return {Promise} promise that resolves when all parent folders + * were created */ - deleteUpload:function(data) { - delete data.jqXHR; + ensureFolderExists: function(fullPath) { + if (!fullPath || fullPath === '/') { + return $.Deferred().resolve().promise(); + } + + // remove trailing slash + if (fullPath.charAt(fullPath.length - 1) === '/') { + fullPath = fullPath.substr(0, fullPath.length - 1); + } + + var self = this; + var promise = this._knownDirs[fullPath]; + + if (this.fileList) { + // assume the current folder exists + this._knownDirs[this.fileList.getCurrentDirectory()] = $.Deferred().resolve().promise(); + } + + if (!promise) { + var deferred = new $.Deferred(); + promise = deferred.promise(); + this._knownDirs[fullPath] = promise; + + // make sure all parents already exist + var parentPath = OC.dirname(fullPath); + var parentPromise = this._knownDirs[parentPath]; + if (!parentPromise) { + parentPromise = this.ensureFolderExists(parentPath); + } + + parentPromise.then(function() { + self.filesClient.createDirectory(fullPath).always(function(status) { + // 405 is expected if the folder already exists + if ((status >= 200 && status < 300) || status === 405) { + self.trigger('createdfolder', fullPath); + deferred.resolve(); + return; + } + OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: fullPath})); + deferred.reject(); + }); + }, function() { + deferred.reject(); + }); + } + + return promise; + }, + + /** + * Submit the given uploads + * + * @param {Array} array of uploads to start + */ + submitUploads: function(uploads) { + var self = this; + _.each(uploads, function(upload) { + self._uploads[upload.data.uploadId] = upload; + upload.submit(); + }); + }, + + /** + * Show conflict for the given file object + * + * @param {OC.FileUpload} file upload object + */ + showConflict: function(fileUpload) { + //show "file already exists" dialog + var self = this; + var file = fileUpload.getFile(); + // retrieve more info about this file + this.filesClient.getFileInfo(fileUpload.getFullPath()).then(function(status, fileInfo) { + var original = fileInfo; + var replacement = file; + OC.dialogs.fileexists(fileUpload, original, replacement, self); + }); }, /** * cancels all uploads */ cancelUploads:function() { this.log('canceling uploads'); - jQuery.each(this._uploads, function(i, jqXHR) { - jqXHR.abort(); + jQuery.each(this._uploads, function(i, upload) { + upload.abort(); }); - this._uploads = []; + this.clear(); }, - rememberUpload:function(jqXHR) { - if (jqXHR) { - this._uploads.push(jqXHR); + /** + * Clear uploads + */ + clear: function() { + this._uploads = {}; + this._knownDirs = {}; + }, + /** + * Returns an upload by id + * + * @param {int} data uploadId + * @return {OC.FileUpload} file upload + */ + getUpload: function(data) { + if (_.isString(data)) { + return this._uploads[data]; + } else if (data.uploadId) { + return this._uploads[data.uploadId]; } + return null; }, + showUploadCancelMessage: _.debounce(function() { OC.Notification.showTemporary(t('files', 'Upload cancelled.'), {timeout: 10}); }, 500), @@ -106,8 +541,8 @@ OC.Upload = { isProcessing:function() { var count = 0; - jQuery.each(this._uploads, function(i, data) { - if (data.state() === 'pending') { + jQuery.each(this._uploads, function(i, upload) { + if (upload.isPending()) { count++; } }); @@ -115,9 +550,8 @@ OC.Upload = { }, /** * callback for the conflicts dialog - * @param {object} data */ - onCancel:function(data) { + onCancel:function() { this.cancelUploads(); }, /** @@ -147,43 +581,29 @@ OC.Upload = { }, /** * handle skipping an upload - * @param {object} data + * @param {OC.FileUpload} upload */ - onSkip:function(data) { - this.log('skip', null, data); - this.deleteUpload(data); + onSkip:function(upload) { + this.log('skip', null, upload); + upload.deleteUpload(); }, /** * handle replacing a file on the server with an uploaded file - * @param {object} data + * @param {FileUpload} data */ - onReplace:function(data) { - this.log('replace', null, data); - if (data.data) { - data.data.append('resolution', 'replace'); - } else { - if (!data.formData) { - data.formData = {}; - } - addFormData(data.formData, {resolution: 'replace'}); - } - data.submit(); + onReplace:function(upload) { + this.log('replace', null, upload); + upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_OVERWRITE); + this.submitUploads([upload]); }, /** * handle uploading a file and letting the server decide a new name - * @param {object} data + * @param {object} upload */ - onAutorename:function(data) { - this.log('autorename', null, data); - if (data.data) { - data.data.append('resolution', 'autorename'); - } else { - if (!data.formData) { - data.formData = {}; - } - addFormData(data.formData, {resolution: 'autorename'}); - } - data.submit(); + onAutorename:function(upload) { + this.log('autorename', null, upload); + upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_AUTORENAME); + this.submitUploads([upload]); }, _trace:false, //TODO implement log handler for JS per class? log:function(caption, e, data) { @@ -205,11 +625,20 @@ OC.Upload = { * @param {function} callbacks.onCancel */ checkExistingFiles: function (selection, callbacks) { - var fileList = FileList; + var fileList = this.fileList; var conflicts = []; // only keep non-conflicting uploads selection.uploads = _.filter(selection.uploads, function(upload) { - var fileInfo = fileList.findFile(upload.files[0].name); + var file = upload.getFile(); + if (file.relativePath) { + // can't check in subfolder contents + return true; + } + if (!fileList) { + // no list to check against + return true; + } + var fileInfo = fileList.findFile(file.name); if (fileInfo) { conflicts.push([ // original @@ -225,9 +654,9 @@ OC.Upload = { }); if (conflicts.length) { // wait for template loading - OC.dialogs.fileexists(null, null, null, OC.Upload).done(function() { + OC.dialogs.fileexists(null, null, null, this).done(function() { _.each(conflicts, function(conflictData) { - OC.dialogs.fileexists(conflictData[1], conflictData[0], conflictData[1].files[0], OC.Upload); + OC.dialogs.fileexists(conflictData[1], conflictData[0], conflictData[1].getFile(), this); }); }); } @@ -240,15 +669,16 @@ OC.Upload = { }, _hideProgressBar: function() { + var self = this; $('#uploadprogresswrapper .stop').fadeOut(); $('#uploadprogressbar').fadeOut(function() { - $('#file_upload_start').trigger(new $.Event('resized')); + self.$uploadEl.trigger(new $.Event('resized')); }); }, _showProgressBar: function() { $('#uploadprogressbar').fadeIn(); - $('#file_upload_start').trigger(new $.Event('resized')); + this.$uploadEl.trigger(new $.Event('resized')); }, /** @@ -269,12 +699,34 @@ OC.Upload = { return ($tr.attr('data-mounttype') === 'shared-root' && $tr.attr('data-mime') !== 'httpd/unix-directory'); }, - init: function() { + /** + * Initialize the upload object + * + * @param {Object} $uploadEl upload element + * @param {Object} options + * @param {OCA.Files.FileList} [options.fileList] file list object + * @param {OC.Files.Client} [options.filesClient] files client object + * @param {Object} [options.dropZone] drop zone for drag and drop upload + */ + init: function($uploadEl, options) { var self = this; - if ( $('#file_upload_start').exists() ) { - var file_upload_param = { - dropZone: $('#app-content'), // restrict dropZone to app-content div - pasteZone: null, + + options = options || {}; + + this.fileList = options.fileList; + this.filesClient = options.filesClient || OC.Files.getClient(); + + $uploadEl = $($uploadEl); + this.$uploadEl = $uploadEl; + + if ($uploadEl.exists()) { + $('#uploadprogresswrapper .stop').on('click', function() { + self.cancelUploads(); + }); + + this.fileUploadParam = { + type: 'PUT', + dropZone: options.dropZone, // restrict dropZone to content div autoUpload: false, sequentialUploads: true, //singleFileUploads is on by default, so the data.files array will always have length 1 @@ -295,9 +747,13 @@ OC.Upload = { * @returns {boolean} */ add: function(e, data) { - OC.Upload.log('add', e, data); + self.log('add', e, data); var that = $(this), freeSpace; + var upload = new OC.FileUpload(self, data); + // can't link directly due to jQuery not liking cyclic deps on its ajax object + data.uploadId = upload.getId(); + // we need to collect all data upload objects before // starting the upload so we can check their existence // and set individual conflict actions. Unfortunately, @@ -317,16 +773,17 @@ OC.Upload = { biggestFileBytes: 0 }; } + // TODO: move originalFiles to a separate container, maybe inside OC.Upload var selection = data.originalFiles.selection; // add uploads if ( selection.uploads.length < selection.filesToUpload ) { // remember upload - selection.uploads.push(data); + selection.uploads.push(upload); } //examine file - var file = data.files[0]; + var file = upload.getFile(); try { // FIXME: not so elegant... need to refactor that method to return a value Files.isFileNameValid(file.name); @@ -336,9 +793,14 @@ OC.Upload = { data.errorThrown = errorMessage; } + if (data.targetDir) { + upload.setTargetFolder(data.targetDir); + delete data.targetDir; + } + // in case folder drag and drop is not supported file will point to a directory // http://stackoverflow.com/a/20448357 - if ( ! file.type && file.size%4096 === 0 && file.size <= 102400) { + if ( ! file.type && file.size % 4096 === 0 && file.size <= 102400) { var dirUploadFailure = false; try { var reader = new FileReader(); @@ -390,7 +852,7 @@ OC.Upload = { // end upload for whole selection on error if (data.errorThrown) { - // trigger fileupload fail + // trigger fileupload fail handler var fu = that.data('blueimp-fileupload') || that.data('fileupload'); fu._trigger('fail', e, data); return false; //don't upload anything @@ -405,9 +867,7 @@ OC.Upload = { var callbacks = { onNoConflicts: function (selection) { - $.each(selection.uploads, function(i, upload) { - upload.submit(); - }); + self.submitUploads(selection.uploads); }, onSkipConflicts: function (selection) { //TODO mark conflicting files as toskip @@ -425,7 +885,7 @@ OC.Upload = { } }; - OC.Upload.checkExistingFiles(selection, callbacks); + self.checkExistingFiles(selection, callbacks); } @@ -436,106 +896,60 @@ OC.Upload = { * @param {object} e */ start: function(e) { - OC.Upload.log('start', e, null); + self.log('start', e, null); //hide the tooltip otherwise it covers the progress bar $('#upload').tipsy('hide'); }, - submit: function(e, data) { - OC.Upload.rememberUpload(data); - if (!data.formData) { - data.formData = {}; - } - - var fileDirectory = ''; - if(typeof data.files[0].relativePath !== 'undefined') { - fileDirectory = data.files[0].relativePath; + fail: function(e, data) { + var upload = self.getUpload(data); + var status = null; + if (upload) { + status = upload.getResponseStatus(); } + self.log('fail', e, upload); - var params = { - requesttoken: oc_requesttoken, - dir: data.targetDir || FileList.getCurrentDirectory(), - file_directory: fileDirectory, - }; - if (data.files[0].isReceivedShare) { - params.isReceivedShare = true; + if (data.textStatus === 'abort') { + self.showUploadCancelMessage(); + } else if (status === 412) { + // file already exists + self.showConflict(upload); + } else if (status === 404) { + // target folder does not exist any more + OC.Notification.showTemporary( + t('files', 'Target folder "{dir}" does not exist any more', {dir: upload.getFullPath()}) + ); + self.cancelUploads(); + } else if (status === 507) { + // not enough space + OC.Notification.showTemporary( + t('files', 'Not enough free space') + ); + self.cancelUploads(); + } else { + // HTTP connection problem or other error + OC.Notification.showTemporary(data.errorThrown, {timeout: 10}); } - addFormData(data.formData, params); - }, - fail: function(e, data) { - OC.Upload.log('fail', e, data); - if (typeof data.textStatus !== 'undefined' && data.textStatus !== 'success' ) { - if (data.textStatus === 'abort') { - OC.Upload.showUploadCancelMessage(); - } else { - // HTTP connection problem - var message = t('files', 'Error uploading file "{fileName}": {message}', { - fileName: escapeHTML(data.files[0].name), - message: data.errorThrown - }, undefined, {escape: false}); - OC.Notification.show(message, {timeout: 0, type: 'error'}); - if (data.result) { - var result = JSON.parse(data.result); - if (result && result[0] && result[0].data && result[0].data.code === 'targetnotfound') { - // abort upload of next files if any - OC.Upload.cancelUploads(); - } - } - } + if (upload) { + upload.deleteUpload(); } - OC.Upload.deleteUpload(data); }, /** * called for every successful upload * @param {object} e * @param {object} data */ - done: function(e, data) { - OC.Upload.log('done', e, data); - // handle different responses (json or body from iframe for ie) - var response; - if (typeof data.result === 'string') { - response = data.result; - } else { - //fetch response from iframe - response = data.result[0].body.innerText; - } - var result = JSON.parse(response); + done:function(e, data) { + var upload = self.getUpload(data); + var that = $(this); + self.log('done', e, upload); - delete data.jqXHR; - - var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); - - if (result.status === 'error' && result.data && result.data.message){ - data.textStatus = 'servererror'; - data.errorThrown = result.data.message; - fu._trigger('fail', e, data); - } else if (typeof result[0] === 'undefined') { - data.textStatus = 'servererror'; - data.errorThrown = t('files', 'Could not get result from server.'); - fu._trigger('fail', e, data); - } else if (result[0].status === 'readonly') { - var original = result[0]; - var replacement = data.files[0]; - OC.dialogs.fileexists(data, original, replacement, OC.Upload); - } else if (result[0].status === 'existserror') { - //show "file already exists" dialog - var original = result[0]; - var replacement = data.files[0]; - OC.dialogs.fileexists(data, original, replacement, OC.Upload); - } else if (result[0].status !== 'success') { - //delete data.jqXHR; - data.textStatus = 'servererror'; - data.errorThrown = result[0].data.message; // error message has been translated on server + var status = upload.getResponseStatus(); + if (status < 200 || status >= 300) { + // trigger fail handler + var fu = that.data('blueimp-fileupload') || that.data('fileupload'); fu._trigger('fail', e, data); - } else { // Successful upload - // Checking that the uploaded file is the last one and contained in the current directory - if (data.files[0] === data.originalFiles[data.originalFiles.length - 1] && - result[0].directory === FileList.getCurrentDirectory()) { - // Scroll to the last uploaded file and highlight all of them - var fileList = _.pluck(data.originalFiles, 'name'); - FileList.highlightFiles(fileList); - } + return; } }, /** @@ -544,15 +958,14 @@ OC.Upload = { * @param {object} data */ stop: function(e, data) { - OC.Upload.log('stop', e, data); + self.log('stop', e, data); } }; // initialize jquery fileupload (blueimp) - var fileupload = $('#file_upload_start').fileupload(file_upload_param); - window.file_upload_param = fileupload; + var fileupload = this.$uploadEl.fileupload(this.fileUploadParam); - if (supportAjaxUploadWithProgress()) { + if (this._supportAjaxUploadWithProgress()) { //remaining time var lastUpdate = new Date().getMilliseconds(); var lastSize = 0; @@ -561,19 +974,17 @@ OC.Upload = { var bufferIndex = 0; var bufferTotal = 0; for(var i = 0; i < bufferSize;i++){ - buffer[i] = 0; + buffer[i] = 0; } + // add progress handlers fileupload.on('fileuploadadd', function(e, data) { - OC.Upload.log('progress handle fileuploadadd', e, data); - //show cancel button - //if (data.dataType !== 'iframe') { //FIXME when is iframe used? only for ie? - // $('#uploadprogresswrapper .stop').show(); - //} + self.log('progress handle fileuploadadd', e, data); + self.trigger('add', e, data); }); // add progress handlers fileupload.on('fileuploadstart', function(e, data) { - OC.Upload.log('progress handle fileuploadstart', e, data); + self.log('progress handle fileuploadstart', e, data); $('#uploadprogresswrapper .stop').show(); $('#uploadprogresswrapper .label').show(); $('#uploadprogressbar').progressbar({value: 0}); @@ -584,14 +995,16 @@ OC.Upload = { + t('files', '...') + '</span></em>'); $('#uploadprogressbar').tipsy({gravity:'n', fade:true, live:true}); - OC.Upload._showProgressBar(); + self._showProgressBar(); + self.trigger('start', e, data); }); fileupload.on('fileuploadprogress', function(e, data) { - OC.Upload.log('progress handle fileuploadprogress', e, data); + self.log('progress handle fileuploadprogress', e, data); //TODO progressbar in row + self.trigger('progress', e, data); }); fileupload.on('fileuploadprogressall', function(e, data) { - OC.Upload.log('progress handle fileuploadprogressall', e, data); + self.log('progress handle fileuploadprogressall', e, data); var progress = (data.loaded / data.total) * 100; var thisUpdate = new Date().getMilliseconds(); var diffUpdate = (thisUpdate - lastUpdate)/1000; // eg. 2s @@ -608,14 +1021,14 @@ OC.Upload = { var smoothRemainingSeconds = (bufferTotal / bufferSize); //seconds var date = new Date(smoothRemainingSeconds * 1000); var timeStringDesktop = ""; - var timeStringMobile = ""; + var timeStringMobile = ""; if(date.getUTCHours() > 0){ - timeStringDesktop = t('files', '{hours}:{minutes}:{seconds} hour{plural_s} left' , { + timeStringDesktop = t('files', '{hours}:{minutes}:{seconds} hour{plural_s} left' , { hours:date.getUTCHours(), minutes: ('0' + date.getUTCMinutes()).slice(-2), seconds: ('0' + date.getUTCSeconds()).slice(-2), plural_s: ( smoothRemainingSeconds === 3600 ? "": "s") // 1 hour = 1*60m*60s = 3600s - }); + }); timeStringMobile = t('files', '{hours}:{minutes}h' , { hours:date.getUTCHours(), minutes: ('0' + date.getUTCMinutes()).slice(-2), @@ -626,12 +1039,12 @@ OC.Upload = { minutes: date.getUTCMinutes(), seconds: ('0' + date.getUTCSeconds()).slice(-2), plural_s: (smoothRemainingSeconds === 60 ? "": "s") // 1 minute = 1*60s = 60s - }); + }); timeStringMobile = t('files', '{minutes}:{seconds}m' , { minutes: date.getUTCMinutes(), seconds: ('0' + date.getUTCSeconds()).slice(-2) }); - } else if(date.getUTCSeconds() > 0){ + } else if(date.getUTCSeconds() > 0){ timeStringDesktop = t('files', '{seconds} second{plural_s} left' , { seconds: date.getUTCSeconds(), plural_s: (smoothRemainingSeconds === 1 ? "": "s") // 1 second = 1s = 1s @@ -651,17 +1064,21 @@ OC.Upload = { }) ); $('#uploadprogressbar').progressbar('value', progress); + self.trigger('progressall', e, data); }); fileupload.on('fileuploadstop', function(e, data) { - OC.Upload.log('progress handle fileuploadstop', e, data); - OC.Upload._hideProgressBar(); + self.log('progress handle fileuploadstop', e, data); + + self.clear(); + self._hideProgressBar(); }); fileupload.on('fileuploadfail', function(e, data) { - OC.Upload.log('progress handle fileuploadfail', e, data); + self.log('progress handle fileuploadfail', e, data); //if user pressed cancel hide upload progress bar and cancel button if (data.errorThrown === 'abort') { - OC.Upload._hideProgressBar(); + self._hideProgressBar(); } + self.trigger('fail', e, data); }); var disableDropState = function() { $('#app-content').removeClass('file-drag'); @@ -696,55 +1113,53 @@ OC.Upload = { filerow.find('.thumbnail').addClass('icon-filetype-folder-drag-accept'); } }); - fileupload.on('fileuploaddragleave fileuploaddrop', disableDropState); - } else { - // for all browsers that don't support the progress bar - // IE 8 & 9 - - // show a spinner - fileupload.on('fileuploadstart', function() { - $('#upload').addClass('icon-loading'); - $('#upload .icon-upload').hide(); + fileupload.on('fileuploaddragleave fileuploaddrop', function (){ + $('#app-content').removeClass('file-drag'); + $('.dropping-to-dir').removeClass('dropping-to-dir'); + $('.dir-drop').removeClass('dir-drop'); + $('.icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept'); }); - // hide a spinner - fileupload.on('fileuploadstop fileuploadfail', function() { - $('#upload').removeClass('icon-loading'); - $('#upload .icon-upload').show(); + fileupload.on('fileuploadchunksend', function(e, data) { + // modify the request to adjust it to our own chunking + var upload = self.getUpload(data); + var range = data.contentRange.split(' ')[1]; + var chunkId = range.split('/')[0]; + data.url = OC.getRootPath() + + '/remote.php/dav/uploads' + + '/' + encodeURIComponent(OC.getCurrentUser().uid) + + '/' + encodeURIComponent(upload.getId()) + + '/' + encodeURIComponent(chunkId); + delete data.contentRange; + delete data.headers['Content-Range']; + }); + fileupload.on('fileuploaddone', function(e, data) { + var upload = self.getUpload(data); + upload.done().then(function() { + self.trigger('done', e, upload); + }); + }); + fileupload.on('fileuploaddrop', function(e, data) { + self.trigger('drop', e, data); }); - } - } - $.assocArraySize = function(obj) { - // http://stackoverflow.com/a/6700/11236 - var size = 0, key; - for (key in obj) { - if (obj.hasOwnProperty(key)) { - size++; - } } - return size; - }; + } // warn user not to leave the page while upload is in progress $(window).on('beforeunload', function(e) { - if (OC.Upload.isProcessing()) { + if (self.isProcessing()) { return t('files', 'File upload is in progress. Leaving the page now will cancel the upload.'); } }); //add multiply file upload attribute to all browsers except konqueror (which crashes when it's used) if (navigator.userAgent.search(/konqueror/i) === -1) { - $('#file_upload_start').attr('multiple', 'multiple'); + this.$uploadEl.attr('multiple', 'multiple'); } - window.file_upload_param = file_upload_param; - return file_upload_param; + return this.fileUploadParam; } -}; - -$(document).ready(function() { - OC.Upload.init(); -}); +}, OC.Backbone.Events); diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 53ad8eafeef..e728a816cc0 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -30,6 +30,7 @@ * @param {Object} [options.dragOptions] drag options, disabled by default * @param {Object} [options.folderDropOptions] folder drop options, disabled by default * @param {boolean} [options.detailsViewEnabled=true] whether to enable details view + * @param {boolean} [options.enableUpload=false] whether to enable uploader * @param {OC.Files.Client} [options.filesClient] files client to use */ var FileList = function($el, options) { @@ -189,6 +190,11 @@ _folderDropOptions: null, /** + * @type OC.Uploader + */ + _uploader: null, + + /** * Initialize the file list and its components * * @param $el container element with existing markup for the #controls @@ -328,8 +334,6 @@ this.$el.find('.selectedActions a').tooltip({placement:'top'}); - this.setupUploadEvents(); - this.$container.on('scroll', _.bind(this._onScroll, this)); if (options.scrollTo) { @@ -338,6 +342,20 @@ }); } + if (options.enableUpload) { + // TODO: auto-create this element + var $uploadEl = this.$el.find('#file_upload_start'); + if ($uploadEl.exists()) { + this._uploader = new OC.Uploader($uploadEl, { + fileList: this, + filesClient: this.filesClient, + dropZone: $('#content') + }); + + this.setupUploadEvents(this._uploader); + } + } + OC.Plugins.attach('OCA.Files.FileList', this); }, @@ -1420,7 +1438,10 @@ return; } this._setCurrentDir(targetDir, changeUrl, fileId); - return this.reload().then(function(success){ + + // discard finished uploads list, we'll get it through a regular reload + this._uploads = {}; + this.reload().then(function(success){ if (!success) { self.changeDirectory(currentDir, true); } @@ -1662,6 +1683,24 @@ return OCA.Files.Files.getDownloadUrl(files, dir || this.getCurrentDirectory(), isDir); }, + getUploadUrl: function(fileName, dir) { + if (_.isUndefined(dir)) { + dir = this.getCurrentDirectory(); + } + + var pathSections = dir.split('/'); + if (!_.isUndefined(fileName)) { + pathSections.push(fileName); + } + var encodedPath = ''; + _.each(pathSections, function(section) { + if (section !== '') { + encodedPath += '/' + encodeURIComponent(section); + } + }); + return OC.linkToRemoteBase('webdav') + encodedPath; + }, + /** * Generates a preview URL based on the URL space. * @param urlSpec attributes for the URL @@ -2123,19 +2162,11 @@ ) .done(function() { // TODO: error handling / conflicts - self.filesClient.getFileInfo( - targetPath, { - properties: self._getWebdavProperties() - } - ) - .then(function(status, data) { - self.add(data, {animate: true, scrollTo: true}); - deferred.resolve(status, data); - }) - .fail(function(status) { - OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name})); - deferred.reject(status); - }); + self.addAndFetchFileInfo(targetPath, '', {scrollTo: true}).then(function(status, data) { + deferred.resolve(status, data); + }, function() { + OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name})); + }); }) .fail(function(status) { if (status === 412) { @@ -2176,32 +2207,19 @@ var targetPath = this.getCurrentDirectory() + '/' + name; this.filesClient.createDirectory(targetPath) - .done(function(createStatus) { - self.filesClient.getFileInfo( - targetPath, { - properties: self._getWebdavProperties() - } - ) - .done(function(status, data) { - self.add(data, {animate: true, scrollTo: true}); - deferred.resolve(status, data); - }) - .fail(function() { - OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: name})); - deferred.reject(createStatus); - }); + .done(function() { + self.addAndFetchFileInfo(targetPath, '', {scrollTo:true}).then(function(status, data) { + deferred.resolve(status, data); + }, function() { + OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: name})); + }); }) .fail(function(createStatus) { // method not allowed, folder might exist already if (createStatus === 405) { - self.filesClient.getFileInfo( - targetPath, { - properties: self._getWebdavProperties() - } - ) + // add it to the list, for completeness + self.addAndFetchFileInfo(targetPath, '', {scrollTo:true}) .done(function(status, data) { - // add it to the list, for completeness - self.add(data, {animate: true, scrollTo: true}); OC.Notification.showTemporary( t('files', 'Could not create folder "{dir}" because it already exists', {dir: name}) ); @@ -2224,6 +2242,60 @@ }, /** + * Add file into the list by fetching its information from the server first. + * + * If the given directory does not match the current directory, nothing will + * be fetched. + * + * @param {String} fileName file name + * @param {String} [dir] optional directory, defaults to the current one + * @param {Object} options same options as #add + * @return {Promise} promise that resolves with the file info, or an + * already resolved Promise if no info was fetched. The promise rejects + * if the file was not found or an error occurred. + * + * @since 9.0 + */ + addAndFetchFileInfo: function(fileName, dir, options) { + var self = this; + var deferred = $.Deferred(); + if (_.isUndefined(dir)) { + dir = this.getCurrentDirectory(); + } else { + dir = dir || '/'; + } + + var targetPath = OC.joinPaths(dir, fileName); + + if ((OC.dirname(targetPath) || '/') !== this.getCurrentDirectory()) { + // no need to fetch information + deferred.resolve(); + return deferred.promise(); + } + + var addOptions = _.extend({ + animate: true, + scrollTo: false + }, options || {}); + + this.filesClient.getFileInfo(targetPath, { + properties: this._getWebdavProperties() + }) + .then(function(status, data) { + // remove first to avoid duplicates + self.remove(data.name); + self.add(data, addOptions); + deferred.resolve(status, data); + }) + .fail(function(status) { + OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name})); + deferred.reject(status); + }); + + return deferred.promise(); + }, + + /** * Returns whether the given file name exists in the list * * @param {string} file file name @@ -2591,19 +2663,19 @@ /** * Setup file upload events related to the file-upload plugin + * + * @param {OC.Uploader} uploader */ - setupUploadEvents: function() { + setupUploadEvents: function(uploader) { var self = this; - // handle upload events - var fileUploadStart = this.$el; - var delegatedElement = '#file_upload_start'; + self._uploads = {}; // detect the progress bar resize - fileUploadStart.on('resized', this._onResize); + uploader.on('resized', this._onResize); - fileUploadStart.on('fileuploaddrop', delegatedElement, function(e, data) { - OC.Upload.log('filelist handle fileuploaddrop', e, data); + uploader.on('drop', function(e, data) { + self._uploader.log('filelist handle fileuploaddrop', e, data); if (self.$el.hasClass('hidden')) { // do not upload to invisible lists @@ -2654,25 +2726,20 @@ // add target dir data.targetDir = dir; } else { - // we are dropping somewhere inside the file list, which will - // upload the file to the current directory - data.targetDir = self.getCurrentDirectory(); - // cancel uploads to current dir if no permission var isCreatable = (self.getDirectoryPermissions() & OC.PERMISSION_CREATE) !== 0; if (!isCreatable) { self._showPermissionDeniedNotification(); return false; } - } - }); - fileUploadStart.on('fileuploadadd', function(e, data) { - OC.Upload.log('filelist handle fileuploadadd', e, data); - //finish delete if we are uploading a deleted file - if (self.deleteFiles && self.deleteFiles.indexOf(data.files[0].name)!==-1) { - self.finishDelete(null, true); //delete file before continuing + // we are dropping somewhere inside the file list, which will + // upload the file to the current directory + data.targetDir = self.getCurrentDirectory(); } + }); + uploader.on('add', function(e, data) { + self._uploader.log('filelist handle fileuploadadd', e, data); // add ui visualization to existing folder if (data.context && data.context.data('type') === 'dir') { @@ -2694,135 +2761,74 @@ } } + if (!data.targetDir) { + data.targetDir = self.getCurrentDirectory(); + } + }); /* * when file upload done successfully add row to filelist * update counter when uploading to sub folder */ - fileUploadStart.on('fileuploaddone', function(e, data) { - OC.Upload.log('filelist handle fileuploaddone', e, data); + uploader.on('done', function(e, upload) { + self._uploader.log('filelist handle fileuploaddone', e, data); - var response; - if (typeof data.result === 'string') { - response = data.result; - } else { - // fetch response from iframe - response = data.result[0].body.innerText; + var data = upload.data; + var status = data.jqXHR.status; + if (status < 200 || status >= 300) { + // error was handled in OC.Uploads already + return; } - var result = JSON.parse(response); - - if (typeof result[0] !== 'undefined' && result[0].status === 'success') { - var file = result[0]; - var size = 0; - - if (data.context && data.context.data('type') === 'dir') { - - // update upload counter ui - var uploadText = data.context.find('.uploadtext'); - var currentUploads = parseInt(uploadText.attr('currentUploads'), 10); - currentUploads -= 1; - uploadText.attr('currentUploads', currentUploads); - var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads); - if (currentUploads === 0) { - self.showFileBusyState(uploadText.closest('tr'), false); - uploadText.text(translatedText); - uploadText.hide(); - } else { - uploadText.text(translatedText); - } - - // update folder size - size = parseInt(data.context.data('size'), 10); - size += parseInt(file.size, 10); - data.context.attr('data-size', size); - data.context.find('td.filesize').text(humanFileSize(size)); - } else { - // only append new file if uploaded into the current folder - if (file.directory !== self.getCurrentDirectory()) { - // Uploading folders actually uploads a list of files - // for which the target directory (file.directory) might lie deeper - // than the current directory - - var fileDirectory = file.directory.replace('/','').replace(/\/$/, ""); - var currentDirectory = self.getCurrentDirectory().replace('/','').replace(/\/$/, "") + '/'; - - if (currentDirectory !== '/') { - // abort if fileDirectory does not start with current one - if (fileDirectory.indexOf(currentDirectory) !== 0) { - return; - } - - // remove the current directory part - fileDirectory = fileDirectory.substr(currentDirectory.length); - } - - // only take the first section of the path - fileDirectory = fileDirectory.split('/'); - - var fd; - // if the first section exists / is a subdir - if (fileDirectory.length) { - fileDirectory = fileDirectory[0]; - - // See whether it is already in the list - fd = self.findFileEl(fileDirectory); - if (fd.length === 0) { - var dir = { - name: fileDirectory, - type: 'dir', - mimetype: 'httpd/unix-directory', - permissions: file.permissions, - size: 0, - id: file.parentId - }; - fd = self.add(dir, {insert: true}); - } - // update folder size - size = parseInt(fd.attr('data-size'), 10); - size += parseInt(file.size, 10); - fd.attr('data-size', size); - fd.find('td.filesize').text(OC.Util.humanFileSize(size)); - } - - return; - } - - // add as stand-alone row to filelist - size = t('files', 'Pending'); - if (data.files[0].size>=0) { - size=data.files[0].size; - } - //should the file exist in the list remove it - self.remove(file.name); - - // create new file context - data.context = self.add(file, {animate: true}); - } + var fileName = upload.getFileName(); + var fetchInfoPromise = self.addAndFetchFileInfo(fileName, upload.getFullPath()); + if (!self._uploads) { + self._uploads = {}; + } + if (OC.isSamePath(OC.dirname(upload.getFullPath() + '/'), self.getCurrentDirectory())) { + self._uploads[fileName] = fetchInfoPromise; } - }); - fileUploadStart.on('fileuploadstop', function() { - OC.Upload.log('filelist handle fileuploadstop'); - //cleanup uploading to a dir var uploadText = self.$fileList.find('tr .uploadtext'); self.showFileBusyState(uploadText.closest('tr'), false); uploadText.fadeOut(); uploadText.attr('currentUploads', 0); - + }); + uploader.on('createdfolder', function(fullPath) { + self.addAndFetchFileInfo(OC.basename(fullPath), OC.dirname(fullPath)); + }); + uploader.on('stop', function() { + self._uploader.log('filelist handle fileuploadstop'); + + // prepare list of uploaded file names in the current directory + // and discard the other ones + var promises = _.values(self._uploads); + var fileNames = _.keys(self._uploads); + self._uploads = []; + + // as soon as all info is fetched + $.when.apply($, promises).then(function() { + // highlight uploaded files + self.highlightFiles(fileNames); + }); self.updateStorageStatistics(); + + var uploadText = self.$fileList.find('tr .uploadtext'); + self.showFileBusyState(uploadText.closest('tr'), false); + uploadText.fadeOut(); + uploadText.attr('currentUploads', 0); }); - fileUploadStart.on('fileuploadfail', function(e, data) { - OC.Upload.log('filelist handle fileuploadfail', e, data); + uploader.on('fail', function(e, data) { + self._uploader.log('filelist handle fileuploadfail', e, data); + + self._uploads = []; //if user pressed cancel hide upload chrome - if (data.errorThrown === 'abort') { - //cleanup uploading to a dir - var uploadText = self.$fileList.find('tr .uploadtext'); - self.showFileBusyState(uploadText.closest('tr'), false); - uploadText.fadeOut(); - uploadText.attr('currentUploads', 0); - } + //cleanup uploading to a dir + var uploadText = self.$fileList.find('tr .uploadtext'); + self.showFileBusyState(uploadText.closest('tr'), false); + uploadText.fadeOut(); + uploadText.attr('currentUploads', 0); self.updateStorageStatistics(); }); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 2873b84bc9a..0be098b2e73 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -226,17 +226,6 @@ // TODO: move file list related code (upload) to OCA.Files.FileList $('#file_action_panel').attr('activeAction', false); - // Triggers invisible file input - $('#upload a').on('click', function() { - $(this).parent().children('#file_upload_start').trigger('click'); - return false; - }); - - // Trigger cancelling of file upload - $('#uploadprogresswrapper .stop').on('click', function() { - OC.Upload.cancelUploads(); - }); - // drag&drop support using jquery.fileupload // TODO use OC.dialogs $(document).bind('drop dragover', function (e) { diff --git a/apps/files/templates/list.php b/apps/files/templates/list.php index dda7a1814a0..e741849f38b 100644 --- a/apps/files/templates/list.php +++ b/apps/files/templates/list.php @@ -75,8 +75,7 @@ </table> <input type="hidden" name="dir" id="dir" value="" /> <div class="hiddenuploadfield"> - <input type="file" id="file_upload_start" class="hiddenuploadfield" name="files[]" - data-url="<?php print_unescaped(OCP\Util::linkTo('files', 'ajax/upload.php')); ?>" /> + <input type="file" id="file_upload_start" class="hiddenuploadfield" name="files[]" /> </div> <div id="editor"></div><!-- FIXME Do not use this div in your app! It is deprecated and will be removed in the future! --> <div id="uploadsize-message" title="<?php p($l->t('Upload too large'))?>"> diff --git a/apps/files/tests/js/fileUploadSpec.js b/apps/files/tests/js/fileUploadSpec.js index 0483d4649d4..19f8cde7e44 100644 --- a/apps/files/tests/js/fileUploadSpec.js +++ b/apps/files/tests/js/fileUploadSpec.js @@ -19,11 +19,11 @@ * */ -/* global FileList */ - describe('OC.Upload tests', function() { var $dummyUploader; var testFile; + var uploader; + var failStub; beforeEach(function() { testFile = { @@ -46,59 +46,64 @@ describe('OC.Upload tests', function() { '</div>' ); $dummyUploader = $('#file_upload_start'); + uploader = new OC.Uploader($dummyUploader); + failStub = sinon.stub(); + uploader.on('fail', failStub); }); afterEach(function() { - delete window.file_upload_param; $dummyUploader = undefined; + failStub = undefined; }); - describe('Adding files for upload', function() { - var params; - var failStub; - beforeEach(function() { - params = OC.Upload.init(); - failStub = sinon.stub(); - $dummyUploader.on('fileuploadfail', failStub); - }); - afterEach(function() { - params = undefined; - failStub = undefined; - }); - - /** - * Add file for upload - * @param file file data - */ - function addFile(file) { - return params.add.call( + /** + * Add file for upload + * @param {Array.<File>} files array of file data to simulate upload + * @return {Array.<Object>} array of uploadinfo or null if add() returned false + */ + function addFiles(uploader, files) { + return _.map(files, function(file) { + var jqXHR = {status: 200}; + var uploadInfo = { + originalFiles: files, + files: [file], + jqXHR: jqXHR, + response: sinon.stub.returns(jqXHR), + submit: sinon.stub() + }; + if (uploader.fileUploadParam.add.call( $dummyUploader[0], {}, - { - originalFiles: {}, - files: [file] - }); - } + uploadInfo + )) { + return uploadInfo; + } + return null; + }); + } + describe('Adding files for upload', function() { it('adds file when size is below limits', function() { - var result = addFile(testFile); - expect(result).toEqual(true); + var result = addFiles(uploader, [testFile]); + expect(result[0]).not.toEqual(null); + expect(result[0].submit.calledOnce).toEqual(true); }); it('adds file when free space is unknown', function() { var result; $('#free_space').val(-2); - result = addFile(testFile); + result = addFiles(uploader, [testFile]); - expect(result).toEqual(true); + expect(result[0]).not.toEqual(null); + expect(result[0].submit.calledOnce).toEqual(true); expect(failStub.notCalled).toEqual(true); }); it('does not add file if it exceeds upload limit', function() { var result; $('#upload_limit').val(1000); - result = addFile(testFile); + result = addFiles(uploader, [testFile]); - expect(result).toEqual(false); + expect(result[0]).toEqual(null); expect(failStub.calledOnce).toEqual(true); expect(failStub.getCall(0).args[1].textStatus).toEqual('sizeexceedlimit'); expect(failStub.getCall(0).args[1].errorThrown).toEqual( @@ -109,9 +114,9 @@ describe('OC.Upload tests', function() { var result; $('#free_space').val(1000); - result = addFile(testFile); + result = addFiles(uploader, [testFile]); - expect(result).toEqual(false); + expect(result[0]).toEqual(null); expect(failStub.calledOnce).toEqual(true); expect(failStub.getCall(0).args[1].textStatus).toEqual('notenoughspace'); expect(failStub.getCall(0).args[1].errorThrown).toEqual( @@ -120,12 +125,10 @@ describe('OC.Upload tests', function() { }); }); describe('Upload conflicts', function() { - var oldFileList; var conflictDialogStub; - var callbacks; + var fileList; beforeEach(function() { - oldFileList = FileList; $('#testArea').append( '<div id="tableContainer">' + '<table id="filestable">' + @@ -145,74 +148,56 @@ describe('OC.Upload tests', function() { '</table>' + '</div>' ); - FileList = new OCA.Files.FileList($('#tableContainer')); + fileList = new OCA.Files.FileList($('#tableContainer')); - FileList.add({name: 'conflict.txt', mimetype: 'text/plain'}); - FileList.add({name: 'conflict2.txt', mimetype: 'text/plain'}); + fileList.add({name: 'conflict.txt', mimetype: 'text/plain'}); + fileList.add({name: 'conflict2.txt', mimetype: 'text/plain'}); conflictDialogStub = sinon.stub(OC.dialogs, 'fileexists'); - callbacks = { - onNoConflicts: sinon.stub() - }; + + uploader = new OC.Uploader($dummyUploader, { + fileList: fileList + }); }); afterEach(function() { conflictDialogStub.restore(); - FileList.destroy(); - FileList = oldFileList; + fileList.destroy(); }); it('does not show conflict dialog when no client side conflict', function() { - var selection = { - // yes, the format of uploads is weird... - uploads: [ - {files: [{name: 'noconflict.txt'}]}, - {files: [{name: 'noconflict2.txt'}]} - ] - }; - - OC.Upload.checkExistingFiles(selection, callbacks); + var result = addFiles(uploader, [{name: 'noconflict.txt'}, {name: 'noconflict2.txt'}]); expect(conflictDialogStub.notCalled).toEqual(true); - expect(callbacks.onNoConflicts.calledOnce).toEqual(true); - expect(callbacks.onNoConflicts.calledWith(selection)).toEqual(true); + expect(result[0].submit.calledOnce).toEqual(true); + expect(result[1].submit.calledOnce).toEqual(true); }); it('shows conflict dialog when no client side conflict', function() { - var selection = { - // yes, the format of uploads is weird... - uploads: [ - {files: [{name: 'conflict.txt'}]}, - {files: [{name: 'conflict2.txt'}]}, - {files: [{name: 'noconflict.txt'}]} - ] - }; - var deferred = $.Deferred(); conflictDialogStub.returns(deferred.promise()); deferred.resolve(); - OC.Upload.checkExistingFiles(selection, callbacks); + var result = addFiles(uploader, [ + {name: 'conflict.txt'}, + {name: 'conflict2.txt'}, + {name: 'noconflict.txt'} + ]); expect(conflictDialogStub.callCount).toEqual(3); - expect(conflictDialogStub.getCall(1).args[0]) - .toEqual({files: [ { name: 'conflict.txt' } ]}); + expect(conflictDialogStub.getCall(1).args[0].getFileName()) + .toEqual('conflict.txt'); expect(conflictDialogStub.getCall(1).args[1]) .toEqual({ name: 'conflict.txt', mimetype: 'text/plain', directory: '/' }); expect(conflictDialogStub.getCall(1).args[2]).toEqual({ name: 'conflict.txt' }); // yes, the dialog must be called several times... - expect(conflictDialogStub.getCall(2).args[0]).toEqual({ - files: [ { name: 'conflict2.txt' } ] - }); + expect(conflictDialogStub.getCall(2).args[0].getFileName()).toEqual('conflict2.txt'); expect(conflictDialogStub.getCall(2).args[1]) .toEqual({ name: 'conflict2.txt', mimetype: 'text/plain', directory: '/' }); expect(conflictDialogStub.getCall(2).args[2]).toEqual({ name: 'conflict2.txt' }); - expect(callbacks.onNoConflicts.calledOnce).toEqual(true); - expect(callbacks.onNoConflicts.calledWith({ - uploads: [ - {files: [{name: 'noconflict.txt'}]} - ] - })).toEqual(true); + expect(result[0].submit.calledOnce).toEqual(false); + expect(result[1].submit.calledOnce).toEqual(false); + expect(result[2].submit.calledOnce).toEqual(true); }); }); }); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index 304f8438a59..1c73bc845c5 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -159,7 +159,8 @@ describe('OCA.Files.FileList tests', function() { pageSizeStub = sinon.stub(OCA.Files.FileList.prototype, 'pageSize').returns(20); fileList = new OCA.Files.FileList($('#app-content-files'), { filesClient: filesClient, - config: filesConfig + config: filesConfig, + enableUpload: true }); }); afterEach(function() { @@ -2441,7 +2442,7 @@ describe('OCA.Files.FileList tests', function() { deferredInfo.resolve( 200, - new FileInfo({ + new FileInfo({ path: '/subdir', name: 'test.txt', mimetype: 'text/plain' @@ -2501,27 +2502,116 @@ describe('OCA.Files.FileList tests', function() { // TODO: error cases // TODO: unique name cases }); + describe('addAndFetchFileInfo', function() { + var getFileInfoStub; + var getFileInfoDeferred; + + beforeEach(function() { + getFileInfoDeferred = $.Deferred(); + getFileInfoStub = sinon.stub(OC.Files.Client.prototype, 'getFileInfo'); + getFileInfoStub.returns(getFileInfoDeferred.promise()); + }); + afterEach(function() { + getFileInfoStub.restore(); + }); + it('does not fetch if the given folder is not the current one', function() { + var promise = fileList.addAndFetchFileInfo('testfile.txt', '/another'); + expect(getFileInfoStub.notCalled).toEqual(true); + + expect(promise.state()).toEqual('resolved'); + }); + it('fetches info when folder is the current one', function() { + fileList.addAndFetchFileInfo('testfile.txt', '/subdir'); + expect(getFileInfoStub.calledOnce).toEqual(true); + expect(getFileInfoStub.getCall(0).args[0]).toEqual('/subdir/testfile.txt'); + }); + it('adds file data to list when fetching is done', function() { + fileList.addAndFetchFileInfo('testfile.txt', '/subdir'); + getFileInfoDeferred.resolve(200, { + name: 'testfile.txt', + size: 100 + }); + expect(fileList.findFileEl('testfile.txt').attr('data-size')).toEqual('100'); + }); + it('replaces file data to list when fetching is done', function() { + fileList.addAndFetchFileInfo('testfile.txt', '/subdir', {replace: true}); + fileList.add({ + name: 'testfile.txt', + size: 95 + }); + getFileInfoDeferred.resolve(200, { + name: 'testfile.txt', + size: 100 + }); + expect(fileList.findFileEl('testfile.txt').attr('data-size')).toEqual('100'); + }); + it('resolves promise with file data when fetching is done', function() { + var promise = fileList.addAndFetchFileInfo('testfile.txt', '/subdir', {replace: true}); + getFileInfoDeferred.resolve(200, { + name: 'testfile.txt', + size: 100 + }); + expect(promise.state()).toEqual('resolved'); + promise.then(function(status, data) { + expect(status).toEqual(200); + expect(data.name).toEqual('testfile.txt'); + expect(data.size).toEqual(100); + }); + }); + }); /** * Test upload mostly by testing the code inside the event handlers * that were registered on the magic upload object */ describe('file upload', function() { - var $uploader; + var uploadData; + var uploader; beforeEach(function() { - // note: this isn't the real blueimp file uploader from jquery.fileupload - // but it makes it possible to simulate the event triggering to - // test the response of the handlers - $uploader = $('#file_upload_start'); fileList.setFiles(testFiles); + uploader = fileList._uploader; + // simulate data structure from jquery.upload + uploadData = { + files: [{ + name: 'upload.txt' + }] + }; }); afterEach(function() { - $uploader = null; + uploader = null; + uploadData = null; }); + describe('enableupload', function() { + it('sets up uploader when enableUpload is true', function() { + expect(fileList._uploader).toBeDefined(); + }); + it('does not sets up uploader when enableUpload is false', function() { + fileList.destroy(); + fileList = new OCA.Files.FileList($('#app-content-files'), { + filesClient: filesClient + }); + expect(fileList._uploader).toBeFalsy(); + }); + }); + + describe('adding files for upload', function() { + /** + * Simulate add event on the given target + * + * @return event object including the result + */ + function addFile(data) { + uploader.trigger('add', {}, data || {}); + } + + it('sets target dir to the current directory', function() { + addFile(uploadData); + expect(uploadData.targetDir).toEqual('/subdir'); + }); + }); describe('dropping external files', function() { - var uploadData; /** * Simulate drop event on the given target @@ -2535,80 +2625,71 @@ describe('OCA.Files.FileList tests', function() { target: $target } }; - var ev = new $.Event('fileuploaddrop', eventData); - $uploader.trigger(ev, data || {}); - return ev; + uploader.trigger('drop', eventData, data || {}); + return !!data.targetDir; } - beforeEach(function() { - // simulate data structure from jquery.upload - uploadData = { - files: [{ - relativePath: 'fileToUpload.txt' - }] - }; - }); - afterEach(function() { - uploadData = null; - }); it('drop on a tr or crumb outside file list does not trigger upload', function() { var $anotherTable = $('<table><tbody><tr><td>outside<div class="crumb">crumb</div></td></tr></table>'); var ev; $('#testArea').append($anotherTable); ev = dropOn($anotherTable.find('tr'), uploadData); - expect(ev.result).toEqual(false); + expect(ev).toEqual(false); - ev = dropOn($anotherTable.find('.crumb')); - expect(ev.result).toEqual(false); + ev = dropOn($anotherTable.find('.crumb'), uploadData); + expect(ev).toEqual(false); }); it('drop on an element outside file list container does not trigger upload', function() { var $anotherEl = $('<div>outside</div>'); var ev; $('#testArea').append($anotherEl); - ev = dropOn($anotherEl); + ev = dropOn($anotherEl, uploadData); - expect(ev.result).toEqual(false); + expect(ev).toEqual(false); }); it('drop on an element inside the table triggers upload', function() { var ev; ev = dropOn(fileList.$fileList.find('th:first'), uploadData); - expect(ev.result).not.toEqual(false); + expect(ev).not.toEqual(false); + expect(uploadData.targetDir).toEqual('/subdir'); }); it('drop on an element on the table container triggers upload', function() { var ev; ev = dropOn($('#app-content-files'), uploadData); - expect(ev.result).not.toEqual(false); + expect(ev).not.toEqual(false); + expect(uploadData.targetDir).toEqual('/subdir'); }); it('drop on an element inside the table does not trigger upload if no upload permission', function() { $('#permissions').val(0); var ev; - ev = dropOn(fileList.$fileList.find('th:first')); + ev = dropOn(fileList.$fileList.find('th:first'), uploadData); - expect(ev.result).toEqual(false); + expect(ev).toEqual(false); expect(notificationStub.calledOnce).toEqual(true); }); it('drop on an folder does not trigger upload if no upload permission on that folder', function() { var $tr = fileList.findFileEl('somedir'); var ev; $tr.data('permissions', OC.PERMISSION_READ); - ev = dropOn($tr); + ev = dropOn($tr, uploadData); - expect(ev.result).toEqual(false); + expect(ev).toEqual(false); expect(notificationStub.calledOnce).toEqual(true); }); it('drop on a file row inside the table triggers upload to current folder', function() { var ev; ev = dropOn(fileList.findFileEl('One.txt').find('td:first'), uploadData); - expect(ev.result).not.toEqual(false); + expect(ev).not.toEqual(false); + expect(uploadData.targetDir).toEqual('/subdir'); }); it('drop on a folder row inside the table triggers upload to target folder', function() { var ev; ev = dropOn(fileList.findFileEl('somedir').find('td:eq(2)'), uploadData); - expect(ev.result).not.toEqual(false); + expect(ev).not.toEqual(false); expect(uploadData.targetDir).toEqual('/subdir/somedir'); }); it('drop on a breadcrumb inside the table triggers upload to target folder', function() { @@ -2616,7 +2697,7 @@ describe('OCA.Files.FileList tests', function() { fileList.changeDirectory('a/b/c/d'); ev = dropOn(fileList.$el.find('.crumb:eq(2)'), uploadData); - expect(ev.result).not.toEqual(false); + expect(ev).not.toEqual(false); expect(uploadData.targetDir).toEqual('/a/b'); }); it('renders upload indicator element for folders only', function() { @@ -2635,6 +2716,93 @@ describe('OCA.Files.FileList tests', function() { expect(fileList.findFileEl('afile.txt').find('.uploadtext').length).toEqual(0); }); }); + + describe('after folder creation due to folder upload', function() { + it('fetches folder info', function() { + var fetchInfoStub = sinon.stub(fileList, 'addAndFetchFileInfo'); + + uploader.trigger('createdfolder', '/subdir/newfolder'); + + expect(fetchInfoStub.calledOnce).toEqual(true); + expect(fetchInfoStub.getCall(0).args[0]).toEqual('newfolder'); + expect(fetchInfoStub.getCall(0).args[1]).toEqual('/subdir'); + + fetchInfoStub.restore(); + }); + }); + + describe('after upload', function() { + var fetchInfoStub; + + beforeEach(function() { + fetchInfoStub = sinon.stub(fileList, 'addAndFetchFileInfo'); + + }); + afterEach(function() { + fetchInfoStub.restore(); + }); + + + function createUpload(name, dir) { + var jqXHR = { + status: 200 + }; + return { + getFileName: sinon.stub().returns(name), + getFullPath: sinon.stub().returns(dir), + data: { + jqXHR: jqXHR + } + }; + } + + /** + * Simulate add event on the given target + * + * @return event object including the result + */ + function addFile(data) { + var ev = new $.Event('done', { + jqXHR: {status: 200} + }); + var deferred = $.Deferred(); + fetchInfoStub.returns(deferred.promise()); + uploader.trigger('done', ev, data || {}); + return deferred; + } + + it('fetches file info', function() { + addFile(createUpload('upload.txt', '/subdir')); + expect(fetchInfoStub.calledOnce).toEqual(true); + expect(fetchInfoStub.getCall(0).args[0]).toEqual('upload.txt'); + expect(fetchInfoStub.getCall(0).args[1]).toEqual('/subdir'); + }); + it('highlights all uploaded files after all fetches are done', function() { + var highlightStub = sinon.stub(fileList, 'highlightFiles'); + var def1 = addFile(createUpload('upload.txt', '/subdir')); + var def2 = addFile(createUpload('upload2.txt', '/subdir')); + var def3 = addFile(createUpload('upload3.txt', '/another')); + uploader.trigger('stop', {}); + + expect(highlightStub.notCalled).toEqual(true); + def1.resolve(); + expect(highlightStub.notCalled).toEqual(true); + def2.resolve(); + def3.resolve(); + expect(highlightStub.calledOnce).toEqual(true); + expect(highlightStub.getCall(0).args[0]).toEqual(['upload.txt', 'upload2.txt']); + + highlightStub.restore(); + }); + it('queries storage stats', function() { + var statStub = sinon.stub(fileList, 'updateStorageStatistics'); + addFile(createUpload('upload.txt', '/subdir')); + expect(statStub.notCalled).toEqual(true); + uploader.trigger('stop', {}); + expect(statStub.calledOnce).toEqual(true); + statStub.restore(); + }); + }); }); describe('Handling errors', function () { var deferredList; diff --git a/apps/files_sharing/js/files_drop.js b/apps/files_sharing/js/files_drop.js index 984eb06b9e3..64051844d03 100644 --- a/apps/files_sharing/js/files_drop.js +++ b/apps/files_sharing/js/files_drop.js @@ -22,50 +22,70 @@ _template: undefined, initialize: function () { + + var filesClient = new OC.Files.Client({ + host: OC.getHost(), + port: OC.getPort(), + userName: $('#sharingToken').val(), + // note: password not be required, the endpoint + // will recognize previous validation from the session + root: OC.getRootPath() + '/public.php/webdav', + useHTTPS: OC.getProtocol() === 'https' + }); + $(document).bind('drop dragover', function (e) { // Prevent the default browser drop action: e.preventDefault(); }); var output = this.template(); $('#public-upload').fileupload({ - url: OC.linkTo('files', 'ajax/upload.php'), - dataType: 'json', + type: 'PUT', dropZone: $('#public-upload'), - formData: { - dirToken: $('#sharingToken').val() - }, + sequentialUploads: true, add: function(e, data) { var errors = []; - if(data.files[0]['size'] && data.files[0]['size'] > $('#maxFilesizeUpload').val()) { - errors.push('File is too big'); + + var name = data.files[0].name; + + var base = OC.getProtocol() + '://' + OC.getHost(); + data.url = base + OC.getRootPath() + '/public.php/webdav/' + encodeURI(name); + + data.multipart = false; + + if (!data.headers) { + data.headers = {}; + } + + var userName = filesClient.getUserName(); + var password = filesClient.getPassword(); + if (userName) { + // copy username/password from DAV client + data.headers['Authorization'] = + 'Basic ' + btoa(userName + ':' + (password || '')); } $('#drop-upload-done-indicator').addClass('hidden'); $('#drop-upload-progress-indicator').removeClass('hidden'); _.each(data['files'], function(file) { - if(errors.length === 0) { - $('#public-upload ul').append(output({isUploading: true, name: escapeHTML(file.name)})); - $('[data-toggle="tooltip"]').tooltip(); - data.submit(); - } else { - OC.Notification.showTemporary(OC.L10N.translate('files_sharing', 'Could not upload "{filename}"', {filename: file.name})); - $('#public-upload ul').append(output({isUploading: false, name: escapeHTML(file.name)})); - $('[data-toggle="tooltip"]').tooltip(); - } + $('#public-upload ul').append(output({isUploading: true, name: escapeHTML(file.name)})); + $('[data-toggle="tooltip"]').tooltip(); + data.submit(); }); + + return true; }, - success: function (response) { - if(response.status !== 'error') { - var mimeTypeUrl = OC.MimeType.getIconUrl(response['mimetype']); - $('#public-upload ul li[data-name="' + escapeHTML(response['filename']) + '"]').html('<img src="' + escapeHTML(mimeTypeUrl) + '"/> ' + escapeHTML(response['filename'])); + done: function(e, data) { + // Created + if (data.jqXHR.status === 201) { + var mimeTypeUrl = OC.MimeType.getIconUrl(data.files[0].type); + $('#public-upload ul li[data-name="' + escapeHTML(data.files[0].name) + '"]').html('<img src="' + escapeHTML(mimeTypeUrl) + '"/> ' + escapeHTML(data.files[0].name)); $('[data-toggle="tooltip"]').tooltip(); } else { - var name = response[0]['data']['filename']; + var name = data.files[0].name; OC.Notification.showTemporary(OC.L10N.translate('files_sharing', 'Could not upload "{filename}"', {filename: name})); $('#public-upload ul li[data-name="' + escapeHTML(name) + '"]').html(output({isUploading: false, name: escapeHTML(name)})); $('[data-toggle="tooltip"]').tooltip(); } - }, progressall: function (e, data) { var progress = parseInt(data.loaded / data.total * 100, 10); diff --git a/apps/files_sharing/js/public.js b/apps/files_sharing/js/public.js index 0dfff235998..0045c7156d2 100644 --- a/apps/files_sharing/js/public.js +++ b/apps/files_sharing/js/public.js @@ -72,7 +72,8 @@ OCA.Sharing.PublicApp = { folderDropOptions: folderDropOptions, fileActions: fileActions, detailsViewEnabled: false, - filesClient: filesClient + filesClient: filesClient, + enableUpload: true } ); this.files = OCA.Files.Files; @@ -170,6 +171,30 @@ OCA.Sharing.PublicApp = { return OC.generateUrl('/s/' + token + '/download') + '?' + OC.buildQueryString(params); }; + this.fileList.getUploadUrl = function(fileName, dir) { + if (_.isUndefined(dir)) { + dir = this.getCurrentDirectory(); + } + + var pathSections = dir.split('/'); + if (!_.isUndefined(fileName)) { + pathSections.push(fileName); + } + var encodedPath = ''; + _.each(pathSections, function(section) { + if (section !== '') { + encodedPath += '/' + encodeURIComponent(section); + } + }); + var base = ''; + + if (!this._uploader.isXHRUpload()) { + // also add auth in URL due to POST workaround + base = OC.getProtocol() + '://' + token + '@' + OC.getHost() + (OC.getPort() ? ':' + OC.getPort() : ''); + } + return base + OC.getRootPath() + '/public.php/webdav' + encodedPath; + }; + this.fileList.getAjaxUrl = function (action, params) { params = params || {}; params.t = token; @@ -203,20 +228,12 @@ OCA.Sharing.PublicApp = { OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments); }; - var file_upload_start = $('#file_upload_start'); - file_upload_start.on('fileuploadadd', function (e, data) { - var fileDirectory = ''; - if (typeof data.files[0].relativePath !== 'undefined') { - fileDirectory = data.files[0].relativePath; + this.fileList._uploader.on('fileuploadadd', function(e, data) { + if (!data.headers) { + data.headers = {}; } - // Add custom data to the upload handler - data.formData = { - requesttoken: $('#publicUploadRequestToken').val(), - dirToken: $('#dirToken').val(), - subdir: data.targetDir || self.fileList.getCurrentDirectory(), - file_directory: fileDirectory - }; + data.headers.Authorization = 'Basic ' + btoa(token + ':'); }); // do not allow sharing from the public page diff --git a/apps/files_sharing/tests/js/publicAppSpec.js b/apps/files_sharing/tests/js/publicAppSpec.js index 58565744882..e8ec9899ecc 100644 --- a/apps/files_sharing/tests/js/publicAppSpec.js +++ b/apps/files_sharing/tests/js/publicAppSpec.js @@ -87,10 +87,18 @@ describe('OCA.Sharing.PublicApp tests', function() { }); it('Uses public webdav endpoint', function() { + App._initialized = false; + fakeServer.restore(); + window.fakeServer = sinon.fakeServer.create(); + + // uploader function messes up with fakeServer + var uploaderDetectStub = sinon.stub(OC.Uploader.prototype, '_supportAjaxUploadWithProgress'); + App.initialize($('#preview')); expect(fakeServer.requests.length).toEqual(1); expect(fakeServer.requests[0].method).toEqual('PROPFIND'); expect(fakeServer.requests[0].url).toEqual('https://example.com:9876/owncloud/public.php/webdav/subdir'); expect(fakeServer.requests[0].requestHeaders.Authorization).toEqual('Basic c2g0dG9rOm51bGw='); + uploaderDetectStub.restore(); }); describe('Download Url', function() { @@ -118,5 +126,20 @@ describe('OCA.Sharing.PublicApp tests', function() { .toEqual(OC.webroot + '/index.php/apps/files_sharing/ajax/test.php?a=1&b=x%20y&t=sh4tok'); }); }); + describe('Upload Url', function() { + var fileList; + + beforeEach(function() { + fileList = App.fileList; + }); + it('returns correct upload URL', function() { + expect(fileList.getUploadUrl('some file.txt')) + .toEqual('/owncloud/public.php/webdav/subdir/some%20file.txt'); + }); + it('returns correct upload URL with specified dir', function() { + expect(fileList.getUploadUrl('some file.txt', 'sub')) + .toEqual('/owncloud/public.php/webdav/sub/some%20file.txt'); + }); + }); }); }); |