Merge pull request #1283 from nextcloud/us_files-ui-webdav-upload

Use Webdav PUT for uploads
This commit is contained in:
Lukas Reschke 2016-10-25 10:31:03 +02:00 committed by GitHub
commit 79706e0ddc
20 changed files with 1579 additions and 880 deletions

View File

@ -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();

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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))));
}

View File

@ -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' => '.*'))

View File

@ -93,6 +93,7 @@
direction: $('#defaultFileSortingDirection').val()
},
config: this._filesConfig,
enableUpload: true
}
);
this.files.initialize();

File diff suppressed because it is too large Load Diff

View File

@ -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) {
@ -188,6 +189,11 @@
_dragOptions: null,
_folderDropOptions: null,
/**
* @type OC.Uploader
*/
_uploader: null,
/**
* Initialize the file list and its components
*
@ -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})
);
@ -2223,6 +2241,60 @@
return promise;
},
/**
* 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
*
@ -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;
}
// we are dropping somewhere inside the file list, which will
// upload the file to the current directory
data.targetDir = self.getCurrentDirectory();
}
});
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
}
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;
}
var uploadText = self.$fileList.find('tr .uploadtext');
self.showFileBusyState(uploadText.closest('tr'), false);
uploadText.fadeOut();
uploadText.attr('currentUploads', 0);
});
fileUploadStart.on('fileuploadstop', function() {
OC.Upload.log('filelist handle fileuploadstop');
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);
});
uploader.on('fail', function(e, data) {
self._uploader.log('filelist handle fileuploadfail', e, data);
self._uploads = [];
//if user pressed cancel hide upload chrome
//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();
});
fileUploadStart.on('fileuploadfail', function(e, data) {
OC.Upload.log('filelist handle fileuploadfail', e, data);
//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);
}
self.updateStorageStatistics();
});

View File

@ -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) {

View File

@ -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'))?>">

View File

@ -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);
});
});
});

View File

@ -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;

View File

@ -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);

View File

@ -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

View File

@ -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');
});
});
});
});

View File

@ -729,8 +729,47 @@
*/
addFileInfoParser: function(parserFunction) {
this._fileInfoParsers.push(parserFunction);
}
},
/**
* Returns the dav.Client instance used internally
*
* @since 9.2
* @return {dav.Client}
*/
getClient: function() {
return this._client;
},
/**
* Returns the user name
*
* @since 9.2
* @return {String} userName
*/
getUserName: function() {
return this._client.userName;
},
/**
* Returns the password
*
* @since 9.2
* @return {String} password
*/
getPassword: function() {
return this._client.password;
},
/**
* Returns the base URL
*
* @since 9.2
* @return {String} base URL
*/
getBaseUrl: function() {
return this._client.baseUrl;
}
};
/**

View File

@ -422,6 +422,28 @@ var OC={
return path.replace(/\\/g,'/').replace(/\/[^\/]*$/, '');
},
/**
* Returns whether the given paths are the same, without
* leading, trailing or doubled slashes and also removing
* the dot sections.
*
* @param {String} path1 first path
* @param {String} path2 second path
* @return {bool} true if the paths are the same
*
* @since 9.0
*/
isSamePath: function(path1, path2) {
var filterDot = function(p) {
return p !== '.';
};
var pathSections1 = _.filter((path1 || '').split('/'), filterDot);
var pathSections2 = _.filter((path2 || '').split('/'), filterDot);
path1 = OC.joinPaths.apply(OC, pathSections1);
path2 = OC.joinPaths.apply(OC, pathSections2);
return path1 === path2;
},
/**
* Join path sections
*

View File

@ -188,6 +188,71 @@ describe('Core base tests', function() {
expect(OC.joinPaths('/', '//', '/')).toEqual('/');
});
});
describe('isSamePath', function() {
it('recognizes empty paths are equal', function() {
expect(OC.isSamePath('', '')).toEqual(true);
expect(OC.isSamePath('/', '')).toEqual(true);
expect(OC.isSamePath('//', '')).toEqual(true);
expect(OC.isSamePath('/', '/')).toEqual(true);
expect(OC.isSamePath('/', '//')).toEqual(true);
});
it('recognizes path with single sections as equal regardless of extra slashes', function() {
expect(OC.isSamePath('abc', 'abc')).toEqual(true);
expect(OC.isSamePath('/abc', 'abc')).toEqual(true);
expect(OC.isSamePath('//abc', 'abc')).toEqual(true);
expect(OC.isSamePath('abc', '/abc')).toEqual(true);
expect(OC.isSamePath('abc/', 'abc')).toEqual(true);
expect(OC.isSamePath('abc/', 'abc/')).toEqual(true);
expect(OC.isSamePath('/abc/', 'abc/')).toEqual(true);
expect(OC.isSamePath('/abc/', '/abc/')).toEqual(true);
expect(OC.isSamePath('//abc/', '/abc/')).toEqual(true);
expect(OC.isSamePath('//abc//', '/abc/')).toEqual(true);
expect(OC.isSamePath('abc', 'def')).toEqual(false);
expect(OC.isSamePath('/abc', 'def')).toEqual(false);
expect(OC.isSamePath('//abc', 'def')).toEqual(false);
expect(OC.isSamePath('abc', '/def')).toEqual(false);
expect(OC.isSamePath('abc/', 'def')).toEqual(false);
expect(OC.isSamePath('abc/', 'def/')).toEqual(false);
expect(OC.isSamePath('/abc/', 'def/')).toEqual(false);
expect(OC.isSamePath('/abc/', '/def/')).toEqual(false);
expect(OC.isSamePath('//abc/', '/def/')).toEqual(false);
expect(OC.isSamePath('//abc//', '/def/')).toEqual(false);
});
it('recognizes path with multiple sections as equal regardless of extra slashes', function() {
expect(OC.isSamePath('abc/def', 'abc/def')).toEqual(true);
expect(OC.isSamePath('/abc/def', 'abc/def')).toEqual(true);
expect(OC.isSamePath('abc/def', '/abc/def')).toEqual(true);
expect(OC.isSamePath('abc/def/', '/abc/def/')).toEqual(true);
expect(OC.isSamePath('/abc/def/', '/abc/def/')).toEqual(true);
expect(OC.isSamePath('/abc/def/', 'abc/def/')).toEqual(true);
expect(OC.isSamePath('//abc/def/', 'abc/def/')).toEqual(true);
expect(OC.isSamePath('//abc/def//', 'abc/def/')).toEqual(true);
expect(OC.isSamePath('abc/def', 'abc/ghi')).toEqual(false);
expect(OC.isSamePath('/abc/def', 'abc/ghi')).toEqual(false);
expect(OC.isSamePath('abc/def', '/abc/ghi')).toEqual(false);
expect(OC.isSamePath('abc/def/', '/abc/ghi/')).toEqual(false);
expect(OC.isSamePath('/abc/def/', '/abc/ghi/')).toEqual(false);
expect(OC.isSamePath('/abc/def/', 'abc/ghi/')).toEqual(false);
expect(OC.isSamePath('//abc/def/', 'abc/ghi/')).toEqual(false);
expect(OC.isSamePath('//abc/def//', 'abc/ghi/')).toEqual(false);
});
it('recognizes path entries with dot', function() {
expect(OC.isSamePath('.', '')).toEqual(true);
expect(OC.isSamePath('.', '.')).toEqual(true);
expect(OC.isSamePath('.', '/')).toEqual(true);
expect(OC.isSamePath('/.', '/')).toEqual(true);
expect(OC.isSamePath('/./', '/')).toEqual(true);
expect(OC.isSamePath('/./', '/.')).toEqual(true);
expect(OC.isSamePath('/./', '/./')).toEqual(true);
expect(OC.isSamePath('/./', '/./')).toEqual(true);
expect(OC.isSamePath('a/./b', 'a/b')).toEqual(true);
expect(OC.isSamePath('a/b/.', 'a/b')).toEqual(true);
expect(OC.isSamePath('./a/b', 'a/b')).toEqual(true);
});
});
describe('filePath', function() {
beforeEach(function() {
OC.webroot = 'http://localhost';

View File

@ -78,6 +78,14 @@ class PermissionsMask extends Wrapper {
}
public function rename($path1, $path2) {
$p = strpos($path1, $path2);
if ($p === 0) {
$part = substr($path1, strlen($path2));
//This is a rename of the transfer file to the original file
if (strpos($part, '.ocTransferId') === 0) {
return $this->checkMask(Constants::PERMISSION_CREATE) and parent::rename($path1, $path2);
}
}
return $this->checkMask(Constants::PERMISSION_UPDATE) and parent::rename($path1, $path2);
}