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:
@@ -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
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user