file_browser_handlers.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  1. package handlers
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "io"
  7. "mime/multipart"
  8. "net"
  9. "net/http"
  10. "os"
  11. "path/filepath"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "github.com/gin-gonic/gin"
  16. "github.com/seaweedfs/seaweedfs/weed/admin/dash"
  17. "github.com/seaweedfs/seaweedfs/weed/admin/view/app"
  18. "github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
  19. "github.com/seaweedfs/seaweedfs/weed/glog"
  20. "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
  21. )
  22. type FileBrowserHandlers struct {
  23. adminServer *dash.AdminServer
  24. }
  25. func NewFileBrowserHandlers(adminServer *dash.AdminServer) *FileBrowserHandlers {
  26. return &FileBrowserHandlers{
  27. adminServer: adminServer,
  28. }
  29. }
  30. // ShowFileBrowser renders the file browser page
  31. func (h *FileBrowserHandlers) ShowFileBrowser(c *gin.Context) {
  32. // Get path from query parameter, default to root
  33. path := c.DefaultQuery("path", "/")
  34. // Get file browser data
  35. browserData, err := h.adminServer.GetFileBrowser(path)
  36. if err != nil {
  37. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file browser data: " + err.Error()})
  38. return
  39. }
  40. // Set username
  41. username := c.GetString("username")
  42. if username == "" {
  43. username = "admin"
  44. }
  45. browserData.Username = username
  46. // Render HTML template
  47. c.Header("Content-Type", "text/html")
  48. browserComponent := app.FileBrowser(*browserData)
  49. layoutComponent := layout.Layout(c, browserComponent)
  50. err = layoutComponent.Render(c.Request.Context(), c.Writer)
  51. if err != nil {
  52. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
  53. return
  54. }
  55. }
  56. // DeleteFile handles file deletion API requests
  57. func (h *FileBrowserHandlers) DeleteFile(c *gin.Context) {
  58. var request struct {
  59. Path string `json:"path" binding:"required"`
  60. }
  61. if err := c.ShouldBindJSON(&request); err != nil {
  62. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
  63. return
  64. }
  65. // Delete file via filer
  66. err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
  67. _, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
  68. Directory: filepath.Dir(request.Path),
  69. Name: filepath.Base(request.Path),
  70. IsDeleteData: true,
  71. IsRecursive: true,
  72. IgnoreRecursiveError: false,
  73. })
  74. return err
  75. })
  76. if err != nil {
  77. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file: " + err.Error()})
  78. return
  79. }
  80. c.JSON(http.StatusOK, gin.H{"message": "File deleted successfully"})
  81. }
  82. // DeleteMultipleFiles handles multiple file deletion API requests
  83. func (h *FileBrowserHandlers) DeleteMultipleFiles(c *gin.Context) {
  84. var request struct {
  85. Paths []string `json:"paths" binding:"required"`
  86. }
  87. if err := c.ShouldBindJSON(&request); err != nil {
  88. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
  89. return
  90. }
  91. if len(request.Paths) == 0 {
  92. c.JSON(http.StatusBadRequest, gin.H{"error": "No paths provided"})
  93. return
  94. }
  95. var deletedCount int
  96. var failedCount int
  97. var errors []string
  98. // Delete each file/folder
  99. for _, path := range request.Paths {
  100. err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
  101. _, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
  102. Directory: filepath.Dir(path),
  103. Name: filepath.Base(path),
  104. IsDeleteData: true,
  105. IsRecursive: true,
  106. IgnoreRecursiveError: false,
  107. })
  108. return err
  109. })
  110. if err != nil {
  111. failedCount++
  112. errors = append(errors, fmt.Sprintf("%s: %v", path, err))
  113. } else {
  114. deletedCount++
  115. }
  116. }
  117. // Prepare response
  118. response := map[string]interface{}{
  119. "deleted": deletedCount,
  120. "failed": failedCount,
  121. "total": len(request.Paths),
  122. }
  123. if len(errors) > 0 {
  124. response["errors"] = errors
  125. }
  126. if deletedCount > 0 {
  127. if failedCount == 0 {
  128. response["message"] = fmt.Sprintf("Successfully deleted %d item(s)", deletedCount)
  129. } else {
  130. response["message"] = fmt.Sprintf("Deleted %d item(s), failed to delete %d item(s)", deletedCount, failedCount)
  131. }
  132. c.JSON(http.StatusOK, response)
  133. } else {
  134. response["message"] = "Failed to delete all selected items"
  135. c.JSON(http.StatusInternalServerError, response)
  136. }
  137. }
  138. // CreateFolder handles folder creation requests
  139. func (h *FileBrowserHandlers) CreateFolder(c *gin.Context) {
  140. var request struct {
  141. Path string `json:"path" binding:"required"`
  142. FolderName string `json:"folder_name" binding:"required"`
  143. }
  144. if err := c.ShouldBindJSON(&request); err != nil {
  145. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
  146. return
  147. }
  148. // Clean and validate folder name
  149. folderName := strings.TrimSpace(request.FolderName)
  150. if folderName == "" || strings.Contains(folderName, "/") || strings.Contains(folderName, "\\") {
  151. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid folder name"})
  152. return
  153. }
  154. // Create full path for new folder
  155. fullPath := filepath.Join(request.Path, folderName)
  156. if !strings.HasPrefix(fullPath, "/") {
  157. fullPath = "/" + fullPath
  158. }
  159. // Create folder via filer
  160. err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
  161. _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
  162. Directory: filepath.Dir(fullPath),
  163. Entry: &filer_pb.Entry{
  164. Name: filepath.Base(fullPath),
  165. IsDirectory: true,
  166. Attributes: &filer_pb.FuseAttributes{
  167. FileMode: uint32(0755 | os.ModeDir), // Directory mode
  168. Uid: filer_pb.OS_UID,
  169. Gid: filer_pb.OS_GID,
  170. Crtime: time.Now().Unix(),
  171. Mtime: time.Now().Unix(),
  172. TtlSec: 0,
  173. },
  174. },
  175. })
  176. return err
  177. })
  178. if err != nil {
  179. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create folder: " + err.Error()})
  180. return
  181. }
  182. c.JSON(http.StatusOK, gin.H{"message": "Folder created successfully"})
  183. }
  184. // UploadFile handles file upload requests
  185. func (h *FileBrowserHandlers) UploadFile(c *gin.Context) {
  186. // Get the current path
  187. currentPath := c.PostForm("path")
  188. if currentPath == "" {
  189. currentPath = "/"
  190. }
  191. // Parse multipart form
  192. err := c.Request.ParseMultipartForm(1 << 30) // 1GB max memory for large file uploads
  193. if err != nil {
  194. c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse multipart form: " + err.Error()})
  195. return
  196. }
  197. // Get uploaded files (supports multiple files)
  198. files := c.Request.MultipartForm.File["files"]
  199. if len(files) == 0 {
  200. c.JSON(http.StatusBadRequest, gin.H{"error": "No files uploaded"})
  201. return
  202. }
  203. var uploadResults []map[string]interface{}
  204. var failedUploads []string
  205. // Process each uploaded file
  206. for _, fileHeader := range files {
  207. // Validate file name
  208. fileName := fileHeader.Filename
  209. if fileName == "" {
  210. failedUploads = append(failedUploads, "invalid filename")
  211. continue
  212. }
  213. // Create full path for the file
  214. fullPath := filepath.Join(currentPath, fileName)
  215. if !strings.HasPrefix(fullPath, "/") {
  216. fullPath = "/" + fullPath
  217. }
  218. // Open the file
  219. file, err := fileHeader.Open()
  220. if err != nil {
  221. failedUploads = append(failedUploads, fmt.Sprintf("%s: %v", fileName, err))
  222. continue
  223. }
  224. // Upload file to filer
  225. err = h.uploadFileToFiler(fullPath, fileHeader)
  226. file.Close()
  227. if err != nil {
  228. failedUploads = append(failedUploads, fmt.Sprintf("%s: %v", fileName, err))
  229. } else {
  230. uploadResults = append(uploadResults, map[string]interface{}{
  231. "name": fileName,
  232. "size": fileHeader.Size,
  233. "path": fullPath,
  234. })
  235. }
  236. }
  237. // Prepare response
  238. response := map[string]interface{}{
  239. "uploaded": len(uploadResults),
  240. "failed": len(failedUploads),
  241. "files": uploadResults,
  242. }
  243. if len(failedUploads) > 0 {
  244. response["errors"] = failedUploads
  245. }
  246. if len(uploadResults) > 0 {
  247. if len(failedUploads) == 0 {
  248. response["message"] = fmt.Sprintf("Successfully uploaded %d file(s)", len(uploadResults))
  249. } else {
  250. response["message"] = fmt.Sprintf("Uploaded %d file(s), %d failed", len(uploadResults), len(failedUploads))
  251. }
  252. c.JSON(http.StatusOK, response)
  253. } else {
  254. response["message"] = "All file uploads failed"
  255. c.JSON(http.StatusInternalServerError, response)
  256. }
  257. }
  258. // uploadFileToFiler uploads a file directly to the filer using multipart form data
  259. func (h *FileBrowserHandlers) uploadFileToFiler(filePath string, fileHeader *multipart.FileHeader) error {
  260. // Get filer address from admin server
  261. filerAddress := h.adminServer.GetFilerAddress()
  262. if filerAddress == "" {
  263. return fmt.Errorf("filer address not configured")
  264. }
  265. // Validate and sanitize the filer address
  266. if err := h.validateFilerAddress(filerAddress); err != nil {
  267. return fmt.Errorf("invalid filer address: %w", err)
  268. }
  269. // Validate and sanitize the file path
  270. cleanFilePath, err := h.validateAndCleanFilePath(filePath)
  271. if err != nil {
  272. return fmt.Errorf("invalid file path: %w", err)
  273. }
  274. // Open the file
  275. file, err := fileHeader.Open()
  276. if err != nil {
  277. return fmt.Errorf("failed to open file: %w", err)
  278. }
  279. defer file.Close()
  280. // Create multipart form data
  281. var body bytes.Buffer
  282. writer := multipart.NewWriter(&body)
  283. // Create form file field
  284. part, err := writer.CreateFormFile("file", fileHeader.Filename)
  285. if err != nil {
  286. return fmt.Errorf("failed to create form file: %w", err)
  287. }
  288. // Copy file content to form
  289. _, err = io.Copy(part, file)
  290. if err != nil {
  291. return fmt.Errorf("failed to copy file content: %w", err)
  292. }
  293. // Close the writer to finalize the form
  294. err = writer.Close()
  295. if err != nil {
  296. return fmt.Errorf("failed to close multipart writer: %w", err)
  297. }
  298. // Create the upload URL with validated components
  299. uploadURL := fmt.Sprintf("http://%s%s", filerAddress, cleanFilePath)
  300. // Create HTTP request
  301. req, err := http.NewRequest("POST", uploadURL, &body)
  302. if err != nil {
  303. return fmt.Errorf("failed to create request: %w", err)
  304. }
  305. // Set content type with boundary
  306. req.Header.Set("Content-Type", writer.FormDataContentType())
  307. // Send request
  308. client := &http.Client{Timeout: 60 * time.Second} // Increased timeout for larger files
  309. resp, err := client.Do(req)
  310. if err != nil {
  311. return fmt.Errorf("failed to upload file: %w", err)
  312. }
  313. defer resp.Body.Close()
  314. // Check response
  315. if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
  316. responseBody, _ := io.ReadAll(resp.Body)
  317. return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(responseBody))
  318. }
  319. return nil
  320. }
  321. // validateFilerAddress validates that the filer address is safe to use
  322. func (h *FileBrowserHandlers) validateFilerAddress(address string) error {
  323. if address == "" {
  324. return fmt.Errorf("filer address cannot be empty")
  325. }
  326. // Parse the address to validate it's a proper host:port format
  327. host, port, err := net.SplitHostPort(address)
  328. if err != nil {
  329. return fmt.Errorf("invalid address format: %w", err)
  330. }
  331. // Validate host is not empty
  332. if host == "" {
  333. return fmt.Errorf("host cannot be empty")
  334. }
  335. // Validate port is numeric and in valid range
  336. if port == "" {
  337. return fmt.Errorf("port cannot be empty")
  338. }
  339. portNum, err := strconv.Atoi(port)
  340. if err != nil {
  341. return fmt.Errorf("invalid port number: %w", err)
  342. }
  343. if portNum < 1 || portNum > 65535 {
  344. return fmt.Errorf("port number must be between 1 and 65535")
  345. }
  346. // Additional security: prevent private network access unless explicitly allowed
  347. // This helps prevent SSRF attacks to internal services
  348. ip := net.ParseIP(host)
  349. if ip != nil {
  350. // Check for localhost, private networks, and other dangerous addresses
  351. if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() {
  352. // Only allow if it's the configured filer (trusted)
  353. // In production, you might want to be more restrictive
  354. glog.V(2).Infof("Allowing access to private/local address: %s (configured filer)", address)
  355. }
  356. }
  357. return nil
  358. }
  359. // validateAndCleanFilePath validates and cleans the file path to prevent path traversal
  360. func (h *FileBrowserHandlers) validateAndCleanFilePath(filePath string) (string, error) {
  361. if filePath == "" {
  362. return "", fmt.Errorf("file path cannot be empty")
  363. }
  364. // Clean the path to remove any .. or . components
  365. cleanPath := filepath.Clean(filePath)
  366. // Ensure the path starts with /
  367. if !strings.HasPrefix(cleanPath, "/") {
  368. cleanPath = "/" + cleanPath
  369. }
  370. // Prevent path traversal attacks
  371. if strings.Contains(cleanPath, "..") {
  372. return "", fmt.Errorf("path traversal not allowed")
  373. }
  374. // Additional validation: ensure path doesn't contain dangerous characters
  375. if strings.ContainsAny(cleanPath, "\x00\r\n") {
  376. return "", fmt.Errorf("path contains invalid characters")
  377. }
  378. return cleanPath, nil
  379. }
  380. // DownloadFile handles file download requests
  381. func (h *FileBrowserHandlers) DownloadFile(c *gin.Context) {
  382. filePath := c.Query("path")
  383. if filePath == "" {
  384. c.JSON(http.StatusBadRequest, gin.H{"error": "File path is required"})
  385. return
  386. }
  387. // Get filer address
  388. filerAddress := h.adminServer.GetFilerAddress()
  389. if filerAddress == "" {
  390. c.JSON(http.StatusInternalServerError, gin.H{"error": "Filer address not configured"})
  391. return
  392. }
  393. // Validate and sanitize the file path
  394. cleanFilePath, err := h.validateAndCleanFilePath(filePath)
  395. if err != nil {
  396. c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file path: " + err.Error()})
  397. return
  398. }
  399. // Create the download URL
  400. downloadURL := fmt.Sprintf("http://%s%s", filerAddress, cleanFilePath)
  401. // Set headers for file download
  402. fileName := filepath.Base(cleanFilePath)
  403. c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
  404. c.Header("Content-Type", "application/octet-stream")
  405. // Proxy the request to filer
  406. c.Redirect(http.StatusFound, downloadURL)
  407. }
  408. // ViewFile handles file viewing requests (for text files, images, etc.)
  409. func (h *FileBrowserHandlers) ViewFile(c *gin.Context) {
  410. filePath := c.Query("path")
  411. if filePath == "" {
  412. c.JSON(http.StatusBadRequest, gin.H{"error": "File path is required"})
  413. return
  414. }
  415. // Get file metadata first
  416. var fileEntry dash.FileEntry
  417. err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
  418. resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{
  419. Directory: filepath.Dir(filePath),
  420. Name: filepath.Base(filePath),
  421. })
  422. if err != nil {
  423. return err
  424. }
  425. entry := resp.Entry
  426. if entry == nil {
  427. return fmt.Errorf("file not found")
  428. }
  429. // Convert to FileEntry
  430. var modTime time.Time
  431. if entry.Attributes != nil && entry.Attributes.Mtime > 0 {
  432. modTime = time.Unix(entry.Attributes.Mtime, 0)
  433. }
  434. var size int64
  435. if entry.Attributes != nil {
  436. size = int64(entry.Attributes.FileSize)
  437. }
  438. // Determine MIME type with comprehensive extension support
  439. mime := h.determineMimeType(entry.Name)
  440. fileEntry = dash.FileEntry{
  441. Name: entry.Name,
  442. FullPath: filePath,
  443. IsDirectory: entry.IsDirectory,
  444. Size: size,
  445. ModTime: modTime,
  446. Mime: mime,
  447. }
  448. return nil
  449. })
  450. if err != nil {
  451. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file metadata: " + err.Error()})
  452. return
  453. }
  454. // Check if file is viewable as text
  455. var content string
  456. var viewable bool
  457. var reason string
  458. // First check if it's a known text type or if we should check content
  459. isKnownTextType := strings.HasPrefix(fileEntry.Mime, "text/") ||
  460. fileEntry.Mime == "application/json" ||
  461. fileEntry.Mime == "application/javascript" ||
  462. fileEntry.Mime == "application/xml"
  463. // For unknown types, check if it might be text by content
  464. if !isKnownTextType && fileEntry.Mime == "application/octet-stream" {
  465. isKnownTextType = h.isLikelyTextFile(filePath, 512)
  466. if isKnownTextType {
  467. // Update MIME type for better display
  468. fileEntry.Mime = "text/plain"
  469. }
  470. }
  471. if isKnownTextType {
  472. // Limit text file size for viewing (max 1MB)
  473. if fileEntry.Size > 1024*1024 {
  474. viewable = false
  475. reason = "File too large for viewing (>1MB)"
  476. } else {
  477. // Get file content from filer
  478. filerAddress := h.adminServer.GetFilerAddress()
  479. if filerAddress != "" {
  480. cleanFilePath, err := h.validateAndCleanFilePath(filePath)
  481. if err == nil {
  482. fileURL := fmt.Sprintf("http://%s%s", filerAddress, cleanFilePath)
  483. client := &http.Client{Timeout: 30 * time.Second}
  484. resp, err := client.Get(fileURL)
  485. if err == nil && resp.StatusCode == http.StatusOK {
  486. defer resp.Body.Close()
  487. contentBytes, err := io.ReadAll(resp.Body)
  488. if err == nil {
  489. content = string(contentBytes)
  490. viewable = true
  491. } else {
  492. viewable = false
  493. reason = "Failed to read file content"
  494. }
  495. } else {
  496. viewable = false
  497. reason = "Failed to fetch file from filer"
  498. }
  499. } else {
  500. viewable = false
  501. reason = "Invalid file path"
  502. }
  503. } else {
  504. viewable = false
  505. reason = "Filer address not configured"
  506. }
  507. }
  508. } else {
  509. // Not a text file, but might be viewable as image or PDF
  510. if strings.HasPrefix(fileEntry.Mime, "image/") || fileEntry.Mime == "application/pdf" {
  511. viewable = true
  512. } else {
  513. viewable = false
  514. reason = "File type not supported for viewing"
  515. }
  516. }
  517. c.JSON(http.StatusOK, gin.H{
  518. "file": fileEntry,
  519. "content": content,
  520. "viewable": viewable,
  521. "reason": reason,
  522. })
  523. }
  524. // GetFileProperties handles file properties requests
  525. func (h *FileBrowserHandlers) GetFileProperties(c *gin.Context) {
  526. filePath := c.Query("path")
  527. if filePath == "" {
  528. c.JSON(http.StatusBadRequest, gin.H{"error": "File path is required"})
  529. return
  530. }
  531. // Get detailed file information from filer
  532. var properties map[string]interface{}
  533. err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
  534. resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{
  535. Directory: filepath.Dir(filePath),
  536. Name: filepath.Base(filePath),
  537. })
  538. if err != nil {
  539. return err
  540. }
  541. entry := resp.Entry
  542. if entry == nil {
  543. return fmt.Errorf("file not found")
  544. }
  545. properties = make(map[string]interface{})
  546. properties["name"] = entry.Name
  547. properties["full_path"] = filePath
  548. properties["is_directory"] = entry.IsDirectory
  549. if entry.Attributes != nil {
  550. properties["size"] = entry.Attributes.FileSize
  551. properties["size_formatted"] = h.formatBytes(int64(entry.Attributes.FileSize))
  552. if entry.Attributes.Mtime > 0 {
  553. modTime := time.Unix(entry.Attributes.Mtime, 0)
  554. properties["modified_time"] = modTime.Format("2006-01-02 15:04:05")
  555. properties["modified_timestamp"] = entry.Attributes.Mtime
  556. }
  557. if entry.Attributes.Crtime > 0 {
  558. createTime := time.Unix(entry.Attributes.Crtime, 0)
  559. properties["created_time"] = createTime.Format("2006-01-02 15:04:05")
  560. properties["created_timestamp"] = entry.Attributes.Crtime
  561. }
  562. properties["file_mode"] = dash.FormatFileMode(entry.Attributes.FileMode)
  563. properties["file_mode_formatted"] = dash.FormatFileMode(entry.Attributes.FileMode)
  564. properties["file_mode_octal"] = fmt.Sprintf("%o", entry.Attributes.FileMode)
  565. properties["uid"] = entry.Attributes.Uid
  566. properties["gid"] = entry.Attributes.Gid
  567. properties["ttl_seconds"] = entry.Attributes.TtlSec
  568. if entry.Attributes.TtlSec > 0 {
  569. properties["ttl_formatted"] = fmt.Sprintf("%d seconds", entry.Attributes.TtlSec)
  570. }
  571. }
  572. // Get extended attributes
  573. if entry.Extended != nil {
  574. extended := make(map[string]string)
  575. for key, value := range entry.Extended {
  576. extended[key] = string(value)
  577. }
  578. properties["extended"] = extended
  579. }
  580. // Get chunk information for files
  581. if !entry.IsDirectory && len(entry.Chunks) > 0 {
  582. chunks := make([]map[string]interface{}, 0, len(entry.Chunks))
  583. for _, chunk := range entry.Chunks {
  584. chunkInfo := map[string]interface{}{
  585. "file_id": chunk.FileId,
  586. "offset": chunk.Offset,
  587. "size": chunk.Size,
  588. "modified_ts": chunk.ModifiedTsNs,
  589. "e_tag": chunk.ETag,
  590. "source_fid": chunk.SourceFileId,
  591. }
  592. chunks = append(chunks, chunkInfo)
  593. }
  594. properties["chunks"] = chunks
  595. properties["chunk_count"] = len(entry.Chunks)
  596. }
  597. // Determine MIME type
  598. if !entry.IsDirectory {
  599. mime := h.determineMimeType(entry.Name)
  600. properties["mime_type"] = mime
  601. }
  602. return nil
  603. })
  604. if err != nil {
  605. c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file properties: " + err.Error()})
  606. return
  607. }
  608. c.JSON(http.StatusOK, properties)
  609. }
  610. // Helper function to format bytes
  611. func (h *FileBrowserHandlers) formatBytes(bytes int64) string {
  612. const unit = 1024
  613. if bytes < unit {
  614. return fmt.Sprintf("%d B", bytes)
  615. }
  616. div, exp := int64(unit), 0
  617. for n := bytes / unit; n >= unit; n /= unit {
  618. div *= unit
  619. exp++
  620. }
  621. return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
  622. }
  623. // Helper function to determine MIME type from filename
  624. func (h *FileBrowserHandlers) determineMimeType(filename string) string {
  625. ext := strings.ToLower(filepath.Ext(filename))
  626. // Text files
  627. switch ext {
  628. case ".txt", ".log", ".cfg", ".conf", ".ini", ".properties":
  629. return "text/plain"
  630. case ".md", ".markdown":
  631. return "text/markdown"
  632. case ".html", ".htm":
  633. return "text/html"
  634. case ".css":
  635. return "text/css"
  636. case ".js", ".mjs":
  637. return "application/javascript"
  638. case ".ts":
  639. return "text/typescript"
  640. case ".json":
  641. return "application/json"
  642. case ".xml":
  643. return "application/xml"
  644. case ".yaml", ".yml":
  645. return "text/yaml"
  646. case ".csv":
  647. return "text/csv"
  648. case ".sql":
  649. return "text/sql"
  650. case ".sh", ".bash", ".zsh", ".fish":
  651. return "text/x-shellscript"
  652. case ".py":
  653. return "text/x-python"
  654. case ".go":
  655. return "text/x-go"
  656. case ".java":
  657. return "text/x-java"
  658. case ".c":
  659. return "text/x-c"
  660. case ".cpp", ".cc", ".cxx", ".c++":
  661. return "text/x-c++"
  662. case ".h", ".hpp":
  663. return "text/x-c-header"
  664. case ".php":
  665. return "text/x-php"
  666. case ".rb":
  667. return "text/x-ruby"
  668. case ".pl":
  669. return "text/x-perl"
  670. case ".rs":
  671. return "text/x-rust"
  672. case ".swift":
  673. return "text/x-swift"
  674. case ".kt":
  675. return "text/x-kotlin"
  676. case ".scala":
  677. return "text/x-scala"
  678. case ".dockerfile":
  679. return "text/x-dockerfile"
  680. case ".gitignore", ".gitattributes":
  681. return "text/plain"
  682. case ".env":
  683. return "text/plain"
  684. // Image files
  685. case ".jpg", ".jpeg":
  686. return "image/jpeg"
  687. case ".png":
  688. return "image/png"
  689. case ".gif":
  690. return "image/gif"
  691. case ".bmp":
  692. return "image/bmp"
  693. case ".webp":
  694. return "image/webp"
  695. case ".svg":
  696. return "image/svg+xml"
  697. case ".ico":
  698. return "image/x-icon"
  699. // Document files
  700. case ".pdf":
  701. return "application/pdf"
  702. case ".doc":
  703. return "application/msword"
  704. case ".docx":
  705. return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
  706. case ".xls":
  707. return "application/vnd.ms-excel"
  708. case ".xlsx":
  709. return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  710. case ".ppt":
  711. return "application/vnd.ms-powerpoint"
  712. case ".pptx":
  713. return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
  714. // Archive files
  715. case ".zip":
  716. return "application/zip"
  717. case ".tar":
  718. return "application/x-tar"
  719. case ".gz":
  720. return "application/gzip"
  721. case ".bz2":
  722. return "application/x-bzip2"
  723. case ".7z":
  724. return "application/x-7z-compressed"
  725. case ".rar":
  726. return "application/x-rar-compressed"
  727. // Video files
  728. case ".mp4":
  729. return "video/mp4"
  730. case ".avi":
  731. return "video/x-msvideo"
  732. case ".mov":
  733. return "video/quicktime"
  734. case ".wmv":
  735. return "video/x-ms-wmv"
  736. case ".flv":
  737. return "video/x-flv"
  738. case ".webm":
  739. return "video/webm"
  740. // Audio files
  741. case ".mp3":
  742. return "audio/mpeg"
  743. case ".wav":
  744. return "audio/wav"
  745. case ".flac":
  746. return "audio/flac"
  747. case ".aac":
  748. return "audio/aac"
  749. case ".ogg":
  750. return "audio/ogg"
  751. default:
  752. // For files without extension or unknown extensions,
  753. // we'll check if they might be text files by content
  754. return "application/octet-stream"
  755. }
  756. }
  757. // Helper function to check if a file is likely a text file by checking content
  758. func (h *FileBrowserHandlers) isLikelyTextFile(filePath string, maxCheckSize int64) bool {
  759. filerAddress := h.adminServer.GetFilerAddress()
  760. if filerAddress == "" {
  761. return false
  762. }
  763. cleanFilePath, err := h.validateAndCleanFilePath(filePath)
  764. if err != nil {
  765. return false
  766. }
  767. fileURL := fmt.Sprintf("http://%s%s", filerAddress, cleanFilePath)
  768. client := &http.Client{Timeout: 10 * time.Second}
  769. resp, err := client.Get(fileURL)
  770. if err != nil || resp.StatusCode != http.StatusOK {
  771. return false
  772. }
  773. defer resp.Body.Close()
  774. // Read first few bytes to check if it's text
  775. buffer := make([]byte, min(maxCheckSize, 512))
  776. n, err := resp.Body.Read(buffer)
  777. if err != nil && err != io.EOF {
  778. return false
  779. }
  780. if n == 0 {
  781. return true // Empty file can be considered text
  782. }
  783. // Check if content is printable text
  784. return h.isPrintableText(buffer[:n])
  785. }
  786. // Helper function to check if content is printable text
  787. func (h *FileBrowserHandlers) isPrintableText(data []byte) bool {
  788. if len(data) == 0 {
  789. return true
  790. }
  791. // Count printable characters
  792. printable := 0
  793. for _, b := range data {
  794. if b >= 32 && b <= 126 || b == 9 || b == 10 || b == 13 {
  795. // Printable ASCII, tab, newline, carriage return
  796. printable++
  797. } else if b >= 128 {
  798. // Potential UTF-8 character
  799. printable++
  800. }
  801. }
  802. // If more than 95% of characters are printable, consider it text
  803. return float64(printable)/float64(len(data)) > 0.95
  804. }
  805. // Helper function for min
  806. func min(a, b int64) int64 {
  807. if a < b {
  808. return a
  809. }
  810. return b
  811. }