Skip to content

Commit ef4f30d

Browse files
Upload QtMesh Cloud scan reports after file complete and keep GUI responsive.
Add uploadFileReport() and call it after completeUpload in the CLI, session, and MCP paths so analysis shows on the website. Move scan, manifest prep, and project listing off the main thread to prevent the app from freezing during upload. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6782f5e commit ef4f30d

14 files changed

Lines changed: 1082 additions & 188 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: 26 additions & 18 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,53 @@ 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-
}
5289-
}
5279+
5280+
QStringList selectedPaths;
5281+
for (const DependencyEntry& entry : DependencyResolver::detect(filePath))
5282+
selectedPaths.append(entry.absolutePath);
5283+
5284+
CloudPackageUploadRequest request;
5285+
request.mainAssetPath = filePath;
5286+
request.selectedAbsolutePaths = selectedPaths;
5287+
request.projectName = projectName;
5288+
request.createNewProject = true;
5289+
request.runLocalScan = runScan;
52905290

52915291
QtMeshCloudSession session(token);
52925292
QEventLoop loop;
52935293
QString projectUrl;
52945294
QString error;
5295+
QString reportWarning;
5296+
bool uploadOk = false;
5297+
int uploadedFileCount = selectedPaths.size();
52955298
connect(&session, &QtMeshCloudSession::uploadFinished, &loop,
52965299
[&](bool ok, const QString& err, const QString& url, const QString&) {
5300+
uploadOk = ok;
52975301
projectUrl = url;
5298-
error = err;
5299-
if (!ok && error.isEmpty())
5300-
error = QStringLiteral("Upload failed");
5302+
if (ok && !err.isEmpty())
5303+
reportWarning = err;
5304+
else if (!ok)
5305+
error = err.isEmpty() ? QStringLiteral("Upload failed") : err;
53015306
loop.quit();
53025307
});
53035308
connect(&session, &QtMeshCloudSession::uploadCanceled, &loop, [&]() {
5309+
uploadOk = false;
53045310
error = QStringLiteral("Upload canceled");
53055311
loop.quit();
53065312
});
5307-
session.uploadPackage(manifest);
5313+
session.uploadPackageFromAssets(request);
53085314
loop.exec();
53095315

5310-
if (!error.isEmpty())
5316+
if (!uploadOk)
53115317
return makeErrorResult(error);
53125318

53135319
QJsonObject content;
53145320
content[QStringLiteral("ok")] = true;
53155321
content[QStringLiteral("projectUrl")] = projectUrl;
5316-
content[QStringLiteral("fileCount")] = manifest.files.size();
5322+
content[QStringLiteral("fileCount")] = uploadedFileCount;
5323+
if (!reportWarning.isEmpty())
5324+
content[QStringLiteral("reportWarning")] = reportWarning;
53175325
return makeSuccessResult(
53185326
QString::fromUtf8(QJsonDocument(content).toJson(QJsonDocument::Indented)));
53195327
}

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)