|
|
@@ -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>📁 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()">
|
|
|
- 📥 上传文件
|
|
|
- </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 class="btn btn-primary" onclick="refresh()">
|
|
|
🔄 刷新
|
|
|
</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>📁 新建文件夹</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>
|
|
|
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>
|