/* Copyright (C) 2000-2002 Stephan Kulow Copyright (C) 2000-2002 David Faure Copyright (C) 2000-2002 Waldo Bastian Copyright (C) 2006 Allan Sandfeld Jensen Copyright (C) 2007 Thiago Macieira This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License (LGPL) 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. */ #define QT_NO_CAST_FROM_ASCII #include "file.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_POSIX_ACL # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KIO; #define MAX_IPC_SIZE (1024*32) #ifdef HAVE_POSIX_ACL static void appendACLAtoms(const QByteArray & path, UDSEntry& entry, mode_t type); #endif int main(int argc, char **argv) { QCoreApplication app(argc, argv); // needed for QSocketNotifier KComponentData componentData("kio_file", "kdelibs4"); (void) KGlobal::locale(); kDebug(7101) << "Starting" << getpid(); if (argc != 2) { fprintf(stderr, "Usage: kio_file app-socket\n"); exit(-1); } FileProtocol slave(argv[1]); slave.dispatchLoop(); kDebug(7101) << "Done"; return 0; } FileProtocol::FileProtocol(const QByteArray &app) : SlaveBase("file", app) { } FileProtocol::~FileProtocol() { } #ifdef HAVE_POSIX_ACL static QString aclToText(acl_t acl) { ssize_t size = 0; char* txt = acl_to_text(acl, &size); const QString ret = QString::fromLatin1(txt, size); acl_free(txt); return ret; } #endif int FileProtocol::setACL(const char *path, mode_t perm, bool directoryDefault) { int ret = 0; #ifdef HAVE_POSIX_ACL const QString ACLString = metaData(QLatin1String("ACL_STRING")); const QString defaultACLString = metaData(QLatin1String("DEFAULT_ACL_STRING")); // Empty strings mean leave as is if ( !ACLString.isEmpty() ) { acl_t acl = 0; if (ACLString == QLatin1String("ACL_DELETE")) { // user told us to delete the extended ACL, so let's write only // the minimal (UNIX permission bits) part acl = acl_from_mode( perm ); } acl = acl_from_text(ACLString.toLatin1()); if (acl_valid(acl) == 0) { // let's be safe ret = acl_set_file(path, ACL_TYPE_ACCESS, acl); kDebug(7101) << "Set ACL on:" << path << "to:" << aclToText(acl); } acl_free(acl); if (ret != 0) { return ret; // better stop trying right away } } if (directoryDefault && !defaultACLString.isEmpty()) { if (defaultACLString == QLatin1String("ACL_DELETE")) { // user told us to delete the default ACL, do so ret += acl_delete_def_file(path); } else { acl_t acl = acl_from_text(defaultACLString.toLatin1()); if (acl_valid(acl) == 0) { // let's be safe ret += acl_set_file(path, ACL_TYPE_DEFAULT, acl); kDebug(7101) << "Set Default ACL on:" << path << "to:" << aclToText(acl); } acl_free(acl); } } #else Q_UNUSED(path); Q_UNUSED(perm); Q_UNUSED(directoryDefault); #endif return ret; } void FileProtocol::chmod(const KUrl& url, int permissions) { const QString path(url.toLocalFile()); const QByteArray _path(QFile::encodeName(path)); /* FIXME: Should be atomic */ if (KDE::chmod(path, permissions) == -1 || (setACL(_path.data(), permissions, false ) == -1) || /* if not a directory, cannot set default ACLs */ (setACL(_path.data(), permissions, true ) == -1 && errno != ENOTDIR)) { switch (errno) { case EPERM: case EACCES: error(KIO::ERR_ACCESS_DENIED, path); break; #if defined(ENOTSUP) case ENOTSUP: // from setACL since chmod can't return ENOTSUP error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Setting ACL for %1", path)); break; #endif case ENOSPC: error(KIO::ERR_DISK_FULL, path); break; default: error(KIO::ERR_CANNOT_CHMOD, path); } } else { finished(); } } void FileProtocol::setModificationTime(const KUrl &url, const QDateTime &mtime) { const QString path(url.toLocalFile()); KDE_struct_stat statbuf; if (KDE::lstat(path, &statbuf) == 0) { struct utimbuf utbuf; utbuf.actime = statbuf.st_atime; // access time, unchanged utbuf.modtime = mtime.toTime_t(); // modification time if (KDE::utime(path, &utbuf) != 0) { // TODO: errno could be EACCES, EPERM, EROFS error(KIO::ERR_CANNOT_SETTIME, path); } else { finished(); } } else { error(KIO::ERR_DOES_NOT_EXIST, path); } } void FileProtocol::mkdir(const KUrl &url, int permissions) { const QString path(url.toLocalFile()); kDebug(7101) << path << "permission=" << permissions; // Remove existing file or symlink, if requested (#151851) if (metaData(QLatin1String("overwrite")) == QLatin1String("true")) { QFile::remove(path); } KDE_struct_stat buff; if (KDE::lstat(path, &buff) == -1) { if (KDE::mkdir(path, 0777 /*umask will be applied*/) != 0) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, path); return; } else if (errno == ENOSPC) { error(KIO::ERR_DISK_FULL, path); return; } else { error(KIO::ERR_COULD_NOT_MKDIR, path); return; } } else { if (permissions != -1) { chmod(url, permissions); } else { finished(); } return; } } if (S_ISDIR(buff.st_mode)) { kDebug(7101) << "ERR_DIR_ALREADY_EXIST"; error(KIO::ERR_DIR_ALREADY_EXIST, path); return; } error(KIO::ERR_FILE_ALREADY_EXIST, path); return; } void FileProtocol::get(const KUrl &url) { if (!url.isLocalFile()) { KUrl redir(url); redir.setScheme(config()->readEntry("DefaultRemoteProtocol", "sftp")); redirection(redir); finished(); return; } const QString path(url.toLocalFile()); KDE_struct_stat buff; if (KDE::stat(path, &buff) == -1 ) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, path); } else { error(KIO::ERR_DOES_NOT_EXIST, path); } return; } if (S_ISDIR(buff.st_mode)) { error(KIO::ERR_IS_DIRECTORY, path); return; } if (!S_ISREG(buff.st_mode)) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, path); return; } int fd = KDE::open(path, O_RDONLY); if (fd < 0) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, path); return; } #if HAVE_FADVISE posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL); #endif totalSize(buff.st_size); KIO::filesize_t processed_size = 0; const QString resumeOffset = metaData(QLatin1String("resume")); if (!resumeOffset.isEmpty()) { bool ok = false; KIO::fileoffset_t offset = resumeOffset.toLongLong(&ok); if (ok && (offset > 0) && (offset < buff.st_size)) { if (KDE_lseek(fd, offset, SEEK_SET) == offset) { canResume(); processed_size = offset; kDebug(7101) << "Resume offset:" << KIO::number(offset); } } } char buffer[MAX_IPC_SIZE]; while (true) { int n = ::read(fd, buffer, MAX_IPC_SIZE); if (n == -1) { if (errno == EINTR) { continue; } error(KIO::ERR_COULD_NOT_READ, path); ::close(fd); return; } if (n == 0) { break; // Finished } data(QByteArray::fromRawData(buffer, n)); processed_size += n; processedSize(processed_size); // kDebug(7101) << "Processed: " << KIO::number(processed_size); } data(QByteArray()); ::close(fd); processedSize(buff.st_size); finished(); } int write_all(int fd, const char *buf, size_t len) { while (len > 0) { ssize_t written = write(fd, buf, len); if (written < 0) { if (errno == EINTR) { continue; } return -1; } buf += written; len -= written; } return 0; } void FileProtocol::put(const KUrl &url, int _mode, KIO::JobFlags _flags) { const QString dest_orig = url.toLocalFile(); kDebug(7101) << dest_orig << "mode=" << _mode; QString dest_part(dest_orig + QLatin1String(".part")); KDE_struct_stat buff_orig; const bool bOrigExists = (KDE::lstat(dest_orig, &buff_orig) != -1); bool bPartExists = false; const bool bMarkPartial = config()->readEntry("MarkPartial", true); if (bMarkPartial) { KDE_struct_stat buff_part; bPartExists = (KDE::stat(dest_part, &buff_part ) != -1); if (bPartExists && !(_flags & KIO::Resume) && !(_flags & KIO::Overwrite) && buff_part.st_size > 0 && S_ISREG(buff_part.st_mode)) { kDebug(7101) << "calling canResume with" << KIO::number(buff_part.st_size); // Maybe use this partial file for resuming, tell about the size and the app will tell // if it's ok to resume or not. _flags |= canResume(buff_part.st_size) ? KIO::Resume : KIO::DefaultFlags; kDebug(7101) << "got answer" << (_flags & KIO::Resume); } } if (bOrigExists && !(_flags & KIO::Overwrite) && !(_flags & KIO::Resume)) { if (S_ISDIR(buff_orig.st_mode)) { error(KIO::ERR_DIR_ALREADY_EXIST, dest_orig); } else { error(KIO::ERR_FILE_ALREADY_EXIST, dest_orig); } return; } int result = -1; QString dest; int fd = -1; // Loop until 0 (end of data) do { QByteArray buffer; dataReq(); // Request for data result = readData(buffer); if (result >= 0) { if (dest.isEmpty()) { if (bMarkPartial) { kDebug(7101) << "Appending .part extension to" << dest_orig; dest = dest_part; if (bPartExists && !(_flags & KIO::Resume)) { kDebug(7101) << "Deleting partial file" << dest_part; QFile::remove(dest_part); // Catch errors when we try to open the file. } } else { dest = dest_orig; if (bOrigExists && !(_flags & KIO::Resume)) { kDebug(7101) << "Deleting destination file" << dest_orig; QFile::remove(dest_orig); // Catch errors when we try to open the file. } } if (_flags & KIO::Resume) { fd = KDE::open(dest, O_RDWR); // append if resuming if (fd != -1) { KDE_lseek(fd, 0, SEEK_END); // Seek to end } } else { // WABA: Make sure that we keep writing permissions ourselves, // otherwise we can be in for a surprise on NFS. mode_t initialMode; if (_mode != -1) { initialMode = _mode | S_IWUSR | S_IRUSR; } else { initialMode = 0666; } fd = KDE::open(dest, O_CREAT | O_TRUNC | O_WRONLY, initialMode); } if (fd < 0) { kDebug(7101) << "####################### COULD NOT WRITE" << dest << "_mode=" << _mode; kDebug(7101) << "errno==" << errno << "(" << strerror(errno) << ")"; if (errno == EACCES) { error(KIO::ERR_WRITE_ACCESS_DENIED, dest); } else { error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); } return; } } if (write_all(fd, buffer.data(), buffer.size())) { if (errno == ENOSPC) { // disk full error(KIO::ERR_DISK_FULL, dest_orig); result = -2; // means: remove dest file } else { kWarning(7101) << "Couldn't write. Error:" << strerror(errno); error(KIO::ERR_COULD_NOT_WRITE, dest_orig); result = -1; } } } } while (result > 0); // An error occurred deal with it. if (result < 0) { kDebug(7101) << "Error during 'put'. Aborting."; if (fd != -1) { ::close(fd); } KDE_struct_stat buff; if (bMarkPartial && KDE::stat(dest, &buff) == 0) { int size = config()->readEntry("MinimumKeepSize", DEFAULT_MINIMUM_KEEP_SIZE); if (buff.st_size < size) { QFile::remove(dest); } } if (fd != -1) { return; } // falltrough when nothing was opened } if (fd == -1) { // got nothing to write out, so never opened the file finished(); return; } if (::close(fd) == -1) { kWarning(7101) << "Error when closing file descriptor:" << strerror(errno); error(KIO::ERR_COULD_NOT_WRITE, dest_orig); return; } // after full download rename the file back to original name if (bMarkPartial) { // If the original URL is a symlink and we were asked to overwrite it, // remove the symlink first. This ensures that we do not overwrite the // current source if the symlink points to it. if((_flags & KIO::Overwrite) && S_ISLNK(buff_orig.st_mode)) { QFile::remove(dest_orig); } if (KDE::rename(dest, dest_orig)) { kWarning(7101) << " Couldn't rename " << dest << " to " << dest_orig; error(KIO::ERR_CANNOT_RENAME_PARTIAL, dest_orig); return; } org::kde::KDirNotify::emitFileRenamed(dest, dest_orig); } // set final permissions if (_mode != -1 && !(_flags & KIO::Resume)) { if (KDE::chmod(dest_orig, _mode) != 0) { // couldn't chmod. Eat the error if the filesystem apparently doesn't support it. KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(dest_orig); if (mp && mp->testFileSystemFlag(KMountPoint::SupportsChmod)) { warning(i18n("Could not change permissions for\n%1" , dest_orig)); } } } // set modification time const QString mtimeStr = metaData(QLatin1String("modified")); if (!mtimeStr.isEmpty()) { QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate); if (dt.isValid()) { KDE_struct_stat dest_statbuf; if (KDE::stat(dest_orig, &dest_statbuf) == 0) { struct timeval utbuf[2]; // access time utbuf[0].tv_sec = dest_statbuf.st_atime; // access time, unchanged ## TODO preserve msec utbuf[0].tv_usec = 0; // modification time utbuf[1].tv_sec = dt.toTime_t(); utbuf[1].tv_usec = dt.time().msec() * 1000; utimes( QFile::encodeName(dest_orig), utbuf ); } } } // job done finished(); } QString FileProtocol::getUserName(uid_t uid) const { if (!mUsercache.contains(uid)) { const KUser kuser(uid); if (!kuser.isValid()) { return QString::number(uid); } mUsercache.insert(uid, kuser.loginName()); } return mUsercache[uid]; } QString FileProtocol::getGroupName(gid_t gid) const { if (!mGroupcache.contains(gid)) { const KUserGroup kusergroup(gid); if (!kusergroup.isValid()) { return QString::number(gid); } mGroupcache.insert(gid, kusergroup.name()); } return mGroupcache[gid]; } bool FileProtocol::createUDSEntry(const QString &filename, const QByteArray &path, UDSEntry &entry, short int details) { assert(entry.count() == 0); // by contract :-) // entry.reserve( 8 ); // speed up QHash insertion entry.insert(KIO::UDSEntry::UDS_NAME, filename); mode_t type; mode_t access; KDE_struct_stat buff; if (KDE_lstat(path.data(), &buff) == 0) { if (details > 2) { entry.insert(KIO::UDSEntry::UDS_DEVICE_ID, buff.st_dev); entry.insert(KIO::UDSEntry::UDS_INODE, buff.st_ino); } if (S_ISLNK(buff.st_mode)) { char buffer2[1000]; ::memset(buffer2, 0, 1000 * sizeof(char)); readlink(path.data(), buffer2, 999); entry.insert(KIO::UDSEntry::UDS_LINK_DEST, QFile::decodeName(buffer2)); // A symlink -> follow it only if details>1 if (details > 1 && KDE_stat(path.data(), &buff) == -1) { // It is a link pointing to nowhere type = S_IFMT - 1; access = S_IRWXU | S_IRWXG | S_IRWXO; entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, type); entry.insert(KIO::UDSEntry::UDS_ACCESS, access); entry.insert(KIO::UDSEntry::UDS_SIZE, 0LL); goto notype; } } } else { // kWarning() << "lstat didn't work on " << path.data(); return false; } type = buff.st_mode & S_IFMT; // extract file type access = buff.st_mode & 07777; // extract permissions entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, type); entry.insert(KIO::UDSEntry::UDS_ACCESS, access); entry.insert(KIO::UDSEntry::UDS_SIZE, buff.st_size); #ifdef HAVE_POSIX_ACL if (details > 0) { /* Append an atom indicating whether the file has extended acl information. If it's a * directory and it has a default ACL, also append that. */ appendACLAtoms(path, entry, type); } #endif notype: if (details > 1) { // In real "remote" slaves, this usually depends on the protocol but not here - it is // determined only from the mode and only for non-regular files if (S_ISDIR(type)) { entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("inode/directory")); } else if (S_ISCHR(type)) { entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("inode/chardevice")); } else if (S_ISBLK(type)) { entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("inode/blockdevice")); } else if (S_ISFIFO(type)) { entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("inode/fifo")); } else if (S_ISSOCK(type)) { entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("inode/socket")); } } if (details > 0) { entry.insert(KIO::UDSEntry::UDS_MODIFICATION_TIME, buff.st_mtime); entry.insert(KIO::UDSEntry::UDS_USER, getUserName(buff.st_uid)); entry.insert(KIO::UDSEntry::UDS_GROUP, getGroupName(buff.st_gid)); entry.insert(KIO::UDSEntry::UDS_ACCESS_TIME, buff.st_atime); } // Note: buff.st_ctime isn't the creation time ! // We made that mistake for KDE 2.0, but it's in fact the // "file status" change time, which we don't care about. return true; } /************************************* * * ACL handling helpers * *************************************/ #ifdef HAVE_POSIX_ACL static void appendACLAtoms(const QByteArray &path, UDSEntry &entry, mode_t type) { // first check for a noop if (acl_extended_file(path.data()) == 0) { return; } acl_t acl = 0; acl_t defaultAcl = 0; bool isDir = S_ISDIR(type); // do we have an acl for the file, and/or a default acl for the dir, if it is one? acl = acl_get_file(path.data(), ACL_TYPE_ACCESS); /* Sadly libacl does not provided a means of checking for extended ACL and default * ACL separately. Since a directory can have both, we need to check again. */ if (isDir) { if (acl) { if (acl_equiv_mode(acl, 0) == 0) { acl_free(acl); acl = 0; } } defaultAcl = acl_get_file(path.data(), ACL_TYPE_DEFAULT); } if (acl || defaultAcl) { kDebug(7101) << path.constData() << "has extended ACL entries"; entry.insert(KIO::UDSEntry::UDS_EXTENDED_ACL, 1); } if (acl) { const QString str = aclToText(acl); entry.insert(KIO::UDSEntry::UDS_ACL_STRING, str); kDebug(7101) << path.constData() << "ACL:" << str; } if (defaultAcl ) { const QString str = aclToText(defaultAcl); entry.insert(KIO::UDSEntry::UDS_DEFAULT_ACL_STRING, str); kDebug(7101) << path.constData() << "DEFAULT ACL:" << str; } if (acl) { acl_free(acl); } if (defaultAcl) { acl_free(defaultAcl); } } #endif // HAVE_POSIX_ACL // We could port this to KTempDir::removeDir but then we wouldn't be able to tell the user // where exactly the deletion failed, in case of errors. bool FileProtocol::deleteRecursive(const QString &path) { //kDebug() << path; QDirIterator it( path, QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden, QDirIterator::Subdirectories ); QStringList dirsToDelete; while (it.hasNext()) { const QString itemPath = it.next(); //kDebug() << "itemPath=" << itemPath; const QFileInfo info = it.fileInfo(); if (info.isDir() && !info.isSymLink()) { dirsToDelete.prepend(itemPath); } else { //kDebug() << "QFile::remove" << itemPath; if (!QFile::remove(itemPath)) { error(KIO::ERR_CANNOT_DELETE, itemPath); return false; } } } QDir dir; Q_FOREACH(const QString &itemPath, dirsToDelete) { //kDebug() << "QDir::rmdir" << itemPath; if (!dir.rmdir(itemPath)) { error(KIO::ERR_CANNOT_DELETE, itemPath); return false; } } return true; } #include "moc_file.cpp"