first commit
This commit is contained in:
347
src/tools/plecUpload.ts
Normal file
347
src/tools/plecUpload.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { File } from 'node:buffer';
|
||||
import { handleClipboardAndOpen, singletonPanel } from './shared';
|
||||
|
||||
const store: { panel: vscode.WebviewPanel | null } = { panel: null };
|
||||
|
||||
export function openPlecUpload(_context: vscode.ExtensionContext) {
|
||||
const { panel, isNew } = singletonPanel(store, 'hplToolbox.plecUpload', 'PLEC Upload');
|
||||
if (!isNew) return;
|
||||
|
||||
panel.webview.html = getHtml();
|
||||
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||
try {
|
||||
if (handleClipboardAndOpen(msg)) return;
|
||||
|
||||
if (msg.type === 'pickFile') {
|
||||
const picked = await vscode.window.showOpenDialog({
|
||||
canSelectMany: false,
|
||||
filters: { HTML: ['html', 'htm'] },
|
||||
openLabel: 'Select',
|
||||
});
|
||||
if (picked && picked[0]) {
|
||||
const fp = picked[0].fsPath;
|
||||
panel.webview.postMessage({
|
||||
type: 'fileSelected',
|
||||
rowId: msg.rowId,
|
||||
path: fp,
|
||||
name: path.basename(fp),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'upload') {
|
||||
await handleUpload(panel, msg.rows);
|
||||
return;
|
||||
}
|
||||
} catch (err: any) {
|
||||
panel.webview.postMessage({ type: 'error', message: err?.message || String(err) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface UploadRow {
|
||||
id: string;
|
||||
path: string;
|
||||
iteration: string;
|
||||
}
|
||||
|
||||
async function handleUpload(panel: vscode.WebviewPanel, rows: UploadRow[]) {
|
||||
const cfg = vscode.workspace.getConfiguration('hplToolbox.plec');
|
||||
const uploadUrl = cfg.get<string>('uploadUrl')!;
|
||||
const originUrl = cfg.get<string>('originUrl')!;
|
||||
const previewBase = cfg.get<string>('previewBase')!;
|
||||
const skipExistsCheck = cfg.get<boolean>('skipExistsCheck')!;
|
||||
|
||||
const valid: UploadRow[] = [];
|
||||
for (const r of rows) {
|
||||
if (!r.path || !fs.existsSync(r.path)) {
|
||||
panel.webview.postMessage({ type: 'rowError', rowId: r.id, message: 'File missing' });
|
||||
return;
|
||||
}
|
||||
if (!r.iteration || !r.iteration.trim()) {
|
||||
panel.webview.postMessage({ type: 'rowError', rowId: r.id, message: 'Iteration name required' });
|
||||
return;
|
||||
}
|
||||
if (!/\.html?$/i.test(r.path)) {
|
||||
panel.webview.postMessage({ type: 'rowError', rowId: r.id, message: 'Must be .html' });
|
||||
return;
|
||||
}
|
||||
valid.push(r);
|
||||
}
|
||||
if (valid.length === 0) {
|
||||
panel.webview.postMessage({ type: 'error', message: 'Nothing to upload.' });
|
||||
return;
|
||||
}
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: 'Checking filename availability...' });
|
||||
|
||||
if (!skipExistsCheck) {
|
||||
for (const r of valid) {
|
||||
const fname = path.basename(r.path);
|
||||
const exists = await checkFileExists(previewBase + encodeURIComponent(fname));
|
||||
if (exists === true) {
|
||||
panel.webview.postMessage({
|
||||
type: 'rowError',
|
||||
rowId: r.id,
|
||||
message: `Filename "${fname}" already exists on preview server. Rename and retry.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
panel.webview.postMessage({ type: 'status', message: 'Uploading...' });
|
||||
|
||||
const form = new FormData();
|
||||
for (let i = 0; i < valid.length; i++) {
|
||||
const r = valid[i];
|
||||
const idx = i + 1;
|
||||
const buf = fs.readFileSync(r.path);
|
||||
const file = new File([buf], path.basename(r.path), { type: 'text/html' });
|
||||
form.append(`fileUpload_${idx}`, file as any);
|
||||
form.append(`iterationNameUpload_${idx}`, encodeURI(r.iteration.replace(/\s+/g, '')));
|
||||
}
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: {
|
||||
'Origin': originUrl,
|
||||
'Referer': originUrl + '/',
|
||||
'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': '*/*',
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
panel.webview.postMessage({ type: 'error', message: 'Network error: ' + (err?.message || err) });
|
||||
return;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
panel.webview.postMessage({
|
||||
type: 'error',
|
||||
message: `HTTP ${res.status}\n${text.slice(0, 800)}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let json: any;
|
||||
try { json = JSON.parse(text); }
|
||||
catch {
|
||||
panel.webview.postMessage({
|
||||
type: 'error',
|
||||
message: `Server returned non-JSON:\n${text.slice(0, 800)}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const preview = json.preview || json.previewURL || null;
|
||||
panel.webview.postMessage({ type: 'result', preview, raw: json, rawText: text });
|
||||
}
|
||||
|
||||
async function checkFileExists(url: string): Promise<boolean | null> {
|
||||
try {
|
||||
const res = await fetch(url, { method: 'HEAD' });
|
||||
if (res.status === 200) return true;
|
||||
if (res.status === 404) return false;
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); padding: 16px; }
|
||||
h2 { margin-top: 0; }
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
|
||||
th, td { padding: 6px 8px; text-align: left; vertical-align: middle; border-bottom: 1px solid var(--vscode-panel-border); }
|
||||
th { font-weight: 600; font-size: 12px; text-transform: uppercase; opacity: 0.7; }
|
||||
input[type=text] {
|
||||
width: 100%; padding: 4px 6px;
|
||||
background: var(--vscode-input-background); color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border, transparent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
button {
|
||||
background: var(--vscode-button-background); color: var(--vscode-button-foreground);
|
||||
border: none; padding: 6px 12px; border-radius: 2px; cursor: pointer;
|
||||
}
|
||||
button:hover { background: var(--vscode-button-hoverBackground); }
|
||||
button.secondary {
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
}
|
||||
button.secondary:hover { background: var(--vscode-button-secondaryHoverBackground); }
|
||||
.file-cell { display: flex; align-items: center; gap: 8px; }
|
||||
.file-name { opacity: 0.85; font-size: 12px; word-break: break-all; }
|
||||
.row-error { color: var(--vscode-errorForeground); font-size: 12px; margin-top: 4px; }
|
||||
.actions { margin-top: 12px; display: flex; gap: 8px; }
|
||||
#status { margin-top: 12px; min-height: 20px; }
|
||||
.err { color: var(--vscode-errorForeground); white-space: pre-wrap; }
|
||||
.ok { color: var(--vscode-testing-iconPassed, var(--vscode-foreground)); }
|
||||
.result {
|
||||
margin-top: 12px; padding: 10px;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-left: 3px solid var(--vscode-textBlockQuote-border);
|
||||
word-break: break-all;
|
||||
}
|
||||
.result a { color: var(--vscode-textLink-foreground); }
|
||||
.result-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||||
.remove-btn { background: transparent; color: var(--vscode-foreground); padding: 4px 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>PLEC Upload</h2>
|
||||
<p style="opacity:0.8;font-size:12px;margin-top:0;">Pick HTML files, give each an iteration name, then upload. The preview link is returned by the server.</p>
|
||||
|
||||
<table id="rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:45%">HTML File</th>
|
||||
<th style="width:45%">Iteration Name</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<div class="actions">
|
||||
<button id="addRow" class="secondary">+ Add Row</button>
|
||||
<button id="upload">Upload</button>
|
||||
</div>
|
||||
|
||||
<div id="status"></div>
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
let nextId = 1;
|
||||
const tbody = document.querySelector('#rows tbody');
|
||||
const statusEl = document.getElementById('status');
|
||||
const resultsEl = document.getElementById('results');
|
||||
|
||||
function addRow() {
|
||||
const id = 'r' + (nextId++);
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.id = id;
|
||||
tr.innerHTML = \`
|
||||
<td>
|
||||
<div class="file-cell">
|
||||
<button class="pick secondary">Choose...</button>
|
||||
<span class="file-name" data-name>(no file)</span>
|
||||
</div>
|
||||
<input type="hidden" data-path />
|
||||
<div class="row-error" data-error></div>
|
||||
</td>
|
||||
<td><input type="text" data-iter placeholder="iteration name" /></td>
|
||||
<td><button class="remove-btn" title="Remove">×</button></td>
|
||||
\`;
|
||||
tr.querySelector('.pick').addEventListener('click', () => {
|
||||
vscode.postMessage({ type: 'pickFile', rowId: id });
|
||||
});
|
||||
tr.querySelector('.remove-btn').addEventListener('click', () => {
|
||||
tr.remove();
|
||||
if (!tbody.children.length) addRow();
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
document.getElementById('addRow').addEventListener('click', addRow);
|
||||
|
||||
document.getElementById('upload').addEventListener('click', () => {
|
||||
clearErrors();
|
||||
statusEl.textContent = '';
|
||||
resultsEl.innerHTML = '';
|
||||
const rows = [...tbody.querySelectorAll('tr')].map(tr => ({
|
||||
id: tr.dataset.id,
|
||||
path: tr.querySelector('[data-path]').value,
|
||||
iteration: tr.querySelector('[data-iter]').value,
|
||||
})).filter(r => r.path || r.iteration);
|
||||
if (rows.length === 0) {
|
||||
statusEl.innerHTML = '<span class="err">Add at least one row with a file and iteration name.</span>';
|
||||
return;
|
||||
}
|
||||
vscode.postMessage({ type: 'upload', rows });
|
||||
});
|
||||
|
||||
function clearErrors() {
|
||||
document.querySelectorAll('[data-error]').forEach(el => el.textContent = '');
|
||||
}
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
const m = e.data;
|
||||
if (m.type === 'fileSelected') {
|
||||
const tr = tbody.querySelector('tr[data-id="' + m.rowId + '"]');
|
||||
if (!tr) return;
|
||||
tr.querySelector('[data-path]').value = m.path;
|
||||
tr.querySelector('[data-name]').textContent = m.name;
|
||||
const iter = tr.querySelector('[data-iter]');
|
||||
if (!iter.value) iter.value = m.name.replace(/\\.html?$/i, '');
|
||||
} else if (m.type === 'status') {
|
||||
statusEl.textContent = m.message;
|
||||
statusEl.className = '';
|
||||
} else if (m.type === 'error') {
|
||||
statusEl.innerHTML = '';
|
||||
const div = document.createElement('div');
|
||||
div.className = 'err';
|
||||
div.textContent = m.message;
|
||||
statusEl.appendChild(div);
|
||||
} else if (m.type === 'rowError') {
|
||||
const tr = tbody.querySelector('tr[data-id="' + m.rowId + '"]');
|
||||
if (tr) tr.querySelector('[data-error]').textContent = m.message;
|
||||
statusEl.innerHTML = '<span class="err">Fix row errors and retry.</span>';
|
||||
} else if (m.type === 'result') {
|
||||
statusEl.innerHTML = '<span class="ok">Upload complete.</span>';
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'result';
|
||||
if (m.preview) {
|
||||
wrap.innerHTML = '<div><strong>Preview:</strong> <a href="' + m.preview + '">' + m.preview + '</a></div>';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'result-actions';
|
||||
const copy = document.createElement('button');
|
||||
copy.textContent = 'Copy';
|
||||
copy.onclick = () => vscode.postMessage({ type: 'copy', text: m.preview });
|
||||
const open = document.createElement('button');
|
||||
open.textContent = 'Open';
|
||||
open.className = 'secondary';
|
||||
open.onclick = () => vscode.postMessage({ type: 'open', text: m.preview });
|
||||
const showRaw = document.createElement('button');
|
||||
showRaw.textContent = 'Show server response';
|
||||
showRaw.className = 'secondary';
|
||||
showRaw.onclick = () => {
|
||||
const pre = document.createElement('pre');
|
||||
pre.style.marginTop = '8px';
|
||||
pre.style.whiteSpace = 'pre-wrap';
|
||||
pre.textContent = m.rawText || JSON.stringify(m.raw, null, 2);
|
||||
wrap.appendChild(pre);
|
||||
showRaw.disabled = true;
|
||||
};
|
||||
actions.appendChild(copy); actions.appendChild(open); actions.appendChild(showRaw);
|
||||
wrap.appendChild(actions);
|
||||
} else {
|
||||
wrap.innerHTML = '<div>No preview field in response. Raw JSON:</div><pre>' +
|
||||
(m.rawText || JSON.stringify(m.raw, null, 2)).replace(/</g, '<') + '</pre>';
|
||||
}
|
||||
resultsEl.innerHTML = '';
|
||||
resultsEl.appendChild(wrap);
|
||||
}
|
||||
});
|
||||
|
||||
addRow();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
Reference in New Issue
Block a user