Skip to content

Commit 82a89af

Browse files
committed
webui/kpm: implement chunked file upload
1 parent 34a85b6 commit 82a89af

3 files changed

Lines changed: 101 additions & 43 deletions

File tree

webui/page/kpm.js

Lines changed: 77 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { exec, toast } from 'kernelsu-alt';
1+
import { exec, spawn, toast } from 'kernelsu-alt';
22
import { modDir, persistDir, superkey } from '../index.js';
33

44
let allKpms = [];
@@ -147,7 +147,41 @@ async function renderKpmList() {
147147
});
148148
}
149149

150-
async function handleFileUpload(accept, containerId, onLoaded) {
150+
async function uploadFile(file, targetPath, onProgress, signal) {
151+
const CHUNK_SIZE = 96 * 1024; // 96KB chunks
152+
let offset = 0;
153+
154+
await exec(`mkdir -p "$(dirname "${targetPath}")" && : > "${targetPath}"`);
155+
156+
while (offset < file.size) {
157+
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
158+
const chunk = file.slice(offset, offset + CHUNK_SIZE);
159+
const base64 = await new Promise((resolve, reject) => {
160+
const reader = new FileReader();
161+
reader.onload = () => resolve(reader.result.split(',')[1]);
162+
reader.onerror = reject;
163+
reader.readAsDataURL(chunk);
164+
});
165+
166+
const result = await new Promise((resolve, reject) => {
167+
const child = spawn(`echo '${base64}' | base64 -d >> "${targetPath}"`);
168+
child.on('exit', (code) => {
169+
if (code === 0) resolve({ errno: 0 });
170+
else resolve({ errno: code, stderr: `Exit code ${code}` });
171+
});
172+
child.on('error', (err) => reject(err));
173+
});
174+
175+
if (result.errno !== 0) {
176+
throw new Error(result.stderr || 'Write error');
177+
}
178+
179+
offset += CHUNK_SIZE;
180+
if (onProgress) onProgress(Math.min(offset, file.size) / file.size);
181+
}
182+
}
183+
184+
async function handleFileUpload(accept, containerId, onSelected) {
151185
const input = document.createElement('input');
152186
input.type = 'file';
153187
input.accept = accept;
@@ -160,14 +194,18 @@ async function handleFileUpload(accept, containerId, onLoaded) {
160194
return;
161195
}
162196

163-
const reader = new FileReader();
164-
197+
const abortController = new AbortController();
165198
const loadingCard = document.createElement('div');
166199
loadingCard.className = 'card module-card';
167200
loadingCard.innerHTML = `
168-
<div class="module-card-header">
169-
<div class="module-card-title">Uploading ${file.name}</div>
170-
<div class="module-card-subtitle">Please wait...</div>
201+
<div class="module-card-header flex-header">
202+
<div class="header-info">
203+
<div class="module-card-title">${file.name}</div>
204+
<div class="module-card-subtitle" id="upload-progress-text">Please wait...</div>
205+
</div>
206+
<md-outlined-icon-button id="cancel-upload">
207+
<md-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg></md-icon>
208+
</md-outlined-icon-button>
171209
</div>
172210
<div class="module-card-content">
173211
<md-linear-progress indeterminate></md-linear-progress>
@@ -176,35 +214,43 @@ async function handleFileUpload(accept, containerId, onLoaded) {
176214
const container = document.getElementById(containerId);
177215
container.prepend(loadingCard);
178216

179-
reader.onabort = () => loadingCard.remove();
180-
reader.onerror = () => {
181-
loadingCard.remove();
182-
toast('Failed to read file');
217+
const progressBar = loadingCard.querySelector('md-linear-progress');
218+
const progressText = loadingCard.querySelector('#upload-progress-text');
219+
const cancelBtn = loadingCard.querySelector('#cancel-upload');
220+
221+
cancelBtn.onclick = () => {
222+
abortController.abort();
183223
};
184-
reader.onload = async () => {
185-
loadingCard.remove();
186-
const base64 = reader.result.split(',')[1];
187-
await onLoaded(file, base64);
224+
225+
const onProgress = (percent) => {
226+
const p = Math.round(percent * 100);
227+
progressBar.value = percent;
228+
progressBar.indeterminate = false;
229+
progressText.textContent = `Uploading... ${p}%`;
188230
};
189-
reader.readAsDataURL(file);
231+
232+
try {
233+
await onSelected(file, onProgress, abortController.signal);
234+
} catch (err) {
235+
if (err.name === 'AbortError') {
236+
toast('Upload cancelled');
237+
} else {
238+
toast(`Error: ${err.message}`);
239+
}
240+
} finally {
241+
loadingCard.remove();
242+
}
190243
};
191244
input.click();
192245
}
193246

194247
async function uploadAndLoadModule() {
195-
handleFileUpload('.kpm', 'kpm-list', async (file, base64) => {
248+
handleFileUpload('.kpm', 'kpm-list', async (file, onProgress, signal) => {
249+
const tmpPath = `${modDir}/tmp/${file.name}`;
196250
try {
197-
const result = await exec(`
198-
mkdir -p ${modDir}/tmp
199-
rm -rf ${modDir}/tmp/*
200-
echo '${base64}' | base64 -d > ${modDir}/tmp/${file.name}
201-
`);
202-
if (result.errno !== 0) {
203-
toast(`Failed to write file: ${result.stderr}`);
204-
return;
205-
}
206-
207-
const info = await getKpmInfo(`${modDir}/tmp/${file.name}`);
251+
await exec(`mkdir -p ${modDir}/tmp && rm -rf ${modDir}/tmp/*`);
252+
await uploadFile(file, tmpPath, onProgress, signal);
253+
const info = await getKpmInfo(tmpPath);
208254
if (info && info.name) {
209255
const dialog = document.getElementById('load-dialog');
210256
dialog.querySelector('#module-name').textContent = info.name;
@@ -241,7 +287,8 @@ async function uploadAndLoadModule() {
241287
exec(`rm -rf ${modDir}/tmp`);
242288
}
243289
} catch (e) {
244-
toast(`Error: ${e.message}`);
290+
exec(`rm -rf ${modDir}/tmp`);
291+
throw e;
245292
}
246293
});
247294
}
@@ -285,4 +332,4 @@ export function initKPMPage() {
285332
refreshKpmList();
286333
}
287334

288-
export { loadModule, refreshKpmList, uploadAndLoadModule, handleFileUpload }
335+
export { loadModule, refreshKpmList, uploadAndLoadModule, handleFileUpload, uploadFile }

webui/page/patch.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { exec, spawn, toast } from 'kernelsu-alt';
22
import { modDir, superkey } from '../index.js';
3-
import { handleFileUpload } from './kpm.js';
3+
import { handleFileUpload, uploadFile } from './kpm.js';
44

55
function uInt2String(ver) {
66
const val = typeof ver === 'string' ? parseInt(ver, 16) : ver;
@@ -198,7 +198,7 @@ function renderKpmList() {
198198
card.className = 'card module-card';
199199
card.innerHTML = `
200200
<div class="module-card-header">
201-
<div class="tag-wrapper">
201+
<div class="flex-header">
202202
<div class="module-card-title">${item.name}</div>
203203
${isNew ? '' : '<div class="tag">EMBEDDED</div>'}
204204
</div>
@@ -267,15 +267,16 @@ function openOptionDialog(item) {
267267
}
268268

269269
async function embedKPM() {
270-
handleFileUpload('.kpm', 'kpm-embed-list', async (file, base64) => {
270+
handleFileUpload('.kpm', 'kpm-embed-list', async (file, onProgress, signal) => {
271271
// Generate random filename
272272
const randName = Math.random().toString(36).substring(7) + '.kpm';
273273
const tmpPath = `${modDir}/tmp/${randName}`;
274-
const writeResult = await exec(`echo '${base64}' | base64 -d > ${tmpPath}`);
275274

276-
if (writeResult.errno) {
277-
toast("Failed to upload KPM file");
278-
return;
275+
try {
276+
await uploadFile(file, tmpPath, onProgress, signal);
277+
} catch (e) {
278+
exec(`rm -f ${tmpPath}`);
279+
throw e;
279280
}
280281

281282
const result = await exec(`kptools -l -M "${randName}"`, {

webui/styles.css

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,12 +274,28 @@ body {
274274
color: var(--md-sys-color-on-surface-variant);
275275
}
276276

277+
.module-card .flex-header {
278+
display: flex;
279+
justify-content: space-between;
280+
align-items: flex-start;
281+
gap: 12px;
282+
}
283+
284+
.header-info {
285+
flex: 1;
286+
min-width: 0;
287+
}
288+
277289
.module-card-content {
278290
padding: 16px 0;
279291
font-size: 0.8em;
280292
color: var(--md-sys-color-outline);
281293
}
282294

295+
.module-card-content md-linear-progress {
296+
width: 100%;
297+
}
298+
283299
.module-card-actions {
284300
padding-top: 16px;
285301
display: flex;
@@ -291,12 +307,6 @@ body {
291307
--md-filled-tonal-icon-button-container-width: 64px;
292308
}
293309

294-
.module-card .tag-wrapper {
295-
display: flex;
296-
justify-content: space-between;
297-
align-items: flex-start;
298-
}
299-
300310
.app-item .tag-wrapper {
301311
display: flex;
302312
justify-content: flex-start;

0 commit comments

Comments
 (0)