diff --git a/apps/files/css/files.css b/apps/files/css/files.css
index 748bc2e63a1..ced2006ec06 100644
--- a/apps/files/css/files.css
+++ b/apps/files/css/files.css
@@ -23,7 +23,9 @@
#new>ul>li>p { cursor:pointer; }
#new>ul>li>form>input { padding:0.3em; margin:-0.3em; }
-#upload {
+#trash { height:17px; margin:0 0 0 1em; z-index:1010; position:absolute; right:13.5em; }
+
+#upload {
height:27px; padding:0; margin-left:0.2em; overflow:hidden;
}
#upload a {
@@ -109,6 +111,7 @@ table td.filename form { font-size:.85em; margin-left:3em; margin-right:3em; }
}
#fileList .fileactions a.action img { position:relative; top:.2em; }
#fileList a.action { display:inline; margin:-.5em 0; padding:1em .5em 1em .5em !important; }
+#fileList img.move2trash { display:inline; margin:-.5em 0; padding:1em .5em 1em .5em !important; float:right; }
a.action.delete { float:right; }
a.action>img { max-height:16px; max-width:16px; vertical-align:text-bottom; }
.selectedActions { display:none; float:right; }
diff --git a/apps/files/index.php b/apps/files/index.php
index d2cd76a7106..7e767cc8a4f 100644
--- a/apps/files/index.php
+++ b/apps/files/index.php
@@ -131,9 +131,10 @@ if ($needUpgrade) {
$tmpl->assign('isCreatable', \OC\Files\Filesystem::isCreatable($dir . '/'));
$tmpl->assign('permissions', $permissions);
$tmpl->assign('files', $files);
+ $tmpl->assign('trash', \OCP\App::isEnabled('files_trashbin'));
$tmpl->assign('uploadMaxFilesize', $maxUploadFilesize);
$tmpl->assign('uploadMaxHumanFilesize', OCP\Util::humanFileSize($maxUploadFilesize));
$tmpl->assign('allowZipDownload', intval(OCP\Config::getSystemValue('allowZipDownload', true)));
$tmpl->assign('usedSpacePercent', (int)$storageInfo['relative']);
$tmpl->printPage();
-}
+}
\ No newline at end of file
diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js
index f5ee363a4c8..c30f1bcddd8 100644
--- a/apps/files/js/fileactions.js
+++ b/apps/files/js/fileactions.js
@@ -147,15 +147,19 @@ $(document).ready(function () {
} else {
var downloadScope = 'file';
}
- FileActions.register(downloadScope, 'Download', OC.PERMISSION_READ, function () {
- return OC.imagePath('core', 'actions/download');
- }, function (filename) {
- window.location = OC.filePath('files', 'ajax', 'download.php') + '?files=' + encodeURIComponent(filename) + '&dir=' + encodeURIComponent($('#dir').val());
- });
-
+
+ if (typeof disableDownloadActions == 'undefined' || !disableDownloadActions) {
+ FileActions.register(downloadScope, 'Download', OC.PERMISSION_READ, function () {
+ return OC.imagePath('core', 'actions/download');
+ }, function (filename) {
+ window.location = OC.filePath('files', 'ajax', 'download.php') + '?files=' + encodeURIComponent(filename) + '&dir=' + encodeURIComponent($('#dir').val());
+ });
+ }
+
$('#fileList tr').each(function(){
FileActions.display($(this).children('td.filename'));
});
+
});
FileActions.register('all', 'Delete', OC.PERMISSION_DELETE, function () {
@@ -185,6 +189,7 @@ FileActions.register('all', 'Rename', OC.PERMISSION_UPDATE, function () {
FileList.rename(filename);
});
+
FileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename) {
window.location = OC.linkTo('files', 'index.php') + '?dir=' + encodeURIComponent($('#dir').val()).replace(/%2F/g, '/') + '/' + encodeURIComponent(filename);
});
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index 04b7d92e2c3..d0810f70145 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -271,65 +271,39 @@ var FileList={
}
},
do_delete:function(files){
+ if(files.substr){
+ files=[files];
+ }
+ for (var i in files) {
+ var deleteAction = $('tr').filterAttr('data-file',files[i]).children("td.date").children(".action.delete");
+ var oldHTML = deleteAction[0].outerHTML;
+ var newHTML = '
';
+ deleteAction[0].outerHTML = newHTML;
+ }
// Finish any existing actions
if (FileList.lastAction) {
FileList.lastAction();
}
- FileList.prepareDeletion(files);
-
- if (!FileList.useUndo) {
- FileList.lastAction();
- } else {
- // NOTE: Temporary fix to change the text to unshared for files in root of Shared folder
- if ($('#dir').val() == '/Shared') {
- OC.Notification.showHtml(t('files', 'unshared {files}', {'files': escapeHTML(files)})+''+t('files', 'undo')+'');
- } else {
- OC.Notification.showHtml(t('files', 'deleted {files}', {'files': escapeHTML(files)})+''+t('files', 'undo')+'');
- }
- }
- },
- finishDelete:function(ready,sync){
- if(!FileList.deleteCanceled && FileList.deleteFiles){
- var fileNames=JSON.stringify(FileList.deleteFiles);
- $.ajax({
- url: OC.filePath('files', 'ajax', 'delete.php'),
- async:!sync,
- type:'post',
- data: {dir:$('#dir').val(),files:fileNames},
- complete: function(data){
- boolOperationFinished(data, function(){
- OC.Notification.hide();
- $.each(FileList.deleteFiles,function(index,file){
- FileList.remove(file);
+ var fileNames = JSON.stringify(files);
+ $.post(OC.filePath('files', 'ajax', 'delete.php'),
+ {dir:$('#dir').val(),files:fileNames},
+ function(result){
+ if (result.status == 'success') {
+ $.each(files,function(index,file){
+ var files = $('tr').filterAttr('data-file',file);
+ files.hide();
+ files.find('input[type="checkbox"]').removeAttr('checked');
+ files.removeClass('selected');
});
- FileList.deleteCanceled=true;
- FileList.deleteFiles=null;
- FileList.lastAction = null;
- if(ready){
- ready();
- }
- });
- }
- });
- }
- },
- prepareDeletion:function(files){
- if(files.substr){
- files=[files];
- }
- $.each(files,function(index,file){
- var files = $('tr').filterAttr('data-file',file);
- files.hide();
- files.find('input[type="checkbox"]').removeAttr('checked');
- files.removeClass('selected');
- });
- procesSelection();
- FileList.deleteCanceled=false;
- FileList.deleteFiles=files;
- FileList.lastAction = function() {
- FileList.finishDelete(null, true);
- };
+ procesSelection();
+ } else {
+ $.each(files,function(index,file) {
+ var deleteAction = $('tr').filterAttr('data-file',file).children("td.date").children(".move2trash");
+ deleteAction[0].outerHTML = oldHTML;
+ });
+ }
+ });
}
};
diff --git a/apps/files/js/files.js b/apps/files/js/files.js
index 8e0399b0e0c..3d09d6aa2b6 100644
--- a/apps/files/js/files.js
+++ b/apps/files/js/files.js
@@ -114,6 +114,11 @@ $(document).ready(function() {
$(this).parent().children('#file_upload_start').trigger('click');
return false;
});
+
+ // Show trash bin
+ $('#trash a').live('click', function() {
+ window.location=OC.filePath('files_trashbin', '', 'index.php');
+ });
var lastChecked;
diff --git a/apps/files/templates/index.php b/apps/files/templates/index.php
index b66b523ae38..2d4ed9ab2d9 100644
--- a/apps/files/templates/index.php
+++ b/apps/files/templates/index.php
@@ -35,6 +35,11 @@
+
+
';
+ var undeleteAction = $('tr').filterAttr('data-file',filename).children("td.date");
+ undeleteAction[0].innerHTML = undeleteAction[0].innerHTML+spinner;
+ $.post(OC.filePath('files_trashbin','ajax','undelete.php'),
+ {files:tr.attr('data-file'), dirlisting:tr.attr('data-dirlisting') },
+ function(result){
+ for (var i = 0; i < result.data.success.length; i++) {
+ var row = document.getElementById(result.data.success[i].filename);
+ row.parentNode.removeChild(row);
+ }
+ if (result.status != 'success') {
+ OC.dialogs.alert(result.data.message, 'Error');
+ }
+ });
+
+ });
+ };
+
+ // Sets the select_all checkbox behaviour :
+ $('#select_all').click(function() {
+ if($(this).attr('checked')){
+ // Check all
+ $('td.filename input:checkbox').attr('checked', true);
+ $('td.filename input:checkbox').parent().parent().addClass('selected');
+ }else{
+ // Uncheck all
+ $('td.filename input:checkbox').attr('checked', false);
+ $('td.filename input:checkbox').parent().parent().removeClass('selected');
+ }
+ processSelection();
+ });
+
+ $('td.filename input:checkbox').live('change',function(event) {
+ if (event.shiftKey) {
+ var last = $(lastChecked).parent().parent().prevAll().length;
+ var first = $(this).parent().parent().prevAll().length;
+ var start = Math.min(first, last);
+ var end = Math.max(first, last);
+ var rows = $(this).parent().parent().parent().children('tr');
+ for (var i = start; i < end; i++) {
+ $(rows).each(function(index) {
+ if (index == i) {
+ var checkbox = $(this).children().children('input:checkbox');
+ $(checkbox).attr('checked', 'checked');
+ $(checkbox).parent().parent().addClass('selected');
+ }
+ });
+ }
+ }
+ var selectedCount=$('td.filename input:checkbox:checked').length;
+ $(this).parent().parent().toggleClass('selected');
+ if(!$(this).attr('checked')){
+ $('#select_all').attr('checked',false);
+ }else{
+ if(selectedCount==$('td.filename input:checkbox').length){
+ $('#select_all').attr('checked',true);
+ }
+ }
+ processSelection();
+ });
+
+ $('.undelete').click('click',function(event) {
+ var spinner = '
';
+ var files=getSelectedFiles('file');
+ var fileslist=files.join(';');
+ var dirlisting=getSelectedFiles('dirlisting')[0];
+
+ for (var i in files) {
+ var undeleteAction = $('tr').filterAttr('data-file',files[i]).children("td.date");
+ undeleteAction[0].innerHTML = undeleteAction[0].innerHTML+spinner;
+ }
+
+ $.post(OC.filePath('files_trashbin','ajax','undelete.php'),
+ {files:fileslist, dirlisting:dirlisting},
+ function(result){
+ for (var i = 0; i < result.data.success.length; i++) {
+ var row = document.getElementById(result.data.success[i].filename);
+ row.parentNode.removeChild(row);
+ }
+ if (result.status != 'success') {
+ OC.dialogs.alert(result.data.message, 'Error');
+ }
+ });
+ });
+
+
+});
+
+function processSelection(){
+ var selected=getSelectedFiles();
+ var selectedFiles=selected.filter(function(el){return el.type=='file'});
+ var selectedFolders=selected.filter(function(el){return el.type=='dir'});
+ if(selectedFiles.length==0 && selectedFolders.length==0) {
+ $('#headerName>span.name').text(t('files','Name'));
+ $('#modified').text(t('files','Deleted'));
+ $('table').removeClass('multiselect');
+ $('.selectedActions').hide();
+ }
+ else {
+ $('.selectedActions').show();
+ var selection='';
+ if(selectedFolders.length>0){
+ if(selectedFolders.length==1){
+ selection+=t('files','1 folder');
+ }else{
+ selection+=t('files','{count} folders',{count: selectedFolders.length});
+ }
+ if(selectedFiles.length>0){
+ selection+=' & ';
+ }
+ }
+ if(selectedFiles.length>0){
+ if(selectedFiles.length==1){
+ selection+=t('files','1 file');
+ }else{
+ selection+=t('files','{count} files',{count: selectedFiles.length});
+ }
+ }
+ $('#headerName>span.name').text(selection);
+ $('#modified').text('');
+ $('table').addClass('multiselect');
+ }
+}
+
+/**
+ * @brief get a list of selected files
+ * @param string property (option) the property of the file requested
+ * @return array
+ *
+ * possible values for property: name, mime, size and type
+ * if property is set, an array with that property for each file is returnd
+ * if it's ommited an array of objects with all properties is returned
+ */
+function getSelectedFiles(property){
+ var elements=$('td.filename input:checkbox:checked').parent().parent();
+ var files=[];
+ elements.each(function(i,element){
+ var file={
+ name:$(element).attr('data-filename'),
+ file:$(element).attr('data-file'),
+ timestamp:$(element).attr('data-timestamp'),
+ type:$(element).attr('data-type'),
+ dirlisting:$(element).attr('data-dirlisting')
+ };
+ if(property){
+ files.push(file[property]);
+ }else{
+ files.push(file);
+ }
+ });
+ return files;
+}
\ No newline at end of file
diff --git a/apps/files_trashbin/lib/hooks.php b/apps/files_trashbin/lib/hooks.php
new file mode 100644
index 00000000000..d3bee105b51
--- /dev/null
+++ b/apps/files_trashbin/lib/hooks.php
@@ -0,0 +1,45 @@
+.
+ *
+ */
+
+/**
+ * This class contains all hooks.
+ */
+
+namespace OCA_Trash;
+
+class Hooks {
+
+ /**
+ * @brief Copy files to trash bin
+ * @param array
+ *
+ * This function is connected to the delete signal of OC_Filesystem
+ * to copy the file to the trash bin
+ */
+ public static function remove_hook($params) {
+
+ if ( \OCP\App::isEnabled('files_trashbin') ) {
+ $path = $params['path'];
+ Trashbin::move2trash($path);
+ }
+ }
+}
diff --git a/apps/files_trashbin/lib/trash.php b/apps/files_trashbin/lib/trash.php
new file mode 100644
index 00000000000..a7eff3d44e0
--- /dev/null
+++ b/apps/files_trashbin/lib/trash.php
@@ -0,0 +1,264 @@
+.
+ *
+ */
+
+namespace OCA_Trash;
+
+class Trashbin {
+
+ const DEFAULT_RETENTION_OBLIGATION=180; // how long do we keep files in the trash bin if no other value is defined in the config file (unit: days)
+ /**
+ * move file to the trash bin
+ *
+ * @param $file_path path to the deleted file/directory relative to the files root directory
+ */
+ public static function move2trash($file_path) {
+ $user = \OCP\User::getUser();
+ $view = new \OC_FilesystemView('/'. $user);
+ if (!$view->is_dir('files_trashbin')) {
+ $view->mkdir('files_trashbin');
+ $view->mkdir("versions_trashbin");
+ }
+
+ $path_parts = pathinfo($file_path);
+
+ $deleted = $path_parts['basename'];
+ $location = $path_parts['dirname'];
+ $timestamp = time();
+ $mime = $view->getMimeType('files'.$file_path);
+
+ if ( $view->is_dir('files'.$file_path) ) {
+ $type = 'dir';
+ } else {
+ $type = 'file';
+ }
+
+ self::copy_recursive($file_path, 'files_trashbin/'.$deleted.'.d'.$timestamp, $view);
+
+ if ( $view->file_exists('files_trashbin/'.$deleted.'.d'.$timestamp) ) {
+ $query = \OC_DB::prepare("INSERT INTO *PREFIX*files_trash (id,timestamp,location,type,mime,user) VALUES (?,?,?,?,?,?)");
+ $result = $query->execute(array($deleted, $timestamp, $location, $type, $mime, $user));
+ if ( !$result ) { // if file couldn't be added to the database than also don't store it in the trash bin.
+ $view->deleteAll('files_trashbin/'.$deleted.'.d'.$timestamp);
+ \OC_Log::write('files_trashbin', 'trash bin database couldn\'t be updated', \OC_log::ERROR);
+ return;
+ }
+
+ if ( \OCP\App::isEnabled('files_versions') ) {
+ if ( $view->is_dir('files_versions'.$file_path) ) {
+ $view->rename('files_versions'.$file_path, 'versions_trashbin/'. $deleted.'.d'.$timestamp);
+ } else if ( $versions = \OCA_Versions\Storage::getVersions($file_path) ) {
+ foreach ($versions as $v) {
+ $view->rename('files_versions'.$v['path'].'.v'.$v['version'], 'versions_trashbin/'. $deleted.'.v'.$v['version'].'.d'.$timestamp);
+ }
+ }
+ }
+ } else {
+ \OC_Log::write('files_trashbin', 'Couldn\'t move '.$file_path.' to the trash bin' , \OC_log::ERROR);
+ }
+
+ self::expire();
+ }
+
+
+ /**
+ * restore files from trash bin
+ * @param $file path to the deleted file
+ * @param $filename name of the file
+ * @param $timestamp time when the file was deleted
+ */
+ public static function restore($file, $filename, $timestamp) {
+
+ $user = \OCP\User::getUser();
+ $view = new \OC_FilesystemView('/'.$user);
+
+ if ( $timestamp ) {
+ $query = \OC_DB::prepare('SELECT location,type FROM *PREFIX*files_trash WHERE user=? AND id=? AND timestamp=?');
+ $result = $query->execute(array($user,$filename,$timestamp))->fetchAll();
+ if ( count($result) != 1 ) {
+ \OC_Log::write('files_trashbin', 'trash bin database inconsistent!', \OC_Log::ERROR);
+ return false;
+ }
+
+ // if location no longer exists, restore file in the root directory
+ $location = $result[0]['location'];
+ if ( $result[0]['location'] != '/' &&
+ (!$view->is_dir('files'.$result[0]['location']) ||
+ !$view->isUpdatable('files'.$result[0]['location'])) ) {
+ $location = '';
+ }
+ } else {
+ $path_parts = pathinfo($filename);
+ $result[] = array(
+ 'location' => $path_parts['dirname'],
+ 'type' => $view->is_dir('/files_trashbin/'.$file) ? 'dir' : 'files',
+ );
+ $location = '';
+ }
+
+ $source = \OC_Filesystem::normalizePath('files_trashbin/'.$file);
+ $target = \OC_Filesystem::normalizePath('files/'.$location.'/'.$filename);
+
+ // we need a extension in case a file/dir with the same name already exists
+ $ext = self::getUniqueExtension($location, $filename, $view);
+ $mtime = $view->filemtime($source);
+ if( $view->rename($source, $target.$ext) ) {
+ $view->touch($target.$ext, $mtime);
+ // if versioning app is enabled, copy versions from the trash bin back to the original location
+ if ( \OCP\App::isEnabled('files_versions') ) {
+ if ( $result[0]['type'] == 'dir' ) {
+ $view->rename(\OC_Filesystem::normalizePath('versions_trashbin/'. $file), \OC_Filesystem::normalizePath('files_versions/'.$location.'/'.$filename.$ext));
+ } else if ( $versions = self::getVersionsFromTrash($file, $timestamp) ) {
+ foreach ($versions as $v) {
+ if ($timestamp ) {
+ $view->rename('versions_trashbin/'.$filename.'.v'.$v.'.d'.$timestamp, 'files_versions/'.$location.'/'.$filename.$ext.'.v'.$v);
+ } else {
+ $view->rename('versions_trashbin/'.$file.'.v'.$v, 'files_versions/'.$location.'/'.$filename.$ext.'.v'.$v);
+ }
+ }
+ }
+ }
+
+ if ( $timestamp ) {
+ $query = \OC_DB::prepare('DELETE FROM *PREFIX*files_trash WHERE user=? AND id=? AND timestamp=?');
+ $query->execute(array($user,$filename,$timestamp));
+ }
+
+ return true;
+ } else {
+ \OC_Log::write('files_trashbin', 'Couldn\'t restore file from trash bin, '.$filename , \OC_log::ERROR);
+ }
+
+ return false;
+ }
+
+ /**
+ * clean up the trash bin
+ */
+ private static function expire() {
+
+ $view = new \OC_FilesystemView('/'.\OCP\User::getUser());
+ $user = \OCP\User::getUser();
+
+ $query = \OC_DB::prepare('SELECT location,type,id,timestamp FROM *PREFIX*files_trash WHERE user=?');
+ $result = $query->execute(array($user))->fetchAll();
+
+ $retention_obligation = \OC_Config::getValue('trashbin_retention_obligation', self::DEFAULT_RETENTION_OBLIGATION);
+
+ $limit = time() - ($retention_obligation * 86400);
+
+ foreach ( $result as $r ) {
+ $timestamp = $r['timestamp'];
+ $filename = $r['id'];
+ if ( $r['timestamp'] < $limit ) {
+ $view->unlink('files_trashbin/'.$filename.'.d'.$timestamp);
+ if ($r['type'] == 'dir') {
+ $view->unlink('versions_trashbin/'.$filename.'.d'.$timestamp);
+ } else if ( $versions = self::getVersionsFromTrash($filename, $timestamp) ) {
+ foreach ($versions as $v) {
+ $view->unlink('versions_trashbin/'.$filename.'.v'.$v.'.d'.$timestamp);
+ }
+ }
+ }
+ }
+
+ $query = \OC_DB::prepare('DELETE FROM *PREFIX*files_trash WHERE user=? AND timestamp');
+ $query->execute(array($user,$limit));
+ }
+
+ /**
+ * recursive copy to copy a whole directory
+ *
+ * @param $source source path, relative to the users files directory
+ * @param $destination destination path relative to the users root directoy
+ * @param $view file view for the users root directory
+ */
+ private static function copy_recursive( $source, $destination, $view ) {
+ if ( $view->is_dir( 'files'.$source ) ) {
+ $view->mkdir( $destination );
+ $view->touch($destination, $view->filemtime('files'.$source));
+ foreach ( \OC_Files::getDirectoryContent($source) as $i ) {
+ $pathDir = $source.'/'.$i['name'];
+ if ( $view->is_dir('files'.$pathDir) ) {
+ self::copy_recursive($pathDir, $destination.'/'.$i['name'], $view);
+ } else {
+ $view->copy( 'files'.$pathDir, $destination . '/' . $i['name'] );
+ $view->touch($destination . '/' . $i['name'], $view->filemtime('files'.$pathDir));
+ }
+ }
+ } else {
+ $view->copy( 'files'.$source, $destination );
+ $view->touch($destination, $view->filemtime('files'.$source));
+ }
+ }
+
+ /**
+ * find all versions which belong to the file we want to restore
+ * @param $filename name of the file which should be restored
+ * @param $timestamp timestamp when the file was deleted
+ */
+ private static function getVersionsFromTrash($filename, $timestamp) {
+ $view = new \OC_FilesystemView('/'.\OCP\User::getUser().'/versions_trashbin');
+ $versionsName = \OCP\Config::getSystemValue('datadirectory').$view->getAbsolutePath($filename);
+ $versions = array();
+
+ if ($timestamp ) {
+ // fetch for old versions
+ $matches = glob( $versionsName.'.v*.d'.$timestamp );
+ $offset = -strlen($timestamp)-2;
+ } else {
+ $matches = glob( $versionsName.'.v*' );
+ }
+
+ foreach( $matches as $ma ) {
+ if ( $timestamp ) {
+ $parts = explode( '.v', substr($ma, 0, $offset) );
+ $versions[] = ( end( $parts ) );
+ } else {
+ $parts = explode( '.v', $ma );
+ $versions[] = ( end( $parts ) );
+ }
+ }
+ return $versions;
+ }
+
+ /**
+ * find unique extension for restored file if a file with the same name already exists
+ * @param $location where the file should be restored
+ * @param $filename name of the file
+ * @param $view filesystem view relative to users root directory
+ * @return string with unique extension
+ */
+ private static function getUniqueExtension($location, $filename, $view) {
+ $ext = '';
+ if ( $view->file_exists('files'.$location.'/'.$filename) ) {
+ $tmpext = '.restored';
+ $ext = $tmpext;
+ $i = 1;
+ while ( $view->file_exists('files'.$location.'/'.$filename.$ext) ) {
+ $ext = $tmpext.$i;
+ $i++;
+ }
+ }
+ return $ext;
+ }
+
+}
diff --git a/apps/files_trashbin/templates/index.php b/apps/files_trashbin/templates/index.php
new file mode 100644
index 00000000000..c3e51b4becd
--- /dev/null
+++ b/apps/files_trashbin/templates/index.php
@@ -0,0 +1,34 @@
+
+|
+
+ t( 'Name' ); ?>
+
+
+ |
+ + t( 'Deleted' ); ?> + | +
|---|