feat: simplify HTTP file server to read-only mode (browse + download only)

This commit is contained in:
Your Name
2026-04-28 22:18:05 +08:00
parent 8cfa25f0f3
commit c7319a4686
2 changed files with 3 additions and 240 deletions
View File
+3 -240
View File
@@ -3,7 +3,6 @@ package httpfile
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
@@ -41,12 +40,7 @@ func (s *Server) Start() error {
// API endpoints
mux.HandleFunc("/api/list", s.apiListDir)
if s.config.HTTPFile.Upload {
mux.HandleFunc("/api/upload", s.apiUpload)
mux.HandleFunc("/api/create-folder", s.apiCreateFolder)
}
mux.HandleFunc("/api/download", s.apiDownload)
mux.HandleFunc("/api/delete", s.apiDelete)
addr := fmt.Sprintf("%s:%d", s.config.HTTPFile.Host, s.config.HTTPFile.Port)
log.Printf("HTTP file server listening on http://%s", addr)
@@ -121,110 +115,6 @@ func (s *Server) apiDownload(w http.ResponseWriter, r *http.Request) {
log.Printf("HTTP download: %s from %s", file, r.RemoteAddr)
}
func (s *Server) apiUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse multipart form (max 500MB)
err := r.ParseMultipartForm(500 << 20)
if err != nil {
http.Error(w, `{"error":"Failed to parse upload"}`, http.StatusBadRequest)
return
}
dir := r.FormValue("path")
if dir == "" {
dir = "/"
}
files := r.MultipartForm.File["files"]
if len(files) == 0 {
http.Error(w, `{"error":"No files uploaded"}`, http.StatusBadRequest)
return
}
realDir := s.toRealPath(dir)
for _, fileHeader := range files {
file, err := fileHeader.Open()
if err != nil {
continue
}
destPath := filepath.Join(realDir, fileHeader.Filename)
destFile, err := os.Create(destPath)
if err != nil {
file.Close()
continue
}
io.Copy(destFile, file)
destFile.Close()
file.Close()
log.Printf("HTTP upload: %s (%d bytes) from %s", fileHeader.Filename, fileHeader.Size, r.RemoteAddr)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func (s *Server) apiCreateFolder(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var req struct {
Path string `json:"path"`
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
return
}
if req.Name == "" {
http.Error(w, `{"error":"Folder name required"}`, http.StatusBadRequest)
return
}
realPath := filepath.Join(s.toRealPath(req.Path), req.Name)
if err := os.MkdirAll(realPath, 0755); err != nil {
http.Error(w, `{"error":"Failed to create folder"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func (s *Server) apiDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, `{"error":"Method not allowed"}`, http.StatusMethodNotAllowed)
return
}
var req struct {
Path string `json:"path"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"Invalid request"}`, http.StatusBadRequest)
return
}
realPath := s.toRealPath(req.Path)
if err := os.RemoveAll(realPath); err != nil {
http.Error(w, `{"error":"Delete failed"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func (s *Server) toRealPath(httpPath string) string {
rootDir := s.config.HTTPFile.RootDir
cleanPath := strings.TrimPrefix(httpPath, "/")
@@ -313,21 +203,14 @@ const fileBrowserHTML = `<!DOCTYPE html>
<div class="container">
<div class="header">
<h1>&#128193; HTTP 文件服务器</h1>
<p>在线浏览、下载、上传文件</p>
<p>在线浏览、下载文件</p>
</div>
<div class="breadcrumb" id="breadcrumb">
</div>
<div class="actions">
<button class="btn btn-primary" onclick="document.getElementById('uploadInput').click()">
&#128229; 上传文件
</button>
<input type="file" id="uploadInput" multiple style="display:none" onchange="uploadFiles(this.files)">
<button class="btn btn-success" onclick="showNewFolderModal()">
&#128193; 新建文件夹
</button>
<button class="btn btn-danger" onclick="refresh()">
<button class="btn btn-primary" onclick="refresh()">
&#128260; 刷新
</button>
</div>
@@ -347,30 +230,6 @@ const fileBrowserHTML = `<!DOCTYPE html>
</div>
</div>
<!-- New Folder Modal -->
<div class="modal-overlay" id="folderModal">
<div class="modal">
<h2>&#128193; 新建文件夹</h2>
<div class="form-group">
<label>文件夹名称</label>
<input type="text" id="folderName" placeholder="输入文件夹名称" onkeypress="if(event.key==='Enter')createFolder()">
</div>
<div class="modal-actions">
<button class="btn" style="background:#eee" onclick="closeFolderModal()">取消</button>
<button class="btn btn-primary" onclick="createFolder()">创建</button>
</div>
</div>
</div>
<!-- Upload Progress -->
<div class="modal-overlay" id="uploadModal">
<div class="modal">
<h2>&#128229; 上传进度</h2>
<div id="uploadStatus" style="margin-bottom:12px; font-size:14px; color:#666;"></div>
<div class="progress-bar show"><div class="progress-fill" id="progressFill"></div></div>
</div>
</div>
<script>
let currentPath = '/';
@@ -403,7 +262,7 @@ function loadFiles() {
(f.isDir ? '<a class="file-link" href="#" onclick="enterFolder(\\'' + escapeStr(f.name) + '\\');return false;">' + f.name + '</a>' : '<span>' + f.name + '</span>') +
'</div></td><td>' + (f.isDir ? '-' : f.size) + '</td><td>' + f.modTime + '</td><td class="actions-cell">' +
(f.isDir ? '<button class="btn btn-sm btn-primary" onclick="enterFolder(\\'' + escapeStr(f.name) + '\\')">打开</button>' : '<a href="/api/download?file=' + encodeURIComponent(currentPath + f.name) + '" class="btn btn-sm btn-success">下载</a>') +
'<button class="btn btn-sm btn-danger" onclick="deleteItem(\\'' + escapeStr(f.name) + '\\', ' + f.isDir + ')">删除</button></td></tr>';
'</td></tr>';
tbody.appendChild(tr);
});
@@ -439,91 +298,6 @@ function goTo(path) {
function refresh() {
loadFiles();
showToast('已刷新');
}
function showNewFolderModal() {
document.getElementById('folderName').value = '';
document.getElementById('folderModal').classList.add('show');
document.getElementById('folderName').focus();
}
function closeFolderModal() {
document.getElementById('folderModal').classList.remove('show');
}
function createFolder() {
const name = document.getElementById('folderName').value.trim();
if (!name) { showToast('请输入文件夹名称', 'error'); return; }
fetch('/api/create-folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentPath, name: name })
}).then(r => r.json()).then(data => {
if (data.status === 'ok') {
showToast('文件夹已创建');
closeFolderModal();
loadFiles();
} else {
showToast(data.error || '创建失败', 'error');
}
});
}
function uploadFiles(files) {
if (!files || files.length === 0) return;
document.getElementById('uploadModal').classList.add('show');
document.getElementById('uploadStatus').textContent = '正在上传 ' + files.length + ' 个文件...';
document.getElementById('progressFill').style.width = '0%';
const formData = new FormData();
formData.append('path', currentPath);
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total * 100).toFixed(1);
document.getElementById('progressFill').style.width = percent + '%';
}
};
xhr.onload = function() {
setTimeout(() => {
document.getElementById('uploadModal').classList.remove('show');
if (xhr.status === 200) {
showToast('上传成功');
loadFiles();
} else {
showToast('上传失败', 'error');
}
}, 500);
};
xhr.send(formData);
document.getElementById('uploadInput').value = '';
}
function deleteItem(name, isDir) {
const confirmMsg = isDir ? '确定要删除文件夹 "' + name + '" 及其所有内容吗?' : '确定要删除文件 "' + name + '" 吗?';
if (!confirm(confirmMsg)) return;
fetch('/api/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentPath + name })
}).then(r => r.json()).then(data => {
if (data.status === 'ok') {
showToast('已删除');
loadFiles();
} else {
showToast(data.error || '删除失败', 'error');
}
});
}
function getFileIcon(name) {
@@ -556,17 +330,6 @@ function showToast(msg, type) {
setTimeout(() => toast.remove(), 3000);
}
// Drag and drop support
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('dragover', function(e) { e.preventDefault(); });
document.addEventListener('drop', function(e) {
e.preventDefault();
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
uploadFiles(e.dataTransfer.files);
}
});
});
init();
</script>
</body>