add more spacing
[personal-kdebase.git] / runtime / kioslave / trash / trashimpl.cpp
blob57e969d3f8d0541cfba955925eb274f98f1fdf2f
1 /* This file is part of the KDE project
2 Copyright (C) 2004 David Faure <faure@kde.org>
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Library General Public
6 License as published by the Free Software Foundation; either
7 version 2 of the License, or (at your option) any later version.
9 This library is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Library General Public License for more details.
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to
16 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301, USA.
20 #include "trashimpl.h"
21 #include "discspaceutil.h"
23 #include <klocale.h>
24 #include <kde_file.h>
25 #include <kio/global.h>
26 #include <kio/renamedialog.h>
27 #include <kio/job.h>
28 #include <kio/chmodjob.h>
29 #include <kio/copyjob.h>
30 #include <kio/deletejob.h>
31 #include <kdebug.h>
32 #include <kurl.h>
33 #include <kdirnotify.h>
34 #include <kglobal.h>
35 #include <kstandarddirs.h>
36 #include <kglobalsettings.h>
37 #include <kfileitem.h>
38 #include <kconfiggroup.h>
39 #include <kmountpoint.h>
41 #include <QApplication>
42 #include <QEventLoop>
43 #include <QFile>
44 #include <QDir>
45 #include <kjobuidelegate.h>
47 #include <sys/stat.h>
48 #include <sys/types.h>
49 #include <sys/param.h>
50 #include <fcntl.h>
51 #include <unistd.h>
52 #include <dirent.h>
53 #include <stdlib.h>
54 #include <errno.h>
56 #include <solid/device.h>
57 #include <solid/block.h>
58 #include <solid/storageaccess.h>
60 TrashImpl::TrashImpl() :
61 QObject(),
62 m_lastErrorCode( 0 ),
63 m_initStatus( InitToBeDone ),
64 m_homeDevice( 0 ),
65 m_trashDirectoriesScanned( false ),
66 // not using kio_trashrc since KIO uses that one already for kio_trash
67 // so better have a separate one, for faster parsing by e.g. kmimetype.cpp
68 m_config( "trashrc" )
70 KDE_struct_stat buff;
71 if ( KDE_lstat( QFile::encodeName( QDir::homePath() ), &buff ) == 0 ) {
72 m_homeDevice = buff.st_dev;
73 } else {
74 kError() << "Should never happen: couldn't stat $HOME " << strerror( errno ) << endl;
78 /**
79 * Test if a directory exists, create otherwise
80 * @param _name full path of the directory
81 * @return errorcode, or 0 if the dir was created or existed already
82 * Warning, don't use return value like a bool
84 int TrashImpl::testDir( const QString &_name ) const
86 DIR *dp = opendir( QFile::encodeName(_name) );
87 if ( dp == NULL )
89 QString name = _name;
90 if ( name.endsWith( '/' ) )
91 name.truncate( name.length() - 1 );
92 QByteArray path = QFile::encodeName(name);
94 bool ok = KDE_mkdir( path, S_IRWXU ) == 0;
95 if ( !ok && errno == EEXIST ) {
96 #if 0 // this would require to use SlaveBase's method to ask the question
97 //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) );
98 //if ( ret == KMessageBox::Yes ) {
99 #endif
100 if ( KDE_rename( path, path + ".orig" ) == 0 ) {
101 ok = KDE_mkdir( path, S_IRWXU ) == 0;
102 } else { // foo.orig existed already. How likely is that?
103 ok = false;
105 if ( !ok ) {
106 return KIO::ERR_DIR_ALREADY_EXIST;
108 #if 0
109 //} else {
110 // return 0;
112 #endif
114 if ( !ok )
116 //KMessageBox::sorry( 0, i18n( "Could not create directory %1. Check for permissions." ).arg( name ) );
117 kWarning() << "could not create " << name ;
118 return KIO::ERR_COULD_NOT_MKDIR;
119 } else {
120 kDebug() << name << " created.";
123 else // exists already
125 closedir( dp );
127 return 0; // success
130 bool TrashImpl::init()
132 if ( m_initStatus == InitOK )
133 return true;
134 if ( m_initStatus == InitError )
135 return false;
137 // Check the trash directory and its info and files subdirs
138 // see also kdesktop/init.cc for first time initialization
139 m_initStatus = InitError;
140 // $XDG_DATA_HOME/Trash, i.e. ~/.local/share/Trash by default.
141 const QString xdgDataDir = KGlobal::dirs()->localxdgdatadir();
142 if ( !KStandardDirs::makeDir( xdgDataDir, 0700 ) ) {
143 kWarning() << "failed to create " << xdgDataDir ;
144 return false;
147 const QString trashDir = xdgDataDir + "Trash";
148 int err;
149 if ( ( err = testDir( trashDir ) ) ) {
150 error( err, trashDir );
151 return false;
153 if ( ( err = testDir( trashDir + "/info" ) ) ) {
154 error( err, trashDir + "/info" );
155 return false;
157 if ( ( err = testDir( trashDir + "/files" ) ) ) {
158 error( err, trashDir + "/files" );
159 return false;
161 m_trashDirectories.insert( 0, trashDir );
162 m_initStatus = InitOK;
163 kDebug() << "initialization OK, home trash dir: " << trashDir;
164 return true;
167 void TrashImpl::migrateOldTrash()
169 kDebug() ;
171 KConfigGroup g( KGlobal::config(), "Paths" );
172 const QString oldTrashDir = g.readPathEntry( "Trash", QString() );
174 if ( oldTrashDir.isEmpty() )
175 return;
177 const QStringList entries = listDir( oldTrashDir );
178 bool allOK = true;
179 for ( QStringList::const_iterator entryIt = entries.begin(), entryEnd = entries.end();
180 entryIt != entryEnd ; ++entryIt )
182 QString srcPath = *entryIt;
183 if ( srcPath == "." || srcPath == ".." || srcPath == ".directory" )
184 continue;
185 srcPath.prepend( oldTrashDir ); // make absolute
186 int trashId;
187 QString fileId;
188 if ( !createInfo( srcPath, trashId, fileId ) ) {
189 kWarning() << "Trash migration: failed to create info for " << srcPath ;
190 allOK = false;
191 } else {
192 bool ok = moveToTrash( srcPath, trashId, fileId );
193 if ( !ok ) {
194 (void)deleteInfo( trashId, fileId );
195 kWarning() << "Trash migration: failed to create info for " << srcPath ;
196 allOK = false;
197 } else {
198 kDebug() << "Trash migration: moved " << srcPath;
202 if ( allOK ) {
203 // We need to remove the old one, otherwise the desktop will have two trashcans...
204 kDebug() << "Trash migration: all OK, removing old trash directory";
205 synchronousDel( oldTrashDir, false, true );
209 bool TrashImpl::createInfo( const QString& origPath, int& trashId, QString& fileId )
211 kDebug() << origPath;
212 // Check source
213 const QByteArray origPath_c( QFile::encodeName( origPath ) );
215 * off_t should be 64bit on Unix systems to have large file support
216 * FIXME: on windows this gets disabled until trash gets integrated
218 #ifndef Q_OS_WIN
219 char off_t_should_be_64bit[sizeof(off_t) >= 8 ? 1:-1]; (void)off_t_should_be_64bit;
220 #endif
221 KDE_struct_stat buff_src;
222 if ( KDE_lstat( origPath_c.data(), &buff_src ) == -1 ) {
223 if ( errno == EACCES )
224 error( KIO::ERR_ACCESS_DENIED, origPath );
225 else
226 error( KIO::ERR_DOES_NOT_EXIST, origPath );
227 return false;
230 // Choose destination trash
231 trashId = findTrashDirectory( origPath );
232 if ( trashId < 0 ) {
233 kWarning() << "OUCH - internal error, TrashImpl::findTrashDirectory returned " << trashId ;
234 return false; // ### error() needed?
236 kDebug() << "trashing to " << trashId;
238 // Grab original filename
239 KUrl url;
240 url.setPath( origPath );
241 const QString origFileName = url.fileName();
243 // Make destination file in info/
244 url.setPath( infoPath( trashId, origFileName ) ); // we first try with origFileName
245 KUrl baseDirectory;
246 baseDirectory.setPath( url.directory() );
247 // Here we need to use O_EXCL to avoid race conditions with other kioslave processes
248 int fd = 0;
249 do {
250 kDebug() << "trying to create " << url.path() ;
251 fd = KDE_open( QFile::encodeName( url.path() ), O_WRONLY | O_CREAT | O_EXCL, 0600 );
252 if ( fd < 0 ) {
253 if ( errno == EEXIST ) {
254 url.setFileName( KIO::RenameDialog::suggestName( baseDirectory, url.fileName() ) );
255 // and try again on the next iteration
256 } else {
257 error( KIO::ERR_COULD_NOT_WRITE, url.path() );
258 return false;
261 } while ( fd < 0 );
262 const QString infoPath = url.path();
263 fileId = url.fileName();
264 Q_ASSERT( fileId.endsWith( ".trashinfo" ) );
265 fileId.truncate( fileId.length() - 10 ); // remove .trashinfo from fileId
267 FILE* file = ::fdopen( fd, "w" );
268 if ( !file ) { // can't see how this would happen
269 error( KIO::ERR_COULD_NOT_WRITE, infoPath );
270 return false;
273 // Contents of the info file. We could use KSimpleConfig, but that would
274 // mean closing and reopening fd, i.e. opening a race condition...
275 QByteArray info = "[Trash Info]\n";
276 info += "Path=";
277 // Escape filenames according to the way they are encoded on the filesystem
278 // All this to basically get back to the raw 8-bit representation of the filename...
279 if ( trashId == 0 ) // home trash: absolute path
280 info += QUrl::toPercentEncoding( origPath, "/" );
281 else
282 info += QUrl::toPercentEncoding( makeRelativePath( topDirectoryPath( trashId ), origPath ), "/" );
283 info += '\n';
284 info += "DeletionDate=";
285 info += QDateTime::currentDateTime().toString( Qt::ISODate ).toLatin1();
286 info += '\n';
287 size_t sz = info.size();
289 size_t written = ::fwrite(info.data(), 1, sz, file);
290 if ( written != sz ) {
291 ::fclose( file );
292 QFile::remove( infoPath );
293 error( KIO::ERR_DISK_FULL, infoPath );
294 return false;
297 ::fclose( file );
299 kDebug() << "info file created in trashId=" << trashId << " : " << fileId;
300 return true;
303 QString TrashImpl::makeRelativePath( const QString& topdir, const QString& path )
305 const QString realPath = KStandardDirs::realFilePath( path );
306 // topdir ends with '/'
307 if ( realPath.startsWith( topdir ) ) {
308 const QString rel = realPath.mid( topdir.length() );
309 Q_ASSERT( rel[0] != '/' );
310 return rel;
311 } else { // shouldn't happen...
312 kWarning() << "Couldn't make relative path for " << realPath << " (" << path << "), with topdir=" << topdir ;
313 return realPath;
317 void TrashImpl::enterLoop()
319 QEventLoop eventLoop;
320 connect(this, SIGNAL(leaveModality()),
321 &eventLoop, SLOT(quit()));
322 eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
325 QString TrashImpl::infoPath( int trashId, const QString& fileId ) const
327 QString trashPath = trashDirectoryPath( trashId );
328 trashPath += "/info/";
329 trashPath += fileId;
330 trashPath += ".trashinfo";
331 return trashPath;
334 QString TrashImpl::filesPath( int trashId, const QString& fileId ) const
336 QString trashPath = trashDirectoryPath( trashId );
337 trashPath += "/files/";
338 trashPath += fileId;
339 return trashPath;
342 bool TrashImpl::deleteInfo( int trashId, const QString& fileId )
344 bool ok = QFile::remove( infoPath( trashId, fileId ) );
345 if ( ok )
346 fileRemoved();
347 return ok;
350 bool TrashImpl::moveToTrash( const QString& origPath, int trashId, const QString& fileId )
352 kDebug() ;
353 if ( !adaptTrashSize( origPath, trashId ) )
354 return false;
356 const QString dest = filesPath( trashId, fileId );
357 if ( !move( origPath, dest ) ) {
358 // Maybe the move failed due to no permissions to delete source.
359 // In that case, delete dest to keep things consistent, since KIO doesn't do it.
360 if ( QFileInfo( dest ).isFile() )
361 QFile::remove( dest );
362 else
363 synchronousDel( dest, false, true );
364 return false;
366 fileAdded();
367 return true;
370 bool TrashImpl::moveFromTrash( const QString& dest, int trashId, const QString& fileId, const QString& relativePath )
372 QString src = filesPath( trashId, fileId );
373 if ( !relativePath.isEmpty() ) {
374 src += '/';
375 src += relativePath;
377 if ( !move( src, dest ) )
378 return false;
379 return true;
382 bool TrashImpl::move( const QString& src, const QString& dest )
384 if ( directRename( src, dest ) ) {
385 // This notification is done by KIO::moveAs when using the code below
386 // But if we do a direct rename we need to do the notification ourselves
387 org::kde::KDirNotify::emitFilesAdded( dest );
388 return true;
390 if ( m_lastErrorCode != KIO::ERR_UNSUPPORTED_ACTION )
391 return false;
393 KUrl urlSrc, urlDest;
394 urlSrc.setPath( src );
395 urlDest.setPath( dest );
396 kDebug() << urlSrc << " -> " << urlDest;
397 KIO::CopyJob* job = KIO::moveAs( urlSrc, urlDest, KIO::HideProgressInfo );
398 job->setUiDelegate(0);
399 connect( job, SIGNAL( result(KJob*) ),
400 this, SLOT( jobFinished(KJob*) ) );
401 enterLoop();
403 return m_lastErrorCode == 0;
406 void TrashImpl::jobFinished(KJob* job)
408 kDebug() << " error=" << job->error();
409 error( job->error(), job->errorText() );
410 emit leaveModality();
413 bool TrashImpl::copyToTrash( const QString& origPath, int trashId, const QString& fileId )
415 kDebug() ;
416 if ( !adaptTrashSize( origPath, trashId ) )
417 return false;
419 const QString dest = filesPath( trashId, fileId );
420 if ( !copy( origPath, dest ) )
421 return false;
422 fileAdded();
423 return true;
426 bool TrashImpl::copyFromTrash( const QString& dest, int trashId, const QString& fileId, const QString& relativePath )
428 QString src = filesPath( trashId, fileId );
429 if ( !relativePath.isEmpty() ) {
430 src += '/';
431 src += relativePath;
433 return copy( src, dest );
436 bool TrashImpl::copy( const QString& src, const QString& dest )
438 // kio_file's copy() method is quite complex (in order to be fast), let's just call it...
439 m_lastErrorCode = 0;
440 KUrl urlSrc;
441 urlSrc.setPath( src );
442 KUrl urlDest;
443 urlDest.setPath( dest );
444 kDebug() << "copying " << src << " to " << dest;
445 KIO::CopyJob* job = KIO::copyAs( urlSrc, urlDest, KIO::HideProgressInfo );
446 job->setUiDelegate(0);
447 connect( job, SIGNAL( result(KJob*) ),
448 this, SLOT( jobFinished(KJob*) ) );
449 enterLoop();
451 return m_lastErrorCode == 0;
454 bool TrashImpl::directRename( const QString& src, const QString& dest )
456 kDebug() << src << " -> " << dest;
457 if ( KDE_rename( QFile::encodeName( src ), QFile::encodeName( dest ) ) != 0 ) {
458 if (errno == EXDEV) {
459 error( KIO::ERR_UNSUPPORTED_ACTION, QString::fromLatin1("rename") );
460 } else {
461 if (( errno == EACCES ) || (errno == EPERM)) {
462 error( KIO::ERR_ACCESS_DENIED, dest );
463 } else if (errno == EROFS) { // The file is on a read-only filesystem
464 error( KIO::ERR_CANNOT_DELETE, src );
465 } else {
466 error( KIO::ERR_CANNOT_RENAME, src );
469 return false;
471 return true;
474 #if 0
475 bool TrashImplKDE_mkdir( int trashId, const QString& fileId, int permissions )
477 const QString path = filesPath( trashId, fileId );
478 if ( KDE_mkdir( QFile::encodeName( path ), permissions ) != 0 ) {
479 if ( errno == EACCES ) {
480 error( KIO::ERR_ACCESS_DENIED, path );
481 return false;
482 } else if ( errno == ENOSPC ) {
483 error( KIO::ERR_DISK_FULL, path );
484 return false;
485 } else {
486 error( KIO::ERR_COULD_NOT_MKDIR, path );
487 return false;
489 } else {
490 if ( permissions != -1 )
491 ::chmod( QFile::encodeName( path ), permissions );
493 return true;
495 #endif
497 bool TrashImpl::del( int trashId, const QString& fileId )
499 QString info = infoPath(trashId, fileId);
500 QString file = filesPath(trashId, fileId);
502 QByteArray info_c = QFile::encodeName(info);
504 KDE_struct_stat buff;
505 if ( KDE_lstat( info_c.data(), &buff ) == -1 ) {
506 if ( errno == EACCES )
507 error( KIO::ERR_ACCESS_DENIED, file );
508 else
509 error( KIO::ERR_DOES_NOT_EXIST, file );
510 return false;
513 if ( !synchronousDel( file, true, QFileInfo(file).isDir() ) )
514 return false;
516 QFile::remove( info );
517 fileRemoved();
518 return true;
521 bool TrashImpl::synchronousDel( const QString& path, bool setLastErrorCode, bool isDir )
523 const int oldErrorCode = m_lastErrorCode;
524 const QString oldErrorMsg = m_lastErrorMessage;
525 KUrl url;
526 url.setPath( path );
527 // First ensure that all dirs have u+w permissions,
528 // otherwise we won't be able to delete files in them (#130780).
529 if ( isDir ) {
530 kDebug() << "chmod'ing " << url;
531 KFileItem fileItem( url, "inode/directory", KFileItem::Unknown );
532 KFileItemList fileItemList;
533 fileItemList.append( fileItem );
534 KIO::ChmodJob* chmodJob = KIO::chmod( fileItemList, 0200, 0200, QString(), QString(), true /*recursive*/, KIO::HideProgressInfo );
535 connect( chmodJob, SIGNAL( result(KJob *) ),
536 this, SLOT( jobFinished(KJob *) ) );
537 enterLoop();
540 KIO::DeleteJob *job = KIO::del( url, KIO::HideProgressInfo );
541 connect( job, SIGNAL( result(KJob*) ),
542 this, SLOT( jobFinished(KJob*) ) );
543 enterLoop();
544 bool ok = m_lastErrorCode == 0;
545 if ( !setLastErrorCode ) {
546 m_lastErrorCode = oldErrorCode;
547 m_lastErrorMessage = oldErrorMsg;
549 return ok;
552 bool TrashImpl::emptyTrash()
554 kDebug() ;
555 // The naive implementation "delete info and files in every trash directory"
556 // breaks when deleted directories contain files owned by other users.
557 // We need to ensure that the .trashinfo file is only removed when the
558 // corresponding files could indeed be removed (#116371)
560 // On the other hand, we certainly want to remove any file that has no associated
561 // .trashinfo file for some reason (#167051)
563 QSet<QString> unremoveableFiles;
565 const TrashedFileInfoList fileInfoList = list();
567 TrashedFileInfoList::const_iterator it = fileInfoList.begin();
568 const TrashedFileInfoList::const_iterator end = fileInfoList.end();
569 for ( ; it != end ; ++it ) {
570 const TrashedFileInfo& info = *it;
571 const QString filesPath = info.physicalPath;
572 if ( synchronousDel( filesPath, true, true ) ) {
573 QFile::remove( infoPath( info.trashId, info.fileId ) );
574 } else {
575 // error code is set already
576 // just remember not to remove this file
577 unremoveableFiles.insert(filesPath);
578 kDebug() << "Unremoveable:" << filesPath;
582 // Now do the orphaned-files cleanup
583 TrashDirMap::const_iterator trit = m_trashDirectories.constBegin();
584 for (; trit != m_trashDirectories.constEnd() ; ++trit) {
585 //const int trashId = trit.key();
586 QString filesDir = trit.value();
587 filesDir += "/files";
588 Q_FOREACH(const QString& fileName, listDir(filesDir)) {
589 if (fileName == "." || fileName == "..")
590 continue;
591 const QString filePath = filesDir + '/' + fileName;
592 if (!unremoveableFiles.contains(filePath)) {
593 kWarning() << "Removing orphaned file" << filePath;
594 QFile::remove(filePath);
599 fileRemoved();
601 return m_lastErrorCode == 0;
604 TrashImpl::TrashedFileInfoList TrashImpl::list()
606 // Here we scan for trash directories unconditionally. This allows
607 // noticing plugged-in [e.g. removeable] devices, or new mounts etc.
608 scanTrashDirectories();
610 TrashedFileInfoList lst;
611 // For each known trash directory...
612 TrashDirMap::const_iterator it = m_trashDirectories.constBegin();
613 for ( ; it != m_trashDirectories.constEnd() ; ++it ) {
614 const int trashId = it.key();
615 QString infoPath = it.value();
616 infoPath += "/info";
617 // Code taken from kio_file
618 const QStringList entryNames = listDir( infoPath );
619 //char path_buffer[PATH_MAX];
620 //getcwd(path_buffer, PATH_MAX - 1);
621 //if ( chdir( infoPathEnc ) )
622 // continue;
623 for ( QStringList::const_iterator entryIt = entryNames.constBegin(), entryEnd = entryNames.constEnd();
624 entryIt != entryEnd ; ++entryIt )
626 QString fileName = *entryIt;
627 if ( fileName == "." || fileName == ".." )
628 continue;
629 if ( !fileName.endsWith( ".trashinfo" ) ) {
630 kWarning() << "Invalid info file found in " << infoPath << " : " << fileName ;
631 continue;
633 fileName.truncate( fileName.length() - 10 );
635 TrashedFileInfo info;
636 if ( infoForFile( trashId, fileName, info ) )
637 lst << info;
640 return lst;
643 // Returns the entries in a given directory - including "." and ".."
644 QStringList TrashImpl::listDir( const QString& physicalPath )
646 QDir dir( physicalPath );
647 return dir.entryList( QDir::Dirs | QDir::Files | QDir::Hidden );
650 bool TrashImpl::infoForFile( int trashId, const QString& fileId, TrashedFileInfo& info )
652 kDebug() << trashId << " " << fileId;
653 info.trashId = trashId; // easy :)
654 info.fileId = fileId; // equally easy
655 info.physicalPath = filesPath( trashId, fileId );
656 return readInfoFile( infoPath( trashId, fileId ), info, trashId );
659 bool TrashImpl::readInfoFile( const QString& infoPath, TrashedFileInfo& info, int trashId )
661 KConfig cfg( infoPath, KConfig::SimpleConfig);
662 if ( !cfg.hasGroup( "Trash Info" ) ) {
663 error( KIO::ERR_CANNOT_OPEN_FOR_READING, infoPath );
664 return false;
666 const KConfigGroup group = cfg.group( "Trash Info" );
667 info.origPath = QUrl::fromPercentEncoding( group.readEntry( "Path" ).toLatin1() );
668 if ( info.origPath.isEmpty() )
669 return false; // path is mandatory...
670 if ( trashId == 0 ) {
671 Q_ASSERT( info.origPath[0] == '/' );
672 } else {
673 const QString topdir = topDirectoryPath( trashId ); // includes trailing slash
674 info.origPath.prepend( topdir );
676 const QString line = group.readEntry( "DeletionDate" );
677 if ( !line.isEmpty() ) {
678 info.deletionDate = QDateTime::fromString( line, Qt::ISODate );
680 return true;
683 QString TrashImpl::physicalPath( int trashId, const QString& fileId, const QString& relativePath )
685 QString filePath = filesPath( trashId, fileId );
686 if ( !relativePath.isEmpty() ) {
687 filePath += '/';
688 filePath += relativePath;
690 return filePath;
693 void TrashImpl::error( int e, const QString& s )
695 if ( e )
696 kDebug() << e << " " << s;
697 m_lastErrorCode = e;
698 m_lastErrorMessage = s;
701 bool TrashImpl::isEmpty() const
703 // For each known trash directory...
704 if ( !m_trashDirectoriesScanned )
705 scanTrashDirectories();
706 TrashDirMap::const_iterator it = m_trashDirectories.constBegin();
707 for ( ; it != m_trashDirectories.constEnd() ; ++it ) {
708 QString infoPath = it.value();
709 infoPath += "/info";
711 DIR *dp = opendir( QFile::encodeName( infoPath ) );
712 if ( dp )
714 KDE_struct_dirent *ep;
715 ep = KDE_readdir( dp );
716 ep = KDE_readdir( dp ); // ignore '.' and '..' dirent
717 ep = KDE_readdir( dp ); // look for third file
718 closedir( dp );
719 if ( ep != 0 ) {
720 //kDebug() << ep->d_name << " in " << infoPath << " -> not empty";
721 return false; // not empty
725 return true;
728 void TrashImpl::fileAdded()
730 KConfigGroup group = m_config.group( "Status" );
731 if ( group.readEntry( "Empty", true) == true ) {
732 group.writeEntry( "Empty", false );
733 m_config.sync();
735 // The apps showing the trash (e.g. kdesktop) will be notified
736 // of this change when KDirNotify::FilesAdded("trash:/") is emitted,
737 // which will be done by the job soon after this.
740 void TrashImpl::fileRemoved()
742 if ( isEmpty() ) {
743 KConfigGroup group = m_config.group( "Status" );
744 group.writeEntry( "Empty", true );
745 m_config.sync();
747 // The apps showing the trash (e.g. kdesktop) will be notified
748 // of this change when KDirNotify::FilesRemoved(...) is emitted,
749 // which will be done by the job soon after this.
752 static int idForDevice(const Solid::Device& device)
754 const Solid::Block* block = device.as<Solid::Block>();
755 kDebug() << "major=" << block->deviceMajor() << " minor=" << block->deviceMinor();
756 return block->deviceMajor()*1000 + block->deviceMinor();
759 int TrashImpl::findTrashDirectory( const QString& origPath )
761 kDebug() << origPath;
762 // First check if same device as $HOME, then we use the home trash right away.
763 KDE_struct_stat buff;
764 if ( KDE_lstat( QFile::encodeName( origPath ), &buff ) == 0
765 && buff.st_dev == m_homeDevice )
766 return 0;
768 KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath( origPath );
769 if (!mp)
770 return 0;
771 QString mountPoint = mp->mountPoint();
772 const QString trashDir = trashForMountPoint( mountPoint, true );
773 kDebug() << "mountPoint=" << mountPoint << " trashDir=" << trashDir;
774 if ( trashDir.isEmpty() )
775 return 0; // no trash available on partition
776 int id = idForTrashDirectory( trashDir );
777 if ( id > -1 ) {
778 kDebug() << " known with id " << id;
779 return id;
781 // new trash dir found, register it
782 // but we need stability in the trash IDs, so that restoring or asking
783 // for properties works even kio_trash gets killed because idle.
784 #if 0
785 kDebug() << "found " << trashDir;
786 m_trashDirectories.insert( ++m_lastId, trashDir );
787 if ( !mountPoint.endsWith( '/' ) )
788 mountPoint += '/';
789 m_topDirectories.insert( m_lastId, mountPoint );
790 return m_lastId;
791 #endif
793 const QString query = "[StorageAccess.accessible == true AND StorageAccess.filePath == '"+mountPoint+"']";
794 //kDebug() << "doing solid query:" << query;
795 const QList<Solid::Device> lst = Solid::Device::listFromQuery(query);
796 //kDebug() << "got" << lst.count() << "devices";
797 if ( lst.isEmpty() ) // not a device. Maybe some tmpfs mount for instance.
798 return 0; // use the home trash instead
799 // Pretend we got exactly one...
800 const Solid::Device device = lst[0];
802 // new trash dir found, register it
803 id = idForDevice( device );
804 m_trashDirectories.insert( id, trashDir );
805 kDebug() << "found" << trashDir << "gave it id" << id;
806 if ( !mountPoint.endsWith( '/' ) )
807 mountPoint += '/';
808 m_topDirectories.insert( id, mountPoint );
810 return idForTrashDirectory( trashDir );
813 void TrashImpl::scanTrashDirectories() const
815 const QList<Solid::Device> lst = Solid::Device::listFromQuery("StorageAccess.accessible == true");
816 for ( QList<Solid::Device>::ConstIterator it = lst.begin() ; it != lst.end() ; ++it ) {
817 QString topdir = (*it).as<Solid::StorageAccess>()->filePath();
818 QString trashDir = trashForMountPoint( topdir, false );
819 if ( !trashDir.isEmpty() ) {
820 // OK, trashDir is a valid trash directory. Ensure it's registered.
821 int trashId = idForTrashDirectory( trashDir );
822 if ( trashId == -1 ) {
823 // new trash dir found, register it
824 trashId = idForDevice( *it );
825 m_trashDirectories.insert( trashId, trashDir );
826 kDebug() << "found " << trashDir << " gave it id " << trashId;
827 if ( !topdir.endsWith( '/' ) )
828 topdir += '/';
829 m_topDirectories.insert( trashId, topdir );
833 m_trashDirectoriesScanned = true;
836 TrashImpl::TrashDirMap TrashImpl::trashDirectories() const
838 if ( !m_trashDirectoriesScanned )
839 scanTrashDirectories();
840 return m_trashDirectories;
843 TrashImpl::TrashDirMap TrashImpl::topDirectories() const
845 if ( !m_trashDirectoriesScanned )
846 scanTrashDirectories();
847 return m_topDirectories;
850 QString TrashImpl::trashForMountPoint( const QString& topdir, bool createIfNeeded ) const
852 // (1) Administrator-created $topdir/.Trash directory
854 const QString rootTrashDir = topdir + "/.Trash";
855 const QByteArray rootTrashDir_c = QFile::encodeName( rootTrashDir );
856 // Can't use QFileInfo here since we need to test for the sticky bit
857 uid_t uid = getuid();
858 KDE_struct_stat buff;
859 const unsigned int requiredBits = S_ISVTX; // Sticky bit required
860 if ( KDE_lstat( rootTrashDir_c, &buff ) == 0 ) {
861 if ( (S_ISDIR(buff.st_mode)) // must be a dir
862 && (!S_ISLNK(buff.st_mode)) // not a symlink
863 && ((buff.st_mode & requiredBits) == requiredBits)
864 && (::access(rootTrashDir_c, W_OK))
866 const QString trashDir = rootTrashDir + '/' + QString::number( uid );
867 const QByteArray trashDir_c = QFile::encodeName( trashDir );
868 if ( KDE_lstat( trashDir_c, &buff ) == 0 ) {
869 if ( (buff.st_uid == uid) // must be owned by user
870 && (S_ISDIR(buff.st_mode)) // must be a dir
871 && (!S_ISLNK(buff.st_mode)) // not a symlink
872 && (buff.st_mode & 0777) == 0700 ) { // rwx for user
873 return trashDir;
875 kDebug() << "Directory " << trashDir << " exists but didn't pass the security checks, can't use it";
877 else if ( createIfNeeded && initTrashDirectory( trashDir_c ) ) {
878 return trashDir;
880 } else {
881 kDebug() << "Root trash dir " << rootTrashDir << " exists but didn't pass the security checks, can't use it";
885 // (2) $topdir/.Trash-$uid
886 const QString trashDir = topdir + "/.Trash-" + QString::number( uid );
887 const QByteArray trashDir_c = QFile::encodeName( trashDir );
888 if ( KDE_lstat( trashDir_c, &buff ) == 0 )
890 if ( (buff.st_uid == uid) // must be owned by user
891 && (S_ISDIR(buff.st_mode)) // must be a dir
892 && (!S_ISLNK(buff.st_mode)) // not a symlink
893 && ((buff.st_mode & 0777) == 0700) ) { // rwx for user, ------ for group and others
895 if ( checkTrashSubdirs( trashDir_c ) )
896 return trashDir;
898 kDebug() << "Directory " << trashDir << " exists but didn't pass the security checks, can't use it";
899 // Exists, but not useable
900 return QString();
902 if ( createIfNeeded && initTrashDirectory( trashDir_c ) ) {
903 return trashDir;
905 return QString();
908 int TrashImpl::idForTrashDirectory( const QString& trashDir ) const
910 // If this is too slow we can always use a reverse map...
911 TrashDirMap::ConstIterator it = m_trashDirectories.constBegin();
912 for ( ; it != m_trashDirectories.constEnd() ; ++it ) {
913 if ( it.value() == trashDir ) {
914 return it.key();
917 return -1;
920 bool TrashImpl::initTrashDirectory( const QByteArray& trashDir_c ) const
922 kDebug() << trashDir_c;
923 if ( KDE_mkdir( trashDir_c, 0700 ) != 0 )
924 return false;
925 kDebug() ;
926 // This trash dir will be useable only if the directory is owned by user.
927 // In theory this is the case, but not on e.g. USB keys...
928 uid_t uid = getuid();
929 KDE_struct_stat buff;
930 if ( KDE_lstat( trashDir_c, &buff ) != 0 )
931 return false; // huh?
932 if ( (buff.st_uid == uid) // must be owned by user
933 && ((buff.st_mode & 0777) == 0700) ) { // rwx for user, --- for group and others
935 return checkTrashSubdirs( trashDir_c );
937 } else {
938 kDebug() << trashDir_c << " just created, by it doesn't have the right permissions, must be a FAT partition. Removing it again.";
939 // Not good, e.g. USB key. Delete again.
940 // I'm paranoid, it would be better to find a solution that allows
941 // to trash directly onto the USB key, but I don't see how that would
942 // pass the security checks. It would also make the USB key appears as
943 // empty when it's in fact full...
944 ::rmdir( trashDir_c );
945 return false;
947 return true;
950 bool TrashImpl::checkTrashSubdirs( const QByteArray& trashDir_c ) const
952 // testDir currently works with a QString - ## optimize
953 QString trashDir = QFile::decodeName( trashDir_c );
954 const QString info = trashDir + "/info";
955 if ( testDir( info ) != 0 )
956 return false;
957 const QString files = trashDir + "/files";
958 if ( testDir( files ) != 0 )
959 return false;
960 return true;
963 QString TrashImpl::trashDirectoryPath( int trashId ) const
965 // Never scanned for trash dirs? (This can happen after killing kio_trash
966 // and reusing a directory listing from the earlier instance.)
967 if ( !m_trashDirectoriesScanned )
968 scanTrashDirectories();
969 Q_ASSERT( m_trashDirectories.contains( trashId ) );
970 return m_trashDirectories[trashId];
973 QString TrashImpl::topDirectoryPath( int trashId ) const
975 if ( !m_trashDirectoriesScanned )
976 scanTrashDirectories();
977 assert( trashId != 0 );
978 Q_ASSERT( m_topDirectories.contains( trashId ) );
979 return m_topDirectories[trashId];
982 // Helper method. Creates a URL with the format trash:/trashid-fileid or
983 // trash:/trashid-fileid/relativePath/To/File for a file inside a trashed directory.
984 KUrl TrashImpl::makeURL( int trashId, const QString& fileId, const QString& relativePath )
986 KUrl url;
987 url.setProtocol( "trash" );
988 QString path = "/";
989 path += QString::number( trashId );
990 path += '-';
991 path += fileId;
992 if ( !relativePath.isEmpty() ) {
993 path += '/';
994 path += relativePath;
996 url.setPath( path );
997 return url;
1000 // Helper method. Parses a trash URL with the URL scheme defined in makeURL.
1001 // The trash:/ URL itself isn't parsed here, must be caught by the caller before hand.
1002 bool TrashImpl::parseURL( const KUrl& url, int& trashId, QString& fileId, QString& relativePath )
1004 if ( url.protocol() != "trash" )
1005 return false;
1006 const QString path = url.path();
1007 int start = 0;
1008 if ( path[0] == '/' ) // always true I hope
1009 start = 1;
1010 int slashPos = path.indexOf( '-', 0 ); // don't match leading slash
1011 if ( slashPos <= 0 )
1012 return false;
1013 bool ok = false;
1014 trashId = path.mid( start, slashPos - start ).toInt( &ok );
1015 Q_ASSERT( ok );
1016 if ( !ok )
1017 return false;
1018 start = slashPos + 1;
1019 slashPos = path.indexOf( '/', start );
1020 if ( slashPos <= 0 ) {
1021 fileId = path.mid( start );
1022 relativePath.clear();
1023 return true;
1025 fileId = path.mid( start, slashPos - start );
1026 relativePath = path.mid( slashPos + 1 );
1027 return true;
1030 bool TrashImpl::adaptTrashSize( const QString& origPath, int trashId )
1032 KConfig config( "ktrashrc" );
1034 const QString trashPath = trashDirectoryPath( trashId );
1035 KConfigGroup group = config.group( trashPath );
1037 bool useTimeLimit = group.readEntry( "UseTimeLimit", false );
1038 bool useSizeLimit = group.readEntry( "UseSizeLimit", true );
1039 double percent = group.readEntry( "Percent", 10.0 );
1040 int actionType = group.readEntry( "LimitReachedAction", 0 );
1042 if ( useTimeLimit ) { // delete all files in trash older than X days
1043 const int maxDays = group.readEntry( "Days", 7 );
1044 const QDateTime currentDate = QDateTime::currentDateTime();
1046 const TrashedFileInfoList trashedFiles = list();
1047 for ( int i = 0; i < trashedFiles.count(); ++i ) {
1048 struct TrashedFileInfo info = trashedFiles.at( i );
1049 if ( info.trashId != trashId )
1050 continue;
1052 if ( info.deletionDate.daysTo( currentDate ) > maxDays )
1053 del( info.trashId, info.fileId );
1056 return true;
1059 if ( useSizeLimit ) { // check if size limit exceeded
1061 // calculate size of the files to be put into the trash
1062 qulonglong additionalSize = DiscSpaceUtil::sizeOfPath( origPath );
1064 DiscSpaceUtil util( trashPath + "/files/" );
1065 if ( util.usage( additionalSize ) >= percent ) {
1066 if ( actionType == 0 ) { // warn the user only
1067 m_lastErrorCode = KIO::ERR_SLAVE_DEFINED;
1068 m_lastErrorMessage = i18n( "The trash has reached its maximum size!\nCleanup the trash manually." );
1069 return false;
1070 } else {
1071 // before we start to remove any files from the trash,
1072 // check whether the new file will fit into the trash
1073 // at all...
1075 qulonglong partitionSize = util.size();
1077 if ( (((double)additionalSize/(double)partitionSize)*100) >= percent ) {
1078 m_lastErrorCode = KIO::ERR_SLAVE_DEFINED;
1079 m_lastErrorMessage = i18n( "The file is too large to be trashed." );
1080 return false;
1083 // the size of the to be trashed file is ok, so lets start removing
1084 // some other files from the trash
1086 QDir dir( trashPath + "/files" );
1087 QFileInfoList infos;
1088 if ( actionType == 1 ) // delete oldest files first
1089 infos = dir.entryInfoList( QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Time | QDir::Reversed );
1090 else if ( actionType == 2 ) // delete biggest files first
1091 infos = dir.entryInfoList( QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot, QDir::Size );
1092 else
1093 qWarning( "Should never happen!" );
1095 bool deleteFurther = true;
1096 for ( int i = 0; (i < infos.count()) && deleteFurther; ++i ) {
1097 const QFileInfo info = infos.at( i );
1099 del( trashId, info.fileName() ); // delete trashed file
1101 if ( util.usage( additionalSize ) < percent ) // check whether we have enough space now
1102 deleteFurther = false;
1108 return true;
1111 #include "trashimpl.moc"