Media Upload Fix

This commit is contained in:
2026-02-22 13:44:39 -05:00
parent 3b4d5e4080
commit 3c669c7e26
3 changed files with 119 additions and 41 deletions

View File

@@ -24,7 +24,7 @@ func (rt *Router) UploadSimulationResource(w http.ResponseWriter, r *http.Reques
return return
} }
err = r.ParseMultipartForm(500 << 20) // 500 MB max memory/file bounds err = r.ParseMultipartForm(32 << 20) // 32 MB max memory bounds, rest spills to disk
if err != nil { if err != nil {
http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest) http.Error(w, "Error parsing form: "+err.Error(), http.StatusBadRequest)
return return

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
let { simName, resourceName, isEditing, onDelete } = $props(); let { simName, resourceName, isEditing, onDelete } = $props();
let showVideo = $state(false);
function getResourceUrl(simName: string, resourceName: string) { function getResourceUrl(simName: string, resourceName: string) {
return `/results/${simName}/${resourceName}`; return `/results/${simName}/${resourceName}`;
} }
@@ -17,7 +19,8 @@
return ( return (
fname.toLowerCase().endsWith(".mp4") || fname.toLowerCase().endsWith(".mp4") ||
fname.toLowerCase().endsWith(".avi") || fname.toLowerCase().endsWith(".avi") ||
fname.toLowerCase().endsWith(".webm") fname.toLowerCase().endsWith(".webm") ||
fname.toLowerCase().endsWith(".mkv")
); );
} }
</script> </script>
@@ -28,6 +31,11 @@
<a href={getResourceUrl(simName, resourceName)} download class="dl-link" <a href={getResourceUrl(simName, resourceName)} download class="dl-link"
>[Download]</a >[Download]</a
> >
{#if isVideo(resourceName)}
<button class="watch-link" onclick={() => (showVideo = !showVideo)}>
[{showVideo ? "Hide" : "Watch"}]
</button>
{/if}
{#if isEditing} {#if isEditing}
<button <button
class="delete-link" class="delete-link"
@@ -41,17 +49,14 @@
{#if isImage(resourceName)} {#if isImage(resourceName)}
<img src={getResourceUrl(simName, resourceName)} alt={resourceName} /> <img src={getResourceUrl(simName, resourceName)} alt={resourceName} />
{:else if isVideo(resourceName)} {:else if isVideo(resourceName)}
<video controls> {#if showVideo}
<source <video controls src={getResourceUrl(simName, resourceName)}>
src={getResourceUrl(simName, resourceName)} <track kind="captions" />
type="video/webm" <a href={getResourceUrl(simName, resourceName)}
/> >Download Video</a
<source >
src={getResourceUrl(simName, resourceName)}
type="video/mp4"
/>
<a href={getResourceUrl(simName, resourceName)}>Download Video</a>
</video> </video>
{/if}
{:else} {:else}
<ul> <ul>
<li> <li>
@@ -94,6 +99,21 @@
.dl-link:visited { .dl-link:visited {
color: #800080; color: #800080;
} }
.watch-link {
font-weight: normal;
margin-left: 10px;
color: #0000ff;
text-decoration: underline;
font-size: 14px;
background: none;
border: none;
cursor: pointer;
padding: 0;
font-family: inherit;
}
.watch-link:hover {
color: #800080;
}
.delete-link { .delete-link {
font-weight: normal; font-weight: normal;
margin-left: 10px; margin-left: 10px;

View File

@@ -157,6 +157,8 @@
let newName = $state(""); let newName = $state("");
let uploadFiles = $state<FileList | null>(null); let uploadFiles = $state<FileList | null>(null);
let saveError = $state(""); let saveError = $state("");
let uploadProgress = $state(0);
let isUploading = $state(false);
async function handleSave() { async function handleSave() {
if (!simulation) return; if (!simulation) return;
@@ -182,32 +184,55 @@
// 2. Upload Logic // 2. Upload Logic
if (uploadFiles && uploadFiles.length > 0) { if (uploadFiles && uploadFiles.length > 0) {
isUploading = true;
for (let i = 0; i < uploadFiles.length; i++) { for (let i = 0; i < uploadFiles.length; i++) {
const formData = new FormData(); const file = uploadFiles[i];
formData.append("file", uploadFiles[i]); uploadProgress = 0;
const uploadRes = await fetch(`/api/simulations/${id}/upload`, { try {
method: "POST", const upData: any = await new Promise((resolve, reject) => {
body: formData, const xhr = new XMLHttpRequest();
xhr.open("POST", `/api/simulations/${id}/upload`);
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
uploadProgress = Math.round((event.loaded / event.total) * 100);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(xhr.responseText || `Failed to upload: ${file.name}`);
}
};
xhr.onerror = () => reject(`Network error uploading: ${file.name}`);
const formData = new FormData();
formData.append("file", file);
xhr.send(formData);
}); });
if (!uploadRes.ok) {
saveError = `Failed to upload: ${uploadFiles[i].name}`;
return;
}
const upData = await uploadRes.json();
if (!simulation.resources.includes(upData.filename)) { if (!simulation.resources.includes(upData.filename)) {
simulation.resources = [ simulation.resources = [
...simulation.resources, ...simulation.resources,
upData.filename, upData.filename,
]; ];
} }
} catch (err: any) {
saveError = err.toString();
isUploading = false;
return;
}
} }
} }
isEditing = false; isEditing = false;
isUploading = false;
uploadFiles = null; uploadFiles = null;
uploadProgress = 0;
} }
function cancelEdit() { function cancelEdit() {
@@ -217,6 +242,8 @@
isEditing = false; isEditing = false;
saveError = ""; saveError = "";
uploadFiles = null; uploadFiles = null;
uploadProgress = 0;
isUploading = false;
} }
async function handleDelete() { async function handleDelete() {
@@ -297,11 +324,15 @@
{#if saveError} {#if saveError}
<p class="error">{saveError}</p> <p class="error">{saveError}</p>
{/if} {/if}
{#if isUploading}
<div class="progress-bar-container">
<div class="progress-bar" style={`width: ${uploadProgress}%`}></div>
<span class="progress-text">{uploadProgress}%</span>
</div>
{/if}
<div class="form-actions"> <div class="form-actions">
<button class="save-btn" onclick={handleSave}>Save</button> <button class="save-btn" onclick={handleSave} disabled={isUploading}>Save</button>
<button class="cancel-btn" onclick={cancelEdit} <button class="cancel-btn" onclick={cancelEdit} disabled={isUploading}>Cancel</button>
>Cancel</button
>
</div> </div>
</div> </div>
{/if} {/if}
@@ -538,11 +569,15 @@
background-color: #e0e0e0; background-color: #e0e0e0;
font-family: inherit; font-family: inherit;
} }
.edit-btn:hover, .edit-btn:hover:not(:disabled),
.save-btn:hover, .save-btn:hover:not(:disabled),
.cancel-btn:hover { .cancel-btn:hover:not(:disabled) {
background-color: #d0d0d0; background-color: #d0d0d0;
} }
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.delete-btn { .delete-btn {
background-color: #ffcccc; background-color: #ffcccc;
color: #990000; color: #990000;
@@ -569,16 +604,39 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
.form-group label { .edit-form label {
display: inline-block;
width: 100px;
font-weight: bold; font-weight: bold;
min-width: 120px;
} }
.form-group input[type="text"] { .edit-form input[type="text"] {
flex: 1; flex: 1;
max-width: 400px;
padding: 5px; padding: 5px;
font-family: inherit;
border: 1px solid #000; border: 1px solid #000;
font-family: inherit;
}
.progress-bar-container {
width: 100%;
background-color: #ddd;
border: 1px solid #000;
margin-top: 10px;
position: relative;
height: 25px;
}
.progress-bar {
height: 100%;
background-color: #4CAF50;
transition: width 0.2s ease-in-out;
}
.progress-text {
position: absolute;
width: 100%;
text-align: center;
top: 0;
left: 0;
line-height: 25px;
font-weight: bold;
color: #000;
} }
.form-group input[type="file"] { .form-group input[type="file"] {
font-family: inherit; font-family: inherit;