diff options
Diffstat (limited to 'kioslave/trash')
-rw-r--r-- | kioslave/trash/DESIGN | 53 | ||||
-rw-r--r-- | kioslave/trash/Makefile.am | 31 | ||||
-rw-r--r-- | kioslave/trash/kfile-plugin/Makefile.am | 14 | ||||
-rw-r--r-- | kioslave/trash/kfile-plugin/RETURNED_ITEMS | 4 | ||||
-rw-r--r-- | kioslave/trash/kfile-plugin/kfile_trash.cpp | 93 | ||||
-rw-r--r-- | kioslave/trash/kfile-plugin/kfile_trash.desktop | 79 | ||||
-rw-r--r-- | kioslave/trash/kfile-plugin/kfile_trash.h | 42 | ||||
-rw-r--r-- | kioslave/trash/kfile-plugin/kfile_trash_system.desktop | 79 | ||||
-rw-r--r-- | kioslave/trash/kio_trash.cpp | 596 | ||||
-rw-r--r-- | kioslave/trash/kio_trash.h | 71 | ||||
-rw-r--r-- | kioslave/trash/ktrash.cpp | 102 | ||||
-rw-r--r-- | kioslave/trash/testtrash.cpp | 1198 | ||||
-rw-r--r-- | kioslave/trash/testtrash.h | 118 | ||||
-rw-r--r-- | kioslave/trash/trash.protocol | 89 | ||||
-rw-r--r-- | kioslave/trash/trashimpl.cpp | 962 | ||||
-rw-r--r-- | kioslave/trash/trashimpl.h | 182 |
16 files changed, 3713 insertions, 0 deletions
diff --git a/kioslave/trash/DESIGN b/kioslave/trash/DESIGN new file mode 100644 index 000000000..63179da08 --- /dev/null +++ b/kioslave/trash/DESIGN @@ -0,0 +1,53 @@ +DESIGN +====== +kio_trash implements the XDG trash standard currently at http://www.ramendik.ru/docs/trashspec.html + +In case race conditions between the various instances of kio_trash +are a problem, trashimpl could be moved to a kded module, and +kio_trash would use DCOP to talk to it. It's a bit hard to come up +with use cases where the race conditions would matter though. + +BUGS +==== +* Undo of "restore" isn't available. Need to get origPath by metadata I guess. + +TODO +==== +* Clean up konq_popupmenu.cc for Type=Link URL=trash:/ :( +* Also, provide metainfo for trash contents for that desktop link. +=> maybe we need a new mimetype? + Like application/x-trash-desktop, inheriting application/x-desktop. + And a "trash.trashdesktop" filename or so (ouch, migration issues...) + +* Detect removeable media to avoid .Trash-foo on it. How? + +* Trashcan properties (properties for trash:/? hmm. Easier with separate dialog) + - Maximum size for trash can (#18109 suggests a %, but a MB size is easier). + This means to delete the oldest files from the trash automatically. #79553 + +* Err, should we support renaming? :) Difficult to disable... + In fact it's already not disabled in readonly directories (e.g. "/") -> todo + (for F2 and kpropertiesdialog) + +* Deleting oldest files when size is bigger than a certain configurable amount (#79553) + +Bugs closed by kio_trash +======================== +#79826 (3.3 only) +#62848 (configurable trash location) +#78116 (.directory) +#18109 (general one) +#17744 (restore) +#76380 #56821 (trashing on same partition) + +Choice of URL scheme +==================== +We use trash:/trashid-fileid[/relativepath] +This gave problems with CopyJob::startRenameJob which exposed trashid-fileid +to the user as a filename when dropping a file out of the trash. +But this was fixed with the fileNameUsedForCopying=Name setting. + +A previous experiment was trash:/filename[/relativepath]?t=trashid&id=fileid +but this gives problems with going Up (it first removes the query), +with KDirLister (it wouldn't know when to remove the query, to find the URL +of the parent directory). diff --git a/kioslave/trash/Makefile.am b/kioslave/trash/Makefile.am new file mode 100644 index 000000000..7aaa28a9f --- /dev/null +++ b/kioslave/trash/Makefile.am @@ -0,0 +1,31 @@ +INCLUDES = $(all_includes) +METASOURCES = AUTO + +SUBDIRS = . kfile-plugin + +kde_module_LTLIBRARIES = kio_trash.la + +kio_trash_la_SOURCES = kio_trash.cpp +kio_trash_la_LIBADD = libtrashcommon.la $(LIB_KIO) +kio_trash_la_LDFLAGS = $(all_libraries) -module $(KDE_PLUGIN) -no-undefined + +bin_PROGRAMS = ktrash +ktrash_SOURCES = ktrash.cpp +ktrash_LDADD = $(LIB_KIO) +ktrash_LDFLAGS = $(all_libraries) $(KDE_RPATH) + +kde_services_DATA = trash.protocol + +noinst_LTLIBRARIES = libtrashcommon.la +libtrashcommon_la_SOURCES = trashimpl.cpp + +check_PROGRAMS = testtrash +testtrash_SOURCES = testtrash.cpp +testtrash_LDADD = libtrashcommon.la $(LIB_KIO) +testtrash_LDFLAGS = $(all_libraries) + +TESTS = testtrash + +messages: + $(XGETTEXT) `find . -name "*.cc" -o -name "*.cpp" -o -name "*.h"` -o $(podir)/kio_trash.pot + diff --git a/kioslave/trash/kfile-plugin/Makefile.am b/kioslave/trash/kfile-plugin/Makefile.am new file mode 100644 index 000000000..0668553a8 --- /dev/null +++ b/kioslave/trash/kfile-plugin/Makefile.am @@ -0,0 +1,14 @@ +## Makefile.am for trash file meta info plugin + +AM_CPPFLAGS = $(all_includes) + +kde_module_LTLIBRARIES = kfile_trash.la + +kfile_trash_la_SOURCES = kfile_trash.cpp +kfile_trash_la_LDFLAGS = $(all_libraries) -module $(KDE_PLUGIN) +kfile_trash_la_LIBADD = ../libtrashcommon.la $(LIB_KIO) + +METASOURCES = AUTO + +services_DATA = kfile_trash.desktop kfile_trash_system.desktop +servicesdir = $(kde_servicesdir) diff --git a/kioslave/trash/kfile-plugin/RETURNED_ITEMS b/kioslave/trash/kfile-plugin/RETURNED_ITEMS new file mode 100644 index 000000000..3e34c5b6d --- /dev/null +++ b/kioslave/trash/kfile-plugin/RETURNED_ITEMS @@ -0,0 +1,4 @@ +kfile_trash +=========== +QString OriginalPath +DateTime DateOfDeletion diff --git a/kioslave/trash/kfile-plugin/kfile_trash.cpp b/kioslave/trash/kfile-plugin/kfile_trash.cpp new file mode 100644 index 000000000..baa27143c --- /dev/null +++ b/kioslave/trash/kfile-plugin/kfile_trash.cpp @@ -0,0 +1,93 @@ +/* This file is part of the KDE project + * Copyright (C) 2004 David Faure <[email protected]> + * Based on kfile_txt.cpp by Nadeem Hasan <[email protected]> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation version 2. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + */ + +#include "kfile_trash.h" + +#include <kgenericfactory.h> +#include <kdebug.h> + +#include <qfile.h> +#include <qstringlist.h> +#include <qdatetime.h> + +typedef KGenericFactory<KTrashPlugin> TrashFactory; + +K_EXPORT_COMPONENT_FACTORY(kfile_trash, TrashFactory("kfile_trash")) + +KTrashPlugin::KTrashPlugin(QObject *parent, const char *name, + const QStringList &args) : KFilePlugin(parent, name, args) +{ + KGlobal::locale()->insertCatalogue( "kio_trash" ); + + kdDebug(7034) << "Trash file meta info plugin\n"; + + makeMimeTypeInfo("trash"); + makeMimeTypeInfo("system"); + + (void)impl.init(); +} + +void KTrashPlugin::makeMimeTypeInfo(const QString& mimeType) +{ + KFileMimeTypeInfo* info = addMimeTypeInfo( mimeType ); + + KFileMimeTypeInfo::GroupInfo* group = + addGroupInfo(info, "General", i18n("General")); + + KFileMimeTypeInfo::ItemInfo* item; + item = addItemInfo(group, "OriginalPath", i18n("Original Path"), QVariant::String); + item = addItemInfo(group, "DateOfDeletion", i18n("Date of Deletion"), QVariant::DateTime); +} + +bool KTrashPlugin::readInfo(KFileMetaInfo& info, uint) +{ + KURL url = info.url(); + + if ( url.protocol()=="system" + && url.path().startsWith("/trash") ) + { + QString path = url.path(); + path.remove(0, 6); + url.setProtocol("trash"); + url.setPath(path); + } + + //kdDebug() << k_funcinfo << info.url() << endl; + if ( url.protocol() != "trash" ) + return false; + + int trashId; + QString fileId; + QString relativePath; + if ( !TrashImpl::parseURL( url, trashId, fileId, relativePath ) ) + return false; + + TrashImpl::TrashedFileInfo trashInfo; + if ( !impl.infoForFile( trashId, fileId, trashInfo ) ) + return false; + + KFileMetaInfoGroup group = appendGroup(info, "General"); + appendItem(group, "OriginalPath", trashInfo.origPath); + appendItem(group, "DateOfDeletion", trashInfo.deletionDate); + + return true; +} + +#include "kfile_trash.moc" diff --git a/kioslave/trash/kfile-plugin/kfile_trash.desktop b/kioslave/trash/kfile-plugin/kfile_trash.desktop new file mode 100644 index 000000000..02d90422a --- /dev/null +++ b/kioslave/trash/kfile-plugin/kfile_trash.desktop @@ -0,0 +1,79 @@ +[Desktop Entry] +Type=Service +Name=Trash File Info +Name[af]=Asblik inligting +Name[ar]=معلومات مفل المهملات +Name[az]=Zibil Faylı Məlumatı +Name[be]=Інфармацыя аб файле сметніцы +Name[bg]=Информация за кошчето +Name[bn]=আবর্জনা ফাইল তথ্য +Name[br]=Titouroù diwar-benn ar pod-lastez +Name[bs]=Smeće informacije o datoteci +Name[ca]=Informació del fitxer paperera +Name[cs]=Info o koši +Name[csb]=Wëdowiédzô ò lopkù w kòszu +Name[da]=Fil-info om affald +Name[de]=Mülleimer-Information +Name[el]=Πληροφορίες για τον Κάδο Απορριμμάτων +Name[en_GB]=Wastebin File Info +Name[eo]=Rubuja informo +Name[es]=Información de la papelera +Name[et]=Prügikasti failiinfo +Name[eu]=Zakarontziaren infoa +Name[fa]=اطلاعات پروندۀ زباله +Name[fi]=Roskakorin tiedot +Name[fr]=Info Fichier Corbeille +Name[fy]=Jiskefet ynformaasje +Name[gl]=Información do Lixo +Name[he]=מידע אודות קובץ אשפה +Name[hi]=रद्दी फ़ाइल जानकारी +Name[hr]=Podaci o otpadu +Name[hu]=Szemétkosár-jellemzők +Name[is]=Upplýsingar um ruslaskrá +Name[it]=Informazioni file del cestino +Name[ja]=ごみ箱情報 +Name[ka]=ურნაში არსებული ფაილის შესახებ ცნობი +Name[kk]=Өшірілген файл мәліметі +Name[km]=ព័ត៌មានឯកសារសំរាម +Name[ko]=휴지통 파일 정보 +Name[lt]=Šiukšlių bylos informacija +Name[lv]=Atkritumu faila informācija +Name[mk]=Инфо. за датотека од Корпата +Name[ms]=Maklumat Fail Sampah +Name[mt]=Skart +Name[nb]=Søppelfilinformasjon +Name[nds]=Affalltünn-Informatschonen +Name[ne]=रद्दीटोकरी फाइल सूचना +Name[nl]=Prullenbakinformatie +Name[nn]=Søppelfilinformasjon +Name[pa]=ਰੱਦੀ ਫਾਇਲ ਜਾਣਕਾਰੀ +Name[pl]=Informacja o pliku w koszu +Name[pt]=Informações de Ficheiros no Lixo +Name[pt_BR]=Informações sobre o Arquivo de Lixo +Name[ro]=Informații fișier șters +Name[ru]=Сведения о файле в корзине +Name[rw]=Ibisobanuro byo Guta Idosiye +Name[se]=Ruskalihttedieđut +Name[sk]=Informácie o koši +Name[sl]=Informacije o Smeteh +Name[sr]=Информације о фајлу у смећу +Name[sr@Latn]=Informacije o fajlu u smeću +Name[sv]=Information om filer i papperskorgen +Name[ta]=குப்பைத்தொட்டி கோப்பு தகவல் +Name[te]=చెత్త బుట్ట దస్త్ర వివరాలు +Name[tg]=Файли ахборотии ахлотдон +Name[th]=ข้อมูลแฟ้มถังขยะ +Name[tr]=Çöp Dosya Bilgisi +Name[tt]=Taşlanğan Birem Turında +Name[uk]=Інформація про файл у смітнику +Name[uz]=Chiqindilar qutisi haqida maʼlumot +Name[uz@cyrillic]=Чиқиндилар қутиси ҳақида маълумот +Name[vi]=Thông tin về Tập tin trong Thùng rác +Name[wa]=Informåcion sol fitchî batch +Name[zh_CN]=回收站文件信息 +Name[zh_TW]=資源回收桶檔案資訊 +ServiceTypes=KFilePlugin +X-KDE-Library=kfile_trash +X-KDE-Protocol=trash +PreferredGroups=General +PreferredItems=OriginalPath,DateOfDeletion diff --git a/kioslave/trash/kfile-plugin/kfile_trash.h b/kioslave/trash/kfile-plugin/kfile_trash.h new file mode 100644 index 000000000..8316f7402 --- /dev/null +++ b/kioslave/trash/kfile-plugin/kfile_trash.h @@ -0,0 +1,42 @@ +/* This file is part of the KDE project + * Copyright (C) 2004 David Faure <[email protected]> + * Based on kfile_txt.h by Nadeem Hasan <[email protected]> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation version 2. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + */ + +#ifndef __KFILE_TRASH_H_ +#define __KFILE_TRASH_H_ + +#include <kfilemetainfo.h> +#include "../trashimpl.h" + +class QStringList; + +class KTrashPlugin: public KFilePlugin +{ + Q_OBJECT + +public: + KTrashPlugin(QObject *parent, const char *name, const QStringList& args); + virtual bool readInfo(KFileMetaInfo& info, uint what); + +private: + void makeMimeTypeInfo(const QString& mimeType); + TrashImpl impl; +}; + +#endif diff --git a/kioslave/trash/kfile-plugin/kfile_trash_system.desktop b/kioslave/trash/kfile-plugin/kfile_trash_system.desktop new file mode 100644 index 000000000..5f57b5af3 --- /dev/null +++ b/kioslave/trash/kfile-plugin/kfile_trash_system.desktop @@ -0,0 +1,79 @@ +[Desktop Entry] +Type=Service +Name=Trash File Info +Name[af]=Asblik inligting +Name[ar]=معلومات مفل المهملات +Name[az]=Zibil Faylı Məlumatı +Name[be]=Інфармацыя аб файле сметніцы +Name[bg]=Информация за кошчето +Name[bn]=আবর্জনা ফাইল তথ্য +Name[br]=Titouroù diwar-benn ar pod-lastez +Name[bs]=Smeće informacije o datoteci +Name[ca]=Informació del fitxer paperera +Name[cs]=Info o koši +Name[csb]=Wëdowiédzô ò lopkù w kòszu +Name[da]=Fil-info om affald +Name[de]=Mülleimer-Information +Name[el]=Πληροφορίες για τον Κάδο Απορριμμάτων +Name[en_GB]=Wastebin File Info +Name[eo]=Rubuja informo +Name[es]=Información de la papelera +Name[et]=Prügikasti failiinfo +Name[eu]=Zakarontziaren infoa +Name[fa]=اطلاعات پروندۀ زباله +Name[fi]=Roskakorin tiedot +Name[fr]=Info Fichier Corbeille +Name[fy]=Jiskefet ynformaasje +Name[gl]=Información do Lixo +Name[he]=מידע אודות קובץ אשפה +Name[hi]=रद्दी फ़ाइल जानकारी +Name[hr]=Podaci o otpadu +Name[hu]=Szemétkosár-jellemzők +Name[is]=Upplýsingar um ruslaskrá +Name[it]=Informazioni file del cestino +Name[ja]=ごみ箱情報 +Name[ka]=ურნაში არსებული ფაილის შესახებ ცნობი +Name[kk]=Өшірілген файл мәліметі +Name[km]=ព័ត៌មានឯកសារសំរាម +Name[ko]=휴지통 파일 정보 +Name[lt]=Šiukšlių bylos informacija +Name[lv]=Atkritumu faila informācija +Name[mk]=Инфо. за датотека од Корпата +Name[ms]=Maklumat Fail Sampah +Name[mt]=Skart +Name[nb]=Søppelfilinformasjon +Name[nds]=Affalltünn-Informatschonen +Name[ne]=रद्दीटोकरी फाइल सूचना +Name[nl]=Prullenbakinformatie +Name[nn]=Søppelfilinformasjon +Name[pa]=ਰੱਦੀ ਫਾਇਲ ਜਾਣਕਾਰੀ +Name[pl]=Informacja o pliku w koszu +Name[pt]=Informações de Ficheiros no Lixo +Name[pt_BR]=Informações sobre o Arquivo de Lixo +Name[ro]=Informații fișier șters +Name[ru]=Сведения о файле в корзине +Name[rw]=Ibisobanuro byo Guta Idosiye +Name[se]=Ruskalihttedieđut +Name[sk]=Informácie o koši +Name[sl]=Informacije o Smeteh +Name[sr]=Информације о фајлу у смећу +Name[sr@Latn]=Informacije o fajlu u smeću +Name[sv]=Information om filer i papperskorgen +Name[ta]=குப்பைத்தொட்டி கோப்பு தகவல் +Name[te]=చెత్త బుట్ట దస్త్ర వివరాలు +Name[tg]=Файли ахборотии ахлотдон +Name[th]=ข้อมูลแฟ้มถังขยะ +Name[tr]=Çöp Dosya Bilgisi +Name[tt]=Taşlanğan Birem Turında +Name[uk]=Інформація про файл у смітнику +Name[uz]=Chiqindilar qutisi haqida maʼlumot +Name[uz@cyrillic]=Чиқиндилар қутиси ҳақида маълумот +Name[vi]=Thông tin về Tập tin trong Thùng rác +Name[wa]=Informåcion sol fitchî batch +Name[zh_CN]=回收站文件信息 +Name[zh_TW]=資源回收桶檔案資訊 +ServiceTypes=KFilePlugin +X-KDE-Library=kfile_trash +X-KDE-Protocol=system +PreferredGroups=General +PreferredItems=OriginalPath,DateOfDeletion diff --git a/kioslave/trash/kio_trash.cpp b/kioslave/trash/kio_trash.cpp new file mode 100644 index 000000000..7912cbb7c --- /dev/null +++ b/kioslave/trash/kio_trash.cpp @@ -0,0 +1,596 @@ +/* This file is part of the KDE project + Copyright (C) 2004 David Faure <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "kio_trash.h" +#include <kio/job.h> + +#include <kapplication.h> +#include <kdebug.h> +#include <klocale.h> +#include <klargefile.h> +#include <kcmdlineargs.h> +#include <kmimetype.h> +#include <kprocess.h> + +#include <dcopclient.h> +#include <qdatastream.h> +#include <qtextstream.h> +#include <qfile.h> +#include <qeventloop.h> + +#include <time.h> +#include <pwd.h> +#include <grp.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <stdlib.h> + +static const KCmdLineOptions options[] = +{ + { "+protocol", I18N_NOOP( "Protocol name" ), 0 }, + { "+pool", I18N_NOOP( "Socket name" ), 0 }, + { "+app", I18N_NOOP( "Socket name" ), 0 }, + KCmdLineLastOption +}; + +extern "C" { + int KDE_EXPORT kdemain( int argc, char **argv ) + { + //KInstance instance( "kio_trash" ); + // KApplication is necessary to use kio_file + putenv(strdup("SESSION_MANAGER=")); + KApplication::disableAutoDcopRegistration(); + KCmdLineArgs::init(argc, argv, "kio_trash", 0, 0, 0, 0); + KCmdLineArgs::addCmdLineOptions( options ); + KApplication app( false, false ); + + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + TrashProtocol slave( args->arg(0), args->arg(1), args->arg(2) ); + slave.dispatchLoop(); + return 0; + } +} + +#define INIT_IMPL \ + if ( !impl.init() ) { \ + error( impl.lastErrorCode(), impl.lastErrorMessage() ); \ + return; \ + } + +TrashProtocol::TrashProtocol( const QCString& protocol, const QCString &pool, const QCString &app) + : SlaveBase(protocol, pool, app ) +{ + struct passwd *user = getpwuid( getuid() ); + if ( user ) + m_userName = QString::fromLatin1(user->pw_name); + struct group *grp = getgrgid( getgid() ); + if ( grp ) + m_groupName = QString::fromLatin1(grp->gr_name); +} + +TrashProtocol::~TrashProtocol() +{ +} + +void TrashProtocol::restore( const KURL& trashURL ) +{ + int trashId; + QString fileId, relativePath; + bool ok = TrashImpl::parseURL( trashURL, trashId, fileId, relativePath ); + if ( !ok ) { + error( KIO::ERR_SLAVE_DEFINED, i18n( "Malformed URL %1" ).arg( trashURL.prettyURL() ) ); + return; + } + TrashedFileInfo info; + ok = impl.infoForFile( trashId, fileId, info ); + if ( !ok ) { + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + return; + } + KURL dest; + dest.setPath( info.origPath ); + if ( !relativePath.isEmpty() ) + dest.addPath( relativePath ); + + // Check that the destination directory exists, to improve the error code in case it doesn't. + const QString destDir = dest.directory(); + KDE_struct_stat buff; + if ( KDE_lstat( QFile::encodeName( destDir ), &buff ) == -1 ) { + error( KIO::ERR_SLAVE_DEFINED, + i18n( "The directory %1 does not exist anymore, so it is not possible to restore this item to its original location. " + "You can either recreate that directory and use the restore operation again, or drag the item anywhere else to restore it." ).arg( destDir ) ); + return; + } + + copyOrMove( trashURL, dest, false /*overwrite*/, Move ); +} + +void TrashProtocol::rename( const KURL &oldURL, const KURL &newURL, bool overwrite ) +{ + INIT_IMPL; + + kdDebug()<<"TrashProtocol::rename(): old="<<oldURL<<" new="<<newURL<<" overwrite=" << overwrite<<endl; + + if ( oldURL.protocol() == "trash" && newURL.protocol() == "trash" ) { + error( KIO::ERR_CANNOT_RENAME, oldURL.prettyURL() ); + return; + } + + copyOrMove( oldURL, newURL, overwrite, Move ); +} + +void TrashProtocol::copy( const KURL &src, const KURL &dest, int /*permissions*/, bool overwrite ) +{ + INIT_IMPL; + + kdDebug()<<"TrashProtocol::copy(): " << src << " " << dest << endl; + + if ( src.protocol() == "trash" && dest.protocol() == "trash" ) { + error( KIO::ERR_UNSUPPORTED_ACTION, i18n( "This file is already in the trash bin." ) ); + return; + } + + copyOrMove( src, dest, overwrite, Copy ); +} + +void TrashProtocol::copyOrMove( const KURL &src, const KURL &dest, bool overwrite, CopyOrMove action ) +{ + if ( src.protocol() == "trash" && dest.isLocalFile() ) { + // Extracting (e.g. via dnd). Ignore original location stored in info file. + int trashId; + QString fileId, relativePath; + bool ok = TrashImpl::parseURL( src, trashId, fileId, relativePath ); + if ( !ok ) { + error( KIO::ERR_SLAVE_DEFINED, i18n( "Malformed URL %1" ).arg( src.prettyURL() ) ); + return; + } + const QString destPath = dest.path(); + if ( QFile::exists( destPath ) ) { + if ( overwrite ) { + ok = QFile::remove( destPath ); + Q_ASSERT( ok ); // ### TODO + } else { + error( KIO::ERR_FILE_ALREADY_EXIST, destPath ); + return; + } + } + + if ( action == Move ) { + kdDebug() << "calling moveFromTrash(" << destPath << " " << trashId << " " << fileId << ")" << endl; + ok = impl.moveFromTrash( destPath, trashId, fileId, relativePath ); + } else { // Copy + kdDebug() << "calling copyFromTrash(" << destPath << " " << trashId << " " << fileId << ")" << endl; + ok = impl.copyFromTrash( destPath, trashId, fileId, relativePath ); + } + if ( !ok ) { + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + } else { + if ( action == Move && relativePath.isEmpty() ) + (void)impl.deleteInfo( trashId, fileId ); + finished(); + } + return; + } else if ( src.isLocalFile() && dest.protocol() == "trash" ) { + QString dir = dest.directory(); + //kdDebug() << "trashing a file to " << dir << endl; + // Trashing a file + // We detect the case where this isn't normal trashing, but + // e.g. if kwrite tries to save (moving tempfile over destination) + if ( dir.length() <= 1 && src.fileName() == dest.fileName() ) // new toplevel entry + { + const QString srcPath = src.path(); + // In theory we should use TrashImpl::parseURL to give the right filename to createInfo, + // in case the trash URL didn't contain the same filename as srcPath. + // But this can only happen with copyAs/moveAs, not available in the GUI + // for the trash (New/... or Rename from iconview/listview). + int trashId; + QString fileId; + if ( !impl.createInfo( srcPath, trashId, fileId ) ) { + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + } else { + bool ok; + if ( action == Move ) { + kdDebug() << "calling moveToTrash(" << srcPath << " " << trashId << " " << fileId << ")" << endl; + ok = impl.moveToTrash( srcPath, trashId, fileId ); + } else { // Copy + kdDebug() << "calling copyToTrash(" << srcPath << " " << trashId << " " << fileId << ")" << endl; + ok = impl.copyToTrash( srcPath, trashId, fileId ); + } + if ( !ok ) { + (void)impl.deleteInfo( trashId, fileId ); + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + } else { + // Inform caller of the final URL. Used by konq_undo. + const KURL url = impl.makeURL( trashId, fileId, QString::null ); + setMetaData( "trashURL-" + srcPath, url.url() ); + finished(); + } + } + return; + } else { + kdDebug() << "returning KIO::ERR_ACCESS_DENIED, it's not allowed to add a file to an existing trash directory" << endl; + // It's not allowed to add a file to an existing trash directory. + error( KIO::ERR_ACCESS_DENIED, dest.prettyURL() ); + return; + } + } else + error( KIO::ERR_UNSUPPORTED_ACTION, "should never happen" ); +} + +static void addAtom(KIO::UDSEntry& entry, unsigned int ID, long long l, const QString& s = QString::null) +{ + KIO::UDSAtom atom; + atom.m_uds = ID; + atom.m_long = l; + atom.m_str = s; + entry.append(atom); +} + +void TrashProtocol::createTopLevelDirEntry(KIO::UDSEntry& entry) +{ + entry.clear(); + addAtom(entry, KIO::UDS_NAME, 0, "."); + addAtom(entry, KIO::UDS_FILE_TYPE, S_IFDIR); + addAtom(entry, KIO::UDS_ACCESS, 0700); + addAtom(entry, KIO::UDS_MIME_TYPE, 0, "inode/directory"); + addAtom(entry, KIO::UDS_USER, 0, m_userName); + addAtom(entry, KIO::UDS_GROUP, 0, m_groupName); +} + +void TrashProtocol::stat(const KURL& url) +{ + INIT_IMPL; + const QString path = url.path(); + if( path.isEmpty() || path == "/" ) { + // The root is "virtual" - it's not a single physical directory + KIO::UDSEntry entry; + createTopLevelDirEntry( entry ); + statEntry( entry ); + finished(); + } else { + int trashId; + QString fileId, relativePath; + + bool ok = TrashImpl::parseURL( url, trashId, fileId, relativePath ); + + if ( !ok ) { + // ######## do we still need this? + kdDebug() << k_funcinfo << url << " looks fishy, returning does-not-exist" << endl; + // A URL like trash:/file simply means that CopyJob is trying to see if + // the destination exists already (it made up the URL by itself). + error( KIO::ERR_DOES_NOT_EXIST, url.prettyURL() ); + //error( KIO::ERR_SLAVE_DEFINED, i18n( "Malformed URL %1" ).arg( url.prettyURL() ) ); + return; + } + + const QString filePath = impl.physicalPath( trashId, fileId, relativePath ); + if ( filePath.isEmpty() ) { + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + return; + } + + QString fileName = filePath.section('/', -1, -1, QString::SectionSkipEmpty); + + QString fileURL = QString::null; + if ( url.path().length() > 1 ) { + fileURL = url.url(); + } + + KIO::UDSEntry entry; + TrashedFileInfo info; + ok = impl.infoForFile( trashId, fileId, info ); + if ( ok ) + ok = createUDSEntry( filePath, fileName, fileURL, entry, info ); + + if ( !ok ) { + error( KIO::ERR_COULD_NOT_STAT, url.prettyURL() ); + } + + statEntry( entry ); + finished(); + } +} + +void TrashProtocol::del( const KURL &url, bool /*isfile*/ ) +{ + int trashId; + QString fileId, relativePath; + + bool ok = TrashImpl::parseURL( url, trashId, fileId, relativePath ); + if ( !ok ) { + error( KIO::ERR_SLAVE_DEFINED, i18n( "Malformed URL %1" ).arg( url.prettyURL() ) ); + return; + } + + ok = relativePath.isEmpty(); + if ( !ok ) { + error( KIO::ERR_ACCESS_DENIED, url.prettyURL() ); + return; + } + + ok = impl.del(trashId, fileId); + if ( !ok ) { + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + return; + } + + finished(); +} + +void TrashProtocol::listDir(const KURL& url) +{ + INIT_IMPL; + kdDebug() << "listdir: " << url << endl; + if ( url.path().length() <= 1 ) { + listRoot(); + return; + } + int trashId; + QString fileId; + QString relativePath; + bool ok = TrashImpl::parseURL( url, trashId, fileId, relativePath ); + if ( !ok ) { + error( KIO::ERR_SLAVE_DEFINED, i18n( "Malformed URL %1" ).arg( url.prettyURL() ) ); + return; + } + //was: const QString physicalPath = impl.physicalPath( trashId, fileId, relativePath ); + + // Get info for deleted directory - the date of deletion and orig path will be used + // for all the items in it, and we need the physicalPath. + TrashedFileInfo info; + ok = impl.infoForFile( trashId, fileId, info ); + if ( !ok || info.physicalPath.isEmpty() ) { + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + return; + } + if ( !relativePath.isEmpty() ) { + info.physicalPath += "/"; + info.physicalPath += relativePath; + } + + // List subdir. Can't use kio_file here since we provide our own info... + kdDebug() << k_funcinfo << "listing " << info.physicalPath << endl; + QStrList entryNames = impl.listDir( info.physicalPath ); + totalSize( entryNames.count() ); + KIO::UDSEntry entry; + QStrListIterator entryIt( entryNames ); + for (; entryIt.current(); ++entryIt) { + QString fileName = QFile::decodeName( entryIt.current() ); + if ( fileName == ".." ) + continue; + const QString filePath = info.physicalPath + "/" + fileName; + // shouldn't be necessary + //const QString url = TrashImpl::makeURL( trashId, fileId, relativePath + "/" + fileName ); + entry.clear(); + TrashedFileInfo infoForItem( info ); + infoForItem.origPath += '/'; + infoForItem.origPath += fileName; + if ( ok && createUDSEntry( filePath, fileName, QString::null /*url*/, entry, infoForItem ) ) { + listEntry( entry, false ); + } + } + entry.clear(); + listEntry( entry, true ); + finished(); +} + +bool TrashProtocol::createUDSEntry( const QString& physicalPath, const QString& fileName, const QString& url, KIO::UDSEntry& entry, const TrashedFileInfo& info ) +{ + QCString physicalPath_c = QFile::encodeName( physicalPath ); + KDE_struct_stat buff; + if ( KDE_lstat( physicalPath_c, &buff ) == -1 ) { + kdWarning() << "couldn't stat " << physicalPath << endl; + return false; + } + if (S_ISLNK(buff.st_mode)) { + char buffer2[ 1000 ]; + int n = readlink( physicalPath_c, buffer2, 1000 ); + if ( n != -1 ) { + buffer2[ n ] = 0; + } + + addAtom( entry, KIO::UDS_LINK_DEST, 0, QFile::decodeName( buffer2 ) ); + // Follow symlink + // That makes sense in kio_file, but not in the trash, especially for the size + // #136876 +#if 0 + if ( KDE_stat( physicalPath_c, &buff ) == -1 ) { + // It is a link pointing to nowhere + buff.st_mode = S_IFLNK | S_IRWXU | S_IRWXG | S_IRWXO; + buff.st_mtime = 0; + buff.st_atime = 0; + buff.st_size = 0; + } +#endif + } + mode_t type = buff.st_mode & S_IFMT; // extract file type + mode_t access = buff.st_mode & 07777; // extract permissions + access &= 07555; // make it readonly, since it's in the trashcan + addAtom( entry, KIO::UDS_NAME, 0, fileName ); + addAtom( entry, KIO::UDS_FILE_TYPE, type ); + if ( !url.isEmpty() ) + addAtom( entry, KIO::UDS_URL, 0, url ); + + KMimeType::Ptr mt = KMimeType::findByPath( physicalPath, buff.st_mode ); + addAtom( entry, KIO::UDS_MIME_TYPE, 0, mt->name() ); + addAtom( entry, KIO::UDS_ACCESS, access ); + addAtom( entry, KIO::UDS_SIZE, buff.st_size ); + addAtom( entry, KIO::UDS_USER, 0, m_userName ); // assumption + addAtom( entry, KIO::UDS_GROUP, 0, m_groupName ); // assumption + addAtom( entry, KIO::UDS_MODIFICATION_TIME, buff.st_mtime ); + addAtom( entry, KIO::UDS_ACCESS_TIME, buff.st_atime ); // ## or use it for deletion time? + addAtom( entry, KIO::UDS_EXTRA, 0, info.origPath ); + addAtom( entry, KIO::UDS_EXTRA, 0, info.deletionDate.toString( Qt::ISODate ) ); + return true; +} + +void TrashProtocol::listRoot() +{ + INIT_IMPL; + const TrashedFileInfoList lst = impl.list(); + totalSize( lst.count() ); + KIO::UDSEntry entry; + createTopLevelDirEntry( entry ); + listEntry( entry, false ); + for ( TrashedFileInfoList::ConstIterator it = lst.begin(); it != lst.end(); ++it ) { + const KURL url = TrashImpl::makeURL( (*it).trashId, (*it).fileId, QString::null ); + KURL origURL; + origURL.setPath( (*it).origPath ); + entry.clear(); + if ( createUDSEntry( (*it).physicalPath, origURL.fileName(), url.url(), entry, *it ) ) + listEntry( entry, false ); + } + entry.clear(); + listEntry( entry, true ); + finished(); +} + +void TrashProtocol::special( const QByteArray & data ) +{ + INIT_IMPL; + QDataStream stream( data, IO_ReadOnly ); + int cmd; + stream >> cmd; + + switch (cmd) { + case 1: + if ( impl.emptyTrash() ) + finished(); + else + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + break; + case 2: + impl.migrateOldTrash(); + finished(); + break; + case 3: + { + KURL url; + stream >> url; + restore( url ); + break; + } + default: + kdWarning(7116) << "Unknown command in special(): " << cmd << endl; + error( KIO::ERR_UNSUPPORTED_ACTION, QString::number(cmd) ); + break; + } +} + +void TrashProtocol::put( const KURL& url, int /*permissions*/, bool /*overwrite*/, bool /*resume*/ ) +{ + INIT_IMPL; + kdDebug() << "put: " << url << endl; + // create deleted file. We need to get the mtime and original location from metadata... + // Maybe we can find the info file for url.fileName(), in case ::rename() was called first, and failed... + error( KIO::ERR_ACCESS_DENIED, url.prettyURL() ); +} + +void TrashProtocol::get( const KURL& url ) +{ + INIT_IMPL; + kdDebug() << "get() : " << url << endl; + if ( !url.isValid() ) { + kdDebug() << kdBacktrace() << endl; + error( KIO::ERR_SLAVE_DEFINED, i18n( "Malformed URL %1" ).arg( url.url() ) ); + return; + } + if ( url.path().length() <= 1 ) { + error( KIO::ERR_IS_DIRECTORY, url.prettyURL() ); + return; + } + int trashId; + QString fileId; + QString relativePath; + bool ok = TrashImpl::parseURL( url, trashId, fileId, relativePath ); + if ( !ok ) { + error( KIO::ERR_SLAVE_DEFINED, i18n( "Malformed URL %1" ).arg( url.prettyURL() ) ); + return; + } + const QString physicalPath = impl.physicalPath( trashId, fileId, relativePath ); + if ( physicalPath.isEmpty() ) { + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + return; + } + + // Usually we run jobs in TrashImpl (for e.g. future kdedmodule) + // But for this one we wouldn't use DCOP for every bit of data... + KURL fileURL; + fileURL.setPath( physicalPath ); + KIO::Job* job = KIO::get( fileURL ); + connect( job, SIGNAL( data( KIO::Job*, const QByteArray& ) ), + this, SLOT( slotData( KIO::Job*, const QByteArray& ) ) ); + connect( job, SIGNAL( mimetype( KIO::Job*, const QString& ) ), + this, SLOT( slotMimetype( KIO::Job*, const QString& ) ) ); + connect( job, SIGNAL( result(KIO::Job *) ), + this, SLOT( jobFinished(KIO::Job *) ) ); + qApp->eventLoop()->enterLoop(); +} + +void TrashProtocol::slotData( KIO::Job*, const QByteArray&arr ) +{ + data( arr ); +} + +void TrashProtocol::slotMimetype( KIO::Job*, const QString& mt ) +{ + mimeType( mt ); +} + +void TrashProtocol::jobFinished( KIO::Job* job ) +{ + if ( job->error() ) + error( job->error(), job->errorText() ); + else + finished(); + qApp->eventLoop()->exitLoop(); +} + +#if 0 +void TrashProtocol::mkdir( const KURL& url, int /*permissions*/ ) +{ + INIT_IMPL; + // create info about deleted dir + // ############ Problem: we don't know the original path. + // Let's try to avoid this case (we should get to copy() instead, for local files) + kdDebug() << "mkdir: " << url << endl; + QString dir = url.directory(); + + if ( dir.length() <= 1 ) // new toplevel entry + { + // ## we should use TrashImpl::parseURL to give the right filename to createInfo + int trashId; + QString fileId; + if ( !impl.createInfo( url.path(), trashId, fileId ) ) { + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + } else { + if ( !impl.mkdir( trashId, fileId, permissions ) ) { + (void)impl.deleteInfo( trashId, fileId ); + error( impl.lastErrorCode(), impl.lastErrorMessage() ); + } else + finished(); + } + } else { + // Well it's not allowed to add a directory to an existing deleted directory. + error( KIO::ERR_ACCESS_DENIED, url.prettyURL() ); + } +} +#endif + +#include "kio_trash.moc" diff --git a/kioslave/trash/kio_trash.h b/kioslave/trash/kio_trash.h new file mode 100644 index 000000000..726db431e --- /dev/null +++ b/kioslave/trash/kio_trash.h @@ -0,0 +1,71 @@ +/* This file is part of the KDE project + Copyright (C) 2004 David Faure <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KIO_TRASH_H +#define KIO_TRASH_H + +#include <kio/slavebase.h> +#include "trashimpl.h" +namespace KIO { class Job; } + +typedef TrashImpl::TrashedFileInfo TrashedFileInfo; +typedef TrashImpl::TrashedFileInfoList TrashedFileInfoList; + +class TrashProtocol : public QObject, public KIO::SlaveBase +{ + Q_OBJECT +public: + TrashProtocol( const QCString& protocol, const QCString &pool, const QCString &app); + virtual ~TrashProtocol(); + virtual void stat(const KURL& url); + virtual void listDir(const KURL& url); + virtual void get( const KURL& url ); + virtual void put( const KURL& url, int , bool overwrite, bool ); + virtual void rename( const KURL &, const KURL &, bool ); + virtual void copy( const KURL &src, const KURL &dest, int permissions, bool overwrite ); + // TODO (maybe) chmod( const KURL& url, int permissions ); + virtual void del( const KURL &url, bool isfile ); + /** + * Special actions: (first int in the byte array) + * 1 : empty trash + * 2 : migrate old (pre-kde-3.4) trash contents + * 3 : restore a file to its original location. Args: KURL trashURL. + */ + virtual void special( const QByteArray & data ); + +private slots: + void slotData( KIO::Job*, const QByteArray& ); + void slotMimetype( KIO::Job*, const QString& ); + void jobFinished( KIO::Job* job ); + +private: + typedef enum CopyOrMove { Copy, Move }; + void copyOrMove( const KURL& src, const KURL& dest, bool overwrite, CopyOrMove action ); + void createTopLevelDirEntry(KIO::UDSEntry& entry); + bool createUDSEntry( const QString& physicalPath, const QString& fileName, const QString& url, + KIO::UDSEntry& entry, const TrashedFileInfo& info ); + void listRoot(); + void restore( const KURL& trashURL ); + + TrashImpl impl; + QString m_userName; + QString m_groupName; +}; + +#endif diff --git a/kioslave/trash/ktrash.cpp b/kioslave/trash/ktrash.cpp new file mode 100644 index 000000000..4fa5ccd27 --- /dev/null +++ b/kioslave/trash/ktrash.cpp @@ -0,0 +1,102 @@ +/* This file is part of the KDE project + Copyright (C) 2004 David Faure <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include <kapplication.h> +#include <kio/netaccess.h> +#include <kio/job.h> +#include <kcmdlineargs.h> +#include <klocale.h> +#include <kdirnotify_stub.h> +#include <kdebug.h> + +static KCmdLineOptions options[] = +{ + { "empty", I18N_NOOP( "Empty the contents of the trash" ), 0 }, + //{ "migrate", I18N_NOOP( "Migrate contents of old trash" ), 0 }, + { "restore <file>", I18N_NOOP( "Restore a trashed file to its original location" ), 0 }, + // This hack is for the servicemenu on trash.desktop which uses Exec=ktrash -empty. %f is implied... + { "+[ignored]", I18N_NOOP( "Ignored" ), 0 }, + KCmdLineLastOption +}; + +int main(int argc, char *argv[]) +{ + KApplication::disableAutoDcopRegistration(); + KCmdLineArgs::init( argc, argv, "ktrash", + I18N_NOOP( "ktrash" ), + I18N_NOOP( "Helper program to handle the KDE trash can\n" + "Note: to move files to the trash, do not use ktrash, but \"kfmclient move 'url' trash:/\"" ), + KDE_VERSION_STRING ); + KCmdLineArgs::addCmdLineOptions( options ); + KApplication app; + + KCmdLineArgs* args = KCmdLineArgs::parsedArgs(); + if ( args->isSet( "empty" ) ) { + // We use a kio job instead of linking to TrashImpl, for a smaller binary + // (and the possibility of a central service at some point) + QByteArray packedArgs; + QDataStream stream( packedArgs, IO_WriteOnly ); + stream << (int)1; + KIO::Job* job = KIO::special( "trash:/", packedArgs ); + (void)KIO::NetAccess::synchronousRun( job, 0 ); + + // Update konq windows opened on trash:/ + KDirNotify_stub allDirNotify("*", "KDirNotify*"); + allDirNotify.FilesAdded( "trash:/" ); // yeah, files were removed, but we don't know which ones... + return 0; + } + +#if 0 + // This is only for testing. KDesktop handles it automatically. + if ( args->isSet( "migrate" ) ) { + QByteArray packedArgs; + QDataStream stream( packedArgs, IO_WriteOnly ); + stream << (int)2; + KIO::Job* job = KIO::special( "trash:/", packedArgs ); + (void)KIO::NetAccess::synchronousRun( job, 0 ); + return 0; + } +#endif + + QCString restoreArg = args->getOption( "restore" ); + if ( !restoreArg.isEmpty() ) { + + if (restoreArg.find("system:/trash")==0) { + restoreArg.remove(0, 13); + restoreArg.prepend("trash:"); + } + + KURL trashURL( restoreArg ); + if ( !trashURL.isValid() || trashURL.protocol() != "trash" ) { + kdError() << "Invalid URL for restoring a trashed file:" << trashURL << endl; + return 1; + } + + QByteArray packedArgs; + QDataStream stream( packedArgs, IO_WriteOnly ); + stream << (int)3 << trashURL; + KIO::Job* job = KIO::special( trashURL, packedArgs ); + bool ok = KIO::NetAccess::synchronousRun( job, 0 ); + if ( !ok ) + kdError() << KIO::NetAccess::lastErrorString() << endl; + return 0; + } + + return 0; +} diff --git a/kioslave/trash/testtrash.cpp b/kioslave/trash/testtrash.cpp new file mode 100644 index 000000000..4557c5031 --- /dev/null +++ b/kioslave/trash/testtrash.cpp @@ -0,0 +1,1198 @@ +/* This file is part of the KDE project + Copyright (C) 2004 David Faure <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +// Get those asserts to work +#undef NDEBUG +#undef NO_DEBUG + +#include "kio_trash.h" +#include "testtrash.h" + +#include <config.h> + +#include <kurl.h> +#include <klocale.h> +#include <kapplication.h> +#include <kio/netaccess.h> +#include <kio/job.h> +#include <kdebug.h> +#include <kcmdlineargs.h> + +#include <qdir.h> +#include <qfileinfo.h> +#include <qvaluevector.h> + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <assert.h> +#include <kfileitem.h> +#include <kstandarddirs.h> + +static bool check(const QString& txt, QString a, QString b) +{ + if (a.isEmpty()) + a = QString::null; + if (b.isEmpty()) + b = QString::null; + if (a == b) { + kdDebug() << txt << " : checking '" << a << "' against expected value '" << b << "'... " << "ok" << endl; + } + else { + kdDebug() << txt << " : checking '" << a << "' against expected value '" << b << "'... " << "KO !" << endl; + exit(1); + } + return true; +} + +// There are two ways to test encoding things: +// * with utf8 filenames +// * with latin1 filenames +// +//#define UTF8TEST 1 + +int main(int argc, char *argv[]) +{ + // Ensure a known QFile::encodeName behavior for trashUtf8FileFromHome + // However this assume your $HOME doesn't use characters from other locales... + setenv( "LC_ALL", "en_GB.ISO-8859-1", 1 ); +#ifdef UTF8TEST + setenv( "KDE_UTF8_FILENAMES", "true", 1 ); +#else + unsetenv( "KDE_UTF8_FILENAMES" ); +#endif + + // Use another directory than the real one, just to keep things clean + setenv( "XDG_DATA_HOME", QFile::encodeName( QDir::homeDirPath() + "/.local-testtrash" ), true ); + setenv( "KDE_FORK_SLAVES", "yes", true ); + + KApplication::disableAutoDcopRegistration(); + KCmdLineArgs::init(argc,argv,"testtrash", 0, 0, 0, 0); + KApplication app; + + TestTrash test; + test.setup(); + test.runAll(); + kdDebug() << "All tests OK." << endl; + return 0; // success. The exit(1) in check() is what happens in case of failure. +} + +QString TestTrash::homeTmpDir() const +{ + return QDir::homeDirPath() + "/.kde/testtrash/"; +} + +QString TestTrash::readOnlyDirPath() const +{ + return homeTmpDir() + QString( "readonly" ); +} + +QString TestTrash::otherTmpDir() const +{ + // This one needs to be on another partition + return "/tmp/testtrash/"; +} + +QString TestTrash::utf8FileName() const +{ + return QString( "test" ) + QChar( 0x2153 ); // "1/3" character, not part of latin1 +} + +QString TestTrash::umlautFileName() const +{ + return QString( "umlaut" ) + QChar( 0xEB ); +} + +static void removeFile( const QString& trashDir, const QString& fileName ) +{ + QDir dir; + dir.remove( trashDir + fileName ); + assert( !QDir( trashDir + fileName ).exists() ); +} + +static void removeDir( const QString& trashDir, const QString& dirName ) +{ + QDir dir; + dir.rmdir( trashDir + dirName ); + assert( !QDir( trashDir + dirName ).exists() ); +} + +void TestTrash::setup() +{ + m_trashDir = KGlobal::dirs()->localxdgdatadir() + "Trash"; + kdDebug() << "setup: using trash directory " << m_trashDir << endl; + + // Look for another writable partition than $HOME (not mandatory) + TrashImpl impl; + impl.init(); + + TrashImpl::TrashDirMap trashDirs = impl.trashDirectories(); + TrashImpl::TrashDirMap topDirs = impl.topDirectories(); + bool foundTrashDir = false; + m_otherPartitionId = 0; + m_tmpIsWritablePartition = false; + m_tmpTrashId = -1; + QValueVector<int> writableTopDirs; + for ( TrashImpl::TrashDirMap::ConstIterator it = trashDirs.begin(); it != trashDirs.end() ; ++it ) { + if ( it.key() == 0 ) { + assert( it.data() == m_trashDir ); + assert( topDirs.find( 0 ) == topDirs.end() ); + foundTrashDir = true; + } else { + assert( topDirs.find( it.key() ) != topDirs.end() ); + const QString topdir = topDirs[it.key()]; + if ( QFileInfo( topdir ).isWritable() ) { + writableTopDirs.append( it.key() ); + if ( topdir == "/tmp/" ) { + m_tmpIsWritablePartition = true; + m_tmpTrashId = it.key(); + kdDebug() << "/tmp is on its own partition (trashid=" << m_tmpTrashId << "), some tests will be skipped" << endl; + removeFile( it.data(), "/info/fileFromOther.trashinfo" ); + removeFile( it.data(), "/files/fileFromOther" ); + removeFile( it.data(), "/info/symlinkFromOther.trashinfo" ); + removeFile( it.data(), "/files/symlinkFromOther" ); + removeFile( it.data(), "/info/trashDirFromOther.trashinfo" ); + removeFile( it.data(), "/files/trashDirFromOther/testfile" ); + removeDir( it.data(), "/files/trashDirFromOther" ); + } + } + } + } + for ( QValueVector<int>::const_iterator it = writableTopDirs.begin(); it != writableTopDirs.end(); ++it ) { + const QString topdir = topDirs[ *it ]; + const QString trashdir = trashDirs[ *it ]; + assert( !topdir.isEmpty() ); + assert( !trashDirs.isEmpty() ); + if ( topdir != "/tmp/" || // we'd prefer not to use /tmp here, to separate the tests + ( writableTopDirs.count() > 1 ) ) // but well, if we have no choice, take it + { + m_otherPartitionTopDir = topdir; + m_otherPartitionTrashDir = trashdir; + m_otherPartitionId = *it; + kdDebug() << "OK, found another writable partition: topDir=" << m_otherPartitionTopDir + << " trashDir=" << m_otherPartitionTrashDir << " id=" << m_otherPartitionId << endl; + break; + } + } + // Check that m_trashDir got listed + assert( foundTrashDir ); + if ( m_otherPartitionTrashDir.isEmpty() ) + kdWarning() << "No writable partition other than $HOME found, some tests will be skipped" << endl; + + // Start with a clean base dir + if ( QFileInfo( homeTmpDir() ).exists() ) { + bool ok = KIO::NetAccess::del( homeTmpDir(), 0 ); + if ( !ok ) + kdFatal() << "Couldn't delete " << homeTmpDir() << endl; + } + if ( QFileInfo( otherTmpDir() ).exists() ) { + bool ok = KIO::NetAccess::del( otherTmpDir(), 0 ); + if ( !ok ) + kdFatal() << "Couldn't delete " << otherTmpDir() << endl; + } + QDir dir; // TT: why not a static method? + bool ok = dir.mkdir( homeTmpDir() ); + if ( !ok ) + kdFatal() << "Couldn't create " << homeTmpDir() << endl; + ok = dir.mkdir( otherTmpDir() ); + if ( !ok ) + kdFatal() << "Couldn't create " << otherTmpDir() << endl; + cleanTrash(); +} + + +void TestTrash::cleanTrash() +{ + kdDebug() << k_funcinfo << endl; + // Start with a relatively clean trash too + removeFile( m_trashDir, "/info/fileFromHome.trashinfo" ); + removeFile( m_trashDir, "/files/fileFromHome" ); + removeFile( m_trashDir, "/info/fileFromHome_1.trashinfo" ); + removeFile( m_trashDir, "/files/fileFromHome_1" ); + removeFile( m_trashDir, "/info/file%2f.trashinfo" ); + removeFile( m_trashDir, "/files/file%2f" ); + removeFile( m_trashDir, "/info/" + utf8FileName() + ".trashinfo" ); + removeFile( m_trashDir, "/files/" + utf8FileName() ); + removeFile( m_trashDir, "/info/" + umlautFileName() + ".trashinfo" ); + removeFile( m_trashDir, "/files/" + umlautFileName() ); + removeFile( m_trashDir, "/info/fileFromOther.trashinfo" ); + removeFile( m_trashDir, "/files/fileFromOther" ); + removeFile( m_trashDir, "/info/symlinkFromHome.trashinfo" ); + removeFile( m_trashDir, "/files/symlinkFromHome" ); + removeFile( m_trashDir, "/info/symlinkFromOther.trashinfo" ); + removeFile( m_trashDir, "/files/symlinkFromOther" ); + removeFile( m_trashDir, "/info/brokenSymlinkFromHome.trashinfo" ); + removeFile( m_trashDir, "/files/brokenSymlinkFromHome" ); + removeFile( m_trashDir, "/info/trashDirFromHome.trashinfo" ); + removeFile( m_trashDir, "/files/trashDirFromHome/testfile" ); + removeFile( m_trashDir, "/info/readonly.trashinfo" ); + removeDir( m_trashDir, "/files/trashDirFromHome" ); + removeFile( m_trashDir, "/info/trashDirFromHome_1.trashinfo" ); + removeFile( m_trashDir, "/files/trashDirFromHome_1/testfile" ); + removeDir( m_trashDir, "/files/trashDirFromHome_1" ); + removeFile( m_trashDir, "/info/trashDirFromOther.trashinfo" ); + removeFile( m_trashDir, "/files/trashDirFromOther/testfile" ); + removeDir( m_trashDir, "/files/trashDirFromOther" ); + KIO::NetAccess::del( m_trashDir + "/files/readonly", 0 ); + // for trashDirectoryOwnedByRoot + KIO::NetAccess::del( m_trashDir + "/files/cups", 0 ); + KIO::NetAccess::del( m_trashDir + "/files/boot", 0 ); + KIO::NetAccess::del( m_trashDir + "/files/etc", 0 ); + + //system( "find ~/.local-testtrash/share/Trash" ); +} + +void TestTrash::runAll() +{ + urlTestFile(); + urlTestDirectory(); + urlTestSubDirectory(); + + trashFileFromHome(); + trashPercentFileFromHome(); +#ifdef UTF8TEST + trashUtf8FileFromHome(); +#endif + trashUmlautFileFromHome(); + trashReadOnlyDirFromHome(); + testTrashNotEmpty(); + trashFileFromOther(); + trashFileIntoOtherPartition(); + trashFileOwnedByRoot(); + trashSymlinkFromHome(); + trashSymlinkFromOther(); + trashBrokenSymlinkFromHome(); + trashDirectoryFromHome(); + trashDirectoryFromOther(); + trashDirectoryOwnedByRoot(); + + tryRenameInsideTrash(); + + statRoot(); + statFileInRoot(); + statDirectoryInRoot(); + statSymlinkInRoot(); + statFileInDirectory(); + + copyFileFromTrash(); + // To test case of already-existing destination, uncomment this. + // This brings up the "rename" dialog though, so it can't be fully automated + //copyFileFromTrash(); + copyFileInDirectoryFromTrash(); + copyDirectoryFromTrash(); + copySymlinkFromTrash(); + + moveFileFromTrash(); + moveFileInDirectoryFromTrash(); + moveDirectoryFromTrash(); + moveSymlinkFromTrash(); + + listRootDir(); + listRecursiveRootDir(); + listSubDir(); + + delRootFile(); + delFileInDirectory(); + delDirectory(); + + getFile(); + restoreFile(); + restoreFileFromSubDir(); + restoreFileToDeletedDirectory(); + + emptyTrash(); + + // TODO: test + // - trash migration + // - the actual updating of the trash icon on the desktop +} + +void TestTrash::urlTestFile() +{ + const KURL url = TrashImpl::makeURL( 1, "fileId", QString::null ); + check( "makeURL for a file", url.url(), "trash:/1-fileId" ); + + int trashId; + QString fileId; + QString relativePath; + bool ok = TrashImpl::parseURL( url, trashId, fileId, relativePath ); + assert( ok ); + check( "parseURL: trashId", QString::number( trashId ), "1" ); + check( "parseURL: fileId", fileId, "fileId" ); + check( "parseURL: relativePath", relativePath, QString::null ); +} + +void TestTrash::urlTestDirectory() +{ + const KURL url = TrashImpl::makeURL( 1, "fileId", "subfile" ); + check( "makeURL", url.url(), "trash:/1-fileId/subfile" ); + + int trashId; + QString fileId; + QString relativePath; + bool ok = TrashImpl::parseURL( url, trashId, fileId, relativePath ); + assert( ok ); + check( "parseURL: trashId", QString::number( trashId ), "1" ); + check( "parseURL: fileId", fileId, "fileId" ); + check( "parseURL: relativePath", relativePath, "subfile" ); +} + +void TestTrash::urlTestSubDirectory() +{ + const KURL url = TrashImpl::makeURL( 1, "fileId", "subfile/foobar" ); + check( "makeURL", url.url(), "trash:/1-fileId/subfile/foobar" ); + + int trashId; + QString fileId; + QString relativePath; + bool ok = TrashImpl::parseURL( url, trashId, fileId, relativePath ); + assert( ok ); + check( "parseURL: trashId", QString::number( trashId ), "1" ); + check( "parseURL: fileId", fileId, "fileId" ); + check( "parseURL: relativePath", relativePath, "subfile/foobar" ); +} + +static void checkInfoFile( const QString& infoPath, const QString& origFilePath ) +{ + kdDebug() << k_funcinfo << infoPath << endl; + QFileInfo info( infoPath ); + assert( info.exists() ); + assert( info.isFile() ); + KSimpleConfig infoFile( info.absFilePath(), true ); + if ( !infoFile.hasGroup( "Trash Info" ) ) + kdFatal() << "no Trash Info group in " << info.absFilePath() << endl; + infoFile.setGroup( "Trash Info" ); + const QString origPath = infoFile.readEntry( "Path" ); + assert( !origPath.isEmpty() ); + assert( origPath == KURL::encode_string( origFilePath, KGlobal::locale()->fileEncodingMib() ) ); + const QString date = infoFile.readEntry( "DeletionDate" ); + assert( !date.isEmpty() ); + assert( date.contains( "T" ) ); +} + +static void createTestFile( const QString& path ) +{ + QFile f( path ); + if ( !f.open( IO_WriteOnly ) ) + kdFatal() << "Can't create " << path << endl; + f.writeBlock( "Hello world\n", 12 ); + f.close(); + assert( QFile::exists( path ) ); +} + +void TestTrash::trashFile( const QString& origFilePath, const QString& fileId ) +{ + // setup + if ( !QFile::exists( origFilePath ) ) + createTestFile( origFilePath ); + KURL u; + u.setPath( origFilePath ); + + // test + KIO::Job* job = KIO::move( u, "trash:/" ); + QMap<QString, QString> metaData; + //bool ok = KIO::NetAccess::move( u, "trash:/" ); + bool ok = KIO::NetAccess::synchronousRun( job, 0, 0, 0, &metaData ); + if ( !ok ) + kdError() << "moving " << u << " to trash failed with error " << KIO::NetAccess::lastError() << " " << KIO::NetAccess::lastErrorString() << endl; + assert( ok ); + if ( origFilePath.startsWith( "/tmp" ) && m_tmpIsWritablePartition ) { + kdDebug() << " TESTS SKIPPED" << endl; + } else { + checkInfoFile( m_trashDir + "/info/" + fileId + ".trashinfo", origFilePath ); + + QFileInfo files( m_trashDir + "/files/" + fileId ); + assert( files.isFile() ); + assert( files.size() == 12 ); + } + + // coolo suggests testing that the original file is actually gone, too :) + assert( !QFile::exists( origFilePath ) ); + + assert( !metaData.isEmpty() ); + bool found = false; + QMap<QString, QString>::ConstIterator it = metaData.begin(); + for ( ; it != metaData.end() ; ++it ) { + if ( it.key().startsWith( "trashURL" ) ) { + const QString origPath = it.key().mid( 9 ); + KURL trashURL( it.data() ); + kdDebug() << trashURL << endl; + assert( !trashURL.isEmpty() ); + assert( trashURL.protocol() == "trash" ); + int trashId = 0; + if ( origFilePath.startsWith( "/tmp" ) && m_tmpIsWritablePartition ) + trashId = m_tmpTrashId; + assert( trashURL.path() == "/" + QString::number( trashId ) + "-" + fileId ); + found = true; + } + } + assert( found ); +} + +void TestTrash::trashFileFromHome() +{ + kdDebug() << k_funcinfo << endl; + const QString fileName = "fileFromHome"; + trashFile( homeTmpDir() + fileName, fileName ); + + // Do it again, check that we got a different id + trashFile( homeTmpDir() + fileName, fileName + "_1" ); +} + +void TestTrash::trashPercentFileFromHome() +{ + kdDebug() << k_funcinfo << endl; + const QString fileName = "file%2f"; + trashFile( homeTmpDir() + fileName, fileName ); +} + +void TestTrash::trashUtf8FileFromHome() +{ + kdDebug() << k_funcinfo << endl; + const QString fileName = utf8FileName(); + trashFile( homeTmpDir() + fileName, fileName ); +} + +void TestTrash::trashUmlautFileFromHome() +{ + kdDebug() << k_funcinfo << endl; + const QString fileName = umlautFileName(); + trashFile( homeTmpDir() + fileName, fileName ); +} + +void TestTrash::testTrashNotEmpty() +{ + KSimpleConfig cfg( "trashrc", true ); + assert( cfg.hasGroup( "Status" ) ); + cfg.setGroup( "Status" ); + assert( cfg.readBoolEntry( "Empty", true ) == false ); +} + +void TestTrash::trashFileFromOther() +{ + kdDebug() << k_funcinfo << endl; + const QString fileName = "fileFromOther"; + trashFile( otherTmpDir() + fileName, fileName ); +} + +void TestTrash::trashFileIntoOtherPartition() +{ + if ( m_otherPartitionTrashDir.isEmpty() ) { + kdDebug() << k_funcinfo << " - SKIPPED" << endl; + return; + } + kdDebug() << k_funcinfo << endl; + const QString fileName = "testtrash-file"; + const QString origFilePath = m_otherPartitionTopDir + fileName; + const QString fileId = fileName; + // cleanup + QFile::remove( m_otherPartitionTrashDir + "/info/" + fileId + ".trashinfo" ); + QFile::remove( m_otherPartitionTrashDir + "/files/" + fileId ); + + // setup + if ( !QFile::exists( origFilePath ) ) + createTestFile( origFilePath ); + KURL u; + u.setPath( origFilePath ); + + // test + KIO::Job* job = KIO::move( u, "trash:/" ); + QMap<QString, QString> metaData; + bool ok = KIO::NetAccess::synchronousRun( job, 0, 0, 0, &metaData ); + assert( ok ); + // Note that the Path stored in the info file is relative, on other partitions (#95652) + checkInfoFile( m_otherPartitionTrashDir + "/info/" + fileId + ".trashinfo", fileName ); + + QFileInfo files( m_otherPartitionTrashDir + "/files/" + fileId ); + assert( files.isFile() ); + assert( files.size() == 12 ); + + // coolo suggests testing that the original file is actually gone, too :) + assert( !QFile::exists( origFilePath ) ); + + assert( !metaData.isEmpty() ); + bool found = false; + QMap<QString, QString>::ConstIterator it = metaData.begin(); + for ( ; it != metaData.end() ; ++it ) { + if ( it.key().startsWith( "trashURL" ) ) { + const QString origPath = it.key().mid( 9 ); + KURL trashURL( it.data() ); + kdDebug() << trashURL << endl; + assert( !trashURL.isEmpty() ); + assert( trashURL.protocol() == "trash" ); + assert( trashURL.path() == QString( "/%1-%2" ).arg( m_otherPartitionId ).arg( fileId ) ); + found = true; + } + } + assert( found ); +} + +void TestTrash::trashFileOwnedByRoot() +{ + kdDebug() << k_funcinfo << endl; + KURL u; + u.setPath( "/etc/passwd" ); + const QString fileId = "passwd"; + + KIO::CopyJob* job = KIO::move( u, "trash:/" ); + job->setInteractive( false ); // no skip dialog, thanks + QMap<QString, QString> metaData; + //bool ok = KIO::NetAccess::move( u, "trash:/" ); + bool ok = KIO::NetAccess::synchronousRun( job, 0, 0, 0, &metaData ); + assert( !ok ); + assert( KIO::NetAccess::lastError() == KIO::ERR_ACCESS_DENIED ); + const QString infoPath( m_trashDir + "/info/" + fileId + ".trashinfo" ); + assert( !QFile::exists( infoPath ) ); + + QFileInfo files( m_trashDir + "/files/" + fileId ); + assert( !files.exists() ); + + assert( QFile::exists( u.path() ) ); +} + +void TestTrash::trashSymlink( const QString& origFilePath, const QString& fileId, bool broken ) +{ + kdDebug() << k_funcinfo << endl; + // setup + const char* target = broken ? "/nonexistent" : "/tmp"; + bool ok = ::symlink( target, QFile::encodeName( origFilePath ) ) == 0; + assert( ok ); + KURL u; + u.setPath( origFilePath ); + + // test + ok = KIO::NetAccess::move( u, "trash:/" ); + assert( ok ); + if ( origFilePath.startsWith( "/tmp" ) && m_tmpIsWritablePartition ) { + kdDebug() << " TESTS SKIPPED" << endl; + return; + } + checkInfoFile( m_trashDir + "/info/" + fileId + ".trashinfo", origFilePath ); + + QFileInfo files( m_trashDir + "/files/" + fileId ); + assert( files.isSymLink() ); + assert( files.readLink() == QFile::decodeName( target ) ); + assert( !QFile::exists( origFilePath ) ); +} + +void TestTrash::trashSymlinkFromHome() +{ + kdDebug() << k_funcinfo << endl; + const QString fileName = "symlinkFromHome"; + trashSymlink( homeTmpDir() + fileName, fileName, false ); +} + +void TestTrash::trashSymlinkFromOther() +{ + kdDebug() << k_funcinfo << endl; + const QString fileName = "symlinkFromOther"; + trashSymlink( otherTmpDir() + fileName, fileName, false ); +} + +void TestTrash::trashBrokenSymlinkFromHome() +{ + kdDebug() << k_funcinfo << endl; + const QString fileName = "brokenSymlinkFromHome"; + trashSymlink( homeTmpDir() + fileName, fileName, true ); +} + +void TestTrash::trashDirectory( const QString& origPath, const QString& fileId ) +{ + kdDebug() << k_funcinfo << fileId << endl; + // setup + if ( !QFileInfo( origPath ).exists() ) { + QDir dir; + bool ok = dir.mkdir( origPath ); + Q_ASSERT( ok ); + } + createTestFile( origPath + "/testfile" ); + KURL u; u.setPath( origPath ); + + // test + bool ok = KIO::NetAccess::move( u, "trash:/" ); + assert( ok ); + if ( origPath.startsWith( "/tmp" ) && m_tmpIsWritablePartition ) { + kdDebug() << " TESTS SKIPPED" << endl; + return; + } + checkInfoFile( m_trashDir + "/info/" + fileId + ".trashinfo", origPath ); + + QFileInfo filesDir( m_trashDir + "/files/" + fileId ); + assert( filesDir.isDir() ); + QFileInfo files( m_trashDir + "/files/" + fileId + "/testfile" ); + assert( files.exists() ); + assert( files.isFile() ); + assert( files.size() == 12 ); + assert( !QFile::exists( origPath ) ); +} + +void TestTrash::trashDirectoryFromHome() +{ + kdDebug() << k_funcinfo << endl; + QString dirName = "trashDirFromHome"; + trashDirectory( homeTmpDir() + dirName, dirName ); + // Do it again, check that we got a different id + trashDirectory( homeTmpDir() + dirName, dirName + "_1" ); +} + +void TestTrash::trashReadOnlyDirFromHome() +{ + kdDebug() << k_funcinfo << endl; + const QString dirName = readOnlyDirPath(); + QDir dir; + bool ok = dir.mkdir( dirName ); + Q_ASSERT( ok ); + // #130780 + const QString subDirPath = dirName + "/readonly_subdir"; + ok = dir.mkdir( subDirPath ); + Q_ASSERT( ok ); + createTestFile( subDirPath + "/testfile_in_subdir" ); + ::chmod( QFile::encodeName( subDirPath ), 0500 ); + + trashDirectory( dirName, "readonly" ); +} + +void TestTrash::trashDirectoryFromOther() +{ + kdDebug() << k_funcinfo << endl; + QString dirName = "trashDirFromOther"; + trashDirectory( otherTmpDir() + dirName, dirName ); +} + +void TestTrash::tryRenameInsideTrash() +{ + kdDebug() << k_funcinfo << " with file_move" << endl; + bool worked = KIO::NetAccess::file_move( "trash:/0-tryRenameInsideTrash", "trash:/foobar" ); + assert( !worked ); + assert( KIO::NetAccess::lastError() == KIO::ERR_CANNOT_RENAME ); + + kdDebug() << k_funcinfo << " with move" << endl; + worked = KIO::NetAccess::move( "trash:/0-tryRenameInsideTrash", "trash:/foobar" ); + assert( !worked ); + assert( KIO::NetAccess::lastError() == KIO::ERR_CANNOT_RENAME ); +} + +void TestTrash::delRootFile() +{ + kdDebug() << k_funcinfo << endl; + + // test deleting a trashed file + bool ok = KIO::NetAccess::del( "trash:/0-fileFromHome", 0L ); + assert( ok ); + + QFileInfo file( m_trashDir + "/files/fileFromHome" ); + assert( !file.exists() ); + QFileInfo info( m_trashDir + "/info/fileFromHome.trashinfo" ); + assert( !info.exists() ); + + // trash it again, we might need it later + const QString fileName = "fileFromHome"; + trashFile( homeTmpDir() + fileName, fileName ); +} + +void TestTrash::delFileInDirectory() +{ + kdDebug() << k_funcinfo << endl; + + // test deleting a file inside a trashed directory -> not allowed + bool ok = KIO::NetAccess::del( "trash:/0-trashDirFromHome/testfile", 0L ); + assert( !ok ); + assert( KIO::NetAccess::lastError() == KIO::ERR_ACCESS_DENIED ); + + QFileInfo dir( m_trashDir + "/files/trashDirFromHome" ); + assert( dir.exists() ); + QFileInfo file( m_trashDir + "/files/trashDirFromHome/testfile" ); + assert( file.exists() ); + QFileInfo info( m_trashDir + "/info/trashDirFromHome.trashinfo" ); + assert( info.exists() ); +} + +void TestTrash::delDirectory() +{ + kdDebug() << k_funcinfo << endl; + + // test deleting a trashed directory + bool ok = KIO::NetAccess::del( "trash:/0-trashDirFromHome", 0L ); + assert( ok ); + + QFileInfo dir( m_trashDir + "/files/trashDirFromHome" ); + assert( !dir.exists() ); + QFileInfo file( m_trashDir + "/files/trashDirFromHome/testfile" ); + assert( !file.exists() ); + QFileInfo info( m_trashDir + "/info/trashDirFromHome.trashinfo" ); + assert( !info.exists() ); + + // trash it again, we'll need it later + QString dirName = "trashDirFromHome"; + trashDirectory( homeTmpDir() + dirName, dirName ); +} + +void TestTrash::statRoot() +{ + kdDebug() << k_funcinfo << endl; + KURL url( "trash:/" ); + KIO::UDSEntry entry; + bool ok = KIO::NetAccess::stat( url, entry, 0 ); + assert( ok ); + KFileItem item( entry, url ); + assert( item.isDir() ); + assert( !item.isLink() ); + assert( item.isReadable() ); + assert( item.isWritable() ); + assert( !item.isHidden() ); + assert( item.name() == "." ); + assert( item.acceptsDrops() ); +} + +void TestTrash::statFileInRoot() +{ + kdDebug() << k_funcinfo << endl; + KURL url( "trash:/0-fileFromHome" ); + KIO::UDSEntry entry; + bool ok = KIO::NetAccess::stat( url, entry, 0 ); + assert( ok ); + KFileItem item( entry, url ); + assert( item.isFile() ); + assert( !item.isDir() ); + assert( !item.isLink() ); + assert( item.isReadable() ); + assert( !item.isWritable() ); + assert( !item.isHidden() ); + assert( item.name() == "fileFromHome" ); + assert( !item.acceptsDrops() ); +} + +void TestTrash::statDirectoryInRoot() +{ + kdDebug() << k_funcinfo << endl; + KURL url( "trash:/0-trashDirFromHome" ); + KIO::UDSEntry entry; + bool ok = KIO::NetAccess::stat( url, entry, 0 ); + assert( ok ); + KFileItem item( entry, url ); + assert( item.isDir() ); + assert( !item.isLink() ); + assert( item.isReadable() ); + assert( !item.isWritable() ); + assert( !item.isHidden() ); + assert( item.name() == "trashDirFromHome" ); + assert( !item.acceptsDrops() ); +} + +void TestTrash::statSymlinkInRoot() +{ + kdDebug() << k_funcinfo << endl; + KURL url( "trash:/0-symlinkFromHome" ); + KIO::UDSEntry entry; + bool ok = KIO::NetAccess::stat( url, entry, 0 ); + assert( ok ); + KFileItem item( entry, url ); + assert( item.isLink() ); + assert( item.linkDest() == "/tmp" ); + assert( item.isReadable() ); + assert( !item.isWritable() ); + assert( !item.isHidden() ); + assert( item.name() == "symlinkFromHome" ); + assert( !item.acceptsDrops() ); +} + +void TestTrash::statFileInDirectory() +{ + kdDebug() << k_funcinfo << endl; + KURL url( "trash:/0-trashDirFromHome/testfile" ); + KIO::UDSEntry entry; + bool ok = KIO::NetAccess::stat( url, entry, 0 ); + assert( ok ); + KFileItem item( entry, url ); + assert( item.isFile() ); + assert( !item.isLink() ); + assert( item.isReadable() ); + assert( !item.isWritable() ); + assert( !item.isHidden() ); + assert( item.name() == "testfile" ); + assert( !item.acceptsDrops() ); +} + +void TestTrash::copyFromTrash( const QString& fileId, const QString& destPath, const QString& relativePath ) +{ + KURL src( "trash:/0-" + fileId ); + if ( !relativePath.isEmpty() ) + src.addPath( relativePath ); + KURL dest; + dest.setPath( destPath ); + + assert( KIO::NetAccess::exists( src, true, (QWidget*)0 ) ); + + // A dnd would use copy(), but we use copyAs to ensure the final filename + //kdDebug() << k_funcinfo << "copyAs:" << src << " -> " << dest << endl; + KIO::Job* job = KIO::copyAs( src, dest ); + bool ok = KIO::NetAccess::synchronousRun( job, 0 ); + assert( ok ); + QString infoFile( m_trashDir + "/info/" + fileId + ".trashinfo" ); + assert( QFile::exists( infoFile ) ); + + QFileInfo filesItem( m_trashDir + "/files/" + fileId ); + assert( filesItem.exists() ); + + assert( QFile::exists( destPath ) ); +} + +void TestTrash::copyFileFromTrash() +{ + kdDebug() << k_funcinfo << endl; + const QString destPath = otherTmpDir() + "fileFromHome_copied"; + copyFromTrash( "fileFromHome", destPath ); + assert( QFileInfo( destPath ).isFile() ); + assert( QFileInfo( destPath ).size() == 12 ); +} + +void TestTrash::copyFileInDirectoryFromTrash() +{ + kdDebug() << k_funcinfo << endl; + const QString destPath = otherTmpDir() + "testfile_copied"; + copyFromTrash( "trashDirFromHome", destPath, "testfile" ); + assert( QFileInfo( destPath ).isFile() ); + assert( QFileInfo( destPath ).size() == 12 ); +} + +void TestTrash::copyDirectoryFromTrash() +{ + kdDebug() << k_funcinfo << endl; + const QString destPath = otherTmpDir() + "trashDirFromHome_copied"; + copyFromTrash( "trashDirFromHome", destPath ); + assert( QFileInfo( destPath ).isDir() ); +} + +void TestTrash::copySymlinkFromTrash() +{ + kdDebug() << k_funcinfo << endl; + const QString destPath = otherTmpDir() + "symlinkFromHome_copied"; + copyFromTrash( "symlinkFromHome", destPath ); + assert( QFileInfo( destPath ).isSymLink() ); +} + +void TestTrash::moveFromTrash( const QString& fileId, const QString& destPath, const QString& relativePath ) +{ + KURL src( "trash:/0-" + fileId ); + if ( !relativePath.isEmpty() ) + src.addPath( relativePath ); + KURL dest; + dest.setPath( destPath ); + + assert( KIO::NetAccess::exists( src, true, (QWidget*)0 ) ); + + // A dnd would use move(), but we use moveAs to ensure the final filename + KIO::Job* job = KIO::moveAs( src, dest ); + bool ok = KIO::NetAccess::synchronousRun( job, 0 ); + assert( ok ); + QString infoFile( m_trashDir + "/info/" + fileId + ".trashinfo" ); + assert( !QFile::exists( infoFile ) ); + + QFileInfo filesItem( m_trashDir + "/files/" + fileId ); + assert( !filesItem.exists() ); + + assert( QFile::exists( destPath ) ); +} + +void TestTrash::moveFileFromTrash() +{ + kdDebug() << k_funcinfo << endl; + const QString destPath = otherTmpDir() + "fileFromHome_restored"; + moveFromTrash( "fileFromHome", destPath ); + assert( QFileInfo( destPath ).isFile() ); + assert( QFileInfo( destPath ).size() == 12 ); + + // trash it again for later + const QString fileName = "fileFromHome"; + trashFile( homeTmpDir() + fileName, fileName ); +} + +void TestTrash::moveFileInDirectoryFromTrash() +{ + kdDebug() << k_funcinfo << endl; + const QString destPath = otherTmpDir() + "testfile_restored"; + copyFromTrash( "trashDirFromHome", destPath, "testfile" ); + assert( QFileInfo( destPath ).isFile() ); + assert( QFileInfo( destPath ).size() == 12 ); +} + +void TestTrash::moveDirectoryFromTrash() +{ + kdDebug() << k_funcinfo << endl; + const QString destPath = otherTmpDir() + "trashDirFromHome_restored"; + moveFromTrash( "trashDirFromHome", destPath ); + assert( QFileInfo( destPath ).isDir() ); + + // trash it again, we'll need it later + QString dirName = "trashDirFromHome"; + trashDirectory( homeTmpDir() + dirName, dirName ); +} + +void TestTrash::trashDirectoryOwnedByRoot() +{ + KURL u; + if ( QFile::exists( "/etc/cups" ) ) + u.setPath( "/etc/cups" ); + else if ( QFile::exists( "/boot" ) ) + u.setPath( "/boot" ); + else + u.setPath( "/etc" ); + const QString fileId = u.path(); + kdDebug() << k_funcinfo << "fileId=" << fileId << endl; + + KIO::CopyJob* job = KIO::move( u, "trash:/" ); + job->setInteractive( false ); // no skip dialog, thanks + QMap<QString, QString> metaData; + bool ok = KIO::NetAccess::synchronousRun( job, 0, 0, 0, &metaData ); + assert( !ok ); + const int err = KIO::NetAccess::lastError(); + assert( err == KIO::ERR_ACCESS_DENIED + || err == KIO::ERR_CANNOT_OPEN_FOR_READING ); + + const QString infoPath( m_trashDir + "/info/" + fileId + ".trashinfo" ); + assert( !QFile::exists( infoPath ) ); + + QFileInfo files( m_trashDir + "/files/" + fileId ); + assert( !files.exists() ); + + assert( QFile::exists( u.path() ) ); +} + +void TestTrash::moveSymlinkFromTrash() +{ + kdDebug() << k_funcinfo << endl; + const QString destPath = otherTmpDir() + "symlinkFromHome_restored"; + moveFromTrash( "symlinkFromHome", destPath ); + assert( QFileInfo( destPath ).isSymLink() ); +} + +void TestTrash::getFile() +{ + kdDebug() << k_funcinfo << endl; + const QString fileId = "fileFromHome_1"; + const KURL url = TrashImpl::makeURL( 0, fileId, QString::null ); + QString tmpFile; + bool ok = KIO::NetAccess::download( url, tmpFile, 0 ); + assert( ok ); + QFile file( tmpFile ); + ok = file.open( IO_ReadOnly ); + assert( ok ); + QByteArray str = file.readAll(); + QCString cstr( str.data(), str.size() + 1 ); + if ( cstr != "Hello world\n" ) + kdFatal() << "get() returned the following data:" << cstr << endl; + file.close(); + KIO::NetAccess::removeTempFile( tmpFile ); +} + +void TestTrash::restoreFile() +{ + kdDebug() << k_funcinfo << endl; + const QString fileId = "fileFromHome_1"; + const KURL url = TrashImpl::makeURL( 0, fileId, QString::null ); + const QString infoFile( m_trashDir + "/info/" + fileId + ".trashinfo" ); + const QString filesItem( m_trashDir + "/files/" + fileId ); + + assert( QFile::exists( infoFile ) ); + assert( QFile::exists( filesItem ) ); + + QByteArray packedArgs; + QDataStream stream( packedArgs, IO_WriteOnly ); + stream << (int)3 << url; + KIO::Job* job = KIO::special( url, packedArgs ); + bool ok = KIO::NetAccess::synchronousRun( job, 0 ); + assert( ok ); + + assert( !QFile::exists( infoFile ) ); + assert( !QFile::exists( filesItem ) ); + + const QString destPath = homeTmpDir() + "fileFromHome"; + assert( QFile::exists( destPath ) ); +} + +void TestTrash::restoreFileFromSubDir() +{ + kdDebug() << k_funcinfo << endl; + const QString fileId = "trashDirFromHome_1/testfile"; + assert( !QFile::exists( homeTmpDir() + "trashDirFromHome_1" ) ); + + const KURL url = TrashImpl::makeURL( 0, fileId, QString::null ); + const QString infoFile( m_trashDir + "/info/trashDirFromHome_1.trashinfo" ); + const QString filesItem( m_trashDir + "/files/trashDirFromHome_1/testfile" ); + + assert( QFile::exists( infoFile ) ); + assert( QFile::exists( filesItem ) ); + + QByteArray packedArgs; + QDataStream stream( packedArgs, IO_WriteOnly ); + stream << (int)3 << url; + KIO::Job* job = KIO::special( url, packedArgs ); + bool ok = KIO::NetAccess::synchronousRun( job, 0 ); + assert( !ok ); + // dest dir doesn't exist -> error message + assert( KIO::NetAccess::lastError() == KIO::ERR_SLAVE_DEFINED ); + + // check that nothing happened + assert( QFile::exists( infoFile ) ); + assert( QFile::exists( filesItem ) ); + assert( !QFile::exists( homeTmpDir() + "trashDirFromHome_1" ) ); +} + +void TestTrash::restoreFileToDeletedDirectory() +{ + kdDebug() << k_funcinfo << endl; + // Ensure we'll get "fileFromHome" as fileId + removeFile( m_trashDir, "/info/fileFromHome.trashinfo" ); + removeFile( m_trashDir, "/files/fileFromHome" ); + trashFileFromHome(); + // Delete orig dir + bool delOK = KIO::NetAccess::del( homeTmpDir(), 0 ); + assert( delOK ); + + const QString fileId = "fileFromHome"; + const KURL url = TrashImpl::makeURL( 0, fileId, QString::null ); + const QString infoFile( m_trashDir + "/info/" + fileId + ".trashinfo" ); + const QString filesItem( m_trashDir + "/files/" + fileId ); + + assert( QFile::exists( infoFile ) ); + assert( QFile::exists( filesItem ) ); + + QByteArray packedArgs; + QDataStream stream( packedArgs, IO_WriteOnly ); + stream << (int)3 << url; + KIO::Job* job = KIO::special( url, packedArgs ); + bool ok = KIO::NetAccess::synchronousRun( job, 0 ); + assert( !ok ); + // dest dir doesn't exist -> error message + assert( KIO::NetAccess::lastError() == KIO::ERR_SLAVE_DEFINED ); + + // check that nothing happened + assert( QFile::exists( infoFile ) ); + assert( QFile::exists( filesItem ) ); + + const QString destPath = homeTmpDir() + "fileFromHome"; + assert( !QFile::exists( destPath ) ); +} + +void TestTrash::listRootDir() +{ + kdDebug() << k_funcinfo << endl; + m_entryCount = 0; + m_listResult.clear(); + KIO::ListJob* job = KIO::listDir( "trash:/" ); + connect( job, SIGNAL( entries( KIO::Job*, const KIO::UDSEntryList& ) ), + SLOT( slotEntries( KIO::Job*, const KIO::UDSEntryList& ) ) ); + bool ok = KIO::NetAccess::synchronousRun( job, 0 ); + assert( ok ); + kdDebug() << "listDir done - m_entryCount=" << m_entryCount << endl; + assert( m_entryCount > 1 ); + + kdDebug() << k_funcinfo << m_listResult << endl; + assert( m_listResult.contains( "." ) == 1 ); // found it, and only once +} + +void TestTrash::listRecursiveRootDir() +{ + kdDebug() << k_funcinfo << endl; + m_entryCount = 0; + m_listResult.clear(); + KIO::ListJob* job = KIO::listRecursive( "trash:/" ); + connect( job, SIGNAL( entries( KIO::Job*, const KIO::UDSEntryList& ) ), + SLOT( slotEntries( KIO::Job*, const KIO::UDSEntryList& ) ) ); + bool ok = KIO::NetAccess::synchronousRun( job, 0 ); + assert( ok ); + kdDebug() << "listDir done - m_entryCount=" << m_entryCount << endl; + assert( m_entryCount > 1 ); + + kdDebug() << k_funcinfo << m_listResult << endl; + assert( m_listResult.contains( "." ) == 1 ); // found it, and only once +} + +void TestTrash::listSubDir() +{ + kdDebug() << k_funcinfo << endl; + m_entryCount = 0; + m_listResult.clear(); + KIO::ListJob* job = KIO::listDir( "trash:/0-trashDirFromHome" ); + connect( job, SIGNAL( entries( KIO::Job*, const KIO::UDSEntryList& ) ), + SLOT( slotEntries( KIO::Job*, const KIO::UDSEntryList& ) ) ); + bool ok = KIO::NetAccess::synchronousRun( job, 0 ); + assert( ok ); + kdDebug() << "listDir done - m_entryCount=" << m_entryCount << endl; + assert( m_entryCount == 2 ); + + kdDebug() << k_funcinfo << m_listResult << endl; + assert( m_listResult.contains( "." ) == 1 ); // found it, and only once + assert( m_listResult.contains( "testfile" ) == 1 ); // found it, and only once +} + +void TestTrash::slotEntries( KIO::Job*, const KIO::UDSEntryList& lst ) +{ + for( KIO::UDSEntryList::ConstIterator it = lst.begin(); it != lst.end(); ++it ) { + KIO::UDSEntry::ConstIterator it2 = (*it).begin(); + QString displayName; + KURL url; + for( ; it2 != (*it).end(); it2++ ) { + switch ((*it2).m_uds) { + case KIO::UDS_NAME: + displayName = (*it2).m_str; + break; + case KIO::UDS_URL: + url = (*it2).m_str; + break; + } + } + kdDebug() << k_funcinfo << displayName << " " << url << endl; + if ( !url.isEmpty() ) { + assert( url.protocol() == "trash" ); + } + m_listResult << displayName; + } + m_entryCount += lst.count(); +} + +void TestTrash::emptyTrash() +{ + // ## Even though we use a custom XDG_DATA_HOME value, emptying the + // trash would still empty the other trash directories in other partitions. + // So we can't activate this test by default. +#if 0 + kdDebug() << k_funcinfo << endl; + QByteArray packedArgs; + QDataStream stream( packedArgs, IO_WriteOnly ); + stream << (int)1; + KIO::Job* job = KIO::special( "trash:/", packedArgs ); + bool ok = KIO::NetAccess::synchronousRun( job, 0 ); + assert( ok ); + + KSimpleConfig cfg( "trashrc", true ); + assert( cfg.hasGroup( "Status" ) ); + cfg.setGroup( "Status" ); + assert( cfg.readBoolEntry( "Empty", false ) == true ); + + assert( !QFile::exists( m_trashDir + "/files/fileFromHome" ) ); + assert( !QFile::exists( m_trashDir + "/files/readonly" ) ); + assert( !QFile::exists( m_trashDir + "/info/readonly.trashinfo" ) ); + +#else + kdDebug() << k_funcinfo << " : SKIPPED" << endl; +#endif +} + +#include "testtrash.moc" diff --git a/kioslave/trash/testtrash.h b/kioslave/trash/testtrash.h new file mode 100644 index 000000000..70d06dc8b --- /dev/null +++ b/kioslave/trash/testtrash.h @@ -0,0 +1,118 @@ +/* This file is part of the KDE project + Copyright (C) 2004 David Faure <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef TESTTRASH_H +#define TESTTRASH_H + +#include <qobject.h> + +class TestTrash : public QObject +{ + Q_OBJECT + +public: + TestTrash() {} + void setup(); + void cleanTrash(); + void runAll(); + + // tests + + void urlTestFile(); + void urlTestDirectory(); + void urlTestSubDirectory(); + + void trashFileFromHome(); + void trashPercentFileFromHome(); + void trashUtf8FileFromHome(); + void trashUmlautFileFromHome(); + void testTrashNotEmpty(); + void trashFileFromOther(); + void trashFileIntoOtherPartition(); + void trashFileOwnedByRoot(); + void trashSymlinkFromHome(); + void trashSymlinkFromOther(); + void trashBrokenSymlinkFromHome(); + void trashDirectoryFromHome(); + void trashReadOnlyDirFromHome(); + void trashDirectoryFromOther(); + void trashDirectoryOwnedByRoot(); + + void tryRenameInsideTrash(); + + void statRoot(); + void statFileInRoot(); + void statDirectoryInRoot(); + void statSymlinkInRoot(); + void statFileInDirectory(); + + void copyFileFromTrash(); + void copyFileInDirectoryFromTrash(); + void copyDirectoryFromTrash(); + void copySymlinkFromTrash(); + + void moveFileFromTrash(); + void moveFileInDirectoryFromTrash(); + void moveDirectoryFromTrash(); + void moveSymlinkFromTrash(); + + void listRootDir(); + void listRecursiveRootDir(); + void listSubDir(); + + void delRootFile(); + void delFileInDirectory(); + void delDirectory(); + + void getFile(); + void restoreFile(); + void restoreFileFromSubDir(); + void restoreFileToDeletedDirectory(); + + void emptyTrash(); + +private slots: + void slotEntries( KIO::Job*, const KIO::UDSEntryList& ); + +private: + void trashFile( const QString& origFilePath, const QString& fileId ); + void trashSymlink( const QString& origFilePath, const QString& fileName, bool broken ); + void trashDirectory( const QString& origPath, const QString& fileName ); + void copyFromTrash( const QString& fileId, const QString& destPath, const QString& relativePath = QString::null ); + void moveFromTrash( const QString& fileId, const QString& destPath, const QString& relativePath = QString::null ); + + QString homeTmpDir() const; + QString otherTmpDir() const; + QString utf8FileName() const; + QString umlautFileName() const; + QString readOnlyDirPath() const; + + QString m_trashDir; + + QString m_otherPartitionTopDir; + QString m_otherPartitionTrashDir; + bool m_tmpIsWritablePartition; + int m_tmpTrashId; + int m_otherPartitionId; + + int m_entryCount; + QStringList m_listResult; +}; + +#endif diff --git a/kioslave/trash/trash.protocol b/kioslave/trash/trash.protocol new file mode 100644 index 000000000..8387fdcb7 --- /dev/null +++ b/kioslave/trash/trash.protocol @@ -0,0 +1,89 @@ +[Protocol] +exec=kio_trash +protocol=trash +input=none +output=filesystem +# AccessDate doesn't make sense. +# OTOH we need deletion date :) +listing=Name,Type,Size,Extra1,Extra2,Date,Access,Owner,Group,Link +reading=true +writing=true +makedir=false +deleting=true +linking=false +moving=true +Icon=trashcan_full +maxInstances=2 +Class=:local +renameFromFile=true +renameToFile=true +copyFromFile=true +copyToFile=true +deleteRecursive=true +fileNameUsedForCopying=Name +#TODO DocPath +ExtraNames=Original Path,Deletion Date +ExtraNames[af]=Oorspronklike gids, Uitvee datum +ExtraNames[ar]=المسار الأصلي ، تاريخ المحو +ExtraNames[be]=Арыгінальнае месцазнаходжанне,Час выдалення +ExtraNames[bg]=Местоположение, дата на изтриване +ExtraNames[bn]=পূর্বতন পাথ, মোছার তারিখ +ExtraNames[bs]=Originalni put,Datum brisanja +ExtraNames[ca]=Camí original,Data d'esborrat +ExtraNames[cs]=Původní cesta,Datum smazání +ExtraNames[csb]=Òriginalnô stegna,Datum remniãcô +ExtraNames[da]=Original sti, sletningsdato +ExtraNames[de]=Ursprünglicher Pfad, Löschzeitpunkt +ExtraNames[el]=Αρχική διαδρομή,Ημερομηνία διαγραφής +ExtraNames[eo]=Originala Vojo,Forigo Dato +ExtraNames[es]=Ruta original,Fecha de borrado +ExtraNames[et]=Algne asukoht,Kustutamisaeg +ExtraNames[eu]=Jatorrizko bideizena, ezabatzeko data +ExtraNames[fa]=مسیر اصلی، تاریخ حذف +ExtraNames[fi]=Alkuperäinen polku, poistopäivä +ExtraNames[fr]=Emplacement d'origine, date de suppression +ExtraNames[fy]=Oarspronklike lokaasje,datum fan wiskjen +ExtraNames[ga]=Bunchonair,Dáta Scriosta +ExtraNames[gl]=Rota Orixinal,Data de Eliminación +ExtraNames[he]=נתיב מקורי, תאריך מחיקה +ExtraNames[hi]=मूल पथ, मिटाने की तिथि +ExtraNames[hr]=Izvorna putanja,Datum brisanja +ExtraNames[hu]=Eredeti elérési út,Törlési dátum +ExtraNames[is]=Upprunaleg slóð,dagsetning á eyðingu +ExtraNames[it]=Percorso originale,ora di eliminazione +ExtraNames[ja]=元のパス,削除日 +ExtraNames[ka]=საწყისი გეზი, წაშლის თარიღი +ExtraNames[kk]=Бұрынғы жолы, Өшірілген кезі +ExtraNames[km]=ផ្លូវលំនាំដើម កាលបរិច្ឆេទលុប +ExtraNames[ko]=원본 경로,삭제 날짜 +ExtraNames[lt]=Originalus kelias,Trynimo data +ExtraNames[lv]=Orģinālais ceļš,Dzēšanas datums +ExtraNames[mk]=Оригинална патека,Датум на бришење +ExtraNames[ms]=Laluan Asli, Tarikh Penghapusan +ExtraNames[nb]=Opprinnelig sti, slettedato +ExtraNames[nds]=Orginaalpadd,Wegdodatum +ExtraNames[ne]=मौलिक मार्ग, मेट्ने मिति +ExtraNames[nl]=Oorspronkelijke locatie,Datum van verwijdering +ExtraNames[nn]=Opprinneleg stig, slettedato +ExtraNames[pa]=ਅਸਲੀ ਮਾਰਗ,ਹਟਾਉਣ ਮਿਤੀ +ExtraNames[pl]=Ścieżka oryginalna,Data usunięcia +ExtraNames[pt]=Localização Original,Data de Remoção +ExtraNames[pt_BR]=Caminho Original,Data da Remoção +ExtraNames[ro]=Calea originală,Data ștergerii +ExtraNames[ru]=Исходный путь, дата удаления +ExtraNames[rw]=Inzira Mwimerere, Itariki y'Ihanagura +ExtraNames[sl]=Prvotna pot,Datum brisanja +ExtraNames[sr]=првобитна путања,датум брисања +ExtraNames[sr@Latn]=prvobitna putanja,datum brisanja +ExtraNames[sv]=Ursprunglig sökväg,Borttagsdatum +ExtraNames[ta]=மூல பாதை,நீக்கப்பட்ட தேதி +ExtraNames[te]=అసలు దారు, తీసివేసిన తేది +ExtraNames[th]=พาธเดิม,วันที่ที่ลบ +ExtraNames[tr]=Orjinal Yol, Silinme Tarihi +ExtraNames[tt]=Çığanaq Yulı,Beterelü Çağı +ExtraNames[uk]=Шлях,Дата вилучення +ExtraNames[vi]=Đường dẫn Trước khi vứt,Ngày Vứt +ExtraNames[wa]=Oridjinå tchmin,Date di disfaçaedje +ExtraNames[zh_CN]=原始路径,删除日期 +ExtraNames[zh_TW]=原始路徑,刪除日期 +ExtraTypes=QString,QDateTime diff --git a/kioslave/trash/trashimpl.cpp b/kioslave/trash/trashimpl.cpp new file mode 100644 index 000000000..9507c77ab --- /dev/null +++ b/kioslave/trash/trashimpl.cpp @@ -0,0 +1,962 @@ +/* This file is part of the KDE project + Copyright (C) 2004 David Faure <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "trashimpl.h" +#include <klocale.h> +#include <klargefile.h> +#include <kio/global.h> +#include <kio/renamedlg.h> +#include <kio/job.h> +#include <kdebug.h> +#include <kurl.h> +#include <kdirnotify_stub.h> +#include <kglobal.h> +#include <kstandarddirs.h> +#include <kglobalsettings.h> +#include <kmountpoint.h> +#include <kfileitem.h> +#include <kio/chmodjob.h> + +#include <dcopref.h> + +#include <qapplication.h> +#include <qeventloop.h> +#include <qfile.h> +#include <qdir.h> + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/param.h> +#include <fcntl.h> +#include <unistd.h> +#include <dirent.h> +#include <stdlib.h> +#include <errno.h> + +TrashImpl::TrashImpl() : + QObject(), + m_lastErrorCode( 0 ), + m_initStatus( InitToBeDone ), + m_lastId( 0 ), + m_homeDevice( 0 ), + m_trashDirectoriesScanned( false ), + m_mibEnum( KGlobal::locale()->fileEncodingMib() ), + // not using kio_trashrc since KIO uses that one already for kio_trash + // so better have a separate one, for faster parsing by e.g. kmimetype.cpp + m_config( "trashrc" ) +{ + KDE_struct_stat buff; + if ( KDE_lstat( QFile::encodeName( QDir::homeDirPath() ), &buff ) == 0 ) { + m_homeDevice = buff.st_dev; + } else { + kdError() << "Should never happen: couldn't stat $HOME " << strerror( errno ) << endl; + } +} + +/** + * Test if a directory exists, create otherwise + * @param _name full path of the directory + * @return errorcode, or 0 if the dir was created or existed already + * Warning, don't use return value like a bool + */ +int TrashImpl::testDir( const QString &_name ) const +{ + DIR *dp = opendir( QFile::encodeName(_name) ); + if ( dp == NULL ) + { + QString name = _name; + if ( name.endsWith( "/" ) ) + name.truncate( name.length() - 1 ); + QCString path = QFile::encodeName(name); + + bool ok = ::mkdir( path, S_IRWXU ) == 0; + if ( !ok && errno == EEXIST ) { +#if 0 // this would require to use SlaveBase's method to ask the question + //int ret = KMessageBox::warningYesNo( 0, i18n("%1 is a file, but KDE needs it to be a directory. Move it to %2.orig and create directory?").arg(name).arg(name) ); + //if ( ret == KMessageBox::Yes ) { +#endif + if ( ::rename( path, path + ".orig" ) == 0 ) { + ok = ::mkdir( path, S_IRWXU ) == 0; + } else { // foo.orig existed already. How likely is that? + ok = false; + } + if ( !ok ) { + return KIO::ERR_DIR_ALREADY_EXIST; + } +#if 0 + //} else { + // return 0; + //} +#endif + } + if ( !ok ) + { + //KMessageBox::sorry( 0, i18n( "Couldn't create directory %1. Check for permissions." ).arg( name ) ); + kdWarning() << "could not create " << name << endl; + return KIO::ERR_COULD_NOT_MKDIR; + } else { + kdDebug() << name << " created." << endl; + } + } + else // exists already + { + closedir( dp ); + } + return 0; // success +} + +bool TrashImpl::init() +{ + if ( m_initStatus == InitOK ) + return true; + if ( m_initStatus == InitError ) + return false; + + // Check the trash directory and its info and files subdirs + // see also kdesktop/init.cc for first time initialization + m_initStatus = InitError; + // $XDG_DATA_HOME/Trash, i.e. ~/.local/share/Trash by default. + const QString xdgDataDir = KGlobal::dirs()->localxdgdatadir(); + if ( !KStandardDirs::makeDir( xdgDataDir, 0700 ) ) { + kdWarning() << "failed to create " << xdgDataDir << endl; + return false; + } + + const QString trashDir = xdgDataDir + "Trash"; + int err; + if ( ( err = testDir( trashDir ) ) ) { + error( err, trashDir ); + return false; + } + if ( ( err = testDir( trashDir + "/info" ) ) ) { + error( err, trashDir + "/info" ); + return false; + } + if ( ( err = testDir( trashDir + "/files" ) ) ) { + error( err, trashDir + "/files" ); + return false; + } + m_trashDirectories.insert( 0, trashDir ); + m_initStatus = InitOK; + kdDebug() << k_funcinfo << "initialization OK, home trash dir: " << trashDir << endl; + return true; +} + +void TrashImpl::migrateOldTrash() +{ + kdDebug() << k_funcinfo << endl; + const QString oldTrashDir = KGlobalSettings::trashPath(); + const QStrList entries = listDir( oldTrashDir ); + bool allOK = true; + QStrListIterator entryIt( entries ); + for (; entryIt.current(); ++entryIt) { + QString srcPath = QFile::decodeName( *entryIt ); + if ( srcPath == "." || srcPath == ".." || srcPath == ".directory" ) + continue; + srcPath.prepend( oldTrashDir ); // make absolute + int trashId; + QString fileId; + if ( !createInfo( srcPath, trashId, fileId ) ) { + kdWarning() << "Trash migration: failed to create info for " << srcPath << endl; + allOK = false; + } else { + bool ok = moveToTrash( srcPath, trashId, fileId ); + if ( !ok ) { + (void)deleteInfo( trashId, fileId ); + kdWarning() << "Trash migration: failed to create info for " << srcPath << endl; + allOK = false; + } else { + kdDebug() << "Trash migration: moved " << srcPath << endl; + } + } + } + if ( allOK ) { + // We need to remove the old one, otherwise the desktop will have two trashcans... + kdDebug() << "Trash migration: all OK, removing old trash directory" << endl; + synchronousDel( oldTrashDir, false, true ); + } +} + +bool TrashImpl::createInfo( const QString& origPath, int& trashId, QString& fileId ) +{ + kdDebug() << k_funcinfo << origPath << endl; + // Check source + const QCString origPath_c( QFile::encodeName( origPath ) ); + KDE_struct_stat buff_src; + if ( KDE_lstat( origPath_c.data(), &buff_src ) == -1 ) { + if ( errno == EACCES ) + error( KIO::ERR_ACCESS_DENIED, origPath ); + else + error( KIO::ERR_DOES_NOT_EXIST, origPath ); + return false; + } + + // Choose destination trash + trashId = findTrashDirectory( origPath ); + if ( trashId < 0 ) { + kdWarning() << "OUCH - internal error, TrashImpl::findTrashDirectory returned " << trashId << endl; + return false; // ### error() needed? + } + kdDebug() << k_funcinfo << "trashing to " << trashId << endl; + + // Grab original filename + KURL url; + url.setPath( origPath ); + const QString origFileName = url.fileName(); + + // Make destination file in info/ + url.setPath( infoPath( trashId, origFileName ) ); // we first try with origFileName + KURL baseDirectory; + baseDirectory.setPath( url.directory() ); + // Here we need to use O_EXCL to avoid race conditions with other kioslave processes + int fd = 0; + do { + kdDebug() << k_funcinfo << "trying to create " << url.path() << endl; + fd = ::open( QFile::encodeName( url.path() ), O_WRONLY | O_CREAT | O_EXCL, 0600 ); + if ( fd < 0 ) { + if ( errno == EEXIST ) { + url.setFileName( KIO::RenameDlg::suggestName( baseDirectory, url.fileName() ) ); + // and try again on the next iteration + } else { + error( KIO::ERR_COULD_NOT_WRITE, url.path() ); + return false; + } + } + } while ( fd < 0 ); + const QString infoPath = url.path(); + fileId = url.fileName(); + Q_ASSERT( fileId.endsWith( ".trashinfo" ) ); + fileId.truncate( fileId.length() - 10 ); // remove .trashinfo from fileId + + FILE* file = ::fdopen( fd, "w" ); + if ( !file ) { // can't see how this would happen + error( KIO::ERR_COULD_NOT_WRITE, infoPath ); + return false; + } + + // Contents of the info file. We could use KSimpleConfig, but that would + // mean closing and reopening fd, i.e. opening a race condition... + QCString info = "[Trash Info]\n"; + info += "Path="; + // Escape filenames according to the way they are encoded on the filesystem + // All this to basically get back to the raw 8-bit representation of the filename... + if ( trashId == 0 ) // home trash: absolute path + info += KURL::encode_string( origPath, m_mibEnum ).latin1(); + else + info += KURL::encode_string( makeRelativePath( topDirectoryPath( trashId ), origPath ), m_mibEnum ).latin1(); + info += "\n"; + info += "DeletionDate="; + info += QDateTime::currentDateTime().toString( Qt::ISODate ).latin1(); + info += "\n"; + size_t sz = info.size() - 1; // avoid trailing 0 from QCString + + size_t written = ::fwrite(info.data(), 1, sz, file); + if ( written != sz ) { + ::fclose( file ); + QFile::remove( infoPath ); + error( KIO::ERR_DISK_FULL, infoPath ); + return false; + } + + ::fclose( file ); + + kdDebug() << k_funcinfo << "info file created in trashId=" << trashId << " : " << fileId << endl; + return true; +} + +QString TrashImpl::makeRelativePath( const QString& topdir, const QString& path ) +{ + const QString realPath = KStandardDirs::realFilePath( path ); + // topdir ends with '/' + if ( realPath.startsWith( topdir ) ) { + const QString rel = realPath.mid( topdir.length() ); + Q_ASSERT( rel[0] != '/' ); + return rel; + } else { // shouldn't happen... + kdWarning() << "Couldn't make relative path for " << realPath << " (" << path << "), with topdir=" << topdir << endl; + return realPath; + } +} + +QString TrashImpl::infoPath( int trashId, const QString& fileId ) const +{ + QString trashPath = trashDirectoryPath( trashId ); + trashPath += "/info/"; + trashPath += fileId; + trashPath += ".trashinfo"; + return trashPath; +} + +QString TrashImpl::filesPath( int trashId, const QString& fileId ) const +{ + QString trashPath = trashDirectoryPath( trashId ); + trashPath += "/files/"; + trashPath += fileId; + return trashPath; +} + +bool TrashImpl::deleteInfo( int trashId, const QString& fileId ) +{ + bool ok = QFile::remove( infoPath( trashId, fileId ) ); + if ( ok ) + fileRemoved(); + return ok; +} + +bool TrashImpl::moveToTrash( const QString& origPath, int trashId, const QString& fileId ) +{ + kdDebug() << k_funcinfo << endl; + const QString dest = filesPath( trashId, fileId ); + if ( !move( origPath, dest ) ) { + // Maybe the move failed due to no permissions to delete source. + // In that case, delete dest to keep things consistent, since KIO doesn't do it. + if ( QFileInfo( dest ).isFile() ) + QFile::remove( dest ); + else + synchronousDel( dest, false, true ); + return false; + } + fileAdded(); + return true; +} + +bool TrashImpl::moveFromTrash( const QString& dest, int trashId, const QString& fileId, const QString& relativePath ) +{ + QString src = filesPath( trashId, fileId ); + if ( !relativePath.isEmpty() ) { + src += '/'; + src += relativePath; + } + if ( !move( src, dest ) ) + return false; + return true; +} + +bool TrashImpl::move( const QString& src, const QString& dest ) +{ + if ( directRename( src, dest ) ) { + // This notification is done by KIO::moveAs when using the code below + // But if we do a direct rename we need to do the notification ourselves + KDirNotify_stub allDirNotify( "*", "KDirNotify*" ); + KURL urlDest; urlDest.setPath( dest ); + urlDest.setPath( urlDest.directory() ); + allDirNotify.FilesAdded( urlDest ); + return true; + } + if ( m_lastErrorCode != KIO::ERR_UNSUPPORTED_ACTION ) + return false; + + KURL urlSrc, urlDest; + urlSrc.setPath( src ); + urlDest.setPath( dest ); + kdDebug() << k_funcinfo << urlSrc << " -> " << urlDest << endl; + KIO::CopyJob* job = KIO::moveAs( urlSrc, urlDest, false ); +#ifdef KIO_COPYJOB_HAS_SETINTERACTIVE + job->setInteractive( false ); +#endif + connect( job, SIGNAL( result(KIO::Job *) ), + this, SLOT( jobFinished(KIO::Job *) ) ); + qApp->eventLoop()->enterLoop(); + + return m_lastErrorCode == 0; +} + +void TrashImpl::jobFinished(KIO::Job* job) +{ + kdDebug() << k_funcinfo << " error=" << job->error() << endl; + error( job->error(), job->errorText() ); + qApp->eventLoop()->exitLoop(); +} + +bool TrashImpl::copyToTrash( const QString& origPath, int trashId, const QString& fileId ) +{ + kdDebug() << k_funcinfo << endl; + const QString dest = filesPath( trashId, fileId ); + if ( !copy( origPath, dest ) ) + return false; + fileAdded(); + return true; +} + +bool TrashImpl::copyFromTrash( const QString& dest, int trashId, const QString& fileId, const QString& relativePath ) +{ + QString src = filesPath( trashId, fileId ); + if ( !relativePath.isEmpty() ) { + src += '/'; + src += relativePath; + } + return copy( src, dest ); +} + +bool TrashImpl::copy( const QString& src, const QString& dest ) +{ + // kio_file's copy() method is quite complex (in order to be fast), let's just call it... + m_lastErrorCode = 0; + KURL urlSrc; + urlSrc.setPath( src ); + KURL urlDest; + urlDest.setPath( dest ); + kdDebug() << k_funcinfo << "copying " << src << " to " << dest << endl; + KIO::CopyJob* job = KIO::copyAs( urlSrc, urlDest, false ); +#ifdef KIO_COPYJOB_HAS_SETINTERACTIVE + job->setInteractive( false ); +#endif + connect( job, SIGNAL( result( KIO::Job* ) ), + this, SLOT( jobFinished( KIO::Job* ) ) ); + qApp->eventLoop()->enterLoop(); + + return m_lastErrorCode == 0; +} + +bool TrashImpl::directRename( const QString& src, const QString& dest ) +{ + kdDebug() << k_funcinfo << src << " -> " << dest << endl; + if ( ::rename( QFile::encodeName( src ), QFile::encodeName( dest ) ) != 0 ) { + if (errno == EXDEV) { + error( KIO::ERR_UNSUPPORTED_ACTION, QString::fromLatin1("rename") ); + } else { + if (( errno == EACCES ) || (errno == EPERM)) { + error( KIO::ERR_ACCESS_DENIED, dest ); + } else if (errno == EROFS) { // The file is on a read-only filesystem + error( KIO::ERR_CANNOT_DELETE, src ); + } else { + error( KIO::ERR_CANNOT_RENAME, src ); + } + } + return false; + } + return true; +} + +#if 0 +bool TrashImpl::mkdir( int trashId, const QString& fileId, int permissions ) +{ + const QString path = filesPath( trashId, fileId ); + if ( ::mkdir( QFile::encodeName( path ), permissions ) != 0 ) { + if ( errno == EACCES ) { + error( KIO::ERR_ACCESS_DENIED, path ); + return false; + } else if ( errno == ENOSPC ) { + error( KIO::ERR_DISK_FULL, path ); + return false; + } else { + error( KIO::ERR_COULD_NOT_MKDIR, path ); + return false; + } + } else { + if ( permissions != -1 ) + ::chmod( QFile::encodeName( path ), permissions ); + } + return true; +} +#endif + +bool TrashImpl::del( int trashId, const QString& fileId ) +{ + QString info = infoPath(trashId, fileId); + QString file = filesPath(trashId, fileId); + + QCString info_c = QFile::encodeName(info); + + KDE_struct_stat buff; + if ( KDE_lstat( info_c.data(), &buff ) == -1 ) { + if ( errno == EACCES ) + error( KIO::ERR_ACCESS_DENIED, file ); + else + error( KIO::ERR_DOES_NOT_EXIST, file ); + return false; + } + + if ( !synchronousDel( file, true, QFileInfo(file).isDir() ) ) + return false; + + QFile::remove( info ); + fileRemoved(); + return true; +} + +bool TrashImpl::synchronousDel( const QString& path, bool setLastErrorCode, bool isDir ) +{ + const int oldErrorCode = m_lastErrorCode; + const QString oldErrorMsg = m_lastErrorMessage; + KURL url; + url.setPath( path ); + + // First ensure that all dirs have u+w permissions, + // otherwise we won't be able to delete files in them (#130780). + if ( isDir ) { + kdDebug() << k_funcinfo << "chmod'ing " << url << endl; + KFileItem fileItem( url, "inode/directory", KFileItem::Unknown ); + KFileItemList fileItemList; + fileItemList.append( &fileItem ); + KIO::ChmodJob* chmodJob = KIO::chmod( fileItemList, 0200, 0200, QString::null, QString::null, true /*recursive*/, false /*showProgressInfo*/ ); + connect( chmodJob, SIGNAL( result(KIO::Job *) ), + this, SLOT( jobFinished(KIO::Job *) ) ); + qApp->eventLoop()->enterLoop(); + } + + kdDebug() << k_funcinfo << "deleting " << url << endl; + KIO::DeleteJob *job = KIO::del( url, false, false ); + connect( job, SIGNAL( result(KIO::Job *) ), + this, SLOT( jobFinished(KIO::Job *) ) ); + qApp->eventLoop()->enterLoop(); + bool ok = m_lastErrorCode == 0; + if ( !setLastErrorCode ) { + m_lastErrorCode = oldErrorCode; + m_lastErrorMessage = oldErrorMsg; + } + return ok; +} + +bool TrashImpl::emptyTrash() +{ + kdDebug() << k_funcinfo << endl; + // The naive implementation "delete info and files in every trash directory" + // breaks when deleted directories contain files owned by other users. + // We need to ensure that the .trashinfo file is only removed when the + // corresponding files could indeed be removed. + + const TrashedFileInfoList fileInfoList = list(); + + TrashedFileInfoList::const_iterator it = fileInfoList.begin(); + const TrashedFileInfoList::const_iterator end = fileInfoList.end(); + for ( ; it != end ; ++it ) { + const TrashedFileInfo& info = *it; + const QString filesPath = info.physicalPath; + if ( synchronousDel( filesPath, true, true ) ) { + QFile::remove( infoPath( info.trashId, info.fileId ) ); + } // else error code is set + } + fileRemoved(); + + return m_lastErrorCode == 0; +} + +TrashImpl::TrashedFileInfoList TrashImpl::list() +{ + // Here we scan for trash directories unconditionally. This allows + // noticing plugged-in [e.g. removeable] devices, or new mounts etc. + scanTrashDirectories(); + + TrashedFileInfoList lst; + // For each known trash directory... + TrashDirMap::const_iterator it = m_trashDirectories.begin(); + for ( ; it != m_trashDirectories.end() ; ++it ) { + const int trashId = it.key(); + QString infoPath = it.data(); + infoPath += "/info"; + // Code taken from kio_file + QStrList entryNames = listDir( infoPath ); + //char path_buffer[PATH_MAX]; + //getcwd(path_buffer, PATH_MAX - 1); + //if ( chdir( infoPathEnc ) ) + // continue; + QStrListIterator entryIt( entryNames ); + for (; entryIt.current(); ++entryIt) { + QString fileName = QFile::decodeName( *entryIt ); + if ( fileName == "." || fileName == ".." ) + continue; + if ( !fileName.endsWith( ".trashinfo" ) ) { + kdWarning() << "Invalid info file found in " << infoPath << " : " << fileName << endl; + continue; + } + fileName.truncate( fileName.length() - 10 ); + + TrashedFileInfo info; + if ( infoForFile( trashId, fileName, info ) ) + lst << info; + } + } + return lst; +} + +// Returns the entries in a given directory - including "." and ".." +QStrList TrashImpl::listDir( const QString& physicalPath ) +{ + const QCString physicalPathEnc = QFile::encodeName( physicalPath ); + kdDebug() << k_funcinfo << "listing " << physicalPath << endl; + QStrList entryNames; + DIR *dp = opendir( physicalPathEnc ); + if ( dp == 0 ) + return entryNames; + KDE_struct_dirent *ep; + while ( ( ep = KDE_readdir( dp ) ) != 0L ) + entryNames.append( ep->d_name ); + closedir( dp ); + return entryNames; +} + +bool TrashImpl::infoForFile( int trashId, const QString& fileId, TrashedFileInfo& info ) +{ + kdDebug() << k_funcinfo << trashId << " " << fileId << endl; + info.trashId = trashId; // easy :) + info.fileId = fileId; // equally easy + info.physicalPath = filesPath( trashId, fileId ); + return readInfoFile( infoPath( trashId, fileId ), info, trashId ); +} + +bool TrashImpl::readInfoFile( const QString& infoPath, TrashedFileInfo& info, int trashId ) +{ + KSimpleConfig cfg( infoPath, true ); + if ( !cfg.hasGroup( "Trash Info" ) ) { + error( KIO::ERR_CANNOT_OPEN_FOR_READING, infoPath ); + return false; + } + cfg.setGroup( "Trash Info" ); + info.origPath = KURL::decode_string( cfg.readEntry( "Path" ), m_mibEnum ); + if ( info.origPath.isEmpty() ) + return false; // path is mandatory... + if ( trashId == 0 ) + Q_ASSERT( info.origPath[0] == '/' ); + else { + const QString topdir = topDirectoryPath( trashId ); // includes trailing slash + info.origPath.prepend( topdir ); + } + QString line = cfg.readEntry( "DeletionDate" ); + if ( !line.isEmpty() ) { + info.deletionDate = QDateTime::fromString( line, Qt::ISODate ); + } + return true; +} + +QString TrashImpl::physicalPath( int trashId, const QString& fileId, const QString& relativePath ) +{ + QString filePath = filesPath( trashId, fileId ); + if ( !relativePath.isEmpty() ) { + filePath += "/"; + filePath += relativePath; + } + return filePath; +} + +void TrashImpl::error( int e, const QString& s ) +{ + if ( e ) + kdDebug() << k_funcinfo << e << " " << s << endl; + m_lastErrorCode = e; + m_lastErrorMessage = s; +} + +bool TrashImpl::isEmpty() const +{ + // For each known trash directory... + if ( !m_trashDirectoriesScanned ) + scanTrashDirectories(); + TrashDirMap::const_iterator it = m_trashDirectories.begin(); + for ( ; it != m_trashDirectories.end() ; ++it ) { + QString infoPath = it.data(); + infoPath += "/info"; + + DIR *dp = opendir( QFile::encodeName( infoPath ) ); + if ( dp ) + { + struct dirent *ep; + ep = readdir( dp ); + ep = readdir( dp ); // ignore '.' and '..' dirent + ep = readdir( dp ); // look for third file + closedir( dp ); + if ( ep != 0 ) { + //kdDebug() << ep->d_name << " in " << infoPath << " -> not empty" << endl; + return false; // not empty + } + } + } + return true; +} + +void TrashImpl::fileAdded() +{ + m_config.setGroup( "Status" ); + if ( m_config.readBoolEntry( "Empty", true ) == true ) { + m_config.writeEntry( "Empty", false ); + m_config.sync(); + } + // The apps showing the trash (e.g. kdesktop) will be notified + // of this change when KDirNotify::FilesAdded("trash:/") is emitted, + // which will be done by the job soon after this. +} + +void TrashImpl::fileRemoved() +{ + if ( isEmpty() ) { + m_config.setGroup( "Status" ); + m_config.writeEntry( "Empty", true ); + m_config.sync(); + } + // The apps showing the trash (e.g. kdesktop) will be notified + // of this change when KDirNotify::FilesRemoved(...) is emitted, + // which will be done by the job soon after this. +} + +int TrashImpl::findTrashDirectory( const QString& origPath ) +{ + kdDebug() << k_funcinfo << origPath << endl; + // First check if same device as $HOME, then we use the home trash right away. + KDE_struct_stat buff; + if ( KDE_lstat( QFile::encodeName( origPath ), &buff ) == 0 + && buff.st_dev == m_homeDevice ) + return 0; + + QString mountPoint = KIO::findPathMountPoint( origPath ); + const QString trashDir = trashForMountPoint( mountPoint, true ); + kdDebug() << "mountPoint=" << mountPoint << " trashDir=" << trashDir << endl; + if ( trashDir.isEmpty() ) + return 0; // no trash available on partition + int id = idForTrashDirectory( trashDir ); + if ( id > -1 ) { + kdDebug() << " known with id " << id << endl; + return id; + } + // new trash dir found, register it + // but we need stability in the trash IDs, so that restoring or asking + // for properties works even kio_trash gets killed because idle. +#if 0 + kdDebug() << k_funcinfo << "found " << trashDir << endl; + m_trashDirectories.insert( ++m_lastId, trashDir ); + if ( !mountPoint.endsWith( "/" ) ) + mountPoint += '/'; + m_topDirectories.insert( m_lastId, mountPoint ); + return m_lastId; +#endif + scanTrashDirectories(); + return idForTrashDirectory( trashDir ); +} + +void TrashImpl::scanTrashDirectories() const +{ + const KMountPoint::List lst = KMountPoint::currentMountPoints(); + for ( KMountPoint::List::ConstIterator it = lst.begin() ; it != lst.end() ; ++it ) { + const QCString str = (*it)->mountType().latin1(); + // Skip pseudo-filesystems, there's no chance we'll find a .Trash on them :) + // ## Maybe we should also skip readonly filesystems + if ( str != "proc" && str != "devfs" && str != "usbdevfs" && + str != "sysfs" && str != "devpts" && str != "subfs" /* #96259 */ && + str != "autofs" /* #101116 */ ) { + QString topdir = (*it)->mountPoint(); + QString trashDir = trashForMountPoint( topdir, false ); + if ( !trashDir.isEmpty() ) { + // OK, trashDir is a valid trash directory. Ensure it's registered. + int trashId = idForTrashDirectory( trashDir ); + if ( trashId == -1 ) { + // new trash dir found, register it + m_trashDirectories.insert( ++m_lastId, trashDir ); + kdDebug() << k_funcinfo << "found " << trashDir << " gave it id " << m_lastId << endl; + if ( !topdir.endsWith( "/" ) ) + topdir += '/'; + m_topDirectories.insert( m_lastId, topdir ); + } + } + } + } + m_trashDirectoriesScanned = true; +} + +TrashImpl::TrashDirMap TrashImpl::trashDirectories() const +{ + if ( !m_trashDirectoriesScanned ) + scanTrashDirectories(); + return m_trashDirectories; +} + +TrashImpl::TrashDirMap TrashImpl::topDirectories() const +{ + if ( !m_trashDirectoriesScanned ) + scanTrashDirectories(); + return m_topDirectories; +} + +QString TrashImpl::trashForMountPoint( const QString& topdir, bool createIfNeeded ) const +{ + // (1) Administrator-created $topdir/.Trash directory + + const QString rootTrashDir = topdir + "/.Trash"; + const QCString rootTrashDir_c = QFile::encodeName( rootTrashDir ); + // Can't use QFileInfo here since we need to test for the sticky bit + uid_t uid = getuid(); + KDE_struct_stat buff; + const uint requiredBits = S_ISVTX; // Sticky bit required + if ( KDE_lstat( rootTrashDir_c, &buff ) == 0 ) { + if ( (S_ISDIR(buff.st_mode)) // must be a dir + && (!S_ISLNK(buff.st_mode)) // not a symlink + && ((buff.st_mode & requiredBits) == requiredBits) + && (::access(rootTrashDir_c, W_OK)) + ) { + const QString trashDir = rootTrashDir + "/" + QString::number( uid ); + const QCString trashDir_c = QFile::encodeName( trashDir ); + if ( KDE_lstat( trashDir_c, &buff ) == 0 ) { + if ( (buff.st_uid == uid) // must be owned by user + && (S_ISDIR(buff.st_mode)) // must be a dir + && (!S_ISLNK(buff.st_mode)) // not a symlink + && (buff.st_mode & 0777) == 0700 ) { // rwx for user + return trashDir; + } + kdDebug() << "Directory " << trashDir << " exists but didn't pass the security checks, can't use it" << endl; + } + else if ( createIfNeeded && initTrashDirectory( trashDir_c ) ) { + return trashDir; + } + } else { + kdDebug() << "Root trash dir " << rootTrashDir << " exists but didn't pass the security checks, can't use it" << endl; + } + } + + // (2) $topdir/.Trash-$uid + const QString trashDir = topdir + "/.Trash-" + QString::number( uid ); + const QCString trashDir_c = QFile::encodeName( trashDir ); + if ( KDE_lstat( trashDir_c, &buff ) == 0 ) + { + if ( (buff.st_uid == uid) // must be owned by user + && (S_ISDIR(buff.st_mode)) // must be a dir + && (!S_ISLNK(buff.st_mode)) // not a symlink + && ((buff.st_mode & 0777) == 0700) ) { // rwx for user, ------ for group and others + + if ( checkTrashSubdirs( trashDir_c ) ) + return trashDir; + } + kdDebug() << "Directory " << trashDir << " exists but didn't pass the security checks, can't use it" << endl; + // Exists, but not useable + return QString::null; + } + if ( createIfNeeded && initTrashDirectory( trashDir_c ) ) { + return trashDir; + } + return QString::null; +} + +int TrashImpl::idForTrashDirectory( const QString& trashDir ) const +{ + // If this is too slow we can always use a reverse map... + TrashDirMap::ConstIterator it = m_trashDirectories.begin(); + for ( ; it != m_trashDirectories.end() ; ++it ) { + if ( it.data() == trashDir ) { + return it.key(); + } + } + return -1; +} + +bool TrashImpl::initTrashDirectory( const QCString& trashDir_c ) const +{ + if ( ::mkdir( trashDir_c, 0700 ) != 0 ) + return false; + // This trash dir will be useable only if the directory is owned by user. + // In theory this is the case, but not on e.g. USB keys... + uid_t uid = getuid(); + KDE_struct_stat buff; + if ( KDE_lstat( trashDir_c, &buff ) != 0 ) + return false; // huh? + if ( (buff.st_uid == uid) // must be owned by user + && ((buff.st_mode & 0777) == 0700) ) { // rwx for user, --- for group and others + + return checkTrashSubdirs( trashDir_c ); + + } else { + kdDebug() << trashDir_c << " just created, by it doesn't have the right permissions, must be a FAT partition. Removing it again." << endl; + // Not good, e.g. USB key. Delete again. + // I'm paranoid, it would be better to find a solution that allows + // to trash directly onto the USB key, but I don't see how that would + // pass the security checks. It would also make the USB key appears as + // empty when it's in fact full... + ::rmdir( trashDir_c ); + return false; + } + return true; +} + +bool TrashImpl::checkTrashSubdirs( const QCString& trashDir_c ) const +{ + // testDir currently works with a QString - ## optimize + QString trashDir = QFile::decodeName( trashDir_c ); + const QString info = trashDir + "/info"; + if ( testDir( info ) != 0 ) + return false; + const QString files = trashDir + "/files"; + if ( testDir( files ) != 0 ) + return false; + return true; +} + +QString TrashImpl::trashDirectoryPath( int trashId ) const +{ + // Never scanned for trash dirs? (This can happen after killing kio_trash + // and reusing a directory listing from the earlier instance.) + if ( !m_trashDirectoriesScanned ) + scanTrashDirectories(); + Q_ASSERT( m_trashDirectories.contains( trashId ) ); + return m_trashDirectories[trashId]; +} + +QString TrashImpl::topDirectoryPath( int trashId ) const +{ + if ( !m_trashDirectoriesScanned ) + scanTrashDirectories(); + assert( trashId != 0 ); + Q_ASSERT( m_topDirectories.contains( trashId ) ); + return m_topDirectories[trashId]; +} + +// Helper method. Creates a URL with the format trash:/trashid-fileid or +// trash:/trashid-fileid/relativePath/To/File for a file inside a trashed directory. +KURL TrashImpl::makeURL( int trashId, const QString& fileId, const QString& relativePath ) +{ + KURL url; + url.setProtocol( "trash" ); + QString path = "/"; + path += QString::number( trashId ); + path += '-'; + path += fileId; + if ( !relativePath.isEmpty() ) { + path += '/'; + path += relativePath; + } + url.setPath( path ); + return url; +} + +// Helper method. Parses a trash URL with the URL scheme defined in makeURL. +// The trash:/ URL itself isn't parsed here, must be caught by the caller before hand. +bool TrashImpl::parseURL( const KURL& url, int& trashId, QString& fileId, QString& relativePath ) +{ + if ( url.protocol() != "trash" ) + return false; + const QString path = url.path(); + int start = 0; + if ( path[0] == '/' ) // always true I hope + start = 1; + int slashPos = path.find( '-', 0 ); // don't match leading slash + if ( slashPos <= 0 ) + return false; + bool ok = false; + trashId = path.mid( start, slashPos - start ).toInt( &ok ); + Q_ASSERT( ok ); + if ( !ok ) + return false; + start = slashPos + 1; + slashPos = path.find( '/', start ); + if ( slashPos <= 0 ) { + fileId = path.mid( start ); + relativePath = QString::null; + return true; + } + fileId = path.mid( start, slashPos - start ); + relativePath = path.mid( slashPos + 1 ); + return true; +} + +#include "trashimpl.moc" diff --git a/kioslave/trash/trashimpl.h b/kioslave/trash/trashimpl.h new file mode 100644 index 000000000..2b16ec67e --- /dev/null +++ b/kioslave/trash/trashimpl.h @@ -0,0 +1,182 @@ +/* This file is part of the KDE project + Copyright (C) 2004 David Faure <[email protected]> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef TRASHIMPL_H +#define TRASHIMPL_H + +#include <kio/jobclasses.h> +#include <ksimpleconfig.h> + +#include <qstring.h> +#include <qdatetime.h> +#include <qmap.h> +#include <qvaluelist.h> +#include <qstrlist.h> +#include <assert.h> + +/** + * Implementation of all low-level operations done by kio_trash + * The structure of the trash directory follows the freedesktop.org standard <TODO URL> + */ +class TrashImpl : public QObject +{ + Q_OBJECT +public: + TrashImpl(); + + /// Check the "home" trash directory + /// This MUST be called before doing anything else + bool init(); + + /// Create info for a file to be trashed + /// Returns trashId and fileId + /// The caller is then responsible for actually trashing the file + bool createInfo( const QString& origPath, int& trashId, QString& fileId ); + + /// Delete info file for a file to be trashed + /// Usually used for undoing what createInfo did if trashing failed + bool deleteInfo( int trashId, const QString& fileId ); + + /// Moving a file or directory into the trash. The ids come from createInfo. + bool moveToTrash( const QString& origPath, int trashId, const QString& fileId ); + + /// Moving a file or directory out of the trash. The ids come from createInfo. + bool moveFromTrash( const QString& origPath, int trashId, const QString& fileId, const QString& relativePath ); + + /// Copying a file or directory into the trash. The ids come from createInfo. + bool copyToTrash( const QString& origPath, int trashId, const QString& fileId ); + + /// Copying a file or directory out of the trash. The ids come from createInfo. + bool copyFromTrash( const QString& origPath, int trashId, const QString& fileId, const QString& relativePath ); + + /// Create a top-level trashed directory + //bool mkdir( int trashId, const QString& fileId, int permissions ); + + /// Get rid of a trashed file + bool del( int trashId, const QString& fileId ); + + /// Empty trash, i.e. delete all trashed files + bool emptyTrash(); + + /// Return true if the trash is empty + bool isEmpty() const; + + struct TrashedFileInfo { + int trashId; // for the url + QString fileId; // for the url + QString physicalPath; // for stat'ing etc. + QString origPath; // from info file + QDateTime deletionDate; // from info file + }; + /// List trashed files + typedef QValueList<TrashedFileInfo> TrashedFileInfoList; + TrashedFileInfoList list(); + + /// Return the info for a given trashed file + bool infoForFile( int trashId, const QString& fileId, TrashedFileInfo& info ); + + /// Return the physicalPath for a given trashed file - helper method which + /// encapsulates the call to infoForFile. Don't use if you need more info from TrashedFileInfo. + QString physicalPath( int trashId, const QString& fileId, const QString& relativePath ); + + /// Move data from the old trash system to the new one + void migrateOldTrash(); + + /// KIO error code + int lastErrorCode() const { return m_lastErrorCode; } + QString lastErrorMessage() const { return m_lastErrorMessage; } + + QStrList listDir( const QString& physicalPath ); + + static KURL makeURL( int trashId, const QString& fileId, const QString& relativePath ); + static bool parseURL( const KURL& url, int& trashId, QString& fileId, QString& relativePath ); + + typedef QMap<int, QString> TrashDirMap; + /// @internal This method is for TestTrash only. Home trash is included (id 0). + TrashDirMap trashDirectories() const; + /// @internal This method is for TestTrash only. No entry with id 0. + TrashDirMap topDirectories() const; + +private: + /// Helper method. Moves a file or directory using the appropriate method. + bool move( const QString& src, const QString& dest ); + bool copy( const QString& src, const QString& dest ); + /// Helper method. Tries to call ::rename(src,dest) and does error handling. + bool directRename( const QString& src, const QString& dest ); + + void fileAdded(); + void fileRemoved(); + + // Warning, returns error code, not a bool + int testDir( const QString& name ) const; + void error( int e, const QString& s ); + + bool readInfoFile( const QString& infoPath, TrashedFileInfo& info, int trashId ); + + QString infoPath( int trashId, const QString& fileId ) const; + QString filesPath( int trashId, const QString& fileId ) const; + + /// Find the trash dir to use for a given file to delete, based on original path + int findTrashDirectory( const QString& origPath ); + + QString trashDirectoryPath( int trashId ) const; + QString topDirectoryPath( int trashId ) const; + + bool synchronousDel( const QString& path, bool setLastErrorCode, bool isDir ); + + void scanTrashDirectories() const; + + int idForTrashDirectory( const QString& trashDir ) const; + bool initTrashDirectory( const QCString& trashDir_c ) const; + bool checkTrashSubdirs( const QCString& trashDir_c ) const; + QString trashForMountPoint( const QString& topdir, bool createIfNeeded ) const; + static QString makeRelativePath( const QString& topdir, const QString& path ); + +private slots: + void jobFinished(KIO::Job *job); + +private: + /// Last error code stored in class to simplify API. + /// Note that this means almost no method can be const. + int m_lastErrorCode; + QString m_lastErrorMessage; + + enum { InitToBeDone, InitOK, InitError } m_initStatus; + + // A "trash directory" is a physical directory on disk, + // e.g. $HOME/.local/share/Trash/$uid or /mnt/foo/.Trash/$uid + // It has an id (number) and a path. + // The home trash has id 0. + mutable TrashDirMap m_trashDirectories; // id -> path of trash directory + mutable TrashDirMap m_topDirectories; // id -> $topdir of partition + mutable int m_lastId; + dev_t m_homeDevice; + mutable bool m_trashDirectoriesScanned; + int m_mibEnum; + + KSimpleConfig m_config; + + // We don't cache any data related to the trashed files. + // Another kioslave could change that behind our feet. + // If we want to start caching data - and avoiding some race conditions -, + // we should turn this class into a kded module and use DCOP to talk to it + // from the kioslave. +}; + +#endif |