aboutsummaryrefslogtreecommitdiff
path: root/weed/admin/handlers/file_browser_handlers.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/admin/handlers/file_browser_handlers.go')
-rw-r--r--weed/admin/handlers/file_browser_handlers.go494
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
+}