feat: simplify HTTP file server to read-only mode (browse + download only)
This commit is contained in:
+3
-240
@@ -3,7 +3,6 @@ package httpfile
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -41,12 +40,7 @@ func (s *Server) Start() error {
|
|||||||
|
|
||||||
// API endpoints
|
// API endpoints
|
||||||
mux.HandleFunc("/api/list", s.apiListDir)
|
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/download", s.apiDownload)
|
||||||
mux.HandleFunc("/api/delete", s.apiDelete)
|
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", s.config.HTTPFile.Host, s.config.HTTPFile.Port)
|
addr := fmt.Sprintf("%s:%d", s.config.HTTPFile.Host, s.config.HTTPFile.Port)
|
||||||
log.Printf("HTTP file server listening on http://%s", addr)
|
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)
|
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 {
|
func (s *Server) toRealPath(httpPath string) string {
|
||||||
rootDir := s.config.HTTPFile.RootDir
|
rootDir := s.config.HTTPFile.RootDir
|
||||||
cleanPath := strings.TrimPrefix(httpPath, "/")
|
cleanPath := strings.TrimPrefix(httpPath, "/")
|
||||||
@@ -313,21 +203,14 @@ const fileBrowserHTML = `<!DOCTYPE html>
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>📁 HTTP 文件服务器</h1>
|
<h1>📁 HTTP 文件服务器</h1>
|
||||||
<p>在线浏览、下载、上传文件</p>
|
<p>在线浏览、下载文件</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="breadcrumb" id="breadcrumb">
|
<div class="breadcrumb" id="breadcrumb">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn btn-primary" onclick="document.getElementById('uploadInput').click()">
|
<button class="btn btn-primary" onclick="refresh()">
|
||||||
📥 上传文件
|
|
||||||
</button>
|
|
||||||
<input type="file" id="uploadInput" multiple style="display:none" onchange="uploadFiles(this.files)">
|
|
||||||
<button class="btn btn-success" onclick="showNewFolderModal()">
|
|
||||||
📁 新建文件夹
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger" onclick="refresh()">
|
|
||||||
🔄 刷新
|
🔄 刷新
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -347,30 +230,6 @@ const fileBrowserHTML = `<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Folder Modal -->
|
|
||||||
<div class="modal-overlay" id="folderModal">
|
|
||||||
<div class="modal">
|
|
||||||
<h2>📁 新建文件夹</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>📥 上传进度</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>
|
<script>
|
||||||
let currentPath = '/';
|
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>') +
|
(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">' +
|
'</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>') +
|
(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);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -439,91 +298,6 @@ function goTo(path) {
|
|||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
loadFiles();
|
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) {
|
function getFileIcon(name) {
|
||||||
@@ -556,17 +330,6 @@ function showToast(msg, type) {
|
|||||||
setTimeout(() => toast.remove(), 3000);
|
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();
|
init();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user