diff --git a/package-lock.json b/package-lock.json
index e583faf3e..7b2be179f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -59,6 +59,7 @@
"strip-ansi": "^7.1.2",
"tar": "^7.5.2",
"ts-node": "^10.9.2",
+ "tus-js-client": "^4.3.1",
"unzipper": "^0.12.3",
"winreg": "^1.2.5",
"wpcom": "^7.1.1",
@@ -20665,9 +20666,9 @@
}
},
"node_modules/js-base64": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
- "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
+ "version": "3.7.8",
+ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
+ "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
"license": "BSD-3-Clause"
},
"node_modules/js-tokens": {
@@ -24352,25 +24353,14 @@
}
},
"node_modules/proper-lockfile": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-2.0.1.tgz",
- "integrity": "sha512-rjaeGbsmhNDcDInmwi4MuI6mRwJu6zq8GjYCLuSuE7GF+4UjgzkL69sVKKJ2T2xH61kK7rXvGYpvaTu909oXaQ==",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
+ "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
"license": "MIT",
"dependencies": {
- "graceful-fs": "^4.1.2",
- "retry": "^0.10.0"
- },
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/proper-lockfile/node_modules/retry": {
- "version": "0.10.1",
- "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
- "integrity": "sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ==",
- "license": "MIT",
- "engines": {
- "node": "*"
+ "graceful-fs": "^4.2.4",
+ "retry": "^0.12.0",
+ "signal-exit": "^3.0.2"
}
},
"node_modules/property-information": {
@@ -25301,7 +25291,6 @@
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
@@ -27527,18 +27516,21 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/tus-js-client": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-2.3.2.tgz",
- "integrity": "sha512-5a2rm7gp+G7Z+ZB0AO4PzD/dwczB3n1fZeWO5W8AWLJ12RRk1rY4Aeb2VAYX9oKGE+/rGPrdxoFPA/vDSVKnpg==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-4.3.1.tgz",
+ "integrity": "sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==",
"license": "MIT",
"dependencies": {
"buffer-from": "^1.1.2",
"combine-errors": "^3.0.3",
"is-stream": "^2.0.0",
- "js-base64": "^2.6.1",
+ "js-base64": "^3.7.2",
"lodash.throttle": "^4.1.1",
- "proper-lockfile": "^2.0.1",
+ "proper-lockfile": "^4.1.2",
"url-parse": "^1.5.7"
+ },
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/tus-js-client/node_modules/is-stream": {
@@ -29323,6 +29315,70 @@
"wp-error": "^1.3.0"
}
},
+ "node_modules/wpcom-xhr-request/node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/wpcom/node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/wpcom/node_modules/js-base64": {
+ "version": "2.6.4",
+ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
+ "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/wpcom/node_modules/proper-lockfile": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-2.0.1.tgz",
+ "integrity": "sha512-rjaeGbsmhNDcDInmwi4MuI6mRwJu6zq8GjYCLuSuE7GF+4UjgzkL69sVKKJ2T2xH61kK7rXvGYpvaTu909oXaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "retry": "^0.10.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/wpcom/node_modules/retry": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
+ "integrity": "sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/wpcom/node_modules/tus-js-client": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-2.3.2.tgz",
+ "integrity": "sha512-5a2rm7gp+G7Z+ZB0AO4PzD/dwczB3n1fZeWO5W8AWLJ12RRk1rY4Aeb2VAYX9oKGE+/rGPrdxoFPA/vDSVKnpg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.1.2",
+ "combine-errors": "^3.0.3",
+ "is-stream": "^2.0.0",
+ "js-base64": "^2.6.1",
+ "lodash.throttle": "^4.1.1",
+ "proper-lockfile": "^2.0.1",
+ "url-parse": "^1.5.7"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
diff --git a/package.json b/package.json
index 0c3478445..ba4f220b8 100644
--- a/package.json
+++ b/package.json
@@ -153,6 +153,7 @@
"strip-ansi": "^7.1.2",
"tar": "^7.5.2",
"ts-node": "^10.9.2",
+ "tus-js-client": "^4.3.1",
"unzipper": "^0.12.3",
"winreg": "^1.2.5",
"wpcom": "^7.1.1",
diff --git a/src/hooks/sync-sites/use-sync-push.ts b/src/hooks/sync-sites/use-sync-push.ts
index 1bb778c87..6552b0afe 100644
--- a/src/hooks/sync-sites/use-sync-push.ts
+++ b/src/hooks/sync-sites/use-sync-push.ts
@@ -11,6 +11,7 @@ import {
usePullPushStates,
} from 'src/hooks/sync-sites/use-pull-push-states';
import { useAuth } from 'src/hooks/use-auth';
+import { useIpcListener } from 'src/hooks/use-ipc-listener';
import {
useSyncStatesProgressInfo,
PushStateProgressInfo,
@@ -308,6 +309,7 @@ export function useSyncPush( {
try {
const response = await getIpcApi().pushArchive(
+ selectedSite.id,
remoteSiteId,
archivePath,
options?.optionsToSync,
@@ -378,6 +380,24 @@ export function useSyncPush( {
isKeyCancelled,
] );
+ useIpcListener(
+ 'sync-upload-paused',
+ ( _event, payload: { selectedSiteId: string; remoteSiteId: number; error: string } ) => {
+ updatePushState( payload.selectedSiteId, payload.remoteSiteId, {
+ status: pushStatesProgressInfo.uploadingPaused,
+ } );
+ }
+ );
+
+ useIpcListener(
+ 'sync-upload-resumed',
+ ( _event, payload: { selectedSiteId: string; remoteSiteId: number } ) => {
+ updatePushState( payload.selectedSiteId, payload.remoteSiteId, {
+ status: pushStatesProgressInfo.uploading,
+ } );
+ }
+ );
+
const isAnySitePushing = useMemo< boolean >( () => {
return Object.values( pushStates ).some( ( state ) => isKeyPushing( state.status.key ) );
}, [ pushStates, isKeyPushing ] );
diff --git a/src/hooks/use-sync-states-progress-info.ts b/src/hooks/use-sync-states-progress-info.ts
index 1057ed7f2..4864b40b0 100644
--- a/src/hooks/use-sync-states-progress-info.ts
+++ b/src/hooks/use-sync-states-progress-info.ts
@@ -16,7 +16,8 @@ export type PushStateProgressInfo = {
| 'finishing'
| 'finished'
| 'failed'
- | 'cancelled';
+ | 'cancelled'
+ | 'uploadingPaused';
progress: number;
message: string;
};
@@ -105,6 +106,11 @@ export function useSyncStatesProgressInfo() {
progress: 40,
message: __( 'Uploading Studio siteā¦' ),
},
+ uploadingPaused: {
+ key: 'uploadingPaused',
+ progress: 45,
+ message: __( 'Uploading paused' ),
+ },
creatingRemoteBackup: {
key: 'creatingRemoteBackup',
progress: 50,
@@ -164,6 +170,10 @@ export function useSyncStatesProgressInfo() {
return pushingStateKeys.includes( key );
};
+ const isKeyUploadingPaused = ( key: PushStateProgressInfo[ 'key' ] | undefined ) => {
+ return key === 'uploadingPaused';
+ };
+
const isKeyImporting = ( key: PushStateProgressInfo[ 'key' ] | undefined ) => {
const pushingStateKeys: PushStateProgressInfo[ 'key' ][] = [
'creatingRemoteBackup',
@@ -296,5 +306,6 @@ export function useSyncStatesProgressInfo() {
getBackupStatusWithProgress,
getPullStatusWithProgress,
getPushStatusWithProgress,
+ isKeyUploadingPaused,
};
}
diff --git a/src/ipc-utils.ts b/src/ipc-utils.ts
index 7e1a91059..9893ac993 100644
--- a/src/ipc-utils.ts
+++ b/src/ipc-utils.ts
@@ -38,6 +38,8 @@ export interface IpcEvents {
},
];
'site-context-menu-action': [ { action: string; siteId: string } ];
+ 'sync-upload-paused': [ { error: string; selectedSiteId: string; remoteSiteId: number } ];
+ 'sync-upload-resumed': [ { selectedSiteId: string; remoteSiteId: number } ];
'snapshot-error': [ { operationId: crypto.UUID; data: SnapshotEventData } ];
'snapshot-fatal-error': [ { operationId: crypto.UUID; data: { message: string } } ];
'snapshot-output': [ { operationId: crypto.UUID; data: SnapshotEventData } ];
diff --git a/src/modules/sync/components/sync-connected-sites.tsx b/src/modules/sync/components/sync-connected-sites.tsx
index fd2cb10de..2a6a2f48a 100644
--- a/src/modules/sync/components/sync-connected-sites.tsx
+++ b/src/modules/sync/components/sync-connected-sites.tsx
@@ -1,7 +1,7 @@
import { Icon } from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
import { sprintf } from '@wordpress/i18n';
-import { cloudUpload, cloudDownload, info, close } from '@wordpress/icons';
+import { cloudUpload, cloudDownload, info, close, error } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { useState } from 'react';
import { ArrowIcon } from 'src/components/arrow-icon';
@@ -199,6 +199,7 @@ const SyncConnectedSitesSectionItem = ( {
isKeyFailed,
isKeyCancelled,
getPullStatusWithProgress,
+ isKeyUploadingPaused,
} = useSyncStatesProgressInfo();
const sitePullState = getPullState( selectedSite.id, connectedSite.id );
@@ -211,6 +212,7 @@ const SyncConnectedSitesSectionItem = ( {
const pushState = getPushState( selectedSite.id, connectedSite.id );
const isPushing = pushState && isKeyPushing( pushState.status.key );
+ const isUploadingPaused = pushState && isKeyUploadingPaused( pushState.status.key );
const isPushError = pushState && isKeyFailed( pushState.status.key );
const hasPushFinished = pushState && isKeyFinished( pushState.status.key );
const hasPushCancelled = pushState && isKeyCancelled( pushState.status.key );
@@ -292,6 +294,19 @@ const SyncConnectedSitesSectionItem = ( {
{ __( 'Pull complete' ) }
) }
+ { pushState?.status && isUploadingPaused && (
+