Skip to content

Commit ed7387d

Browse files
Merge pull request #748 from fernandotonon/feature/cloud-report-upload
Upload QtMesh Cloud scan reports after file complete
2 parents 6782f5e + 208c36d commit ed7387d

14 files changed

Lines changed: 1142 additions & 189 deletions

src/CLIPipeline.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,7 @@ void CLIPipeline::printUsage()
777777
" cloud list [--json] List cloud projects for the signed-in account.\n"
778778
" cloud upload <file> [--name <n>] [--no-scan] [--json]\n"
779779
" Package the asset + dependencies and upload to QtMesh Cloud.\n"
780+
" After files/complete, uploads the local scan report to the main file.\n"
780781
" cloud delete <project-id> Delete a cloud project by id.\n"
781782
"\n"
782783
"Global options:\n"

src/CloudCLIPipeline.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,21 @@ int cmdCloudUpload(int argc, char* argv[])
346346
return 1;
347347
}
348348

349+
QString reportWarning;
350+
if (!manifest.scanSummary.isEmpty() && !mainFileId.isEmpty()) {
351+
const auto reportResult = QtMeshCloudClient::uploadFileReport(
352+
token, project.ownerSlug, project.projectSlug, mainFileId, manifest.scanSummary);
353+
if (!reportResult.ok) {
354+
reportWarning = QStringLiteral("File uploaded, but analysis report upload failed.");
355+
SentryReporter::addBreadcrumb(QStringLiteral("file.export"),
356+
QStringLiteral("QtMesh Cloud CLI report upload failed"),
357+
QStringLiteral("warning"));
358+
err() << "Warning: " << reportWarning << Qt::endl;
359+
if (!reportResult.errorString.isEmpty())
360+
err() << reportResult.errorString << Qt::endl;
361+
}
362+
}
363+
349364
const QString projectUrl = project.projectUrl;
350365
SentryReporter::addBreadcrumb(QStringLiteral("file.export"),
351366
QStringLiteral("QtMesh Cloud CLI upload completed"));
@@ -355,6 +370,8 @@ int cmdCloudUpload(int argc, char* argv[])
355370
obj.insert(QStringLiteral("ok"), true);
356371
obj.insert(QStringLiteral("projectUrl"), projectUrl);
357372
obj.insert(QStringLiteral("fileCount"), manifest.files.size());
373+
if (!reportWarning.isEmpty())
374+
obj.insert(QStringLiteral("reportWarning"), reportWarning);
358375
out() << QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)) << Qt::endl;
359376
} else {
360377
out() << "Uploaded " << manifest.files.size() << " file(s).";

src/CloudUploadDialog.cpp

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -166,22 +166,19 @@ bool CloudUploadDialog::runLocalScanBeforeUpload() const
166166
return m_runScanCheck->isChecked();
167167
}
168168

169-
QStringList CloudUploadDialog::selectedAbsolutePaths() const
169+
QStringList CloudUploadDialog::selectedAbsolutePathsForUpload() const
170170
{
171-
QStringList extras;
172-
for (int row = 0; row < m_dependencyList->count(); ++row) {
173-
QListWidgetItem* item = m_dependencyList->item(row);
174-
if (item && item->checkState() == Qt::Checked)
175-
extras.append(item->data(Qt::UserRole).toString());
176-
}
177-
return extras;
171+
return selectedAbsolutePaths();
178172
}
179173

180-
PackageMetadata CloudUploadDialog::manifest() const
174+
PackageMetadata CloudUploadDialog::buildManifestForUpload(const QString& mainAssetPath,
175+
const QStringList& selectedAbsolutePaths,
176+
const QString& projectName,
177+
const QJsonObject& scanSummary)
181178
{
182-
const QString mainCanonical = QFileInfo(m_mainAssetPath).absoluteFilePath();
179+
const QString mainCanonical = QFileInfo(mainAssetPath).absoluteFilePath();
183180
QSet<QString> selected;
184-
for (const QString& path : selectedAbsolutePaths())
181+
for (const QString& path : selectedAbsolutePaths)
185182
selected.insert(QFileInfo(path).absoluteFilePath());
186183

187184
QStringList extras;
@@ -190,7 +187,7 @@ PackageMetadata CloudUploadDialog::manifest() const
190187
extras.append(path);
191188
}
192189
PackageMetadata metadata =
193-
ProjectPackager::buildManifest(m_mainAssetPath, extras, projectName());
190+
ProjectPackager::buildManifest(mainAssetPath, extras, projectName);
194191
metadata.files.erase(
195192
std::remove_if(metadata.files.begin(), metadata.files.end(),
196193
[&](const PackageEntry& entry) {
@@ -200,7 +197,24 @@ PackageMetadata CloudUploadDialog::manifest() const
200197
metadata.totalSize = 0;
201198
for (const PackageEntry& entry : metadata.files)
202199
metadata.totalSize += entry.size;
203-
if (!m_scanSummary.isEmpty())
204-
metadata.scanSummary = m_scanSummary;
200+
if (!scanSummary.isEmpty())
201+
metadata.scanSummary = scanSummary;
205202
return metadata;
206203
}
204+
205+
QStringList CloudUploadDialog::selectedAbsolutePaths() const
206+
{
207+
QStringList extras;
208+
for (int row = 0; row < m_dependencyList->count(); ++row) {
209+
QListWidgetItem* item = m_dependencyList->item(row);
210+
if (item && item->checkState() == Qt::Checked)
211+
extras.append(item->data(Qt::UserRole).toString());
212+
}
213+
return extras;
214+
}
215+
216+
PackageMetadata CloudUploadDialog::manifest() const
217+
{
218+
return buildManifestForUpload(m_mainAssetPath, selectedAbsolutePaths(), projectName(),
219+
m_scanSummary);
220+
}

src/CloudUploadDialog.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@ class CloudUploadDialog : public QDialog {
2727
bool hasSelectedProject() const;
2828
QtMeshCloudClient::ProjectSummary selectedProject() const;
2929
QString projectName() const;
30+
QStringList selectedAbsolutePathsForUpload() const;
3031
PackageMetadata manifest() const;
3132
bool runLocalScanBeforeUpload() const;
3233

34+
static PackageMetadata buildManifestForUpload(const QString& mainAssetPath,
35+
const QStringList& selectedAbsolutePaths,
36+
const QString& projectName,
37+
const QJsonObject& scanSummary = QJsonObject());
38+
3339
private:
3440
void rebuildDependencyList();
3541
QStringList selectedAbsolutePaths() const;

src/CloudUploadProgress.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ void CloudUploadProgress::updateProgress(int current, int total, const QString&
4343
{
4444
m_bar->setMaximum(qMax(1, total));
4545
m_bar->setValue(current);
46-
m_label->setText(tr("Uploading %1…").arg(fileName));
46+
if (fileName.isEmpty())
47+
return;
48+
if (fileName.endsWith(QChar(0x2026)) || fileName.endsWith(QLatin1Char('.')))
49+
m_label->setText(fileName);
50+
else
51+
m_label->setText(tr("Uploading %1…").arg(fileName));
4752
}
4853

4954
void CloudUploadProgress::finish(bool ok, const QString& message)

src/MCPServer.cpp

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "UvUnwrap.h"
3030
#include "AssetScanController.h"
3131
#include "CloudCredentialStore.h"
32+
#include "DependencyResolver.h"
3233
#include "ProjectPackager.h"
3334
#include "QtMeshCloudClient.h"
3435
#include "QtMeshCloudSession.h"
@@ -5274,46 +5275,71 @@ QJsonObject MCPServer::toolCloudUpload(const QJsonObject &args)
52745275
if (projectName.isEmpty())
52755276
projectName = QFileInfo(filePath).completeBaseName();
52765277

5277-
PackageMetadata manifest = ProjectPackager::buildManifest(filePath, {}, projectName);
52785278
const bool runScan = !args.contains(QStringLiteral("scan")) || args.value(QStringLiteral("scan")).toBool(true);
5279-
if (runScan) {
5280-
QString scanError;
5281-
const QByteArray scanJson = AssetScanController::runIsolatedScanJsonSync(
5282-
QFileInfo(filePath).absolutePath(), QFileInfo(filePath).fileName(), &scanError);
5283-
if (!scanJson.isEmpty()) {
5284-
QJsonParseError parseError;
5285-
const QJsonDocument doc = QJsonDocument::fromJson(scanJson, &parseError);
5286-
if (parseError.error == QJsonParseError::NoError && doc.isObject())
5287-
manifest.scanSummary = doc.object();
5288-
}
5279+
5280+
const QString mainCanonical = QFileInfo(filePath).canonicalFilePath();
5281+
QStringList selectedPaths;
5282+
selectedPaths.append(mainCanonical);
5283+
for (const DependencyEntry& entry : DependencyResolver::detect(filePath)) {
5284+
if (!entry.exists || !entry.checkedByDefault)
5285+
continue;
5286+
const QString absolute = QFileInfo(entry.absolutePath).absoluteFilePath();
5287+
if (absolute == mainCanonical)
5288+
continue;
5289+
selectedPaths.append(absolute);
52895290
}
52905291

5292+
CloudPackageUploadRequest request;
5293+
request.mainAssetPath = filePath;
5294+
request.selectedAbsolutePaths = selectedPaths;
5295+
request.projectName = projectName;
5296+
request.createNewProject = true;
5297+
request.runLocalScan = runScan;
5298+
52915299
QtMeshCloudSession session(token);
52925300
QEventLoop loop;
52935301
QString projectUrl;
52945302
QString error;
5303+
QString reportWarning;
5304+
bool uploadOk = false;
5305+
const int uploadedFileCount = selectedPaths.size();
52955306
connect(&session, &QtMeshCloudSession::uploadFinished, &loop,
52965307
[&](bool ok, const QString& err, const QString& url, const QString&) {
5308+
uploadOk = ok;
52975309
projectUrl = url;
5298-
error = err;
5299-
if (!ok && error.isEmpty())
5300-
error = QStringLiteral("Upload failed");
5310+
if (ok && !err.isEmpty())
5311+
reportWarning = err;
5312+
else if (!ok)
5313+
error = err.isEmpty() ? QStringLiteral("Upload failed") : err;
53015314
loop.quit();
53025315
});
53035316
connect(&session, &QtMeshCloudSession::uploadCanceled, &loop, [&]() {
5317+
uploadOk = false;
53045318
error = QStringLiteral("Upload canceled");
53055319
loop.quit();
53065320
});
5307-
session.uploadPackage(manifest);
5321+
QTimer timeout;
5322+
timeout.setSingleShot(true);
5323+
timeout.setInterval(10 * 60 * 1000);
5324+
connect(&timeout, &QTimer::timeout, &loop, [&]() {
5325+
session.cancel();
5326+
uploadOk = false;
5327+
error = QStringLiteral("Upload timed out");
5328+
loop.quit();
5329+
});
5330+
timeout.start();
5331+
session.uploadPackageFromAssets(request);
53085332
loop.exec();
53095333

5310-
if (!error.isEmpty())
5334+
if (!uploadOk)
53115335
return makeErrorResult(error);
53125336

53135337
QJsonObject content;
53145338
content[QStringLiteral("ok")] = true;
53155339
content[QStringLiteral("projectUrl")] = projectUrl;
5316-
content[QStringLiteral("fileCount")] = manifest.files.size();
5340+
content[QStringLiteral("fileCount")] = uploadedFileCount;
5341+
if (!reportWarning.isEmpty())
5342+
content[QStringLiteral("reportWarning")] = reportWarning;
53175343
return makeSuccessResult(
53185344
QString::fromUtf8(QJsonDocument(content).toJson(QJsonDocument::Indented)));
53195345
}

src/QtMeshCloudClient.cpp

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,93 @@ QtMeshCloudClient::CompleteUploadResult QtMeshCloudClient::completeUpload(
10561056
return out;
10571057
}
10581058

1059+
QtMeshCloudClient::UploadResult QtMeshCloudClient::uploadFileReport(
1060+
const QString& bearerToken,
1061+
const QString& ownerSlug,
1062+
const QString& projectSlug,
1063+
const QString& fileId,
1064+
const QJsonObject& report,
1065+
int timeoutMs)
1066+
{
1067+
UploadResult out;
1068+
if (bearerToken.isEmpty()) {
1069+
out.errorString = QStringLiteral("missing bearer token");
1070+
return out;
1071+
}
1072+
if (ownerSlug.isEmpty() || projectSlug.isEmpty() || fileId.isEmpty()) {
1073+
out.errorString = QStringLiteral("owner slug, project slug, and fileId are required");
1074+
return out;
1075+
}
1076+
if (report.isEmpty()) {
1077+
out.errorString = QStringLiteral("report JSON is empty");
1078+
return out;
1079+
}
1080+
1081+
const QByteArray payload = QJsonDocument(report).toJson(QJsonDocument::Compact);
1082+
static constexpr int kMaxReportBytes = 5 * 1024 * 1024;
1083+
if (payload.size() > kMaxReportBytes) {
1084+
out.errorString = QStringLiteral("report exceeds 5 MB limit");
1085+
return out;
1086+
}
1087+
1088+
const QString path = ownerProjectPath(ownerSlug, projectSlug,
1089+
QStringLiteral("files/%1/report")
1090+
.arg(QString::fromUtf8(QUrl::toPercentEncoding(fileId))));
1091+
const QUrl url(apiBaseUrl() + path);
1092+
if (!url.isValid()) {
1093+
out.errorString = QStringLiteral("invalid API base URL");
1094+
return out;
1095+
}
1096+
1097+
QNetworkAccessManager nam;
1098+
QNetworkRequest req = authorizedJsonRequest(url, bearerToken, timeoutMs);
1099+
1100+
SentryReporter::addBreadcrumb(QStringLiteral("cloud.upload"),
1101+
QStringLiteral("QtMesh Cloud uploadFileReport: start fileId=%1 bytes=%2")
1102+
.arg(fileId, QString::number(payload.size())));
1103+
1104+
QNetworkReply* reply = nam.put(req, payload);
1105+
QEventLoop loop;
1106+
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
1107+
loop.exec();
1108+
1109+
out.httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
1110+
const QByteArray responseBody = reply->readAll();
1111+
const auto nerr = reply->error();
1112+
const QString transportErr = reply->errorString();
1113+
reply->deleteLater();
1114+
1115+
if (nerr != QNetworkReply::NoError || out.httpStatus < 200 || out.httpStatus >= 300) {
1116+
out.responseBodySnippet = trimSnippet(responseBody);
1117+
out.errorString = nerr != QNetworkReply::NoError ? transportErr : QStringLiteral("HTTP %1").arg(out.httpStatus);
1118+
if (!out.responseBodySnippet.isEmpty())
1119+
out.errorString += QStringLiteral("") + out.responseBodySnippet;
1120+
SentryReporter::addBreadcrumb(QStringLiteral("cloud.upload"),
1121+
QStringLiteral("QtMesh Cloud uploadFileReport: failure HTTP %1").arg(out.httpStatus),
1122+
QStringLiteral("warning"));
1123+
return out;
1124+
}
1125+
1126+
QJsonObject root;
1127+
if (!parseJsonObjectBody(responseBody, root, out.errorString))
1128+
return out;
1129+
if (root.contains(QStringLiteral("ok")) && !root.value(QStringLiteral("ok")).toBool()) {
1130+
out.errorString = jsonErrorCode(root);
1131+
if (out.errorString.isEmpty())
1132+
out.errorString = QStringLiteral("report upload rejected");
1133+
out.responseBodySnippet = trimSnippet(responseBody);
1134+
SentryReporter::addBreadcrumb(QStringLiteral("cloud.upload"),
1135+
QStringLiteral("QtMesh Cloud uploadFileReport: rejected"),
1136+
QStringLiteral("warning"));
1137+
return out;
1138+
}
1139+
1140+
out.ok = true;
1141+
SentryReporter::addBreadcrumb(QStringLiteral("cloud.upload"),
1142+
QStringLiteral("QtMesh Cloud uploadFileReport: ok fileId=%1").arg(fileId));
1143+
return out;
1144+
}
1145+
10591146
QtMeshCloudClient::ManifestResult QtMeshCloudClient::fetchProjectManifest(const QString& bearerToken,
10601147
const QString& ownerSlug,
10611148
const QString& projectSlug,

src/QtMeshCloudClient.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,15 @@ class QtMeshCloudClient {
197197
const QString& mainFileId = QString(),
198198
int timeoutMs = 30000);
199199

200+
/// PUT /v1/u/:owner/p/:project/files/:fileId/report — scan report after files/complete.
201+
/// Body is the JSON from `ScanEngine::scanReportToJsonObject()` (max 5 MB).
202+
static UploadResult uploadFileReport(const QString& bearerToken,
203+
const QString& ownerSlug,
204+
const QString& projectSlug,
205+
const QString& fileId,
206+
const QJsonObject& report,
207+
int timeoutMs = 30000);
208+
200209
struct ManifestResult {
201210
bool ok = false;
202211
int httpStatus = 0;

0 commit comments

Comments
 (0)