strapi-content-sync-pro 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -21
- package/admin/src/components/ConfigTab.jsx +4 -2
- package/admin/src/components/HelpTab.jsx +43 -21
- package/docs/Screenshot 2026-04-20 160506.png +0 -0
- package/docs/Screenshot 2026-04-20 160558.png +0 -0
- package/docs/Screenshot 2026-04-20 175903.png +0 -0
- package/docs/Screenshot 2026-04-20 175931.png +0 -0
- package/docs/Screenshot 2026-04-20 180001.png +0 -0
- package/docs/Screenshot 2026-04-20 180041.png +0 -0
- package/docs/Screenshot 2026-04-20 180116.png +0 -0
- package/docs/Screenshot 2026-04-20 180135.png +0 -0
- package/docs/Screenshot 2026-04-20 180202.png +0 -0
- package/docs/Screenshot 2026-04-20 180228.png +0 -0
- package/docs/Screenshot 2026-04-20 180251.png +0 -0
- package/docs/Screenshot 2026-04-20 180301.png +0 -0
- package/docs/logo-horizontal.svg +33 -0
- package/docs/logo-mark.svg +38 -0
- package/docs/logo-square.svg +27 -0
- package/package.json +2 -1
- package/server/src/services/sync.js +141 -0
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Content Sync Pro Plugin for Strapi
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/logo-horizontal.svg" alt="Content Sync Pro" width="720" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
3
7
|
A powerful Strapi v5 plugin to copy, migrate, and live-sync content, media, and data between multiple Strapi environments.
|
|
4
8
|
|
|
5
9
|
[](https://www.npmjs.com/package/strapi-content-sync-pro)
|
|
@@ -10,7 +14,7 @@ A powerful Strapi v5 plugin to copy, migrate, and live-sync content, media, and
|
|
|
10
14
|
Plugin intro: https://youtu.be/hr3dD6dLgLQ
|
|
11
15
|
|
|
12
16
|
<a href="https://youtu.be/hr3dD6dLgLQ" target="_blank" rel="noopener noreferrer">
|
|
13
|
-
<img src="docs/Screenshot%202026-04-20%20160506.png" alt="Content Sync Pro
|
|
17
|
+
<img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20160506.png" alt="Content Sync Pro � watch the intro video" width="100%" />
|
|
14
18
|
</a>
|
|
15
19
|
|
|
16
20
|
## Screenshots
|
|
@@ -22,24 +26,24 @@ Plugin intro: https://youtu.be/hr3dD6dLgLQ
|
|
|
22
26
|
|
|
23
27
|
<table>
|
|
24
28
|
<tr>
|
|
25
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20160506.png" alt="Content Sync Pro - screenshot 1" width="100%" /></td>
|
|
26
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20160558.png" alt="Content Sync Pro - screenshot 2" width="100%" /></td>
|
|
27
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20175903.png" alt="Content Sync Pro - screenshot 3" width="100%" /></td>
|
|
29
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20160506.png" alt="Content Sync Pro - screenshot 1" width="100%" /></td>
|
|
30
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20160558.png" alt="Content Sync Pro - screenshot 2" width="100%" /></td>
|
|
31
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20175903.png" alt="Content Sync Pro - screenshot 3" width="100%" /></td>
|
|
28
32
|
</tr>
|
|
29
33
|
<tr>
|
|
30
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20175931.png" alt="Content Sync Pro - screenshot 4" width="100%" /></td>
|
|
31
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20180001.png" alt="Content Sync Pro - screenshot 5" width="100%" /></td>
|
|
32
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20180041.png" alt="Content Sync Pro - screenshot 6" width="100%" /></td>
|
|
34
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20175931.png" alt="Content Sync Pro - screenshot 4" width="100%" /></td>
|
|
35
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20180001.png" alt="Content Sync Pro - screenshot 5" width="100%" /></td>
|
|
36
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20180041.png" alt="Content Sync Pro - screenshot 6" width="100%" /></td>
|
|
33
37
|
</tr>
|
|
34
38
|
<tr>
|
|
35
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20180116.png" alt="Content Sync Pro - screenshot 7" width="100%" /></td>
|
|
36
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20180135.png" alt="Content Sync Pro - screenshot 8" width="100%" /></td>
|
|
37
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20180202.png" alt="Content Sync Pro - screenshot 9" width="100%" /></td>
|
|
39
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20180116.png" alt="Content Sync Pro - screenshot 7" width="100%" /></td>
|
|
40
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20180135.png" alt="Content Sync Pro - screenshot 8" width="100%" /></td>
|
|
41
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20180202.png" alt="Content Sync Pro - screenshot 9" width="100%" /></td>
|
|
38
42
|
</tr>
|
|
39
43
|
<tr>
|
|
40
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20180228.png" alt="Content Sync Pro - screenshot 10" width="100%" /></td>
|
|
41
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20180251.png" alt="Content Sync Pro - screenshot 11" width="100%" /></td>
|
|
42
|
-
<td width="33%"><img src="docs/Screenshot%202026-04-20%20180301.png" alt="Content Sync Pro - screenshot 12" width="100%" /></td>
|
|
44
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20180228.png" alt="Content Sync Pro - screenshot 10" width="100%" /></td>
|
|
45
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20180251.png" alt="Content Sync Pro - screenshot 11" width="100%" /></td>
|
|
46
|
+
<td width="33%"><img src="https://raw.githubusercontent.com/eharain/strapi-content-sync-pro/master/docs/Screenshot%202026-04-20%20180301.png" alt="Content Sync Pro - screenshot 12" width="100%" /></td>
|
|
43
47
|
</tr>
|
|
44
48
|
</table>
|
|
45
49
|
</details>
|
|
@@ -56,7 +60,7 @@ Plugin intro: https://youtu.be/hr3dD6dLgLQ
|
|
|
56
60
|
- **Alerts & Logging** - Detailed sync logs. Receive success/failure alerts via Email (using Strapi's email provider) or Webhooks.
|
|
57
61
|
- **Secure Communication** - API token authentication combined with HMAC-SHA256 request signing using a shared secret.
|
|
58
62
|
|
|
59
|
-
##
|
|
63
|
+
## Prerequisites
|
|
60
64
|
|
|
61
65
|
- Strapi v5.0.0 or higher
|
|
62
66
|
- Node.js 20.0.0 or higher
|
|
@@ -100,7 +104,7 @@ npm run develop
|
|
|
100
104
|
2. Go to **Configuration** tab
|
|
101
105
|
3. Enter your remote server details:
|
|
102
106
|
- **Base URL**: The remote Strapi instance URL (e.g., `https://api.example.com`)
|
|
103
|
-
- **API Token**: Generate from remote Strapi's Settings
|
|
107
|
+
- **API Token**: Generate from remote Strapi's Settings ? API Tokens
|
|
104
108
|
- **Instance ID**: Unique identifier for this instance
|
|
105
109
|
- **Shared Secret**: Same secret on both instances for HMAC signing
|
|
106
110
|
|
|
@@ -154,14 +158,14 @@ Configure **when** sync runs in the Sync tab:
|
|
|
154
158
|
|
|
155
159
|
Full media synchronization between Strapi instances:
|
|
156
160
|
|
|
157
|
-
- **URL Strategy** (HTTP)
|
|
158
|
-
- **rsync Strategy**
|
|
159
|
-
- **Profile-based**
|
|
160
|
-
- **DB + File Sync**
|
|
161
|
+
- **URL Strategy** (HTTP) � Works with any upload provider (local, S3, Cloudinary). Downloads and re-uploads via the Upload API.
|
|
162
|
+
- **rsync Strategy** � Host-level file copy using the `rsync` binary. Fastest for local-provider setups with SSH access.
|
|
163
|
+
- **Profile-based** � Create media sync profiles with direction, conflict strategy, MIME filters, filename patterns, and execution settings.
|
|
164
|
+
- **DB + File Sync** � Syncs both the `plugin::upload.file` database rows and the actual file bytes.
|
|
161
165
|
|
|
162
166
|
## Enforcement
|
|
163
167
|
|
|
164
|
-
Pre-sync validation (Configuration
|
|
168
|
+
Pre-sync validation (Configuration ? Enforcement):
|
|
165
169
|
|
|
166
170
|
- **Schema Match** - Verify content type schemas match (strict/compatible/none)
|
|
167
171
|
- **Version Check** - Verify Strapi versions (exact/minor/major/none)
|
|
@@ -169,7 +173,7 @@ Pre-sync validation (Configuration → Enforcement):
|
|
|
169
173
|
|
|
170
174
|
## Alerts
|
|
171
175
|
|
|
172
|
-
Get notified of sync events (Configuration
|
|
176
|
+
Get notified of sync events (Configuration ? Alerts):
|
|
173
177
|
|
|
174
178
|
- **Strapi Logs** - Logs to sync log and server console
|
|
175
179
|
- **Email** - Requires Strapi email plugin configured
|
|
@@ -230,6 +234,14 @@ Check the **Logs** tab for detailed sync history including:
|
|
|
230
234
|
- Direction (push/pull)
|
|
231
235
|
- Status and error messages
|
|
232
236
|
|
|
237
|
+
## Security & Privacy
|
|
238
|
+
|
|
239
|
+
- **No usage tracking.** This plugin does not collect, transmit, or store any analytics or telemetry data.
|
|
240
|
+
- **Credential handling.** The optional "Generate Token" feature lets you authenticate to **your own** remote Strapi server to create an API token. Credentials are sent directly from your browser to your server via the plugin's backend proxy, used once, and **never stored** on disk, in the database, or in memory after the request completes.
|
|
241
|
+
- **API Tokens** are encrypted at rest using Strapi's built-in store.
|
|
242
|
+
- **HMAC-SHA256** signatures protect all inter-instance requests from tampering.
|
|
243
|
+
- **Masked secrets** � API tokens and shared secrets are masked (`��������`) in all API responses.
|
|
244
|
+
|
|
233
245
|
## Contributing
|
|
234
246
|
|
|
235
247
|
Contributions are welcome! Please open an issue or submit a pull request.
|
|
@@ -476,9 +476,11 @@ const ConfigTab = () => {
|
|
|
476
476
|
<Modal.Title>Generate API Token</Modal.Title>
|
|
477
477
|
</Modal.Header>
|
|
478
478
|
<Modal.Body>
|
|
479
|
-
<Typography variant="omega" textColor="neutral600" paddingBottom={
|
|
479
|
+
<Typography variant="omega" textColor="neutral600" paddingBottom={2}>
|
|
480
480
|
Log in to <strong>{config.baseUrl}</strong> to automatically create an API token.
|
|
481
|
-
|
|
481
|
+
</Typography>
|
|
482
|
+
<Typography variant="pi" textColor="neutral500" paddingBottom={4}>
|
|
483
|
+
Your credentials are sent directly to your remote server, used once to create a token, and never stored on disk, in the database, or in memory.
|
|
482
484
|
</Typography>
|
|
483
485
|
|
|
484
486
|
<Flex direction="column" gap={4}>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box, Typography, Tabs, Divider } from '@strapi/design-system';
|
|
2
2
|
|
|
3
|
-
const INTRO_VIDEO_URL = 'https://
|
|
4
|
-
const
|
|
3
|
+
const INTRO_VIDEO_URL = 'https://www.youtube.com/watch?v=hr3dD6dLgLQ';
|
|
4
|
+
const INTRO_VIDEO_THUMBNAIL = 'https://img.youtube.com/vi/hr3dD6dLgLQ/hqdefault.jpg';
|
|
5
5
|
|
|
6
6
|
const HelpSection = ({ title, children }) => (
|
|
7
7
|
<Box paddingBottom={6}>
|
|
@@ -60,9 +60,7 @@ export const HelpTab = () => {
|
|
|
60
60
|
<Box paddingTop={4}>
|
|
61
61
|
<HelpSection title="Video walkthrough">
|
|
62
62
|
<Typography variant="omega" paddingBottom={3}>
|
|
63
|
-
Watch the
|
|
64
|
-
{' '}
|
|
65
|
-
<DocLink href={INTRO_VIDEO_URL}>YouTube</DocLink>
|
|
63
|
+
Watch the help video to get started:
|
|
66
64
|
</Typography>
|
|
67
65
|
|
|
68
66
|
<Box
|
|
@@ -71,22 +69,46 @@ export const HelpTab = () => {
|
|
|
71
69
|
padding={3}
|
|
72
70
|
style={{ maxWidth: '960px' }}
|
|
73
71
|
>
|
|
74
|
-
<
|
|
75
|
-
<Box
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
72
|
+
<a href={INTRO_VIDEO_URL} target="_blank" rel="noopener noreferrer" style={{ display: 'block', textDecoration: 'none' }}>
|
|
73
|
+
<Box style={{ position: 'relative', paddingTop: '56.25%', overflow: 'hidden', borderRadius: '4px' }}>
|
|
74
|
+
<img
|
|
75
|
+
src={INTRO_VIDEO_THUMBNAIL}
|
|
76
|
+
alt="Content Sync Pro — plugin intro"
|
|
77
|
+
style={{
|
|
78
|
+
position: 'absolute',
|
|
79
|
+
top: 0,
|
|
80
|
+
left: 0,
|
|
81
|
+
width: '100%',
|
|
82
|
+
height: '100%',
|
|
83
|
+
objectFit: 'cover',
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
<Box
|
|
87
|
+
style={{
|
|
88
|
+
position: 'absolute',
|
|
89
|
+
top: '50%',
|
|
90
|
+
left: '50%',
|
|
91
|
+
transform: 'translate(-50%, -50%)',
|
|
92
|
+
width: '68px',
|
|
93
|
+
height: '48px',
|
|
94
|
+
backgroundColor: 'rgba(255,0,0,0.85)',
|
|
95
|
+
borderRadius: '12px',
|
|
96
|
+
display: 'flex',
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
justifyContent: 'center',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
|
|
102
|
+
<polygon points="8,5 20,12 8,19" />
|
|
103
|
+
</svg>
|
|
104
|
+
</Box>
|
|
105
|
+
</Box>
|
|
106
|
+
</a>
|
|
107
|
+
<Box paddingTop={2}>
|
|
108
|
+
<Typography variant="pi" textColor="neutral500">
|
|
109
|
+
▶ Watch the help video on{' '}
|
|
110
|
+
<DocLink href={INTRO_VIDEO_URL}>YouTube</DocLink>
|
|
111
|
+
</Typography>
|
|
90
112
|
</Box>
|
|
91
113
|
</Box>
|
|
92
114
|
</HelpSection>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="320" viewBox="0 0 1200 320" role="img" aria-label="Content Sync Pro logo">
|
|
3
|
+
<defs>
|
|
4
|
+
<linearGradient id="csp-g" x1="40" y1="40" x2="240" y2="240" gradientUnits="userSpaceOnUse">
|
|
5
|
+
<stop offset="0" stop-color="#6C63FF"/>
|
|
6
|
+
<stop offset="1" stop-color="#4945FF"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
|
|
10
|
+
<!-- mark -->
|
|
11
|
+
<g transform="translate(24,24)">
|
|
12
|
+
<rect x="0" y="0" width="272" height="272" rx="64" fill="url(#csp-g)"/>
|
|
13
|
+
<circle cx="92" cy="120" r="22" fill="#FFFFFF" opacity="0.92"/>
|
|
14
|
+
<circle cx="180" cy="120" r="22" fill="#FFFFFF" opacity="0.92"/>
|
|
15
|
+
<g fill="none" stroke="#FFFFFF" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" opacity="0.95">
|
|
16
|
+
<path d="M110 104 H172"/>
|
|
17
|
+
<path d="M172 104 L160 92"/>
|
|
18
|
+
<path d="M172 104 L160 116"/>
|
|
19
|
+
<path d="M164 136 H102"/>
|
|
20
|
+
<path d="M102 136 L114 124"/>
|
|
21
|
+
<path d="M102 136 L114 148"/>
|
|
22
|
+
</g>
|
|
23
|
+
<path d="M136 158 C156 158 172 150 182 143 V176 C182 202 164 222 136 232 C108 222 90 202 90 176 V143 C100 150 116 158 136 158 Z" fill="#FFFFFF" opacity="0.92"/>
|
|
24
|
+
<path d="M118 186 L130 198 L156 172" fill="none" stroke="#4945FF" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
25
|
+
</g>
|
|
26
|
+
|
|
27
|
+
<!-- wordmark -->
|
|
28
|
+
<g transform="translate(340,88)">
|
|
29
|
+
<text x="0" y="0" font-size="54" font-weight="700" font-family="Inter, Segoe UI, Arial, sans-serif" fill="#111827">Content Sync</text>
|
|
30
|
+
<text x="0" y="70" font-size="54" font-weight="700" font-family="Inter, Segoe UI, Arial, sans-serif" fill="#4945FF">Pro</text>
|
|
31
|
+
<text x="140" y="70" font-size="22" font-weight="500" font-family="Inter, Segoe UI, Arial, sans-serif" fill="#6B7280">Strapi v5 plugin</text>
|
|
32
|
+
</g>
|
|
33
|
+
</svg>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" role="img" aria-label="Content Sync Pro logo mark">
|
|
3
|
+
<defs>
|
|
4
|
+
<linearGradient id="csp-g" x1="96" y1="96" x2="416" y2="416" gradientUnits="userSpaceOnUse">
|
|
5
|
+
<stop offset="0" stop-color="#6C63FF"/>
|
|
6
|
+
<stop offset="1" stop-color="#4945FF"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<filter id="csp-shadow" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
|
|
9
|
+
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-color="#000" flood-opacity="0.18"/>
|
|
10
|
+
</filter>
|
|
11
|
+
</defs>
|
|
12
|
+
|
|
13
|
+
<!-- background badge -->
|
|
14
|
+
<rect x="56" y="56" width="400" height="400" rx="92" fill="url(#csp-g)" filter="url(#csp-shadow)"/>
|
|
15
|
+
|
|
16
|
+
<!-- sync nodes -->
|
|
17
|
+
<circle cx="182" cy="236" r="34" fill="#FFFFFF" opacity="0.92"/>
|
|
18
|
+
<circle cx="330" cy="236" r="34" fill="#FFFFFF" opacity="0.92"/>
|
|
19
|
+
|
|
20
|
+
<!-- bi-directional arrows -->
|
|
21
|
+
<g fill="none" stroke="#FFFFFF" stroke-width="18" stroke-linecap="round" stroke-linejoin="round" opacity="0.95">
|
|
22
|
+
<!-- top arrow: left -> right -->
|
|
23
|
+
<path d="M208 210 H320"/>
|
|
24
|
+
<path d="M320 210 L302 192"/>
|
|
25
|
+
<path d="M320 210 L302 228"/>
|
|
26
|
+
|
|
27
|
+
<!-- bottom arrow: right -> left -->
|
|
28
|
+
<path d="M304 262 H192"/>
|
|
29
|
+
<path d="M192 262 L210 244"/>
|
|
30
|
+
<path d="M192 262 L210 280"/>
|
|
31
|
+
</g>
|
|
32
|
+
|
|
33
|
+
<!-- safety shield + check ("pro" / enforcement) -->
|
|
34
|
+
<g transform="translate(0,2)">
|
|
35
|
+
<path d="M256 310 C290 310 316 296 332 284 V338 C332 380 302 412 256 430 C210 412 180 380 180 338 V284 C196 296 222 310 256 310 Z" fill="#FFFFFF" opacity="0.92"/>
|
|
36
|
+
<path d="M226 356 L246 376 L292 330" fill="none" stroke="#4945FF" stroke-width="18" stroke-linecap="round" stroke-linejoin="round"/>
|
|
37
|
+
</g>
|
|
38
|
+
</svg>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-label="Content Sync Pro square logo">
|
|
3
|
+
<defs>
|
|
4
|
+
<linearGradient id="csp-g" x1="160" y1="160" x2="864" y2="864" gradientUnits="userSpaceOnUse">
|
|
5
|
+
<stop offset="0" stop-color="#6C63FF"/>
|
|
6
|
+
<stop offset="1" stop-color="#4945FF"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
|
|
10
|
+
<rect x="112" y="112" width="800" height="800" rx="180" fill="url(#csp-g)"/>
|
|
11
|
+
|
|
12
|
+
<circle cx="364" cy="470" r="68" fill="#FFFFFF" opacity="0.92"/>
|
|
13
|
+
<circle cx="660" cy="470" r="68" fill="#FFFFFF" opacity="0.92"/>
|
|
14
|
+
|
|
15
|
+
<g fill="none" stroke="#FFFFFF" stroke-width="36" stroke-linecap="round" stroke-linejoin="round" opacity="0.95">
|
|
16
|
+
<path d="M416 418 H640"/>
|
|
17
|
+
<path d="M640 418 L604 382"/>
|
|
18
|
+
<path d="M640 418 L604 454"/>
|
|
19
|
+
|
|
20
|
+
<path d="M608 522 H384"/>
|
|
21
|
+
<path d="M384 522 L420 486"/>
|
|
22
|
+
<path d="M384 522 L420 558"/>
|
|
23
|
+
</g>
|
|
24
|
+
|
|
25
|
+
<path d="M512 620 C580 620 632 592 664 568 V676 C664 760 604 824 512 860 C420 824 360 760 360 676 V568 C392 592 444 620 512 620 Z" fill="#FFFFFF" opacity="0.92"/>
|
|
26
|
+
<path d="M452 712 L492 752 L588 656" fill="none" stroke="#4945FF" stroke-width="36" stroke-linecap="round" stroke-linejoin="round"/>
|
|
27
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-content-sync-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Strapi v5 plugin to copy, migrate, and live-sync content, media, and data between multiple Strapi environments with bi-directional sync, field-level policies, scheduling, and alerts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"files": [
|
|
60
60
|
"admin/",
|
|
61
61
|
"server/",
|
|
62
|
+
"docs/",
|
|
62
63
|
"README.md",
|
|
63
64
|
"LICENSE"
|
|
64
65
|
],
|
|
@@ -160,6 +160,147 @@ module.exports = ({ strapi }) => {
|
|
|
160
160
|
return { syncedAt: new Date().toISOString(), results };
|
|
161
161
|
},
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Sync a single content type using a given profile.
|
|
165
|
+
* Called by the execution service (on-demand / scheduled / live runs).
|
|
166
|
+
*
|
|
167
|
+
* options:
|
|
168
|
+
* - profile: sync profile { contentType, direction, conflictStrategy, isSimple, fieldPolicies }
|
|
169
|
+
* - syncDependencies: boolean (currently informational; dependency resolution handled upstream)
|
|
170
|
+
* - dependencyDepth: number
|
|
171
|
+
*/
|
|
172
|
+
async syncContentType(uid, options = {}) {
|
|
173
|
+
if (!uid) {
|
|
174
|
+
throw new Error('Content type uid is required');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const logService = plugin().service('syncLog');
|
|
178
|
+
const configService = plugin().service('config');
|
|
179
|
+
const syncConfigService = plugin().service('syncConfig');
|
|
180
|
+
const syncProfilesService = plugin().service('syncProfiles');
|
|
181
|
+
const executionService = plugin().service('syncExecution');
|
|
182
|
+
|
|
183
|
+
const remoteConfig = await configService.getConfig({ safe: false });
|
|
184
|
+
if (!remoteConfig || !remoteConfig.baseUrl) {
|
|
185
|
+
throw new Error('Remote server not configured');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { profile } = options;
|
|
189
|
+
const syncConfig = await syncConfigService.getSyncConfig();
|
|
190
|
+
const ctConfig = (syncConfig.contentTypes || []).find((ct) => ct.uid === uid) || { uid, fields: [] };
|
|
191
|
+
|
|
192
|
+
const direction = profile?.direction || ctConfig.direction || 'both';
|
|
193
|
+
const conflictStrategy = profile?.conflictStrategy || syncConfig.conflictStrategy || 'latest';
|
|
194
|
+
const fields = ctConfig.fields || [];
|
|
195
|
+
|
|
196
|
+
// Field-level policies: prefer the policies on the provided profile,
|
|
197
|
+
// otherwise fall back to the active profile for the content type.
|
|
198
|
+
let fieldPolicies = null;
|
|
199
|
+
if (profile) {
|
|
200
|
+
if (!profile.isSimple && Array.isArray(profile.fieldPolicies) && profile.fieldPolicies.length > 0) {
|
|
201
|
+
fieldPolicies = {};
|
|
202
|
+
for (const fp of profile.fieldPolicies) {
|
|
203
|
+
fieldPolicies[fp.field] = fp.direction;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
fieldPolicies = await syncProfilesService.getFieldPoliciesForContentType(uid);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const globalExec = (await executionService.getGlobalSettings?.()) || {};
|
|
211
|
+
const pageSize = Number(globalExec.syncPageSize) || 100;
|
|
212
|
+
|
|
213
|
+
const timestamps = await getLastSyncTimestamps();
|
|
214
|
+
const lastSyncAt = timestamps[uid] || null;
|
|
215
|
+
const syncStartTime = new Date().toISOString();
|
|
216
|
+
|
|
217
|
+
let pushed = 0;
|
|
218
|
+
let pulled = 0;
|
|
219
|
+
let errors = 0;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const localRecords = await fetchLocalRecords(strapi, uid, { fields, lastSyncAt, pageSize });
|
|
223
|
+
const remoteRecords = await fetchRemoteRecords(remoteConfig, uid, { fields, lastSyncAt, pageSize });
|
|
224
|
+
|
|
225
|
+
const diff = compareRecords(localRecords, remoteRecords, { direction, conflictStrategy });
|
|
226
|
+
|
|
227
|
+
for (const { local } of diff.toPush) {
|
|
228
|
+
try {
|
|
229
|
+
const filteredRecord = syncProfilesService.filterFieldsByPolicy(local, fieldPolicies, 'push');
|
|
230
|
+
await applyRemote(remoteConfig, uid, filteredRecord, fields);
|
|
231
|
+
pushed++;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
errors++;
|
|
234
|
+
await logService.log({ action: 'push', contentType: uid, syncId: local.syncId, direction: 'push', status: 'error', message: err.message });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const { remote } of diff.toPull) {
|
|
239
|
+
try {
|
|
240
|
+
const filteredRecord = syncProfilesService.filterFieldsByPolicy(remote, fieldPolicies, 'pull');
|
|
241
|
+
await applyLocal(strapi, uid, filteredRecord, fields);
|
|
242
|
+
pulled++;
|
|
243
|
+
} catch (err) {
|
|
244
|
+
errors++;
|
|
245
|
+
await logService.log({ action: 'pull', contentType: uid, syncId: remote.syncId, direction: 'pull', status: 'error', message: err.message });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const record of diff.toCreateRemote) {
|
|
250
|
+
try {
|
|
251
|
+
const filteredRecord = syncProfilesService.filterFieldsByPolicy(record, fieldPolicies, 'push');
|
|
252
|
+
await applyRemote(remoteConfig, uid, filteredRecord, fields);
|
|
253
|
+
pushed++;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
errors++;
|
|
256
|
+
await logService.log({ action: 'create_remote', contentType: uid, syncId: record.syncId, direction: 'push', status: 'error', message: err.message });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const record of diff.toCreateLocal) {
|
|
261
|
+
try {
|
|
262
|
+
const filteredRecord = syncProfilesService.filterFieldsByPolicy(record, fieldPolicies, 'pull');
|
|
263
|
+
await applyLocal(strapi, uid, filteredRecord, fields);
|
|
264
|
+
pulled++;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
errors++;
|
|
267
|
+
await logService.log({ action: 'create_local', contentType: uid, syncId: record.syncId, direction: 'pull', status: 'error', message: err.message });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await setLastSyncTimestamp(uid, syncStartTime);
|
|
272
|
+
|
|
273
|
+
const summary = {
|
|
274
|
+
uid,
|
|
275
|
+
pushed,
|
|
276
|
+
pulled,
|
|
277
|
+
errors,
|
|
278
|
+
hasFieldPolicies: !!fieldPolicies,
|
|
279
|
+
profile: profile ? { id: profile.id, name: profile.name } : null,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
await logService.log({
|
|
283
|
+
action: 'sync_complete',
|
|
284
|
+
contentType: uid,
|
|
285
|
+
direction,
|
|
286
|
+
status: errors > 0 ? 'partial' : 'success',
|
|
287
|
+
message: `Pushed: ${pushed}, Pulled: ${pulled}, Errors: ${errors}${fieldPolicies ? ' (with field policies)' : ''}`,
|
|
288
|
+
details: summary,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return { syncedAt: new Date().toISOString(), ...summary };
|
|
292
|
+
} catch (err) {
|
|
293
|
+
await logService.log({
|
|
294
|
+
action: 'sync_error',
|
|
295
|
+
contentType: uid,
|
|
296
|
+
direction,
|
|
297
|
+
status: 'error',
|
|
298
|
+
message: err.message,
|
|
299
|
+
});
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
|
|
163
304
|
/**
|
|
164
305
|
* Step 8 — Push a single record to the remote (called by lifecycle hooks).
|
|
165
306
|
* Now supports field-level policies.
|