mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-23 13:18:13 -05:00
495 lines
18 KiB
HTML
Vendored
495 lines
18 KiB
HTML
Vendored
{{define "dashboard-default/file"}}
|
|
<!DOCTYPE html>
|
|
<html lang="{{.Conf.Language}}">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>File List</title>
|
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
|
<link rel="stylesheet" href="https://unpkg.com/mdui@2/mdui.css" />
|
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
|
<script src="https://unpkg.com/mdui@2/mdui.global.js"></script>
|
|
</head>
|
|
<style>
|
|
body {
|
|
font-family: 'Roboto', sans-serif;
|
|
}
|
|
|
|
.file-list {
|
|
list-style-type: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.file-item {
|
|
margin: 5px 0;
|
|
border: 1px solid #ccc;
|
|
border-radius: 5px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
display: block;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
#top-app-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px;
|
|
}
|
|
|
|
#current-directory {
|
|
font-size: 1rem;
|
|
font-weight: normal;
|
|
margin: 0 15px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: calc(100% - 100px);
|
|
box-sizing: border-box;
|
|
}
|
|
</style>
|
|
|
|
<body>
|
|
<div id="top-app-bar">
|
|
<mdui-dropdown>
|
|
<mdui-button-icon slot="trigger" icon="menu"></mdui-button-icon>
|
|
<mdui-menu>
|
|
<mdui-menu-item id="refresh">{{tr "Refresh"}}</mdui-menu-item>
|
|
<mdui-menu-item id="copy">{{tr "CopyPath"}}</mdui-menu-item>
|
|
<mdui-menu-item id="goto">{{tr "Goto"}}</mdui-menu-item>
|
|
</mdui-menu>
|
|
</mdui-dropdown>
|
|
<span id="current-directory"></span>
|
|
<mdui-button-icon id="upload" icon="upload"></mdui-button-icon>
|
|
</div>
|
|
|
|
<mdui-list id="file-list" class="file-list"></mdui-list>
|
|
|
|
<mdui-dialog id="error-dialog" headline="Error"
|
|
description="{{tr "FMError"}}"></mdui-dialog>
|
|
|
|
<mdui-dialog id="upd-modal" class="modal">
|
|
<mdui-linear-progress id="upd-progress"></mdui-linear-progress>
|
|
</mdui-dialog>
|
|
|
|
<mdui-dialog id="goto-dialog" headline="{{tr "GotoHeadline"}}" close-on-overlay-click>
|
|
<mdui-text-field id="goto-text" variant="outlined" value=""></mdui-text-field>
|
|
<mdui-button id="goto-go" slot="action" variant="text">{{tr "GotoGo"}}</mdui-button>
|
|
<mdui-button id="goto-close" slot="action" variant="tonal">{{tr "GotoClose"}}</mdui-button>
|
|
</mdui-dialog>
|
|
|
|
<script>
|
|
let currentPath = '/opt/nezha/';
|
|
let fileName = '';
|
|
let receivedBuffer = []; // 用于缓存数据块
|
|
let expectedLength = 0;
|
|
let receivedLength = 0;
|
|
let isFirstChunk = true;
|
|
let isUpCompleted = false;
|
|
let handleReady = false;
|
|
let worker;
|
|
|
|
function updateDirectoryTitle() {
|
|
const directoryTitle = document.getElementById('current-directory');
|
|
directoryTitle.textContent = `${currentPath}`;
|
|
}
|
|
|
|
function updateFileList(items) {
|
|
const fileListElement = document.getElementById('file-list');
|
|
fileListElement.innerHTML = '';
|
|
|
|
if (currentPath !== '/') {
|
|
const upItem = document.createElement('mdui-list-item');
|
|
upItem.className = 'file-item up-directory';
|
|
upItem.setAttribute('icon', 'arrow_back');
|
|
upItem.textContent = "..";
|
|
upItem.onclick = function () {
|
|
const lastSlashIndex = currentPath.lastIndexOf('/', currentPath.length - 2);
|
|
currentPath = currentPath.substring(0, lastSlashIndex + 1) || '/';
|
|
listFile();
|
|
};
|
|
fileListElement.appendChild(upItem);
|
|
}
|
|
|
|
items.sort((a, b) => {
|
|
if (a.fileType === 'dir' && b.fileType !== 'dir') {
|
|
return -1;
|
|
}
|
|
if (a.fileType !== 'dir' && b.fileType === 'dir') {
|
|
return 1;
|
|
}
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
items.forEach(item => {
|
|
const listItem = document.createElement('mdui-list-item');
|
|
listItem.className = `file-item ${item.fileType.toLowerCase()}`;
|
|
listItem.setAttribute('nonclickable', 'true');
|
|
listItem.setAttribute('icon', 'insert_drive_file');
|
|
listItem.textContent = `${item.name}`;
|
|
|
|
if (item.fileType === 'dir') {
|
|
listItem.setAttribute('nonclickable', 'false');
|
|
listItem.setAttribute('icon', 'folder');
|
|
listItem.style.cursor = 'pointer';
|
|
listItem.onclick = function () {
|
|
currentPath += `${item.name}/`;
|
|
listFile();
|
|
};
|
|
} else {
|
|
const downloadButton = document.createElement('mdui-button-icon');
|
|
downloadButton.setAttribute('slot', 'end-icon');
|
|
downloadButton.setAttribute('icon', 'download');
|
|
downloadButton.onclick = function () {
|
|
const filePath = currentPath + item.name;
|
|
fileName = item.name;
|
|
downloadFile(filePath);
|
|
};
|
|
listItem.appendChild(downloadButton);
|
|
}
|
|
|
|
fileListElement.appendChild(listItem);
|
|
});
|
|
updateDirectoryTitle();
|
|
}
|
|
|
|
function resetUpdState() {
|
|
receivedBuffer = [];
|
|
expectedLength = 0;
|
|
receivedLength = 0;
|
|
isFirstChunk = true;
|
|
}
|
|
|
|
function downloadFile(filePath) {
|
|
showUpdModal('d');
|
|
|
|
const prefix = new Int8Array([1]); // Request download
|
|
const filePathMessage = new TextEncoder().encode(filePath);
|
|
|
|
const msg = new Int8Array(prefix.length + filePathMessage.length);
|
|
msg.set(prefix);
|
|
msg.set(filePathMessage, prefix.length);
|
|
|
|
socket.send(msg);
|
|
}
|
|
|
|
function listFile() {
|
|
const prefix = new Int8Array([0]);
|
|
const resizeMessage = new TextEncoder().encode(currentPath);
|
|
|
|
const msg = new Int8Array(prefix.length + resizeMessage.length);
|
|
msg.set(prefix);
|
|
msg.set(resizeMessage, prefix.length);
|
|
|
|
socket.send(msg);
|
|
}
|
|
|
|
async function uploadFile(file) {
|
|
showUpdModal('u');
|
|
|
|
const chunkSize = 1048576; // 1MB chunk
|
|
let offset = 0;
|
|
|
|
const filePath = `${currentPath}${file.name}`;
|
|
const fileSize = file.size;
|
|
const messageType = 2;
|
|
|
|
// Build header (type + file size + path)
|
|
const filePathBytes = new TextEncoder().encode(filePath);
|
|
const header = new ArrayBuffer(1 + 8 + filePathBytes.length);
|
|
const headerView = new DataView(header);
|
|
|
|
headerView.setUint8(0, messageType);
|
|
headerView.setBigUint64(1, BigInt(fileSize), false);
|
|
|
|
new Uint8Array(header, 9).set(filePathBytes);
|
|
|
|
// Send header
|
|
socket.send(header);
|
|
|
|
// Send data chunks
|
|
while (offset < fileSize) {
|
|
const chunk = file.slice(offset, offset + chunkSize);
|
|
const arrayBuffer = await readFileAsArrayBuffer(chunk);
|
|
socket.send(arrayBuffer);
|
|
offset += chunkSize;
|
|
}
|
|
|
|
const checkCompletion = setInterval(() => {
|
|
if (isUpCompleted) {
|
|
clearInterval(checkCompletion);
|
|
hideUpdModal();
|
|
resetUpdState();
|
|
listFile();
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
async function parseFileList(arrayBuffer) {
|
|
const dataView = new DataView(arrayBuffer);
|
|
const items = [];
|
|
let offset = 4;
|
|
|
|
const pathLength = dataView.getUint32(offset);
|
|
offset += 4;
|
|
|
|
const pathArray = new Uint8Array(arrayBuffer, offset, pathLength);
|
|
currentPath = new TextDecoder('utf-8').decode(pathArray);
|
|
offset += pathLength;
|
|
|
|
while (offset < dataView.byteLength) {
|
|
const fileType = dataView.getUint8(offset);
|
|
offset += 1;
|
|
|
|
const nameLength = dataView.getUint8(offset);
|
|
offset += 1;
|
|
|
|
if (offset + nameLength > dataView.byteLength) {
|
|
console.error('Error: Name length exceeds buffer size');
|
|
break;
|
|
}
|
|
|
|
const nameArray = new Uint8Array(arrayBuffer, offset, nameLength);
|
|
const name = new TextDecoder('utf-8').decode(nameArray);
|
|
offset += nameLength;
|
|
|
|
items.push({
|
|
fileType: fileType === 0x01 ? 'dir' : 'f',
|
|
name: name,
|
|
});
|
|
}
|
|
|
|
return { items };
|
|
}
|
|
|
|
function readFileAsArrayBuffer(blob) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result);
|
|
reader.onerror = () => reject(reader.error);
|
|
reader.readAsArrayBuffer(blob);
|
|
});
|
|
}
|
|
|
|
function concatenateArrayBuffers(buffers) {
|
|
let totalLength = 0;
|
|
buffers.forEach(buf => totalLength += buf.byteLength);
|
|
|
|
const result = new Uint8Array(totalLength);
|
|
let offset = 0;
|
|
buffers.forEach(buf => {
|
|
result.set(new Uint8Array(buf), offset);
|
|
offset += buf.byteLength;
|
|
});
|
|
|
|
return result.buffer;
|
|
}
|
|
|
|
function arraysEqual(a, b) {
|
|
if (a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (a[i] !== b[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function copyTextToClipboard(text) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
} catch (err) {
|
|
console.error('Failed to copy text to clipboard: ', err);
|
|
}
|
|
}
|
|
|
|
async function handleError(errMsg) {
|
|
try {
|
|
console.error('Received error: ', errMsg);
|
|
hideUpdModal();
|
|
const errorDialog = document.getElementById('error-dialog');
|
|
errorDialog.open = true;
|
|
if (socket.readyState === WebSocket.OPEN) {
|
|
socket.close(1000, 'Closing due to error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error while handling error and closing WebSocket:', error);
|
|
}
|
|
}
|
|
|
|
function showUpdModal(operation) {
|
|
const modal = document.getElementById('upd-modal');
|
|
modal.open = true;
|
|
if (operation === 'd') {
|
|
modal.setAttribute('headline', 'Downloading...');
|
|
} else if (operation === 'u') {
|
|
modal.setAttribute('headline', 'Uploading...');
|
|
}
|
|
}
|
|
|
|
function hideUpdModal() {
|
|
const modal = document.getElementById('upd-modal');
|
|
modal.open = false;
|
|
}
|
|
|
|
function waitForHandleReady() {
|
|
return new Promise(resolve => {
|
|
const checkReady = () => {
|
|
if (handleReady) {
|
|
resolve();
|
|
} else {
|
|
setTimeout(checkReady, 10);
|
|
}
|
|
};
|
|
checkReady();
|
|
});
|
|
}
|
|
|
|
const socket = new WebSocket((window.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/file/' + '{{.SessionID}}');
|
|
socket.binaryType = 'arraybuffer';
|
|
|
|
socket.onmessage = async function (event) {
|
|
try {
|
|
const arrayBuffer = event.data;
|
|
|
|
if (isFirstChunk) {
|
|
const identifier = new Uint8Array(arrayBuffer, 0, 4);
|
|
const fileIdentifier = new Uint8Array([0x4E, 0x5A, 0x54, 0x44]); // NZTD
|
|
const fileNameIdentifier = new Uint8Array([0x4E, 0x5A, 0x46, 0x4E]); // NZFN
|
|
const errorIdentifier = new Uint8Array([0x4E, 0x45, 0x52, 0x52]); // NERR
|
|
const completeIdentifier = new Uint8Array([0x4E, 0x5A, 0x55, 0x50]); // NZUP
|
|
|
|
if (arraysEqual(identifier, fileIdentifier)) {
|
|
worker = new Worker('/static/file.js');
|
|
worker.onmessage = async function (event) {
|
|
switch (event.data.type) {
|
|
case 'error':
|
|
console.error('Error from worker:', event.data.error);
|
|
break;
|
|
case 'progress':
|
|
handleReady = true;
|
|
break;
|
|
case 'result':
|
|
handleReady = false;
|
|
const url = URL.createObjectURL(event.data.blob);
|
|
const anchor = document.createElement('a');
|
|
anchor.href = url;
|
|
anchor.download = event.data.fileName;
|
|
anchor.click();
|
|
URL.revokeObjectURL(url);
|
|
|
|
// Delete the file in OPFS
|
|
window.addEventListener('beforeunload', async () => {
|
|
await worker.postMessage({ operation: 3, arrayBuffer: null, fileName: event.data.fileName });
|
|
});
|
|
|
|
hideUpdModal();
|
|
resetUpdState();
|
|
break;
|
|
}
|
|
};
|
|
await worker.postMessage({ operation: 1, arrayBuffer: arrayBuffer, fileName: fileName });
|
|
isFirstChunk = false;
|
|
} else if (arraysEqual(identifier, fileNameIdentifier)) {
|
|
// List files
|
|
const { items } = await parseFileList(arrayBuffer);
|
|
updateFileList(items);
|
|
return;
|
|
} else if (arraysEqual(identifier, errorIdentifier)) {
|
|
// Handle error
|
|
const errBytes = arrayBuffer.slice(4);
|
|
const errMsg = new TextDecoder('utf-8').decode(errBytes);
|
|
await handleError(errMsg);
|
|
return;
|
|
} else if (arraysEqual(identifier, completeIdentifier)) {
|
|
// Upload is completed
|
|
isUpCompleted = true;
|
|
return;
|
|
} else {
|
|
console.log('Unknown identifier');
|
|
return;
|
|
}
|
|
} else {
|
|
await waitForHandleReady();
|
|
await worker.postMessage({ operation: 2, arrayBuffer: arrayBuffer, fileName: fileName });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error processing received data:', error);
|
|
}
|
|
};
|
|
|
|
socket.onopen = function (event) {
|
|
listFile();
|
|
};
|
|
|
|
socket.onerror = function (event) {
|
|
console.error('WebSocket error:', event);
|
|
};
|
|
|
|
socket.onclose = function (event) {
|
|
console.log('WebSocket connection closed:', event);
|
|
};
|
|
|
|
document.getElementById('refresh').addEventListener('click', listFile);
|
|
|
|
document.getElementById('copy').addEventListener('click', async () => {
|
|
await copyTextToClipboard(currentPath);
|
|
});
|
|
|
|
document.getElementById('goto').addEventListener('click', function () {
|
|
const dialog = document.getElementById('goto-dialog');
|
|
const textField = document.getElementById('goto-text');
|
|
const goButton = document.getElementById('goto-go');
|
|
const closeButton = document.getElementById('goto-close');
|
|
|
|
dialog.open = true;
|
|
|
|
// Ensure the path ends with a separator
|
|
const updateText = function (event) {
|
|
let text = event.target.value;
|
|
if (!text.endsWith('/')) {
|
|
text += '/';
|
|
}
|
|
return text;
|
|
};
|
|
|
|
const handleGoClick = function () {
|
|
let text = updateText({ target: textField });
|
|
currentPath = text;
|
|
listFile();
|
|
dialog.open = false;
|
|
};
|
|
|
|
textField.removeEventListener('change', updateText);
|
|
textField.addEventListener('change', updateText);
|
|
|
|
goButton.removeEventListener('click', handleGoClick);
|
|
goButton.addEventListener('click', handleGoClick);
|
|
|
|
closeButton.addEventListener("click", () => dialog.open = false);
|
|
});
|
|
|
|
document.getElementById('upload').addEventListener('click', async function () {
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.style.display = 'none';
|
|
|
|
fileInput.addEventListener('change', async function (event) {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
await uploadFile(file);
|
|
isUpCompleted = false;
|
|
}
|
|
});
|
|
|
|
document.body.appendChild(fileInput);
|
|
fileInput.click();
|
|
document.body.removeChild(fileInput);
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|
|
{{end}} |