2
2
// Licensed under the MIT license.
3
3
4
4
import { hooks } from "@feathersjs/hooks" ;
5
- import { LogProvider , SystemError , UserError } from "@microsoft/teamsfx-api" ;
5
+ import { LogProvider , SystemError , TeamsAppManifest , UserError } from "@microsoft/teamsfx-api" ;
6
6
import AdmZip from "adm-zip" ;
7
7
import FormData from "form-data" ;
8
8
import fs from "fs-extra" ;
@@ -20,10 +20,17 @@ import { waitSeconds } from "../../common/utils";
20
20
import { WrappedAxiosClient } from "../../common/wrappedAxiosClient" ;
21
21
import { NotExtendedToM365Error } from "./errors" ;
22
22
import { MosServiceEndpoint } from "./serviceConstant" ;
23
+ import { IsDeclarativeAgentManifest } from "../../common/projectTypeChecker" ;
24
+ import stripBom from "strip-bom" ;
23
25
24
26
const M365ErrorSource = "M365" ;
25
27
const M365ErrorComponent = "PackageService" ;
26
28
29
+ export enum AppScope {
30
+ Personal = "Personal" ,
31
+ Shared = "Shared" ,
32
+ }
33
+
27
34
// Call m365 service for package CRUD
28
35
export class PackageService {
29
36
private static sharedInstance : PackageService ;
@@ -139,14 +146,96 @@ export class PackageService {
139
146
}
140
147
141
148
@hooks ( [ ErrorContextMW ( { source : M365ErrorSource , component : M365ErrorComponent } ) ] )
142
- public async sideLoading ( token : string , manifestPath : string ) : Promise < [ string , string ] > {
149
+ public async sideLoading (
150
+ token : string ,
151
+ packagePath : string ,
152
+ appScope = AppScope . Personal
153
+ ) : Promise < [ string , string , string ] > {
154
+ const manifest = this . getManifestFromZip ( packagePath ) ;
155
+ if ( ! manifest ) {
156
+ throw new Error ( "Invalid app package zip. manifest.json is missing" ) ;
157
+ }
158
+ const isDelcarativeAgentApp = IsDeclarativeAgentManifest ( manifest ) ;
159
+ if ( isDelcarativeAgentApp ) {
160
+ const res = await this . sideLoadingV2 ( token , packagePath , appScope ) ;
161
+ let shareLink = "" ;
162
+ if ( appScope == AppScope . Shared ) {
163
+ shareLink = await this . getShareLink ( token , res [ 0 ] ) ;
164
+ }
165
+ return [ res [ 0 ] , res [ 1 ] , shareLink ] ;
166
+ } else {
167
+ const res = await this . sideLoadingV1 ( token , packagePath ) ;
168
+ return [ res [ 0 ] , res [ 1 ] , "" ] ;
169
+ }
170
+ }
171
+ // Side loading using Builder API
172
+ @hooks ( [ ErrorContextMW ( { source : M365ErrorSource , component : M365ErrorComponent } ) ] )
173
+ public async sideLoadingV2 (
174
+ token : string ,
175
+ manifestPath : string ,
176
+ appScope : AppScope
177
+ ) : Promise < [ string , string ] > {
143
178
try {
144
179
this . checkZip ( manifestPath ) ;
145
180
const data = await fs . readFile ( manifestPath ) ;
146
181
const content = new FormData ( ) ;
147
182
content . append ( "package" , data ) ;
148
183
const serviceUrl = await this . getTitleServiceUrl ( token ) ;
149
- this . logger ?. verbose ( "Uploading package ..." ) ;
184
+ this . logger ?. debug ( "Uploading package with sideLoading V2 ..." ) ;
185
+ const uploadHeaders = content . getHeaders ( ) ;
186
+ uploadHeaders [ "Authorization" ] = `Bearer ${ token } ` ;
187
+ const uploadResponse = await this . axiosInstance . post (
188
+ "/builder/v1/users/packages" ,
189
+ content . getBuffer ( ) ,
190
+ {
191
+ baseURL : serviceUrl ,
192
+ headers : uploadHeaders ,
193
+ params : {
194
+ scope : appScope ,
195
+ } ,
196
+ }
197
+ ) ;
198
+
199
+ const statusId = uploadResponse . data . statusId ;
200
+ this . logger ?. debug ( `Acquiring package with statusId: ${ statusId as string } ...` ) ;
201
+
202
+ do {
203
+ const statusResponse = await this . axiosInstance . get (
204
+ `/builder/v1/users/packages/status/${ statusId as string } ` ,
205
+ {
206
+ baseURL : serviceUrl ,
207
+ headers : { Authorization : `Bearer ${ token } ` } ,
208
+ }
209
+ ) ;
210
+ const resCode = statusResponse . status ;
211
+ this . logger ?. debug ( `Package status: ${ resCode } ...` ) ;
212
+ if ( resCode === 200 ) {
213
+ const titleId : string = statusResponse . data . titleId ;
214
+ const appId : string = statusResponse . data . appId ;
215
+ this . logger ?. info ( `TitleId: ${ titleId } ` ) ;
216
+ this . logger ?. info ( `AppId: ${ appId } ` ) ;
217
+ this . logger ?. verbose ( "Sideloading done." ) ;
218
+ return [ titleId , appId ] ;
219
+ } else {
220
+ await waitSeconds ( 2 ) ;
221
+ }
222
+ } while ( true ) ;
223
+ } catch ( error : any ) {
224
+ if ( error . response ) {
225
+ error = this . traceError ( error ) ;
226
+ }
227
+ throw assembleError ( error , M365ErrorSource ) ;
228
+ }
229
+ }
230
+ @hooks ( [ ErrorContextMW ( { source : M365ErrorSource , component : M365ErrorComponent } ) ] )
231
+ public async sideLoadingV1 ( token : string , manifestPath : string ) : Promise < [ string , string ] > {
232
+ try {
233
+ this . checkZip ( manifestPath ) ;
234
+ const data = await fs . readFile ( manifestPath ) ;
235
+ const content = new FormData ( ) ;
236
+ content . append ( "package" , data ) ;
237
+ const serviceUrl = await this . getTitleServiceUrl ( token ) ;
238
+ this . logger ?. debug ( "Uploading package with sideLoading V1 ..." ) ;
150
239
const uploadHeaders = content . getHeaders ( ) ;
151
240
uploadHeaders [ "Authorization" ] = `Bearer ${ token } ` ;
152
241
const uploadResponse = await this . axiosInstance . post (
@@ -211,6 +300,27 @@ export class PackageService {
211
300
}
212
301
}
213
302
@hooks ( [ ErrorContextMW ( { source : M365ErrorSource , component : M365ErrorComponent } ) ] )
303
+ public async getShareLink ( token : string , titleId : string ) : Promise < string > {
304
+ const serviceUrl = await this . getTitleServiceUrl ( token ) ;
305
+ try {
306
+ const resp = await this . axiosInstance . get (
307
+ `/marketplace/v1/users/titles/${ titleId } /sharingInfo` ,
308
+ {
309
+ baseURL : serviceUrl ,
310
+ headers : {
311
+ Authorization : `Bearer ${ token } ` ,
312
+ } ,
313
+ }
314
+ ) ;
315
+ return resp . data . unifiedStoreLink ;
316
+ } catch ( error : any ) {
317
+ if ( error . response ) {
318
+ error = this . traceError ( error ) ;
319
+ }
320
+ throw assembleError ( error , M365ErrorSource ) ;
321
+ }
322
+ }
323
+ @hooks ( [ ErrorContextMW ( { source : M365ErrorSource , component : M365ErrorComponent } ) ] )
214
324
public async getLaunchInfoByManifestId ( token : string , manifestId : string ) : Promise < any > {
215
325
try {
216
326
const serviceUrl = await this . getTitleServiceUrl ( token ) ;
@@ -293,6 +403,24 @@ export class PackageService {
293
403
} ) ;
294
404
this . logger ?. verbose ( "Unacquiring done." ) ;
295
405
} catch ( error : any ) {
406
+ // try to delete in the builder API
407
+ try {
408
+ const serviceUrl = await this . getTitleServiceUrl ( token ) ;
409
+ this . logger ?. verbose ( `Unacquiring package with TitleId ${ titleId } in builder API...` ) ;
410
+ await this . axiosInstance . delete ( `/builder/v1/users/titles/${ titleId } ` , {
411
+ baseURL : serviceUrl ,
412
+ headers : {
413
+ Authorization : `Bearer ${ token } ` ,
414
+ } ,
415
+ } ) ;
416
+ this . logger ?. verbose ( "Unacquiring using builder api done." ) ;
417
+ return ;
418
+ } catch ( subError : any ) {
419
+ if ( subError . response ) {
420
+ subError = this . traceError ( subError ) ;
421
+ }
422
+ this . logger ?. error ( subError ) ;
423
+ }
296
424
if ( error . response ) {
297
425
error = this . traceError ( error ) ;
298
426
}
@@ -440,4 +568,15 @@ export class PackageService {
440
568
this . logger ?. warning ( `Please make sure input path is a valid app package zip. ${ path } ` ) ;
441
569
}
442
570
}
571
+
572
+ private getManifestFromZip ( path : string ) : TeamsAppManifest | undefined {
573
+ const zip = new AdmZip ( path ) ;
574
+ const manifestEntry = zip . getEntry ( "manifest.json" ) ;
575
+ if ( ! manifestEntry ) {
576
+ return undefined ;
577
+ }
578
+ let manifestContent = manifestEntry . getData ( ) . toString ( "utf8" ) ;
579
+ manifestContent = stripBom ( manifestContent ) ;
580
+ return JSON . parse ( manifestContent ) as TeamsAppManifest ;
581
+ }
443
582
}
0 commit comments