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 new-document warning, clean publish prompt, and dirty save-before-upload
decisions, plus cloud browse no-canvas/show-browser and selected-download decisions, plus cloud browse no-canvas/show-browser and selected-download
decisions, plus bulk upload progress visibility, zero-file, and clamped 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, - `pp_app_core_document_session_tests` covers clean and dirty app session,
document-open action planning and executor dispatch/rejection, save-request, document-open action planning and executor dispatch/rejection, save-request,
close-request executor dispatch/no-op preservation, document-save executor 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 `App::rec_loop` and `pano_cli plan-recording-session`; retained PBO
equirect generation, dirty-stroke mutation, MP4 encoder calls, and frame equirect generation, dirty-stroke mutation, MP4 encoder calls, and frame
label rendering remain in the legacy app/canvas/video path. 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, - 2026-06-04: DEBT-0036 was narrowed again. Canvas stroke commit,
thumbnail, and object-draw history paths now query saved blend state through thumbnail, and object-draw history paths now query saved blend state through
tested `pp_renderer_gl` capability-state dispatch; CanvasLayer equirect 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-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-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-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-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-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 | | 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 LogRemote, and cloud browse-dialog curl sites consume the same default platform
policy helper; retained cloud/network execution remains tracked under policy helper; retained cloud/network execution remains tracked under
`DEBT-0038`. `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 `pano_cli parse-layout` exercises the XML layout path. Continue expanding
document behavior toward legacy Canvas parity and then port OpenGL classes document behavior toward legacy Canvas parity and then port OpenGL classes
behind the renderer boundary. behind the renderer boundary.
@@ -1605,6 +1614,19 @@ Results:
`pano_cli_plan_cloud_browse_selected_smoke`, and `pano_cli_plan_cloud_browse_selected_smoke`, and
`pano_cli_plan_cloud_browse_no_canvas_smoke` passed and expose app-core cloud `pano_cli_plan_cloud_browse_no_canvas_smoke` passed and expose app-core cloud
browse/download-selection decisions as JSON. 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 - `PanoPainter`, `pp_app_core_document_cloud_tests`, and `pano_cli` built after
live cloud upload, bulk upload, and browse/download execution moved behind live cloud upload, bulk upload, and browse/download execution moved behind
the `CloudServices` boundary and `src/legacy_cloud_services.*`. the `CloudServices` boundary and `src/legacy_cloud_services.*`.

View File

@@ -11,6 +11,7 @@
#include "app_core/app_startup.h" #include "app_core/app_startup.h"
#include "app_core/app_thread.h" #include "app_core/app_thread.h"
#include "app_core/canvas_tool_ui.h" #include "app_core/canvas_tool_ui.h"
#include "app_core/document_cloud.h"
#include "app_core/document_recording.h" #include "app_core/document_recording.h"
#include "app_core/document_route.h" #include "app_core/document_route.h"
#include "app_core/document_session.h" #include "app_core/document_session.h"
@@ -272,16 +273,28 @@ void App::initLog()
int progress_callback_download(void *clientp, curl_off_t dltotal, int progress_callback_download(void *clientp, curl_off_t dltotal,
curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{ {
std::function<void(float)> progress = *(std::function<void(float)>*)clientp; (void)ultotal;
progress((float)dlnow / (float)dltotal); (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; return 0;
} }
int progress_callback_upload(void *clientp, curl_off_t dltotal, int progress_callback_upload(void *clientp, curl_off_t dltotal,
curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{ {
std::function<void(float)> progress = *(std::function<void(float)>*)clientp; (void)dltotal;
progress((float)ulnow / (float)ultotal); (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; return 0;
} }
#endif //CURL #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) void App::download(std::string url, std::string dest_filepath, std::function<void(float)> progress)
{ {
#if WITH_CURL #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(); CURL *curl = curl_easy_init();
if (curl) if (curl)
{ {
FILE* fp = fopen(dest_filepath.c_str(), "wb"); 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()); 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_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_write); 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); 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_XFERINFOFUNCTION, progress_callback_download);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress); 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) void App::upload(std::string filename, std::string name, std::function<void(float)> progress)
{ {
#if WITH_CURL #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; CURL *curl;
struct curl_httppost *formpost = NULL; 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_HTTPPOST, formpost);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_handler); 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); 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_XFERINFOFUNCTION, progress_callback_upload);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress);

View File

@@ -3,6 +3,7 @@
#include "foundation/result.h" #include "foundation/result.h"
#include <cstddef> #include <cstddef>
#include <cstdint>
#include <limits> #include <limits>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -25,6 +26,17 @@ enum class CloudDownloadSelectionAction {
start_download, start_download,
}; };
enum class CloudTransferDirection {
download,
upload,
};
enum class CloudTransferAction {
reject_missing_source,
reject_missing_destination,
start_transfer,
};
struct CloudUploadPlan { struct CloudUploadPlan {
CloudUploadAction action = CloudUploadAction::unavailable_no_canvas; CloudUploadAction action = CloudUploadAction::unavailable_no_canvas;
bool save_before_upload = false; bool save_before_upload = false;
@@ -42,6 +54,18 @@ struct CloudDownloadRequest {
std::string selected_name; 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 { class CloudServices {
public: public:
virtual ~CloudServices() = default; 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( [[nodiscard]] inline pp::foundation::Status execute_cloud_upload_plan(
const CloudUploadPlan& plan, const CloudUploadPlan& plan,
CloudServices& services) CloudServices& services)

View File

@@ -924,6 +924,30 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast" LABELS "app;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"command\":\"plan-cloud-upload-all\".*\"fileCount\":3.*\"progressUiAvailable\":false.*\"progressTotal\":3.*\"showProgress\":false") 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 add_test(NAME pano_cli_plan_recording_session_stopped_smoke
COMMAND pano_cli plan-recording-session --frame-count 12) COMMAND pano_cli plan-recording-session --frame-count 12)
set_tests_properties(pano_cli_plan_recording_session_stopped_smoke PROPERTIES 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); 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) void executor_dispatches_cloud_upload_variants(pp::tests::Harness& harness)
{ {
FakeCloudServices services; 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 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 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 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 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 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); 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; 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 { struct PlanRecordingSessionArgs {
bool running = false; bool running = false;
std::uint32_t frame_count = 0; std::uint32_t frame_count = 0;
@@ -1895,6 +1905,32 @@ const char* cloud_download_selection_action_name(pp::app::CloudDownloadSelection
return "wait-for-selection"; 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 const char* recording_start_action_name(pp::app::RecordingStartAction action) noexcept
{ {
switch (action) { switch (action) {
@@ -2074,6 +2110,20 @@ pp::foundation::Result<int> parse_i32_arg(std::string_view text)
return pp::foundation::Result<int>::success(value); 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() void print_help()
{ {
std::cout std::cout
@@ -2096,6 +2146,7 @@ void print_help()
<< " plan-cloud-upload [--no-canvas] [--new-document] [--unsaved]\n" << " plan-cloud-upload [--no-canvas] [--new-document] [--unsaved]\n"
<< " plan-cloud-browse [--no-canvas] [--selected-file FILE]\n" << " plan-cloud-browse [--no-canvas] [--selected-file FILE]\n"
<< " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\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-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-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" << " 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; 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( pp::foundation::Status parse_plan_recording_session_args(
int argc, int argc,
char** argv, char** argv,
@@ -10725,6 +10875,10 @@ int main(int argc, char** argv)
return plan_cloud_upload_all(argc, argv); return plan_cloud_upload_all(argc, argv);
} }
if (command == "plan-cloud-transfer") {
return plan_cloud_transfer(argc, argv);
}
if (command == "plan-recording-session") { if (command == "plan-recording-session") {
return plan_recording_session(argc, argv); return plan_recording_session(argc, argv);
} }