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

571 lines
19 KiB

fs: Break fs package to top-level and introduce ObjectAPI interface. ObjectAPI interface brings in changes needed for XL ObjectAPI layer. The new interface for any ObjectAPI layer is as below ``` // ObjectAPI interface. type ObjectAPI interface { // Bucket resource API. DeleteBucket(bucket string) *probe.Error ListBuckets() ([]BucketInfo, *probe.Error) MakeBucket(bucket string) *probe.Error GetBucketInfo(bucket string) (BucketInfo, *probe.Error) // Bucket query API. ListObjects(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsResult, *probe.Error) ListMultipartUploads(bucket string, resources BucketMultipartResourcesMetadata) (BucketMultipartResourcesMetadata, *probe.Error) // Object resource API. GetObject(bucket, object string, startOffset int64) (io.ReadCloser, *probe.Error) GetObjectInfo(bucket, object string) (ObjectInfo, *probe.Error) PutObject(bucket string, object string, size int64, data io.Reader, metadata map[string]string) (ObjectInfo, *probe.Error) DeleteObject(bucket, object string) *probe.Error // Object query API. NewMultipartUpload(bucket, object string) (string, *probe.Error) PutObjectPart(bucket, object, uploadID string, partID int, size int64, data io.Reader, md5Hex string) (string, *probe.Error) ListObjectParts(bucket, object string, resources ObjectResourcesMetadata) (ObjectResourcesMetadata, *probe.Error) CompleteMultipartUpload(bucket string, object string, uploadID string, parts []CompletePart) (ObjectInfo, *probe.Error) AbortMultipartUpload(bucket, object, uploadID string) *probe.Error } ```
9 years ago
  1. /*
  2. * Minio Cloud Storage, (C) 2015, 2016, 2017 Minio, Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package cmd
  17. import (
  18. "encoding/xml"
  19. "net/http"
  20. "net/url"
  21. "path"
  22. "time"
  23. )
  24. const (
  25. timeFormatAMZ = "2006-01-02T15:04:05Z" // Reply date format
  26. timeFormatAMZLong = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
  27. maxObjectList = 1000 // Limit number of objects in a listObjectsResponse.
  28. maxUploadsList = 1000 // Limit number of uploads in a listUploadsResponse.
  29. maxPartsList = 1000 // Limit number of parts in a listPartsResponse.
  30. )
  31. // LocationResponse - format for location response.
  32. type LocationResponse struct {
  33. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LocationConstraint" json:"-"`
  34. Location string `xml:",chardata"`
  35. }
  36. // ListObjectsResponse - format for list objects response.
  37. type ListObjectsResponse struct {
  38. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult" json:"-"`
  39. Name string
  40. Prefix string
  41. Marker string
  42. // When response is truncated (the IsTruncated element value in the response
  43. // is true), you can use the key name in this field as marker in the subsequent
  44. // request to get next set of objects. Server lists objects in alphabetical
  45. // order Note: This element is returned only if you have delimiter request parameter
  46. // specified. If response does not include the NextMaker and it is truncated,
  47. // you can use the value of the last Key in the response as the marker in the
  48. // subsequent request to get the next set of object keys.
  49. NextMarker string `xml:"NextMarker,omitempty"`
  50. MaxKeys int
  51. Delimiter string
  52. // A flag that indicates whether or not ListObjects returned all of the results
  53. // that satisfied the search criteria.
  54. IsTruncated bool
  55. Contents []Object
  56. CommonPrefixes []CommonPrefix
  57. // Encoding type used to encode object keys in the response.
  58. EncodingType string `xml:"EncodingType,omitempty"`
  59. }
  60. // ListObjectsV2Response - format for list objects response.
  61. type ListObjectsV2Response struct {
  62. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult" json:"-"`
  63. Name string
  64. Prefix string
  65. StartAfter string `xml:"StartAfter,omitempty"`
  66. // When response is truncated (the IsTruncated element value in the response
  67. // is true), you can use the key name in this field as marker in the subsequent
  68. // request to get next set of objects. Server lists objects in alphabetical
  69. // order Note: This element is returned only if you have delimiter request parameter
  70. // specified. If response does not include the NextMaker and it is truncated,
  71. // you can use the value of the last Key in the response as the marker in the
  72. // subsequent request to get the next set of object keys.
  73. ContinuationToken string `xml:"ContinuationToken,omitempty"`
  74. NextContinuationToken string `xml:"NextContinuationToken,omitempty"`
  75. KeyCount int
  76. MaxKeys int
  77. Delimiter string
  78. // A flag that indicates whether or not ListObjects returned all of the results
  79. // that satisfied the search criteria.
  80. IsTruncated bool
  81. Contents []Object
  82. CommonPrefixes []CommonPrefix
  83. // Encoding type used to encode object keys in the response.
  84. EncodingType string `xml:"EncodingType,omitempty"`
  85. }
  86. // Part container for part metadata.
  87. type Part struct {
  88. PartNumber int
  89. LastModified string
  90. ETag string
  91. Size int64
  92. }
  93. // ListPartsResponse - format for list parts response.
  94. type ListPartsResponse struct {
  95. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListPartsResult" json:"-"`
  96. Bucket string
  97. Key string
  98. UploadID string `xml:"UploadId"`
  99. Initiator Initiator
  100. Owner Owner
  101. // The class of storage used to store the object.
  102. StorageClass string
  103. PartNumberMarker int
  104. NextPartNumberMarker int
  105. MaxParts int
  106. IsTruncated bool
  107. // List of parts.
  108. Parts []Part `xml:"Part"`
  109. }
  110. // ListMultipartUploadsResponse - format for list multipart uploads response.
  111. type ListMultipartUploadsResponse struct {
  112. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListMultipartUploadsResult" json:"-"`
  113. Bucket string
  114. KeyMarker string
  115. UploadIDMarker string `xml:"UploadIdMarker"`
  116. NextKeyMarker string
  117. NextUploadIDMarker string `xml:"NextUploadIdMarker"`
  118. Delimiter string
  119. Prefix string
  120. EncodingType string `xml:"EncodingType,omitempty"`
  121. MaxUploads int
  122. IsTruncated bool
  123. // List of pending uploads.
  124. Uploads []Upload `xml:"Upload"`
  125. // Delimed common prefixes.
  126. CommonPrefixes []CommonPrefix
  127. }
  128. // ListBucketsResponse - format for list buckets response
  129. type ListBucketsResponse struct {
  130. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListAllMyBucketsResult" json:"-"`
  131. Owner Owner
  132. // Container for one or more buckets.
  133. Buckets struct {
  134. Buckets []Bucket `xml:"Bucket"`
  135. } // Buckets are nested
  136. }
  137. // Upload container for in progress multipart upload
  138. type Upload struct {
  139. Key string
  140. UploadID string `xml:"UploadId"`
  141. Initiator Initiator
  142. Owner Owner
  143. StorageClass string
  144. Initiated string
  145. HealUploadInfo *HealObjectInfo `xml:"HealObjectInfo,omitempty"`
  146. }
  147. // CommonPrefix container for prefix response in ListObjectsResponse
  148. type CommonPrefix struct {
  149. Prefix string
  150. }
  151. // Bucket container for bucket metadata
  152. type Bucket struct {
  153. Name string
  154. CreationDate string // time string of format "2006-01-02T15:04:05.000Z"
  155. HealBucketInfo *HealBucketInfo `xml:"HealBucketInfo,omitempty"`
  156. }
  157. // Object container for object metadata
  158. type Object struct {
  159. Key string
  160. LastModified string // time string of format "2006-01-02T15:04:05.000Z"
  161. ETag string
  162. Size int64
  163. // Owner of the object.
  164. Owner Owner
  165. // The class of storage used to store the object.
  166. StorageClass string
  167. HealObjectInfo *HealObjectInfo `xml:"HealObjectInfo,omitempty"`
  168. }
  169. // CopyObjectResponse container returns ETag and LastModified of the successfully copied object
  170. type CopyObjectResponse struct {
  171. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyObjectResult" json:"-"`
  172. LastModified string // time string of format "2006-01-02T15:04:05.000Z"
  173. ETag string // md5sum of the copied object.
  174. }
  175. // CopyObjectPartResponse container returns ETag and LastModified of the successfully copied object
  176. type CopyObjectPartResponse struct {
  177. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyPartResult" json:"-"`
  178. LastModified string // time string of format "2006-01-02T15:04:05.000Z"
  179. ETag string // md5sum of the copied object part.
  180. }
  181. // Initiator inherit from Owner struct, fields are same
  182. type Initiator Owner
  183. // Owner - bucket owner/principal
  184. type Owner struct {
  185. ID string
  186. DisplayName string
  187. }
  188. // InitiateMultipartUploadResponse container for InitiateMultiPartUpload response, provides uploadID to start MultiPart upload
  189. type InitiateMultipartUploadResponse struct {
  190. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ InitiateMultipartUploadResult" json:"-"`
  191. Bucket string
  192. Key string
  193. UploadID string `xml:"UploadId"`
  194. }
  195. // CompleteMultipartUploadResponse container for completed multipart upload response
  196. type CompleteMultipartUploadResponse struct {
  197. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUploadResult" json:"-"`
  198. Location string
  199. Bucket string
  200. Key string
  201. ETag string
  202. }
  203. // DeleteError structure.
  204. type DeleteError struct {
  205. Code string
  206. Message string
  207. Key string
  208. }
  209. // DeleteObjectsResponse container for multiple object deletes.
  210. type DeleteObjectsResponse struct {
  211. XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DeleteResult" json:"-"`
  212. // Collection of all deleted objects
  213. DeletedObjects []ObjectIdentifier `xml:"Deleted,omitempty"`
  214. // Collection of errors deleting certain objects.
  215. Errors []DeleteError `xml:"Error,omitempty"`
  216. }
  217. // PostResponse container for POST object request when success_action_status is set to 201
  218. type PostResponse struct {
  219. Bucket string
  220. Key string
  221. ETag string
  222. Location string
  223. }
  224. // getLocation get URL location.
  225. func getLocation(r *http.Request) string {
  226. return path.Clean(r.URL.Path) // Clean any trailing slashes.
  227. }
  228. // getObjectLocation gets the relative URL for an object
  229. func getObjectLocation(bucketName string, key string) string {
  230. return "/" + bucketName + "/" + key
  231. }
  232. // generates ListBucketsResponse from array of BucketInfo which can be
  233. // serialized to match XML and JSON API spec output.
  234. func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
  235. var listbuckets []Bucket
  236. var data = ListBucketsResponse{}
  237. var owner = Owner{}
  238. owner.ID = globalMinioDefaultOwnerID
  239. owner.DisplayName = globalMinioDefaultOwnerID
  240. for _, bucket := range buckets {
  241. var listbucket = Bucket{}
  242. listbucket.Name = bucket.Name
  243. listbucket.CreationDate = bucket.Created.Format(timeFormatAMZLong)
  244. listbucket.HealBucketInfo = bucket.HealBucketInfo
  245. listbuckets = append(listbuckets, listbucket)
  246. }
  247. data.Owner = owner
  248. data.Buckets.Buckets = listbuckets
  249. return data
  250. }
  251. // generates an ListObjectsV1 response for the said bucket with other enumerated options.
  252. func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
  253. var contents []Object
  254. var prefixes []CommonPrefix
  255. var owner = Owner{}
  256. var data = ListObjectsResponse{}
  257. owner.ID = globalMinioDefaultOwnerID
  258. owner.DisplayName = globalMinioDefaultOwnerID
  259. for _, object := range resp.Objects {
  260. var content = Object{}
  261. if object.Name == "" {
  262. continue
  263. }
  264. content.Key = object.Name
  265. content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
  266. if object.MD5Sum != "" {
  267. content.ETag = "\"" + object.MD5Sum + "\""
  268. }
  269. content.Size = object.Size
  270. content.StorageClass = globalMinioDefaultStorageClass
  271. content.Owner = owner
  272. // object.HealObjectInfo is non-empty only when resp is constructed in ListObjectsHeal.
  273. content.HealObjectInfo = object.HealObjectInfo
  274. contents = append(contents, content)
  275. }
  276. // TODO - support EncodingType in xml decoding
  277. data.Name = bucket
  278. data.Contents = contents
  279. data.Prefix = prefix
  280. data.Marker = marker
  281. data.Delimiter = delimiter
  282. data.MaxKeys = maxKeys
  283. data.NextMarker = resp.NextMarker
  284. data.IsTruncated = resp.IsTruncated
  285. for _, prefix := range resp.Prefixes {
  286. var prefixItem = CommonPrefix{}
  287. prefixItem.Prefix = prefix
  288. prefixes = append(prefixes, prefixItem)
  289. }
  290. data.CommonPrefixes = prefixes
  291. return data
  292. }
  293. // generates an ListObjectsV2 response for the said bucket with other enumerated options.
  294. func generateListObjectsV2Response(bucket, prefix, token, startAfter, delimiter string, fetchOwner bool, maxKeys int, resp ListObjectsInfo) ListObjectsV2Response {
  295. var contents []Object
  296. var prefixes []CommonPrefix
  297. var owner = Owner{}
  298. var data = ListObjectsV2Response{}
  299. if fetchOwner {
  300. owner.ID = globalMinioDefaultOwnerID
  301. owner.DisplayName = globalMinioDefaultOwnerID
  302. }
  303. for _, object := range resp.Objects {
  304. var content = Object{}
  305. if object.Name == "" {
  306. continue
  307. }
  308. content.Key = object.Name
  309. content.LastModified = object.ModTime.UTC().Format(timeFormatAMZLong)
  310. if object.MD5Sum != "" {
  311. content.ETag = "\"" + object.MD5Sum + "\""
  312. }
  313. content.Size = object.Size
  314. content.StorageClass = globalMinioDefaultStorageClass
  315. content.Owner = owner
  316. contents = append(contents, content)
  317. }
  318. // TODO - support EncodingType in xml decoding
  319. data.Name = bucket
  320. data.Contents = contents
  321. data.StartAfter = startAfter
  322. data.Delimiter = delimiter
  323. data.Prefix = prefix
  324. data.MaxKeys = maxKeys
  325. data.ContinuationToken = token
  326. data.NextContinuationToken = resp.NextMarker
  327. data.IsTruncated = resp.IsTruncated
  328. for _, prefix := range resp.Prefixes {
  329. var prefixItem = CommonPrefix{}
  330. prefixItem.Prefix = prefix
  331. prefixes = append(prefixes, prefixItem)
  332. }
  333. data.CommonPrefixes = prefixes
  334. data.KeyCount = len(data.Contents) + len(data.CommonPrefixes)
  335. return data
  336. }
  337. // generates CopyObjectResponse from etag and lastModified time.
  338. func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse {
  339. return CopyObjectResponse{
  340. ETag: "\"" + etag + "\"",
  341. LastModified: lastModified.UTC().Format(timeFormatAMZLong),
  342. }
  343. }
  344. // generates CopyObjectPartResponse from etag and lastModified time.
  345. func generateCopyObjectPartResponse(etag string, lastModified time.Time) CopyObjectPartResponse {
  346. return CopyObjectPartResponse{
  347. ETag: "\"" + etag + "\"",
  348. LastModified: lastModified.UTC().Format(timeFormatAMZLong),
  349. }
  350. }
  351. // generates InitiateMultipartUploadResponse for given bucket, key and uploadID.
  352. func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) InitiateMultipartUploadResponse {
  353. return InitiateMultipartUploadResponse{
  354. Bucket: bucket,
  355. Key: key,
  356. UploadID: uploadID,
  357. }
  358. }
  359. // generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
  360. func generateCompleteMultpartUploadResponse(bucket, key, location, etag string) CompleteMultipartUploadResponse {
  361. return CompleteMultipartUploadResponse{
  362. Location: location,
  363. Bucket: bucket,
  364. Key: key,
  365. ETag: etag,
  366. }
  367. }
  368. // generates ListPartsResponse from ListPartsInfo.
  369. func generateListPartsResponse(partsInfo ListPartsInfo) ListPartsResponse {
  370. // TODO - support EncodingType in xml decoding
  371. listPartsResponse := ListPartsResponse{}
  372. listPartsResponse.Bucket = partsInfo.Bucket
  373. listPartsResponse.Key = partsInfo.Object
  374. listPartsResponse.UploadID = partsInfo.UploadID
  375. listPartsResponse.StorageClass = globalMinioDefaultStorageClass
  376. listPartsResponse.Initiator.ID = globalMinioDefaultOwnerID
  377. listPartsResponse.Initiator.DisplayName = globalMinioDefaultOwnerID
  378. listPartsResponse.Owner.ID = globalMinioDefaultOwnerID
  379. listPartsResponse.Owner.DisplayName = globalMinioDefaultOwnerID
  380. listPartsResponse.MaxParts = partsInfo.MaxParts
  381. listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker
  382. listPartsResponse.IsTruncated = partsInfo.IsTruncated
  383. listPartsResponse.NextPartNumberMarker = partsInfo.NextPartNumberMarker
  384. listPartsResponse.Parts = make([]Part, len(partsInfo.Parts))
  385. for index, part := range partsInfo.Parts {
  386. newPart := Part{}
  387. newPart.PartNumber = part.PartNumber
  388. newPart.ETag = "\"" + part.ETag + "\""
  389. newPart.Size = part.Size
  390. newPart.LastModified = part.LastModified.UTC().Format(timeFormatAMZLong)
  391. listPartsResponse.Parts[index] = newPart
  392. }
  393. return listPartsResponse
  394. }
  395. // generates ListMultipartUploadsResponse for given bucket and ListMultipartsInfo.
  396. func generateListMultipartUploadsResponse(bucket string, multipartsInfo ListMultipartsInfo) ListMultipartUploadsResponse {
  397. listMultipartUploadsResponse := ListMultipartUploadsResponse{}
  398. listMultipartUploadsResponse.Bucket = bucket
  399. listMultipartUploadsResponse.Delimiter = multipartsInfo.Delimiter
  400. listMultipartUploadsResponse.IsTruncated = multipartsInfo.IsTruncated
  401. listMultipartUploadsResponse.EncodingType = multipartsInfo.EncodingType
  402. listMultipartUploadsResponse.Prefix = multipartsInfo.Prefix
  403. listMultipartUploadsResponse.KeyMarker = multipartsInfo.KeyMarker
  404. listMultipartUploadsResponse.NextKeyMarker = multipartsInfo.NextKeyMarker
  405. listMultipartUploadsResponse.MaxUploads = multipartsInfo.MaxUploads
  406. listMultipartUploadsResponse.NextUploadIDMarker = multipartsInfo.NextUploadIDMarker
  407. listMultipartUploadsResponse.UploadIDMarker = multipartsInfo.UploadIDMarker
  408. listMultipartUploadsResponse.CommonPrefixes = make([]CommonPrefix, len(multipartsInfo.CommonPrefixes))
  409. for index, commonPrefix := range multipartsInfo.CommonPrefixes {
  410. listMultipartUploadsResponse.CommonPrefixes[index] = CommonPrefix{
  411. Prefix: commonPrefix,
  412. }
  413. }
  414. listMultipartUploadsResponse.Uploads = make([]Upload, len(multipartsInfo.Uploads))
  415. for index, upload := range multipartsInfo.Uploads {
  416. newUpload := Upload{}
  417. newUpload.UploadID = upload.UploadID
  418. newUpload.Key = upload.Object
  419. newUpload.Initiated = upload.Initiated.UTC().Format(timeFormatAMZLong)
  420. newUpload.HealUploadInfo = upload.HealUploadInfo
  421. listMultipartUploadsResponse.Uploads[index] = newUpload
  422. }
  423. return listMultipartUploadsResponse
  424. }
  425. // generate multi objects delete response.
  426. func generateMultiDeleteResponse(quiet bool, deletedObjects []ObjectIdentifier, errs []DeleteError) DeleteObjectsResponse {
  427. deleteResp := DeleteObjectsResponse{}
  428. if !quiet {
  429. deleteResp.DeletedObjects = deletedObjects
  430. }
  431. deleteResp.Errors = errs
  432. return deleteResp
  433. }
  434. func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
  435. setCommonHeaders(w)
  436. if mType != mimeNone {
  437. w.Header().Set("Content-Type", string(mType))
  438. }
  439. w.WriteHeader(statusCode)
  440. if response != nil {
  441. w.Write(response)
  442. w.(http.Flusher).Flush()
  443. }
  444. }
  445. // mimeType represents various MIME type used API responses.
  446. type mimeType string
  447. const (
  448. // Means no response type.
  449. mimeNone mimeType = ""
  450. // Means response type is JSON.
  451. mimeJSON mimeType = "application/json"
  452. // Means response type is XML.
  453. mimeXML mimeType = "application/xml"
  454. )
  455. // writeSuccessResponseJSON writes success headers and response if any,
  456. // with content-type set to `application/json`.
  457. func writeSuccessResponseJSON(w http.ResponseWriter, response []byte) {
  458. writeResponse(w, http.StatusOK, response, mimeJSON)
  459. }
  460. // writeSuccessResponseXML writes success headers and response if any,
  461. // with content-type set to `application/xml`.
  462. func writeSuccessResponseXML(w http.ResponseWriter, response []byte) {
  463. writeResponse(w, http.StatusOK, response, mimeXML)
  464. }
  465. // writeSuccessNoContent writes success headers with http status 204
  466. func writeSuccessNoContent(w http.ResponseWriter) {
  467. writeResponse(w, http.StatusNoContent, nil, mimeNone)
  468. }
  469. // writeRedirectSeeOther writes Location header with http status 303
  470. func writeRedirectSeeOther(w http.ResponseWriter, location string) {
  471. w.Header().Set("Location", location)
  472. writeResponse(w, http.StatusSeeOther, nil, mimeNone)
  473. }
  474. func writeSuccessResponseHeadersOnly(w http.ResponseWriter) {
  475. writeResponse(w, http.StatusOK, nil, mimeNone)
  476. }
  477. // writeErrorRespone writes error headers
  478. func writeErrorResponse(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL) {
  479. apiError := getAPIError(errorCode)
  480. // Generate error response.
  481. errorResponse := getAPIErrorResponse(apiError, reqURL.Path)
  482. encodedErrorResponse := encodeResponse(errorResponse)
  483. writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeXML)
  484. }
  485. func writeErrorResponseHeadersOnly(w http.ResponseWriter, errorCode APIErrorCode) {
  486. apiError := getAPIError(errorCode)
  487. writeResponse(w, apiError.HTTPStatusCode, nil, mimeNone)
  488. }