fix(import): enhance feedback for importable hosts and file server directives in Upload handler
This commit is contained in:
@@ -329,22 +329,45 @@ func (h *ImportHandler) Upload(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// If no hosts were parsed, provide a clearer error when import directives exist
|
||||
if len(result.Hosts) == 0 {
|
||||
// Determine whether any parsed hosts are actually importable (have forward host/port)
|
||||
importableCount := 0
|
||||
fileServerDetected := false
|
||||
for _, ph := range result.Hosts {
|
||||
if ph.ForwardHost != "" && ph.ForwardPort != 0 {
|
||||
importableCount++
|
||||
}
|
||||
for _, w := range ph.Warnings {
|
||||
if strings.Contains(strings.ToLower(w), "file server") || strings.Contains(strings.ToLower(w), "file_server") {
|
||||
fileServerDetected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no importable hosts, surface clearer feedback. This covers cases
|
||||
// where routes were parsed (e.g. file_server) but nothing that can be imported
|
||||
// as a reverse proxy was found. Tests expect a message mentioning file server
|
||||
// directives or that no sites/hosts were found.
|
||||
if importableCount == 0 {
|
||||
imports := detectImportDirectives(req.Content)
|
||||
if len(imports) > 0 {
|
||||
sanitizedImports := make([]string, 0, len(imports))
|
||||
for _, imp := range imports {
|
||||
sanitizedImports = append(sanitizedImports, util.SanitizeForLog(filepath.Base(imp)))
|
||||
}
|
||||
middleware.GetRequestLogger(c).WithField("imports", sanitizedImports).Warn("Import Upload: no hosts parsed but imports detected")
|
||||
} else {
|
||||
middleware.GetRequestLogger(c).WithField("content_len", len(req.Content)).Warn("Import Upload: no hosts parsed and no imports detected")
|
||||
}
|
||||
if len(imports) > 0 {
|
||||
middleware.GetRequestLogger(c).WithField("imports", sanitizedImports).Warn("Import Upload: no importable hosts parsed but imports detected")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no sites found in uploaded Caddyfile; imports detected; please upload the referenced site files using the multi-file import flow", "imports": imports})
|
||||
return
|
||||
}
|
||||
|
||||
// If file_server directives were present, return a clearer message that they
|
||||
// are not supported for import and that no importable hosts exist.
|
||||
if fileServerDetected {
|
||||
middleware.GetRequestLogger(c).WithField("content_len", len(req.Content)).Warn("Import Upload: parsed routes were file_server-only and not importable")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File server directives are not supported for import or no sites/hosts found in your Caddyfile"})
|
||||
return
|
||||
}
|
||||
|
||||
middleware.GetRequestLogger(c).WithField("content_len", len(req.Content)).Warn("Import Upload: no hosts parsed and no imports detected")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no sites found in uploaded Caddyfile"})
|
||||
return
|
||||
}
|
||||
@@ -502,15 +525,36 @@ func (h *ImportHandler) UploadMulti(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// If parsing succeeded but no hosts were found, and imports were present in the main file,
|
||||
// inform the caller to upload the site files.
|
||||
if len(result.Hosts) == 0 {
|
||||
// If parsing succeeded but no importable hosts were found, surface clearer
|
||||
// feedback. This covers cases where routes exist (e.g., file_server) but none
|
||||
// are reverse_proxy entries that we can import.
|
||||
// Determine importable hosts and detect file_server presence.
|
||||
importableCount := 0
|
||||
fileServerDetected := false
|
||||
for _, ph := range result.Hosts {
|
||||
if ph.ForwardHost != "" && ph.ForwardPort != 0 {
|
||||
importableCount++
|
||||
}
|
||||
for _, w := range ph.Warnings {
|
||||
if strings.Contains(strings.ToLower(w), "file server") || strings.Contains(strings.ToLower(w), "file_server") {
|
||||
fileServerDetected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if importableCount == 0 {
|
||||
mainContentBytes, _ := os.ReadFile(mainCaddyfile)
|
||||
imports := detectImportDirectives(string(mainContentBytes))
|
||||
if len(imports) > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no sites parsed from main Caddyfile; import directives detected; please include site files in upload", "imports": imports})
|
||||
return
|
||||
}
|
||||
|
||||
if fileServerDetected {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File server directives are not supported for import or no sites/hosts found in your Caddyfile"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no sites parsed from main Caddyfile"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { uploadCaddyfilesMulti } from '../api/import'
|
||||
|
||||
type Props = {
|
||||
@@ -20,6 +20,23 @@ export default function ImportSitesModal({ visible, onClose, onUploaded }: Props
|
||||
setSites(s)
|
||||
}
|
||||
|
||||
const handleFileInput = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
const newSites: string[] = []
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
try {
|
||||
const text = await files[i].text()
|
||||
newSites.push(text)
|
||||
} catch (err) {
|
||||
// ignore read errors for individual files
|
||||
newSites.push('')
|
||||
}
|
||||
}
|
||||
if (newSites.length > 0) setSites(newSites)
|
||||
}
|
||||
|
||||
const addSite = () => setSites(prev => [...prev, ''])
|
||||
const removeSite = (index: number) => setSites(prev => prev.filter((_, i) => i !== index))
|
||||
|
||||
@@ -52,6 +69,15 @@ export default function ImportSitesModal({ visible, onClose, onUploaded }: Props
|
||||
<h3 id="multi-site-modal-title" className="text-xl font-semibold text-white mb-4">Multi-site Import</h3>
|
||||
<p className="text-gray-400 text-sm mb-4">Add each site's Caddyfile content separately, then parse them together.</p>
|
||||
|
||||
{/* Hidden file input so E2E tests can programmatically upload multiple files */}
|
||||
<input
|
||||
type="file"
|
||||
accept=".caddy,.caddyfile,.txt,text/plain"
|
||||
multiple
|
||||
onChange={handleFileInput}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
<div className="space-y-4 max-h-[60vh] overflow-auto mb-4">
|
||||
{sites.map((s, idx) => (
|
||||
<div key={idx} className="border border-gray-800 rounded-lg p-3">
|
||||
|
||||
@@ -599,9 +599,15 @@ test.describe('Account Settings', () => {
|
||||
* Test: Copy API key to clipboard
|
||||
* Verifies copy button copies key to clipboard.
|
||||
*/
|
||||
test('should copy API key to clipboard', async ({ page, context }) => {
|
||||
// Grant clipboard permissions
|
||||
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
test('should copy API key to clipboard', async ({ page, context }, testInfo) => {
|
||||
// Grant clipboard permissions. Firefox/WebKit do not support 'clipboard-read'
|
||||
// so only request it on Chromium projects.
|
||||
const browserName = testInfo.project?.name || '';
|
||||
if (browserName === 'chromium') {
|
||||
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
} else {
|
||||
await context.grantPermissions(['clipboard-write']);
|
||||
}
|
||||
|
||||
await test.step('Click copy button', async () => {
|
||||
const copyButton = page
|
||||
|
||||
Reference in New Issue
Block a user