api.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. package share
  2. /*
  3. Share management API
  4. Authenticated endpoints under /system/file_system/share/, registered by
  5. RegisterAPIEndpoints. Reading the share state of a path (info, checkShared)
  6. never creates a share; shares are only added through create (or the legacy
  7. get-or-create endpoint "new").
  8. */
  9. import (
  10. "encoding/json"
  11. "errors"
  12. "net/http"
  13. "sort"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "imuslab.com/arozos/mod/filesystem/arozfs"
  18. "imuslab.com/arozos/mod/share/shareEntry"
  19. "imuslab.com/arozos/mod/user"
  20. "imuslab.com/arozos/mod/utils"
  21. )
  22. // APIPrefix is the path every share management endpoint is mounted under
  23. const APIPrefix = "/system/file_system/share/"
  24. // EndpointRegistrar is anything that can mount an authenticated endpoint,
  25. // usually the permission router (prouter.RouterDef) of the File Manager.
  26. type EndpointRegistrar interface {
  27. HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request)) error
  28. }
  29. // RegisterAPIEndpoints mounts the share management API on the given router.
  30. // The router is expected to handle login and module permission checks.
  31. func (s *Manager) RegisterAPIEndpoints(router EndpointRegistrar) {
  32. router.HandleFunc(APIPrefix+"info", s.HandleShareInfo)
  33. router.HandleFunc(APIPrefix+"create", s.HandleCreateShare)
  34. router.HandleFunc(APIPrefix+"update", s.HandleUpdateShare)
  35. router.HandleFunc(APIPrefix+"delete", s.HandleDeleteShare)
  36. router.HandleFunc(APIPrefix+"recipients", s.HandleListRecipients)
  37. router.HandleFunc(APIPrefix+"checkShared", s.HandleShareCheck)
  38. router.HandleFunc(APIPrefix+"list", s.HandleListAllShares)
  39. //Legacy endpoints, kept for older web apps and float window callers
  40. router.HandleFunc(APIPrefix+"new", s.HandleCreateNewShare)
  41. router.HandleFunc(APIPrefix+"edit", s.HandleEditShare)
  42. }
  43. // ShareInfo is the client facing view of a share. It never carries the
  44. // password hash or the real path of the shared file.
  45. type ShareInfo struct {
  46. UUID string
  47. FileVirtualPath string
  48. Owner string
  49. Permission string
  50. Accessibles []string
  51. IsFolder bool
  52. AccessLevel string
  53. AllowDownload bool
  54. ShowFileList bool
  55. HasPassword bool
  56. ExpireAt int64
  57. CreatedAt int64
  58. IsExpired bool
  59. SharePath string //Relative share link, e.g. /share/<uuid>
  60. }
  61. func newShareInfo(so *shareEntry.ShareOption, now time.Time) *ShareInfo {
  62. return &ShareInfo{
  63. UUID: so.UUID,
  64. FileVirtualPath: so.FileVirtualPath,
  65. Owner: so.Owner,
  66. Permission: so.Permission,
  67. Accessibles: append([]string{}, so.Accessibles...),
  68. IsFolder: so.IsFolder,
  69. AccessLevel: so.AccessLevel,
  70. AllowDownload: so.AllowDownload,
  71. ShowFileList: so.ShowFileList,
  72. HasPassword: so.HasPassword(),
  73. ExpireAt: so.ExpireAt,
  74. CreatedAt: so.CreatedAt,
  75. IsExpired: so.IsExpired(now),
  76. SharePath: "/share/" + so.UUID,
  77. }
  78. }
  79. func sendJSON(w http.ResponseWriter, v interface{}) {
  80. js, err := json.Marshal(v)
  81. if err != nil {
  82. utils.SendErrorResponse(w, "Unable to encode response")
  83. return
  84. }
  85. utils.SendJSONResponse(w, string(js))
  86. }
  87. // requirePost rejects requests that change state through GET, so a share
  88. // cannot be created or edited by an image tag or a link on another site.
  89. func requirePost(w http.ResponseWriter, r *http.Request) bool {
  90. if r.Method != http.MethodPost {
  91. w.WriteHeader(http.StatusMethodNotAllowed)
  92. utils.SendErrorResponse(w, "Method not allowed")
  93. return false
  94. }
  95. return true
  96. }
  97. /*
  98. parseShareSettings reads the share settings from the request. Every field
  99. is optional, a missing field is left unchanged.
  100. permission anyone / signedin / samegroup / groups / users
  101. accessibles comma separated user or group names (groups / users)
  102. mode legacy form of the two above, e.g. "users:alice,bob"
  103. accessLevel view / edit (edit is folder only)
  104. allowDownload true / false
  105. showFileList true / false
  106. password new password for the share
  107. clearPassword true to remove the password
  108. expireAt unix timestamp in seconds, 0 for never
  109. expireIn seconds from now, takes priority over expireAt
  110. */
  111. func parseShareSettings(r *http.Request) (shareEntry.ShareSettings, error) {
  112. settings := shareEntry.ShareSettings{}
  113. r.ParseForm()
  114. permission := strings.TrimSpace(r.Form.Get("permission"))
  115. if permission == "" {
  116. if mode := strings.TrimSpace(r.Form.Get("mode")); mode != "" {
  117. ok, sharetype, names := validateShareModes(mode)
  118. if !ok {
  119. return settings, errors.New("Invalid share setting")
  120. }
  121. permission = sharetype
  122. settings.Accessibles = names
  123. }
  124. } else {
  125. settings.Accessibles = splitNameList(r.Form.Get("accessibles"))
  126. }
  127. if permission != "" {
  128. settings.Permission = &permission
  129. }
  130. if accessLevel := strings.TrimSpace(r.Form.Get("accessLevel")); accessLevel != "" {
  131. settings.AccessLevel = &accessLevel
  132. }
  133. if v, err := utils.PostBool(r, "allowDownload"); err == nil {
  134. settings.AllowDownload = &v
  135. }
  136. if v, err := utils.PostBool(r, "showFileList"); err == nil {
  137. settings.ShowFileList = &v
  138. }
  139. if clear, _ := utils.PostBool(r, "clearPassword"); clear {
  140. empty := ""
  141. settings.Password = &empty
  142. } else if password := r.Form.Get("password"); password != "" {
  143. settings.Password = &password
  144. }
  145. if expireIn := strings.TrimSpace(r.Form.Get("expireIn")); expireIn != "" {
  146. seconds, err := strconv.ParseInt(expireIn, 10, 64)
  147. if err != nil || seconds <= 0 {
  148. return settings, errors.New("Invalid expiry duration")
  149. }
  150. expireAt := time.Now().Unix() + seconds
  151. settings.ExpireAt = &expireAt
  152. } else if expireAtStr := strings.TrimSpace(r.Form.Get("expireAt")); expireAtStr != "" {
  153. expireAt, err := strconv.ParseInt(expireAtStr, 10, 64)
  154. if err != nil {
  155. return settings, errors.New("Invalid expiry date")
  156. }
  157. settings.ExpireAt = &expireAt
  158. }
  159. return settings, nil
  160. }
  161. func splitNameList(list string) []string {
  162. results := []string{}
  163. for _, name := range strings.Split(list, ",") {
  164. if name = strings.TrimSpace(name); name != "" {
  165. results = append(results, name)
  166. }
  167. }
  168. return results
  169. }
  170. /*
  171. Validate Share Mode string
  172. will return
  173. 1. bool => Is valid
  174. 2. permission type: {basic / groups / users}
  175. 3. mode string
  176. */
  177. func validateShareModes(mode string) (bool, string, []string) {
  178. // user:a,b,c,d
  179. validModes := []string{shareEntry.PermissionAnyone, shareEntry.PermissionSignedIn, shareEntry.PermissionSameGroup}
  180. if utils.StringInArray(validModes, mode) {
  181. //Standard modes
  182. return true, mode, []string{}
  183. } else if len(mode) > 7 && mode[:7] == "groups:" {
  184. //Handle custom group case like groups:a,b,c,d
  185. groups := splitNameList(mode[7:])
  186. return len(groups) > 0, shareEntry.PermissionGroups, groups
  187. } else if len(mode) > 6 && mode[:6] == "users:" {
  188. //Handle custom usersname like users:a,b,c,d
  189. users := splitNameList(mode[6:])
  190. return len(users) > 0, shareEntry.PermissionUsers, users
  191. }
  192. return false, "", []string{}
  193. }
  194. // pathShareInfo is the response of the info endpoint
  195. type pathShareInfo struct {
  196. Path string
  197. Filename string
  198. IsFolder bool
  199. Size int64 //Bytes, -1 if not calculated
  200. FileCount int //Files inside a folder, -1 if not calculated
  201. IsShared bool
  202. CanManage bool //The user can edit or remove every share listed below
  203. Shares []*ShareInfo
  204. }
  205. /*
  206. HandleShareInfo returns the share metadata of a path without changing anything.
  207. GET share/info?path=user:/Music/album[&folderStat=true]
  208. folderStat walks a shared folder to report its size and file count, which can
  209. be slow on a large folder, so the dialog asks for it separately.
  210. */
  211. func (s *Manager) HandleShareInfo(w http.ResponseWriter, r *http.Request) {
  212. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  213. if err != nil {
  214. utils.SendErrorResponse(w, "User not logged in")
  215. return
  216. }
  217. vpath, err := utils.GetPara(r, "path")
  218. if err != nil {
  219. utils.SendErrorResponse(w, "Invalid path given")
  220. return
  221. }
  222. fsh, err := userinfo.GetFileSystemHandlerFromVirtualPath(vpath)
  223. if err != nil {
  224. utils.SendErrorResponse(w, "Invalid path given")
  225. return
  226. }
  227. fshAbs := fsh.FileSystemAbstraction
  228. rpath, err := fshAbs.VirtualPathToRealPath(vpath, userinfo.Username)
  229. if err != nil || !fshAbs.FileExists(rpath) {
  230. utils.SendErrorResponse(w, "File not exists")
  231. return
  232. }
  233. result := pathShareInfo{
  234. Path: vpath,
  235. Filename: arozfs.Base(rpath),
  236. IsFolder: fshAbs.IsDir(rpath),
  237. Size: -1,
  238. FileCount: -1,
  239. Shares: []*ShareInfo{},
  240. }
  241. if !result.IsFolder {
  242. result.Size = fshAbs.GetFileSize(rpath)
  243. } else if folderStat, _ := utils.GetBool(r, "folderStat"); folderStat {
  244. result.Size, result.FileCount = fsh.GetDirctorySizeFromRealPath(rpath, false)
  245. }
  246. now := time.Now()
  247. result.IsShared = s.FileIsShared(userinfo, vpath)
  248. for _, so := range s.ListSharesOfPath(userinfo, vpath) {
  249. result.Shares = append(result.Shares, newShareInfo(so, now))
  250. }
  251. //Shares listed are the ones this user can manage. A shared path with
  252. //none listed belongs to someone else.
  253. result.CanManage = !result.IsShared || len(result.Shares) > 0
  254. sendJSON(w, result)
  255. }
  256. // HandleCreateShare always adds a new share to the given path (POST path + settings)
  257. func (s *Manager) HandleCreateShare(w http.ResponseWriter, r *http.Request) {
  258. if !requirePost(w, r) {
  259. return
  260. }
  261. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  262. if err != nil {
  263. utils.SendErrorResponse(w, "User not logged in")
  264. return
  265. }
  266. vpath, err := utils.PostPara(r, "path")
  267. if err != nil {
  268. utils.SendErrorResponse(w, "Invalid path given")
  269. return
  270. }
  271. settings, err := parseShareSettings(r)
  272. if err != nil {
  273. utils.SendErrorResponse(w, err.Error())
  274. return
  275. }
  276. so, err := s.CreateShare(userinfo, vpath, settings)
  277. if err != nil {
  278. utils.SendErrorResponse(w, err.Error())
  279. return
  280. }
  281. sendJSON(w, newShareInfo(so, time.Now()))
  282. }
  283. // HandleUpdateShare changes the settings of a share (POST uuid + settings)
  284. func (s *Manager) HandleUpdateShare(w http.ResponseWriter, r *http.Request) {
  285. if !requirePost(w, r) {
  286. return
  287. }
  288. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  289. if err != nil {
  290. utils.SendErrorResponse(w, "User not logged in")
  291. return
  292. }
  293. uuid, err := utils.PostPara(r, "uuid")
  294. if err != nil {
  295. utils.SendErrorResponse(w, "Invalid share uuid given")
  296. return
  297. }
  298. settings, err := parseShareSettings(r)
  299. if err != nil {
  300. utils.SendErrorResponse(w, err.Error())
  301. return
  302. }
  303. so, err := s.UpdateShare(userinfo, uuid, settings)
  304. if err != nil {
  305. utils.SendErrorResponse(w, err.Error())
  306. return
  307. }
  308. sendJSON(w, newShareInfo(so, time.Now()))
  309. }
  310. // Check if a file is shared. Returns the first share of the path for older callers.
  311. func (s *Manager) HandleShareCheck(w http.ResponseWriter, r *http.Request) {
  312. //Get the vpath from paramters
  313. vpath, err := utils.PostPara(r, "path")
  314. if err != nil {
  315. utils.SendErrorResponse(w, "Invalid path given")
  316. return
  317. }
  318. //Get userinfo
  319. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  320. if err != nil {
  321. utils.SendErrorResponse(w, "User not logged in")
  322. return
  323. }
  324. type Result struct {
  325. IsShared bool
  326. ShareCount int
  327. ShareUUID *ShareInfo
  328. }
  329. result := Result{
  330. IsShared: s.FileIsShared(userinfo, vpath),
  331. ShareUUID: &ShareInfo{},
  332. }
  333. shares := s.ListSharesOfPath(userinfo, vpath)
  334. result.ShareCount = len(shares)
  335. if result.IsShared {
  336. now := time.Now()
  337. for _, so := range shares {
  338. if !so.IsExpired(now) {
  339. result.ShareUUID = newShareInfo(so, now)
  340. break
  341. }
  342. }
  343. }
  344. sendJSON(w, result)
  345. }
  346. // Legacy: return the first share of the path, creating one if it is not shared yet
  347. func (s *Manager) HandleCreateNewShare(w http.ResponseWriter, r *http.Request) {
  348. //Get the vpath from paramters
  349. vpath, err := utils.PostPara(r, "path")
  350. if err != nil {
  351. utils.SendErrorResponse(w, "Invalid path given")
  352. return
  353. }
  354. //Get userinfo
  355. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  356. if err != nil {
  357. utils.SendErrorResponse(w, "User not logged in")
  358. return
  359. }
  360. //Get the target fsh that this vpath come from
  361. vpathSourceFsh := userinfo.GetRootFSHFromVpathInUserScope(vpath)
  362. if vpathSourceFsh == nil {
  363. utils.SendErrorResponse(w, "Invalid vpath given")
  364. return
  365. }
  366. share, err := s.CreateNewShare(userinfo, vpathSourceFsh, vpath)
  367. if err != nil {
  368. utils.SendErrorResponse(w, err.Error())
  369. return
  370. }
  371. sendJSON(w, newShareInfo(share, time.Now()))
  372. }
  373. // Handle Share Edit (legacy, prefer share/update).
  374. // For allowing groups / users, use the following syntax
  375. // groups:group1,group2,group3
  376. // users:user1,user2,user3
  377. // For basic modes, use the following keywords
  378. // anyone / signedin / samegroup
  379. // anyone: Anyone who has the link
  380. // signedin: Anyone logged in to this system
  381. // samegroup: The requesting user has the same (or more) user group as the share owner
  382. func (s *Manager) HandleEditShare(w http.ResponseWriter, r *http.Request) {
  383. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  384. if err != nil {
  385. utils.SendErrorResponse(w, "User not logged in")
  386. return
  387. }
  388. uuid, err := utils.PostPara(r, "uuid")
  389. if err != nil {
  390. utils.SendErrorResponse(w, "Invalid path given")
  391. return
  392. }
  393. shareMode, _ := utils.PostPara(r, "mode")
  394. if shareMode == "" {
  395. shareMode = shareEntry.PermissionSignedIn
  396. }
  397. //Validate and extract the storage mode
  398. ok, sharetype, names := validateShareModes(shareMode)
  399. if !ok {
  400. utils.SendErrorResponse(w, "Invalid share setting")
  401. return
  402. }
  403. _, err = s.UpdateShare(userinfo, uuid, shareEntry.ShareSettings{
  404. Permission: &sharetype,
  405. Accessibles: names,
  406. })
  407. if err != nil {
  408. utils.SendErrorResponse(w, err.Error())
  409. return
  410. }
  411. utils.SendOK(w)
  412. }
  413. // Delete a share by uuid, or every share on a path by vpath
  414. func (s *Manager) HandleDeleteShare(w http.ResponseWriter, r *http.Request) {
  415. //Get userinfo
  416. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  417. if err != nil {
  418. utils.SendErrorResponse(w, "User not logged in")
  419. return
  420. }
  421. //Get the vpath from paramters
  422. uuid, err := utils.PostPara(r, "uuid")
  423. if err == nil {
  424. err = s.DeleteShareByUUID(userinfo, uuid)
  425. } else {
  426. //Try to get it from vpath (or path, as sent by the Photo app)
  427. vpath, perr := utils.PostPara(r, "vpath")
  428. if perr != nil {
  429. vpath, perr = utils.PostPara(r, "path")
  430. }
  431. if perr != nil {
  432. utils.SendErrorResponse(w, "Invalid uuid or vpath given")
  433. return
  434. }
  435. if !s.FileIsShared(userinfo, vpath) && len(s.ListSharesOfPath(userinfo, vpath)) == 0 {
  436. utils.SendErrorResponse(w, "Invalid uuid or vpath given")
  437. return
  438. }
  439. err = s.DeleteShareByVpath(userinfo, vpath)
  440. }
  441. if err != nil {
  442. utils.SendErrorResponse(w, err.Error())
  443. } else {
  444. utils.SendOK(w)
  445. }
  446. }
  447. /*
  448. HandleListRecipients lists the users and permission groups a share can be
  449. given to, for the invite pickers of the share dialog.
  450. */
  451. func (s *Manager) HandleListRecipients(w http.ResponseWriter, r *http.Request) {
  452. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  453. if err != nil {
  454. utils.SendErrorResponse(w, "User not logged in")
  455. return
  456. }
  457. type Recipient struct {
  458. Username string
  459. Groups []string
  460. IsSelf bool
  461. }
  462. type Result struct {
  463. Users []Recipient
  464. Groups []string
  465. }
  466. result := Result{Users: []Recipient{}, Groups: []string{}}
  467. permissionHandler := s.options.UserHandler.GetPermissionHandler()
  468. for _, username := range s.options.UserHandler.GetAuthAgent().ListUsers() {
  469. groups := []string{}
  470. if pgs, err := permissionHandler.GetUsersPermissionGroup(username); err == nil {
  471. for _, pg := range pgs {
  472. groups = append(groups, pg.Name)
  473. }
  474. }
  475. result.Users = append(result.Users, Recipient{
  476. Username: username,
  477. Groups: groups,
  478. IsSelf: username == userinfo.Username,
  479. })
  480. }
  481. for _, pg := range permissionHandler.PermissionGroups {
  482. result.Groups = append(result.Groups, pg.Name)
  483. }
  484. sort.Slice(result.Users, func(i, j int) bool {
  485. return strings.ToLower(result.Users[i].Username) < strings.ToLower(result.Users[j].Username)
  486. })
  487. sort.Strings(result.Groups)
  488. sendJSON(w, result)
  489. }
  490. // List all the shares the user can see (used by the Shares Manager)
  491. func (s *Manager) HandleListAllShares(w http.ResponseWriter, r *http.Request) {
  492. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  493. if err != nil {
  494. utils.SendErrorResponse(w, "User not logged in")
  495. return
  496. }
  497. fshId, _ := utils.GetPara(r, "fsh")
  498. results := []*shareEntry.ShareOption{}
  499. if fshId == "" {
  500. //List all
  501. allFsh := userinfo.GetAllFileSystemHandler()
  502. for _, thisFsh := range allFsh {
  503. allShares := s.ListAllShareByFshId(thisFsh.UUID, userinfo)
  504. for _, thisShare := range allShares {
  505. if s.ShareIsValid(thisShare) {
  506. results = append(results, thisShare)
  507. }
  508. }
  509. }
  510. } else {
  511. //List fsh only
  512. targetFsh, err := userinfo.GetFileSystemHandlerFromVirtualPath(fshId)
  513. if err != nil {
  514. utils.SendErrorResponse(w, err.Error())
  515. return
  516. }
  517. sharesInThisFsh := s.ListAllShareByFshId(targetFsh.UUID, userinfo)
  518. for _, thisShare := range sharesInThisFsh {
  519. if s.ShareIsValid(thisShare) {
  520. results = append(results, thisShare)
  521. }
  522. }
  523. }
  524. //Reduce the data
  525. type Share struct {
  526. UUID string
  527. FileVirtualPath string
  528. Owner string
  529. Permission string
  530. IsFolder bool
  531. IsOwnerOfShare bool
  532. CanAccess bool
  533. CanOpenInFileManager bool
  534. CanDelete bool
  535. HasPassword bool
  536. ExpireAt int64
  537. IsExpired bool
  538. }
  539. now := time.Now()
  540. reducedResult := []*Share{}
  541. for _, result := range results {
  542. permissionText := result.Permission
  543. if result.Permission == shareEntry.PermissionGroups || result.Permission == shareEntry.PermissionUsers {
  544. permissionText = permissionText + " (" + strings.Join(result.Accessibles, ", ") + ")"
  545. }
  546. thisShareInfo := Share{
  547. UUID: result.UUID,
  548. FileVirtualPath: result.FileVirtualPath,
  549. Owner: result.Owner,
  550. Permission: permissionText,
  551. IsFolder: result.IsFolder,
  552. IsOwnerOfShare: userinfo.Username == result.Owner,
  553. CanAccess: result.IsAccessibleBy(userinfo.Username, userinfo.GetUserPermissionGroupNames()),
  554. CanOpenInFileManager: s.UserCanOpenShareInFileManager(result, userinfo),
  555. CanDelete: s.CanModifyShare(userinfo, result),
  556. HasPassword: result.HasPassword(),
  557. ExpireAt: result.ExpireAt,
  558. IsExpired: result.IsExpired(now),
  559. }
  560. reducedResult = append(reducedResult, &thisShareInfo)
  561. }
  562. sendJSON(w, reducedResult)
  563. }
  564. // shareOwnerOf is a small helper for handlers that need the owner of a share
  565. func (s *Manager) shareOwnerOf(so *shareEntry.ShareOption) (*user.User, error) {
  566. return s.options.UserHandler.GetUserInfoFromUsername(so.Owner)
  567. }