You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							936 lines
						
					
					
						
							25 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							936 lines
						
					
					
						
							25 KiB
						
					
					
				
								package handlers
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"bytes"
							 | 
						|
									"context"
							 | 
						|
									"fmt"
							 | 
						|
									"io"
							 | 
						|
									"mime/multipart"
							 | 
						|
									"net"
							 | 
						|
									"net/http"
							 | 
						|
									"os"
							 | 
						|
									"path/filepath"
							 | 
						|
									"strconv"
							 | 
						|
									"strings"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/gin-gonic/gin"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/admin/dash"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/glog"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								type FileBrowserHandlers struct {
							 | 
						|
									adminServer *dash.AdminServer
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func NewFileBrowserHandlers(adminServer *dash.AdminServer) *FileBrowserHandlers {
							 | 
						|
									return &FileBrowserHandlers{
							 | 
						|
										adminServer: adminServer,
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ShowFileBrowser renders the file browser page
							 | 
						|
								func (h *FileBrowserHandlers) ShowFileBrowser(c *gin.Context) {
							 | 
						|
									// Get path from query parameter, default to root
							 | 
						|
									path := c.DefaultQuery("path", "/")
							 | 
						|
								
							 | 
						|
									// Get file browser data
							 | 
						|
									browserData, err := h.adminServer.GetFileBrowser(path)
							 | 
						|
									if err != nil {
							 | 
						|
										c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file browser data: " + err.Error()})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Set username
							 | 
						|
									username := c.GetString("username")
							 | 
						|
									if username == "" {
							 | 
						|
										username = "admin"
							 | 
						|
									}
							 | 
						|
									browserData.Username = username
							 | 
						|
								
							 | 
						|
									// Render HTML template
							 | 
						|
									c.Header("Content-Type", "text/html")
							 | 
						|
									browserComponent := app.FileBrowser(*browserData)
							 | 
						|
									layoutComponent := layout.Layout(c, browserComponent)
							 | 
						|
									err = layoutComponent.Render(c.Request.Context(), c.Writer)
							 | 
						|
									if err != nil {
							 | 
						|
										c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// DeleteFile handles file deletion API requests
							 | 
						|
								func (h *FileBrowserHandlers) DeleteFile(c *gin.Context) {
							 | 
						|
									var request struct {
							 | 
						|
										Path string `json:"path" binding:"required"`
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := c.ShouldBindJSON(&request); err != nil {
							 | 
						|
										c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Delete file via filer
							 | 
						|
									err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										_, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
							 | 
						|
											Directory:            filepath.Dir(request.Path),
							 | 
						|
											Name:                 filepath.Base(request.Path),
							 | 
						|
											IsDeleteData:         true,
							 | 
						|
											IsRecursive:          true,
							 | 
						|
											IgnoreRecursiveError: false,
							 | 
						|
										})
							 | 
						|
										return err
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									if err != nil {
							 | 
						|
										c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file: " + err.Error()})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									c.JSON(http.StatusOK, gin.H{"message": "File deleted successfully"})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// DeleteMultipleFiles handles multiple file deletion API requests
							 | 
						|
								func (h *FileBrowserHandlers) DeleteMultipleFiles(c *gin.Context) {
							 | 
						|
									var request struct {
							 | 
						|
										Paths []string `json:"paths" binding:"required"`
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := c.ShouldBindJSON(&request); err != nil {
							 | 
						|
										c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if len(request.Paths) == 0 {
							 | 
						|
										c.JSON(http.StatusBadRequest, gin.H{"error": "No paths provided"})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var deletedCount int
							 | 
						|
									var failedCount int
							 | 
						|
									var errors []string
							 | 
						|
								
							 | 
						|
									// Delete each file/folder
							 | 
						|
									for _, path := range request.Paths {
							 | 
						|
										err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
							 | 
						|
											_, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
							 | 
						|
												Directory:            filepath.Dir(path),
							 | 
						|
												Name:                 filepath.Base(path),
							 | 
						|
												IsDeleteData:         true,
							 | 
						|
												IsRecursive:          true,
							 | 
						|
												IgnoreRecursiveError: false,
							 | 
						|
											})
							 | 
						|
											return err
							 | 
						|
										})
							 | 
						|
								
							 | 
						|
										if err != nil {
							 | 
						|
											failedCount++
							 | 
						|
											errors = append(errors, fmt.Sprintf("%s: %v", path, err))
							 | 
						|
										} else {
							 | 
						|
											deletedCount++
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Prepare response
							 | 
						|
									response := map[string]interface{}{
							 | 
						|
										"deleted": deletedCount,
							 | 
						|
										"failed":  failedCount,
							 | 
						|
										"total":   len(request.Paths),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if len(errors) > 0 {
							 | 
						|
										response["errors"] = errors
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if deletedCount > 0 {
							 | 
						|
										if failedCount == 0 {
							 | 
						|
											response["message"] = fmt.Sprintf("Successfully deleted %d item(s)", deletedCount)
							 | 
						|
										} else {
							 | 
						|
											response["message"] = fmt.Sprintf("Deleted %d item(s), failed to delete %d item(s)", deletedCount, failedCount)
							 | 
						|
										}
							 | 
						|
										c.JSON(http.StatusOK, response)
							 | 
						|
									} else {
							 | 
						|
										response["message"] = "Failed to delete all selected items"
							 | 
						|
										c.JSON(http.StatusInternalServerError, response)
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// CreateFolder handles folder creation requests
							 | 
						|
								func (h *FileBrowserHandlers) CreateFolder(c *gin.Context) {
							 | 
						|
									var request struct {
							 | 
						|
										Path       string `json:"path" binding:"required"`
							 | 
						|
										FolderName string `json:"folder_name" binding:"required"`
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := c.ShouldBindJSON(&request); err != nil {
							 | 
						|
										c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Clean and validate folder name
							 | 
						|
									folderName := strings.TrimSpace(request.FolderName)
							 | 
						|
									if folderName == "" || strings.Contains(folderName, "/") || strings.Contains(folderName, "\\") {
							 | 
						|
										c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid folder name"})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create full path for new folder
							 | 
						|
									fullPath := filepath.Join(request.Path, folderName)
							 | 
						|
									if !strings.HasPrefix(fullPath, "/") {
							 | 
						|
										fullPath = "/" + fullPath
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create folder via filer
							 | 
						|
									err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										_, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
							 | 
						|
											Directory: filepath.Dir(fullPath),
							 | 
						|
											Entry: &filer_pb.Entry{
							 | 
						|
												Name:        filepath.Base(fullPath),
							 | 
						|
												IsDirectory: true,
							 | 
						|
												Attributes: &filer_pb.FuseAttributes{
							 | 
						|
													FileMode: uint32(0755 | os.ModeDir), // Directory mode
							 | 
						|
													Uid:      filer_pb.OS_UID,
							 | 
						|
													Gid:      filer_pb.OS_GID,
							 | 
						|
													Crtime:   time.Now().Unix(),
							 | 
						|
													Mtime:    time.Now().Unix(),
							 | 
						|
													TtlSec:   0,
							 | 
						|
												},
							 | 
						|
											},
							 | 
						|
										})
							 | 
						|
										return err
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									if err != nil {
							 | 
						|
										c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create folder: " + err.Error()})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									c.JSON(http.StatusOK, gin.H{"message": "Folder created successfully"})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// UploadFile handles file upload requests
							 | 
						|
								func (h *FileBrowserHandlers) UploadFile(c *gin.Context) {
							 | 
						|
									// Get the current path
							 | 
						|
									currentPath := c.PostForm("path")
							 | 
						|
									if currentPath == "" {
							 | 
						|
										currentPath = "/"
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Parse multipart form
							 | 
						|
									err := c.Request.ParseMultipartForm(1 << 30) // 1GB max memory for large file uploads
							 | 
						|
									if err != nil {
							 | 
						|
										c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse multipart form: " + err.Error()})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Get uploaded files (supports multiple files)
							 | 
						|
									files := c.Request.MultipartForm.File["files"]
							 | 
						|
									if len(files) == 0 {
							 | 
						|
										c.JSON(http.StatusBadRequest, gin.H{"error": "No files uploaded"})
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var uploadResults []map[string]interface{}
							 | 
						|
									var failedUploads []string
							 | 
						|
								
							 | 
						|
									// Process each uploaded file
							 | 
						|
									for _, fileHeader := range files {
							 | 
						|
										// Validate file name
							 | 
						|
										fileName := fileHeader.Filename
							 | 
						|
										if fileName == "" {
							 | 
						|
											failedUploads = append(failedUploads, "invalid filename")
							 | 
						|
											continue
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Create full path for the file
							 | 
						|
										fullPath := filepath.Join(currentPath, fileName)
							 | 
						|
										if !strings.HasPrefix(fullPath, "/") {
							 | 
						|
											fullPath = "/" + fullPath
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Open the file
							 | 
						|
										file, err := fileHeader.Open()
							 | 
						|
										if err != nil {
							 | 
						|
											failedUploads = append(failedUploads, fmt.Sprintf("%s: %v", fileName, err))
							 | 
						|
											continue
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Upload file to filer
							 | 
						|
										err = h.uploadFileToFiler(fullPath, fileHeader)
							 | 
						|
										file.Close()
							 | 
						|
								
							 | 
						|
										if err != nil {
							 | 
						|
											failedUploads = append(failedUploads, fmt.Sprintf("%s: %v", fileName, err))
							 | 
						|
										} else {
							 | 
						|
											uploadResults = append(uploadResults, map[string]interface{}{
							 | 
						|
												"name": fileName,
							 | 
						|
												"size": fileHeader.Size,
							 | 
						|
												"path": fullPath,
							 | 
						|
											})
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Prepare response
							 | 
						|
									response := map[string]interface{}{
							 | 
						|
										"uploaded": len(uploadResults),
							 | 
						|
										"failed":   len(failedUploads),
							 | 
						|
										"files":    uploadResults,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if len(failedUploads) > 0 {
							 | 
						|
										response["errors"] = failedUploads
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if len(uploadResults) > 0 {
							 | 
						|
										if len(failedUploads) == 0 {
							 | 
						|
											response["message"] = fmt.Sprintf("Successfully uploaded %d file(s)", len(uploadResults))
							 | 
						|
										} else {
							 | 
						|
											response["message"] = fmt.Sprintf("Uploaded %d file(s), %d failed", len(uploadResults), len(failedUploads))
							 | 
						|
										}
							 | 
						|
										c.JSON(http.StatusOK, response)
							 | 
						|
									} else {
							 | 
						|
										response["message"] = "All file uploads failed"
							 | 
						|
										c.JSON(http.StatusInternalServerError, response)
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// uploadFileToFiler uploads a file directly to the filer using multipart form data
							 | 
						|
								func (h *FileBrowserHandlers) uploadFileToFiler(filePath string, fileHeader *multipart.FileHeader) error {
							 | 
						|
									// Get filer address from admin server
							 | 
						|
									filerAddress := h.adminServer.GetFilerAddress()
							 | 
						|
									if filerAddress == "" {
							 | 
						|
										return fmt.Errorf("filer address not configured")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate and sanitize the filer address
							 | 
						|
									if err := h.validateFilerAddress(filerAddress); err != nil {
							 | 
						|
										return fmt.Errorf("invalid filer address: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate and sanitize the file path
							 | 
						|
									cleanFilePath, err := h.validateAndCleanFilePath(filePath)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("invalid file path: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Open the file
							 | 
						|
									file, err := fileHeader.Open()
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to open file: %w", err)
							 | 
						|
									}
							 | 
						|
									defer file.Close()
							 | 
						|
								
							 | 
						|
									// Create multipart form data
							 | 
						|
									var body bytes.Buffer
							 | 
						|
									writer := multipart.NewWriter(&body)
							 | 
						|
								
							 | 
						|
									// Create form file field
							 | 
						|
									part, err := writer.CreateFormFile("file", fileHeader.Filename)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to create form file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Copy file content to form
							 | 
						|
									_, err = io.Copy(part, file)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to copy file content: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Close the writer to finalize the form
							 | 
						|
									err = writer.Close()
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to close multipart writer: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create the upload URL with validated components
							 | 
						|
									uploadURL := fmt.Sprintf("http://%s%s", filerAddress, cleanFilePath)
							 | 
						|
								
							 | 
						|
									// Create HTTP request
							 | 
						|
									req, err := http.NewRequest("POST", uploadURL, &body)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to create request: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Set content type with boundary
							 | 
						|
									req.Header.Set("Content-Type", writer.FormDataContentType())
							 | 
						|
								
							 | 
						|
									// Send request
							 | 
						|
									client := &http.Client{Timeout: 60 * time.Second} // Increased timeout for larger files
							 | 
						|
									resp, err := client.Do(req)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to upload file: %w", err)
							 | 
						|
									}
							 | 
						|
									defer resp.Body.Close()
							 | 
						|
								
							 | 
						|
									// Check response
							 | 
						|
									if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
							 | 
						|
										responseBody, _ := io.ReadAll(resp.Body)
							 | 
						|
										return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(responseBody))
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// validateFilerAddress validates that the filer address is safe to use
							 | 
						|
								func (h *FileBrowserHandlers) validateFilerAddress(address string) error {
							 | 
						|
									if address == "" {
							 | 
						|
										return fmt.Errorf("filer address cannot be empty")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Parse the address to validate it's a proper host:port format
							 | 
						|
									host, port, err := net.SplitHostPort(address)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("invalid address format: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate host is not empty
							 | 
						|
									if host == "" {
							 | 
						|
										return fmt.Errorf("host cannot be empty")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate port is numeric and in valid range
							 | 
						|
									if port == "" {
							 | 
						|
										return fmt.Errorf("port cannot be empty")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									portNum, err := strconv.Atoi(port)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("invalid port number: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if portNum < 1 || portNum > 65535 {
							 | 
						|
										return fmt.Errorf("port number must be between 1 and 65535")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Additional security: prevent private network access unless explicitly allowed
							 | 
						|
									// This helps prevent SSRF attacks to internal services
							 | 
						|
									ip := net.ParseIP(host)
							 | 
						|
									if ip != nil {
							 | 
						|
										// Check for localhost, private networks, and other dangerous addresses
							 | 
						|
										if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() {
							 | 
						|
											// Only allow if it's the configured filer (trusted)
							 | 
						|
											// In production, you might want to be more restrictive
							 | 
						|
											glog.V(2).Infof("Allowing access to private/local address: %s (configured filer)", address)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// validateAndCleanFilePath validates and cleans the file path to prevent path traversal
							 | 
						|
								func (h *FileBrowserHandlers) validateAndCleanFilePath(filePath string) (string, error) {
							 | 
						|
									if filePath == "" {
							 | 
						|
										return "", fmt.Errorf("file path cannot be empty")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Clean the path to remove any .. or . components
							 | 
						|
									cleanPath := filepath.Clean(filePath)
							 | 
						|
								
							 | 
						|
									// Ensure the path starts with /
							 | 
						|
									if !strings.HasPrefix(cleanPath, "/") {
							 | 
						|
										cleanPath = "/" + cleanPath
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Prevent path traversal attacks
							 | 
						|
									if strings.Contains(cleanPath, "..") {
							 | 
						|
										return "", fmt.Errorf("path traversal not allowed")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Additional validation: ensure path doesn't contain dangerous characters
							 | 
						|
									if strings.ContainsAny(cleanPath, "\x00\r\n") {
							 | 
						|
										return "", fmt.Errorf("path contains invalid characters")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									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"] = dash.FormatFileMode(entry.Attributes.FileMode)
							 | 
						|
											properties["file_mode_formatted"] = dash.FormatFileMode(entry.Attributes.FileMode)
							 | 
						|
											properties["file_mode_octal"] = fmt.Sprintf("%o", 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 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
							 | 
						|
								}
							 |