Plan cloud transfer requests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.*`.
|
||||
|
||||
53
src/app.cpp
53
src/app.cpp
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user