File indexing completed on 2024-04-14 04:52:45

0001 /*
0002     SPDX-FileCopyrightText: 2020 Elvis Angelaccio <elvis.angelaccio@kde.org>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "s3backend.h"
0007 #include "s3debug.h"
0008 #include "kio_s3.h"
0009 
0010 #include <KLocalizedString>
0011 
0012 #include <aws/core/auth/AWSCredentialsProvider.h>
0013 #include <aws/core/Aws.h>
0014 #include <aws/s3/model/Bucket.h>
0015 #include <aws/s3/model/CopyObjectRequest.h>
0016 #include <aws/s3/model/DeleteObjectRequest.h>
0017 #include <aws/s3/model/GetObjectRequest.h>
0018 #include <aws/s3/model/HeadObjectRequest.h>
0019 #include <aws/s3/model/ListObjectsV2Request.h>
0020 #include <aws/s3/model/PutObjectRequest.h>
0021 
0022 #include <array>
0023 
0024 static KIO::WorkerResult invalidUrlError() {
0025     static const auto s_invalidUrlError = KIO::WorkerResult::fail(
0026         KIO::ERR_WORKER_DEFINED,
0027         xi18nc("@info", "Invalid S3 URI, bucket name is missing from the host.<nl/>A valid S3 URI must be written in the form: <link>s3://bucket/key</link>")
0028     );
0029 
0030     return s_invalidUrlError;
0031 }
0032 
0033 S3Backend::S3Backend(S3Worker *q)
0034     : q(q)
0035 {
0036     Aws::SDKOptions options;
0037     Aws::InitAPI(options);
0038 
0039     m_configProfileName = Aws::Auth::GetConfigProfileFilename();
0040     qCDebug(S3) << "S3 backend initialized, config profile name:" << m_configProfileName.c_str();
0041 }
0042 
0043 KIO::WorkerResult S3Backend::listDir(const QUrl &url)
0044 {
0045     const auto s3url = S3Url(url);
0046     qCDebug(S3) << "Going to list" << s3url;
0047 
0048     if (s3url.isRoot()) {
0049         const bool hasBuckets = listBuckets();
0050         listCwdEntry(ReadOnlyCwd);
0051         if (hasBuckets) {
0052             return KIO::WorkerResult::pass();
0053         }
0054         return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, xi18nc("@info", "Could not find S3 buckets, please check your AWS configuration."));
0055     }
0056 
0057     if (s3url.isBucket()) {
0058         listBucket(s3url.BucketName());
0059         listCwdEntry();
0060         return KIO::WorkerResult::pass();
0061     }
0062 
0063     if (!s3url.isKey()) {
0064         qCDebug(S3) << "Could not list invalid S3 url:" << url;
0065         return invalidUrlError();
0066     }
0067 
0068     Q_ASSERT(s3url.isKey());
0069 
0070     listKey(s3url);
0071     listCwdEntry();
0072     return KIO::WorkerResult::pass();
0073 }
0074 
0075 KIO::WorkerResult S3Backend::stat(const QUrl &url)
0076 {
0077     const auto s3url = S3Url(url);
0078     qCDebug(S3) << "Going to stat()" << s3url;
0079 
0080     if (s3url.isRoot()) {
0081         return KIO::WorkerResult::pass();
0082     }
0083 
0084     if (s3url.isBucket()) {
0085         KIO::UDSEntry entry;
0086         entry.reserve(4);
0087         entry.fastInsert(KIO::UDSEntry::UDS_NAME, s3url.bucketName());
0088         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0089         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH | S_IXUSR | S_IXGRP | S_IXOTH);
0090         entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
0091         q->statEntry(entry);
0092         return KIO::WorkerResult::pass();
0093     }
0094 
0095     if (!s3url.isKey()) {
0096         qCDebug(S3) << "Could not stat invalid S3 url:" << url;
0097         return invalidUrlError();
0098     }
0099 
0100     Q_ASSERT(s3url.isKey());
0101 
0102     // Try to do an HEAD request for the key.
0103     // If the URL is a folder, S3 will reply only if there is a 0-sized object with that key.
0104     const auto pathComponents = url.path().split(QLatin1Char('/'), Qt::SkipEmptyParts);
0105     // The URL could be s3://<bucketName>/ which would have "/" as path(). Fallback to bucketName in that case.
0106     const bool isRootKey = pathComponents.isEmpty();
0107     const auto fileName = isRootKey ? s3url.bucketName() : pathComponents.last();
0108 
0109     const Aws::Client::ClientConfiguration clientConfiguration(m_configProfileName.c_str());
0110     const Aws::S3::S3Client client(clientConfiguration);
0111 
0112     Aws::S3::Model::HeadObjectRequest headObjectRequest;
0113     headObjectRequest.SetBucket(s3url.BucketName());
0114     headObjectRequest.SetKey(s3url.Key());
0115 
0116     auto headObjectRequestOutcome = client.HeadObject(headObjectRequest);
0117     if (!isRootKey && headObjectRequestOutcome.IsSuccess()) {
0118         Aws::String contentType = headObjectRequestOutcome.GetResult().GetContentType();
0119         // This is set by S3 when creating a 0-sized folder from the AWS console. Use the freedesktop mimetype instead.
0120         if (contentType == "application/x-directory") {
0121             contentType = "inode/directory";
0122         }
0123         const bool isDir = contentType == "inode/directory";
0124         KIO::UDSEntry entry;
0125         entry.reserve(7);
0126         entry.fastInsert(KIO::UDSEntry::UDS_NAME, fileName);
0127         entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, fileName);
0128         entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QString::fromLatin1(contentType.c_str(), contentType.size()));
0129         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, isDir ? S_IFDIR : S_IFREG);
0130         entry.fastInsert(KIO::UDSEntry::UDS_SIZE, headObjectRequestOutcome.GetResult().GetContentLength());
0131         if (isDir) {
0132             entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH);
0133         } else {
0134             // For keys we would need another request (GetObjectAclRequest) to get the permission,
0135             // but it is kind of pointless to map the AWS ACL model to UNIX permission anyway.
0136             // So assume keys are always writable, we'll handle the failure if they are not.
0137             // The same logic will be applied to all the other UDS_ACCESS instances for keys.
0138             entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH );
0139             const auto lastModifiedTime = headObjectRequestOutcome.GetResult().GetLastModified();
0140             entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, lastModifiedTime.SecondsWithMSPrecision());
0141         }
0142 
0143         q->statEntry(entry);
0144     } else {
0145         if (!isRootKey) {
0146             qCDebug(S3).nospace() << "Could not get HEAD object for key: " << s3url.key() << " - " << headObjectRequestOutcome.GetError().GetMessage().c_str() << " - assuming it's a folder.";
0147         }
0148         // HACK: assume this is a folder (i.e. a virtual key without associated object).
0149         // If it were a key or a 0-sized folder the HEAD request would likely have worked.
0150         // This is needed to upload local folders to S3.
0151         KIO::UDSEntry entry;
0152         entry.reserve(6);
0153         entry.fastInsert(KIO::UDSEntry::UDS_NAME, fileName);
0154         entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, fileName);
0155         entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
0156         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0157         entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0);
0158         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH);
0159         q->statEntry(entry);
0160     }
0161 
0162     return KIO::WorkerResult::pass();
0163 }
0164 
0165 KIO::WorkerResult S3Backend::mimetype(const QUrl &url)
0166 {
0167     const auto s3url = S3Url(url);
0168     qCDebug(S3) << "Going to get mimetype for" << s3url;
0169 
0170     q->mimeType(contentType(s3url));
0171     return KIO::WorkerResult::pass();
0172 }
0173 
0174 KIO::WorkerResult S3Backend::get(const QUrl &url)
0175 {
0176     const auto s3url = S3Url(url);
0177     qCDebug(S3) << "Going to get" << s3url;
0178 
0179     const Aws::Client::ClientConfiguration clientConfiguration(m_configProfileName.c_str());
0180     const Aws::S3::S3Client client(clientConfiguration);
0181 
0182     q->mimeType(contentType(s3url));
0183 
0184     Aws::S3::Model::GetObjectRequest objectRequest;
0185     objectRequest.SetBucket(s3url.BucketName());
0186     objectRequest.SetKey(s3url.Key());
0187 
0188     auto getObjectOutcome = client.GetObject(objectRequest);
0189     if (getObjectOutcome.IsSuccess()) {
0190         auto objectResult = getObjectOutcome.GetResultWithOwnership();
0191         auto& retrievedFile = objectResult.GetBody();
0192         qCDebug(S3) << "Key" << s3url.key() << "has Content-Length:" << objectResult.GetContentLength();
0193         q->totalSize(objectResult.GetContentLength());
0194 
0195         std::array<char, 1024*1024> buffer{};
0196         while (!retrievedFile.eof()) {
0197             const auto readBytes = retrievedFile.read(buffer.data(), buffer.size()).gcount();
0198             if (readBytes > 0) {
0199                 q->data(QByteArray(buffer.data(), readBytes));
0200             }
0201         }
0202 
0203         q->data(QByteArray());
0204 
0205     } else {
0206         // NOTE: normally we shouldn't get this error, because KIO does a stat() before the get() and if the url doesn't exist, our stat() assumes it's a folder.
0207         // This is why this error is not shown to users in the frontend.
0208         qCWarning(S3) << "Could not get object with key:" << s3url.key() << " - " << getObjectOutcome.GetError().GetMessage().c_str();
0209     }
0210 
0211     return KIO::WorkerResult::pass();
0212 }
0213 
0214 KIO::WorkerResult S3Backend::put(const QUrl &url, int permissions, KIO::JobFlags flags)
0215 {
0216     Q_UNUSED(permissions)
0217     Q_UNUSED(flags)
0218     const auto s3url = S3Url(url);
0219     qCDebug(S3) << "Going to upload data to" << s3url;
0220 
0221     const Aws::Client::ClientConfiguration clientConfiguration(m_configProfileName.c_str());
0222     const Aws::S3::S3Client client(clientConfiguration);
0223 
0224     Aws::S3::Model::PutObjectRequest request;
0225     request.SetBucket(s3url.BucketName());
0226     request.SetKey(s3url.Key());
0227 
0228     const auto putDataStream = std::make_shared<Aws::StringStream>("");
0229 
0230     int bytesCount = 0;
0231     int n;
0232     do {
0233         QByteArray buffer;
0234         q->dataReq();
0235         n = q->readData(buffer);
0236         if (!buffer.isEmpty()) {
0237             bytesCount += n;
0238             putDataStream->write(buffer.data(), n);
0239         }
0240     } while (n > 0);
0241 
0242     if (n < 0) {
0243         qCWarning(S3) << "Failed to upload data to" << s3url;
0244         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_WRITE, url.toDisplayString());
0245     }
0246 
0247     request.SetBody(putDataStream);
0248 
0249     auto putObjectOutcome = client.PutObject(request);
0250     if (putObjectOutcome.IsSuccess()) {
0251         qCDebug(S3) << "Uploaded" <<  bytesCount << "bytes to key:" << s3url.key();
0252     } else {
0253         qCDebug(S3) << "Could not PUT object with key:" << s3url.key() << " - " << putObjectOutcome.GetError().GetMessage().c_str();
0254     }
0255 
0256     return KIO::WorkerResult::pass();
0257 }
0258 
0259 KIO::WorkerResult S3Backend::copy(const QUrl &src, const QUrl &dest, int permissions, KIO::JobFlags flags)
0260 {
0261     Q_UNUSED(permissions)
0262     Q_UNUSED(flags)
0263 
0264     const auto s3src = S3Url(src);
0265     const auto s3dest = S3Url(dest);
0266     qCDebug(S3) << "Going to copy" << s3src << "to" << s3dest;
0267 
0268     if (src == dest) {
0269         return KIO::WorkerResult::fail(KIO::ERR_FILE_ALREADY_EXIST, QString());
0270     }
0271 
0272     if (s3src.isRoot() || s3src.isBucket()) {
0273         qCDebug(S3) << "Cannot copy from root or bucket url:" << src;
0274         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, src.toDisplayString());
0275     }
0276 
0277     if (!s3src.isKey()) {
0278         qCDebug(S3) << "Cannot copy from invalid S3 url:" << src;
0279         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, src.toDisplayString());
0280     }
0281 
0282     // TODO: can we copy to isBucket() urls?
0283     if (s3dest.isRoot() || s3dest.isBucket()) {
0284         qCDebug(S3) << "Cannot copy to root or bucket url:" << dest;
0285         return KIO::WorkerResult::fail(KIO::ERR_WRITE_ACCESS_DENIED, dest.toDisplayString());
0286     }
0287 
0288     if (!s3dest.isKey()) {
0289         qCDebug(S3) << "Cannot write to invalid S3 url:" << dest;
0290         return KIO::WorkerResult::fail(KIO::ERR_WRITE_ACCESS_DENIED, dest.toDisplayString());
0291     }
0292 
0293     const Aws::Client::ClientConfiguration clientConfiguration(m_configProfileName.c_str());
0294     const Aws::S3::S3Client client(clientConfiguration);
0295 
0296     // Check if destination key already exists, otherwise S3 will overwrite it leading to data loss.
0297     Aws::S3::Model::HeadObjectRequest headObjectRequest;
0298     headObjectRequest.SetBucket(s3dest.BucketName());
0299     headObjectRequest.SetKey(s3dest.Key());
0300     auto headObjectRequestOutcome = client.HeadObject(headObjectRequest);
0301     if (headObjectRequestOutcome.IsSuccess()) {
0302         return KIO::WorkerResult::fail(KIO::ERR_FILE_ALREADY_EXIST, QString());
0303     }
0304 
0305     Aws::S3::Model::CopyObjectRequest request;
0306     request.SetCopySource(s3src.BucketName() + "/" + s3src.Key());
0307     request.SetBucket(s3dest.BucketName());
0308     request.SetKey(s3dest.Key());
0309 
0310     auto copyObjectOutcome = client.CopyObject(request);
0311     if (!copyObjectOutcome.IsSuccess()) {
0312         qCDebug(S3) << "Could not copy" << src << "to" << dest << "- " << copyObjectOutcome.GetError().GetMessage().c_str();
0313         return KIO::WorkerResult::fail(KIO::ERR_WORKER_DEFINED, xi18nc("@info", "Could not copy <link>%1</link> to <link>%2</link>", src.toDisplayString(), dest.toDisplayString()));
0314     }
0315 
0316     return KIO::WorkerResult::pass();
0317 }
0318 
0319 KIO::WorkerResult S3Backend::mkdir(const QUrl &url, int permissions)
0320 {
0321     Q_UNUSED(url)
0322     Q_UNUSED(permissions)
0323     qCDebug(S3) << "Pretending creation of folder" << url;
0324     return KIO::WorkerResult::pass();
0325 }
0326 
0327 KIO::WorkerResult S3Backend::del(const QUrl &url, bool isFile)
0328 {
0329     Q_UNUSED(isFile)
0330     const auto s3url = S3Url(url);
0331     qCDebug(S3) << "Going to delete" << s3url;
0332 
0333     if (s3url.isRoot() || s3url.isBucket()) {
0334         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_DELETE, url.toDisplayString());
0335     }
0336 
0337     if (!s3url.isKey()) {
0338         return invalidUrlError();
0339     }
0340 
0341     const Aws::Client::ClientConfiguration clientConfiguration(m_configProfileName.c_str());
0342     const Aws::S3::S3Client client(clientConfiguration);
0343 
0344     // Start recursive delete by using the root prefix.
0345     if (deletePrefix(client, s3url, s3url.Prefix())) {
0346         return KIO::WorkerResult::pass();
0347     } else {
0348         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_DELETE, url.toDisplayString());
0349     }
0350 }
0351 
0352 KIO::WorkerResult S3Backend::rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags)
0353 {
0354     Q_UNUSED(flags)
0355     qCDebug(S3) << "Going to rename" << src << "to" << dest;
0356 
0357     // FIXME: rename of virtual folders doesn't work, because folders don't exist in S3.
0358     // This would require some special handling:
0359     // 1. detect that src is a folder
0360     // 2. list the folder
0361     // 3. rename each key listed
0362     // Workaround: copy+delete from dolphin...
0363 
0364     const auto copyResult = copy(src, dest, -1, flags);
0365     if (!copyResult.success()) {
0366         qCDebug(S3).nospace() << "Could not copy " << src << " to " << dest << ", aborting rename()";
0367         if (copyResult.error() == KIO::ERR_FILE_ALREADY_EXIST) {
0368             return copyResult;
0369         }
0370         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_RENAME, src.toDisplayString());
0371     }
0372 
0373     const auto delResult = del(src, false);
0374     if (!delResult.success()) {
0375         qCDebug(S3) << "Could not delete" << src << "after it was copied to" << dest;
0376         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_RENAME, src.toDisplayString());
0377     }
0378 
0379     return KIO::WorkerResult::pass();
0380 }
0381 
0382 bool S3Backend::listBuckets()
0383 {
0384     const Aws::Client::ClientConfiguration clientConfiguration(m_configProfileName.c_str());
0385     const Aws::S3::S3Client client(clientConfiguration);
0386     const auto listBucketsOutcome = client.ListBuckets();
0387     bool hasBuckets = false;
0388 
0389     if (listBucketsOutcome.IsSuccess()) {
0390         const auto buckets = listBucketsOutcome.GetResult().GetBuckets();
0391         hasBuckets = !buckets.empty();
0392         for (const auto &bucket : buckets) {
0393             const auto bucketName = QString::fromLatin1(bucket.GetName().c_str(), bucket.GetName().size());
0394             qCDebug(S3) << "Found bucket:" << bucketName;
0395             KIO::UDSEntry entry;
0396             entry.reserve(7);
0397             entry.fastInsert(KIO::UDSEntry::UDS_NAME, bucketName);
0398             entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, bucketName);
0399             entry.fastInsert(KIO::UDSEntry::UDS_URL, QStringLiteral("s3://%1/").arg(bucketName));
0400             entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0401             entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0);
0402             entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH);
0403             entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("folder-network"));
0404             q->listEntry(entry);
0405         }
0406     } else {
0407         qCDebug(S3) << "Could not list buckets:" << listBucketsOutcome.GetError().GetMessage().c_str();
0408     }
0409 
0410     return hasBuckets;
0411 }
0412 
0413 void S3Backend::listBucket(const Aws::String &bucketName)
0414 {
0415     const Aws::Client::ClientConfiguration clientConfiguration(m_configProfileName.c_str());
0416     const Aws::S3::S3Client client(clientConfiguration);
0417 
0418     Aws::S3::Model::ListObjectsV2Request listObjectsRequest;
0419     listObjectsRequest.SetBucket(bucketName);
0420     listObjectsRequest.SetDelimiter("/");
0421 
0422     qCDebug(S3) << "Listing objects in bucket" << bucketName.c_str() << "...";
0423     const auto listObjectsOutcome = client.ListObjectsV2(listObjectsRequest);
0424     if (listObjectsOutcome.IsSuccess()) {
0425 
0426         const auto bucket = QString::fromLatin1(bucketName.c_str(), bucketName.size());
0427         const auto objects = listObjectsOutcome.GetResult().GetContents();
0428         for (const auto &object : objects) {
0429             KIO::UDSEntry entry;
0430             const auto objectKey = QString::fromUtf8(object.GetKey().c_str());
0431             entry.reserve(6);
0432             entry.fastInsert(KIO::UDSEntry::UDS_NAME, objectKey);
0433             entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, objectKey);
0434             entry.fastInsert(KIO::UDSEntry::UDS_URL, QStringLiteral("s3://%1/%2").arg(bucket, objectKey));
0435             entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
0436             entry.fastInsert(KIO::UDSEntry::UDS_SIZE, object.GetSize());
0437             entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH );
0438             q->listEntry(entry);
0439         }
0440 
0441         const auto commonPrefixes = listObjectsOutcome.GetResult().GetCommonPrefixes();
0442         for (const auto &commonPrefix : commonPrefixes) {
0443             KIO::UDSEntry entry;
0444             QString prefix = QString::fromUtf8(commonPrefix.GetPrefix().c_str(), commonPrefix.GetPrefix().size());
0445             if (prefix.endsWith(QLatin1Char('/'))) {
0446                 prefix.chop(1);
0447             }
0448             entry.reserve(6);
0449             entry.fastInsert(KIO::UDSEntry::UDS_NAME, prefix);
0450             entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, prefix);
0451             entry.fastInsert(KIO::UDSEntry::UDS_URL, QStringLiteral("s3://%1/%2/").arg(bucket, prefix));
0452             entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0453             entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH);
0454             entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0);
0455 
0456             q->listEntry(entry);
0457         }
0458     } else {
0459         qCDebug(S3) << "Could not list bucket: " << listObjectsOutcome.GetError().GetMessage().c_str();
0460     }
0461 }
0462 
0463 void S3Backend::listKey(const S3Url &s3url)
0464 {
0465     const Aws::Client::ClientConfiguration clientConfiguration(m_configProfileName.c_str());
0466     const Aws::S3::S3Client client(clientConfiguration);
0467 
0468     const QString prefix = s3url.prefix();
0469 
0470     Aws::S3::Model::ListObjectsV2Request listObjectsRequest;
0471     listObjectsRequest.SetBucket(s3url.BucketName());
0472     listObjectsRequest.SetDelimiter("/");
0473     listObjectsRequest.SetPrefix(s3url.Prefix());
0474 
0475     qCDebug(S3) << "Listing prefix" << prefix << "...";
0476     const auto listObjectsOutcome = client.ListObjectsV2(listObjectsRequest);
0477     if (listObjectsOutcome.IsSuccess()) {
0478         const auto objects = listObjectsOutcome.GetResult().GetContents();
0479         // TODO: handle listObjectsOutcome.GetResult().GetIsTruncated()
0480         // By default the max-keys request parameter is 1000, which is reasonable for us
0481         // since we filter the keys by the name of the folder, but it won't work
0482         // if someone has very big folders with more than 1000 files.
0483         qCDebug(S3) << "Prefix" << prefix << "has" << objects.size() << "objects";
0484         for (const auto &object : objects) {
0485             QString key = QString::fromUtf8(object.GetKey().c_str(), object.GetKey().size());
0486             // Note: key might be empty. 0-sized virtual folders have object.GetKey() equal to prefix.
0487             key.remove(0, prefix.length());
0488 
0489             KIO::UDSEntry entry;
0490             // S3 always appends trailing slash to "folder" objects.
0491             if (key.endsWith(QLatin1Char('/'))) {
0492                 entry.reserve(5);
0493                 entry.fastInsert(KIO::UDSEntry::UDS_NAME, key);
0494                 entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, key);
0495                 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0496                 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH);
0497                 entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0);
0498                 q->listEntry(entry);
0499             } else if (!key.isEmpty()) { // Not a folder.
0500                 entry.reserve(6);
0501                 entry.fastInsert(KIO::UDSEntry::UDS_NAME, key);
0502                 entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, key);
0503                 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
0504                 entry.fastInsert(KIO::UDSEntry::UDS_SIZE, object.GetSize());
0505                 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH );
0506                 entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, object.GetLastModified().SecondsWithMSPrecision());
0507                 q->listEntry(entry);
0508             }
0509         }
0510 
0511         const auto commonPrefixes = listObjectsOutcome.GetResult().GetCommonPrefixes();
0512         qCDebug(S3) << "Prefix" << prefix << "has" << commonPrefixes.size() << "common prefixes";
0513         for (const auto &commonPrefix : commonPrefixes) {
0514             QString subprefix = QString::fromUtf8(commonPrefix.GetPrefix().c_str(), commonPrefix.GetPrefix().size());
0515             if (subprefix.endsWith(QLatin1Char('/'))) {
0516                 subprefix.chop(1);
0517             }
0518             if (subprefix.startsWith(prefix)) {
0519                 subprefix.remove(0, prefix.length());
0520             }
0521             KIO::UDSEntry entry;
0522             entry.reserve(4);
0523             entry.fastInsert(KIO::UDSEntry::UDS_NAME, subprefix);
0524             entry.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, subprefix);
0525             entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0526             entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0);
0527 
0528             q->listEntry(entry);
0529         }
0530     } else {
0531         qCDebug(S3) << "Could not list prefix" << s3url.key() << " - " << listObjectsOutcome.GetError().GetMessage().c_str();
0532     }
0533 }
0534 
0535 void S3Backend::listCwdEntry(CwdAccess access)
0536 {
0537     // List UDSEntry for "."
0538     KIO::UDSEntry entry;
0539     entry.reserve(4);
0540     entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral("."));
0541     entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0542     entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0);
0543     if (access == ReadOnlyCwd) {
0544         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
0545     } else {
0546         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH);
0547     }
0548 
0549     q->listEntry(entry);
0550 }
0551 
0552 bool S3Backend::deletePrefix(const Aws::S3::S3Client &client, const S3Url &s3url, const Aws::String &prefix)
0553 {
0554     qCDebug(S3) << "Going to recursively delete prefix:" << prefix.c_str();
0555     bool outcome = false;
0556     const Aws::String bucketName = s3url.BucketName();
0557     // In order to recursively delete a folder, we need to list by prefix and manually delete each listed key.
0558     Aws::S3::Model::ListObjectsV2Request listObjectsRequest;
0559     listObjectsRequest.SetBucket(bucketName);
0560     listObjectsRequest.SetDelimiter("/");
0561     listObjectsRequest.SetPrefix(prefix);
0562     const auto listObjectsOutcome = client.ListObjectsV2(listObjectsRequest);
0563     if (listObjectsOutcome.IsSuccess()) {
0564         // TODO: handle listObjectsOutcome.GetResult().GetIsTruncated()
0565         // By default the max-keys request parameter is 1000, which is reasonable for us
0566         // since we filter the keys by the name of the folder, but it won't work
0567         // if someone has very big folders with more than 1000 files.
0568         const auto commonPrefixes = listObjectsOutcome.GetResult().GetCommonPrefixes();
0569         qCDebug(S3) << "Prefix" << prefix.c_str() << "has" << commonPrefixes.size() << "common prefixes";
0570         // Recursively delete folder children.
0571         for (const auto &commonPrefix : commonPrefixes) {
0572             const bool recursiveOutcome = deletePrefix(client, s3url, commonPrefix.GetPrefix());
0573             outcome = outcome && recursiveOutcome;
0574         }
0575         const auto objects = listObjectsOutcome.GetResult().GetContents();
0576         // Delete each file child.
0577         if (objects.size() > 0) {
0578             for (const auto &object : objects) {
0579                 Aws::S3::Model::DeleteObjectRequest request;
0580                 request.SetBucket(bucketName);
0581                 request.SetKey(object.GetKey());
0582                 auto deleteObjectOutcome = client.DeleteObject(request);
0583                 if (!deleteObjectOutcome.IsSuccess()) {
0584                     qCDebug(S3) << "Could not delete object with key:" << s3url.key() << " - " << deleteObjectOutcome.GetError().GetMessage().c_str();
0585                     outcome = false;
0586                 }
0587             }
0588         } else { // The prefix was either a file or a folder that contains 0 files.
0589             Aws::S3::Model::DeleteObjectRequest request;
0590             request.SetBucket(bucketName);
0591             request.SetKey(s3url.Key());
0592             auto deleteObjectOutcome = client.DeleteObject(request);
0593             if (!deleteObjectOutcome.IsSuccess()) {
0594                 qCDebug(S3) << "Could not delete object with key:" << s3url.key() << " - " << deleteObjectOutcome.GetError().GetMessage().c_str();
0595                 outcome = false;
0596             }
0597         }
0598         outcome = true;
0599     } else {
0600         qCDebug(S3) << "Could not list prefix:" << prefix.c_str();
0601         outcome = false;
0602     }
0603 
0604     return outcome;
0605 }
0606 
0607 QString S3Backend::contentType(const S3Url &s3url)
0608 {
0609     QString contentType;
0610 
0611     const Aws::Client::ClientConfiguration clientConfiguration(m_configProfileName.c_str());
0612     const Aws::S3::S3Client client(clientConfiguration);
0613 
0614     Aws::S3::Model::HeadObjectRequest headObjectRequest;
0615     headObjectRequest.SetBucket(s3url.BucketName());
0616     headObjectRequest.SetKey(s3url.Key());
0617 
0618     auto headObjectRequestOutcome = client.HeadObject(headObjectRequest);
0619     if (headObjectRequestOutcome.IsSuccess()) {
0620         contentType = QString::fromLatin1(headObjectRequestOutcome.GetResult().GetContentType().c_str(), headObjectRequestOutcome.GetResult().GetContentType().size());
0621         qCDebug(S3) << "Key" << s3url.key() << "has Content-Type:" << contentType;
0622     } else {
0623         qCDebug(S3) << "Could not get content type for key:" << s3url.key() << " - " << headObjectRequestOutcome.GetError().GetMessage().c_str();
0624     }
0625 
0626     return contentType;
0627 }