sftp_server.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package server
  15. import (
  16. "context"
  17. "io"
  18. "io/fs"
  19. "net/http"
  20. "net/url"
  21. "path"
  22. "sort"
  23. "strings"
  24. "sync"
  25. "time"
  26. "github.com/pkg/sftp"
  27. "yunion.io/x/cloudmux/pkg/cloudprovider"
  28. "yunion.io/x/jsonutils"
  29. "yunion.io/x/pkg/errors"
  30. "yunion.io/x/onecloud/pkg/appsrv"
  31. "yunion.io/x/onecloud/pkg/httperrors"
  32. )
  33. const (
  34. SESSION_ID = "<session-id>"
  35. )
  36. var (
  37. sftpMux = sync.Mutex{}
  38. sftpClients = make(map[string]*sftp.Client)
  39. )
  40. func addSftpClient(sId string, client *sftp.Client) {
  41. sftpMux.Lock()
  42. defer sftpMux.Unlock()
  43. sftpClients[sId] = client
  44. }
  45. func delSftpClient(sId string) {
  46. sftpMux.Lock()
  47. defer sftpMux.Unlock()
  48. delete(sftpClients, sId)
  49. }
  50. func getSftpClient(sId string) (*sftp.Client, error) {
  51. sftpMux.Lock()
  52. defer sftpMux.Unlock()
  53. client, ok := sftpClients[sId]
  54. if !ok {
  55. return nil, errors.Wrapf(cloudprovider.ErrNotFound, "%s", sId)
  56. }
  57. return client, nil
  58. }
  59. type sLinkFile struct {
  60. Name string
  61. Path string
  62. Size int64
  63. IsDir bool
  64. Mode string
  65. IsRegular bool
  66. ModeNum fs.FileMode
  67. }
  68. type sFileList struct {
  69. Name string
  70. Path string
  71. Size int64
  72. ModTime time.Time
  73. IsDir bool
  74. Mode string
  75. ModeNum fs.FileMode
  76. IsRegular bool
  77. LinkFile *sLinkFile
  78. }
  79. type Files []sFileList
  80. func (files Files) Len() int {
  81. return len(files)
  82. }
  83. func (files Files) Swap(i, j int) {
  84. files[i], files[j] = files[j], files[i]
  85. }
  86. func (files Files) Less(i, j int) bool {
  87. if files[i].IsDir != files[i].IsDir {
  88. // 文件夹在上
  89. var v = func(b bool) int {
  90. if b {
  91. return 0
  92. }
  93. return 1
  94. }
  95. return v(files[i].IsDir) < v(files[j].IsDir)
  96. }
  97. return files[i].Name < files[j].Name
  98. }
  99. func HandleSftpList(ctx context.Context, w http.ResponseWriter, r *http.Request) {
  100. params, query, _ := appsrv.FetchEnv(ctx, w, r)
  101. dir := "/"
  102. if query.Contains("path") {
  103. dir, _ = query.GetString("path")
  104. }
  105. sId := params[SESSION_ID]
  106. files, err := func() (Files, error) {
  107. client, err := getSftpClient(sId)
  108. if err != nil {
  109. return nil, errors.Wrapf(err, "getSftpClient")
  110. }
  111. files, err := client.ReadDir(dir)
  112. if err != nil {
  113. return nil, errors.Wrapf(httperrors.FsErrorNormalize(err), "ReadDir %s", dir)
  114. }
  115. ret := Files{}
  116. for _, f := range files {
  117. vv := sFileList{
  118. Name: f.Name(),
  119. Mode: f.Mode().String(),
  120. ModeNum: f.Mode().Perm(),
  121. IsRegular: f.Mode().IsRegular(),
  122. Size: f.Size(),
  123. ModTime: f.ModTime(),
  124. IsDir: f.IsDir(),
  125. Path: path.Join(dir, f.Name()),
  126. }
  127. f.Mode().IsRegular()
  128. if f.Mode().Type() == fs.ModeSymlink {
  129. if link, err := client.ReadLink(vv.Path); err == nil {
  130. vv.LinkFile = &sLinkFile{
  131. Name: link,
  132. }
  133. if stat, err := client.Stat(path.Join(dir, link)); err == nil {
  134. vv.LinkFile.IsDir = stat.IsDir()
  135. vv.LinkFile.Path = path.Join(dir, link)
  136. vv.LinkFile.Size = stat.Size()
  137. vv.LinkFile.Mode = stat.Mode().String()
  138. vv.LinkFile.ModeNum = stat.Mode().Perm()
  139. vv.LinkFile.IsRegular = stat.Mode().IsRegular()
  140. }
  141. } else {
  142. err = httperrors.FsErrorNormalize(err)
  143. }
  144. }
  145. ret = append(ret, vv)
  146. }
  147. return ret, nil
  148. }()
  149. if err != nil {
  150. httperrors.GeneralServerError(ctx, w, err)
  151. return
  152. }
  153. sort.Sort(files)
  154. appsrv.SendJSON(w, jsonutils.Marshal(files))
  155. }
  156. func HandleSftpUpload(ctx context.Context, w http.ResponseWriter, r *http.Request) {
  157. params, query, _ := appsrv.FetchEnv(ctx, w, r)
  158. dir := "/"
  159. if query.Contains("path") {
  160. dir, _ = query.GetString("path")
  161. }
  162. sId := params[SESSION_ID]
  163. err := func() error {
  164. sftp, err := getSftpClient(sId)
  165. if err != nil {
  166. return errors.Wrapf(err, "getSftpClient")
  167. }
  168. r.ParseMultipartForm(32 << 20)
  169. file, header, err := r.FormFile("file")
  170. if err != nil {
  171. return errors.Wrapf(err, "FormFile")
  172. }
  173. defer file.Close()
  174. _, err = sftp.Stat(dir)
  175. if err != nil {
  176. return errors.Wrapf(httperrors.FsErrorNormalize(err), "stat %s", dir)
  177. }
  178. newFile, err := sftp.Create(path.Join(dir, header.Filename))
  179. if err != nil {
  180. return errors.Wrapf(httperrors.FsErrorNormalize(err), "create file")
  181. }
  182. defer file.Close()
  183. defer newFile.Close()
  184. _, err = newFile.ReadFrom(file)
  185. if err != nil {
  186. return errors.Wrapf(httperrors.FsErrorNormalize(err), "ReadFrom")
  187. }
  188. return nil
  189. }()
  190. if err != nil {
  191. httperrors.GeneralServerError(ctx, w, err)
  192. return
  193. }
  194. appsrv.SendJSON(w, jsonutils.Marshal(map[string]string{"status": "success"}))
  195. }
  196. func HandleSftpDownload(ctx context.Context, w http.ResponseWriter, r *http.Request) {
  197. params, query, _ := appsrv.FetchEnv(ctx, w, r)
  198. if !query.Contains("path") {
  199. httperrors.GeneralServerError(ctx, w, httperrors.NewMissingParameterError("path"))
  200. return
  201. }
  202. dir, _ := query.GetString("path")
  203. sId := params[SESSION_ID]
  204. err := func() error {
  205. sftp, err := getSftpClient(sId)
  206. if err != nil {
  207. return errors.Wrapf(err, "getSftpClient")
  208. }
  209. file, err := sftp.Stat(dir)
  210. if err != nil {
  211. return errors.Wrapf(httperrors.FsErrorNormalize(err), "stat %s", dir)
  212. }
  213. if file.IsDir() {
  214. return errors.Wrapf(httperrors.ErrInvalidStatus, "dir %s can not be downloaded", dir)
  215. }
  216. reader, err := sftp.Open(dir)
  217. if err != nil {
  218. return errors.Wrapf(httperrors.FsErrorNormalize(err), "open file")
  219. }
  220. defer reader.Close()
  221. w.Header().Add("Content-Disposition", "attachment;filename*=utf-8''"+strings.ReplaceAll(url.QueryEscape(file.Name()), "+", "%20"))
  222. w.Header().Add("Content-Type", "application/octet-stream")
  223. _, err = io.Copy(w, reader)
  224. if err != nil {
  225. return errors.Wrap(httperrors.FsErrorNormalize(err), "Copy")
  226. }
  227. return nil
  228. }()
  229. if err != nil {
  230. httperrors.GeneralServerError(ctx, w, err)
  231. return
  232. }
  233. }