ImageHost: add Gitee host

This commit is contained in:
Le Tan 2021-08-12 20:48:23 +08:00
parent f1d931c276
commit c03884c429
12 changed files with 372 additions and 29 deletions

@ -1 +1 @@
Subproject commit c53fc8dbf6df14b9e1327de0a4907700ffee6049 Subproject commit ac58baf5f07643f57a2a208629b7056c24516ffc

View File

@ -35,10 +35,11 @@ QString MarkdownBuffer::insertImage(const QImage &p_image, const QString &p_imag
void MarkdownBuffer::fetchInitialImages() void MarkdownBuffer::fetchInitialImages()
{ {
Q_ASSERT(m_initialImages.isEmpty()); Q_ASSERT(m_initialImages.isEmpty());
vte::MarkdownLink::TypeFlags linkFlags = vte::MarkdownLink::TypeFlag::LocalRelativeInternal | vte::MarkdownLink::TypeFlag::Remote; // There is compilation error on Linux and macOS using TypeFlags directly.
int linkFlags = vte::MarkdownLink::TypeFlag::LocalRelativeInternal | vte::MarkdownLink::TypeFlag::Remote;
m_initialImages = vte::MarkdownUtils::fetchImagesFromMarkdownText(getContent(), m_initialImages = vte::MarkdownUtils::fetchImagesFromMarkdownText(getContent(),
getResourcePath(), getResourcePath(),
linkFlags); static_cast<vte::MarkdownLink::TypeFlags>(linkFlags));
} }
void MarkdownBuffer::addInsertedImage(const QString &p_imagePath, const QString &p_urlInLink) void MarkdownBuffer::addInsertedImage(const QString &p_imagePath, const QString &p_urlInLink)
@ -57,11 +58,11 @@ QHash<QString, bool> MarkdownBuffer::clearObsoleteImages()
Q_ASSERT(!isModified()); Q_ASSERT(!isModified());
const bool discarded = state() & StateFlag::Discarded; const bool discarded = state() & StateFlag::Discarded;
const vte::MarkdownLink::TypeFlags linkFlags = vte::MarkdownLink::TypeFlag::LocalRelativeInternal | vte::MarkdownLink::TypeFlag::Remote; const int linkFlags = vte::MarkdownLink::TypeFlag::LocalRelativeInternal | vte::MarkdownLink::TypeFlag::Remote;
const auto latestImages = const auto latestImages =
vte::MarkdownUtils::fetchImagesFromMarkdownText(!discarded ? getContent() : m_provider->read(), vte::MarkdownUtils::fetchImagesFromMarkdownText(!discarded ? getContent() : m_provider->read(),
getResourcePath(), getResourcePath(),
linkFlags); static_cast<vte::MarkdownLink::TypeFlags>(linkFlags));
QSet<QString> latestImagesPath; QSet<QString> latestImagesPath;
for (const auto &link : latestImages) { for (const auto &link : latestImages) {
if (link.m_type & vte::MarkdownLink::TypeFlag::Remote) { if (link.m_type & vte::MarkdownLink::TypeFlag::Remote) {

View File

@ -0,0 +1,203 @@
#include "giteeimagehost.h"
#include <QDebug>
#include <QFileInfo>
#include <QByteArray>
#include <utils/utils.h>
#include <utils/webutils.h>
using namespace vnotex;
const QString GiteeImageHost::c_apiUrl = "https://gitee.com/api/v5";
GiteeImageHost::GiteeImageHost(QObject *p_parent)
: ImageHost(p_parent)
{
}
bool GiteeImageHost::ready() const
{
return !m_personalAccessToken.isEmpty() && !m_userName.isEmpty() && !m_repoName.isEmpty();
}
ImageHost::Type GiteeImageHost::getType() const
{
return Type::Gitee;
}
QJsonObject GiteeImageHost::getConfig() const
{
QJsonObject obj;
obj[QStringLiteral("personal_access_token")] = m_personalAccessToken;
obj[QStringLiteral("user_name")] = m_userName;
obj[QStringLiteral("repository_name")] = m_repoName;
return obj;
}
void GiteeImageHost::setConfig(const QJsonObject &p_jobj)
{
parseConfig(p_jobj, m_personalAccessToken, m_userName, m_repoName);
m_imageUrlPrefix = QString("https://gitee.com/%1/%2/raw/master/").arg(m_userName, m_repoName);
}
bool GiteeImageHost::testConfig(const QJsonObject &p_jobj, QString &p_msg)
{
p_msg.clear();
QString token, userName, repoName;
parseConfig(p_jobj, token, userName, repoName);
if (token.isEmpty() || userName.isEmpty() || repoName.isEmpty()) {
p_msg = tr("PersonalAccessToken/UserName/RepositoryName should not be empty.");
return false;
}
auto reply = getRepoInfo(token, userName, repoName);
p_msg = QString::fromUtf8(reply.m_data);
return reply.m_error == QNetworkReply::NoError;
}
vte::NetworkAccess::RawHeaderPairs GiteeImageHost::prepareCommonHeaders()
{
vte::NetworkAccess::RawHeaderPairs rawHeader;
rawHeader.push_back(qMakePair(QByteArray("Content-Type"), QByteArray("application/json;charset=UTF-8")));
return rawHeader;
}
QString GiteeImageHost::addAccessToken(const QString &p_token, QString p_url)
{
if (p_url.contains(QLatin1Char('?'))) {
p_url += QString("&access_token=%1").arg(p_token);
} else {
p_url += QString("?access_token=%1").arg(p_token);
}
return p_url;
}
vte::NetworkReply GiteeImageHost::getRepoInfo(const QString &p_token, const QString &p_userName, const QString &p_repoName) const
{
auto rawHeader = prepareCommonHeaders();
auto urlStr = QString("%1/repos/%2/%3").arg(c_apiUrl, p_userName, p_repoName);
auto reply = vte::NetworkAccess::request(QUrl(addAccessToken(p_token, urlStr)), rawHeader);
return reply;
}
void GiteeImageHost::parseConfig(const QJsonObject &p_jobj,
QString &p_token,
QString &p_userName,
QString &p_repoName)
{
p_token = p_jobj[QStringLiteral("personal_access_token")].toString();
p_userName = p_jobj[QStringLiteral("user_name")].toString();
p_repoName = p_jobj[QStringLiteral("repository_name")].toString();
}
static bool isEmptyResponse(const QByteArray &p_data)
{
return p_data == QByteArray("[]");
}
QString GiteeImageHost::create(const QByteArray &p_data, const QString &p_path, QString &p_msg)
{
QString destUrl;
if (p_path.isEmpty()) {
p_msg = tr("Failed to create image with empty path.");
return destUrl;
}
if (!ready()) {
p_msg = tr("Invalid GitHub image host configuration.");
return QString();
}
auto rawHeader = prepareCommonHeaders();
const auto urlStr = QString("%1/repos/%2/%3/contents/%4").arg(c_apiUrl, m_userName, m_repoName, p_path);
// Check if @p_path already exists.
auto reply = vte::NetworkAccess::request(QUrl(addAccessToken(m_personalAccessToken, urlStr)), rawHeader);
if (reply.m_error == QNetworkReply::NoError) {
if (!isEmptyResponse(reply.m_data)) {
p_msg = tr("The resource already exists at the image host (%1).").arg(p_path);
return QString();
}
} else if (reply.m_error != QNetworkReply::ContentNotFoundError) {
p_msg = tr("Failed to query the resource at the image host (%1) (%2) (%3).").arg(urlStr, reply.errorStr(), reply.m_data);
return QString();
}
// Create the content.
QJsonObject requestDataObj;
requestDataObj[QStringLiteral("access_token")] = m_personalAccessToken;
requestDataObj[QStringLiteral("message")] = QString("VX_ADD: %1").arg(p_path);
requestDataObj[QStringLiteral("content")] = QString::fromUtf8(p_data.toBase64());
auto requestData = Utils::toJsonString(requestDataObj);
reply = vte::NetworkAccess::post(QUrl(urlStr), rawHeader, requestData);
if (reply.m_error != QNetworkReply::NoError) {
p_msg = tr("Failed to create resource at the image host (%1) (%2) (%3).").arg(urlStr, reply.errorStr(), reply.m_data);
return QString();
} else {
auto replyObj = Utils::fromJsonString(reply.m_data);
Q_ASSERT(!replyObj.isEmpty());
auto targetUrl = replyObj[QStringLiteral("content")].toObject().value(QStringLiteral("download_url")).toString();
if (targetUrl.isEmpty()) {
p_msg = tr("Failed to create resource at the image host (%1) (%2) (%3).").arg(urlStr, reply.errorStr(), reply.m_data);
} else {
qDebug() << "created resource" << targetUrl;
}
return targetUrl;
}
}
bool GiteeImageHost::ownsUrl(const QString &p_url) const
{
return p_url.startsWith(m_imageUrlPrefix);
}
bool GiteeImageHost::remove(const QString &p_url, QString &p_msg)
{
Q_ASSERT(ownsUrl(p_url));
if (!ready()) {
p_msg = tr("Invalid GitHub image host configuration.");
return false;
}
const QString resourcePath = WebUtils::purifyUrl(p_url.mid(m_imageUrlPrefix.size()));
auto rawHeader = prepareCommonHeaders();
const auto urlStr = QString("%1/repos/%2/%3/contents/%4").arg(c_apiUrl, m_userName, m_repoName, resourcePath);
// Get the SHA of the resource.
auto reply = vte::NetworkAccess::request(QUrl(addAccessToken(m_personalAccessToken, urlStr)), rawHeader);
if (reply.m_error != QNetworkReply::NoError || isEmptyResponse(reply.m_data)) {
p_msg = tr("Failed to fetch information about the resource (%1).").arg(resourcePath);
return false;
}
auto replyObj = Utils::fromJsonString(reply.m_data);
Q_ASSERT(!replyObj.isEmpty());
const auto sha = replyObj[QStringLiteral("sha")].toString();
if (sha.isEmpty()) {
p_msg = tr("Failed to fetch SHA about the resource (%1) (%2).").arg(resourcePath, QString::fromUtf8(reply.m_data));
return false;
}
// Delete.
QJsonObject requestDataObj;
requestDataObj[QStringLiteral("access_token")] = m_personalAccessToken;
requestDataObj[QStringLiteral("message")] = QString("VX_DEL: %1").arg(resourcePath);
requestDataObj[QStringLiteral("sha")] = sha;
auto requestData = Utils::toJsonString(requestDataObj);
reply = vte::NetworkAccess::deleteResource(QUrl(urlStr), rawHeader, requestData);
if (reply.m_error != QNetworkReply::NoError) {
p_msg = tr("Failed to delete resource (%1) (%2).").arg(resourcePath, QString::fromUtf8(reply.m_data));
return false;
}
qDebug() << "deleted resource" << resourcePath;
return true;
}

View File

@ -0,0 +1,57 @@
#ifndef GITEEIMAGEHOST_H
#define GITEEIMAGEHOST_H
#include "imagehost.h"
#include <vtextedit/networkutils.h>
namespace vnotex
{
class GiteeImageHost : public ImageHost
{
Q_OBJECT
public:
explicit GiteeImageHost(QObject *p_parent);
bool ready() const Q_DECL_OVERRIDE;
Type getType() const Q_DECL_OVERRIDE;
QJsonObject getConfig() const Q_DECL_OVERRIDE;
void setConfig(const QJsonObject &p_jobj) Q_DECL_OVERRIDE;
bool testConfig(const QJsonObject &p_jobj, QString &p_msg) Q_DECL_OVERRIDE;
QString create(const QByteArray &p_data, const QString &p_path, QString &p_msg) Q_DECL_OVERRIDE;
bool remove(const QString &p_url, QString &p_msg) Q_DECL_OVERRIDE;
bool ownsUrl(const QString &p_url) const Q_DECL_OVERRIDE;
private:
// Used to test.
vte::NetworkReply getRepoInfo(const QString &p_token, const QString &p_userName, const QString &p_repoName) const;
static void parseConfig(const QJsonObject &p_jobj,
QString &p_token,
QString &p_userName,
QString &p_repoName);
static vte::NetworkAccess::RawHeaderPairs prepareCommonHeaders();
static QString addAccessToken(const QString &p_token, QString p_url);
QString m_personalAccessToken;
QString m_userName;
QString m_repoName;
QString m_imageUrlPrefix;
static const QString c_apiUrl;
};
}
#endif // GITEEIMAGEHOST_H

View File

@ -105,14 +105,6 @@ QString GitHubImageHost::create(const QByteArray &p_data, const QString &p_path,
return destUrl; return destUrl;
} }
destUrl = createResource(p_data, p_path, p_msg);
return destUrl;
}
QString GitHubImageHost::createResource(const QByteArray &p_content, const QString &p_path, QString &p_msg) const
{
Q_ASSERT(!p_path.isEmpty());
if (!ready()) { if (!ready()) {
p_msg = tr("Invalid GitHub image host configuration."); p_msg = tr("Invalid GitHub image host configuration.");
return QString(); return QString();
@ -134,7 +126,7 @@ QString GitHubImageHost::createResource(const QByteArray &p_content, const QStri
// Create the content. // Create the content.
QJsonObject requestDataObj; QJsonObject requestDataObj;
requestDataObj[QStringLiteral("message")] = QString("VX_ADD: %1").arg(p_path); requestDataObj[QStringLiteral("message")] = QString("VX_ADD: %1").arg(p_path);
requestDataObj[QStringLiteral("content")] = QString::fromUtf8(p_content.toBase64()); requestDataObj[QStringLiteral("content")] = QString::fromUtf8(p_data.toBase64());
auto requestData = Utils::toJsonString(requestDataObj); auto requestData = Utils::toJsonString(requestDataObj);
reply = vte::NetworkAccess::put(QUrl(urlStr), rawHeader, requestData); reply = vte::NetworkAccess::put(QUrl(urlStr), rawHeader, requestData);
if (reply.m_error != QNetworkReply::NoError) { if (reply.m_error != QNetworkReply::NoError) {

View File

@ -29,12 +29,19 @@ namespace vnotex
bool ownsUrl(const QString &p_url) const Q_DECL_OVERRIDE; bool ownsUrl(const QString &p_url) const Q_DECL_OVERRIDE;
protected:
QString m_personalAccessToken;
QString m_userName;
QString m_repoName;
QString m_imageUrlPrefix;
private: private:
// Used to test. // Used to test.
vte::NetworkReply getRepoInfo(const QString &p_token, const QString &p_userName, const QString &p_repoName) const; vte::NetworkReply getRepoInfo(const QString &p_token, const QString &p_userName, const QString &p_repoName) const;
QString createResource(const QByteArray &p_content, const QString &p_path, QString &p_msg) const;
static void parseConfig(const QJsonObject &p_jobj, static void parseConfig(const QJsonObject &p_jobj,
QString &p_token, QString &p_token,
QString &p_userName, QString &p_userName,
@ -46,14 +53,6 @@ namespace vnotex
static vte::NetworkAccess::RawHeaderPairs prepareCommonHeaders(const QString &p_token); static vte::NetworkAccess::RawHeaderPairs prepareCommonHeaders(const QString &p_token);
QString m_personalAccessToken;
QString m_userName;
QString m_repoName;
QString m_imageUrlPrefix;
static const QString c_apiUrl; static const QString c_apiUrl;
}; };
} }

View File

@ -23,6 +23,9 @@ QString ImageHost::typeString(ImageHost::Type p_type)
case Type::GitHub: case Type::GitHub:
return tr("GitHub"); return tr("GitHub");
case Type::Gitee:
return tr("Gitee");
default: default:
Q_ASSERT(false); Q_ASSERT(false);
return QString("Unknown"); return QString("Unknown");

View File

@ -18,6 +18,7 @@ namespace vnotex
enum Type enum Type
{ {
GitHub = 0, GitHub = 0,
Gitee,
MaxHost MaxHost
}; };

View File

@ -1,12 +1,14 @@
QT += widgets QT += widgets
HEADERS += \ HEADERS += \
$$PWD/giteeimagehost.h \
$$PWD/githubimagehost.h \ $$PWD/githubimagehost.h \
$$PWD/imagehost.h \ $$PWD/imagehost.h \
$$PWD/imagehostmgr.h \ $$PWD/imagehostmgr.h \
$$PWD/imagehostutils.h $$PWD/imagehostutils.h
SOURCES += \ SOURCES += \
$$PWD/giteeimagehost.cpp \
$$PWD/githubimagehost.cpp \ $$PWD/githubimagehost.cpp \
$$PWD/imagehost.cpp \ $$PWD/imagehost.cpp \
$$PWD/imagehostmgr.cpp \ $$PWD/imagehostmgr.cpp \

View File

@ -6,6 +6,7 @@
#include <core/editorconfig.h> #include <core/editorconfig.h>
#include "githubimagehost.h" #include "githubimagehost.h"
#include "giteeimagehost.h"
using namespace vnotex; using namespace vnotex;
@ -64,6 +65,9 @@ ImageHost *ImageHostMgr::createImageHost(ImageHost::Type p_type, QObject *p_pare
case ImageHost::Type::GitHub: case ImageHost::Type::GitHub:
return new GitHubImageHost(p_parent); return new GitHubImageHost(p_parent);
case ImageHost::Type::Gitee:
return new GiteeImageHost(p_parent);
default: default:
return nullptr; return nullptr;
} }

View File

@ -16,7 +16,6 @@ namespace vnotex
ImageHostUtils() = delete; ImageHostUtils() = delete;
// According to @p_buffer, generate the relative path on image host for images. // According to @p_buffer, generate the relative path on image host for images.
// Return the relative path folder.
static QString generateRelativePath(const Buffer *p_buffer); static QString generateRelativePath(const Buffer *p_buffer);
}; };
} }

View File

@ -21,6 +21,7 @@
#include <vtextedit/markdownutils.h> #include <vtextedit/markdownutils.h>
#include <vtextedit/vtextedit.h> #include <vtextedit/vtextedit.h>
#include <vtextedit/texteditutils.h> #include <vtextedit/texteditutils.h>
#include <vtextedit/markdownutils.h>
#include <vtextedit/networkutils.h> #include <vtextedit/networkutils.h>
#include <vtextedit/theme.h> #include <vtextedit/theme.h>
@ -1337,16 +1338,22 @@ void MarkdownEditor::setImageHost(ImageHost *p_host)
m_imageHost = p_host; m_imageHost = p_host;
} }
QString MarkdownEditor::saveToImageHost(const QByteArray &p_imageData, const QString &p_destFileName) static QString generateImageHostFileName(const Buffer *p_buffer, const QString &p_destFileName)
{ {
Q_ASSERT(m_imageHost); auto destPath = ImageHostUtils::generateRelativePath(p_buffer);
auto destPath = ImageHostUtils::generateRelativePath(m_buffer);
if (destPath.isEmpty()) { if (destPath.isEmpty()) {
destPath = p_destFileName; destPath = p_destFileName;
} else { } else {
destPath += "/" + p_destFileName; destPath += "/" + p_destFileName;
} }
return destPath;
}
QString MarkdownEditor::saveToImageHost(const QByteArray &p_imageData, const QString &p_destFileName)
{
Q_ASSERT(m_imageHost);
const auto destPath = generateImageHostFileName(m_buffer, p_destFileName);
QString errMsg; QString errMsg;
@ -1390,4 +1397,79 @@ void MarkdownEditor::uploadImagesToImageHost()
auto act = static_cast<QAction *>(sender()); auto act = static_cast<QAction *>(sender());
auto host = ImageHostMgr::getInst().find(act->data().toString()); auto host = ImageHostMgr::getInst().find(act->data().toString());
Q_ASSERT(host); Q_ASSERT(host);
// Only LocalRelativeInternal images.
// Descending order of the link position.
auto images = vte::MarkdownUtils::fetchImagesFromMarkdownText(m_buffer->getContent(),
m_buffer->getResourcePath(),
vte::MarkdownLink::TypeFlag::LocalRelativeInternal);
if (images.isEmpty()) {
return;
}
QProgressDialog proDlg(tr("Uploading local images..."),
tr("Abort"),
0,
images.size(),
this);
proDlg.setWindowModality(Qt::WindowModal);
proDlg.setWindowTitle(tr("Upload Images To Image Host"));
int cnt = 0;
auto cursor = m_textEdit->textCursor();
cursor.beginEditBlock();
for (int i = 0; i < images.size(); ++i) {
const auto &link = images[i];
proDlg.setValue(i + 1);
if (proDlg.wasCanceled()) {
break;
}
proDlg.setLabelText(tr("Upload image (%1)").arg(link.m_path));
Q_ASSERT(i == 0 || link.m_urlInLinkPos < images[i - 1].m_urlInLinkPos);
QByteArray ba;
try {
ba = FileUtils::readFile(link.m_path);
} catch (Exception &e) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
QString("Failed to read local image file (%1) (%2).").arg(link.m_path, e.what()),
this);
continue;
}
if (ba.isEmpty()) {
qWarning() << "Skipped uploading empty image" << link.m_path;
continue;
}
const auto destPath = generateImageHostFileName(m_buffer, PathUtils::fileName(link.m_path));
QString errMsg;
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
const auto targetUrl = host->create(ba, destPath, errMsg);
QApplication::restoreOverrideCursor();
if (targetUrl.isEmpty()) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
QString("Failed to upload image to image host (%1) as (%2).").arg(host->getName(), destPath),
QString(),
errMsg,
this);
continue;
}
// Update the link URL.
cursor.setPosition(link.m_urlInLinkPos);
cursor.setPosition(link.m_urlInLinkPos + link.m_urlInLink.size(), QTextCursor::KeepAnchor);
cursor.insertText(targetUrl);
++cnt;
}
cursor.endEditBlock();
proDlg.setValue(images.size());
if (cnt > 0) {
m_textEdit->setTextCursor(cursor);
}
} }