ufile.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  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 ucloud
  15. import (
  16. "context"
  17. "crypto/hmac"
  18. "crypto/sha1"
  19. "encoding/base64"
  20. "fmt"
  21. "io"
  22. "net/http"
  23. "net/url"
  24. "strconv"
  25. "time"
  26. "yunion.io/x/jsonutils"
  27. "yunion.io/x/log"
  28. "yunion.io/x/pkg/errors"
  29. "yunion.io/x/pkg/util/httputils"
  30. "yunion.io/x/cloudmux/pkg/cloudprovider"
  31. "yunion.io/x/cloudmux/pkg/multicloud"
  32. )
  33. type SBucket struct {
  34. multicloud.SBaseBucket
  35. UcloudTags
  36. region *SRegion
  37. // projectId string
  38. Domain Domain `json:"Domain"`
  39. BucketID string `json:"BucketId"`
  40. Region string `json:"Region"`
  41. CreateTime int64 `json:"CreateTime"`
  42. Biz string `json:"Biz"`
  43. BucketName string `json:"BucketName"`
  44. ModifyTime int64 `json:"ModifyTime"`
  45. Type string `json:"Type"`
  46. Tag string `json:"Tag"`
  47. HasUserDomain int64 `json:"HasUserDomain"`
  48. CDNDomainID []string `json:"CdnDomainId"`
  49. }
  50. type Domain struct {
  51. Src []string `json:"Src"`
  52. CDN []string `json:"Cdn"`
  53. CustomCDN []interface{} `json:"CustomCdn"`
  54. CustomSrc []interface{} `json:"CustomSrc"`
  55. }
  56. type SFile struct {
  57. bucket *SBucket
  58. BucketName string `json:"BucketName"`
  59. FileName string `json:"FileName"`
  60. Size int64 `json:"Size"`
  61. Hash string `json:"Hash"`
  62. MimeType string `json:"MimeType"`
  63. CreateTime int64 `json:"CreateTime"`
  64. ModifyTime int64 `json:"ModifyTime"`
  65. StorageClass string `json:"StorageClass"`
  66. file io.Reader
  67. }
  68. func (client *SUcloudClient) signHeader(httpMethod string, path string, md5 string) string {
  69. contentType := ""
  70. if httpMethod == http.MethodPut {
  71. contentType = "application/octet-stream"
  72. }
  73. data := httpMethod + "\n"
  74. data += md5 + "\n"
  75. data += contentType + "\n"
  76. data += "\n"
  77. data += path
  78. log.Debugf("sign %s", data)
  79. h := hmac.New(sha1.New, []byte(client.accessKeySecret))
  80. h.Write([]byte(data))
  81. return base64.StdEncoding.EncodeToString(h.Sum(nil))
  82. }
  83. func (self *SFile) signHeader(httpMethod string) string {
  84. return self.bucket.region.client.signHeader(httpMethod, "/"+self.bucket.BucketName+"/"+self.FileName, self.Hash)
  85. }
  86. func (self *SFile) auth(httpMethod string) string {
  87. return "UCloud" + " " + self.bucket.region.client.accessKeyId + ":" + self.signHeader(httpMethod)
  88. }
  89. func (self *SFile) GetHost() string {
  90. return self.bucket.Domain.Src[0]
  91. }
  92. func (self *SFile) GetUrl() string {
  93. return fmt.Sprintf("http://%s/%s", self.GetHost(), self.FileName)
  94. }
  95. // https://github.com/ufilesdk-dev/ufile-gosdk/blob/master/auth.go
  96. func (self *SFile) FetchFileUrl() string {
  97. expired := strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10)
  98. // sign
  99. data := "GET\n\n\n" + expired + "\n"
  100. data += "/" + self.bucket.BucketName + "/" + self.FileName
  101. h := hmac.New(sha1.New, []byte(self.bucket.region.client.accessKeySecret))
  102. h.Write([]byte(data))
  103. sign := base64.StdEncoding.EncodeToString(h.Sum(nil))
  104. urlEncoder := url.Values{}
  105. urlEncoder.Add("UCloudPublicKey", self.bucket.region.client.accessKeyId)
  106. urlEncoder.Add("Signature", sign)
  107. urlEncoder.Add("Expires", expired)
  108. querys := urlEncoder.Encode()
  109. return fmt.Sprintf("%s?%s", self.GetUrl(), querys)
  110. }
  111. func (self *SFile) Upload() error {
  112. req, _ := http.NewRequest(http.MethodPut, self.GetUrl(), self.file)
  113. req.Header.Add("Authorization", self.auth(http.MethodPut))
  114. req.Header.Add("Content-MD5", self.Hash)
  115. req.Header.Add("Content-Type", "application/octet-stream")
  116. req.Header.Add("Content-Length", strconv.FormatInt(self.Size, 10))
  117. _, err := doRequest(req)
  118. return err
  119. }
  120. func (self *SFile) Delete() error {
  121. req, _ := http.NewRequest(http.MethodDelete, self.GetUrl(), nil)
  122. req.Header.Add("Authorization", self.auth(http.MethodDelete))
  123. _, err := doRequest(req)
  124. return err
  125. }
  126. func (self *SFile) GetIBucket() cloudprovider.ICloudBucket {
  127. return self.bucket
  128. }
  129. func (self *SFile) GetKey() string {
  130. return self.FileName
  131. }
  132. func (self *SFile) GetSizeBytes() int64 {
  133. return self.Size
  134. }
  135. func (self *SFile) GetLastModified() time.Time {
  136. return time.Unix(self.ModifyTime, 0)
  137. }
  138. func (self *SFile) GetStorageClass() string {
  139. return self.StorageClass
  140. }
  141. func (self *SFile) GetETag() string {
  142. return self.Hash
  143. }
  144. func (self *SFile) GetContentType() string {
  145. return self.MimeType
  146. }
  147. func (self *SFile) GetAcl() cloudprovider.TBucketACLType {
  148. return self.bucket.GetAcl()
  149. }
  150. func (self *SFile) SetAcl(cloudprovider.TBucketACLType) error {
  151. return nil
  152. }
  153. func (self *SFile) GetMeta() http.Header {
  154. return nil
  155. }
  156. func (self *SFile) SetMeta(ctx context.Context, meta http.Header) error {
  157. return cloudprovider.ErrNotSupported
  158. }
  159. func doRequest(req *http.Request) (jsonutils.JSONObject, error) {
  160. // ufile request use no timeout client so as to download/upload large files
  161. res, err := httputils.GetAdaptiveTimeoutClient().Do(req)
  162. if err != nil {
  163. return nil, errors.Wrap(err, "httpclient Do")
  164. }
  165. _, body, err := httputils.ParseJSONResponse("", res, err, false)
  166. if err != nil {
  167. return nil, errors.Wrap(err, "ParseJSONResponse")
  168. }
  169. return body, nil
  170. }
  171. type sPrefixFileListOutput struct {
  172. BucketName string
  173. BucketId string
  174. NextMarker string
  175. DataSet []SFile
  176. }
  177. func (b *SBucket) doPrefixFileList(prefix string, marker string, limit int) (*sPrefixFileListOutput, error) {
  178. params := jsonutils.NewDict()
  179. params.Add(jsonutils.NewString(""), "list")
  180. if len(prefix) > 0 {
  181. params.Add(jsonutils.NewString(prefix), "prefix")
  182. }
  183. if len(marker) > 0 {
  184. params.Add(jsonutils.NewString(marker), "marker")
  185. }
  186. if limit > 0 {
  187. params.Add(jsonutils.NewInt(int64(limit)), "limit")
  188. }
  189. host := fmt.Sprintf("https://%s.ufile.ucloud.cn", b.BucketName)
  190. path := fmt.Sprintf("/?%s", params.QueryString())
  191. log.Debugf("Request %s%s", host, path)
  192. req, _ := http.NewRequest(http.MethodGet, host+path, nil)
  193. sign := b.region.client.signHeader(http.MethodGet, path, "")
  194. auth := "UCloud" + " " + b.region.client.accessKeyId + ":" + sign
  195. req.Header.Add("Authorization", auth)
  196. output := sPrefixFileListOutput{}
  197. body, err := doRequest(req)
  198. if err != nil {
  199. return nil, errors.Wrap(err, "doRequest")
  200. }
  201. err = body.Unmarshal(&output)
  202. if err != nil {
  203. return nil, errors.Wrap(err, "body.Unmarshal")
  204. }
  205. return &output, nil
  206. }
  207. func (b *SBucket) GetProjectId() string {
  208. return b.region.client.projectId
  209. }
  210. func (b *SBucket) GetGlobalId() string {
  211. return b.BucketID
  212. }
  213. func (b *SBucket) GetName() string {
  214. return b.BucketName
  215. }
  216. func (b *SBucket) GetLocation() string {
  217. return b.region.GetId()
  218. }
  219. func (b *SBucket) GetIRegion() cloudprovider.ICloudRegion {
  220. return b.region
  221. }
  222. func (b *SBucket) GetCreatedAt() time.Time {
  223. return time.Unix(b.CreateTime, 0)
  224. }
  225. func (b *SBucket) GetStorageClass() string {
  226. return ""
  227. }
  228. func (b *SBucket) GetAcl() cloudprovider.TBucketACLType {
  229. switch b.Type {
  230. case "public":
  231. return cloudprovider.ACLPublicRead
  232. default:
  233. return cloudprovider.ACLPrivate
  234. }
  235. }
  236. func (b *SBucket) SetAcl(aclStr cloudprovider.TBucketACLType) error {
  237. aclType := "private"
  238. if aclStr == cloudprovider.ACLPublicRead || aclStr == cloudprovider.ACLPublicReadWrite {
  239. aclType = "public"
  240. }
  241. return b.region.updateBucket(b.BucketName, aclType)
  242. }
  243. func (b *SBucket) getSrcUrl() string {
  244. if len(b.Domain.Src) > 0 {
  245. return b.Domain.Src[0]
  246. }
  247. return ""
  248. }
  249. func (b *SBucket) GetAccessUrls() []cloudprovider.SBucketAccessUrl {
  250. ret := make([]cloudprovider.SBucketAccessUrl, 0)
  251. for i, u := range b.Domain.Src {
  252. primary := false
  253. if i == 0 {
  254. primary = true
  255. }
  256. ret = append(ret, cloudprovider.SBucketAccessUrl{
  257. Url: u,
  258. Description: fmt.Sprintf("src%d", i),
  259. Primary: primary,
  260. })
  261. }
  262. for i, u := range b.Domain.CDN {
  263. ret = append(ret, cloudprovider.SBucketAccessUrl{
  264. Url: u,
  265. Description: fmt.Sprintf("cdn%d", i),
  266. })
  267. }
  268. return ret
  269. }
  270. func (b *SBucket) GetStats() cloudprovider.SBucketStats {
  271. stats, _ := cloudprovider.GetIBucketStats(b)
  272. return stats
  273. }
  274. func (b *SBucket) ListObjects(prefix string, marker string, delimiter string, maxCount int) (cloudprovider.SListObjectResult, error) {
  275. result := cloudprovider.SListObjectResult{}
  276. output, err := b.doPrefixFileList(prefix, marker, maxCount)
  277. if err != nil {
  278. return result, errors.Wrap(err, "b.doPrefixFileList")
  279. }
  280. if len(output.NextMarker) > 0 {
  281. result.NextMarker = output.NextMarker
  282. result.IsTruncated = true
  283. }
  284. result.Objects = make([]cloudprovider.ICloudObject, len(output.DataSet))
  285. for i := range output.DataSet {
  286. result.Objects[i] = &output.DataSet[i]
  287. }
  288. return result, nil
  289. }
  290. func (b *SBucket) PutObject(ctx context.Context, key string, input io.Reader, sizeBytes int64, cannedAcl cloudprovider.TBucketACLType, storageClassStr string, meta http.Header) error {
  291. return cloudprovider.ErrNotSupported
  292. }
  293. func (b *SBucket) NewMultipartUpload(ctx context.Context, key string, cannedAcl cloudprovider.TBucketACLType, storageClassStr string, meta http.Header) (string, error) {
  294. return "", cloudprovider.ErrNotSupported
  295. }
  296. func (b *SBucket) UploadPart(ctx context.Context, key string, uploadId string, partIndex int, input io.Reader, partSize int64, offset, totalSize int64) (string, error) {
  297. return "", cloudprovider.ErrNotSupported
  298. }
  299. func (b *SBucket) CompleteMultipartUpload(ctx context.Context, key string, uploadId string, partEtags []string) error {
  300. return cloudprovider.ErrNotSupported
  301. }
  302. func (b *SBucket) AbortMultipartUpload(ctx context.Context, key string, uploadId string) error {
  303. return cloudprovider.ErrNotSupported
  304. }
  305. func (b *SBucket) DeleteObject(ctx context.Context, key string) error {
  306. file := SFile{
  307. bucket: b,
  308. FileName: key,
  309. }
  310. return file.Delete()
  311. }
  312. func (b *SBucket) GetTempUrl(method string, key string, expire time.Duration) (string, error) {
  313. return "", cloudprovider.ErrNotSupported
  314. }
  315. func (b *SBucket) CopyObject(ctx context.Context, destKey string, srcBucket, srcKey string, cannedAcl cloudprovider.TBucketACLType, storageClassStr string, meta http.Header) error {
  316. return cloudprovider.ErrNotSupported
  317. }
  318. func (b *SBucket) GetObject(ctx context.Context, key string, rangeOpt *cloudprovider.SGetObjectRange) (io.ReadCloser, error) {
  319. return nil, cloudprovider.ErrNotSupported
  320. }
  321. func (b *SBucket) CopyPart(ctx context.Context, key string, uploadId string, partIndex int, srcBucketName string, srcKey string, srcOffset int64, srcLength int64) (string, error) {
  322. return "", cloudprovider.ErrNotSupported
  323. }