microsoft/vscode-react-native
Publicmirrored from https://github.com/microsoft/vscode-react-nativeAvailable
src/extension/appcenter/codepush/release-strategy/legacyCodePushServiceClient.ts
111lines · modeblame
bf370babSergey Akhalkov8 years ago | 1 | // Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. | |
| 3 | | |
| 4 | import * as request from "request"; | |
| 5 | import { DefaultApp } from "../../command/commandParams"; | |
| 6 | import * as fs from "fs"; | |
| 7 | import * as Q from "q"; | |
| 8 | import { models } from "../../api/index"; | |
| 9 | | |
| 10 | export interface PackageInfo { | |
| 11 | appVersion?: string; | |
| 12 | description?: string; | |
| 13 | isDisabled?: boolean; | |
| 14 | isMandatory?: boolean; | |
| 15 | label?: string; | |
| 16 | packageHash?: string; | |
| 17 | rollout?: number; | |
| 18 | } | |
| 19 | | |
| 20 | export interface Package extends PackageInfo { | |
| 21 | /*generated*/ blobUrl: string; | |
| 22 | /*generated*/ diffPackageMap?: PackageHashToBlobInfoMap; | |
| 23 | /*generated*/ originalLabel?: string; // Set on "Promote" and "Rollback" | |
| 24 | /*generated*/ originalDeployment?: string; // Set on "Promote" | |
| 25 | /*generated*/ releasedBy?: string; // Set by commitPackage | |
| 26 | /*generated*/ releaseMethod?: string; // "Upload", "Promote" or "Rollback". Unknown if unspecified | |
| 27 | /*generated*/ size: number; | |
| 28 | /*generated*/ uploadTime: number; | |
| 29 | } | |
| 30 | | |
| 31 | export interface PackageHashToBlobInfoMap { | |
| 32 | [packageHash: string]: BlobInfo; | |
| 33 | } | |
| 34 | | |
| 35 | export interface BlobInfo { | |
| 36 | size: number; | |
| 37 | url: string; | |
| 38 | } | |
| 39 | | |
| 40 | export type Headers = { [headerName: string]: string }; | |
| 41 | | |
| 42 | export default class LegacyCodePushServiceClient { | |
| 43 | private static API_VERSION: number = 2; | |
| 44 | | |
774e7f5fSergey Akhalkov8 years ago | 45 | constructor(private accessKey: string, private app: DefaultApp, private serverUrl: string) { |
bf370babSergey Akhalkov8 years ago | 46 | if (!accessKey) throw new Error("A token must be specified to execute server calls."); |
| 47 | if (!serverUrl) throw new Error("A server url must be specified to execute server calls."); | |
| 48 | } | |
| 49 | | |
| 50 | public release(deploymentName: string, filePath: string, updateMetadata: PackageInfo): Q.Promise<models.CodePushRelease> { | |
| 51 | const appName = this.app.identifier; | |
| 52 | return Q.Promise<models.CodePushRelease>((resolve, reject) => { | |
| 53 | const options = { | |
| 54 | url: this.serverUrl + this.urlEncode(`/apps/${this.appNameParam(appName)}/deployments/${deploymentName}/release`), | |
| 55 | headers: { | |
| 56 | "Accept": `application/vnd.code-push.v${LegacyCodePushServiceClient.API_VERSION}+json`, | |
| 57 | "Authorization": `Bearer ${this.accessKey}`, | |
| 58 | }, | |
| 59 | formData: { | |
| 60 | "packageInfo": JSON.stringify(updateMetadata), | |
| 61 | "package": fs.createReadStream(filePath), | |
| 62 | }, | |
| 63 | }; | |
| 64 | | |
| 65 | request.post(options, (err: any, httpResponse: any) => { | |
| 66 | if (err) { | |
| 67 | reject(this.getErrorMessage(err, httpResponse)); | |
| 68 | return; | |
| 69 | } | |
| 70 | if (httpResponse.statusCode === 201) { | |
| 71 | resolve(<models.CodePushRelease>JSON.parse(httpResponse.body).package); | |
| 72 | } else { | |
| 73 | reject(this.getErrorMessage(null, httpResponse)); | |
| 74 | return; | |
| 75 | } | |
| 76 | }); | |
| 77 | }); | |
| 78 | } | |
| 79 | | |
| 80 | // A template string tag function that URL encodes the substituted values | |
| 81 | private urlEncode(strings: any, ...values: string[]): string { | |
| 82 | let result = ""; | |
| 83 | for (let i = 0; i < strings.length; i++) { | |
| 84 | result += strings[i]; | |
| 85 | if (i < values.length) { | |
| 86 | result += encodeURIComponent(values[i]); | |
| 87 | } | |
| 88 | } | |
| 89 | | |
| 90 | return result; | |
| 91 | } | |
| 92 | | |
| 93 | // IIS and Azure web apps have this annoying behavior where %2F (URL encoded slashes) in the URL are URL decoded | |
| 94 | // BEFORE the requests reach node. That essentially means there's no good way to encode a "/" in the app name-- | |
| 95 | // URL encodeing will work when running locally but when running on Azure it gets decoded before express sees it, | |
| 96 | // so app names with slashes don't get routed properly. See https://github.com/tjanczuk/iisnode/issues/343 (or other sites | |
| 97 | // that complain about the same) for some more info. I explored some IIS config based workarounds, but the previous | |
| 98 | // link seems to say they won't work, so I eventually gave up on that. | |
| 99 | // Anyway, to workaround this issue, we now allow the client to encode / characters as ~~ (two tildes, URL encoded). | |
| 100 | // The CLI now converts / to ~~ if / appears in an app name, before passing that as part of the URL. This code below | |
| 101 | // does the encoding. It's hack, but seems like the least bad option here. | |
| 102 | // Eventually, this service will go away & we'll all be on Max's new service. That's hosted in docker, no more IIS, | |
| 103 | // so this issue should go away then. | |
| 104 | private appNameParam(appName: string) { | |
| 105 | return appName.replace("/", "~~"); | |
| 106 | } | |
| 107 | | |
| 108 | private getErrorMessage(error: Error | null, response: request.RequestResponse): string { | |
| 109 | return response && response.body ? response.body : (error ? error.message : ""); | |
| 110 | } | |
| 111 | } |