You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

pub_shards.go 14 kB

3 months ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. package http
  2. import (
  3. "compress/gzip"
  4. "fmt"
  5. "io"
  6. "mime"
  7. "mime/multipart"
  8. "net/http"
  9. "net/url"
  10. "github.com/gin-gonic/gin"
  11. "github.com/samber/lo"
  12. "gitlink.org.cn/cloudream/common/pkgs/logger"
  13. "gitlink.org.cn/cloudream/common/utils/http2"
  14. "gitlink.org.cn/cloudream/common/utils/serder"
  15. "gitlink.org.cn/cloudream/jcs-pub/client/internal/db"
  16. "gitlink.org.cn/cloudream/jcs-pub/client/internal/http/types"
  17. cliapi "gitlink.org.cn/cloudream/jcs-pub/client/sdk/api/v1"
  18. "gitlink.org.cn/cloudream/jcs-pub/common/ecode"
  19. stgglb "gitlink.org.cn/cloudream/jcs-pub/common/globals"
  20. "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/publock/reqbuilder"
  21. corrpc "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/rpc/coordinator"
  22. hubrpc "gitlink.org.cn/cloudream/jcs-pub/common/pkgs/rpc/hub"
  23. jcstypes "gitlink.org.cn/cloudream/jcs-pub/common/types"
  24. "gorm.io/gorm"
  25. )
  26. type PubShardsService struct {
  27. *Server
  28. }
  29. func (s *Server) PubShards() *PubShardsService {
  30. return &PubShardsService{s}
  31. }
  32. func (s *PubShardsService) Create(ctx *gin.Context) {
  33. log := logger.WithField("HTTP", "PubShards.Create")
  34. req, err := types.ShouldBindJSONEx[cliapi.PubShardsCreate](ctx)
  35. if err != nil {
  36. log.Warnf("binding body: %s", err.Error())
  37. ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "%v", err))
  38. return
  39. }
  40. if stgglb.StandaloneMode {
  41. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "client is not online"))
  42. return
  43. }
  44. corCli := stgglb.CoordinatorRPCPool.Get()
  45. defer corCli.Release()
  46. resp, cerr := corCli.CreatePubShards(ctx.Request.Context(), &corrpc.CreatePubShards{
  47. Password: req.Password,
  48. MasterHub: req.MasterHub,
  49. Name: req.Name,
  50. Storage: req.Storage,
  51. Credential: req.Credential,
  52. ShardStore: req.ShardStore,
  53. Features: req.Features,
  54. WorkingDir: jcstypes.PathFromJcsPathString(req.WorkingDir),
  55. })
  56. if cerr != nil {
  57. ctx.JSON(http.StatusOK, types.Failed(ecode.ErrorCode(cerr.Code), cerr.Message))
  58. return
  59. }
  60. ctx.JSON(http.StatusOK, types.OK(cliapi.PubShardsCreateResp{
  61. PubShards: resp.PubShardStore,
  62. }))
  63. }
  64. func (s *PubShardsService) Join(ctx *gin.Context) {
  65. log := logger.WithField("HTTP", "PubShards.Join")
  66. var req cliapi.PubShardsJoin
  67. if err := ctx.ShouldBindJSON(&req); err != nil {
  68. log.Warnf("binding body: %s", err.Error())
  69. ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "%v", err))
  70. return
  71. }
  72. if stgglb.StandaloneMode {
  73. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "client is not online"))
  74. return
  75. }
  76. corCli := stgglb.CoordinatorRPCPool.Get()
  77. defer corCli.Release()
  78. resp, cerr := corCli.UserGetPubShards(ctx.Request.Context(), &corrpc.UserGetPubShards{
  79. PubShardsID: req.PubShardsID,
  80. Password: req.Password,
  81. })
  82. if cerr != nil {
  83. ctx.JSON(http.StatusOK, types.Failed(ecode.ErrorCode(cerr.Code), cerr.Message))
  84. return
  85. }
  86. resp2, cerr2 := s.svc.UserSpaceSvc().Create(cliapi.UserSpaceCreate{
  87. Name: req.Name,
  88. Storage: &jcstypes.PubShardsType{
  89. Type: "PubShards",
  90. Base: resp.PubShards.Storage,
  91. PubShardsID: req.PubShardsID,
  92. Password: req.Password,
  93. MasterHub: resp.MasterHub.HubID,
  94. },
  95. Credential: resp.PubShards.Credential,
  96. ShardStore: &resp.PubShards.ShardStore,
  97. Features: resp.PubShards.Features,
  98. WorkingDir: resp.PubShards.WorkingDir.PushNew("parts", fmt.Sprintf("%v", s.svc.AccToken.GetToken().UserID)).String(),
  99. })
  100. if cerr2 != nil {
  101. ctx.JSON(http.StatusOK, types.Failed(ecode.ErrorCode(cerr2.Code), cerr2.Message))
  102. return
  103. }
  104. ctx.JSON(http.StatusOK, types.OK(cliapi.PubShardsJoinResp{
  105. PubShards: resp.PubShards,
  106. UserSpace: resp2.UserSpace,
  107. }))
  108. }
  109. func (s *PubShardsService) List(ctx *gin.Context) {
  110. log := logger.WithField("HTTP", "PubShards.List")
  111. var req cliapi.PubShardsList
  112. if err := ctx.ShouldBindQuery(&req); err != nil {
  113. log.Warnf("binding query: %s", err.Error())
  114. ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "%v", err))
  115. return
  116. }
  117. if stgglb.StandaloneMode {
  118. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "client is not online"))
  119. return
  120. }
  121. pubUss, err := s.svc.DB.UserSpace().GetAllPubShards(s.svc.DB.DefCtx())
  122. if err != nil {
  123. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "get pub shards: %v", err))
  124. return
  125. }
  126. corCli := stgglb.CoordinatorRPCPool.Get()
  127. defer corCli.Release()
  128. var pubs []jcstypes.PubShards
  129. for _, us := range pubUss {
  130. pubType := us.Storage.(*jcstypes.PubShardsType)
  131. resp, cerr := corCli.UserGetPubShards(ctx.Request.Context(), &corrpc.UserGetPubShards{
  132. PubShardsID: pubType.PubShardsID,
  133. Password: pubType.Password,
  134. })
  135. if cerr != nil {
  136. ctx.JSON(http.StatusOK, types.Failed(ecode.ErrorCode(cerr.Code), cerr.Message))
  137. return
  138. }
  139. pubs = append(pubs, resp.PubShards)
  140. }
  141. ctx.JSON(http.StatusOK, types.OK(cliapi.PubShardsListResp{
  142. PubShards: pubs,
  143. UserSpaces: pubUss,
  144. }))
  145. }
  146. func (s *PubShardsService) Get(ctx *gin.Context) {
  147. log := logger.WithField("HTTP", "PubShards.Get")
  148. var req cliapi.PubShardsGet
  149. if err := ctx.ShouldBindQuery(&req); err != nil {
  150. log.Warnf("binding query: %s", err.Error())
  151. ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "%v", err))
  152. return
  153. }
  154. if stgglb.StandaloneMode {
  155. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "client is not online"))
  156. return
  157. }
  158. pubUss, err := s.svc.DB.UserSpace().GetAllPubShards(s.svc.DB.DefCtx())
  159. if err != nil {
  160. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "get pub shards: %v", err))
  161. return
  162. }
  163. corCli := stgglb.CoordinatorRPCPool.Get()
  164. defer corCli.Release()
  165. for i, us := range pubUss {
  166. pubType := us.Storage.(*jcstypes.PubShardsType)
  167. resp, cerr := corCli.UserGetPubShards(ctx.Request.Context(), &corrpc.UserGetPubShards{
  168. PubShardsID: pubType.PubShardsID,
  169. Password: pubType.Password,
  170. })
  171. if cerr != nil {
  172. ctx.JSON(http.StatusOK, types.Failed(ecode.ErrorCode(cerr.Code), cerr.Message))
  173. return
  174. }
  175. if resp.PubShards.Name == req.Name {
  176. ctx.JSON(http.StatusOK, types.OK(cliapi.PubShardsGetResp{
  177. PubShards: resp.PubShards,
  178. UserSpace: pubUss[i],
  179. }))
  180. return
  181. }
  182. }
  183. ctx.JSON(http.StatusOK, types.Failed(ecode.DataNotFound, "pub shards %v not found", req.Name))
  184. }
  185. func (s *PubShardsService) ExportPackage(ctx *gin.Context) {
  186. log := logger.WithField("HTTP", "PubShards.ExportPackage")
  187. var req cliapi.PubShardsExportPackage
  188. if err := ctx.ShouldBindQuery(&req); err != nil {
  189. log.Warnf("binding query: %s", err.Error())
  190. ctx.JSON(http.StatusBadRequest, types.Failed(ecode.BadArgument, "%v", err))
  191. return
  192. }
  193. if stgglb.StandaloneMode {
  194. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "client is not online"))
  195. return
  196. }
  197. lock, err := reqbuilder.NewBuilder().Package().Pin(req.PackageID).MutexLock(s.svc.PubLock)
  198. if err != nil {
  199. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "lock package: %v", err))
  200. return
  201. }
  202. defer lock.Unlock()
  203. pubs := make(map[jcstypes.UserSpaceID]jcstypes.PubShardsID)
  204. for _, id := range req.AvailablePubShards {
  205. us, err := s.svc.DB.UserSpace().GetByPubShardsID(s.svc.DB.DefCtx(), id)
  206. if err != nil {
  207. if err == gorm.ErrRecordNotFound {
  208. ctx.JSON(http.StatusOK, types.Failed(ecode.DataNotFound, "pub shards %v not found", id))
  209. } else {
  210. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "get pub shards %v: %v", id, err))
  211. }
  212. return
  213. }
  214. pubs[us.UserSpaceID] = id
  215. }
  216. pkg, err := s.svc.DB.Package().GetByID(s.svc.DB.DefCtx(), req.PackageID)
  217. if err != nil {
  218. if err == gorm.ErrRecordNotFound {
  219. ctx.JSON(http.StatusOK, types.Failed(ecode.DataNotFound, "package %v not found", req.PackageID))
  220. } else {
  221. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "get package %v: %v", req.PackageID, err))
  222. }
  223. return
  224. }
  225. objs, err := db.DoTx11(s.svc.DB, s.svc.DB.Object().GetPackageObjectDetails, req.PackageID)
  226. if err != nil {
  227. if err == gorm.ErrRecordNotFound {
  228. ctx.JSON(http.StatusOK, types.Failed(ecode.DataNotFound, "package %v not found", req.PackageID))
  229. } else {
  230. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "get package %v: %v", req.PackageID, err))
  231. }
  232. return
  233. }
  234. var pack jcstypes.PubShardsPackFile
  235. for _, o := range objs {
  236. po := jcstypes.PackObject{
  237. Object: o.Object,
  238. }
  239. for _, b := range o.Blocks {
  240. pub := pubs[b.UserSpaceID]
  241. if pub != "" {
  242. po.Blocks = append(po.Blocks, jcstypes.PackObjectBlock{
  243. Index: b.Index,
  244. PubShardsID: pub,
  245. FileHash: b.FileHash,
  246. Size: b.Size,
  247. })
  248. }
  249. }
  250. pack.Objects = append(pack.Objects, po)
  251. }
  252. zw := gzip.NewWriter(ctx.Writer)
  253. defer zw.Close()
  254. ctx.Header("Content-Disposition", "attachment; filename="+url.PathEscape(pkg.Name)+".pack")
  255. ctx.Header("Content-Type", "application/octet-stream")
  256. ctx.Header("Content-Transfer-Encoding", "binary")
  257. ps := serder.ObjectToJSONStream(pack)
  258. defer ps.Close()
  259. io.Copy(zw, ps)
  260. }
  261. func (s *PubShardsService) ImportPackage(ctx *gin.Context) {
  262. // log := logger.WithField("HTTP", "PubShards.ImportPackage")
  263. if stgglb.StandaloneMode {
  264. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "client is not online"))
  265. return
  266. }
  267. contType := ctx.GetHeader("Content-Type")
  268. mtype, params, err := mime.ParseMediaType(contType)
  269. if err != nil {
  270. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "parse content-type: %v", err))
  271. return
  272. }
  273. if mtype != http2.ContentTypeMultiPart {
  274. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "content-type %v not supported", mtype))
  275. return
  276. }
  277. boundary := params["boundary"]
  278. if boundary == "" {
  279. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "missing boundary in content-type"))
  280. return
  281. }
  282. mr := multipart.NewReader(ctx.Request.Body, boundary)
  283. p, err := mr.NextPart()
  284. if err != nil {
  285. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "read info part: %v", err))
  286. return
  287. }
  288. var info cliapi.PubShardsImportPackage
  289. err = serder.JSONToObjectStream(p, &info)
  290. if err != nil {
  291. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "parse info: %v", err))
  292. return
  293. }
  294. if info.PackageID == 0 {
  295. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "missing packageID"))
  296. return
  297. }
  298. fr, err := mr.NextPart()
  299. if err != nil {
  300. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "read file part: %v", err))
  301. return
  302. }
  303. gr, err := gzip.NewReader(fr)
  304. if err != nil {
  305. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "read gzip: %v", err))
  306. return
  307. }
  308. defer gr.Close()
  309. pack, err := serder.JSONToObjectStreamEx[jcstypes.PubShardsPackFile](gr)
  310. if err != nil {
  311. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "parse pack: %v", err))
  312. return
  313. }
  314. pubShardsUs, err := s.svc.DB.UserSpace().GetAllPubShards(s.svc.DB.DefCtx())
  315. if err != nil {
  316. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "get pub shards: %v", err))
  317. return
  318. }
  319. uss := make(map[jcstypes.PubShardsID]jcstypes.UserSpace)
  320. for _, us := range pubShardsUs {
  321. pub, ok := us.Storage.(*jcstypes.PubShardsType)
  322. if !ok {
  323. continue
  324. }
  325. uss[pub.PubShardsID] = us
  326. }
  327. var willAdds []jcstypes.ObjectDetail
  328. var invalidObjs []jcstypes.Object
  329. // 只添加本客户端加入的PubShards中的块
  330. pubShardsRefs := make(map[jcstypes.PubShardsID][]jcstypes.FileHash)
  331. for _, po := range pack.Objects {
  332. blkIdx := make(map[int]bool)
  333. var addBlks []jcstypes.ObjectBlock
  334. for _, b := range po.Blocks {
  335. u, ok := uss[b.PubShardsID]
  336. if !ok {
  337. continue
  338. }
  339. pubShardsRefs[b.PubShardsID] = append(pubShardsRefs[b.PubShardsID], b.FileHash)
  340. addBlks = append(addBlks, jcstypes.ObjectBlock{
  341. Index: b.Index,
  342. UserSpaceID: u.UserSpaceID,
  343. FileHash: b.FileHash,
  344. Size: b.Size,
  345. })
  346. blkIdx[b.Index] = true
  347. }
  348. switch red := po.Object.Redundancy.(type) {
  349. case *jcstypes.NoneRedundancy:
  350. if len(addBlks) < 1 {
  351. invalidObjs = append(invalidObjs, po.Object)
  352. continue
  353. }
  354. case *jcstypes.RepRedundancy:
  355. if len(addBlks) < 1 {
  356. invalidObjs = append(invalidObjs, po.Object)
  357. continue
  358. }
  359. case *jcstypes.ECRedundancy:
  360. if len(blkIdx) < red.K {
  361. invalidObjs = append(invalidObjs, po.Object)
  362. continue
  363. }
  364. }
  365. willAdds = append(willAdds, jcstypes.ObjectDetail{
  366. Object: po.Object,
  367. Blocks: addBlks,
  368. })
  369. }
  370. // 在每一个PubShards创建FileHash引用,并且根据反馈的不存在的FileHash列表筛选导入列表
  371. for pub, refs := range pubShardsRefs {
  372. us := uss[pub]
  373. pubType := us.Storage.(*jcstypes.PubShardsType)
  374. hubCli := stgglb.HubRPCPool.GetByID(pubType.MasterHub)
  375. resp, cerr := hubCli.PubShardsCreateRefs(ctx.Request.Context(), &hubrpc.PubShardsCreateRefs{
  376. PubShardsID: pubType.PubShardsID,
  377. Password: pubType.Password,
  378. FileHashes: refs,
  379. })
  380. if cerr != nil {
  381. hubCli.Release()
  382. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "create pub shards refs at %v: %v", pubType.PubShardsID, cerr))
  383. return
  384. }
  385. invalidHashes := make(map[jcstypes.FileHash]bool)
  386. for _, h := range resp.InvalidFileHashes {
  387. invalidHashes[h] = true
  388. }
  389. for i := range willAdds {
  390. willAdds[i].Blocks = lo.Reject(willAdds[i].Blocks, func(blk jcstypes.ObjectBlock, idx int) bool {
  391. return blk.UserSpaceID == us.UserSpaceID && invalidHashes[blk.FileHash]
  392. })
  393. }
  394. hubCli.Release()
  395. }
  396. // 再次检查每个对象是否拥有完整块
  397. willAdds = lo.Filter(willAdds, func(obj jcstypes.ObjectDetail, idx int) bool {
  398. blkIdx := make(map[int]bool)
  399. for _, blk := range obj.Blocks {
  400. blkIdx[blk.Index] = true
  401. }
  402. switch red := obj.Object.Redundancy.(type) {
  403. case *jcstypes.NoneRedundancy:
  404. if len(blkIdx) < 1 {
  405. invalidObjs = append(invalidObjs, obj.Object)
  406. return false
  407. }
  408. case *jcstypes.RepRedundancy:
  409. if len(blkIdx) < 1 {
  410. invalidObjs = append(invalidObjs, obj.Object)
  411. return false
  412. }
  413. case *jcstypes.ECRedundancy:
  414. if len(blkIdx) < red.K {
  415. invalidObjs = append(invalidObjs, obj.Object)
  416. return false
  417. }
  418. }
  419. return true
  420. })
  421. _, err = db.DoTx21(s.svc.DB, s.svc.DB.Object().BatchCreateByDetails, info.PackageID, willAdds)
  422. if err != nil {
  423. ctx.JSON(http.StatusOK, types.Failed(ecode.OperationFailed, "create objects: %v", err))
  424. return
  425. }
  426. ctx.JSON(http.StatusOK, types.OK(cliapi.PubShardsImportPackageResp{
  427. InvalidObjects: invalidObjs,
  428. }))
  429. }

本项目旨在将云际存储公共基础设施化,使个人及企业可低门槛使用高效的云际存储服务(安装开箱即用云际存储客户端即可,无需关注其他组件的部署),同时支持用户灵活便捷定制云际存储的功能细节。