diff options
Diffstat (limited to 'weed/admin/handlers/file_browser_handlers.go')
| -rw-r--r-- | weed/admin/handlers/file_browser_handlers.go | 494 |
1 files changed, 494 insertions, 0 deletions
diff --git a/weed/admin/handlers/file_browser_handlers.go b/weed/admin/handlers/file_browser_handlers.go index 1ddd6781e..97621192e 100644 --- a/weed/admin/handlers/file_browser_handlers.go +++ b/weed/admin/handlers/file_browser_handlers.go @@ -445,3 +445,497 @@ func (h *FileBrowserHandlers) validateAndCleanFilePath(filePath string) (string, return cleanPath, nil } + +// DownloadFile handles file download requests +func (h *FileBrowserHandlers) DownloadFile(c *gin.Context) { + filePath := c.Query("path") + if filePath == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "File path is required"}) + return + } + + // Get filer address + filerAddress := h.adminServer.GetFilerAddress() + if filerAddress == "" { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Filer address not configured"}) + return + } + + // Validate and sanitize the file path + cleanFilePath, err := h.validateAndCleanFilePath(filePath) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file path: " + err.Error()}) + return + } + + // Create the download URL + downloadURL := fmt.Sprintf("http://%s%s", filerAddress, cleanFilePath) + + // Set headers for file download + fileName := filepath.Base(cleanFilePath) + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName)) + c.Header("Content-Type", "application/octet-stream") + + // Proxy the request to filer + c.Redirect(http.StatusFound, downloadURL) +} + +// ViewFile handles file viewing requests (for text files, images, etc.) +func (h *FileBrowserHandlers) ViewFile(c *gin.Context) { + filePath := c.Query("path") + if filePath == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "File path is required"}) + return + } + + // Get file metadata first + var fileEntry dash.FileEntry + err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ + Directory: filepath.Dir(filePath), + Name: filepath.Base(filePath), + }) + if err != nil { + return err + } + + entry := resp.Entry + if entry == nil { + return fmt.Errorf("file not found") + } + + // Convert to FileEntry + var modTime time.Time + if entry.Attributes != nil && entry.Attributes.Mtime > 0 { + modTime = time.Unix(entry.Attributes.Mtime, 0) + } + + var size int64 + if entry.Attributes != nil { + size = int64(entry.Attributes.FileSize) + } + + // Determine MIME type with comprehensive extension support + mime := h.determineMimeType(entry.Name) + + fileEntry = dash.FileEntry{ + Name: entry.Name, + FullPath: filePath, + IsDirectory: entry.IsDirectory, + Size: size, + ModTime: modTime, + Mime: mime, + } + + return nil + }) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file metadata: " + err.Error()}) + return + } + + // Check if file is viewable as text + var content string + var viewable bool + var reason string + + // First check if it's a known text type or if we should check content + isKnownTextType := strings.HasPrefix(fileEntry.Mime, "text/") || + fileEntry.Mime == "application/json" || + fileEntry.Mime == "application/javascript" || + fileEntry.Mime == "application/xml" + + // For unknown types, check if it might be text by content + if !isKnownTextType && fileEntry.Mime == "application/octet-stream" { + isKnownTextType = h.isLikelyTextFile(filePath, 512) + if isKnownTextType { + // Update MIME type for better display + fileEntry.Mime = "text/plain" + } + } + + if isKnownTextType { + // Limit text file size for viewing (max 1MB) + if fileEntry.Size > 1024*1024 { + viewable = false + reason = "File too large for viewing (>1MB)" + } else { + // Get file content from filer + filerAddress := h.adminServer.GetFilerAddress() + if filerAddress != "" { + cleanFilePath, err := h.validateAndCleanFilePath(filePath) + if err == nil { + fileURL := fmt.Sprintf("http://%s%s", filerAddress, cleanFilePath) + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Get(fileURL) + if err == nil && resp.StatusCode == http.StatusOK { + defer resp.Body.Close() + contentBytes, err := io.ReadAll(resp.Body) + if err == nil { + content = string(contentBytes) + viewable = true + } else { + viewable = false + reason = "Failed to read file content" + } + } else { + viewable = false + reason = "Failed to fetch file from filer" + } + } else { + viewable = false + reason = "Invalid file path" + } + } else { + viewable = false + reason = "Filer address not configured" + } + } + } else { + // Not a text file, but might be viewable as image or PDF + if strings.HasPrefix(fileEntry.Mime, "image/") || fileEntry.Mime == "application/pdf" { + viewable = true + } else { + viewable = false + reason = "File type not supported for viewing" + } + } + + c.JSON(http.StatusOK, gin.H{ + "file": fileEntry, + "content": content, + "viewable": viewable, + "reason": reason, + }) +} + +// GetFileProperties handles file properties requests +func (h *FileBrowserHandlers) GetFileProperties(c *gin.Context) { + filePath := c.Query("path") + if filePath == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "File path is required"}) + return + } + + // Get detailed file information from filer + var properties map[string]interface{} + err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ + Directory: filepath.Dir(filePath), + Name: filepath.Base(filePath), + }) + if err != nil { + return err + } + + entry := resp.Entry + if entry == nil { + return fmt.Errorf("file not found") + } + + properties = make(map[string]interface{}) + properties["name"] = entry.Name + properties["full_path"] = filePath + properties["is_directory"] = entry.IsDirectory + + if entry.Attributes != nil { + properties["size"] = entry.Attributes.FileSize + properties["size_formatted"] = h.formatBytes(int64(entry.Attributes.FileSize)) + + if entry.Attributes.Mtime > 0 { + modTime := time.Unix(entry.Attributes.Mtime, 0) + properties["modified_time"] = modTime.Format("2006-01-02 15:04:05") + properties["modified_timestamp"] = entry.Attributes.Mtime + } + + if entry.Attributes.Crtime > 0 { + createTime := time.Unix(entry.Attributes.Crtime, 0) + properties["created_time"] = createTime.Format("2006-01-02 15:04:05") + properties["created_timestamp"] = entry.Attributes.Crtime + } + + properties["file_mode"] = fmt.Sprintf("%o", entry.Attributes.FileMode) + properties["file_mode_formatted"] = h.formatFileMode(entry.Attributes.FileMode) + properties["uid"] = entry.Attributes.Uid + properties["gid"] = entry.Attributes.Gid + properties["ttl_seconds"] = entry.Attributes.TtlSec + + if entry.Attributes.TtlSec > 0 { + properties["ttl_formatted"] = fmt.Sprintf("%d seconds", entry.Attributes.TtlSec) + } + } + + // Get extended attributes + if entry.Extended != nil { + extended := make(map[string]string) + for key, value := range entry.Extended { + extended[key] = string(value) + } + properties["extended"] = extended + } + + // Get chunk information for files + if !entry.IsDirectory && len(entry.Chunks) > 0 { + chunks := make([]map[string]interface{}, 0, len(entry.Chunks)) + for _, chunk := range entry.Chunks { + chunkInfo := map[string]interface{}{ + "file_id": chunk.FileId, + "offset": chunk.Offset, + "size": chunk.Size, + "modified_ts": chunk.ModifiedTsNs, + "e_tag": chunk.ETag, + "source_fid": chunk.SourceFileId, + } + chunks = append(chunks, chunkInfo) + } + properties["chunks"] = chunks + properties["chunk_count"] = len(entry.Chunks) + } + + // Determine MIME type + if !entry.IsDirectory { + mime := h.determineMimeType(entry.Name) + properties["mime_type"] = mime + } + + return nil + }) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file properties: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, properties) +} + +// Helper function to format bytes +func (h *FileBrowserHandlers) formatBytes(bytes int64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := int64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} + +// Helper function to format file mode +func (h *FileBrowserHandlers) formatFileMode(mode uint32) string { + // Convert to octal and format as rwx permissions + perm := mode & 0777 + return fmt.Sprintf("%03o", perm) +} + +// Helper function to determine MIME type from filename +func (h *FileBrowserHandlers) determineMimeType(filename string) string { + ext := strings.ToLower(filepath.Ext(filename)) + + // Text files + switch ext { + case ".txt", ".log", ".cfg", ".conf", ".ini", ".properties": + return "text/plain" + case ".md", ".markdown": + return "text/markdown" + case ".html", ".htm": + return "text/html" + case ".css": + return "text/css" + case ".js", ".mjs": + return "application/javascript" + case ".ts": + return "text/typescript" + case ".json": + return "application/json" + case ".xml": + return "application/xml" + case ".yaml", ".yml": + return "text/yaml" + case ".csv": + return "text/csv" + case ".sql": + return "text/sql" + case ".sh", ".bash", ".zsh", ".fish": + return "text/x-shellscript" + case ".py": + return "text/x-python" + case ".go": + return "text/x-go" + case ".java": + return "text/x-java" + case ".c": + return "text/x-c" + case ".cpp", ".cc", ".cxx", ".c++": + return "text/x-c++" + case ".h", ".hpp": + return "text/x-c-header" + case ".php": + return "text/x-php" + case ".rb": + return "text/x-ruby" + case ".pl": + return "text/x-perl" + case ".rs": + return "text/x-rust" + case ".swift": + return "text/x-swift" + case ".kt": + return "text/x-kotlin" + case ".scala": + return "text/x-scala" + case ".dockerfile": + return "text/x-dockerfile" + case ".gitignore", ".gitattributes": + return "text/plain" + case ".env": + return "text/plain" + + // Image files + case ".jpg", ".jpeg": + return "image/jpeg" + case ".png": + return "image/png" + case ".gif": + return "image/gif" + case ".bmp": + return "image/bmp" + case ".webp": + return "image/webp" + case ".svg": + return "image/svg+xml" + case ".ico": + return "image/x-icon" + + // Document files + case ".pdf": + return "application/pdf" + case ".doc": + return "application/msword" + case ".docx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + case ".xls": + return "application/vnd.ms-excel" + case ".xlsx": + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + case ".ppt": + return "application/vnd.ms-powerpoint" + case ".pptx": + return "application/vnd.openxmlformats-officedocument.presentationml.presentation" + + // Archive files + case ".zip": + return "application/zip" + case ".tar": + return "application/x-tar" + case ".gz": + return "application/gzip" + case ".bz2": + return "application/x-bzip2" + case ".7z": + return "application/x-7z-compressed" + case ".rar": + return "application/x-rar-compressed" + + // Video files + case ".mp4": + return "video/mp4" + case ".avi": + return "video/x-msvideo" + case ".mov": + return "video/quicktime" + case ".wmv": + return "video/x-ms-wmv" + case ".flv": + return "video/x-flv" + case ".webm": + return "video/webm" + + // Audio files + case ".mp3": + return "audio/mpeg" + case ".wav": + return "audio/wav" + case ".flac": + return "audio/flac" + case ".aac": + return "audio/aac" + case ".ogg": + return "audio/ogg" + + default: + // For files without extension or unknown extensions, + // we'll check if they might be text files by content + return "application/octet-stream" + } +} + +// Helper function to check if a file is likely a text file by checking content +func (h *FileBrowserHandlers) isLikelyTextFile(filePath string, maxCheckSize int64) bool { + filerAddress := h.adminServer.GetFilerAddress() + if filerAddress == "" { + return false + } + + cleanFilePath, err := h.validateAndCleanFilePath(filePath) + if err != nil { + return false + } + + fileURL := fmt.Sprintf("http://%s%s", filerAddress, cleanFilePath) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(fileURL) + if err != nil || resp.StatusCode != http.StatusOK { + return false + } + defer resp.Body.Close() + + // Read first few bytes to check if it's text + buffer := make([]byte, min(maxCheckSize, 512)) + n, err := resp.Body.Read(buffer) + if err != nil && err != io.EOF { + return false + } + + if n == 0 { + return true // Empty file can be considered text + } + + // Check if content is printable text + return h.isPrintableText(buffer[:n]) +} + +// Helper function to check if content is printable text +func (h *FileBrowserHandlers) isPrintableText(data []byte) bool { + if len(data) == 0 { + return true + } + + // Count printable characters + printable := 0 + for _, b := range data { + if b >= 32 && b <= 126 || b == 9 || b == 10 || b == 13 { + // Printable ASCII, tab, newline, carriage return + printable++ + } else if b >= 128 { + // Potential UTF-8 character + printable++ + } + } + + // If more than 95% of characters are printable, consider it text + return float64(printable)/float64(len(data)) > 0.95 +} + +// Helper function for min +func min(a, b int64) int64 { + if a < b { + return a + } + return b +} |
