feat: add URL-based file upload with SSRF protection
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+170
-24
@@ -5,34 +5,85 @@
|
||||
<div class="flex flex-col items-center justify-center p-8">
|
||||
<h1 class="text-3xl font-bold mb-8">Upload Files</h1>
|
||||
|
||||
<form action="/api/ui-upload" method="POST" enctype="multipart/form-data">
|
||||
<div
|
||||
id="dropZone"
|
||||
class="border-4 border-dashed border-gray-300 rounded-lg p-8 bg-white text-center w-full max-w-lg"
|
||||
ondrop="handleDrop(event)"
|
||||
ondragover="handleDragOver(event)"
|
||||
ondragleave="handleDragLeave(event)"
|
||||
>
|
||||
<p class="text-gray-500 mb-4">
|
||||
Drag & drop files here, or click to select files.
|
||||
</p>
|
||||
<input
|
||||
id="fileInput"
|
||||
type="file"
|
||||
class="hidden"
|
||||
onchange="handleFileSelect(event)"
|
||||
name="files"
|
||||
multiple
|
||||
/>
|
||||
<div class="text-sm text-gray-500 mt-2">
|
||||
<p>Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images</p>
|
||||
<p>Maximum size: 500MB per file</p>
|
||||
<!-- File Upload Section -->
|
||||
<div class="w-full max-w-2xl mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Upload from Computer</h2>
|
||||
<form action="/api/ui-upload" method="POST" enctype="multipart/form-data">
|
||||
<div
|
||||
id="dropZone"
|
||||
class="border-4 border-dashed border-gray-300 rounded-lg p-8 bg-white text-center w-full"
|
||||
ondrop="handleDrop(event)"
|
||||
ondragover="handleDragOver(event)"
|
||||
ondragleave="handleDragLeave(event)"
|
||||
>
|
||||
<p class="text-gray-500 mb-4">
|
||||
Drag & drop files here, or click to select files.
|
||||
</p>
|
||||
<input
|
||||
id="fileInput"
|
||||
type="file"
|
||||
class="hidden"
|
||||
onchange="handleFileSelect(event)"
|
||||
name="files"
|
||||
multiple
|
||||
/>
|
||||
<div class="text-sm text-gray-500 mt-2">
|
||||
<p>Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images</p>
|
||||
<p>Maximum size: 500MB per file</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- URL Upload Section -->
|
||||
<div class="w-full max-w-2xl mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Upload from URL</h2>
|
||||
<div class="bg-white border border-gray-300 rounded-lg p-6">
|
||||
<form id="urlUploadForm" class="space-y-4">
|
||||
<div>
|
||||
<label for="urlInput" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
File URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="urlInput"
|
||||
name="url"
|
||||
placeholder="https://example.com/document.pdf"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Enter a direct link to a file (PDF, Office documents, or images)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="urlFilename" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Filename (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="urlFilename"
|
||||
name="filename"
|
||||
placeholder="my-document.pdf"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Leave empty to use filename from URL
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200"
|
||||
>
|
||||
Download and Process
|
||||
</button>
|
||||
</form>
|
||||
<div id="urlStatusMessage" class="mt-4"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="statusMessage" class="mt-4 text-gray-700"></div>
|
||||
<div id="uploadProgress" class="mt-4 w-full max-w-lg"></div>
|
||||
<div id="uploadProgress" class="mt-4 w-full max-w-2xl"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -89,5 +140,100 @@
|
||||
processFiles(e.target.files, progressContainer, statusMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// URL Upload handling
|
||||
const urlUploadForm = document.getElementById("urlUploadForm");
|
||||
const urlStatusMessage = document.getElementById("urlStatusMessage");
|
||||
|
||||
urlUploadForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const urlInput = document.getElementById("urlInput");
|
||||
const urlFilename = document.getElementById("urlFilename");
|
||||
const url = urlInput.value.trim();
|
||||
const filename = urlFilename.value.trim();
|
||||
|
||||
if (!url) {
|
||||
showUrlStatus("Please enter a URL", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
showUrlStatus("Invalid URL format", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
showUrlStatus("Downloading file from URL...", "info");
|
||||
|
||||
const submitButton = urlUploadForm.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.textContent;
|
||||
submitButton.textContent = "Processing...";
|
||||
submitButton.disabled = true;
|
||||
|
||||
try {
|
||||
const requestBody = { url };
|
||||
if (filename) {
|
||||
requestBody.filename = filename;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/process-url", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showUrlStatus(
|
||||
`Success! File "${data.filename}" (${formatBytes(data.size)}) downloaded and queued for processing.`,
|
||||
"success"
|
||||
);
|
||||
// Clear form
|
||||
urlInput.value = "";
|
||||
urlFilename.value = "";
|
||||
|
||||
// Optionally redirect to files page after short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = "/files";
|
||||
}, 2000);
|
||||
} else {
|
||||
showUrlStatus(`Error: ${data.detail || "Failed to process URL"}`, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
showUrlStatus(`Network error: ${error.message}`, "error");
|
||||
} finally {
|
||||
submitButton.textContent = originalButtonText;
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function showUrlStatus(message, type) {
|
||||
urlStatusMessage.innerHTML = "";
|
||||
const alertDiv = document.createElement("div");
|
||||
alertDiv.className = `px-4 py-3 rounded ${
|
||||
type === "success"
|
||||
? "bg-green-100 border border-green-400 text-green-700"
|
||||
: type === "error"
|
||||
? "bg-red-100 border border-red-400 text-red-700"
|
||||
: "bg-blue-100 border border-blue-400 text-blue-700"
|
||||
}`;
|
||||
alertDiv.textContent = message;
|
||||
urlStatusMessage.appendChild(alertDiv);
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user