diff --git a/CHEATSHEET.md b/CHEATSHEET.md index 4270acb..8ad3870 100644 --- a/CHEATSHEET.md +++ b/CHEATSHEET.md @@ -63,6 +63,7 @@ pgb.getAppLog(id, platform) // get build log for a single platform pgb.downloadApp(id, platform, [path]) // save app to optional path pgb.pullApp(id) // pull new version from repo and trigger a build pgb.buildApp(id, [platform]) // build app, optionally by single platform +pbg.awaitAndDownloadApps(id, [{platform: path}], [pollingIntervalMs: number]) // poll for completed builds, and download them when they are ready pgb.deleteApp(id) // delete app /* COLLABORATORS */ diff --git a/index.js b/index.js index 711159e..69c5f86 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports=function(e){var t={};function r(s){if(t[s])return t[s].exports;var n=t[s]={i:s,l:!1,exports:{}};return e[s].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=e,r.c=t,r.d=function(e,t,s){r.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:s})},r.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r.w={},r(r.s=12)}([function(e,t,r){const s=r(2),n=r(1),o=r(3).Writable;e.exports={merge:function(){const e=(t,r)=>{for(let s in t)t[s]&&t[s].constructor===Object?(r[s]=r[s]||{},r[s]=e(t[s],r[s])):r[s]=t[s];return r};return Array.from(arguments).reduce((t,r)=>e(r,t),{})},mkdirp:function(e){e=e||"";let t=n.resolve(e).split(n.sep);for(let e=1;e{let t,r;return e.endsWith(n.sep)||s.existsSync(e)&&s.statSync(e).isDirectory()?r=e:(r=n.dirname(e),t=n.basename(e)),{filename:t,directory:r}}}},function(e,t){e.exports=require("path")},function(e,t){e.exports=require("fs")},function(e,t){e.exports=require("stream")},function(e,t){e.exports=require("url")},function(e,t){e.exports=require("os")},function(e,t){e.exports=require("yazl")},function(e,t,r){const s=r(2),n=r(6),o=r(1),a=e=>{let t=[],r=[],n=e=>{s.readdirSync(e).forEach(a=>{let i=o.join(e,a);if(!a.startsWith(".")||a.match(/^\.pgb/))try{let e=s.statSync(i);s.closeSync(s.openSync(i,"r")),e.isDirectory()?(t.push({path:i,size:0}),n(i)):t.push({path:i,size:e.size})}catch(e){r.push(`${i} [${e.code}]`)}else r.push(`${i} [HIDDEN]`)})};return n(e),{list:t,skipped:r}};e.exports={getFileList:a,zipDir:(e,t,r)=>new Promise((i,p)=>{let u=a(e),d=s.createWriteStream(t),l=new n.ZipFile,h="",c=0,f=[0],m=0,g=0;const y=(e,t)=>{r&&r.emit(e,t)};y("zip/files",u);for(let t of u.list){let r=o.relative(e,t.path);s.statSync(t.path).isDirectory()?(l.addEmptyDirectory(r),m+=46+r.length+1):(l.addFile(t.path,r),m+=46+r.length),f.push(m+=t.size)}d.on("error",p),l.outputStream.on("error",p),l.outputStream.pipe(d).once("close",()=>{y("zip/write",{size:m,file:h,pos:m,delta:f[f.length-1]-g}),y("zip/end",!0),i()}),l.outputStream.on("data",e=>{for(let e=c;e= 4.0.0"},author:"Brett Rudd ",license:"MIT",repository:{type:"git",url:"phonegap-build/pgb-api"},devDependencies:{eslint:"^4.19.1","eslint-config-standard":"^11.0.0","eslint-plugin-import":"^2.10.0","eslint-plugin-jest":"^21.15.0","eslint-plugin-node":"^6.0.1","eslint-plugin-promise":"^3.7.0","eslint-plugin-standard":"^3.0.1",express:"^4.16.3",jest:"^22.4.3","jest-plugin-fs":"^2.9.0",multer:"^1.3.0",webpack:"^4.5.0","webpack-cli":"^2.0.14","webpack-node-externals":"^1.7.2"},dependencies:{yazl:"^2.4.3"}}},function(e,t,r){const s=r(0).merge,n=r(0).mkdirp,o=r(0).TextStream,a=r(0).getPath,i=r(4),p=r(2),u=r(1),d=r(3).Stream,l={headers:{"User-Agent":`pgb-api/${r(10).version} node/${process.version} (${process.platform})`}},h=(e,t,r)=>{e.opts.events&&e.opts.events.emit&&e.opts.events.emit(t,r)},c=(e,t)=>(t=t||{},new Promise((d,g)=>{let y=0,_={},b=i.parse(e);_.opts=s(l,b,t),f(_);const $="https:"===_.opts.protocol?r(9):r(8);h(_,"api/headers",_.opts.headers),h(_,"debug",`${_.opts.method||"GET"} ${e}`),_.req=$.request(_.opts,s=>{_.response=s;let l=Number.parseInt(s.headers["content-length"])||null,f=Math.trunc(s.statusCode/100);if(3===f&&"location"in s.headers){let r=i.parse(s.headers.location);return t.headers&&_.opts.hostname!==r.hostname&&delete t.headers.Authorization,r=i.resolve(e,r.href),h(_,"debug",`${_.req.method} ${e} -> ${s.statusCode} ${r}`),d(c(r,t))}if(_.opts.save&&2===f)if(_.opts.save instanceof r(3).Writable)_.output=_.opts.save;else{let e=a(_.opts.save);e.filename=decodeURI(e.filename||u.basename(_.opts.pathname)||"app.download"),_.output=u.join(e.directory,e.filename);try{n(e.directory),p.closeSync(p.openSync(_.output,"w")),_.path=u.resolve(_.output)}catch(e){return g(e)}h(_,"debug",`saving to ${_.path}`),_.output=p.createWriteStream(_.output)}h(_,"api/connect",{statusCode:s.statusCode,size:l,headers:s.headers,path:_.path,url:e,method:_.req.method}),h(_,"debug",`${_.req.method} ${e} -> ${s.statusCode}`),_.output=_.output||new o,_.output.once("error",g),s.pipe(_.output),s.on("data",e=>{y+=e.length,h(_,"api/read",{size:l,pos:y,delta:e.length})}),s.once("end",()=>{let e=(e=>{let t,r;if(e.output instanceof o){t=e.output.toString();try{r=JSON.parse(t)}catch(e){}}return e.path||r||t})(_);if(2===f)d(e);else{let t=new Error(e.error||e);t.statusCode=s.statusCode,g(t)}})}),_.req.once("error",g),m(_)})),f=e=>{if(e._payload=[],e._contentLength=0,null==e.opts.data)return;for(let t in e.opts.data){let r=e.opts.data[t];if(e._payload.push("------pgbapi\r\n"),r instanceof d){let s=u.basename(r.path);e._payload.push(`Content-Disposition: form-data; name="${t}"; filename="${s.replace('"','\\"')}"\r\n`),e._payload.push("Content-Type: application/octet-stream\r\n\r\n"),e._payload.push(r),e._payload.push("\r\n")}else e._payload.push(`Content-Disposition: form-data; name="${t}";\r\n\r\n`),r&&"Object"===r.constructor.name&&(r=JSON.stringify(r)),e._payload.push(`${r}\r\n`)}e._payload.push("------pgbapi--\r\n");for(let t of e._payload)e._contentLength+=t.length||p.statSync(t.path).size;e.opts.headers["Content-Length"]=e._contentLength,e.opts.headers["Content-Type"]="multipart/form-data; boundary=----pgbapi"},m=e=>{if(0===e._payload.length)return e.req.end();let t=0;let r=e._payload.slice(0),s=()=>{g=r.shift(),y=(()=>0===r.length?e.req.end():s()),g instanceof d?(g.on("data",r=>{t+=r.length,h(e,"api/write",{size:e._contentLength,pos:t,delta:r.length})}),g.once("end",y),g.pipe(e.req,{end:!1})):(t+=g.length,e.req.write(g),h(e,"api/write",{size:e._contentLength,pos:t,delta:g.length}),y())};s()};var g,y;e.exports={post:(e,t)=>c(e,s(t,{method:"POST"})),put:(e,t)=>c(e,s(t,{method:"PUT"})),del:(e,t)=>c(e,s(t,{method:"DELETE"})),get:c}},function(e,t,r){"use strict";const s=r(0).merge,n=r(0).getPath,o=r(0).mkdirp,a=r(11),i=r(7),p=r(2),u=r(1),d=r(5),l=r(4).parse,h="https://build.phonegap.com/api/v1";e.exports=(e=>new class{constructor(e){this.defaults=s(e)}_get(e,t){return a.get(h+e,s(this.defaults,t))}_post(e,t){return a.post(h+e,s(this.defaults,t))}_put(e,t){return a.put(h+e,s(this.defaults,t))}_del(e,t){return a.del(h+e,s(this.defaults,t))}me(){return this._get("/me")}getToken(){return this._post("/token")}getApps(){return this._get("/apps")}getStatus(e){return this._get(`/apps/${e}/status`)}getApp(e){return this._get(`/apps/${e}`)}getAppLog(e,t){return this._get(`/apps/${e}/logs/${t}/build`)}_app(e,t){return e?this._put(`/apps/${e}`,{data:t}):this._post("/apps",{data:t})}deleteApp(e){return this._del(`/apps/${e}`)}downloadApp(e,t,r){return this._get(`/apps/${e}/${t}`,{save:r})}buildApp(e,t){return this._post(`/apps/${e}/build/${t||""}`)}addCollaborator(e,t,r){return this._post(`/apps/${e}/collaborators`,{data:{email:t,role:r}})}updateCollaborator(e,t,r){return this._put(`/apps/${e}/collaborators/${t}`,{data:{role:r}})}deleteCollaborator(e,t){return this._del(`/apps/${e}/collaborators/${t}`)}getKeys(e){return this._get(`/keys/${e||""}/`)}getKey(e,t){return this._get(`/keys/${e}/${t}`)}addKey(e,t){return this._post(`/keys/${e}`,{data:t})}updateKey(e,t,r){return this._put(`/keys/${e}/${t}`,{data:r})}deleteKey(e,t){return this._del(`/keys/${e}/${t}`)}currentSupport(){return this._get("/current_support")}isRepo(e){try{return e.toString().match(/^[a-z0-9_-][a-z0-9_.-]*\/[a-z0-9_.-]+(#[a-z0-9_.-]*)?$/i)||l(e).hostname}catch(e){return!1}}addApp(e,t){return this.updateApp(null,e,t)}updateApp(e,t,r){r||"string"==typeof t||(r=t,t=null);let s=p.existsSync(t);return t?s&&p.statSync(t).isDirectory()?this.addAppFromDir(e,t,r):this.isRepo(t)?this.addAppfromRepo(e,t,r):this.addAppFromFile(e,t,r):this._app(e,r)}addAppFromDir(e,t,r){return new Promise((s,a)=>{let l=!1,h=r.zip;delete r.zip,h||(h=u.join(d.tmpdir(),"pgb-"+Math.random().toString(32).slice(2)+".zip"),l=!0);let c=n(h);c.filename=c.filename||"app.zip",o(c.directory),h=u.join(c.directory,c.filename);const f=(e,t)=>{this.defaults.events&&this.defaults.events.emit&&this.defaults.events.emit(e,t)},m=()=>{l&&p.existsSync(h)&&p.statSync(h).isFile()&&(p.unlinkSync(h),f("debug",`archive deleted ${h}`))};f("debug",`archiving ${t} to ${h}`),i.zipDir(t,h,this.defaults.events).then(()=>this.addAppFromFile(e,h,r)).then(e=>{m(),s(e)}).catch(e=>{m(),a(e)})})}addAppfromRepo(e,t,r){return this._app(e,s(r,{repo:t}))}addAppFromFile(e,t,r){return new Promise((n,o)=>{let a=p.createReadStream(t);a.once("error",o),this._app(e,s(r,{file:a})).then(n,o)})}pullApp(e,t){return this._app(e,s(t,{pull:!0}))}lockKey(e,t){return this.updateKey(e,t,{lock:!0})}addIOSKey(e,t,r,n){return new Promise((o,a)=>{let i=p.createReadStream(t);i.once("error",a);let u=p.createReadStream(r);return u.once("error",a),this.addKey("ios",s({title:e,profile:i,cert:u},n)).then(o,a)})}addWindowsKey(e,t,r){return new Promise((n,o)=>{let a=p.createReadStream(t);return a.once("error",o),this.addKey("windows",s({title:e,keystore:a},r)).then(n,o)})}addAndroidKey(e,t,r,n){return new Promise((o,a)=>{let i=p.createReadStream(r);return i.once("error",a),this.addKey("android",s({title:e,keystore:i,alias:t},n)).then(o,a)})}addWinphoneKey(e,t,r){return this.addKey("winphone",s({title:e,publisher_id:t},r))}unlockIOSKey(e,t){return this.updateKey("ios",e,{password:t})}unlockAndroidKey(e,t,r){return this.updateKey("android",e,{keystore_pw:t,key_pw:r})}unlockWindowsKey(e,t){return this.updateKey("windows",e,{password:t})}hasAuth(){return!(!this.defaults.headers||!this.defaults.headers.Authorization)}clearAuth(){this.hasAuth()&&delete this.defaults.headers.Authorization}addAuth(e,t){if(this.defaults.headers=this.defaults.headers||{},e&&t){let r=`${e}:${t}`;r=Buffer.from!==Uint8Array.from?Buffer.from(r):new Buffer(r),this.defaults.headers.Authorization=`Basic ${r.toString("base64")}`}else e&&(this.defaults.headers.Authorization=`token ${e}`);return this}}(e))}]); \ No newline at end of file +module.exports=function(e){var t={};function r(s){if(t[s])return t[s].exports;var n=t[s]={i:s,l:!1,exports:{}};return e[s].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=e,r.c=t,r.d=function(e,t,s){r.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:s})},r.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r.w={},r(r.s=12)}([function(e,t,r){const s=r(2),n=r(1),o=r(3).Writable;e.exports={merge:function(){const e=(t,r)=>{for(let s in t)t[s]&&t[s].constructor===Object?(r[s]=r[s]||{},r[s]=e(t[s],r[s])):r[s]=t[s];return r};return Array.from(arguments).reduce((t,r)=>e(r,t),{})},mkdirp:function(e){e=e||"";let t=n.resolve(e).split(n.sep);for(let e=1;e{let t,r;return e.endsWith(n.sep)||s.existsSync(e)&&s.statSync(e).isDirectory()?r=e:(r=n.dirname(e),t=n.basename(e)),{filename:t,directory:r}}}},function(e,t){e.exports=require("path")},function(e,t){e.exports=require("fs")},function(e,t){e.exports=require("stream")},function(e,t){e.exports=require("url")},function(e,t){e.exports=require("os")},function(e,t){e.exports=require("yazl")},function(e,t,r){const s=r(2),n=r(6),o=r(1),i=e=>{let t=[],r=[],n=e=>{s.readdirSync(e).forEach(i=>{let a=o.join(e,i);if(!i.startsWith(".")||i.match(/^\.pgb/))try{let e=s.statSync(a);s.closeSync(s.openSync(a,"r")),e.isDirectory()?(t.push({path:a,size:0}),n(a)):t.push({path:a,size:e.size})}catch(e){r.push(`${a} [${e.code}]`)}else r.push(`${a} [HIDDEN]`)})};return n(e),{list:t,skipped:r}};e.exports={getFileList:i,zipDir:(e,t,r)=>new Promise((a,p)=>{let l=i(e),u=s.createWriteStream(t),d=new n.ZipFile,h="",c=0,f=[0],m=0,g=0;const y=(e,t)=>{r&&r.emit(e,t)};y("zip/files",l);for(let t of l.list){let r=o.relative(e,t.path);s.statSync(t.path).isDirectory()?(d.addEmptyDirectory(r),m+=46+r.length+1):(d.addFile(t.path,r),m+=46+r.length),f.push(m+=t.size)}u.on("error",p),d.outputStream.on("error",p),d.outputStream.pipe(u).once("close",()=>{y("zip/write",{size:m,file:h,pos:m,delta:f[f.length-1]-g}),y("zip/end",!0),a()}),d.outputStream.on("data",e=>{for(let e=c;e= 4.0.0"},author:"Brett Rudd ",license:"MIT",repository:{type:"git",url:"phonegap-build/pgb-api"},devDependencies:{eslint:"^4.19.1","eslint-config-standard":"^11.0.0","eslint-plugin-import":"^2.10.0","eslint-plugin-jest":"^21.15.0","eslint-plugin-node":"^6.0.1","eslint-plugin-promise":"^3.7.0","eslint-plugin-standard":"^3.0.1",express:"^4.16.3",jest:"^22.4.3","jest-plugin-fs":"^2.9.0",multer:"^1.3.0",webpack:"^4.5.0","webpack-cli":"^2.0.14","webpack-node-externals":"^1.7.2"},dependencies:{yazl:"^2.4.3"}}},function(e,t,r){const s=r(0).merge,n=r(0).mkdirp,o=r(0).TextStream,i=r(0).getPath,a=r(4),p=r(2),l=r(1),u=r(3).Stream,d={headers:{"User-Agent":`pgb-api/${r(10).version} node/${process.version} (${process.platform})`}},h=(e,t,r)=>{e.opts.events&&e.opts.events.emit&&e.opts.events.emit(t,r)},c=(e,t)=>(t=t||{},new Promise((u,g)=>{let y=0,_={},b=a.parse(e);_.opts=s(d,b,t),f(_);const w="https:"===_.opts.protocol?r(9):r(8);h(_,"api/headers",_.opts.headers),h(_,"debug",`${_.opts.method||"GET"} ${e}`),_.req=w.request(_.opts,s=>{_.response=s;let d=Number.parseInt(s.headers["content-length"])||null,f=Math.trunc(s.statusCode/100);if(3===f&&"location"in s.headers){let r=a.parse(s.headers.location);return t.headers&&_.opts.hostname!==r.hostname&&delete t.headers.Authorization,r=a.resolve(e,r.href),h(_,"debug",`${_.req.method} ${e} -> ${s.statusCode} ${r}`),u(c(r,t))}if(_.opts.save&&2===f)if(_.opts.save instanceof r(3).Writable)_.output=_.opts.save;else{let e=i(_.opts.save);e.filename=decodeURI(e.filename||l.basename(_.opts.pathname)||"app.download"),_.output=l.join(e.directory,e.filename);try{n(e.directory),p.closeSync(p.openSync(_.output,"w")),_.path=l.resolve(_.output)}catch(e){return g(e)}h(_,"debug",`saving to ${_.path}`),_.output=p.createWriteStream(_.output)}h(_,"api/connect",{statusCode:s.statusCode,size:d,headers:s.headers,path:_.path,url:e,method:_.req.method}),h(_,"debug",`${_.req.method} ${e} -> ${s.statusCode}`),_.output=_.output||new o,_.output.once("error",g),s.pipe(_.output),s.on("data",e=>{y+=e.length,h(_,"api/read",{size:d,pos:y,delta:e.length})}),s.once("end",()=>{let e=(e=>{let t,r;if(e.output instanceof o){t=e.output.toString();try{r=JSON.parse(t)}catch(e){}}return e.path||r||t})(_);if(2===f)u(e);else{let t=new Error(e.error||e);t.statusCode=s.statusCode,g(t)}})}),_.req.once("error",g),m(_)})),f=e=>{if(e._payload=[],e._contentLength=0,null==e.opts.data)return;for(let t in e.opts.data){let r=e.opts.data[t];if(e._payload.push("------pgbapi\r\n"),r instanceof u){let s=l.basename(r.path);e._payload.push(`Content-Disposition: form-data; name="${t}"; filename="${s.replace('"','\\"')}"\r\n`),e._payload.push("Content-Type: application/octet-stream\r\n\r\n"),e._payload.push(r),e._payload.push("\r\n")}else e._payload.push(`Content-Disposition: form-data; name="${t}";\r\n\r\n`),r&&"Object"===r.constructor.name&&(r=JSON.stringify(r)),e._payload.push(`${r}\r\n`)}e._payload.push("------pgbapi--\r\n");for(let t of e._payload)e._contentLength+=t.length||p.statSync(t.path).size;e.opts.headers["Content-Length"]=e._contentLength,e.opts.headers["Content-Type"]="multipart/form-data; boundary=----pgbapi"},m=e=>{if(0===e._payload.length)return e.req.end();let t=0;let r=e._payload.slice(0),s=()=>{g=r.shift(),y=(()=>0===r.length?e.req.end():s()),g instanceof u?(g.on("data",r=>{t+=r.length,h(e,"api/write",{size:e._contentLength,pos:t,delta:r.length})}),g.once("end",y),g.pipe(e.req,{end:!1})):(t+=g.length,e.req.write(g),h(e,"api/write",{size:e._contentLength,pos:t,delta:g.length}),y())};s()};var g,y;e.exports={post:(e,t)=>c(e,s(t,{method:"POST"})),put:(e,t)=>c(e,s(t,{method:"PUT"})),del:(e,t)=>c(e,s(t,{method:"DELETE"})),get:c}},function(e,t,r){"use strict";const s=r(0).merge,n=r(0).getPath,o=r(0).mkdirp,i=r(11),a=r(7),p=r(2),l=r(1),u=r(5),d=r(4).parse,h="https://build.phonegap.com/api/v1";e.exports=(e=>new class{constructor(e){this.defaults=s(e)}_get(e,t){return i.get(h+e,s(this.defaults,t))}_post(e,t){return i.post(h+e,s(this.defaults,t))}_put(e,t){return i.put(h+e,s(this.defaults,t))}_del(e,t){return i.del(h+e,s(this.defaults,t))}me(){return this._get("/me")}getToken(){return this._post("/token")}getApps(){return this._get("/apps")}getStatus(e){return this._get(`/apps/${e}/status`)}getApp(e){return this._get(`/apps/${e}`)}getAppLog(e,t){return this._get(`/apps/${e}/logs/${t}/build`)}_app(e,t){return e?this._put(`/apps/${e}`,{data:t}):this._post("/apps",{data:t})}deleteApp(e){return this._del(`/apps/${e}`)}downloadApp(e,t,r){return this._get(`/apps/${e}/${t}`,{save:r})}buildApp(e,t){return this._post(`/apps/${e}/build/${t||""}`)}addCollaborator(e,t,r){return this._post(`/apps/${e}/collaborators`,{data:{email:t,role:r}})}updateCollaborator(e,t,r){return this._put(`/apps/${e}/collaborators/${t}`,{data:{role:r}})}deleteCollaborator(e,t){return this._del(`/apps/${e}/collaborators/${t}`)}getKeys(e){return this._get(`/keys/${e||""}/`)}getKey(e,t){return this._get(`/keys/${e}/${t}`)}addKey(e,t){return this._post(`/keys/${e}`,{data:t})}updateKey(e,t,r){return this._put(`/keys/${e}/${t}`,{data:r})}deleteKey(e,t){return this._del(`/keys/${e}/${t}`)}currentSupport(){return this._get("/current_support")}isRepo(e){try{return e.toString().match(/^[a-z0-9_-][a-z0-9_.-]*\/[a-z0-9_.-]+(#[a-z0-9_.-]*)?$/i)||d(e).hostname}catch(e){return!1}}addApp(e,t){return this.updateApp(null,e,t)}updateApp(e,t,r){r||"string"==typeof t||(r=t,t=null);let s=p.existsSync(t);return t?s&&p.statSync(t).isDirectory()?this.addAppFromDir(e,t,r):this.isRepo(t)?this.addAppfromRepo(e,t,r):this.addAppFromFile(e,t,r):this._app(e,r)}addAppFromDir(e,t,r){return new Promise((s,i)=>{let d=!1,h=r.zip;delete r.zip,h||(h=l.join(u.tmpdir(),"pgb-"+Math.random().toString(32).slice(2)+".zip"),d=!0);let c=n(h);c.filename=c.filename||"app.zip",o(c.directory),h=l.join(c.directory,c.filename);const f=(e,t)=>{this.defaults.events&&this.defaults.events.emit&&this.defaults.events.emit(e,t)},m=()=>{d&&p.existsSync(h)&&p.statSync(h).isFile()&&(p.unlinkSync(h),f("debug",`archive deleted ${h}`))};f("debug",`archiving ${t} to ${h}`),a.zipDir(t,h,this.defaults.events).then(()=>this.addAppFromFile(e,h,r)).then(e=>{m(),s(e)}).catch(e=>{m(),i(e)})})}addAppfromRepo(e,t,r){return this._app(e,s(r,{repo:t}))}addAppFromFile(e,t,r){return new Promise((n,o)=>{let i=p.createReadStream(t);i.once("error",o),this._app(e,s(r,{file:i})).then(n,o)})}pullApp(e,t){return this._app(e,s(t,{pull:!0}))}lockKey(e,t){return this.updateKey(e,t,{lock:!0})}addIOSKey(e,t,r,n){return new Promise((o,i)=>{let a=p.createReadStream(t);a.once("error",i);let l=p.createReadStream(r);return l.once("error",i),this.addKey("ios",s({title:e,profile:a,cert:l},n)).then(o,i)})}addWindowsKey(e,t,r){return new Promise((n,o)=>{let i=p.createReadStream(t);return i.once("error",o),this.addKey("windows",s({title:e,keystore:i},r)).then(n,o)})}addAndroidKey(e,t,r,n){return new Promise((o,i)=>{let a=p.createReadStream(r);return a.once("error",i),this.addKey("android",s({title:e,keystore:a,alias:t},n)).then(o,i)})}addWinphoneKey(e,t,r){return this.addKey("winphone",s({title:e,publisher_id:t},r))}unlockIOSKey(e,t){return this.updateKey("ios",e,{password:t})}unlockAndroidKey(e,t,r){return this.updateKey("android",e,{keystore_pw:t,key_pw:r})}unlockWindowsKey(e,t){return this.updateKey("windows",e,{password:t})}hasAuth(){return!(!this.defaults.headers||!this.defaults.headers.Authorization)}clearAuth(){this.hasAuth()&&delete this.defaults.headers.Authorization}addAuth(e,t){if(this.defaults.headers=this.defaults.headers||{},e&&t){let r=`${e}:${t}`;r=Buffer.from!==Uint8Array.from?Buffer.from(r):new Buffer(r),this.defaults.headers.Authorization=`Basic ${r.toString("base64")}`}else e&&(this.defaults.headers.Authorization=`token ${e}`);return this}awaitAndDownloadApps(e,t,r){const s=r&&void 0!==r.pollingIntervalMs?r.pollingIntervalMs:1e3;let n={allBuildsFinished:!1,buildFinished:{},activeDownloads:0,errorEncountered:!1,returnValue:{success:{},error:{getStatus:null,downloadApp:{},build:{}}}};const o=(e,t)=>{this.defaults.events&&this.defaults.events.emit&&this.defaults.events.emit(e,t)};return new Promise((r,i)=>{const a=()=>{n.errorEncountered?i(n.returnValue):r(n.returnValue)};let p=()=>{o("downloads/polling",{}),this.getStatus(e).then(r=>{o("downloads/status",r);let s=r.status;for(let i in s)if(n.buildFinished[i]||"complete"!==s[i])n.buildFinished[i]||"error"!==s[i]||(n.buildFinished[i]=!0,n.errorEncountered=!0,n.returnValue.error.build[i]=r.error[i],o("downloads/buildError",{[i]:r.error[i]}));else{const r=i;n.buildFinished[r]=!0,n.activeDownloads++,o("downloads/starting",r),this.downloadApp(e,r,t&&t[r]).then(e=>{n.returnValue.success[r]=e,o("downloads/sucess",{platform:r,return:e})}).catch(e=>{n.errorEncountered=!0,n.returnValue.error.downloadApp[r]=e,o("downloads/error",{platform:r,error:e})}).finally(()=>{n.activeDownloads--,n.allBuildsFinished&&0===n.activeDownloads&&a()})}r.completed&&(n.allBuildsFinished=!0)}).catch(e=>{n.errorEncountered=!0,n.allBuildsFinished=!0,n.returnValue.error.getStatus=e}).finally(()=>{if(n.allBuildsFinished)0===n.activeDownloads&&a();else{const e=setTimeout(p,s);o("downloads/waiting",{timeoutID:e})}})};p()})}}(e))}]); \ No newline at end of file diff --git a/src/api.js b/src/api.js index 97ef802..6b2bec4 100644 --- a/src/api.js +++ b/src/api.js @@ -305,6 +305,117 @@ class PGBApi { } return this } + + /* + * Poll via getStatus for completed platform builds, and download them via downloadApp. + * @param {string} id - phonegab build application identifier. + * @param {object} saves - map: platform -> save, where save is passed to downloadApp when downloading platform. + * @param {object} opts - configuration options + * @return {Promise} - Thenable that executes the poll and download. + */ + awaitAndDownloadApps(id, saves, opts) { + const pollingIntervalMs = (opts && (undefined !== opts.pollingIntervalMs)) + ? opts.pollingIntervalMs + : 1000 + + let state = { + allBuildsFinished: false, + buildFinished: {}, + activeDownloads: 0, + errorEncountered: false, + returnValue: { + success: {}, + error: { + getStatus: null, + downloadApp: {}, + build: {} + } + } + } + + const emit = (evt, data) => { + if (this.defaults.events && this.defaults.events.emit) { + this.defaults.events.emit(evt, data) + } + } + + let pollAndDownload = (resolve, reject) => { + const resolveOrReject = () => { + if (state.errorEncountered) { + reject(state.returnValue) + } else { + resolve(state.returnValue) + } + } + + let pollOnce = () => { + emit('downloads/polling', {}) + this.getStatus(id).then((result) => { + emit('downloads/status', result) + let status = result.status + for (let platform in status) { + // The first time we see a complete build, start to download it. + if (!state.buildFinished[platform] && status[platform] === 'complete') { + const downloadPlatform = platform // closure + state.buildFinished[downloadPlatform] = true + + state.activeDownloads++ + emit('downloads/starting', downloadPlatform) + this.downloadApp(id, downloadPlatform, saves && saves[downloadPlatform]).then((ret) => { + state.returnValue.success[downloadPlatform] = ret + emit('downloads/sucess', { + 'platform': downloadPlatform, + 'return': ret + }) + }).catch((err) => { + state.errorEncountered = true + + state.returnValue.error.downloadApp[downloadPlatform] = err + emit('downloads/error', { + 'platform': downloadPlatform, + 'error': err + }) + }).finally(() => { + state.activeDownloads-- + // Last download to complete resolves or rejects the promise. + if (state.allBuildsFinished && state.activeDownloads === 0) { + resolveOrReject() + } + }) + } else if (!state.buildFinished[platform] && status[platform] === 'error') { + state.buildFinished[platform] = true + state.errorEncountered = true + state.returnValue.error.build[platform] = result.error[platform] + emit('downloads/buildError', {[platform]: result.error[platform]}) + } + } + + if (result.completed) { + // Raise flag for completed downloads to pick up + state.allBuildsFinished = true + } + }).catch((err) => { + state.errorEncountered = true + state.allBuildsFinished = true + state.returnValue.error.getStatus = err + }).finally(() => { + if (state.allBuildsFinished) { + if (state.activeDownloads === 0) { + resolveOrReject() + } + } else { + // try again + const timeoutID = setTimeout(pollOnce, pollingIntervalMs) + emit('downloads/waiting', {'timeoutID': timeoutID}) + } + }) + } + + pollOnce() + } + + return new Promise(pollAndDownload) + }; } module.exports = (opts) => new PGBApi(opts) diff --git a/test/api.js b/test/api.js index 7d2b315..dd741eb 100644 --- a/test/api.js +++ b/test/api.js @@ -455,4 +455,157 @@ describe('api', () => { expect(api.defaults.headers).toEqual({}) }) }) + + describe('downloads', () => { + test('awaitAndDownloadApps success', (done) => { + let eventEmitter = new (require('events'))() + let api = apiClient({events: eventEmitter}) + + eventEmitter.on('downloads/waiting', (evt) => { + jest.runOnlyPendingTimers() + }) + + restClient.get.mockResolvedValueOnce({ + 'completed': false, + 'errors': {}, + 'status': { + 'ios': 'complete', + 'android': 'pending', + 'winphone': 'skipped' + } + }).mockResolvedValueOnce( + 'iosdownload' + ).mockResolvedValueOnce({ + 'completed': true, + 'errors': [], + 'status': { + 'ios': 'complete', + 'android': 'complete', + 'winphone': 'skipped' + } + }).mockResolvedValueOnce( + 'android' + ) + api.awaitAndDownloadApps(12, {}).then((val) => { + expect(val).toEqual({ + 'success': { + 'android': 'android', + 'ios': 'iosdownload' + }, + 'error': { + getStatus: null, + downloadApp: {}, + build: {} + } + }) + done() + }) + } + ) + }) + + test('awaitAndDownloadApps build failure', (done) => { + let eventEmitter = new (require('events'))() + let api = apiClient({events: eventEmitter}) + + eventEmitter.on('downloads/waiting', (evt) => { + jest.runOnlyPendingTimers() + }) + + restClient.get.mockResolvedValueOnce({ + 'completed': false, + 'errors': {}, + 'status': { + 'ios': 'pending', + 'android': 'complete', + 'winphone': 'skipped' + } + }).mockResolvedValueOnce( + 'android' + ).mockResolvedValueOnce({ + 'completed': true, + 'error': {'ios': 'ios_failure'}, + 'status': { + 'ios': 'error', + 'android': 'complete', + 'winphone': 'skipped' + } + }) + api.awaitAndDownloadApps(12, {}).catch((val) => { + expect(val).toEqual({ + 'success': { + 'android': 'android' + }, + 'error': { + getStatus: null, + downloadApp: {}, + build: {'ios': 'ios_failure'} + } + }) + done() + }) + }) + + test('awaitAndDownloadApps download failure', (done) => { + let eventEmitter = new (require('events'))() + let api = apiClient({events: eventEmitter}) + + eventEmitter.on('downloads/waiting', (evt) => { + jest.runOnlyPendingTimers() + }) + + restClient.get.mockResolvedValueOnce({ + 'completed': false, + 'error': {}, + 'status': { + 'ios': 'pending', + 'android': 'complete', + 'winphone': 'skipped' + } + }).mockRejectedValueOnce( + 'android' + ).mockResolvedValueOnce({ + 'completed': true, + 'error': {'ios': 'ios_failure'}, + 'status': { + 'ios': 'error', + 'android': 'complete', + 'winphone': 'skipped' + } + }) + api.awaitAndDownloadApps(12, {}).catch((val) => { + expect(val).toEqual({ + 'success': { + }, + 'error': { + getStatus: null, + downloadApp: {'android': 'android'}, + build: {'ios': 'ios_failure'} + } + }) + done() + }) + }) + + test('awaitAndDownloadApps status failure', (done) => { + // No event emitter to test paths without it + // Not required here since we fail early, so no setTimeout which we need to pump + // in response to events + + restClient.get.mockRejectedValueOnce( + 'some problem with status' + ) + api.awaitAndDownloadApps(12, {}, {pollingIntervalMs: 0}).catch((val) => { + expect(val).toEqual({ + 'success': { + }, + 'error': { + getStatus: 'some problem with status', + downloadApp: {}, + build: {} + } + }) + done() + }) + }) })