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}} |