Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

483 lignes
14 KiB

  1. // Copyright 2017 Google LLC
  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. // +build integration
  15. package storage
  16. import (
  17. "bytes"
  18. "context"
  19. "encoding/base64"
  20. "errors"
  21. "fmt"
  22. "io/ioutil"
  23. "log"
  24. "net/http"
  25. "os"
  26. "strings"
  27. "testing"
  28. "golang.org/x/oauth2"
  29. "golang.org/x/oauth2/google"
  30. "google.golang.org/api/googleapi"
  31. storage "google.golang.org/api/storage/v1"
  32. )
  33. type object struct {
  34. name, contents string
  35. }
  36. var (
  37. projectID string
  38. bucket string
  39. objects = []object{
  40. {"obj1", testContents},
  41. {"obj2", testContents},
  42. {"obj/with/slashes", testContents},
  43. {"resumable", testContents},
  44. {"large", strings.Repeat("a", 514)}, // larger than the first section of content that is sniffed by ContentSniffer.
  45. }
  46. aclObjects = []string{"acl1", "acl2"}
  47. copyObj = "copy-object"
  48. )
  49. const (
  50. envProject = "GCLOUD_TESTS_GOLANG_PROJECT_ID"
  51. envPrivateKey = "GCLOUD_TESTS_GOLANG_KEY"
  52. // NOTE that running this test on a bucket deletes ALL contents of the bucket!
  53. envBucket = "GCLOUD_TESTS_GOLANG_DESTRUCTIVE_TEST_BUCKET_NAME"
  54. testContents = "some text that will be saved to a bucket object"
  55. )
  56. func verifyAcls(obj *storage.Object, wantDomainRole, wantAllUsersRole string) (err error) {
  57. var gotDomainRole, gotAllUsersRole string
  58. for _, acl := range obj.Acl {
  59. if acl.Entity == "domain-google.com" {
  60. gotDomainRole = acl.Role
  61. }
  62. if acl.Entity == "allUsers" {
  63. gotAllUsersRole = acl.Role
  64. }
  65. }
  66. if gotDomainRole != wantDomainRole {
  67. err = fmt.Errorf("domain-google.com role = %q; want %q", gotDomainRole, wantDomainRole)
  68. }
  69. if gotAllUsersRole != wantAllUsersRole {
  70. err = fmt.Errorf("allUsers role = %q; want %q; %v", gotAllUsersRole, wantAllUsersRole, err)
  71. }
  72. return err
  73. }
  74. // TODO(gmlewis): Move this to a common location.
  75. func tokenSource(ctx context.Context, scopes ...string) (oauth2.TokenSource, error) {
  76. keyFile := os.Getenv(envPrivateKey)
  77. if keyFile == "" {
  78. return nil, errors.New(envPrivateKey + " not set")
  79. }
  80. jsonKey, err := ioutil.ReadFile(keyFile)
  81. if err != nil {
  82. return nil, fmt.Errorf("unable to read %q: %v", keyFile, err)
  83. }
  84. conf, err := google.JWTConfigFromJSON(jsonKey, scopes...)
  85. if err != nil {
  86. return nil, fmt.Errorf("google.JWTConfigFromJSON: %v", err)
  87. }
  88. return conf.TokenSource(ctx), nil
  89. }
  90. const defaultType = "text/plain; charset=utf-8"
  91. // writeObject writes some data and default metadata to the specified object.
  92. // Resumable upload is used if resumable is true.
  93. // The written data is returned.
  94. func writeObject(s *storage.Service, bucket, obj string, resumable bool, contents string) error {
  95. o := &storage.Object{
  96. Bucket: bucket,
  97. Name: obj,
  98. ContentType: defaultType,
  99. ContentEncoding: "utf-8",
  100. ContentLanguage: "en",
  101. Metadata: map[string]string{"foo": "bar"},
  102. }
  103. f := strings.NewReader(contents)
  104. insert := s.Objects.Insert(bucket, o)
  105. if resumable {
  106. insert.ResumableMedia(context.Background(), f, int64(len(contents)), defaultType)
  107. } else {
  108. insert.Media(f)
  109. }
  110. _, err := insert.Do()
  111. return err
  112. }
  113. func checkMetadata(t *testing.T, s *storage.Service, bucket, obj string) {
  114. o, err := s.Objects.Get(bucket, obj).Do()
  115. if err != nil {
  116. t.Error(err)
  117. }
  118. if got, want := o.Name, obj; got != want {
  119. t.Errorf("name of %q = %q; want %q", obj, got, want)
  120. }
  121. if got, want := o.ContentType, defaultType; got != want {
  122. t.Errorf("contentType of %q = %q; want %q", obj, got, want)
  123. }
  124. if got, want := o.Metadata["foo"], "bar"; got != want {
  125. t.Errorf("metadata entry foo of %q = %q; want %q", obj, got, want)
  126. }
  127. }
  128. func createService() *storage.Service {
  129. if projectID = os.Getenv(envProject); projectID == "" {
  130. log.Print("no project ID specified")
  131. return nil
  132. }
  133. if bucket = os.Getenv(envBucket); bucket == "" {
  134. log.Print("no bucket specified")
  135. return nil
  136. }
  137. ctx := context.Background()
  138. ts, err := tokenSource(ctx, storage.DevstorageFullControlScope)
  139. if err != nil {
  140. log.Printf("tokenSource: %v", err)
  141. return nil
  142. }
  143. client := oauth2.NewClient(ctx, ts)
  144. s, err := storage.New(client)
  145. if err != nil {
  146. log.Printf("unable to create service: %v", err)
  147. return nil
  148. }
  149. return s
  150. }
  151. func TestMain(m *testing.M) {
  152. if err := cleanup(); err != nil {
  153. log.Fatalf("Pre-test cleanup failed: %v", err)
  154. }
  155. exit := m.Run()
  156. if err := cleanup(); err != nil {
  157. log.Fatalf("Post-test cleanup failed: %v", err)
  158. }
  159. os.Exit(exit)
  160. }
  161. func TestContentType(t *testing.T) {
  162. s := createService()
  163. if s == nil {
  164. t.Fatal("Could not create service")
  165. }
  166. type testCase struct {
  167. objectContentType string
  168. useOptionContentType bool
  169. optionContentType string
  170. wantContentType string
  171. }
  172. // The Media method will use resumable upload if the supplied data is
  173. // larger than googleapi.DefaultUploadChunkSize We run the following
  174. // tests with two different file contents: one that will trigger
  175. // resumable upload, and one that won't.
  176. forceResumableData := bytes.Repeat([]byte("a"), googleapi.DefaultUploadChunkSize+1)
  177. smallData := bytes.Repeat([]byte("a"), 2)
  178. // In the following test, the content type, if any, in the Object struct is always "text/plain".
  179. // The content type configured via googleapi.ContentType, if any, is always "text/html".
  180. for _, tc := range []testCase{
  181. // With content type specified in the object struct
  182. // Temporarily disable this test during rollout of strict Content-Type.
  183. // TODO(djd): Re-enable once strict check is 100%.
  184. // {
  185. // objectContentType: "text/plain",
  186. // useOptionContentType: true,
  187. // optionContentType: "text/html",
  188. // wantContentType: "text/html",
  189. // },
  190. {
  191. objectContentType: "text/plain",
  192. useOptionContentType: true,
  193. optionContentType: "",
  194. wantContentType: "text/plain",
  195. },
  196. {
  197. objectContentType: "text/plain",
  198. useOptionContentType: false,
  199. wantContentType: "text/plain",
  200. },
  201. // Without content type specified in the object struct
  202. {
  203. useOptionContentType: true,
  204. optionContentType: "text/html",
  205. wantContentType: "text/html",
  206. },
  207. {
  208. useOptionContentType: true,
  209. optionContentType: "",
  210. wantContentType: "", // Result is an object without a content type.
  211. },
  212. {
  213. useOptionContentType: false,
  214. wantContentType: "text/plain; charset=utf-8", // sniffed.
  215. },
  216. } {
  217. // The behavior should be the same, regardless of whether resumable upload is used or not.
  218. for _, data := range [][]byte{smallData, forceResumableData} {
  219. o := &storage.Object{
  220. Bucket: bucket,
  221. Name: "test-content-type",
  222. ContentType: tc.objectContentType,
  223. }
  224. call := s.Objects.Insert(bucket, o)
  225. var opts []googleapi.MediaOption
  226. if tc.useOptionContentType {
  227. opts = append(opts, googleapi.ContentType(tc.optionContentType))
  228. }
  229. call.Media(bytes.NewReader(data), opts...)
  230. _, err := call.Do()
  231. if err != nil {
  232. t.Fatalf("unable to insert object %q: %v", o.Name, err)
  233. }
  234. readObj, err := s.Objects.Get(bucket, o.Name).Do()
  235. if err != nil {
  236. t.Error(err)
  237. }
  238. if got, want := readObj.ContentType, tc.wantContentType; got != want {
  239. t.Errorf("contentType of %q; got %q; want %q", o.Name, got, want)
  240. }
  241. }
  242. }
  243. }
  244. func TestFunctions(t *testing.T) {
  245. s := createService()
  246. if s == nil {
  247. t.Fatal("Could not create service")
  248. }
  249. t.Logf("Listing buckets for project %q", projectID)
  250. var numBuckets int
  251. pageToken := ""
  252. for {
  253. call := s.Buckets.List(projectID)
  254. if pageToken != "" {
  255. call.PageToken(pageToken)
  256. }
  257. resp, err := call.Do()
  258. if err != nil {
  259. t.Fatalf("unable to list buckets for project %q: %v", projectID, err)
  260. }
  261. numBuckets += len(resp.Items)
  262. if pageToken = resp.NextPageToken; pageToken == "" {
  263. break
  264. }
  265. }
  266. if numBuckets == 0 {
  267. t.Fatalf("no buckets found for project %q", projectID)
  268. }
  269. for _, obj := range objects {
  270. t.Logf("Writing %q", obj.name)
  271. // TODO(mcgreevy): stop relying on "resumable" name to determine whether to
  272. // do a resumable upload.
  273. err := writeObject(s, bucket, obj.name, obj.name == "resumable", obj.contents)
  274. if err != nil {
  275. t.Fatalf("unable to insert object %q: %v", obj.name, err)
  276. }
  277. }
  278. for _, obj := range objects {
  279. t.Logf("Reading %q", obj.name)
  280. resp, err := s.Objects.Get(bucket, obj.name).Download()
  281. if err != nil {
  282. t.Fatalf("unable to get object %q: %v", obj.name, err)
  283. }
  284. slurp, err := ioutil.ReadAll(resp.Body)
  285. if err != nil {
  286. t.Fatalf("unable to read response body %q: %v", obj.name, err)
  287. }
  288. resp.Body.Close()
  289. if got, want := string(slurp), obj.contents; got != want {
  290. t.Errorf("contents of %q = %q; want %q", obj.name, got, want)
  291. }
  292. }
  293. name := "obj-not-exists"
  294. if _, err := s.Objects.Get(bucket, name).Download(); !isError(err, http.StatusNotFound) {
  295. t.Errorf("object %q should not exist, err = %v", name, err)
  296. } else {
  297. t.Log("Successfully tested StatusNotFound.")
  298. }
  299. for _, obj := range objects {
  300. t.Logf("Checking %q metadata", obj.name)
  301. checkMetadata(t, s, bucket, obj.name)
  302. }
  303. name = objects[0].name
  304. t.Logf("Rewriting %q to %q", name, copyObj)
  305. copy, err := s.Objects.Rewrite(bucket, name, bucket, copyObj, nil).Do()
  306. if err != nil {
  307. t.Fatalf("unable to rewrite object %q to %q: %v", name, copyObj, err)
  308. }
  309. if copy.Resource.Name != copyObj {
  310. t.Errorf("copy object's name = %q; want %q", copy.Resource.Name, copyObj)
  311. }
  312. if copy.Resource.Bucket != bucket {
  313. t.Errorf("copy object's bucket = %q; want %q", copy.Resource.Bucket, bucket)
  314. }
  315. // Note that arrays such as ACLs below are completely overwritten using Patch
  316. // semantics, so these must be updated in a read-modify-write sequence of operations.
  317. // See https://cloud.google.com/storage/docs/json_api/v1/how-tos/performance#patch-semantics
  318. // for more details.
  319. t.Logf("Updating attributes of %q", name)
  320. obj, err := s.Objects.Get(bucket, name).Projection("full").Fields("acl").Do()
  321. if err != nil {
  322. t.Fatalf("Objects.Get(%q, %q): %v", bucket, name, err)
  323. }
  324. if err := verifyAcls(obj, "", ""); err != nil {
  325. t.Errorf("before update ACLs: %v", err)
  326. }
  327. obj.ContentType = "text/html"
  328. for _, entity := range []string{"domain-google.com", "allUsers"} {
  329. obj.Acl = append(obj.Acl, &storage.ObjectAccessControl{Entity: entity, Role: "READER"})
  330. }
  331. updated, err := s.Objects.Patch(bucket, name, obj).Projection("full").Fields("contentType", "acl").Do()
  332. if err != nil {
  333. t.Fatalf("Objects.Patch(%q, %q, %#v) failed with %v", bucket, name, obj, err)
  334. }
  335. if want := "text/html"; updated.ContentType != want {
  336. t.Errorf("updated.ContentType == %q; want %q", updated.ContentType, want)
  337. }
  338. if err := verifyAcls(updated, "READER", "READER"); err != nil {
  339. t.Errorf("after update ACLs: %v", err)
  340. }
  341. t.Log("Testing checksums")
  342. checksumCases := []struct {
  343. name string
  344. contents string
  345. size uint64
  346. md5 string
  347. crc32c uint32
  348. }{
  349. {
  350. name: "checksum-object",
  351. contents: "helloworld",
  352. size: 10,
  353. md5: "fc5e038d38a57032085441e7fe7010b0",
  354. crc32c: 1456190592,
  355. },
  356. {
  357. name: "zero-object",
  358. contents: "",
  359. size: 0,
  360. md5: "d41d8cd98f00b204e9800998ecf8427e",
  361. crc32c: 0,
  362. },
  363. }
  364. for _, c := range checksumCases {
  365. f := strings.NewReader(c.contents)
  366. o := &storage.Object{
  367. Bucket: bucket,
  368. Name: c.name,
  369. ContentType: defaultType,
  370. ContentEncoding: "utf-8",
  371. ContentLanguage: "en",
  372. }
  373. obj, err := s.Objects.Insert(bucket, o).Media(f).Do()
  374. if err != nil {
  375. t.Fatalf("unable to insert object %v: %v", obj, err)
  376. }
  377. if got, want := obj.Size, c.size; got != want {
  378. t.Errorf("object %q size = %v; want %v", c.name, got, want)
  379. }
  380. md5, err := base64.StdEncoding.DecodeString(obj.Md5Hash)
  381. if err != nil {
  382. t.Fatalf("object %q base64 decode of MD5 %q: %v", c.name, obj.Md5Hash, err)
  383. }
  384. if got, want := fmt.Sprintf("%x", md5), c.md5; got != want {
  385. t.Errorf("object %q MD5 = %q; want %q", c.name, got, want)
  386. }
  387. var crc32c uint32
  388. d, err := base64.StdEncoding.DecodeString(obj.Crc32c)
  389. if err != nil {
  390. t.Errorf("object %q base64 decode of CRC32 %q: %v", c.name, obj.Crc32c, err)
  391. }
  392. if err == nil && len(d) == 4 {
  393. crc32c = uint32(d[0])<<24 + uint32(d[1])<<16 + uint32(d[2])<<8 + uint32(d[3])
  394. }
  395. if got, want := crc32c, c.crc32c; got != want {
  396. t.Errorf("object %q CRC32C = %v; want %v", c.name, got, want)
  397. }
  398. }
  399. }
  400. // cleanup destroys ALL objects in the bucket!
  401. func cleanup() error {
  402. s := createService()
  403. if s == nil {
  404. return errors.New("Could not create service")
  405. }
  406. var pageToken string
  407. var failed bool
  408. for {
  409. call := s.Objects.List(bucket)
  410. if pageToken != "" {
  411. call.PageToken(pageToken)
  412. }
  413. resp, err := call.Do()
  414. if err != nil {
  415. return fmt.Errorf("cleanup list failed: %v", err)
  416. }
  417. for _, obj := range resp.Items {
  418. log.Printf("Cleanup deletion of %q", obj.Name)
  419. if err := s.Objects.Delete(bucket, obj.Name).Do(); err != nil {
  420. // Print the error out, but keep going.
  421. log.Printf("Cleanup deletion of %q failed: %v", obj.Name, err)
  422. failed = true
  423. }
  424. if _, err := s.Objects.Get(bucket, obj.Name).Download(); !isError(err, http.StatusNotFound) {
  425. log.Printf("object %q should not exist, err = %v", obj.Name, err)
  426. failed = true
  427. } else {
  428. log.Printf("Successfully deleted %q.", obj.Name)
  429. }
  430. }
  431. if pageToken = resp.NextPageToken; pageToken == "" {
  432. break
  433. }
  434. }
  435. if failed {
  436. return errors.New("Failed to delete at least one object")
  437. }
  438. return nil
  439. }
  440. func isError(err error, code int) bool {
  441. if err == nil {
  442. return false
  443. }
  444. ae, ok := err.(*googleapi.Error)
  445. return ok && ae.Code == code
  446. }