transfer_server.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. package storage
  2. /*
  3. Chunked transfer protocol - server side (signed ACN endpoints, this node's
  4. volumes only):
  5. POST store/begin {VolumeID, Path, Size, Checksum, FileID} -> {SessionID, Have}
  6. POST store/chunk?session=&index= raw bytes + X-Aroz-Chunk-Sha256
  7. POST store/commit {SessionID} -> {Checksum, Size}
  8. POST store/abort {SessionID}
  9. GET store/read?volume=&path=&offset=&length= raw bytes
  10. GET store/stat?volume=&path=
  11. POST store/mkdir | store/delete | store/rename {VolumeID, Path, NewPath, Recursive}
  12. GET store/list?volume=&path=
  13. POST store/checksum {VolumeID, Path}
  14. POST store/place {Size, Path, PreferNode, PreferVolume} (leader only)
  15. POST store/thumbnail {VolumeID, Path} -> {Image} (thumbnail.go)
  16. */
  17. import (
  18. "crypto/sha256"
  19. "encoding/hex"
  20. "encoding/json"
  21. "errors"
  22. "io"
  23. "net/http"
  24. "os"
  25. "path/filepath"
  26. "strconv"
  27. "strings"
  28. "time"
  29. uuid "github.com/satori/go.uuid"
  30. "imuslab.com/arozos/mod/cluster/acn"
  31. "imuslab.com/arozos/mod/cluster/metadata"
  32. )
  33. const (
  34. pathBegin = acn.BasePath + "/store/begin"
  35. pathChunk = acn.BasePath + "/store/chunk"
  36. pathCommit = acn.BasePath + "/store/commit"
  37. pathAbort = acn.BasePath + "/store/abort"
  38. pathRead = acn.BasePath + "/store/read"
  39. pathStatFile = acn.BasePath + "/store/stat"
  40. pathMkdir = acn.BasePath + "/store/mkdir"
  41. pathDelete = acn.BasePath + "/store/delete"
  42. pathRename = acn.BasePath + "/store/rename"
  43. pathList = acn.BasePath + "/store/list"
  44. pathChecksum = acn.BasePath + "/store/checksum"
  45. pathPlace = acn.BasePath + "/store/place"
  46. HeaderChunkSha = "X-Aroz-Chunk-Sha256"
  47. HeaderFileSize = "X-Aroz-File-Size"
  48. )
  49. type BeginRequest struct {
  50. VolumeID string `json:"volumeId"`
  51. Path string `json:"path"`
  52. Size int64 `json:"size"`
  53. Checksum string `json:"checksum"`
  54. FileID string `json:"fileId"`
  55. }
  56. type BeginResponse struct {
  57. SessionID string `json:"sessionId"`
  58. Have []int `json:"have"`
  59. }
  60. type SessionRequest struct {
  61. SessionID string `json:"sessionId"`
  62. }
  63. type CommitResponse struct {
  64. Checksum string `json:"checksum"`
  65. Size int64 `json:"size"`
  66. }
  67. type PathRequest struct {
  68. VolumeID string `json:"volumeId"`
  69. Path string `json:"path"`
  70. NewPath string `json:"newPath,omitempty"`
  71. Recursive bool `json:"recursive,omitempty"`
  72. }
  73. type StatResponse struct {
  74. Exists bool `json:"exists"`
  75. IsDir bool `json:"isDir"`
  76. Size int64 `json:"size"`
  77. ModTime int64 `json:"modTime"`
  78. }
  79. type ListEntry struct {
  80. Name string `json:"name"`
  81. IsDir bool `json:"isDir"`
  82. Size int64 `json:"size"`
  83. ModTime int64 `json:"modTime"`
  84. }
  85. type ChecksumResponse struct {
  86. Checksum string `json:"checksum"`
  87. Size int64 `json:"size"`
  88. }
  89. // session is one in-progress upload onto a local volume.
  90. type session struct {
  91. ID string
  92. VolumeID string
  93. Path string
  94. Size int64
  95. Checksum string
  96. FileID string
  97. partPath string
  98. received map[int]bool
  99. created time.Time
  100. updated time.Time
  101. }
  102. func (s *Service) registerACNHandlers() {
  103. srv := s.m.Server()
  104. srv.HandleFunc(pathBegin, s.handleBegin)
  105. srv.HandleFunc(pathChunk, s.handleChunk)
  106. srv.HandleFunc(pathCommit, s.handleCommit)
  107. srv.HandleFunc(pathAbort, s.handleAbort)
  108. srv.HandleFunc(pathRead, s.handleRead)
  109. srv.HandleFunc(pathStatFile, s.handleStatFile)
  110. srv.HandleFunc(pathMkdir, s.handleMkdir)
  111. srv.HandleFunc(pathDelete, s.handleDelete)
  112. srv.HandleFunc(pathRename, s.handleRename)
  113. srv.HandleFunc(pathList, s.handleList)
  114. srv.HandleFunc(pathChecksum, s.handleChecksum)
  115. srv.HandleFunc(pathPlace, s.handlePlace)
  116. srv.HandleFunc(pathThumbnail, s.handleThumbnail)
  117. }
  118. // localVolumePath resolves a request onto a local volume path.
  119. func (s *Service) localVolumePath(volumeID string, logical string) (*metadata.Volume, string, error) {
  120. vol, ok := s.meta.Volume(volumeID)
  121. if !ok || vol.Removed {
  122. return nil, "", errors.New("volume not found")
  123. }
  124. if vol.NodeID != s.m.NodeID() {
  125. return nil, "", ErrNotLocalVolume
  126. }
  127. real, err := s.realPath(vol, logical)
  128. if err != nil {
  129. return nil, "", err
  130. }
  131. return vol, real, nil
  132. }
  133. func statusFor(err error) int {
  134. switch {
  135. case errors.Is(err, ErrNotLocalVolume), errors.Is(err, os.ErrNotExist):
  136. return http.StatusNotFound
  137. case errors.Is(err, ErrPathEscape):
  138. return http.StatusBadRequest
  139. case errors.Is(err, ErrChecksum):
  140. return http.StatusConflict
  141. }
  142. return http.StatusInternalServerError
  143. }
  144. func (s *Service) handleBegin(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  145. var req BeginRequest
  146. if err := json.Unmarshal(body, &req); err != nil || req.Size < 0 || req.Checksum == "" {
  147. acn.WriteError(w, http.StatusBadRequest, "invalid begin request")
  148. return
  149. }
  150. _, real, err := s.localVolumePath(req.VolumeID, req.Path)
  151. if err != nil {
  152. acn.WriteError(w, statusFor(err), err.Error())
  153. return
  154. }
  155. if err := os.MkdirAll(filepath.Dir(real), 0755); err != nil {
  156. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  157. return
  158. }
  159. s.sessMu.Lock()
  160. defer s.sessMu.Unlock()
  161. //Resume an unfinished session for the same file version
  162. for _, sess := range s.sessions {
  163. if sess.VolumeID == req.VolumeID && sess.Path == metadata.NormalizePath(req.Path) && sess.Checksum == req.Checksum && sess.Size == req.Size {
  164. sess.updated = time.Now()
  165. acn.WriteJSON(w, BeginResponse{SessionID: sess.ID, Have: sess.haveList()})
  166. return
  167. }
  168. }
  169. sess := &session{
  170. ID: uuid.NewV4().String(),
  171. VolumeID: req.VolumeID,
  172. Path: metadata.NormalizePath(req.Path),
  173. Size: req.Size,
  174. Checksum: req.Checksum,
  175. FileID: req.FileID,
  176. received: map[int]bool{},
  177. created: time.Now(),
  178. updated: time.Now(),
  179. }
  180. sess.partPath = real + ".part-" + sess.ID
  181. f, err := os.Create(sess.partPath)
  182. if err != nil {
  183. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  184. return
  185. }
  186. if req.Size > 0 {
  187. f.Truncate(req.Size)
  188. }
  189. f.Close()
  190. s.sessions[sess.ID] = sess
  191. acn.WriteJSON(w, BeginResponse{SessionID: sess.ID, Have: []int{}})
  192. }
  193. func (sess *session) haveList() []int {
  194. out := make([]int, 0, len(sess.received))
  195. for i := range sess.received {
  196. out = append(out, i)
  197. }
  198. return out
  199. }
  200. func (sess *session) chunkCount() int {
  201. if sess.Size == 0 {
  202. return 0
  203. }
  204. return int((sess.Size + ChunkSize - 1) / ChunkSize)
  205. }
  206. func (s *Service) getSession(id string) (*session, bool) {
  207. s.sessMu.Lock()
  208. defer s.sessMu.Unlock()
  209. sess, ok := s.sessions[id]
  210. if ok {
  211. sess.updated = time.Now()
  212. }
  213. return sess, ok
  214. }
  215. func (s *Service) dropSession(id string, removePart bool) {
  216. s.sessMu.Lock()
  217. sess, ok := s.sessions[id]
  218. delete(s.sessions, id)
  219. s.sessMu.Unlock()
  220. if ok && removePart {
  221. os.Remove(sess.partPath)
  222. }
  223. }
  224. func (s *Service) handleChunk(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  225. q := r.URL.Query()
  226. sess, ok := s.getSession(q.Get("session"))
  227. if !ok {
  228. acn.WriteError(w, http.StatusNotFound, "unknown upload session")
  229. return
  230. }
  231. index, err := strconv.Atoi(q.Get("index"))
  232. if err != nil || index < 0 || index >= sess.chunkCount() {
  233. acn.WriteError(w, http.StatusBadRequest, "chunk index out of range")
  234. return
  235. }
  236. if len(body) == 0 || len(body) > MaxChunkSize {
  237. acn.WriteError(w, http.StatusBadRequest, "chunk size invalid")
  238. return
  239. }
  240. sum := sha256.Sum256(body)
  241. if hex.EncodeToString(sum[:]) != strings.ToLower(r.Header.Get(HeaderChunkSha)) {
  242. acn.WriteError(w, http.StatusConflict, "chunk checksum mismatch")
  243. return
  244. }
  245. offset := int64(index) * ChunkSize
  246. if offset+int64(len(body)) > sess.Size {
  247. acn.WriteError(w, http.StatusBadRequest, "chunk exceeds file size")
  248. return
  249. }
  250. f, err := os.OpenFile(sess.partPath, os.O_WRONLY, 0644)
  251. if err != nil {
  252. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  253. return
  254. }
  255. _, err = f.WriteAt(body, offset)
  256. f.Close()
  257. if err != nil {
  258. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  259. return
  260. }
  261. s.sessMu.Lock()
  262. sess.received[index] = true
  263. n := len(sess.received)
  264. s.sessMu.Unlock()
  265. acn.WriteJSON(w, map[string]int{"index": index, "received": n})
  266. }
  267. func (s *Service) handleCommit(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  268. var req SessionRequest
  269. if err := json.Unmarshal(body, &req); err != nil {
  270. acn.WriteError(w, http.StatusBadRequest, "invalid request")
  271. return
  272. }
  273. sess, ok := s.getSession(req.SessionID)
  274. if !ok {
  275. acn.WriteError(w, http.StatusNotFound, "unknown upload session")
  276. return
  277. }
  278. if len(sess.received) != sess.chunkCount() {
  279. acn.WriteError(w, http.StatusConflict, "upload incomplete")
  280. return
  281. }
  282. sum, size, err := fileChecksum(sess.partPath)
  283. if err != nil {
  284. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  285. return
  286. }
  287. if size != sess.Size || sum != strings.ToLower(sess.Checksum) {
  288. s.dropSession(sess.ID, true)
  289. acn.WriteError(w, http.StatusConflict, ErrChecksum.Error())
  290. return
  291. }
  292. _, real, err := s.localVolumePath(sess.VolumeID, sess.Path)
  293. if err != nil {
  294. s.dropSession(sess.ID, true)
  295. acn.WriteError(w, statusFor(err), err.Error())
  296. return
  297. }
  298. if err := os.Rename(sess.partPath, real); err != nil {
  299. s.dropSession(sess.ID, true)
  300. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  301. return
  302. }
  303. s.dropSession(sess.ID, false)
  304. acn.WriteJSON(w, CommitResponse{Checksum: sum, Size: size})
  305. }
  306. func (s *Service) handleAbort(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  307. var req SessionRequest
  308. if err := json.Unmarshal(body, &req); err != nil {
  309. acn.WriteError(w, http.StatusBadRequest, "invalid request")
  310. return
  311. }
  312. s.dropSession(req.SessionID, true)
  313. acn.WriteJSON(w, map[string]bool{"ok": true})
  314. }
  315. func (s *Service) handleRead(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  316. q := r.URL.Query()
  317. _, real, err := s.localVolumePath(q.Get("volume"), q.Get("path"))
  318. if err != nil {
  319. acn.WriteError(w, statusFor(err), err.Error())
  320. return
  321. }
  322. offset, _ := strconv.ParseInt(q.Get("offset"), 10, 64)
  323. length, _ := strconv.ParseInt(q.Get("length"), 10, 64)
  324. if offset < 0 || length <= 0 || length > MaxChunkSize {
  325. acn.WriteError(w, http.StatusBadRequest, "invalid range")
  326. return
  327. }
  328. f, err := os.Open(real)
  329. if err != nil {
  330. acn.WriteError(w, http.StatusNotFound, "file not found on volume")
  331. return
  332. }
  333. defer f.Close()
  334. fi, err := f.Stat()
  335. if err != nil || fi.IsDir() {
  336. acn.WriteError(w, http.StatusNotFound, "not a file")
  337. return
  338. }
  339. if offset > fi.Size() {
  340. acn.WriteError(w, http.StatusBadRequest, "offset beyond end of file")
  341. return
  342. }
  343. if offset+length > fi.Size() {
  344. length = fi.Size() - offset
  345. }
  346. buf := make([]byte, length)
  347. n, err := f.ReadAt(buf, offset)
  348. if err != nil && err != io.EOF {
  349. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  350. return
  351. }
  352. buf = buf[:n]
  353. sum := sha256.Sum256(buf)
  354. w.Header().Set("Content-Type", "application/octet-stream")
  355. w.Header().Set(HeaderChunkSha, hex.EncodeToString(sum[:]))
  356. w.Header().Set(HeaderFileSize, strconv.FormatInt(fi.Size(), 10))
  357. w.Header().Set("Content-Length", strconv.Itoa(n))
  358. w.Write(buf)
  359. }
  360. func (s *Service) handleStatFile(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  361. q := r.URL.Query()
  362. _, real, err := s.localVolumePath(q.Get("volume"), q.Get("path"))
  363. if err != nil {
  364. acn.WriteError(w, statusFor(err), err.Error())
  365. return
  366. }
  367. fi, err := os.Stat(real)
  368. if err != nil {
  369. acn.WriteJSON(w, StatResponse{Exists: false})
  370. return
  371. }
  372. acn.WriteJSON(w, StatResponse{Exists: true, IsDir: fi.IsDir(), Size: fi.Size(), ModTime: fi.ModTime().Unix()})
  373. }
  374. func (s *Service) handleMkdir(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  375. var req PathRequest
  376. if err := json.Unmarshal(body, &req); err != nil {
  377. acn.WriteError(w, http.StatusBadRequest, "invalid request")
  378. return
  379. }
  380. _, real, err := s.localVolumePath(req.VolumeID, req.Path)
  381. if err != nil {
  382. acn.WriteError(w, statusFor(err), err.Error())
  383. return
  384. }
  385. if err := os.MkdirAll(real, 0755); err != nil {
  386. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  387. return
  388. }
  389. acn.WriteJSON(w, map[string]bool{"ok": true})
  390. }
  391. func (s *Service) handleDelete(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  392. var req PathRequest
  393. if err := json.Unmarshal(body, &req); err != nil {
  394. acn.WriteError(w, http.StatusBadRequest, "invalid request")
  395. return
  396. }
  397. vol, real, err := s.localVolumePath(req.VolumeID, req.Path)
  398. if err != nil {
  399. acn.WriteError(w, statusFor(err), err.Error())
  400. return
  401. }
  402. root, _ := s.volumeRoot(vol)
  403. if real == root {
  404. acn.WriteError(w, http.StatusBadRequest, "refusing to delete the volume root")
  405. return
  406. }
  407. if req.Recursive {
  408. err = os.RemoveAll(real)
  409. } else {
  410. err = os.Remove(real)
  411. }
  412. if err != nil && !os.IsNotExist(err) {
  413. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  414. return
  415. }
  416. acn.WriteJSON(w, map[string]bool{"ok": true})
  417. }
  418. func (s *Service) handleRename(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  419. var req PathRequest
  420. if err := json.Unmarshal(body, &req); err != nil || req.NewPath == "" {
  421. acn.WriteError(w, http.StatusBadRequest, "invalid request")
  422. return
  423. }
  424. vol, src, err := s.localVolumePath(req.VolumeID, req.Path)
  425. if err != nil {
  426. acn.WriteError(w, statusFor(err), err.Error())
  427. return
  428. }
  429. dst, err := s.realPath(vol, req.NewPath)
  430. if err != nil {
  431. acn.WriteError(w, statusFor(err), err.Error())
  432. return
  433. }
  434. if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
  435. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  436. return
  437. }
  438. if err := os.Rename(src, dst); err != nil {
  439. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  440. return
  441. }
  442. acn.WriteJSON(w, map[string]bool{"ok": true})
  443. }
  444. func (s *Service) handleList(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  445. q := r.URL.Query()
  446. _, real, err := s.localVolumePath(q.Get("volume"), q.Get("path"))
  447. if err != nil {
  448. acn.WriteError(w, statusFor(err), err.Error())
  449. return
  450. }
  451. entries, err := os.ReadDir(real)
  452. if err != nil {
  453. acn.WriteError(w, http.StatusNotFound, err.Error())
  454. return
  455. }
  456. out := []ListEntry{}
  457. for _, e := range entries {
  458. if strings.Contains(e.Name(), ".part-") {
  459. continue
  460. }
  461. fi, err := e.Info()
  462. if err != nil {
  463. continue
  464. }
  465. out = append(out, ListEntry{Name: e.Name(), IsDir: e.IsDir(), Size: fi.Size(), ModTime: fi.ModTime().Unix()})
  466. }
  467. acn.WriteJSON(w, out)
  468. }
  469. func (s *Service) handleChecksum(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  470. var req PathRequest
  471. if err := json.Unmarshal(body, &req); err != nil {
  472. acn.WriteError(w, http.StatusBadRequest, "invalid request")
  473. return
  474. }
  475. _, real, err := s.localVolumePath(req.VolumeID, req.Path)
  476. if err != nil {
  477. acn.WriteError(w, statusFor(err), err.Error())
  478. return
  479. }
  480. sum, size, err := fileChecksum(real)
  481. if err != nil {
  482. acn.WriteError(w, http.StatusNotFound, err.Error())
  483. return
  484. }
  485. acn.WriteJSON(w, ChecksumResponse{Checksum: sum, Size: size})
  486. }
  487. func (s *Service) handlePlace(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  488. if !s.meta.IsLeader() {
  489. acn.WriteError(w, http.StatusConflict, "this node is not the metadata leader")
  490. return
  491. }
  492. var req PlaceRequest
  493. if err := json.Unmarshal(body, &req); err != nil {
  494. acn.WriteError(w, http.StatusBadRequest, "invalid request")
  495. return
  496. }
  497. var exclude []string
  498. if req.ExcludeVolume != "" {
  499. exclude = []string{req.ExcludeVolume}
  500. }
  501. vol, err := s.pickVolume(req.Size, req.PreferNode, req.PreferVolume, exclude)
  502. if err != nil {
  503. acn.WriteError(w, http.StatusInsufficientStorage, err.Error())
  504. return
  505. }
  506. acn.WriteJSON(w, PlaceResponse{VolumeID: vol.ID})
  507. }
  508. // sweepSessions drops abandoned uploads and their part files.
  509. func (s *Service) sweepSessions() {
  510. cutoff := time.Now().Add(-SessionTTL)
  511. s.sessMu.Lock()
  512. stale := []*session{}
  513. for id, sess := range s.sessions {
  514. if sess.updated.Before(cutoff) {
  515. stale = append(stale, sess)
  516. delete(s.sessions, id)
  517. }
  518. }
  519. s.sessMu.Unlock()
  520. for _, sess := range stale {
  521. os.Remove(sess.partPath)
  522. }
  523. }