file_browser_data.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package dash
  2. import (
  3. "context"
  4. "path/filepath"
  5. "sort"
  6. "strings"
  7. "time"
  8. "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
  9. )
  10. // FileEntry represents a file or directory entry in the file browser
  11. type FileEntry struct {
  12. Name string `json:"name"`
  13. FullPath string `json:"full_path"`
  14. IsDirectory bool `json:"is_directory"`
  15. Size int64 `json:"size"`
  16. ModTime time.Time `json:"mod_time"`
  17. Mode string `json:"mode"`
  18. Uid uint32 `json:"uid"`
  19. Gid uint32 `json:"gid"`
  20. Mime string `json:"mime"`
  21. Replication string `json:"replication"`
  22. Collection string `json:"collection"`
  23. TtlSec int32 `json:"ttl_sec"`
  24. }
  25. // BreadcrumbItem represents a single breadcrumb in the navigation
  26. type BreadcrumbItem struct {
  27. Name string `json:"name"`
  28. Path string `json:"path"`
  29. }
  30. // FileBrowserData contains all data needed for the file browser view
  31. type FileBrowserData struct {
  32. Username string `json:"username"`
  33. CurrentPath string `json:"current_path"`
  34. ParentPath string `json:"parent_path"`
  35. Breadcrumbs []BreadcrumbItem `json:"breadcrumbs"`
  36. Entries []FileEntry `json:"entries"`
  37. TotalEntries int `json:"total_entries"`
  38. TotalSize int64 `json:"total_size"`
  39. LastUpdated time.Time `json:"last_updated"`
  40. IsBucketPath bool `json:"is_bucket_path"`
  41. BucketName string `json:"bucket_name"`
  42. }
  43. // GetFileBrowser retrieves file browser data for a given path
  44. func (s *AdminServer) GetFileBrowser(path string) (*FileBrowserData, error) {
  45. if path == "" {
  46. path = "/"
  47. }
  48. var entries []FileEntry
  49. var totalSize int64
  50. // Get directory listing from filer
  51. err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
  52. stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
  53. Directory: path,
  54. Prefix: "",
  55. Limit: 1000,
  56. InclusiveStartFrom: false,
  57. })
  58. if err != nil {
  59. return err
  60. }
  61. for {
  62. resp, err := stream.Recv()
  63. if err != nil {
  64. if err.Error() == "EOF" {
  65. break
  66. }
  67. return err
  68. }
  69. entry := resp.Entry
  70. if entry == nil {
  71. continue
  72. }
  73. fullPath := path
  74. if !strings.HasSuffix(fullPath, "/") {
  75. fullPath += "/"
  76. }
  77. fullPath += entry.Name
  78. var modTime time.Time
  79. if entry.Attributes != nil && entry.Attributes.Mtime > 0 {
  80. modTime = time.Unix(entry.Attributes.Mtime, 0)
  81. }
  82. var mode string
  83. var uid, gid uint32
  84. var size int64
  85. var replication, collection string
  86. var ttlSec int32
  87. if entry.Attributes != nil {
  88. mode = FormatFileMode(entry.Attributes.FileMode)
  89. uid = entry.Attributes.Uid
  90. gid = entry.Attributes.Gid
  91. size = int64(entry.Attributes.FileSize)
  92. ttlSec = entry.Attributes.TtlSec
  93. }
  94. // Get replication and collection from entry extended attributes or chunks
  95. if entry.Extended != nil {
  96. if repl, ok := entry.Extended["replication"]; ok {
  97. replication = string(repl)
  98. }
  99. if coll, ok := entry.Extended["collection"]; ok {
  100. collection = string(coll)
  101. }
  102. }
  103. // Determine MIME type based on file extension
  104. mime := "application/octet-stream"
  105. if entry.IsDirectory {
  106. mime = "inode/directory"
  107. } else {
  108. ext := strings.ToLower(filepath.Ext(entry.Name))
  109. switch ext {
  110. case ".txt", ".log":
  111. mime = "text/plain"
  112. case ".html", ".htm":
  113. mime = "text/html"
  114. case ".css":
  115. mime = "text/css"
  116. case ".js":
  117. mime = "application/javascript"
  118. case ".json":
  119. mime = "application/json"
  120. case ".xml":
  121. mime = "application/xml"
  122. case ".pdf":
  123. mime = "application/pdf"
  124. case ".jpg", ".jpeg":
  125. mime = "image/jpeg"
  126. case ".png":
  127. mime = "image/png"
  128. case ".gif":
  129. mime = "image/gif"
  130. case ".svg":
  131. mime = "image/svg+xml"
  132. case ".mp4":
  133. mime = "video/mp4"
  134. case ".mp3":
  135. mime = "audio/mpeg"
  136. case ".zip":
  137. mime = "application/zip"
  138. case ".tar":
  139. mime = "application/x-tar"
  140. case ".gz":
  141. mime = "application/gzip"
  142. }
  143. }
  144. fileEntry := FileEntry{
  145. Name: entry.Name,
  146. FullPath: fullPath,
  147. IsDirectory: entry.IsDirectory,
  148. Size: size,
  149. ModTime: modTime,
  150. Mode: mode,
  151. Uid: uid,
  152. Gid: gid,
  153. Mime: mime,
  154. Replication: replication,
  155. Collection: collection,
  156. TtlSec: ttlSec,
  157. }
  158. entries = append(entries, fileEntry)
  159. if !entry.IsDirectory {
  160. totalSize += size
  161. }
  162. }
  163. return nil
  164. })
  165. if err != nil {
  166. return nil, err
  167. }
  168. // Sort entries: directories first, then files, both alphabetically
  169. sort.Slice(entries, func(i, j int) bool {
  170. if entries[i].IsDirectory != entries[j].IsDirectory {
  171. return entries[i].IsDirectory
  172. }
  173. return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name)
  174. })
  175. // Generate breadcrumbs
  176. breadcrumbs := s.generateBreadcrumbs(path)
  177. // Calculate parent path
  178. parentPath := "/"
  179. if path != "/" {
  180. parentPath = filepath.Dir(path)
  181. if parentPath == "." {
  182. parentPath = "/"
  183. }
  184. }
  185. // Check if this is a bucket path
  186. isBucketPath := false
  187. bucketName := ""
  188. if strings.HasPrefix(path, "/buckets/") {
  189. isBucketPath = true
  190. pathParts := strings.Split(strings.Trim(path, "/"), "/")
  191. if len(pathParts) >= 2 {
  192. bucketName = pathParts[1]
  193. }
  194. }
  195. return &FileBrowserData{
  196. CurrentPath: path,
  197. ParentPath: parentPath,
  198. Breadcrumbs: breadcrumbs,
  199. Entries: entries,
  200. TotalEntries: len(entries),
  201. TotalSize: totalSize,
  202. LastUpdated: time.Now(),
  203. IsBucketPath: isBucketPath,
  204. BucketName: bucketName,
  205. }, nil
  206. }
  207. // generateBreadcrumbs creates breadcrumb navigation for the current path
  208. func (s *AdminServer) generateBreadcrumbs(path string) []BreadcrumbItem {
  209. var breadcrumbs []BreadcrumbItem
  210. // Always start with root
  211. breadcrumbs = append(breadcrumbs, BreadcrumbItem{
  212. Name: "Root",
  213. Path: "/",
  214. })
  215. if path == "/" {
  216. return breadcrumbs
  217. }
  218. // Split path and build breadcrumbs
  219. parts := strings.Split(strings.Trim(path, "/"), "/")
  220. currentPath := ""
  221. for _, part := range parts {
  222. if part == "" {
  223. continue
  224. }
  225. currentPath += "/" + part
  226. // Special handling for bucket paths
  227. displayName := part
  228. if len(breadcrumbs) == 1 && part == "buckets" {
  229. displayName = "Object Store Buckets"
  230. } else if len(breadcrumbs) == 2 && strings.HasPrefix(path, "/buckets/") {
  231. displayName = "📦 " + part // Add bucket icon to bucket name
  232. }
  233. breadcrumbs = append(breadcrumbs, BreadcrumbItem{
  234. Name: displayName,
  235. Path: currentPath,
  236. })
  237. }
  238. return breadcrumbs
  239. }