desktop.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. package main
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "io/ioutil"
  6. "log"
  7. "net/http"
  8. "os"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. module "imuslab.com/arozos/mod/modules"
  13. prout "imuslab.com/arozos/mod/prouter"
  14. )
  15. //Desktop script initiation
  16. func DesktopInit() {
  17. log.Println("Starting Desktop Services")
  18. router := prout.NewModuleRouter(prout.RouterOption{
  19. ModuleName: "Desktop",
  20. AdminOnly: false,
  21. UserHandler: userHandler,
  22. DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
  23. sendErrorResponse(w, "Permission Denied")
  24. },
  25. })
  26. //Register all the required API
  27. router.HandleFunc("/system/desktop/listDesktop", desktop_listFiles)
  28. router.HandleFunc("/system/desktop/theme", desktop_theme_handler)
  29. router.HandleFunc("/system/desktop/files", desktop_fileLocation_handler)
  30. router.HandleFunc("/system/desktop/host", desktop_hostdetailHandler)
  31. router.HandleFunc("/system/desktop/user", desktop_handleUserInfo)
  32. router.HandleFunc("/system/desktop/preference", desktop_preference_handler)
  33. router.HandleFunc("/system/desktop/createShortcut", desktop_shortcutHandler)
  34. //API related to desktop based operations
  35. router.HandleFunc("/system/desktop/opr/renameShortcut", desktop_handleShortcutRename)
  36. //Initialize desktop database
  37. err := sysdb.NewTable("desktop")
  38. if err != nil {
  39. log.Fatal(err)
  40. os.Exit(1)
  41. }
  42. //Register Desktop Module
  43. moduleHandler.RegisterModule(module.ModuleInfo{
  44. Name: "Desktop",
  45. Desc: "The Web Desktop experience for everyone",
  46. Group: "Interface Module",
  47. IconPath: "img/desktop/desktop.png",
  48. Version: internal_version,
  49. StartDir: "",
  50. SupportFW: false,
  51. LaunchFWDir: "",
  52. SupportEmb: false,
  53. })
  54. }
  55. /*
  56. FUNCTIONS RELATED TO PARSING DESKTOP FILE ICONS
  57. The functions in this section handle file listing and its icon locations.
  58. */
  59. func desktop_initUserFolderStructure(username string) {
  60. //Call to filesystem for creating user file struture at root dir
  61. userinfo, _ := userHandler.GetUserInfoFromUsername(username)
  62. homedir, err := userinfo.GetHomeDirectory()
  63. if err != nil {
  64. log.Println(err)
  65. }
  66. if !fileExists(filepath.Join(homedir, "Desktop")) {
  67. //Desktop directory not exists. Create one and copy a template desktop
  68. os.MkdirAll(homedir+"Desktop", 0755)
  69. templateFolder := "./system/desktop/template/"
  70. if fileExists(templateFolder) {
  71. templateFiles, _ := filepath.Glob(templateFolder + "*")
  72. for _, tfile := range templateFiles {
  73. input, _ := ioutil.ReadFile(tfile)
  74. ioutil.WriteFile(homedir+"Desktop/"+filepath.Base(tfile), input, 0755)
  75. }
  76. }
  77. }
  78. }
  79. //Return the information about the host
  80. func desktop_hostdetailHandler(w http.ResponseWriter, r *http.Request) {
  81. type returnStruct struct {
  82. Hostname string
  83. DeviceUUID string
  84. BuildVersion string
  85. InternalVersion string
  86. DeviceVendor string
  87. DeviceModel string
  88. VendorIcon string
  89. }
  90. jsonString, _ := json.Marshal(returnStruct{
  91. Hostname: *host_name,
  92. DeviceUUID: deviceUUID,
  93. BuildVersion: build_version,
  94. InternalVersion: internal_version,
  95. DeviceVendor: deviceVendor,
  96. DeviceModel: deviceModel,
  97. VendorIcon: iconVendor,
  98. })
  99. sendJSONResponse(w, string(jsonString))
  100. }
  101. func desktop_handleShortcutRename(w http.ResponseWriter, r *http.Request) {
  102. //Check if the user directory already exists
  103. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  104. if err != nil {
  105. sendErrorResponse(w, "User not logged in")
  106. return
  107. }
  108. //Get the shortcut file that is renaming
  109. target, err := mv(r, "src", false)
  110. if err != nil {
  111. sendErrorResponse(w, "Invalid shortcut file path given")
  112. return
  113. }
  114. //Get the new name
  115. new, err := mv(r, "new", false)
  116. if err != nil {
  117. sendErrorResponse(w, "Invalid new name given")
  118. return
  119. }
  120. //Check if the file actually exists and it is on desktop
  121. rpath, err := userinfo.VirtualPathToRealPath(target)
  122. if err != nil {
  123. sendErrorResponse(w, err.Error())
  124. return
  125. }
  126. if target[:14] != "user:/Desktop/" {
  127. sendErrorResponse(w, "Shortcut not on desktop")
  128. return
  129. }
  130. if !fileExists(rpath) {
  131. sendErrorResponse(w, "File not exists")
  132. return
  133. }
  134. //OK. Change the name of the shortcut
  135. originalShortcut, err := ioutil.ReadFile(rpath)
  136. if err != nil {
  137. sendErrorResponse(w, "Shortcut file read failed")
  138. return
  139. }
  140. lines := strings.Split(string(originalShortcut), "\n")
  141. if len(lines) < 4 {
  142. //Invalid shortcut properties
  143. sendErrorResponse(w, "Invalid shortcut file")
  144. return
  145. }
  146. //Change the 2nd line to the new name
  147. lines[1] = new
  148. newShortcutContent := strings.Join(lines, "\n")
  149. err = ioutil.WriteFile(rpath, []byte(newShortcutContent), 0755)
  150. if err != nil {
  151. sendErrorResponse(w, err.Error())
  152. return
  153. }
  154. sendOK(w)
  155. }
  156. func desktop_listFiles(w http.ResponseWriter, r *http.Request) {
  157. //Check if the user directory already exists
  158. userinfo, _ := userHandler.GetUserInfoFromRequest(w, r)
  159. username := userinfo.Username
  160. //Initiate the user folder structure. Do nothing if the structure already exists.
  161. desktop_initUserFolderStructure(username)
  162. //List all files inside the user desktop directory
  163. userDesktopRealpath, _ := userinfo.VirtualPathToRealPath("user:/Desktop/")
  164. files, err := filepath.Glob(userDesktopRealpath + "/*")
  165. if err != nil {
  166. log.Fatal("Error. Desktop unable to load user files for :" + username)
  167. return
  168. }
  169. //Desktop object structure
  170. type desktopObject struct {
  171. Filepath string
  172. Filename string
  173. Ext string
  174. IsDir bool
  175. IsEmptyDir bool
  176. IsShortcut bool
  177. IsShared bool
  178. ShortcutImage string
  179. ShortcutType string
  180. ShortcutName string
  181. ShortcutPath string
  182. IconX int
  183. IconY int
  184. }
  185. var desktopFiles []desktopObject
  186. for _, this := range files {
  187. //Always use linux convension for directory seperator
  188. if filepath.Base(this)[:1] == "." {
  189. //Skipping hidden files
  190. continue
  191. }
  192. this = filepath.ToSlash(this)
  193. thisFileObject := new(desktopObject)
  194. thisFileObject.Filepath, _ = userinfo.RealPathToVirtualPath(this)
  195. thisFileObject.Filename = filepath.Base(this)
  196. thisFileObject.Ext = filepath.Ext(this)
  197. thisFileObject.IsDir = IsDir(this)
  198. if thisFileObject.IsDir {
  199. //Check if this dir is empty
  200. filesInFolder, _ := filepath.Glob(filepath.ToSlash(filepath.Clean(this)) + "/*")
  201. fc := 0
  202. for _, f := range filesInFolder {
  203. if filepath.Base(f)[:1] != "." {
  204. fc++
  205. }
  206. }
  207. if fc > 0 {
  208. thisFileObject.IsEmptyDir = false
  209. } else {
  210. thisFileObject.IsEmptyDir = true
  211. }
  212. } else {
  213. //File object. Default true
  214. thisFileObject.IsEmptyDir = true
  215. }
  216. //Check if the file is a shortcut
  217. isShortcut := false
  218. if filepath.Ext(this) == ".shortcut" {
  219. isShortcut = true
  220. shortcutInfo, _ := ioutil.ReadFile(this)
  221. infoSegments := strings.Split(strings.ReplaceAll(string(shortcutInfo), "\r\n", "\n"), "\n")
  222. if len(infoSegments) < 4 {
  223. thisFileObject.ShortcutType = "invalid"
  224. } else {
  225. thisFileObject.ShortcutType = infoSegments[0]
  226. thisFileObject.ShortcutName = infoSegments[1]
  227. thisFileObject.ShortcutPath = infoSegments[2]
  228. thisFileObject.ShortcutImage = infoSegments[3]
  229. }
  230. }
  231. thisFileObject.IsShortcut = isShortcut
  232. //Check if this file is shared
  233. thisFileObject.IsShared = shareManager.FileIsShared(this)
  234. //Check the file location
  235. username, _ := authAgent.GetUserName(w, r)
  236. x, y, _ := getDesktopLocatioFromPath(thisFileObject.Filename, username)
  237. //This file already have a location on desktop
  238. thisFileObject.IconX = x
  239. thisFileObject.IconY = y
  240. desktopFiles = append(desktopFiles, *thisFileObject)
  241. }
  242. //Convert the struct to json string
  243. jsonString, _ := json.Marshal(desktopFiles)
  244. sendJSONResponse(w, string(jsonString))
  245. }
  246. //functions to handle desktop icon locations. Location is directly written into the center db.
  247. func getDesktopLocatioFromPath(filename string, username string) (int, int, error) {
  248. //As path include username, there is no different if there are username in the key
  249. locationdata := ""
  250. err := sysdb.Read("desktop", username+"/filelocation/"+filename, &locationdata)
  251. if err != nil {
  252. //The file location is not set. Return error
  253. return -1, -1, errors.New("This file do not have a location registry")
  254. }
  255. type iconLocation struct {
  256. X int
  257. Y int
  258. }
  259. thisFileLocation := iconLocation{
  260. X: -1,
  261. Y: -1,
  262. }
  263. //Start parsing the from the json data
  264. json.Unmarshal([]byte(locationdata), &thisFileLocation)
  265. return thisFileLocation.X, thisFileLocation.Y, nil
  266. }
  267. //Set the icon location of a given filepath
  268. func setDesktopLocationFromPath(filename string, username string, x int, y int) error {
  269. //You cannot directly set path of others people's deskop. Hence, fullpath needed to be parsed from auth username
  270. userinfo, _ := userHandler.GetUserInfoFromUsername(username)
  271. desktoppath, _ := userinfo.VirtualPathToRealPath("user:/Desktop/")
  272. path := filepath.Join(desktoppath, filename)
  273. type iconLocation struct {
  274. X int
  275. Y int
  276. }
  277. newLocation := new(iconLocation)
  278. newLocation.X = x
  279. newLocation.Y = y
  280. //Check if the file exits
  281. if fileExists(path) == false {
  282. return errors.New("Given filename not exists.")
  283. }
  284. //Parse the location to json
  285. jsonstring, err := json.Marshal(newLocation)
  286. if err != nil {
  287. log.Fatal("Unable to parse new file location on desktop for file: " + path)
  288. return err
  289. }
  290. //log.Println(key,string(jsonstring))
  291. //Write result to database
  292. sysdb.Write("desktop", username+"/filelocation/"+filename, string(jsonstring))
  293. return nil
  294. }
  295. func delDesktopLocationFromPath(filename string, username string) {
  296. //Delete a file icon location from db
  297. sysdb.Delete("desktop", username+"/filelocation/"+filename)
  298. }
  299. //Return the user information to the client
  300. func desktop_handleUserInfo(w http.ResponseWriter, r *http.Request) {
  301. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  302. if err != nil {
  303. sendErrorResponse(w, err.Error())
  304. return
  305. }
  306. type returnStruct struct {
  307. Username string
  308. UserIcon string
  309. UserGroups []string
  310. IsAdmin bool
  311. StorageQuotaTotal int64
  312. StorageQuotaLeft int64
  313. }
  314. //Calculate the storage quota left
  315. remainingQuota := userinfo.StorageQuota.TotalStorageQuota - userinfo.StorageQuota.UsedStorageQuota
  316. if userinfo.StorageQuota.TotalStorageQuota == -1 {
  317. remainingQuota = -1
  318. }
  319. //Get the list of user permission group names
  320. pgs := []string{}
  321. for _, pg := range userinfo.GetUserPermissionGroup() {
  322. pgs = append(pgs, pg.Name)
  323. }
  324. jsonString, _ := json.Marshal(returnStruct{
  325. Username: userinfo.Username,
  326. UserIcon: userinfo.GetUserIcon(),
  327. IsAdmin: userinfo.IsAdmin(),
  328. UserGroups: pgs,
  329. StorageQuotaTotal: userinfo.StorageQuota.GetUserStorageQuota(),
  330. StorageQuotaLeft: remainingQuota,
  331. })
  332. sendJSONResponse(w, string(jsonString))
  333. }
  334. //Icon handling function for web endpoint
  335. func desktop_fileLocation_handler(w http.ResponseWriter, r *http.Request) {
  336. get, _ := mv(r, "get", true) //Check if there are get request for a given filepath
  337. set, _ := mv(r, "set", true) //Check if there are any set request for a given filepath
  338. del, _ := mv(r, "del", true) //Delete the given filename coordinate
  339. if set != "" {
  340. //Set location with given paramter
  341. x := 0
  342. y := 0
  343. sx, _ := mv(r, "x", true)
  344. sy, _ := mv(r, "y", true)
  345. path := set
  346. x, err := strconv.Atoi(sx)
  347. if err != nil {
  348. x = 0
  349. }
  350. y, err = strconv.Atoi(sy)
  351. if err != nil {
  352. y = 0
  353. }
  354. //Set location of icon from path
  355. username, _ := authAgent.GetUserName(w, r)
  356. err = setDesktopLocationFromPath(path, username, x, y)
  357. if err != nil {
  358. sendErrorResponse(w, err.Error())
  359. return
  360. }
  361. sendJSONResponse(w, string("\"OK\""))
  362. } else if get != "" {
  363. username, _ := authAgent.GetUserName(w, r)
  364. x, y, _ := getDesktopLocatioFromPath(get, username)
  365. result := []int{x, y}
  366. json_string, _ := json.Marshal(result)
  367. sendJSONResponse(w, string(json_string))
  368. } else if del != "" {
  369. username, _ := authAgent.GetUserName(w, r)
  370. delDesktopLocationFromPath(del, username)
  371. } else {
  372. //No argument has been set
  373. sendTextResponse(w, "Paramter missing.")
  374. }
  375. }
  376. //////////////////////////////// END OF DESKTOP FILE ICON HANDLER ///////////////////////////////////////////////////
  377. func desktop_theme_handler(w http.ResponseWriter, r *http.Request) {
  378. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  379. if err != nil {
  380. sendErrorResponse(w, "User not logged in")
  381. return
  382. }
  383. username := userinfo.Username
  384. //Check if the set GET paramter is set.
  385. targetTheme, _ := mv(r, "set", false)
  386. getUserTheme, _ := mv(r, "get", false)
  387. loadUserTheme, _ := mv(r, "load", false)
  388. if targetTheme == "" && getUserTheme == "" && loadUserTheme == "" {
  389. //List all the currnet themes in the list
  390. themes, err := filepath.Glob("web/img/desktop/bg/*")
  391. if err != nil {
  392. log.Fatal("Error. Unable to search bg from destkop image root. Are you sure the web data folder exists?")
  393. return
  394. }
  395. //Prase the results to json array
  396. //Tips: You must use captial letter for varable in struct that is accessable as public :)
  397. type desktopTheme struct {
  398. Theme string
  399. Bglist []string
  400. }
  401. var desktopThemeList []desktopTheme
  402. acceptBGFormats := []string{
  403. ".jpg",
  404. ".png",
  405. ".gif",
  406. }
  407. for _, file := range themes {
  408. if IsDir(file) {
  409. thisTheme := new(desktopTheme)
  410. thisTheme.Theme = filepath.Base(file)
  411. bglist, _ := filepath.Glob(file + "/*")
  412. var thisbglist []string
  413. for _, bg := range bglist {
  414. ext := filepath.Ext(bg)
  415. //if (sliceutil.Contains(acceptBGFormats, ext) ){
  416. if stringInSlice(ext, acceptBGFormats) {
  417. //This file extension is supported
  418. thisbglist = append(thisbglist, filepath.Base(bg))
  419. }
  420. }
  421. thisTheme.Bglist = thisbglist
  422. desktopThemeList = append(desktopThemeList, *thisTheme)
  423. }
  424. }
  425. //Return the results as JSON string
  426. jsonString, err := json.Marshal(desktopThemeList)
  427. if err != nil {
  428. log.Fatal(err)
  429. }
  430. sendJSONResponse(w, string(jsonString))
  431. return
  432. } else if getUserTheme == "true" {
  433. //Get the user's theme from database
  434. result := ""
  435. sysdb.Read("desktop", username+"/theme", &result)
  436. if result == "" {
  437. //This user has not set a theme yet. Use default
  438. sendJSONResponse(w, string("\"default\""))
  439. return
  440. } else {
  441. //This user already set a theme. Use its set theme
  442. sendJSONResponse(w, string("\""+result+"\""))
  443. return
  444. }
  445. } else if loadUserTheme != "" {
  446. //Load user theme base on folder path
  447. rpath, err := userinfo.VirtualPathToRealPath(loadUserTheme)
  448. if err != nil {
  449. sendErrorResponse(w, "Custom folder load failed")
  450. return
  451. }
  452. //Check if the folder exists
  453. if !fileExists(rpath) {
  454. sendErrorResponse(w, "Custom folder load failed")
  455. return
  456. }
  457. if userinfo.CanRead(loadUserTheme) == false {
  458. //No read permission
  459. sendErrorResponse(w, "Permission denied")
  460. return
  461. }
  462. //Scan for jpg, gif or png
  463. imageList := []string{}
  464. scanPath := filepath.ToSlash(filepath.Clean(rpath)) + "/"
  465. pngFiles, _ := filepath.Glob(scanPath + "*.png")
  466. jpgFiles, _ := filepath.Glob(scanPath + "*.jpg")
  467. gifFiles, _ := filepath.Glob(scanPath + "*.gif")
  468. //Merge all 3 slice into one image list
  469. imageList = append(imageList, pngFiles...)
  470. imageList = append(imageList, jpgFiles...)
  471. imageList = append(imageList, gifFiles...)
  472. //Convert the image list back to vpaths
  473. virtualImageList := []string{}
  474. for _, image := range imageList {
  475. vpath, err := userinfo.RealPathToVirtualPath(image)
  476. if err != nil {
  477. continue
  478. }
  479. virtualImageList = append(virtualImageList, vpath)
  480. }
  481. js, _ := json.Marshal(virtualImageList)
  482. sendJSONResponse(w, string(js))
  483. } else if targetTheme != "" {
  484. //Set the current user theme
  485. sysdb.Write("desktop", username+"/theme", targetTheme)
  486. sendJSONResponse(w, "\"OK\"")
  487. return
  488. }
  489. }
  490. func desktop_preference_handler(w http.ResponseWriter, r *http.Request) {
  491. preferenceType, _ := mv(r, "preference", false)
  492. value, _ := mv(r, "value", false)
  493. remove, _ := mv(r, "remove", false)
  494. username, err := authAgent.GetUserName(w, r)
  495. if err != nil {
  496. //user not logged in. Redirect to login page.
  497. sendErrorResponse(w, "User not logged in")
  498. return
  499. }
  500. if preferenceType == "" && value == "" {
  501. //Invalid options. Return error reply.
  502. sendTextResponse(w, "Error. Undefined paramter.")
  503. return
  504. } else if preferenceType != "" && value == "" && remove == "" {
  505. //Getting config from the key.
  506. result := ""
  507. sysdb.Read("desktop", username+"/preference/"+preferenceType, &result)
  508. jsonString, _ := json.Marshal(result)
  509. sendJSONResponse(w, string(jsonString))
  510. return
  511. } else if preferenceType != "" && value == "" && remove == "true" {
  512. //Remove mode
  513. sysdb.Delete("desktop", username+"/preference/"+preferenceType)
  514. sendOK(w)
  515. return
  516. } else if preferenceType != "" && value != "" {
  517. //Setting config from the key
  518. sysdb.Write("desktop", username+"/preference/"+preferenceType, value)
  519. sendJSONResponse(w, "\"OK\"")
  520. return
  521. } else {
  522. sendTextResponse(w, "Error. Undefined paramter.")
  523. return
  524. }
  525. }
  526. func desktop_shortcutHandler(w http.ResponseWriter, r *http.Request) {
  527. username, err := authAgent.GetUserName(w, r)
  528. if err != nil {
  529. //user not logged in. Redirect to login page.
  530. sendErrorResponse(w, "User not logged in")
  531. return
  532. }
  533. userinfo, _ := userHandler.GetUserInfoFromUsername(username)
  534. shortcutType, err := mv(r, "stype", true)
  535. if err != nil {
  536. sendErrorResponse(w, err.Error())
  537. return
  538. }
  539. shortcutText, err := mv(r, "stext", true)
  540. if err != nil {
  541. sendErrorResponse(w, err.Error())
  542. return
  543. }
  544. shortcutPath, err := mv(r, "spath", true)
  545. if err != nil {
  546. sendErrorResponse(w, err.Error())
  547. return
  548. }
  549. shortcutIcon, err := mv(r, "sicon", true)
  550. if err != nil {
  551. sendErrorResponse(w, err.Error())
  552. return
  553. }
  554. //OK to proceed. Generate a shortcut on the user desktop
  555. userDesktopPath, _ := userinfo.VirtualPathToRealPath("user:/Desktop")
  556. if !fileExists(userDesktopPath) {
  557. os.MkdirAll(userDesktopPath, 0755)
  558. }
  559. //Check if there are desktop icon. If yes, override icon on module
  560. if shortcutType == "module" && fileExists("./web/"+filepath.ToSlash(filepath.Dir(shortcutIcon)+"/desktop_icon.png")) {
  561. shortcutIcon = filepath.ToSlash(filepath.Join(filepath.Dir(shortcutIcon), "/desktop_icon.png"))
  562. }
  563. shortcutText = strings.ReplaceAll(shortcutText, "/", "")
  564. for strings.Contains(shortcutText, "../") {
  565. shortcutText = strings.ReplaceAll(shortcutText, "../", "")
  566. }
  567. shortcutFilename := userDesktopPath + "/" + shortcutText + ".shortcut"
  568. counter := 1
  569. for fileExists(shortcutFilename) {
  570. shortcutFilename = userDesktopPath + "/" + shortcutText + "(" + IntToString(counter) + ")" + ".shortcut"
  571. counter++
  572. }
  573. err = ioutil.WriteFile(shortcutFilename, []byte(shortcutType+"\n"+shortcutText+"\n"+shortcutPath+"\n"+shortcutIcon), 0755)
  574. if err != nil {
  575. sendErrorResponse(w, err.Error())
  576. return
  577. }
  578. sendOK(w)
  579. }