需要简单的权限认证htpasswd工具

sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd {用户名}

配置的具体内容(我的文件存放在var/www/files中,若需更改则修改配置中的文件路径):

server {
    listen 80;
    server_name 你的ip;

    client_max_body_size 200M;

    client_body_timeout 200s;
    proxy_connect_timeout 200s;
    proxy_send_timeout 200s;
    proxy_read_timeout 200s;

    location /file-manager/ {
        alias /var/www/html/file-manager/;
        index index.html;
        auth_basic "Restricted Access";
        auth_basic_user_file /etc/nginx/.htpasswd;
    }

    # 文件列表(需要认证)
    location /files-list/ {
        alias /var/www/files/;
        autoindex on;
        autoindex_exact_size off;
        autoindex_localtime on;

        auth_basic "Restricted Access";
        auth_basic_user_file /etc/nginx/.htpasswd;

        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods 'GET, OPTIONS';
        add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
    }

    # 处理无需认证的 GET 请求
    location /files-get/ {
        alias /var/www/files/;

        # 只允许 GET HEAD OPTIONS 方法
        limit_except GET HEAD OPTIONS {
            deny all;
        }

        add_header Access-Control-Allow-Origin *;

        # 默认设置
        set $disposition "";

        # 如果是图片或视频文件,并且不是强制下载,则设置为内联显示
        if ($request_filename ~* \.(jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm)$) {
            set $disposition "inline";
        }

        # 如果请求包含 "download" 参数,强制下载
        if ($args ~* "download") {
            set $disposition "attachment";
        }

        add_header Content-Disposition $disposition;
    }

    # 需要认证的文件操作(上传、删除、创建文件夹等)
    location /files/ {
        alias /var/www/files/;
        dav_methods PUT DELETE MKCOL;
        create_full_put_path on;
        dav_access user:rw group:rw all:r;

        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/.htpasswd;
    }
}

file-manager是一个简单的html+js前端,调用Nginx中的几个api实现一个极其轻量级的文件管理工具

File Manager代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Manager</title>
    <style>
        .file-item {
            margin-bottom: 10px;
            display: flex;
            align-items: center;
        }
        .file-item button { margin-left: 10px; }
        .directory { cursor: pointer; color: blue; }
        .file { cursor: pointer; color: green; }
        .file-name {
            max-width: 300px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .file-info {
            margin-left: 20px;
            font-size: 0.8em;
            color: #666;
        }
        #upload-progress {
            margin-top: 10px;
            width: 100%;
        }
        .progress-item {
            margin-bottom: 5px;
            border: 1px solid #ddd;
            padding: 5px;
            border-radius: 4px;
        }
        .progress-bar {
            height: 15px;
            background-color: #4CAF50;
            width: 0%;
            border-radius: 2px;
        }
        .upload-container {
            margin-bottom: 20px;
            padding: 15px;
            background-color: #f8f8f8;
            border-radius: 5px;
            border: 1px solid #ddd;
        }
        #drag-area {
            border: 2px dashed #ccc;
            border-radius: 5px;
            padding: 30px;
            text-align: center;
            margin: 10px 0;
            background-color: #fafafa;
            cursor: pointer;
        }
        #drag-area.dragover {
            border-color: #4CAF50;
            background-color: rgba(76, 175, 80, 0.1);
        }
        /* 添加复制通知样式 */
        .copy-notification {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border-radius: 4px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
            display: none;
            z-index: 1000;
        }
        /* 隐藏的复制文本域 */
        #copyTextarea {
            position: absolute;
            left: -9999px;
            top: 0;
        }
    </style>
</head>
<body>
    <h1>File Manager</h1>

    <h2>Current Path: <span id="currentPath">/</span></h2>
    <button onclick="navigateUp()">Go Up</button>
    <div id="fileList"></div>

    <div class="upload-container">
        <h2>Upload Files</h2>
        <div id="drag-area">
            <p>拖放文件到这里或点击选择文件</p>
            <input type="file" id="fileInput" multiple style="display: none;">
        </div>
        <button onclick="document.getElementById('fileInput').click()">选择文件</button>
        <button onclick="uploadSelectedFiles()">上传所选文件</button>
        <div id="selected-files"></div>
        <div id="upload-progress"></div>
    </div>

    <h2>Create Folder</h2>
    <input type="text" id="folderName" placeholder="New folder name">
    <button onclick="createFolder()">Create Folder</button>

    <!-- 添加复制通知 -->
    <div id="copyNotification" class="copy-notification">路径已复制到剪贴板</div>
    
    <!-- 添加隐藏文本域用于复制 -->
    <textarea id="copyTextarea" readonly></textarea>

    <script>
        let currentPath = '/';
        let selectedFiles = [];
        // 获取当前服务器地址
        const serverAddress = window.location.hostname;
        const serverPort = window.location.port ? ':' + window.location.port : '';
        const serverProtocol = window.location.protocol;

        // 显示复制通知
        function showCopyNotification() {
            const notification = document.getElementById('copyNotification');
            notification.style.display = 'block';
            
            setTimeout(() => {
                notification.style.display = 'none';
            }, 2000);
        }

        // 复制文件路径
        function copyFilePath(fileName, isDirectory) {
            // 构建路径
            const path = `${currentPath}${fileName}`.replace(/^\//, '');
            // 完整URL路径,无论是文件还是文件夹都使用 files-get
            const fullUrl = `${serverProtocol}//${serverAddress}${serverPort}/files-get/${path}`;
            
            // 使用隐藏的文本域复制文本
            const textarea = document.getElementById('copyTextarea');
            textarea.value = fullUrl;
            textarea.style.display = 'block';
            textarea.select();
            
            try {
                const successful = document.execCommand('copy');
                if (successful) {
                    showCopyNotification();
                } else {
                    alert('复制失败,请手动复制:' + fullUrl);
                }
            } catch (err) {
                console.error('复制文本时出错:', err);
                alert('复制失败,请手动复制:' + fullUrl);
            }
            
            textarea.style.display = 'none';
        }

        // 列出文件和目录
        async function listFiles(path = '/') {
            try {
                const response = await fetch(`/files-list${path}`);
                const html = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');
                const items = Array.from(doc.querySelectorAll('pre a')).slice(1); // 跳过第一个 "../" 链接

                const fileList = document.getElementById('fileList');
                fileList.innerHTML = '';
                items.forEach(item => {
                    const div = document.createElement('div');
                    div.className = 'file-item';
                    const isDirectory = item.href.endsWith('/');

                    // 提取完整文件名
                    const fullFileName = item.getAttribute('href');
                    const fileName = decodeURIComponent(fullFileName.replace(/\/$/, '')); // 移除末尾的斜杠并解码 URL

                    // 获取文件信息
                    const fileInfoText = item.nextSibling ? item.nextSibling.textContent.trim() : '';
                    const fileInfo = fileInfoText.split(/\s+/);
                    const fileDate = fileInfo.length > 1 ? fileInfo.slice(-4, -2).join(' ') : '';
                    const fileSize = fileInfo.length > 2 ? fileInfo[fileInfo.length - 1] : '';

                    // 创建文件名显示元素
                    const nameSpan = document.createElement('span');
                    nameSpan.className = isDirectory ? 'directory file-name' : 'file file-name';
                    nameSpan.textContent = fileName;
                    nameSpan.title = fileName; // 添加完整文件名作为提示

                    if (isDirectory) {
                        nameSpan.onclick = () => navigateTo(fileName);
                    } else {
                        nameSpan.onclick = () => getFile(fileName);
                    }

                    div.appendChild(nameSpan);

                    // 添加文件信息
                    const infoSpan = document.createElement('span');
                    infoSpan.className = 'file-info';
                    infoSpan.textContent = `${fileDate} ${fileSize}`.trim();
                    div.appendChild(infoSpan);

                    // 添加复制路径按钮
                    const copyButton = document.createElement('button');
                    copyButton.textContent = '复制路径';
                    copyButton.onclick = (e) => {
                        e.stopPropagation();
                        copyFilePath(fileName, isDirectory);
                    };
                    div.appendChild(copyButton);

                    // 添加删除按钮
                    const deleteButton = document.createElement('button');
                    deleteButton.textContent = isDirectory ? 'Delete Folder' : 'Delete File';
                    deleteButton.onclick = (e) => {
                        e.stopPropagation();
                        if (isDirectory) {
                            deleteFolder(fileName);
                        } else {
                            deleteFile(fileName);
                        }
                    };
                    div.appendChild(deleteButton);

                    fileList.appendChild(div);
                });

                document.getElementById('currentPath').textContent = path;
                currentPath = path;
            } catch (error) {
                console.error('Error listing files:', error);
            }
        }

        // 获取/下载文件
        function getFile(fileName) {
            const filePath = `${currentPath}${fileName}`.replace(/^\//, '');
            const fileUrl = `/files-get/${filePath}`;
            window.open(fileUrl, '_blank');
        }

        // 导航到子目录
        function navigateTo(dir) {
            currentPath = `${currentPath}${dir}/`.replace(/\/+/g, '/');
            listFiles(currentPath);
        }

        // 返回上一级目录
        function navigateUp() {
            if (currentPath === '/') return;
            const pathParts = currentPath.split('/').filter(Boolean);
            pathParts.pop();
            currentPath = '/' + pathParts.join('/') + '/';
            listFiles(currentPath);
        }

        // 处理文件选择
        function handleFileSelect(files) {
            selectedFiles = Array.from(files);
            
            // 显示选择的文件
            const selectedFilesContainer = document.getElementById('selected-files');
            selectedFilesContainer.innerHTML = '<h3>已选择的文件:</h3>';
            
            const fileList = document.createElement('ul');
            selectedFiles.forEach(file => {
                const item = document.createElement('li');
                item.textContent = `${file.name} (${formatFileSize(file.size)})`;
                fileList.appendChild(item);
            });
            
            selectedFilesContainer.appendChild(fileList);
        }

        // 格式化文件大小
        function formatFileSize(bytes) {
            if (bytes === 0) return '0 Bytes';
            const k = 1024;
            const sizes = ['Bytes', 'KB', 'MB', 'GB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
        }

        // 上传选择的文件
        async function uploadSelectedFiles() {
            if (selectedFiles.length === 0) {
                alert('请先选择文件');
                return;
            }

            const progressContainer = document.getElementById('upload-progress');
            progressContainer.innerHTML = '';
            
            const uploadPromises = selectedFiles.map(async (file) => {
                // 为每个文件创建进度条
                const progressItem = document.createElement('div');
                progressItem.className = 'progress-item';
                
                const fileNameElement = document.createElement('div');
                fileNameElement.textContent = file.name;
                
                const progressBarContainer = document.createElement('div');
                progressBarContainer.style.width = '100%';
                progressBarContainer.style.backgroundColor = '#f0f0f0';
                progressBarContainer.style.marginTop = '5px';
                
                const progressBar = document.createElement('div');
                progressBar.className = 'progress-bar';
                
                const progressText = document.createElement('div');
                progressText.textContent = '0%';
                
                progressBarContainer.appendChild(progressBar);
                progressItem.appendChild(fileNameElement);
                progressItem.appendChild(progressBarContainer);
                progressItem.appendChild(progressText);
                progressContainer.appendChild(progressItem);
                
                // 使用 XMLHttpRequest 来获取上传进度
                return new Promise((resolve, reject) => {
                    const xhr = new XMLHttpRequest();
                    
                    xhr.upload.addEventListener('progress', (event) => {
                        if (event.lengthComputable) {
                            const percentComplete = Math.round((event.loaded / event.total) * 100);
                            progressBar.style.width = percentComplete + '%';
                            progressText.textContent = percentComplete + '%';
                        }
                    });
                    
                    xhr.addEventListener('load', () => {
                        if (xhr.status >= 200 && xhr.status < 300) {
                            progressText.textContent = '完成';
                            resolve();
                        } else {
                            progressText.textContent = '失败: ' + xhr.statusText;
                            reject(new Error(xhr.statusText));
                        }
                    });
                    
                    xhr.addEventListener('error', () => {
                        progressText.textContent = '上传错误';
                        reject(new Error('Network Error'));
                    });
                    
                    xhr.open('PUT', `/files${currentPath}${file.name}`);
                    xhr.send(file);
                });
            });
            
            try {
                await Promise.all(uploadPromises);
                
                // 清空选择的文件
                selectedFiles = [];
                document.getElementById('selected-files').innerHTML = '';
                document.getElementById('fileInput').value = '';
                
                // 刷新文件列表
                listFiles(currentPath);
                
                setTimeout(() => {
                    alert('所有文件上传完成');
                }, 500);
            } catch (error) {
                console.error('上传过程中发生错误:', error);
            }
        }

        // 删除文件
        async function deleteFile(fileName) {
            if (confirm(`确定要删除文件 "${fileName}" 吗?`)) {
                try {
                    const response = await fetch(`/files${currentPath}${fileName}`, {
                        method: 'DELETE'
                    });

                    if (response.ok) {
                        alert('文件删除成功');
                        listFiles(currentPath);
                    } else {
                        alert('文件删除失败');
                    }
                } catch (error) {
                    console.error('删除文件时出错:', error);
                }
            }
        }

        // 创建文件夹
        async function createFolder() {
            const folderName = document.getElementById('folderName').value;
            if (!folderName) {
                alert('请输入文件夹名称');
                return;
            }

            try {
                const response = await fetch(`/files${currentPath}${folderName}/`, {
                    method: 'MKCOL'
                });

                if (response.ok) {
                    alert('文件夹创建成功');
                    listFiles(currentPath);
                    document.getElementById('folderName').value = '';
                } else {
                    alert('文件夹创建失败');
                }
            } catch (error) {
                console.error('创建文件夹时出错:', error);
            }
        }

        // 删除文件夹
        async function deleteFolder(folderName) {
            if (confirm(`确定要删除文件夹 "${folderName}" 及其所有内容吗?`)) {
                try {
                    const response = await fetch(`/files${currentPath}${folderName}/`, {
                        method: 'DELETE'
                    });

                    if (response.ok) {
                        alert('文件夹删除成功');
                        listFiles(currentPath);
                    } else {
                        alert('文件夹删除失败');
                    }
                } catch (error) {
                    console.error('删除文件夹时出错:', error);
                }
            }
        }

        // 设置拖放区域
        const dragArea = document.getElementById('drag-area');
        const fileInput = document.getElementById('fileInput');

        dragArea.addEventListener('click', () => {
            fileInput.click();
        });

        fileInput.addEventListener('change', (event) => {
            handleFileSelect(event.target.files);
        });

        dragArea.addEventListener('dragover', (event) => {
            event.preventDefault();
            dragArea.classList.add('dragover');
        });

        dragArea.addEventListener('dragleave', () => {
            dragArea.classList.remove('dragover');
        });

        dragArea.addEventListener('drop', (event) => {
            event.preventDefault();
            dragArea.classList.remove('dragover');
            handleFileSelect(event.dataTransfer.files);
        });

        // 初始化
        listFiles();
    </script>
</body>
</html>