#include "uploadclient.h" #include #include #include #include #include #include #include #include #include #include #include UploadClient::UploadClient(QString baseUrl, QString username, QString password, QObject *parent) : QObject(parent) , m_base(std::move(baseUrl)) , m_username(std::move(username)) , m_password(std::move(password)) , m_net(new QNetworkAccessManager(this)) { while (m_base.endsWith(QLatin1Char('/'))) m_base.chop(1); } QByteArray UploadClient::authHeader() const { const QByteArray raw = (m_username + QLatin1Char(':') + m_password).toUtf8(); return "Basic " + raw.toBase64(); } void UploadClient::upload(const QString &filePath, const QString &fileName, const QString &caption, const QString &date, const QString &tags) { auto *file = new QFile(filePath); if (!file->open(QIODevice::ReadOnly)) { delete file; emit uploadFinished(Result::Rejected, {}, {}, QStringLiteral("Could not read the prepared image")); return; } auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); const QByteArray contentType = fileName.endsWith(QStringLiteral(".png"), Qt::CaseInsensitive) ? "image/png" : "image/jpeg"; QHttpPart imagePart; imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant(contentType)); imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(QStringLiteral("form-data; name=\"image\"; filename=\"%1\"").arg(fileName))); file->setParent(multiPart); imagePart.setBodyDevice(file); multiPart->append(imagePart); const auto textPart = [](const QString &name, const QString &value) { QHttpPart part; part.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(QStringLiteral("form-data; name=\"%1\"").arg(name))); part.setBody(value.toUtf8()); return part; }; multiPart->append(textPart(QStringLiteral("caption"), caption)); multiPart->append(textPart(QStringLiteral("date"), date)); multiPart->append(textPart(QStringLiteral("tags"), tags)); QNetworkRequest request(QUrl(m_base + QStringLiteral("/upload"))); request.setRawHeader("Authorization", authHeader()); QNetworkReply *reply = m_net->post(request, multiPart); multiPart->setParent(reply); const QString fallbackSlug = QFileInfo(fileName).completeBaseName(); connect(reply, &QNetworkReply::finished, this, [this, reply, fallbackSlug]() { reply->deleteLater(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const QString body = QString::fromUtf8(reply->readAll()).trimmed(); if (reply->error() == QNetworkReply::NoError && status >= 200 && status < 300) { // Server replies "uploaded slug.ext"; recover the slug. QString name = body; if (name.startsWith(QStringLiteral("uploaded "))) name = name.mid(QStringLiteral("uploaded ").size()).trimmed(); const int dot = name.lastIndexOf(QLatin1Char('.')); QString slug = dot > 0 ? name.left(dot) : name; if (slug.isEmpty()) slug = fallbackSlug; emit uploadFinished(Result::Success, slug, m_base + QStringLiteral("/photo/") + slug + QLatin1Char('/'), {}); return; } if (status == 401 || status == 403) { emit uploadFinished(Result::AuthFailed, {}, {}, QStringLiteral("Wrong username or password")); return; } if (status >= 500 && status < 600) { emit uploadFinished(Result::Transient, {}, {}, QStringLiteral("Server error %1").arg(status)); return; } if (status == 0) { // No HTTP response — a socket/TLS failure. Worth retrying. emit uploadFinished(Result::Transient, {}, {}, reply->errorString()); return; } emit uploadFinished(Result::Rejected, {}, {}, body.isEmpty() ? QStringLiteral("Upload rejected") : body); }); } void UploadClient::testConnection() { QNetworkRequest request(QUrl(m_base + QStringLiteral("/upload"))); request.setRawHeader("Authorization", authHeader()); QNetworkReply *reply = m_net->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() == QNetworkReply::NoError && status >= 200 && status < 300) { emit testFinished(QString()); return; } if (status == 401 || status == 403) { emit testFinished(QStringLiteral("Wrong username or password")); return; } if (status == 0) { emit testFinished(reply->errorString()); return; } emit testFinished(QStringLiteral("Server returned %1").arg(status)); }); } // --- Manage API ------------------------------------------------------------ namespace { QString httpError(QNetworkReply *reply, int status) { if (status == 401 || status == 403) return QStringLiteral("Wrong username or password"); if (status == 0) return reply->errorString(); const QString body = QString::fromUtf8(reply->readAll()).trimmed(); return body.isEmpty() ? QStringLiteral("Server returned %1").arg(status) : body; } } // namespace void UploadClient::listPhotos() { QNetworkRequest request(QUrl(m_base + QStringLiteral("/api/photos"))); request.setRawHeader("Authorization", authHeader()); QNetworkReply *reply = m_net->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() != QNetworkReply::NoError || status < 200 || status >= 300) { emit photosListed({}, httpError(reply, status)); return; } const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); if (!doc.isArray()) { emit photosListed({}, QStringLiteral("Could not read the server's response")); return; } QList photos; const QJsonArray arr = doc.array(); for (const QJsonValue &v : arr) { const QJsonObject o = v.toObject(); RemotePhoto p; p.slug = o.value(QStringLiteral("slug")).toString(); p.caption = o.value(QStringLiteral("caption")).toString(); p.date = o.value(QStringLiteral("date")).toString(); p.thumb = o.value(QStringLiteral("thumb")).toString(); p.url = o.value(QStringLiteral("url")).toString(); const QJsonArray tags = o.value(QStringLiteral("tags")).toArray(); for (const QJsonValue &t : tags) p.tags << t.toString(); photos << p; } emit photosListed(photos, QString()); }); } void UploadClient::fetchTags() { QNetworkRequest request(QUrl(m_base + QStringLiteral("/api/tags.json"))); request.setRawHeader("Authorization", authHeader()); QNetworkReply *reply = m_net->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() != QNetworkReply::NoError || status < 200 || status >= 300) { emit tagsFetched({}); // best-effort; autocomplete is a convenience return; } const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); QStringList names; for (const QJsonValue &v : doc.array()) { const QString name = v.toObject().value(QStringLiteral("tag")).toString(); if (!name.isEmpty()) names << name; } emit tagsFetched(names); }); } void UploadClient::fetchThumbnail(const QString &slug) { QNetworkRequest request(QUrl(m_base + QStringLiteral("/api/thumb/") + slug)); request.setRawHeader("Authorization", authHeader()); QNetworkReply *reply = m_net->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply, slug]() { reply->deleteLater(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() != QNetworkReply::NoError || status < 200 || status >= 300) { emit thumbnailReady(slug, QImage()); // null image on any failure return; } QImage image; image.loadFromData(reply->readAll()); emit thumbnailReady(slug, image); }); } void UploadClient::editPost(const QString &slug, const QString &caption, const QString &date, const QString &tags) { QUrlQuery form; form.addQueryItem(QStringLiteral("slug"), slug); form.addQueryItem(QStringLiteral("caption"), caption); form.addQueryItem(QStringLiteral("date"), date); form.addQueryItem(QStringLiteral("tags"), tags); QNetworkRequest request(QUrl(m_base + QStringLiteral("/api/edit"))); request.setRawHeader("Authorization", authHeader()); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded")); QNetworkReply *reply = m_net->post(request, form.toString(QUrl::FullyEncoded).toUtf8()); connect(reply, &QNetworkReply::finished, this, [this, reply, slug]() { reply->deleteLater(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() == QNetworkReply::NoError && status >= 200 && status < 300) emit editFinished(slug, QString()); else emit editFinished(slug, httpError(reply, status)); }); } void UploadClient::deletePost(const QString &slug) { QUrlQuery form; form.addQueryItem(QStringLiteral("slug"), slug); QNetworkRequest request(QUrl(m_base + QStringLiteral("/api/delete"))); request.setRawHeader("Authorization", authHeader()); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded")); QNetworkReply *reply = m_net->post(request, form.toString(QUrl::FullyEncoded).toUtf8()); connect(reply, &QNetworkReply::finished, this, [this, reply, slug]() { reply->deleteLater(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() != QNetworkReply::NoError || status < 200 || status >= 300) { emit deleteFinished(slug, httpError(reply, status)); return; } // The server reports how many it moved; 0 means the slug wasn't found. const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); const int deleted = doc.object().value(QStringLiteral("deleted")).toInt(0); if (deleted > 0) emit deleteFinished(slug, QString()); else emit deleteFinished(slug, QStringLiteral("Nothing deleted — not found on the server")); }); }