kdelibs/kioslave/file/file.cpp
Ivailo Monev c18bacba12 kio: make KIO::UDSEntry::UDS_URL required/mandatory
if the slaves do not know what URL is being stat()-ed, listed, etc. then
what? this may make the URL different than the one originally requested (as
it should be) in case of redirection(s) for example

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2024-05-31 16:30:03 +03:00

773 lines
24 KiB
C++

/*
Copyright (C) 2000-2002 Stephan Kulow <coolo@kde.org>
Copyright (C) 2000-2002 David Faure <faure@kde.org>
Copyright (C) 2000-2002 Waldo Bastian <bastian@kde.org>
Copyright (C) 2006 Allan Sandfeld Jensen <sandfeld@kde.org>
Copyright (C) 2007 Thiago Macieira <thiago@kde.org>
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 <config.h>
#include <config-acl.h>
#include <config-kioslave-file.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <utime.h>
#include <unistd.h>
#include <string.h>
#ifdef HAVE_POSIX_ACL
# include <sys/acl.h>
# include <acl/libacl.h>
#endif
#include <QByteArray>
#include <QDateTime>
#include <QCoreApplication>
#include <QRegExp>
#include <QFile>
#include <QDirIterator>
#include <kdebug.h>
#include <kurl.h>
#include <kcomponentdata.h>
#include <kconfig.h>
#include <kconfiggroup.h>
#include <ktemporaryfile.h>
#include <klocale.h>
#include <limits.h>
#include <kshell.h>
#include <kmountpoint.h>
#include <kstandarddirs.h>
#include <kdirnotify.h>
#include <kio/ioslave_defaults.h>
#include <kde_file.h>
#include <kglobal.h>
#include <kuser.h>
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 QString &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);
entry.insert(KIO::UDSEntry::UDS_URL, path);
mode_t type;
mode_t access;
KDE_struct_stat buff;
const QByteArray _path(QFile::encodeName(path));
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"