feat: WordPress migration support — upload existing site files

- Add two deployment modes for WordPress: Fresh Install vs Migrate Existing Site
- Fresh Install: vanilla WordPress from official image (existing behavior)
- Migrate: upload ZIP with wp-content/ (themes, plugins, uploads), wp-config.php, .htaccess
- Custom entrypoint merges staged wp-content into PVC on first container run
- Add init container for fresh WordPress builds (empty source context for Kaniko)
- Increase upload limit to 200MB for WordPress sites
- Add PHP upload limits (64MB) and memory config in WordPress Dockerfile
- Update deploy page review step to show WordPress mode info
- All UI in English
This commit is contained in:
keyhan
2026-04-07 23:12:25 +03:30
parent c2c6a32ae8
commit 5979b48a61
4 changed files with 220 additions and 36 deletions
+167 -20
View File
@@ -48,6 +48,10 @@ export default function DeployPage() {
const [dbDumpFile, setDbDumpFile] = useState<File | null>(null);
const dbDumpInputRef = useRef<HTMLInputElement>(null);
const [dbUploadProgress, setDbUploadProgress] = useState(0);
const [wpMode, setWpMode] = useState<'fresh' | 'migrate'>('fresh');
const [wpContentFile, setWpContentFile] = useState<File | null>(null);
const [isWpDragging, setIsWpDragging] = useState(false);
const wpFileInputRef = useRef<HTMLInputElement>(null);
const [selectedCycle, setSelectedCycle] = useState<BillingCycle>('monthly');
const [paymentMethod, setPaymentMethod] = useState<'wallet' | 'gateway'>('wallet');
const [isPaid, setIsPaid] = useState(false);
@@ -99,10 +103,11 @@ export default function DeployPage() {
const res = await api.post('/applications', payload);
const appId = res.data.id;
// Upload source
if (sourceMethod === 'upload' && zipFile) {
// Upload source (regular apps or WordPress migrate)
const fileToUpload = form.runtime === 'wordpress' ? (wpMode === 'migrate' ? wpContentFile : null) : (sourceMethod === 'upload' ? zipFile : null);
if (fileToUpload) {
const formData = new FormData();
formData.append('file', zipFile);
formData.append('file', fileToUpload);
await api.post(`/applications/${appId}/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => { if (e.total) setUploadProgress(Math.round((e.loaded * 100) / e.total)); },
@@ -159,10 +164,11 @@ export default function DeployPage() {
const res = await api.post('/applications', payload);
const appId = res.data.id;
// Upload source
if (sourceMethod === 'upload' && zipFile) {
// Upload source (regular apps or WordPress migrate)
const fileToUpload = form.runtime === 'wordpress' ? (wpMode === 'migrate' ? wpContentFile : null) : (sourceMethod === 'upload' ? zipFile : null);
if (fileToUpload) {
const formData = new FormData();
formData.append('file', zipFile);
formData.append('file', fileToUpload);
await api.post(`/applications/${appId}/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => { if (e.total) setUploadProgress(Math.round((e.loaded * 100) / e.total)); },
@@ -201,9 +207,11 @@ export default function DeployPage() {
const appId = res.data.id;
// Upload zip file if selected
if (sourceMethod === 'upload' && zipFile) {
// Upload source (regular apps or WordPress migrate)
const fileToUpload = data.runtime === 'wordpress' ? (wpMode === 'migrate' ? wpContentFile : null) : (sourceMethod === 'upload' ? zipFile : null);
if (fileToUpload) {
const formData = new FormData();
formData.append('file', zipFile);
formData.append('file', fileToUpload);
await api.post(`/applications/${appId}/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => {
@@ -294,11 +302,35 @@ export default function DeployPage() {
setIsDragging(false);
}, []);
const handleWpFileSelect = useCallback((file: File) => {
const validExtensions = ['.zip', '.tar.gz', '.tgz'];
const hasValidExt = validExtensions.some((ext) => file.name.toLowerCase().endsWith(ext));
if (!hasValidExt) {
toast.error('Only .zip or .tar.gz files are allowed');
return;
}
if (file.size > 200 * 1024 * 1024) {
toast.error('WordPress files must be less than 200MB');
return;
}
setWpContentFile(file);
toast.success(`WordPress files selected: ${file.name}`);
}, []);
const handleWpDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
setIsWpDragging(false);
const file = e.dataTransfer.files[0];
if (file) handleWpFileSelect(file);
}, [handleWpFileSelect]);
const canNext = () => {
if (step === 0) {
if (form.name.length < 2) return false;
// WordPress doesn't require source code
if (form.runtime !== 'wordpress') {
// WordPress: migrate mode requires wp-content file
if (form.runtime === 'wordpress') {
if (wpMode === 'migrate' && !wpContentFile) return false;
} else {
if (sourceMethod === 'upload' && !zipFile) return false;
if (sourceMethod === 'git' && !form.gitUrl) return false;
}
@@ -387,6 +419,8 @@ export default function DeployPage() {
else if (opt.value === 'laravel') { updates.port = 8000; updates.runtimeVersion = ''; updates.phpVersion = '8.3'; }
else if (opt.value === 'wordpress') { updates.port = 80; updates.databaseType = 'mysql'; updates.runtimeVersion = '6.7'; updates.phpVersion = '8.3'; }
setForm({ ...form, ...updates });
// Reset WordPress-specific state when switching types
if (opt.value !== 'wordpress') { setWpMode('fresh'); setWpContentFile(null); }
}}
className={`p-4 rounded-xl border-2 text-left transition-colors ${
form.runtime === opt.value ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'
@@ -534,17 +568,128 @@ export default function DeployPage() {
</div>
)}
{/* WordPress info — shown when WordPress is selected */}
{/* WordPress deployment mode — shown when WordPress is selected */}
{form.runtime === 'wordpress' && (
<div className="p-4 bg-blue-50/50 border border-blue-200 rounded-xl">
<div className="flex items-center gap-2 mb-2">
<Hexagon className="w-5 h-5 text-blue-600" />
<h3 className="text-sm font-semibold text-gray-800">WordPress (Official Image)</h3>
<div className="space-y-4">
<label className="block text-sm font-medium text-gray-700 mb-2">Deployment Mode</label>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => { setWpMode('fresh'); setWpContentFile(null); }}
className={`p-4 rounded-xl border-2 text-left transition-colors ${
wpMode === 'fresh' ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'
}`}
>
<Rocket className="w-5 h-5 text-blue-600" />
<p className="mt-2 font-semibold text-sm text-gray-900">Fresh Install</p>
<p className="text-xs text-gray-500 mt-1">
Start with a clean WordPress. Install themes & plugins via the admin panel.
</p>
</button>
<button
type="button"
onClick={() => setWpMode('migrate')}
className={`p-4 rounded-xl border-2 text-left transition-colors ${
wpMode === 'migrate' ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'
}`}
>
<Upload className="w-5 h-5 text-emerald-600" />
<p className="mt-2 font-semibold text-sm text-gray-900">Migrate Existing Site</p>
<p className="text-xs text-gray-500 mt-1">
Upload your WordPress files (wp-content, themes, plugins) and optionally a DB dump.
</p>
</button>
</div>
<p className="text-xs text-gray-500">
وردپرس از ایمیج رسمی Docker استفاده میکند و نیازی به آپلود سورسکد ندارد.
قالبها و افزونهها از طریق پنل مدیریت وردپرس نصب میشوند.
</p>
{wpMode === 'fresh' && (
<div className="p-4 bg-blue-50/50 border border-blue-200 rounded-xl">
<div className="flex items-center gap-2 mb-2">
<Hexagon className="w-5 h-5 text-blue-600" />
<h3 className="text-sm font-semibold text-gray-800">WordPress (Official Image)</h3>
</div>
<p className="text-xs text-gray-500">
A fresh WordPress installation will be deployed using the official Docker image.
You can install themes and plugins via the WordPress admin panel after deployment.
</p>
</div>
)}
{wpMode === 'migrate' && (
<div className="space-y-3">
<div className="p-3 bg-amber-50 border border-amber-200 rounded-xl">
<p className="text-xs text-gray-600">
<strong>Upload a ZIP</strong> containing your WordPress files. Supported structures:
</p>
<ul className="text-xs text-gray-500 mt-1 ml-4 list-disc space-y-0.5">
<li><code className="bg-amber-100 px-1 rounded">wp-content/</code> themes, plugins, uploads</li>
<li><code className="bg-amber-100 px-1 rounded">wp-config.php</code> custom configuration (optional)</li>
<li>Any custom <code className="bg-amber-100 px-1 rounded">.php</code> files at root level</li>
</ul>
<p className="text-xs text-gray-500 mt-1.5">
You can also upload a SQL database dump in the next step to restore your data.
</p>
</div>
{wpContentFile ? (
<div className="flex items-center justify-between p-4 bg-green-50 border border-green-200 rounded-xl">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center text-green-600">
<CheckCircle className="w-5 h-5" />
</div>
<div>
<p className="text-sm font-medium text-green-800">{wpContentFile.name}</p>
<p className="text-xs text-green-600">
{(wpContentFile.size / (1024 * 1024)).toFixed(2)} MB
</p>
</div>
</div>
<button
type="button"
onClick={() => {
setWpContentFile(null);
if (wpFileInputRef.current) wpFileInputRef.current.value = '';
}}
className="text-sm text-red-500 hover:text-red-700 font-medium"
>
Remove
</button>
</div>
) : (
<div
onDrop={handleWpDrop}
onDragOver={(e) => { e.preventDefault(); setIsWpDragging(true); }}
onDragLeave={(e) => { e.preventDefault(); setIsWpDragging(false); }}
onClick={() => wpFileInputRef.current?.click()}
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
isWpDragging
? 'border-primary-500 bg-primary-50'
: 'border-gray-300 hover:border-primary-400 hover:bg-gray-50'
}`}
>
<div className="space-y-2">
<FolderUp className="w-8 h-8 mx-auto text-gray-400" />
<p className="text-sm font-medium text-gray-700">
Drag & drop your WordPress ZIP here
</p>
<p className="text-xs text-gray-500">
ZIP with <strong>wp-content/</strong> folder Max 200MB
</p>
</div>
</div>
)}
<input
ref={wpFileInputRef}
type="file"
accept=".zip,.tar.gz,.tgz"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleWpFileSelect(file);
e.target.value = '';
}}
/>
</div>
)}
</div>
)}
</div>
@@ -1179,7 +1324,9 @@ export default function DeployPage() {
<span className="text-sm text-gray-500">Source</span>
<span className="text-sm font-medium">
{form.runtime === 'wordpress'
? 'WordPress (Official Image)'
? wpMode === 'migrate' && wpContentFile
? `Migrate: ${wpContentFile.name} (${(wpContentFile.size / (1024 * 1024)).toFixed(1)} MB)`
: 'WordPress (Fresh Install)'
: sourceMethod === 'upload'
? zipFile
? zipFile.name