| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936 |
- 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
- }
|