diskmg.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. package diskmg
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "log"
  7. "net/http"
  8. "os/exec"
  9. "path/filepath"
  10. "regexp"
  11. "runtime"
  12. "strings"
  13. "time"
  14. fs "imuslab.com/arozos/mod/filesystem"
  15. "imuslab.com/arozos/mod/utils"
  16. )
  17. type Lsblk struct {
  18. Blockdevices []struct {
  19. Name string `json:"name"`
  20. MajMin string `json:"maj:min"`
  21. Rm bool `json:"rm"`
  22. Size int64 `json:"size"`
  23. Ro bool `json:"ro"`
  24. Type string `json:"type"`
  25. Mountpoint interface{} `json:"mountpoint"`
  26. Children []struct {
  27. Name string `json:"name"`
  28. MajMin string `json:"maj:min"`
  29. Rm bool `json:"rm"`
  30. Size int64 `json:"size"`
  31. Ro bool `json:"ro"`
  32. Type string `json:"type"`
  33. Mountpoint string `json:"mountpoint"`
  34. } `json:"children"`
  35. } `json:"blockdevices"`
  36. }
  37. type LsblkF struct {
  38. Blockdevices []struct {
  39. Name string `json:"name"`
  40. Fstype interface{} `json:"fstype"`
  41. Label interface{} `json:"label"`
  42. UUID interface{} `json:"uuid"`
  43. Fsavail interface{} `json:"fsavail"`
  44. Fsuse interface{} `json:"fsuse%"`
  45. Mountpoint interface{} `json:"mountpoint"`
  46. Children []struct {
  47. Name string `json:"name"`
  48. Fstype string `json:"fstype"`
  49. Label interface{} `json:"label"`
  50. UUID string `json:"uuid"`
  51. Fsavail int64 `json:"fsavail"`
  52. Fsuse string `json:"fsuse%"`
  53. Mountpoint string `json:"mountpoint"`
  54. } `json:"children"`
  55. } `json:"blockdevices"`
  56. }
  57. var (
  58. supportedFormats = []string{"ntfs", "vfat", "ext4", "ext3", "btrfs"}
  59. )
  60. /*
  61. Diskmg View Generator
  62. This section of the code is a direct translation of the original
  63. AOB's diskmg.php and diskmgWin.php.
  64. If you find any bugs in these code, just remember they are legacy
  65. code and rewriting the whole thing will save you a lot more time.
  66. */
  67. func HandleView(w http.ResponseWriter, r *http.Request) {
  68. partition, _ := utils.GetPara(r, "partition")
  69. detailMode := (partition != "")
  70. if runtime.GOOS == "windows" {
  71. //Windows. Use DiskmgWin binary
  72. if utils.FileExists("./system/disk/diskmg/DiskmgWin.exe") {
  73. out := ""
  74. if detailMode {
  75. cmd := exec.Command("./system/disk/diskmg/DiskmgWin.exe", "-d")
  76. o, err := cmd.CombinedOutput()
  77. if err != nil {
  78. utils.SendErrorResponse(w, "Permission Denied")
  79. return
  80. }
  81. out = string(o)
  82. } else {
  83. cmd := exec.Command("./system/disk/diskmg/DiskmgWin.exe")
  84. o, err := cmd.CombinedOutput()
  85. if err != nil {
  86. utils.SendErrorResponse(w, "Permission Denied")
  87. return
  88. }
  89. out = string(o)
  90. }
  91. out = strings.TrimSpace(out)
  92. lines := strings.Split(out, ";")
  93. results := [][]string{}
  94. for _, line := range lines {
  95. data := strings.Split(line, ",")
  96. if len(data) > 0 && data[0] != "" {
  97. results = append(results, data)
  98. }
  99. }
  100. js, _ := json.Marshal(results)
  101. utils.SendJSONResponse(w, string(js))
  102. } else {
  103. log.Println("system/disk/diskmg/DiskmgWin.exe NOT FOUND. Unable to load Window's disk information")
  104. utils.SendErrorResponse(w, "DiskmgWin.exe not found")
  105. return
  106. }
  107. } else {
  108. //Linux. Use lsblk and df to check volume info
  109. partition := new(Lsblk)
  110. format := new(LsblkF)
  111. df := ""
  112. //Get partition information
  113. cmd := exec.Command("lsblk", "-b", "--json")
  114. o, err := cmd.CombinedOutput()
  115. if err != nil {
  116. utils.SendErrorResponse(w, err.Error())
  117. return
  118. }
  119. err = json.Unmarshal(o, &partition)
  120. if err != nil {
  121. utils.SendErrorResponse(w, err.Error())
  122. return
  123. }
  124. //Get format info
  125. cmd = exec.Command("lsblk", "-f", "-b", "--json")
  126. o, err = cmd.CombinedOutput()
  127. if err != nil {
  128. utils.SendErrorResponse(w, err.Error())
  129. return
  130. }
  131. err = json.Unmarshal(o, &format)
  132. if err != nil {
  133. utils.SendErrorResponse(w, err.Error())
  134. return
  135. }
  136. //Get df info
  137. cmd = exec.Command("df")
  138. o, err = cmd.CombinedOutput()
  139. if err != nil {
  140. utils.SendErrorResponse(w, err.Error())
  141. return
  142. }
  143. df = string(o)
  144. //Filter the df information
  145. for strings.Contains(df, " ") {
  146. df = strings.ReplaceAll(df, " ", " ")
  147. }
  148. dflines := strings.Split(df, "\n")
  149. parsedDf := [][]string{}
  150. for _, line := range dflines {
  151. linedata := strings.Split(line, " ")
  152. parsedDf = append(parsedDf, linedata)
  153. }
  154. //Throw away the table header
  155. parsedDf = parsedDf[1:]
  156. js, _ := json.Marshal([]interface{}{
  157. partition,
  158. format,
  159. parsedDf,
  160. })
  161. utils.SendJSONResponse(w, string(js))
  162. }
  163. }
  164. /*
  165. Mounting a given partition or devices
  166. Manual translated from mountTool.php
  167. Require GET parameter: dev / format / mnt
  168. */
  169. func HandleMount(w http.ResponseWriter, r *http.Request, fsHandlers []*fs.FileSystemHandler) {
  170. if runtime.GOOS == "linux" {
  171. targetDev, _ := utils.GetPara(r, "dev")
  172. format, err := utils.GetPara(r, "format")
  173. if err != nil {
  174. utils.SendErrorResponse(w, "format not defined")
  175. return
  176. }
  177. mountPt, err := utils.GetPara(r, "mnt")
  178. if err != nil {
  179. utils.SendErrorResponse(w, "Mount Point not defined")
  180. return
  181. }
  182. //Check if device is valid
  183. ok, devID := checkDeviceValid(targetDev)
  184. if !ok {
  185. utils.SendErrorResponse(w, "Device name is not valid")
  186. return
  187. }
  188. //Check if the given format is supported
  189. mountingTool := ""
  190. if format == "ntfs" {
  191. mountingTool = "ntfs-3g"
  192. } else if format == "ext4" {
  193. mountingTool = "ext4"
  194. } else if format == "vfat" {
  195. mountingTool = "vfat"
  196. } else if format == "brtfs" {
  197. mountingTool = "brtfs"
  198. } else {
  199. utils.SendErrorResponse(w, "Format not supported")
  200. return
  201. }
  202. //Check if mount point exists, only support /medoa/*
  203. safeMountPoint := filepath.Clean(strings.ReplaceAll(mountPt, "../", ""))
  204. if !utils.FileExists(safeMountPoint) {
  205. utils.SendErrorResponse(w, "Mount point not exists, given: "+safeMountPoint)
  206. return
  207. }
  208. //Check if action is mount or umount
  209. umount, _ := utils.GetPara(r, "umount")
  210. if umount == "true" {
  211. //Unmount the given mountpoint
  212. output, err := Unmount(safeMountPoint, fsHandlers)
  213. if err != nil {
  214. utils.SendErrorResponse(w, output)
  215. return
  216. }
  217. utils.SendTextResponse(w, output)
  218. } else {
  219. o, err := Mount(devID, safeMountPoint, mountingTool, fsHandlers)
  220. if err != nil {
  221. utils.SendErrorResponse(w, o)
  222. return
  223. }
  224. utils.SendTextResponse(w, o)
  225. }
  226. } else {
  227. utils.SendErrorResponse(w, "Platform not supported: "+runtime.GOOS)
  228. return
  229. }
  230. }
  231. /*
  232. Format Tool
  233. Manual translation from AOB's formatTool.php
  234. */
  235. func HandleFormat(w http.ResponseWriter, r *http.Request, fsHandlers []*fs.FileSystemHandler) {
  236. dev, err := utils.PostPara(r, "dev")
  237. if err != nil {
  238. utils.SendErrorResponse(w, "dev not defined")
  239. return
  240. }
  241. format, err := utils.PostPara(r, "format")
  242. if err != nil {
  243. utils.SendErrorResponse(w, "format not defined")
  244. return
  245. }
  246. if runtime.GOOS == "windows" {
  247. utils.SendErrorResponse(w, "This function is Linux Only")
  248. return
  249. }
  250. //Check if format is supported
  251. if !utils.StringInArray(supportedFormats, format) {
  252. utils.SendErrorResponse(w, "Format not supported")
  253. return
  254. }
  255. //Check if device is valid
  256. ok, devID := checkDeviceValid(dev)
  257. if !ok {
  258. utils.SendErrorResponse(w, "Device name is not valid")
  259. return
  260. }
  261. //Check if it is mounted. If yes, umount it
  262. mounted, err := checkDeviceMounted(devID)
  263. if err != nil {
  264. //Fail to check if disk mounted
  265. log.Println(err.Error())
  266. utils.SendErrorResponse(w, "Failed to check disk mount status")
  267. return
  268. }
  269. //This drive is still mounted. Unmount it
  270. if mounted {
  271. //Close all the fsHandler related to this disk
  272. mountpt, err := getDeviceMountPoint(devID)
  273. if err != nil {
  274. utils.SendErrorResponse(w, err.Error())
  275. return
  276. }
  277. log.Println("Unmounting " + mountpt + " for format")
  278. //Unmount the devices
  279. out, err := Unmount(mountpt, fsHandlers)
  280. if err != nil {
  281. utils.SendErrorResponse(w, out)
  282. return
  283. }
  284. }
  285. //Format the drive
  286. var cmd *exec.Cmd
  287. if format == "ntfs" {
  288. cmd = exec.Command("mkfs.ntfs", "-f", "/dev/"+devID)
  289. } else if format == "vfat" {
  290. cmd = exec.Command("mkfs.vfat", "/dev/"+devID)
  291. } else if format == "ext4" {
  292. cmd = exec.Command("mkfs.ext4", "-F", "/dev/"+devID)
  293. } else if format == "ext3" {
  294. utils.SendErrorResponse(w, "Format to ext3 is Work In Progress")
  295. } else if format == "btrfs" {
  296. utils.SendErrorResponse(w, "Format to btrfs is Work In Progress")
  297. } else {
  298. utils.SendErrorResponse(w, "Format tyoe not supported")
  299. }
  300. //Execute format comamnd
  301. log.Println("Formatting of " + "/dev/" + devID + " Started")
  302. output, err := cmd.CombinedOutput()
  303. if err != nil {
  304. log.Println("Format failed: " + string(output))
  305. utils.SendErrorResponse(w, string(output))
  306. return
  307. }
  308. //Reply ok
  309. log.Println(string(output))
  310. //Let the system to reload the disk
  311. time.Sleep(2 * time.Second)
  312. utils.SendOK(w)
  313. }
  314. func Mount(devID string, mountpt string, mountingTool string, fsHandlers []*fs.FileSystemHandler) (string, error) {
  315. //Loop each fsHandler. If exists one that fits and Closed, reopen it
  316. for _, fsh := range fsHandlers {
  317. if strings.Contains(filepath.ToSlash(fsh.Path), filepath.ToSlash(mountpt)) {
  318. //Re-open the file system and set its flag to Open
  319. fsh.Closed = false
  320. }
  321. }
  322. log.Println("Executing Mount Command: ", "mount", "-t", mountingTool, "/dev/"+devID, mountpt)
  323. cmd := exec.Command("mount", "-t", mountingTool, "/dev/"+devID, mountpt)
  324. o, err := cmd.CombinedOutput()
  325. if err != nil {
  326. log.Println("Failed to mount "+devID, string(o))
  327. }
  328. return string(o), err
  329. }
  330. // Unmount a given mountpoint
  331. func Unmount(mountpt string, fsHandlers []*fs.FileSystemHandler) (string, error) {
  332. //Unmount the fsHandlers that related to this mountpt
  333. for _, fsh := range fsHandlers {
  334. if strings.Contains(filepath.ToSlash(fsh.Path), filepath.ToSlash(mountpt)) {
  335. //Close this file system handler
  336. fsh.Closed = true
  337. }
  338. }
  339. log.Println("Executing Umount Command: ", "umount", mountpt)
  340. cmd := exec.Command("umount", mountpt)
  341. o, err := cmd.CombinedOutput()
  342. return string(o), err
  343. }
  344. // Return a list of mountable directory
  345. func HandleListMountPoints(w http.ResponseWriter, r *http.Request) {
  346. mp, _ := filepath.Glob("/media/*")
  347. js, _ := json.Marshal(mp)
  348. utils.SendJSONResponse(w, string(js))
  349. }
  350. // Check if the device is mounted
  351. func checkDeviceMounted(devname string) (bool, error) {
  352. cmd := exec.Command("bash", "-c", "lsblk -f -b --json | grep "+devname)
  353. output, err := cmd.CombinedOutput()
  354. if err != nil {
  355. return false, err
  356. }
  357. //Convert the json map to generic string interface map
  358. jsonMap := make(map[string]interface{})
  359. err = json.Unmarshal(output, &jsonMap)
  360. if err != nil {
  361. return false, err
  362. }
  363. if jsonMap["mountpoint"] != nil {
  364. return true, nil
  365. } else {
  366. return false, nil
  367. }
  368. }
  369. func getDeviceMountPoint(devname string) (string, error) {
  370. cmd := exec.Command("bash", "-c", "lsblk -f -b --json | grep "+devname)
  371. output, err := cmd.CombinedOutput()
  372. if err != nil {
  373. return "", errors.New("Device not mounted")
  374. }
  375. //Convert the json map to generic string interface map
  376. jsonMap := make(map[string]interface{})
  377. err = json.Unmarshal(output, &jsonMap)
  378. if err != nil {
  379. return "", errors.New("Pharse mountpoint error")
  380. }
  381. if jsonMap["mountpoint"] != nil {
  382. return jsonMap["mountpoint"].(string), nil
  383. } else {
  384. return "", errors.New("Unable to get mountpoint from lsblk")
  385. }
  386. }
  387. // Check device valid, only usable in linux
  388. func checkDeviceValid(devname string) (bool, string) {
  389. //Check if the device name is valid
  390. match, _ := regexp.MatchString("sd[a-z][1-9]", devname)
  391. if !match {
  392. return false, ""
  393. }
  394. //Extract the device name from string
  395. re := regexp.MustCompile(`sd[a-z][1-9]`)
  396. devID := re.FindString(devname)
  397. if !utils.FileExists("/dev/" + devID) {
  398. return false, ""
  399. }
  400. return true, devID
  401. }
  402. func HandlePlatform(w http.ResponseWriter, r *http.Request) {
  403. js, _ := json.Marshal(runtime.GOOS)
  404. utils.SendJSONResponse(w, string(js))
  405. }
  406. /*
  407. HandleListDevicesWithInfo returns all block devices with their partitions,
  408. partition UUID (from blkid / lsblk -f), filesystem type, size and mount point.
  409. Used by the storage pool editor UI so the user can pick a partition by name
  410. instead of having to know the raw /dev path.
  411. GET /system/disk/diskmg/devices
  412. */
  413. // lsblkFull is used to parse a single lsblk -b --json -o NAME,SIZE,TYPE,FSTYPE,UUID,LABEL,MOUNTPOINT,MODEL call.
  414. type lsblkFull struct {
  415. Blockdevices []lsblkFullDev `json:"blockdevices"`
  416. }
  417. type lsblkFullDev struct {
  418. Name string `json:"name"`
  419. Size int64 `json:"size"`
  420. Type string `json:"type"`
  421. Fstype interface{} `json:"fstype"`
  422. UUID interface{} `json:"uuid"`
  423. Label interface{} `json:"label"`
  424. Mountpoint interface{} `json:"mountpoint"`
  425. Model interface{} `json:"model"`
  426. Children []lsblkFullDev `json:"children"`
  427. }
  428. // PartitionDeviceInfo is the per-partition record returned to the frontend.
  429. type PartitionDeviceInfo struct {
  430. Name string `json:"name"`
  431. DevPath string `json:"devpath"`
  432. Fstype string `json:"fstype"`
  433. UUID string `json:"uuid"`
  434. Label string `json:"label"`
  435. Size int64 `json:"size"`
  436. Mountpoint string `json:"mountpoint"`
  437. }
  438. // BlockDeviceInfo is the per-disk record returned to the frontend.
  439. type BlockDeviceInfo struct {
  440. Name string `json:"name"`
  441. Model string `json:"model"`
  442. Size int64 `json:"size"`
  443. Partitions []PartitionDeviceInfo `json:"partitions"`
  444. }
  445. func HandleListDevicesWithInfo(w http.ResponseWriter, r *http.Request) {
  446. if runtime.GOOS != "linux" {
  447. utils.SendErrorResponse(w, "This function is Linux only")
  448. return
  449. }
  450. cmd := exec.Command("lsblk", "-b", "--json", "-o", "NAME,SIZE,TYPE,FSTYPE,UUID,LABEL,MOUNTPOINT,MODEL")
  451. o, err := cmd.CombinedOutput()
  452. if err != nil {
  453. utils.SendErrorResponse(w, "lsblk error: "+err.Error())
  454. return
  455. }
  456. var raw lsblkFull
  457. if err := json.Unmarshal(o, &raw); err != nil {
  458. utils.SendErrorResponse(w, "parse error: "+err.Error())
  459. return
  460. }
  461. // helper: safely convert interface{} to string
  462. ifaceStr := func(v interface{}) string {
  463. if v == nil {
  464. return ""
  465. }
  466. return strings.TrimSpace(fmt.Sprintf("%v", v))
  467. }
  468. result := []BlockDeviceInfo{}
  469. for _, dev := range raw.Blockdevices {
  470. // Only show disk/loop/md types; skip rom, etc.
  471. if dev.Type != "disk" && dev.Type != "md" && dev.Type != "loop" {
  472. continue
  473. }
  474. diskInfo := BlockDeviceInfo{
  475. Name: dev.Name,
  476. Model: ifaceStr(dev.Model),
  477. Size: dev.Size,
  478. Partitions: []PartitionDeviceInfo{},
  479. }
  480. for _, part := range dev.Children {
  481. partInfo := PartitionDeviceInfo{
  482. Name: part.Name,
  483. DevPath: "/dev/" + part.Name,
  484. Fstype: ifaceStr(part.Fstype),
  485. UUID: ifaceStr(part.UUID),
  486. Label: ifaceStr(part.Label),
  487. Size: part.Size,
  488. Mountpoint: ifaceStr(part.Mountpoint),
  489. }
  490. diskInfo.Partitions = append(diskInfo.Partitions, partInfo)
  491. }
  492. // If a disk has no children (e.g. unpartitioned), expose the disk itself as
  493. // a single entry so it can still be selected.
  494. if len(diskInfo.Partitions) == 0 {
  495. diskInfo.Partitions = append(diskInfo.Partitions, PartitionDeviceInfo{
  496. Name: dev.Name,
  497. DevPath: "/dev/" + dev.Name,
  498. Fstype: ifaceStr(dev.Fstype),
  499. UUID: ifaceStr(dev.UUID),
  500. Label: ifaceStr(dev.Label),
  501. Size: dev.Size,
  502. Mountpoint: ifaceStr(dev.Mountpoint),
  503. })
  504. }
  505. result = append(result, diskInfo)
  506. }
  507. js, _ := json.Marshal(result)
  508. utils.SendJSONResponse(w, string(js))
  509. }