Plan cloud transfer requests

This commit is contained in:
2026-06-05 07:27:51 +02:00
parent a104f88360
commit a79ef4cda8
8 changed files with 430 additions and 10 deletions

View File

@@ -868,7 +868,9 @@ Known local toolchain state:
new-document warning, clean publish prompt, and dirty save-before-upload
decisions, plus cloud browse no-canvas/show-browser and selected-download
decisions, plus bulk upload progress visibility, zero-file, and clamped
progress-total decisions.
progress-total decisions, plus cloud download/upload transfer request
validation, progress-callback enablement, TLS-verification policy, and
zero/negative/overrun transfer-progress fraction guards.
- `pp_app_core_document_session_tests` covers clean and dirty app session,
document-open action planning and executor dispatch/rejection, save-request,
close-request executor dispatch/no-op preservation, document-save executor

View File

@@ -138,6 +138,13 @@ agent or engineer to remove them without reconstructing context from chat.
`App::rec_loop` and `pano_cli plan-recording-session`; retained PBO
equirect generation, dirty-stroke mutation, MP4 encoder calls, and frame
label rendering remain in the legacy app/canvas/video path.
- 2026-06-05: DEBT-0038 was narrowed. Cloud transfer request/progress policy
now lives in tested `pp_app_core` planning, live `App::download` and
`App::upload` consume those plans before retained CURL setup, and
`pano_cli plan-cloud-transfer` exposes missing endpoint, TLS policy,
progress-callback, and zero/overrun progress cases for automation. CURL
ownership, response/error handling, progress UI, cloud dialog/document
execution, and injected network service work remain open under DEBT-0038.
- 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit,
thumbnail, and object-draw history paths now query saved blend state through
tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect
@@ -189,7 +196,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter |
| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::query_opengl_capability_detection`, `detect_opengl_feature_state`, and `render_device_features` as the backend conversion point; that feature snapshot now includes float32-linear filtering, so canvas stroke texture format selection, renderer diagnostics, grid lightmap render planning, and grid bake target selection no longer read `ShaderManager::ext_*` flags directly. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. The retained `copy_framebuffer_to_texture_2d` utility bridge now routes 2D framebuffer-to-texture copies through tested `pp_renderer_gl` dispatch, retained `RTT::create`/`RTT::destroy` render-target texture parameter setup, optional depth renderbuffer allocation, framebuffer allocation/attachment/status checks, binding restore, and resource deletion now route through tested `pp_renderer_gl` dispatch, retained RTT clear, masked clear with color-write-mask restore, texture bind/unbind, and RGBA8 dirty-region texture writes now route through tested `pp_renderer_gl` dispatch, retained Canvas, NodeCanvas, and NodeStrokePreview texture-unit switches now route through tested active-texture dispatch, retained Canvas, NodeCanvas, NodeStrokePreview, and desktop HMD viewport/scissor/capability execution now route through tested `pp_renderer_gl` dispatch adapters, retained NodeCanvas, CanvasMode, and NodePanelGrid capability-state snapshots now route through tested `pp_renderer_gl` query dispatch, CanvasLayer cube/equirect generation plus frame clears now route blend state, active texture units, viewport execution, color clears, and cube-face framebuffer-to-texture copies through tested `pp_renderer_gl` dispatch adapters, `NodePanelGrid` live heightmap draw and bake setup now route depth/blend state, depth clears, color-write-mask toggles, active texture selection, bake viewport execution, sun-overlay viewport query, and desktop texture-resize readback through tested `pp_renderer_gl` dispatch adapters, retained CanvasMode overlay/mask/transform paths now route active texture, depth/blend state, transform/cut viewport execution, paint-mode blend/depth state snapshots, and canvas-tip pick framebuffer readback through tested `pp_renderer_gl` dispatch adapters, retained simple UI draw paths now share `legacy_ui_gl_dispatch` for blend-state execution, fallback 2D texture unbinds, `NodeViewport` viewport query/restore, color-buffer clears, and clear-color restore, retained `NodeCanvas` plus `NodeStrokePreview` draw-state paths now route viewport query, clear-color query, color-buffer clear, and clear-color restore through tested `pp_renderer_gl` dispatch helpers, and retained `Canvas` plus `CanvasLayer` stroke/object/thumbnail/frame-clear draw-state paths now route saved viewport or clear-color query and restore through the same tested helpers, but actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, brush-preview compositing, and the retained `ShaderManager::ext_*` compatibility fields still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior |
| DEBT-0037 | Open | Modernization | Recording lifecycle/export planning and execution dispatch now consume pure `pp_app_core` through `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `pano_cli plan-recording-session`, and the `RecordingServices` boundary; live execution is centralized in `src/legacy_recording_services.*`, and retained `PBO` allocation/readback/map/unmap/delete operations now route through tested `pp_renderer_gl` dispatch, but the bridge still owns legacy recording thread startup/shutdown, platform recorded-file cleanup, progress UI, retained `App::rec_loop` readback call sites, and `MP4Encoder::write_mp4` execution | Preserve current timelapse/MP4 behavior while recording moves toward app/document/renderer/video services | `pp_app_core_document_recording_tests`; `pp_renderer_gl_capabilities_tests`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --platform-clears-files`; `ctest --preset desktop-fast --build-config Debug` | Recording thread lifecycle, frame readback scheduling, platform cleanup, progress reporting, and MP4 writing are owned by injected app/renderer/video services with `App` methods acting only as adapters |
| DEBT-0038 | Open | Modernization | Cloud upload/browse/bulk planning and execution dispatch now consume pure `pp_app_core` through `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-upload-all`, `pano_cli plan-cloud-browse`, and the `CloudServices` boundary; live execution is centralized in `src/legacy_cloud_services.*`, the app-owned `upload`/`download`/license curl helpers now ask `PlatformServices` for the Android TLS-verification bypass policy, and retained `Asset::open_url`, `LogRemote::net_init`, and `NodeDialogCloud::load_thumbs_thread` curl sites consume the `pp_platform_api` default TLS policy helper instead of spelling Android branches locally, but the bridge still uses legacy save-before-upload, app-owned curl helpers instead of an injected network service, progress/message UI, OpenGL context guarding, `NodeDialogCloud`, `Canvas` project open, layer refresh, and `ActionManager` reset | Preserve current cloud behavior while cloud/network/document import flows move toward app/document/platform services | `pp_app_core_document_cloud_tests`; `pp_platform_api_tests`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `ctest --preset desktop-fast --build-config Debug` | Cloud upload/download, TLS policy, save-before-upload, progress reporting, cloud browse dialog, downloaded project opening, layer refresh, OpenGL context ownership, and action-history reset are owned by injected app/document/network/platform/renderer services with `App` methods acting only as adapters |
| DEBT-0038 | Open | Modernization | Cloud upload/browse/bulk planning and execution dispatch now consume pure `pp_app_core` through `App::cloud_upload`, `App::cloud_upload_all`, `App::cloud_browse`, `pano_cli plan-cloud-upload`, `pano_cli plan-cloud-upload-all`, `pano_cli plan-cloud-browse`, and the `CloudServices` boundary; live execution is centralized in `src/legacy_cloud_services.*`, the app-owned `upload`/`download` CURL helpers now consume `pp_app_core` cloud transfer request/progress planning and the platform TLS-verification bypass policy before retained CURL setup, `pano_cli plan-cloud-transfer` exposes the same missing endpoint, TLS policy, progress-callback, and progress fraction guards, and retained `Asset::open_url`, `LogRemote::net_init`, and `NodeDialogCloud::load_thumbs_thread` curl sites consume the `pp_platform_api` default TLS policy helper instead of spelling Android branches locally, but the bridge still uses legacy save-before-upload, app-owned curl helpers instead of an injected network service, upload form construction, response/error handling, progress/message UI, OpenGL context guarding, `NodeDialogCloud`, `Canvas` project open, layer refresh, and `ActionManager` reset | Preserve current cloud behavior while cloud/network/document import flows move toward app/document/platform services | `pp_app_core_document_cloud_tests`; `pp_platform_api_tests`; `pano_cli plan-cloud-upload --new-document --unsaved`; `pano_cli plan-cloud-browse --selected-file demo.ppi`; `pano_cli plan-cloud-upload-all --file-count 3`; `pano_cli plan-cloud-transfer --direction download --progress --disable-tls-verification`; `ctest --preset desktop-fast --build-config Debug` | Cloud upload/download, TLS policy, save-before-upload, progress reporting, cloud browse dialog, downloaded project opening, layer refresh, OpenGL context ownership, and action-history reset are owned by injected app/document/network/platform/renderer services with `App` methods acting only as adapters |
| DEBT-0039 | Open | Modernization | Document-open planning and execution dispatch now consume pure `pp_app_core` through `App::open_document`, `pano_cli plan-open-route`, `DocumentOpenServices`, and `src/legacy_document_open_services.*`, but the bridge still opens ABR/PPBR import prompts before delegating import execution to `src/legacy_brush_package_import_services.*`, applies unsaved-project discard prompts, calls legacy project-open execution, refreshes layer UI, updates the app title, and clears legacy history directly | Preserve current file-open/import behavior while document loading and brush import move toward app/document/asset/UI services | `pp_app_core_document_route_tests`; `pp_app_core_document_session_tests`; `pano_cli plan-open-route --path D:/Paint/Scenes/demo.ppi --unsaved`; `pano_cli plan-open-route --path D:/Paint/Brushes/clouds.ABR --unsaved`; `ctest --preset desktop-fast --build-config Debug` | Brush import prompting, project-open execution, unsaved-project discard prompting, layer refresh, title updates, and history clearing are owned by injected app/document/asset/UI services with `App::open_document` acting only as an adapter |
| DEBT-0040 | Open | Modernization | Close request, document save, and save-before-workflow planning/execution dispatch now consume pure `pp_app_core` through `App::request_close`, `App::save_document`, `App::continue_document_workflow_after_optional_save`, `pano_cli simulate-app-session`, `DocumentSaveServices`, `CloseRequestServices`, `DocumentWorkflowServices`, and `src/legacy_document_session_services.*`; Save dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, but the bridge still opens legacy message boxes/save dialogs, calls `Canvas::I->project_save`, mutates the unsaved flag on close confirmation, invokes native app close, and routes save-version through the retained legacy dialog | Preserve current close/save/dirty-workflow behavior while document session execution moves toward app/document/UI/platform services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli simulate-app-session --unsaved --save-intent save-dirty-version`; `pano_cli simulate-app-session --no-canvas`; `pano_cli plan-document-file --work-dir D:/Paint --name demo --target-exists`; `pano_cli plan-document-version --directory D:/Paint --doc-name demo.01 --existing-path D:/Paint/demo.02.ppi`; `ctest --preset desktop-fast --build-config Debug` | Close prompt execution, native close requests, dirty-workflow save prompts, existing-project saves, save dialogs, save-version execution, and unsaved-flag mutation are owned by injected app/document/UI/platform services with `App` methods acting only as adapters |
| DEBT-0041 | Open | Modernization | Accepted new-document planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_newdoc`, `pano_cli plan-new-document`, `NewDocumentServices`, and `src/legacy_document_session_services.*`; New Document dialog working-directory picker visibility/path formatting now dispatches through `PlatformServices`, but the bridge still mutates legacy app document fields, clears legacy layer UI, resizes legacy `Canvas`, clears legacy history, creates the default layer through legacy UI, mutates unsaved/new-document flags, updates the title, and handles keyboard/dialog cleanup directly | Preserve current New Document dialog behavior while document creation moves toward app/document/UI services | `pp_app_core_document_session_tests`; `pp_platform_api_tests`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3`; `pano_cli plan-new-document --work-dir D:/Paint --name demo --resolution-index 3 --target-exists`; `pano_cli simulate-app-session --save-intent save`; `ctest --preset desktop-fast --build-config Debug` | New document creation, overwrite confirmation, canvas/document allocation, default layer creation, history clearing, title updates, dirty/new-document state, and keyboard/dialog cleanup are owned by injected app/document/UI services with `App::dialog_newdoc` acting only as a UI adapter |

View File

@@ -944,6 +944,15 @@ verification policy through `PlatformServices`, and the retained Asset,
LogRemote, and cloud browse-dialog curl sites consume the same default platform
policy helper; retained cloud/network execution remains tracked under
`DEBT-0038`.
`pp_app_core` now also owns tested cloud transfer request and progress planning
through `plan_cloud_download_transfer`, `plan_cloud_upload_transfer`, and
`plan_cloud_transfer_progress`. Live `App::download` and `App::upload` consume
those plans before retained CURL setup, including missing endpoint rejection,
progress-callback enablement, TLS-verification policy, and zero/overrun progress
guards; `pano_cli plan-cloud-transfer` exposes the same path for automation.
Actual CURL ownership, upload form construction, response/error handling,
progress UI, and downloaded-project execution remain tracked under
`DEBT-0038`.
`pano_cli parse-layout` exercises the XML layout path. Continue expanding
document behavior toward legacy Canvas parity and then port OpenGL classes
behind the renderer boundary.
@@ -1605,6 +1614,19 @@ Results:
`pano_cli_plan_cloud_browse_selected_smoke`, and
`pano_cli_plan_cloud_browse_no_canvas_smoke` passed and expose app-core cloud
browse/download-selection decisions as JSON.
- `pp_app_core_document_cloud_tests` now also covers cloud transfer request
validation, progress-callback enablement, TLS-verification policy, and
zero/negative/overrun transfer-progress guards.
- `pano_cli_plan_cloud_transfer_download_smoke`,
`pano_cli_plan_cloud_transfer_upload_smoke`,
`pano_cli_plan_cloud_transfer_rejects_missing_destination`, and
`pano_cli_plan_cloud_transfer_zero_total_smoke` passed and expose the
app-core cloud transfer path as JSON.
- `PanoPainter`, `pp_app_core_document_cloud_tests`, and `pano_cli` built after
live `App::download` and `App::upload` started consuming the transfer plans
before retained CURL setup.
- Android arm64 headless `pp_app_core`, `pano_cli`, and
`pp_app_core_document_cloud_tests` built after the cloud transfer slice.
- `PanoPainter`, `pp_app_core_document_cloud_tests`, and `pano_cli` built after
live cloud upload, bulk upload, and browse/download execution moved behind
the `CloudServices` boundary and `src/legacy_cloud_services.*`.

View File

@@ -11,6 +11,7 @@
#include "app_core/app_startup.h"
#include "app_core/app_thread.h"
#include "app_core/canvas_tool_ui.h"
#include "app_core/document_cloud.h"
#include "app_core/document_recording.h"
#include "app_core/document_route.h"
#include "app_core/document_session.h"
@@ -272,16 +273,28 @@ void App::initLog()
int progress_callback_download(void *clientp, curl_off_t dltotal,
curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
std::function<void(float)> progress = *(std::function<void(float)>*)clientp;
progress((float)dlnow / (float)dltotal);
(void)ultotal;
(void)ulnow;
auto* progress = static_cast<std::function<void(float)>*>(clientp);
const auto plan = pp::app::plan_cloud_transfer_progress(
static_cast<std::int64_t>(dltotal),
static_cast<std::int64_t>(dlnow));
if (progress != nullptr && *progress && plan.notify)
(*progress)(plan.fraction);
return 0;
}
int progress_callback_upload(void *clientp, curl_off_t dltotal,
curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
std::function<void(float)> progress = *(std::function<void(float)>*)clientp;
progress((float)ulnow / (float)ultotal);
(void)dltotal;
(void)dlnow;
auto* progress = static_cast<std::function<void(float)>*>(clientp);
const auto plan = pp::app::plan_cloud_transfer_progress(
static_cast<std::int64_t>(ultotal),
static_cast<std::int64_t>(ulnow));
if (progress != nullptr && *progress && plan.notify)
(*progress)(plan.fraction);
return 0;
}
#endif //CURL
@@ -289,17 +302,32 @@ int progress_callback_upload(void *clientp, curl_off_t dltotal,
void App::download(std::string url, std::string dest_filepath, std::function<void(float)> progress)
{
#if WITH_CURL
const auto plan = pp::app::plan_cloud_download_transfer(
url,
dest_filepath,
progress != nullptr,
disables_network_tls_verification());
if (plan.action != pp::app::CloudTransferAction::start_transfer) {
LOG("download skipped: invalid transfer request");
return;
}
CURL *curl = curl_easy_init();
if (curl)
{
FILE* fp = fopen(dest_filepath.c_str(), "wb");
if (fp == nullptr) {
LOG("download failed to open destination %s", dest_filepath.c_str());
curl_easy_cleanup(curl);
return;
}
LOG("download %s to %s", url.c_str(), dest_filepath.c_str());
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_write);
if (disables_network_tls_verification())
if (plan.disable_tls_verification)
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
if (progress)
if (plan.enable_progress)
{
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback_download);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress);
@@ -349,6 +377,15 @@ bool App::check_license()
void App::upload(std::string filename, std::string name, std::function<void(float)> progress)
{
#if WITH_CURL
const auto plan = pp::app::plan_cloud_upload_transfer(
filename,
progress != nullptr,
disables_network_tls_verification());
if (plan.action != pp::app::CloudTransferAction::start_transfer) {
LOG("upload skipped: invalid transfer request");
return;
}
CURL *curl;
struct curl_httppost *formpost = NULL;
@@ -372,9 +409,9 @@ void App::upload(std::string filename, std::string name, std::function<void(floa
curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_handler);
if (disables_network_tls_verification())
if (plan.disable_tls_verification)
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
if (progress)
if (plan.enable_progress)
{
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback_upload);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress);

View File

@@ -3,6 +3,7 @@
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <limits>
#include <string>
#include <string_view>
@@ -25,6 +26,17 @@ enum class CloudDownloadSelectionAction {
start_download,
};
enum class CloudTransferDirection {
download,
upload,
};
enum class CloudTransferAction {
reject_missing_source,
reject_missing_destination,
start_transfer,
};
struct CloudUploadPlan {
CloudUploadAction action = CloudUploadAction::unavailable_no_canvas;
bool save_before_upload = false;
@@ -42,6 +54,18 @@ struct CloudDownloadRequest {
std::string selected_name;
};
struct CloudTransferPlan {
CloudTransferDirection direction = CloudTransferDirection::download;
CloudTransferAction action = CloudTransferAction::reject_missing_source;
bool enable_progress = false;
bool disable_tls_verification = false;
};
struct CloudTransferProgressPlan {
bool notify = false;
float fraction = 0.0F;
};
class CloudServices {
public:
virtual ~CloudServices() = default;
@@ -98,6 +122,77 @@ public:
};
}
[[nodiscard]] constexpr CloudTransferPlan plan_cloud_download_transfer(
std::string_view url,
std::string_view destination_path,
bool has_progress_callback,
bool disables_tls_verification) noexcept
{
if (url.empty()) {
return {
CloudTransferDirection::download,
CloudTransferAction::reject_missing_source,
false,
false,
};
}
if (destination_path.empty()) {
return {
CloudTransferDirection::download,
CloudTransferAction::reject_missing_destination,
false,
false,
};
}
return {
CloudTransferDirection::download,
CloudTransferAction::start_transfer,
has_progress_callback,
disables_tls_verification,
};
}
[[nodiscard]] constexpr CloudTransferPlan plan_cloud_upload_transfer(
std::string_view filename,
bool has_progress_callback,
bool disables_tls_verification) noexcept
{
if (filename.empty()) {
return {
CloudTransferDirection::upload,
CloudTransferAction::reject_missing_source,
false,
false,
};
}
return {
CloudTransferDirection::upload,
CloudTransferAction::start_transfer,
has_progress_callback,
disables_tls_verification,
};
}
[[nodiscard]] constexpr CloudTransferProgressPlan plan_cloud_transfer_progress(
std::int64_t total,
std::int64_t current) noexcept
{
if (total <= 0) {
return {};
}
const auto clamped_current = current < 0
? std::int64_t { 0 }
: (current > total ? total : current);
return {
true,
static_cast<float>(static_cast<double>(clamped_current) / static_cast<double>(total)),
};
}
[[nodiscard]] inline pp::foundation::Status execute_cloud_upload_plan(
const CloudUploadPlan& plan,
CloudServices& services)

View File

@@ -924,6 +924,30 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-cloud-upload-all\".*\"fileCount\":3.*\"progressUiAvailable\":false.*\"progressTotal\":3.*\"showProgress\":false")
add_test(NAME pano_cli_plan_cloud_transfer_download_smoke
COMMAND pano_cli plan-cloud-transfer --direction download --progress --disable-tls-verification --progress-total 100 --progress-current 25)
set_tests_properties(pano_cli_plan_cloud_transfer_download_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-cloud-transfer\".*\"direction\":\"download\".*\"action\":\"start-transfer\".*\"enableProgress\":true.*\"disableTlsVerification\":true.*\"notify\":true.*\"fraction\":0.25")
add_test(NAME pano_cli_plan_cloud_transfer_upload_smoke
COMMAND pano_cli plan-cloud-transfer --direction upload --source D:/Paint/demo.ppi --progress-total 10 --progress-current 20)
set_tests_properties(pano_cli_plan_cloud_transfer_upload_smoke PROPERTIES
LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-cloud-transfer\".*\"direction\":\"upload\".*\"action\":\"start-transfer\".*\"enableProgress\":false.*\"notify\":true.*\"fraction\":1")
add_test(NAME pano_cli_plan_cloud_transfer_rejects_missing_destination
COMMAND pano_cli plan-cloud-transfer --direction download --destination "")
set_tests_properties(pano_cli_plan_cloud_transfer_rejects_missing_destination PROPERTIES
LABELS "app;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-cloud-transfer\".*\"action\":\"reject-missing-destination\".*\"enableProgress\":false")
add_test(NAME pano_cli_plan_cloud_transfer_zero_total_smoke
COMMAND pano_cli plan-cloud-transfer --progress --progress-total 0 --progress-current 10)
set_tests_properties(pano_cli_plan_cloud_transfer_zero_total_smoke PROPERTIES
LABELS "app;integration;desktop-fast;fuzz"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-cloud-transfer\".*\"action\":\"start-transfer\".*\"notify\":false.*\"fraction\":0")
add_test(NAME pano_cli_plan_recording_session_stopped_smoke
COMMAND pano_cli plan-recording-session --frame-count 12)
set_tests_properties(pano_cli_plan_recording_session_stopped_smoke PROPERTIES

View File

@@ -157,6 +157,78 @@ void cloud_bulk_upload_clamps_progress_total(pp::tests::Harness& harness)
PP_EXPECT(harness, plan.show_progress);
}
void cloud_download_transfer_rejects_missing_url(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_cloud_download_transfer("", "D:/Paint/demo.ppi", true, true);
PP_EXPECT(harness, plan.direction == pp::app::CloudTransferDirection::download);
PP_EXPECT(harness, plan.action == pp::app::CloudTransferAction::reject_missing_source);
PP_EXPECT(harness, !plan.enable_progress);
PP_EXPECT(harness, !plan.disable_tls_verification);
}
void cloud_download_transfer_rejects_missing_destination(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_cloud_download_transfer("https://example.invalid/demo.ppi", "", true, true);
PP_EXPECT(harness, plan.direction == pp::app::CloudTransferDirection::download);
PP_EXPECT(harness, plan.action == pp::app::CloudTransferAction::reject_missing_destination);
PP_EXPECT(harness, !plan.enable_progress);
PP_EXPECT(harness, !plan.disable_tls_verification);
}
void cloud_download_transfer_starts_with_progress_and_tls_policy(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_cloud_download_transfer(
"https://example.invalid/demo.ppi",
"D:/Paint/demo.ppi",
true,
true);
PP_EXPECT(harness, plan.direction == pp::app::CloudTransferDirection::download);
PP_EXPECT(harness, plan.action == pp::app::CloudTransferAction::start_transfer);
PP_EXPECT(harness, plan.enable_progress);
PP_EXPECT(harness, plan.disable_tls_verification);
}
void cloud_upload_transfer_rejects_missing_file(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_cloud_upload_transfer("", true, true);
PP_EXPECT(harness, plan.direction == pp::app::CloudTransferDirection::upload);
PP_EXPECT(harness, plan.action == pp::app::CloudTransferAction::reject_missing_source);
PP_EXPECT(harness, !plan.enable_progress);
PP_EXPECT(harness, !plan.disable_tls_verification);
}
void cloud_upload_transfer_starts_with_progress_and_tls_policy(pp::tests::Harness& harness)
{
const auto plan = pp::app::plan_cloud_upload_transfer("D:/Paint/demo.ppi", true, false);
PP_EXPECT(harness, plan.direction == pp::app::CloudTransferDirection::upload);
PP_EXPECT(harness, plan.action == pp::app::CloudTransferAction::start_transfer);
PP_EXPECT(harness, plan.enable_progress);
PP_EXPECT(harness, !plan.disable_tls_verification);
}
void cloud_transfer_progress_ignores_unknown_total(pp::tests::Harness& harness)
{
const auto zero = pp::app::plan_cloud_transfer_progress(0, 4);
const auto negative = pp::app::plan_cloud_transfer_progress(-10, 4);
PP_EXPECT(harness, !zero.notify);
PP_EXPECT(harness, zero.fraction == 0.0F);
PP_EXPECT(harness, !negative.notify);
PP_EXPECT(harness, negative.fraction == 0.0F);
}
void cloud_transfer_progress_clamps_to_valid_fraction(pp::tests::Harness& harness)
{
const auto before_start = pp::app::plan_cloud_transfer_progress(10, -2);
const auto half = pp::app::plan_cloud_transfer_progress(10, 5);
const auto after_end = pp::app::plan_cloud_transfer_progress(10, 20);
PP_EXPECT(harness, before_start.notify);
PP_EXPECT(harness, before_start.fraction == 0.0F);
PP_EXPECT(harness, half.notify);
PP_EXPECT(harness, half.fraction == 0.5F);
PP_EXPECT(harness, after_end.notify);
PP_EXPECT(harness, after_end.fraction == 1.0F);
}
void executor_dispatches_cloud_upload_variants(pp::tests::Harness& harness)
{
FakeCloudServices services;
@@ -247,6 +319,13 @@ int main()
harness.run("cloud bulk upload runs without progress when ui unavailable", cloud_bulk_upload_runs_without_progress_when_ui_unavailable);
harness.run("cloud bulk upload keeps zero file progress explicit", cloud_bulk_upload_keeps_zero_file_progress_explicit);
harness.run("cloud bulk upload clamps progress total", cloud_bulk_upload_clamps_progress_total);
harness.run("cloud download transfer rejects missing url", cloud_download_transfer_rejects_missing_url);
harness.run("cloud download transfer rejects missing destination", cloud_download_transfer_rejects_missing_destination);
harness.run("cloud download transfer starts with progress and tls policy", cloud_download_transfer_starts_with_progress_and_tls_policy);
harness.run("cloud upload transfer rejects missing file", cloud_upload_transfer_rejects_missing_file);
harness.run("cloud upload transfer starts with progress and tls policy", cloud_upload_transfer_starts_with_progress_and_tls_policy);
harness.run("cloud transfer progress ignores unknown total", cloud_transfer_progress_ignores_unknown_total);
harness.run("cloud transfer progress clamps to valid fraction", cloud_transfer_progress_clamps_to_valid_fraction);
harness.run("executor dispatches cloud upload variants", executor_dispatches_cloud_upload_variants);
harness.run("executor dispatches cloud bulk browse and download", executor_dispatches_cloud_bulk_browse_and_download);
harness.run("executor rejects mismatched download action", executor_rejects_mismatched_download_action);

View File

@@ -194,6 +194,16 @@ struct PlanCloudUploadAllArgs {
bool progress_ui_available = true;
};
struct PlanCloudTransferArgs {
pp::app::CloudTransferDirection direction = pp::app::CloudTransferDirection::download;
std::string source = "https://example.invalid/demo.ppi";
std::string destination = "D:/Paint/demo.ppi";
bool progress_callback = false;
bool disable_tls_verification = false;
std::int64_t progress_total = 100;
std::int64_t progress_current = 25;
};
struct PlanRecordingSessionArgs {
bool running = false;
std::uint32_t frame_count = 0;
@@ -1895,6 +1905,32 @@ const char* cloud_download_selection_action_name(pp::app::CloudDownloadSelection
return "wait-for-selection";
}
const char* cloud_transfer_direction_name(pp::app::CloudTransferDirection direction) noexcept
{
switch (direction) {
case pp::app::CloudTransferDirection::download:
return "download";
case pp::app::CloudTransferDirection::upload:
return "upload";
}
return "download";
}
const char* cloud_transfer_action_name(pp::app::CloudTransferAction action) noexcept
{
switch (action) {
case pp::app::CloudTransferAction::reject_missing_source:
return "reject-missing-source";
case pp::app::CloudTransferAction::reject_missing_destination:
return "reject-missing-destination";
case pp::app::CloudTransferAction::start_transfer:
return "start-transfer";
}
return "reject-missing-source";
}
const char* recording_start_action_name(pp::app::RecordingStartAction action) noexcept
{
switch (action) {
@@ -2074,6 +2110,20 @@ pp::foundation::Result<int> parse_i32_arg(std::string_view text)
return pp::foundation::Result<int>::success(value);
}
pp::foundation::Result<std::int64_t> parse_i64_arg(std::string_view text)
{
std::int64_t value = 0;
const auto* begin = text.data();
const auto* end = begin + text.size();
const auto [ptr, ec] = std::from_chars(begin, end, value);
if (ec != std::errc {} || ptr != end) {
return pp::foundation::Result<std::int64_t>::failure(
pp::foundation::Status::invalid_argument("invalid signed integer value"));
}
return pp::foundation::Result<std::int64_t>::success(value);
}
void print_help()
{
std::cout
@@ -2096,6 +2146,7 @@ void print_help()
<< " plan-cloud-upload [--no-canvas] [--new-document] [--unsaved]\n"
<< " plan-cloud-browse [--no-canvas] [--selected-file FILE]\n"
<< " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\n"
<< " plan-cloud-transfer [--direction download|upload] [--source TEXT] [--destination FILE] [--progress] [--disable-tls-verification] [--progress-total N] [--progress-current N]\n"
<< " plan-recording-session [--running] [--frame-count N] [--platform-deletes-recorded-files] [--no-encoder] [--no-canvas]\n"
<< " plan-app-preferences [--ui-scale N] [--display-density N] [--current-scale N] [--scale-option N] [--viewport-scale N] [--rtl] [--timelapse-disabled] [--recording-running] [--vr-controllers-disabled] [--cursor-mode N]\n"
<< " plan-app-startup [--run-counter N] [--auto-timelapse-disabled] [--vr-controllers-disabled] [--license-invalid]\n"
@@ -3525,6 +3576,105 @@ int plan_cloud_upload_all(int argc, char** argv)
return 0;
}
pp::foundation::Status parse_plan_cloud_transfer_args(
int argc,
char** argv,
PlanCloudTransferArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--direction") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const std::string_view value(argv[++i]);
if (value == "download") {
args.direction = pp::app::CloudTransferDirection::download;
} else if (value == "upload") {
args.direction = pp::app::CloudTransferDirection::upload;
} else {
return pp::foundation::Status::invalid_argument("unknown transfer direction");
}
} else if (key == "--source") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.source = argv[++i];
} else if (key == "--destination") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.destination = argv[++i];
} else if (key == "--progress") {
args.progress_callback = true;
} else if (key == "--disable-tls-verification") {
args.disable_tls_verification = true;
} else if (key == "--progress-total") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_i64_arg(argv[++i]);
if (!value) {
return value.status();
}
args.progress_total = value.value();
} else if (key == "--progress-current") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
const auto value = parse_i64_arg(argv[++i]);
if (!value) {
return value.status();
}
args.progress_current = value.value();
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
return pp::foundation::Status::success();
}
int plan_cloud_transfer(int argc, char** argv)
{
PlanCloudTransferArgs args;
const auto status = parse_plan_cloud_transfer_args(argc, argv, args);
if (!status.ok()) {
print_error("plan-cloud-transfer", status.message);
return 2;
}
const auto transfer = args.direction == pp::app::CloudTransferDirection::download
? pp::app::plan_cloud_download_transfer(
args.source,
args.destination,
args.progress_callback,
args.disable_tls_verification)
: pp::app::plan_cloud_upload_transfer(
args.source,
args.progress_callback,
args.disable_tls_verification);
const auto progress = pp::app::plan_cloud_transfer_progress(
args.progress_total,
args.progress_current);
std::cout << "{\"ok\":true,\"command\":\"plan-cloud-transfer\""
<< ",\"state\":{\"direction\":\"" << cloud_transfer_direction_name(args.direction)
<< "\",\"source\":\"" << json_escape(args.source)
<< "\",\"destination\":\"" << json_escape(args.destination)
<< "\",\"progressCallback\":" << json_bool(args.progress_callback)
<< ",\"disableTlsVerification\":" << json_bool(args.disable_tls_verification)
<< ",\"progressTotal\":" << args.progress_total
<< ",\"progressCurrent\":" << args.progress_current
<< "},\"plan\":{\"direction\":\"" << cloud_transfer_direction_name(transfer.direction)
<< "\",\"action\":\"" << cloud_transfer_action_name(transfer.action)
<< "\",\"enableProgress\":" << json_bool(transfer.enable_progress)
<< ",\"disableTlsVerification\":" << json_bool(transfer.disable_tls_verification)
<< "},\"progress\":{\"notify\":" << json_bool(progress.notify)
<< ",\"fraction\":" << progress.fraction
<< "}}\n";
return 0;
}
pp::foundation::Status parse_plan_recording_session_args(
int argc,
char** argv,
@@ -10725,6 +10875,10 @@ int main(int argc, char** argv)
return plan_cloud_upload_all(argc, argv);
}
if (command == "plan-cloud-transfer") {
return plan_cloud_transfer(argc, argv);
}
if (command == "plan-recording-session") {
return plan_recording_session(argc, argv);
}