├── .gitignore ├── README.md ├── README_EN.md ├── csharp-demo ├── .gitignore ├── Demo.cs ├── PGYERAppUploader.cs ├── Request │ ├── BuildInfoRequest.cs │ ├── FormProperty.cs │ ├── GetCosTokenRequest.cs │ ├── ParameterizeBuilder.cs │ ├── Request.cs │ ├── UploadAppRequest.cs │ ├── UploadOption.cs │ └── Validator.cs ├── Response │ ├── BuildInfoResponse.cs │ ├── GetCosTokenResponse.cs │ └── Response.cs ├── csharp-demo.csproj └── csharp-demo.sln ├── java-demo └── AppUploadDemo.java ├── nodejs-demo ├── .gitignore ├── PGYERAppUploader.js ├── demo.js ├── package-lock.json └── package.json ├── php-demo ├── PGYERAppUploader.php └── demo.php ├── python-demo ├── README.md ├── demo.py ├── requirements.txt └── utils │ ├── __init__.py │ └── upload_pgyer.py └── shell-demo ├── README.md └── pgyer_upload.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 蒲公英 API 上传 App 代码示例 2 | 3 | ## 语言 4 | 5 | - [English](/README_EN.md) 6 | - [Simplified Chinese](/README.md) 7 | 8 | ## 说明 9 | 10 | [蒲公英 App 内测分发平台](https://www.pgyer.com) 是一个为 App 安装包提供内测托管、下载分发的平台,支持 iOS App 安装包(`.ipa`文件) 和 Andoid App 安装包(`.apk`文件)。 11 | 12 | 本项目演示了如何将 iOS 或 Android 安装包文件通过蒲公英 API 上传到蒲公英平台,并获取上传结果。 13 | 14 | 目前,我们已经支持了 **Java**, **Node.js**, **PHP**, **Python**, **Shell** 代码,也欢迎开发者贡献其他语言的代码。 15 | 16 | 注意:本项目采用蒲公英新版上传 API,新版 API 上传的速度更快!蒲公英旧版API(如 v1、v2)中的上传接口依然可用,但是速度没有新版快。 17 | 18 | ## 用法 19 | 20 | 具体可见项目中各个语言对应的文件夹中的示例代码: 21 | 22 | - [Shell](/shell-demo) 23 | - [Java](/java-demo) 24 | - [Node.js](/nodejs-demo) 25 | - [PHP](/php-demo) 26 | - [Python](/python-demo) 27 | - [C#](/csharp-demo) 28 | 29 | ## 资源 30 | 31 | - [通过一行 shell 命令自动化上传脚本](https://github.com/PGYER/upload-app-api-example/tree/main/shell-demo) 32 | - [Postman API 调用模板](https://www.postman.com/pgyerdevs/workspace/pgyer-api) 33 | - [Github Actions 工作流](https://github.com/PGYER/pgyer-upload-app-action) 34 | - [Fastlane 插件](https://github.com/shishirui/fastlane-plugin-pgyer) 35 | 36 | ## 链接 37 | 38 | - [蒲公英官方网站](https://www.pgyer.com) 39 | - [查看蒲公英 API KEY](https://www.pgyer.com/account/api) 40 | - [蒲公英 API 上传文档](https://www.pgyer.com/doc/view/api#fastUploadApp) 41 | 42 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # PGYER API Upload App Code Example 2 | 3 | ## Introduction 4 | 5 | [PGYER](https://www.pgyer.com) is a platform for hosting and distributing beta mobile apps, supporting iOS app packages (`.ipa` files) and Android app packages (`.apk` files). 6 | 7 | This project demonstrates how to use the PGYER API to upload iOS or Android installation package files to the PGYER platform and retrieve the upload result. 8 | 9 | Currently, we support code in **Java**, **Node.js**, **PHP**, **Python**, **Shell**, and welcome developers to contribute code in other languages. 10 | 11 | Note: This project uses the new version of the upload API, the upload speed is faster! The old version of API (e.g. v1, v2) is still available, but the speed is not as fast as the new version. 12 | 13 | 14 | ## Usage 15 | 16 | See the example code in the folders for each language in the project: 17 | 18 | - [C#](/csharp-demo) 19 | - [Java](/java-demo) 20 | - [Node.js](/nodejs-demo) 21 | - [PHP](/php-demo) 22 | - [Python](/python-demo) 23 | - [Shell](/shell-demo) 24 | 25 | ## Resources 26 | 27 | - [Upload app with a single shell command](https://github.com/PGYER/upload-app-api-example/tree/main/shell-demo) 28 | - [Postman API Call Template](https://www.postman.com/pgyerdevs/workspace/pgyer-api) 29 | - [Github Actions Workflow](https://github.com/PGYER/pgyer-upload-app-action) 30 | - [Fastlane Plugin](https://github.com/shishirui/fastlane-plugin-pgyer) 31 | 32 | ## Links 33 | 34 | - [PGYER Official Website](https://www.pgyer.com) 35 | - [Get PGYER API KEY](https://www.pgyer.com/account/api) 36 | - [PGYER API Upload Documentation](https://www.pgyer.com/doc/view/api#fastUploadApp) 37 | -------------------------------------------------------------------------------- /csharp-demo/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | [Bb]in/ 15 | [Oo]bj/ 16 | 17 | # MSTest test Results 18 | [Tt]est[Rr]esult*/ 19 | [Bb]uild[Ll]og.* 20 | 21 | *_i.c 22 | *_p.c 23 | *_i.h 24 | *.ilk 25 | *.meta 26 | *.obj 27 | *.pch 28 | *.pdb 29 | *.pgc 30 | *.pgd 31 | *.rsp 32 | *.sbr 33 | *.tlb 34 | *.tli 35 | *.tlh 36 | *.tmp 37 | *.tmp_proj 38 | *.log 39 | *.vspscc 40 | *.vssscc 41 | .builds 42 | *.pidb 43 | *.log 44 | *.svclog 45 | *.scc 46 | 47 | # Visual C++ cache files 48 | ipch/ 49 | *.aps 50 | *.ncb 51 | *.opensdf 52 | *.sdf 53 | *.cachefile 54 | 55 | # Visual Studio profiler 56 | *.psess 57 | *.vsp 58 | *.vspx 59 | 60 | # Guidance Automation Toolkit 61 | *.gpState 62 | 63 | # ReSharper is a .NET coding add-in 64 | _ReSharper*/ 65 | *.[Rr]e[Ss]harper 66 | *.DotSettings.user 67 | 68 | # Click-Once directory 69 | publish/ 70 | 71 | # Publish Web Output 72 | *.Publish.xml 73 | *.pubxml 74 | *.azurePubxml 75 | 76 | # NuGet Packages Directory 77 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 78 | packages/ 79 | ## TODO: If the tool you use requires repositories.config, also uncomment the next line 80 | !packages/repositories.config 81 | 82 | # Windows Azure Build Output 83 | csx/ 84 | *.build.csdef 85 | 86 | # Windows Store app package directory 87 | AppPackages/ 88 | 89 | # Others 90 | sql/ 91 | *.Cache 92 | ClientBin/ 93 | [Ss]tyle[Cc]op.* 94 | ![Ss]tyle[Cc]op.targets 95 | ~$* 96 | *~ 97 | *.dbmdl 98 | *.[Pp]ublish.xml 99 | 100 | *.publishsettings 101 | 102 | # RIA/Silverlight projects 103 | Generated_Code/ 104 | 105 | # Backup & report files from converting an old project file to a newer 106 | # Visual Studio version. Backup files are not needed, because we have git ;-) 107 | _UpgradeReport_Files/ 108 | Backup*/ 109 | UpgradeLog*.XML 110 | UpgradeLog*.htm 111 | 112 | # SQL Server files 113 | App_Data/*.mdf 114 | App_Data/*.ldf 115 | 116 | # ========================= 117 | # Windows detritus 118 | # ========================= 119 | 120 | # Windows image file caches 121 | Thumbs.db 122 | ehthumbs.db 123 | 124 | # Folder config file 125 | Desktop.ini 126 | 127 | # Recycle Bin used on file shares 128 | $RECYCLE.BIN/ 129 | 130 | # Mac desktop service store files 131 | .DS_Store 132 | 133 | _NCrunch* 134 | .vscode 135 | .devcontainer 136 | 137 | *.apk -------------------------------------------------------------------------------- /csharp-demo/Demo.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* 4 | * 以下代码为示例代码,可以在生产环境中使用。使用方法如下: 5 | * 6 | * 先实例化上传器 7 | * 8 | * PGYERAppUploader uploader = new PGYERAppUploader(""); 9 | * 10 | * 在上传器实例化以后, 通过调用 Upload 方法即可完成 App 上传。 11 | * 12 | * 13 | * 14 | * 示例: 15 | * PGYERAppUploader uploader = new PGYERAppUploader(""); 16 | * uploader.Upload(option); 17 | 18 | * 19 | * UploadOption 参数说明: (https://www.pgyer.com/doc/view/api#fastUploadApp) 20 | * 21 | * 对象成员名 是否必选 含义 22 | * FilePath Y App 文件的路径,可以是相对路径 23 | * Oversea Y 是否使用海外加速上传,值为:1 使用海外加速上传,2 国内加速上传;留空根据 IP 自动判断海外加速或国内加速 24 | * BuildInstallType N 应用安装方式,值为(1,2,3,默认为1 公开安装)。1:公开安装,2:密码安装,3:邀请安装 25 | * BuildPassword N 设置App安装密码,密码为空时默认公开安装 26 | * buildDescription N 应用介绍,如没有介绍请传空字符串,或不传。 27 | * BuildUpdateDescription N 版本更新描述,请传空字符串,或不传。 28 | * BuildInstallDate N 是否设置安装有效期,值为:1 设置有效时间, 2 长期有效,如果不填写不修改上一次的设置 29 | * BuildInstallStartDate N 安装有效期开始时间,字符串型,如:2018-01-01 30 | * BuildInstallEndDate N 安装有效期结束时间,字符串型,如:2018-12-31 31 | * BuildChannelShortcut N 所需更新的指定渠道的下载短链接,只可指定一个渠道,字符串型,如:abcd 32 | * 33 | * 34 | * 返回结果 35 | * 36 | * 返回 Response 对象, 主要返回 API 调用的结果, 示例如下: 37 | * 38 | * { 39 | * code: 0, 40 | * message: '', 41 | * data: { 42 | * buildKey: 'xxx', 43 | * buildType: '1', 44 | * buildIsFirst: '0', 45 | * buildIsLastest: '1', 46 | * buildFileKey: 'xxx.ipa', 47 | * buildFileName: '', 48 | * buildFileSize: '40095060', 49 | * buildName: 'xxx', 50 | * buildVersion: '2.2.0', 51 | * buildVersionNo: '1.0.1', 52 | * buildBuildVersion: '9', 53 | * buildIdentifier: 'xxx.xxx.xxx', 54 | * buildIcon: 'xxx', 55 | * buildDescription: '', 56 | * buildUpdateDescription: '', 57 | * buildScreenshots: '', 58 | * buildShortcutUrl: 'xxxx', 59 | * buildCreated: 'xxxx-xx-xx xx:xx:xx', 60 | * buildUpdated: 'xxxx-xx-xx xx:xx:xx', 61 | * buildQRCodeURL: 'https://www.pgyer.com/app/qrcodeHistory/xxxx' 62 | * } 63 | * } 64 | * 65 | */ 66 | 67 | 68 | using System.Text.Json; 69 | 70 | 71 | /** 72 | * 此 Demo 用演示如何使用 PGYER API 上传 App 73 | * 详细文档参照 https://www.pgyer.com/doc/view/api#fastUploadApp 74 | * 适用于 c# 项目 75 | */ 76 | class Demo 77 | { 78 | 79 | public static void Main(string[] args) 80 | { 81 | PGYERAppUploader uploader = new PGYERAppUploader("your api key"); 82 | // enable debug info 83 | uploader.WithDebug(); 84 | UploadOption option = new UploadOption 85 | { 86 | FilePath = "./your-app.apk", 87 | }; 88 | Response response = uploader.Upload(option); 89 | if (response != null) 90 | { 91 | Console.WriteLine(JsonSerializer.Serialize(response)); 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /csharp-demo/PGYERAppUploader.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | using System.Text; 4 | using System.Text.Json; 5 | 6 | class PGYERAppUploader 7 | { 8 | private readonly string _apikey; 9 | private readonly string _baseurl; 10 | private bool _debug; 11 | private readonly string[] _suffixs = [".ipa", ".apk"]; 12 | public PGYERAppUploader(string apikey, string baseurl = "https://api.pgyer.com") 13 | { 14 | this._apikey = apikey; 15 | this._baseurl = baseurl; 16 | } 17 | 18 | public void WithDebug() 19 | { 20 | _debug = true; 21 | } 22 | 23 | private void Record(string message) 24 | { 25 | if (this._debug) 26 | { 27 | Console.WriteLine(string.Format("{0: yyyy-MM-dd HH:mm:ss.3f} {1}", DateTime.Now, message)); 28 | } 29 | } 30 | 31 | public Response Upload(UploadOption option) 32 | { 33 | FileInfo file = new FileInfo(@$"{option.FilePath}"); 34 | if (!file.Exists) 35 | throw new Exception($"no such {option.FilePath} file."); 36 | if (!this._suffixs.Contains(file.Extension.ToLower())) 37 | throw new Exception("invalid file extension, only support .ipa or .apk extension"); 38 | // step 1 39 | // get costoken 40 | CosTokenRequest cosTokenRequest = new CosTokenRequest 41 | { 42 | ApiKey = this._apikey, 43 | BuildType = file.Extension.TrimStart('.'), 44 | Oversea = option.Oversea ?? "", 45 | BuildInstallType = option.BuildInstallType ?? "", 46 | BuildPassword = option.BuildPassword ?? "", 47 | BuildDescription = option.BuildDescription ?? "", 48 | BuildUpdateDescription = option.BuildUpdateDescription ?? "", 49 | BuildInstallDate = option.BuildInstallDate ?? "", 50 | BuildInstallStartDate = option.BuildInstallStartDate ?? "", 51 | BuildInstallEndDate = option.BuildInstallEndDate ?? "", 52 | BuildChannelShortcut = option.BuildChannelShortcut ?? "" 53 | }; 54 | Response cosTokenResponse = this.GetCosToken(cosTokenRequest); 55 | 56 | if (cosTokenResponse.Code != 0 || cosTokenResponse.Data == null || cosTokenResponse.Data.Param == null) 57 | throw new Exception($"Failed to get upload token: {cosTokenResponse.Message}"); 58 | // step 2 59 | // upload app to bucket 60 | UploadAppRequest uploadAppRequest = new UploadAppRequest 61 | { 62 | Key = cosTokenResponse.Data.Param.Key, 63 | Signature = cosTokenResponse.Data.Param.Signature, 64 | SecurityToken = cosTokenResponse.Data.Param.SecurityToken, 65 | Filename = file.Name, 66 | Endpoint = cosTokenResponse.Data.Endpoint, 67 | File = file 68 | }; 69 | HttpResponseMessage uploadAppResponse = this.UploadApp(uploadAppRequest); 70 | if (!uploadAppResponse.StatusCode.Equals(HttpStatusCode.NoContent)) 71 | throw new Exception($"Failed upload app to bucket: {uploadAppResponse.Content.ReadAsStringAsync().Result}"); 72 | this.Record("upload app to bucket successful."); 73 | // step 3 74 | // BuildInfo 75 | BuildInfoRequest buildInfoRequest = new BuildInfoRequest 76 | { 77 | ApiKey = this._apikey, 78 | BuildKey = cosTokenResponse.Data.Param.Key 79 | }; 80 | for (var time = 1; time <= 60; time++) 81 | { 82 | this.Record($"[{time}] get app build info..."); 83 | Response buildInfoResponse = this.BuildInfo(buildInfoRequest); 84 | if (buildInfoResponse.Code != 0 || buildInfoResponse.Data == null) 85 | { 86 | System.Threading.Thread.Sleep(1000); 87 | continue; 88 | } 89 | return buildInfoResponse; 90 | } 91 | return null; 92 | } 93 | 94 | public Response GetCosToken(CosTokenRequest request) 95 | { 96 | 97 | if (!request.Validate()) 98 | { 99 | throw new Exception("CosTokenRequest invalid"); 100 | } 101 | 102 | FormUrlEncodedContent content = new FormUrlEncodedContent(request.Serialize()); 103 | 104 | this.Record($"get upload token with params: {content.ReadAsStringAsync().Result}"); 105 | 106 | HttpResponseMessage response = this.post("/apiv2/app/getCOSToken", content); 107 | 108 | if (!response.IsSuccessStatusCode) 109 | throw new Exception($"POST /apiv2/app/getCOSToken {response.StatusCode} failed"); 110 | 111 | this.Record($"get upload token with response: {response.Content.ReadAsStringAsync().Result}"); 112 | 113 | return JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); 114 | } 115 | 116 | public HttpResponseMessage UploadApp(UploadAppRequest request) 117 | { 118 | if (!request.Validate()) 119 | throw new Exception("UploadAppRequest invalid"); 120 | string record = ""; 121 | string boundary = string.Format("---------------------{0}", DateTime.Now.Ticks.ToString("x")); 122 | using (MultipartFormDataContent multipart = new MultipartFormDataContent(boundary)) 123 | { 124 | multipart.Headers.ContentType = MediaTypeHeaderValue.Parse($"multipart/form-data; boundary={boundary}"); 125 | // data field 126 | foreach (var param in request.Serialize()) 127 | { 128 | var content = new ByteArrayContent(Encoding.UTF8.GetBytes(param.Value)); 129 | content.Headers.TryAddWithoutValidation("Content-Disposition", $"form-data; name=\"{param.Key}\""); 130 | multipart.Add(content); 131 | record += $"{param.Key}={param.Value}&"; 132 | } 133 | // file field 134 | var fileContent = new ByteArrayContent(File.ReadAllBytes(@$"{request.File.FullName}")); 135 | fileContent.Headers.TryAddWithoutValidation("Content-Disposition", $"form-data; name=\"file\"; filename=\"{request.File.Name}\""); 136 | multipart.Add(fileContent); 137 | this.Record($"upload app to bucket with params: {record}file={request.File.FullName}"); 138 | return this.post(request.Endpoint, multipart); 139 | } 140 | } 141 | 142 | public Response BuildInfo(BuildInfoRequest request) 143 | { 144 | if (!request.Validate()) 145 | throw new Exception("BuildInfoRequest invalid"); 146 | 147 | FormUrlEncodedContent content = new FormUrlEncodedContent(request.Serialize()); 148 | 149 | this.Record($"get build info from: {this._baseurl}/apiv2/app/buildInfo?{content.ReadAsStringAsync().Result}"); 150 | 151 | HttpResponseMessage response = this.get($"/apiv2/app/buildInfo?{content.ReadAsStringAsync().Result}"); 152 | 153 | if (!response.IsSuccessStatusCode) 154 | throw new Exception($"GET /apiv2/app/buildInfo {response.StatusCode} failed."); 155 | 156 | return JsonSerializer.Deserialize>(response.Content.ReadAsStringAsync().Result); 157 | } 158 | 159 | private HttpResponseMessage post(string url, HttpContent content, Dictionary headers = null, int timeout = 30) 160 | { 161 | using (var client = new HttpClient()) 162 | { 163 | client.BaseAddress = new Uri(this._baseurl); 164 | client.Timeout = new TimeSpan(0, 0, timeout); 165 | 166 | if (headers != null) 167 | { 168 | foreach (var header in headers) 169 | client.DefaultRequestHeaders.Add(header.Key, header.Value); 170 | } 171 | return client.PostAsync(url, content).Result; 172 | } 173 | } 174 | 175 | private HttpResponseMessage get(string url, Dictionary headers = null, int timeout = 30) 176 | { 177 | using (var client = new HttpClient()) 178 | { 179 | client.Timeout = new TimeSpan(0, 0, timeout); 180 | client.BaseAddress = new Uri(this._baseurl); 181 | if (headers != null) 182 | { 183 | foreach (var header in headers) 184 | client.DefaultRequestHeaders.Add(header.Key, header.Value); 185 | } 186 | return client.GetAsync(url).Result; 187 | } 188 | 189 | } 190 | } -------------------------------------------------------------------------------- /csharp-demo/Request/BuildInfoRequest.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | class BuildInfoRequest 4 | { 5 | 6 | [FormProperty("_api_key", true)] 7 | public string ApiKey { get; set; } 8 | [FormProperty("buildKey", true)] 9 | public string BuildKey { get; set; } 10 | } -------------------------------------------------------------------------------- /csharp-demo/Request/FormProperty.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [AttributeUsage(AttributeTargets.Property)] 5 | public class FormProperty : Request 6 | { 7 | 8 | private string _name { set; get; } 9 | 10 | private bool _required { set; get; } 11 | 12 | public FormProperty(string name, bool required) 13 | { 14 | this._name = name; 15 | this._required = required; 16 | } 17 | 18 | public override bool Validate(object value) 19 | { 20 | if (this._required) 21 | { 22 | return value != null && !string.IsNullOrWhiteSpace(value.ToString()); 23 | } 24 | return true; 25 | } 26 | public override Dictionary Serialize(object value) 27 | { 28 | Dictionary param = new Dictionary { }; 29 | if (value != null && !string.IsNullOrWhiteSpace(value.ToString())) 30 | { 31 | 32 | param.Add(this._name, value.ToString() ?? ""); 33 | } 34 | return param; 35 | } 36 | } -------------------------------------------------------------------------------- /csharp-demo/Request/GetCosTokenRequest.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | class CosTokenRequest 5 | { 6 | 7 | [FormProperty("_api_key", true)] 8 | public string ApiKey { get; set; } 9 | 10 | [FormProperty("buildType", true)] 11 | public string BuildType { get; set; } 12 | 13 | [FormProperty("oversea", false)] 14 | public string Oversea { get; set; } 15 | [FormProperty("buildInstallType", false)] 16 | public string BuildInstallType { get; set; } 17 | [FormProperty("buildPassword", false)] 18 | public string BuildPassword { get; set; } 19 | [FormProperty("buildDescription", false)] 20 | public string BuildDescription { get; set; } 21 | [FormProperty("buildUpdateDescription", false)] 22 | public string BuildUpdateDescription { get; set; } 23 | 24 | [FormProperty("buildInstallDate", false)] 25 | public string BuildInstallDate { get; set; } 26 | [FormProperty("buildInstallStartDate", false)] 27 | public string BuildInstallStartDate { get; set; } 28 | [FormProperty("buildInstallEndDate", false)] 29 | public string BuildInstallEndDate { get; set; } 30 | [FormProperty("buildChannelShortcut", false)] 31 | public string BuildChannelShortcut { get; set; } 32 | 33 | } -------------------------------------------------------------------------------- /csharp-demo/Request/ParameterizeBuilder.cs: -------------------------------------------------------------------------------- 1 | public static class ParameterizeBuilder 2 | { 3 | 4 | public static Dictionary Serialize(this T entity) where T : class 5 | { 6 | 7 | Dictionary parameters = new Dictionary { }; 8 | 9 | Type type = entity.GetType(); 10 | 11 | foreach (var property in type.GetProperties()) 12 | { 13 | if (property.IsDefined(typeof(Request), true)) 14 | { 15 | foreach (Request attribute in property.GetCustomAttributes(typeof(Request), true)) 16 | { 17 | if (attribute == null) 18 | { 19 | throw new Exception("Paramter not instantiate"); 20 | } 21 | var inner = attribute.Serialize(property.GetValue(entity) ?? ""); 22 | parameters = merge(parameters, inner); 23 | } 24 | } 25 | } 26 | 27 | return parameters; 28 | } 29 | 30 | private static Dictionary merge(Dictionary d1, Dictionary d2) 31 | { 32 | foreach (var item in d2) 33 | { 34 | d1.Add(item.Key, item.Value); 35 | } 36 | return d1; 37 | } 38 | } -------------------------------------------------------------------------------- /csharp-demo/Request/Request.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | public abstract class Request : Attribute 4 | { 5 | public abstract bool Validate(object value); 6 | public abstract Dictionary Serialize(object value); 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /csharp-demo/Request/UploadAppRequest.cs: -------------------------------------------------------------------------------- 1 | 2 | class UploadAppRequest 3 | { 4 | 5 | [FormProperty("key", true)] 6 | public string Key { get; set; } 7 | [FormProperty("signature", true)] 8 | public string Signature { get; set; } 9 | [FormProperty("x-cos-security-token", true)] 10 | public string SecurityToken { get; set; } 11 | [FormProperty("x-cos-meta-file-name", false)] 12 | public string Filename { get; set; } 13 | public required string Endpoint { get; set; } 14 | public required FileInfo File {get; set;} 15 | } -------------------------------------------------------------------------------- /csharp-demo/Request/UploadOption.cs: -------------------------------------------------------------------------------- 1 | 2 | class UploadOption 3 | { 4 | public required string FilePath { get; set; } 5 | 6 | public string Oversea { get; set; } 7 | public string BuildInstallType { get; set; } 8 | public string BuildPassword { get; set; } 9 | 10 | public string BuildDescription { get; set; } 11 | public string BuildUpdateDescription { get; set; } 12 | public string BuildInstallDate { get; set; } 13 | public string BuildInstallStartDate { get; set; } 14 | public string BuildInstallEndDate { get; set; } 15 | public string BuildChannelShortcut { get; set; } 16 | } -------------------------------------------------------------------------------- /csharp-demo/Request/Validator.cs: -------------------------------------------------------------------------------- 1 | 2 | public static class Validator 3 | { 4 | 5 | public static bool Validate(this T entity) where T : class 6 | { 7 | 8 | Type type = entity.GetType(); 9 | 10 | foreach (var item in type.GetProperties()) 11 | { 12 | if (item.IsDefined(typeof(Request), true)) 13 | { 14 | foreach (Request attribute in item.GetCustomAttributes(typeof(Request), true)) 15 | { 16 | if (attribute == null) 17 | { 18 | throw new Exception("Validator not instantiate"); 19 | } 20 | if (!attribute.Validate(item.GetValue(entity) ?? "")) 21 | { 22 | return false; 23 | } 24 | } 25 | } 26 | } 27 | return true; 28 | } 29 | } -------------------------------------------------------------------------------- /csharp-demo/Response/BuildInfoResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | public class BuildInfoResponse 4 | { 5 | [JsonPropertyName("buildKey")] 6 | public string BuildKey { get; set; } 7 | [JsonPropertyName("buildType")] 8 | public string BuildType { get; set; } 9 | [JsonPropertyName("buildIsFirst")] 10 | public string BuildIsFirst { get; set; } 11 | [JsonPropertyName("buildIsLastest")] 12 | public string BuildIsLastest { get; set; } 13 | [JsonPropertyName("buildFileSize")] 14 | public string BuildFileSize { get; set; } 15 | [JsonPropertyName("buildName")] 16 | public string BuildName { get; set; } 17 | [JsonPropertyName("buildVersion")] 18 | public string BuildVersion { get; set; } 19 | [JsonPropertyName("buildVersionNo")] 20 | public string BuildVersionNo { get; set; } 21 | [JsonPropertyName("buildBuildVersion")] 22 | public string BuildBuildVersion { get; set; } 23 | [JsonPropertyName("buildIdentifier")] 24 | public string BuildIdentifier { get; set; } 25 | [JsonPropertyName("buildIcon")] 26 | public string BuildIcon { get; set; } 27 | [JsonPropertyName("buildDescription")] 28 | public string BuildDescription { get; set; } 29 | [JsonPropertyName("buildUpdateDescription")] 30 | public string BuildUpdateDescription { get; set; } 31 | [JsonPropertyName("buildScreenShots")] 32 | public string BuildScreenShots { get; set; } 33 | [JsonPropertyName("buildShortcutUrl")] 34 | public string BuildShortcutUrl { get; set; } 35 | [JsonPropertyName("buildQRCodeURL")] 36 | public string BuildQRCodeURL { get; set; } 37 | [JsonPropertyName("buildCreated")] 38 | public string BuildCreated { get; set; } 39 | [JsonPropertyName("buildUpdated")] 40 | public string BuildUpdated { get; set; } 41 | } -------------------------------------------------------------------------------- /csharp-demo/Response/GetCosTokenResponse.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | 6 | public class CosTokenParam{ 7 | [JsonPropertyName("signature")] 8 | public string Signature {get; set;} 9 | [JsonPropertyName("x-cos-security-token")] 10 | public string SecurityToken {get; set;} 11 | [JsonPropertyName("key")] 12 | public string Key {get; set;} 13 | } 14 | 15 | public class CosTokenResponse{ 16 | 17 | [JsonPropertyName("key")] 18 | public string Key {get; set;} 19 | 20 | [JsonPropertyName("endpoint")] 21 | public string Endpoint{get; set;} 22 | 23 | [JsonPropertyName("params")] 24 | public CosTokenParam Param {get; set;} 25 | } -------------------------------------------------------------------------------- /csharp-demo/Response/Response.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | class Response 6 | { 7 | [JsonPropertyName("code")] 8 | public int Code { get; set; } 9 | [JsonPropertyName("message")] 10 | public string Message { get; set; } 11 | [JsonPropertyName("data")] 12 | public T Data { get; set; } 13 | } -------------------------------------------------------------------------------- /csharp-demo/csharp-demo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | csharp_demo 7 | enable 8 | disable 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /csharp-demo/csharp-demo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "csharp-demo", "csharp-demo.csproj", "{2409FBE8-1F5F-4757-8B48-ACF47A51C53C}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {2409FBE8-1F5F-4757-8B48-ACF47A51C53C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {2409FBE8-1F5F-4757-8B48-ACF47A51C53C}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {2409FBE8-1F5F-4757-8B48-ACF47A51C53C}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {2409FBE8-1F5F-4757-8B48-ACF47A51C53C}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {D0D5EBF9-5986-4A57-A85F-E8B9B9562451} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /java-demo/AppUploadDemo.java: -------------------------------------------------------------------------------- 1 | package com.pgyer.app_upload_demo; 2 | 3 | import org.json.JSONObject; 4 | 5 | import java.io.File; 6 | import java.io.FilterOutputStream; 7 | import java.io.IOException; 8 | import java.io.OutputStream; 9 | import java.nio.charset.Charset; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.Timer; 13 | 14 | /** 15 | * @describe 16 | * @author: Caoxy 17 | * @date: 2022/8/19 18 | */ 19 | public class AppUploadDemo implements AppUploadDemo.ProgressHttpEntityWrapper.ProgressCallback{ 20 | public static final String API_KEY_DEV = "";//测试环境apikey 21 | public static final String API_KEY_PRODUCTION = "";//线上环境apikey 22 | public static final String APP_PATH = "";//文件地址 23 | public static final String GET_TOKEN_URL = "https://api.pgyer.com/apiv2/app/getCOSToken"; 24 | public static final int FILE_UPLOAD_SUCCESSFUL = 1001; 25 | 26 | private UploadFileToServiceCallback uploadFileToServiceCallback; 27 | Timer timers = null; 28 | 29 | 30 | //接口方法调用例子开始 31 | /** 32 | * 例子 33 | */ 34 | public void uploadApk(){ 35 | Map params = new HashMap<>(); 36 | params.put("_api_key", API_KEY_DEV); 37 | int buildInstallType = 1; 38 | params.put("buildInstallType", buildInstallType+"");//buildInstallType 1,2,3,默认为1 公开安装 1:公开安装,2:密码安装,3:邀请安装 39 | if (buildInstallType == 2) { 40 | params.put("buildPassword", "");//需要安装密码 41 | } 42 | params.put("buildUpdateDescription", "");//版本更新日志 43 | 44 | File uploadFile = new File(APP_PATH);//apk文件路径 45 | params.put("buildType", "android"); 46 | String url = GET_TOKEN_URL;// 47 | getToken(params, url, new HttpCallback() { 48 | @java.lang.Override 49 | public void onSuccess(int code, String data) { 50 | JSONObject backData = new JSONObject(responseString); 51 | int code = backData.getInt("code"); 52 | JSONObject jsonObject = backData.getJSONObject("data"); 53 | if(code == 0 && jsonObject != null){//获取成功 54 | JSONObject jsonObjectparams = jsonObject.getJSONObject("params"); 55 | Map params = new HashMap<>(); 56 | String key = jsonObjectparams.getString("key"); 57 | params.put("key",key); 58 | params.put("signature",jsonObjectparams.getString("signature")); 59 | params.put("x-cos-security-token",jsonObjectparams.getString("x-cos-security-token")); 60 | String url = jsonObject.getString("endpoint"); 61 | uploadFilr(params, url, uploadFile, API_KEY_DEV, key); 62 | } else {//获取失败 63 | 64 | } 65 | } 66 | 67 | @java.lang.Override 68 | public void onError(int code, String data) { 69 | 70 | } 71 | }) 72 | } 73 | /** 74 | * 例子 上传文件 75 | * @param url 76 | */ 77 | public void uploadFilr(Map params, String url ,File files, String apikey, String buildKey){ 78 | uploadFileToServer(params, url, files, new UploadFileToServiceCallback() { 79 | @java.lang.Override 80 | public void onUploadBack(int code, String msg) { 81 | if(code == FILE_UPLOAD_SUCCESSFUL){ 82 | //数据同步需要时间延时5秒请求同步结果 83 | timers = new Timer(5000, new ActionListener() { 84 | @Override 85 | public void actionPerformed(ActionEvent e) { 86 | if(timers != null){ 87 | timers.stop(); 88 | timers = null; 89 | String url = "https://api.pgyer.com/apiv2/app/buildInfo?_api_key="+apikey+"&buildKey="+buildKey; 90 | uploadResult(url,uploadFileToServiceCallback); 91 | } 92 | } 93 | }); 94 | timers.start(); 95 | } 96 | } 97 | 98 | @java.lang.Override 99 | public void onPackageSizeComputed(long param1Long) {//上传文件的大小 去做度更新UI 100 | 101 | } 102 | 103 | @java.lang.Override 104 | public void onProgressChanged(float param1Long) {//上传文件的进 去做度更新UI 105 | 106 | } 107 | 108 | @java.lang.Override 109 | public void onUploadError(int code, String error) {//文件上传失败返回 110 | 111 | } 112 | }); 113 | } 114 | 115 | /** 116 | * 例子 获取同步结果 117 | * @param url 118 | */ 119 | public void dataSynchronous(String url){ 120 | uploadResult(url, new HttpCallback() { 121 | @java.lang.Override 122 | public void onSuccess(int code, String data) { 123 | JSONObject backDatas = new JSONObject(responseString); 124 | int code = backDatas.getInt("code"); 125 | if(code == 0){//上传成功 126 | backData = responseString; 127 | JSONObject data = backDatas.getJSONObject("data"); 128 | //返回成功后相关文件信息 129 | if (uploadFileToServiceCallback != null) { 130 | uploadFileToServiceCallback.onUploadBack(code,responseString); 131 | } 132 | } else if(code == 1246){//等待同步 133 | //数据同步需要时间延时已经延时5秒还在同步中,再2秒后请求同步结果 134 | timers = new Timer(2000, new ActionListener() { 135 | @Override 136 | public void actionPerformed(ActionEvent e) { 137 | if(timers != null){ 138 | timers.stop(); 139 | timers = null; 140 | dataSynchronous(url); 141 | } 142 | } 143 | }); 144 | timers.start(); 145 | } else {//上传失败 146 | if (uploadFileToServiceCallback != null) { 147 | uploadFileToServiceCallback.onUploadError(code,"上传失败"); 148 | } 149 | } 150 | } 151 | 152 | @java.lang.Override 153 | public void onError(int code, String data) { 154 | 155 | } 156 | }); 157 | } 158 | //接口方法调用例子结束 159 | 160 | /** 161 | * 获取文件相关参数 162 | * @param params 163 | * @param url 164 | * @param httpCallback 165 | */ 166 | public void getToken(Map params, String url, HttpCallback httpCallback){ 167 | sendRequest("POST",url, params ,httpCallback); 168 | } 169 | 170 | /** 171 | * 上传文件 172 | * @param params 173 | * @param url 174 | * @param files 175 | * @param apikey 176 | * @param buildKey 177 | * @param uploadFileToServiceCallback 178 | */ 179 | public void uploadFileToServer(final Map params, String url ,final File files, UploadFileToServiceCallback uploadFileToServiceCallback) { 180 | this.uploadFileToServiceCallback = uploadFileToServiceCallback; 181 | // TODO Auto-generated method stub 182 | new Thread(new Runnable() { 183 | @Override 184 | public void run() { 185 | // TODO Auto-generated method stub 186 | try { 187 | uploadFiles(params, url, files, uploadFileToServiceCallback); 188 | } catch (Exception e) { 189 | // TODO Auto-generated catch block 190 | e.printStackTrace(); 191 | uploadFileToServiceCallback.onUploadError(-1,e.getMessage()); 192 | } 193 | } 194 | }).start(); 195 | } 196 | 197 | /** 198 | * 上传文件实现 199 | * @param params 200 | * @param url 201 | * @param files 202 | * @param uploadFileToServiceCallback 203 | */ 204 | public void uploadFiles(final Map params, String url ,final File files, UploadFileToServiceCallback uploadFileToServiceCallback,) { 205 | // TODO Auto-generated method stub 206 | HttpClient client = HttpClientBuilder.create().build();// 开启一个客户端 HTTP 请求 207 | HttpPost post = new HttpPost(url);//创建 HTTP POST 请求 208 | MultipartEntityBuilder builder = MultipartEntityBuilder.create(); 209 | builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);//设置浏览器兼容模式 210 | for(Map.Entry entry:params.entrySet()){ 211 | builder.addTextBody(entry.getKey(), entry.getValue());//设置请求参数 212 | } 213 | builder.addBinaryBody("file", files); 214 | if (this.uploadFileToServiceCallback != null) 215 | this.uploadFileToServiceCallback.onPackageSizeComputed(100); 216 | HttpEntity entity = builder.build();// 生成 HTTP POST 实体 217 | post.setEntity(new ProgressHttpEntityWrapper(entity,this));//设置请求参数 218 | HttpResponse response = client.execute(post);// 发起请求 并返回请求的响应 219 | int httpCode = response.getStatusLine().getStatusCode(); 220 | if (httpCode == 204) { 221 | if(uploadFileToServiceCallback != null) { 222 | uploadFileToServiceCallback.onUploadBack(FILE_UPLOAD_SUCCESSFUL, "文件上传成功等待同步数据"); 223 | } 224 | } else { 225 | if(uploadFileToServiceCallback != null){ 226 | uploadFileToServiceCallback.onUploadError(httpCode,"上传失败!"); 227 | } 228 | } 229 | 230 | } 231 | 232 | /** 233 | * 获取上传文件后同步信息 234 | * @param url 235 | */ 236 | public void uploadResult(String url,HttpCallback httpCallback){ 237 | sendRequest("GET",url, null ,httpCallback); 238 | } 239 | 240 | /** 241 | * 发起http 请求 242 | * @param httpModle 243 | * @param url 244 | * @param params 245 | * @param httpCallback 246 | */ 247 | public void sendRequest(String httpModle, String url, Map params, HttpCallback httpCallback){ 248 | new Thread(new Runnable() { 249 | @Override 250 | public void run() { 251 | // TODO Auto-generated method stub 252 | try { 253 | HttpClient client = HttpClientBuilder.create().build();// 开启一个客户端 HTTP 请求 254 | if(httpModle.equals("GET")){ 255 | HttpGet httpClient = new HttpGet(url);//创建 HTTP POST 请求 256 | } else if(httpModle.equals("POST")){ 257 | HttpPost httpClient = new HttpPost(url);//创建 HTTP POST 请求 258 | MultipartEntityBuilder builder = MultipartEntityBuilder.create(); 259 | builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);//设置浏览器兼容模式 260 | for(Map.Entry entry:params.entrySet()){ 261 | builder.addTextBody(entry.getKey(), entry.getValue());//设置请求参数 262 | if(entry.getKey().equals("buildUpdateDescription")){ 263 | builder.addTextBody(entry.getKey(), entry.getValue(), ContentType.create(entry.getValue(), Charset.forName("UTF-8"))); 264 | } 265 | } 266 | HttpEntity entity = builder.build();// 生成 HTTP POST 实体 267 | httpClient.setEntity(new HttpEntityWrapper(entity));//设置请求参数 268 | } 269 | HttpResponse response = client.execute(httpClient);// 发起请求 并返回请求的响应 270 | if (response.getStatusLine().getStatusCode() == 200) { 271 | String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); 272 | JSONObject backDatas = new JSONObject(responseString); 273 | int code = backDatas.getInt("code"); 274 | if(httpCallback != null){ 275 | httpCallback.onSuccess(code,responseString); 276 | } 277 | } 278 | } catch (Exception e) { 279 | // TODO Auto-generated catch block 280 | e.printStackTrace(); 281 | httpCallback.onError(-1,e.getMessage()); 282 | } 283 | } 284 | }).start(); 285 | } 286 | 287 | @Override 288 | public void progress(float progress) { 289 | uploadFileToServiceCallback.onProgressChanged(progress); 290 | } 291 | 292 | /** 293 | * 上传文件封装监听 294 | */ 295 | public class ProgressHttpEntityWrapper extends HttpEntityWrapper { 296 | 297 | private final ProgressCallback progressCallback; 298 | 299 | public static interface ProgressCallback { 300 | public void progress(float progress); 301 | } 302 | 303 | public ProgressHttpEntityWrapper(final HttpEntity entity, final ProgressCallback progressCallback) { 304 | super(entity); 305 | this.progressCallback = progressCallback; 306 | } 307 | 308 | @Override 309 | public void writeTo(final OutputStream out) throws IOException { 310 | this.wrappedEntity.writeTo(out instanceof ProgressFilterOutputStream ? out : new ProgressFilterOutputStream(out, this.progressCallback, getContentLength())); 311 | } 312 | 313 | public static class ProgressFilterOutputStream extends FilterOutputStream { 314 | 315 | private final ProgressCallback progressCallback; 316 | private long transferred; 317 | private long totalBytes; 318 | 319 | ProgressFilterOutputStream(final OutputStream out, final ProgressCallback progressCallback, final long totalBytes) { 320 | super(out); 321 | this.progressCallback = progressCallback; 322 | this.transferred = 0; 323 | this.totalBytes = totalBytes; 324 | } 325 | 326 | @Override 327 | public void write(final byte[] b, final int off, final int len) throws IOException { 328 | //super.write(byte b[], int off, int len) calls write(int b) 329 | out.write(b, off, len); 330 | this.transferred += len; 331 | this.progressCallback.progress(getCurrentProgress()); 332 | } 333 | 334 | @Override 335 | public void write(final int b) throws IOException { 336 | out.write(b); 337 | this.transferred++; 338 | this.progressCallback.progress(getCurrentProgress()); 339 | } 340 | 341 | private float getCurrentProgress() { 342 | return ((float) this.transferred / this.totalBytes) * 100; 343 | } 344 | 345 | } 346 | } 347 | 348 | /** 349 | * 上传文件监听回调 350 | */ 351 | public interface UploadFileToServiceCallback { 352 | //上传成功 或者 同步数据接口成功返回 353 | void onUploadBack(int code,String msg); 354 | //上传文件大小 355 | void onPackageSizeComputed(long param1Long); 356 | //上传文件进度 357 | void onProgressChanged(float param1Long); 358 | //上传失败返回 359 | void onUploadError(int code,String error); 360 | } 361 | 362 | /** 363 | * http 请求回调 364 | */ 365 | public interface HttpCallback{ 366 | void onSuccess(int code, String data){ 367 | 368 | } 369 | void onError(int code, String data){ 370 | 371 | } 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /nodejs-demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.ipa -------------------------------------------------------------------------------- /nodejs-demo/PGYERAppUploader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 此 Demo 用演示如何使用 PGYER API 上传 App 3 | * 详细文档参照 https://www.pgyer.com/doc/view/api#fastUploadApp 4 | * 适用于 nodejs 项目 5 | * 本代码需要 npm 包 form-data 支持 运行 npm install --save form-data 即可 6 | */ 7 | 8 | /* 9 | * 以下代码为示例代码,可以在生产环境中使用。使用方法如下: 10 | * 11 | * 先实例化上传器 12 | * 13 | * const uploader = new PGYERAppUploader(); 14 | * 15 | * 在上传器实例化以后, 通过调用 upload 方法即可完成 App 上传。 16 | * 17 | * upload 方法有两种调用方式 18 | * 19 | * 1. 回调方式调用 20 | * 21 | * uploader.upload(uploadOptions: Object, callbackFn(error: Error, result: Object): any): void 22 | * 23 | * 示例: 24 | * const uploader = new PGYERAppUploader('apikey'); 25 | * uploader.upload({ filePath: './app.ipa' }, function (error, data) { 26 | * // code here 27 | * }) 28 | * 29 | * 2. 使用 promise 方式调用 30 | * 31 | * uploader.upload(uploadOptions: Object): Promise 32 | * 33 | * 示例: 34 | * const uploader = new PGYERAppUploader('apikey'); 35 | * uploader.upload({ filePath: './app.ipa' }).then(function (data) { 36 | * // code here 37 | * }).catch(fucntion (error) { 38 | * // code here 39 | * }) 40 | * 41 | * uploadOptions 参数说明: (https://www.pgyer.com/doc/view/api#fastUploadApp) 42 | * 43 | * 对象成员名 是否必选 含义 44 | * filePath Y App 文件的路径,可以是相对路径 45 | * log N Bool 类型,是否打印 log 46 | * buildInstallType N 应用安装方式,值为(1,2,3,默认为1 公开安装)。1:公开安装,2:密码安装,3:邀请安装 47 | * buildPassword N 设置App安装密码,密码为空时默认公开安装 48 | * buildUpdateDescription N 版本更新描述,请传空字符串,或不传。 49 | * buildInstallDate N 是否设置安装有效期,值为:1 设置有效时间, 2 长期有效,如果不填写不修改上一次的设置 50 | * buildInstallStartDate N 安装有效期开始时间,字符串型,如:2018-01-01 51 | * buildInstallEndDate N 安装有效期结束时间,字符串型,如:2018-12-31 52 | * buildChannelShortcut N 所需更新的指定渠道的下载短链接,只可指定一个渠道,字符串型,如:abcd 53 | * 54 | * 55 | * 返回结果 56 | * 57 | * 返回结果是一个对象, 主要返回 API 调用的结果, 示例如下: 58 | * 59 | * { 60 | * code: 0, 61 | * message: '', 62 | * data: { 63 | * buildKey: 'xxx', 64 | * buildType: '1', 65 | * buildIsFirst: '0', 66 | * buildIsLastest: '1', 67 | * buildFileKey: 'xxx.ipa', 68 | * buildFileName: '', 69 | * buildFileSize: '40095060', 70 | * buildName: 'xxx', 71 | * buildVersion: '2.2.0', 72 | * buildVersionNo: '1.0.1', 73 | * buildBuildVersion: '9', 74 | * buildIdentifier: 'xxx.xxx.xxx', 75 | * buildIcon: 'xxx', 76 | * buildDescription: '', 77 | * buildUpdateDescription: '', 78 | * buildScreenshots: '', 79 | * buildShortcutUrl: 'xxxx', 80 | * buildCreated: 'xxxx-xx-xx xx:xx:xx', 81 | * buildUpdated: 'xxxx-xx-xx xx:xx:xx', 82 | * buildQRCodeURL: 'https://www.pgyer.com/app/qrcodeHistory/xxxx' 83 | * } 84 | * } 85 | * 86 | */ 87 | 88 | const https = require('https'); 89 | const fs = require('fs'); 90 | const querystring = require('querystring'); 91 | const FormData = require('form-data'); 92 | 93 | module.exports = function (apiKey) { 94 | const LOG_TAG = '[PGYER APP UPLOADER]'; 95 | let uploadOptions = ''; 96 | this.upload = function (options, callback) { 97 | if (options && typeof options.filePath === 'string') { 98 | uploadOptions = options; 99 | if (typeof callback === 'function') { 100 | uploadApp(callback); 101 | return null; 102 | } else { 103 | return new Promise(function(resolve, reject) { 104 | uploadApp(function (error, data) { 105 | if (error === null) { 106 | return resolve(data); 107 | } 108 | return reject(error); 109 | }); 110 | }); 111 | } 112 | } 113 | 114 | throw new Error('filePath must be a string'); 115 | } 116 | 117 | function uploadApp (callback) { 118 | // step 1: get app upload token 119 | const uploadTokenRequestData = querystring.stringify({ 120 | ...uploadOptions, 121 | _api_key: apiKey, 122 | buildType: uploadOptions.filePath.split('.').pop() 123 | }); 124 | 125 | uploadOptions.log && console.log(LOG_TAG + ' Check API Key ... Please Wait ...'); 126 | const uploadTokenRequest = https.request({ 127 | hostname: 'api.pgyer.com', 128 | path: '/apiv2/app/getCOSToken', 129 | method: 'POST', 130 | headers: { 131 | 'Content-Type' : 'application/x-www-form-urlencoded', 132 | 'Content-Length' : uploadTokenRequestData.length 133 | } 134 | }, response => { 135 | if (response.statusCode !== 200) { 136 | callback(new Error(LOG_TAG + 'Service down: cannot get upload token.'), null); 137 | return; 138 | } 139 | 140 | let responseData = ''; 141 | response.on('data', data => { 142 | responseData += data.toString(); 143 | }) 144 | 145 | response.on('end', () => { 146 | const responseText = responseData.toString(); 147 | try { 148 | const responseInfo = JSON.parse(responseText); 149 | if (responseInfo.code) { 150 | callback(new Error(LOG_TAG + 'Service down: ' + responseInfo.code + ': ' + responseInfo.message), null); 151 | return; 152 | } 153 | uploadApp(responseInfo); 154 | } catch (error) { 155 | callback(error, null); 156 | } 157 | }) 158 | }) 159 | 160 | uploadTokenRequest.write(uploadTokenRequestData); 161 | uploadTokenRequest.end(); 162 | 163 | 164 | // step 2: upload app to bucket 165 | function uploadApp(uploadData) { 166 | uploadOptions.log && console.log(LOG_TAG + ' Uploading app ... Please Wait ...'); 167 | const exsit = fs.existsSync(uploadOptions.filePath); 168 | if (!exsit) { 169 | callback(new Error(LOG_TAG + ' filePath: file not exist'), null); 170 | return; 171 | } 172 | 173 | const statResult = fs.statSync(uploadOptions.filePath); 174 | if (!statResult || !statResult.isFile()) { 175 | callback(new Error(LOG_TAG + ' filePath: path not a file'), null); 176 | return; 177 | } 178 | 179 | const uploadAppRequestData = new FormData(); 180 | uploadAppRequestData.append('signature', uploadData.data.params.signature); 181 | uploadAppRequestData.append('x-cos-security-token', uploadData.data.params['x-cos-security-token']); 182 | uploadAppRequestData.append('key', uploadData.data.params.key); 183 | uploadAppRequestData.append('x-cos-meta-file-name', uploadOptions.filePath.replace(/^.*[\\\/]/, '')); 184 | uploadAppRequestData.append('file', fs.createReadStream(uploadOptions.filePath)); 185 | 186 | uploadAppRequestData.submit(uploadData.data.endpoint, function (error, response) { 187 | if (error) { 188 | callback(error, null); 189 | return; 190 | } 191 | if (response.statusCode === 204) { 192 | setTimeout(() => getUploadResult(uploadData), 1000); 193 | } else { 194 | callback(new Error(LOG_TAG + ' Upload Error!'), null); 195 | } 196 | }); 197 | } 198 | 199 | // step 3: get uploaded app data 200 | function getUploadResult (uploadData) { 201 | const uploadResultRequest = https.request({ 202 | hostname: 'api.pgyer.com', 203 | path: '/apiv2/app/buildInfo?_api_key=' + apiKey + '&buildKey=' + uploadData.data.key, 204 | method: 'POST', 205 | headers: { 206 | 'Content-Type' : 'application/x-www-form-urlencoded', 207 | 'Content-Length' : 0 208 | } 209 | }, response => { 210 | if (response.statusCode !== 200) { 211 | callback(new Error(LOG_TAG + ' Service is down.'), null); 212 | return; 213 | } 214 | 215 | let responseData = ''; 216 | response.on('data', data => { 217 | responseData += data.toString(); 218 | }) 219 | 220 | response.on('end', () => { 221 | const responseText = responseData.toString(); 222 | try { 223 | const responseInfo = JSON.parse(responseText); 224 | if (responseInfo.code === 1247) { 225 | uploadOptions.log && console.log(LOG_TAG + ' Parsing App Data ... Please Wait ...'); 226 | setTimeout(() => getUploadResult(uploadData), 1000); 227 | return; 228 | } else if (responseInfo.code) { 229 | callback(new Error(LOG_TAG + 'Service down: ' + responseInfo.code + ': ' + responseInfo.message), null); 230 | } 231 | callback(null, responseInfo); 232 | } catch (error) { 233 | callback(error, null); 234 | } 235 | }) 236 | 237 | }) 238 | 239 | uploadResultRequest.write(uploadTokenRequestData); 240 | uploadResultRequest.end(); 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /nodejs-demo/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 此 Demo 用演示如何使用 PGYER API 上传 App 3 | * 详细文档参照 https://www.pgyer.com/doc/view/api#fastUploadApp 4 | * 适用于 nodejs 项目 5 | * 本代码需要 npm 包 form-data 支持 运行 npm install --save form-data 即可 6 | */ 7 | 8 | /* 9 | * 以下代码为示例代码,可以在生产环境中使用。使用方法如下: 10 | * 11 | * 先实例化上传器 12 | * 13 | * const uploader = new PGYERAppUploader(); 14 | * 15 | * 在上传器实例化以后, 通过调用 upload 方法即可完成 App 上传。 16 | * 17 | * upload 方法有两种调用方式 18 | * 19 | * 1. 回调方式调用 20 | * 21 | * uploader.upload(uploadOptions: Object, callbackFn(error: Error, result: Object): any): void 22 | * 23 | * 示例: 24 | * const uploader = new PGYERAppUploader('apikey'); 25 | * uploader.upload({ filePath: './app.ipa' }, function (error, data) { 26 | * // code here 27 | * }) 28 | * 29 | * 2. 使用 promise 方式调用 30 | * 31 | * uploader.upload(uploadOptions: Object): Promise 32 | * 33 | * 示例: 34 | * const uploader = new PGYERAppUploader('apikey'); 35 | * uploader.upload({ filePath: './app.ipa' }).then(function (data) { 36 | * // code here 37 | * }).catch(fucntion (error) { 38 | * // code here 39 | * }) 40 | * 41 | * uploadOptions 参数说明: (https://www.pgyer.com/doc/view/api#fastUploadApp) 42 | * 43 | * 对象成员名 是否必选 含义 44 | * filePath Y App 文件的路径,可以是相对路径 45 | * log N Bool 类型,是否打印 log 46 | * buildInstallType N 应用安装方式,值为(1,2,3,默认为1 公开安装)。1:公开安装,2:密码安装,3:邀请安装 47 | * buildPassword N 设置App安装密码,密码为空时默认公开安装 48 | * buildUpdateDescription N 版本更新描述,请传空字符串,或不传。 49 | * buildInstallDate N 是否设置安装有效期,值为:1 设置有效时间, 2 长期有效,如果不填写不修改上一次的设置 50 | * buildInstallStartDate N 安装有效期开始时间,字符串型,如:2018-01-01 51 | * buildInstallEndDate N 安装有效期结束时间,字符串型,如:2018-12-31 52 | * buildChannelShortcut N 所需更新的指定渠道的下载短链接,只可指定一个渠道,字符串型,如:abcd 53 | * 54 | * 55 | * 返回结果 56 | * 57 | * 返回结果是一个对象, 主要返回 API 调用的结果, 示例如下: 58 | * 59 | * { 60 | * code: 0, 61 | * message: '', 62 | * data: { 63 | * buildKey: 'xxx', 64 | * buildType: '1', 65 | * buildIsFirst: '0', 66 | * buildIsLastest: '1', 67 | * buildFileKey: 'xxx.ipa', 68 | * buildFileName: '', 69 | * buildFileSize: '40095060', 70 | * buildName: 'xxx', 71 | * buildVersion: '2.2.0', 72 | * buildVersionNo: '1.0.1', 73 | * buildBuildVersion: '9', 74 | * buildIdentifier: 'xxx.xxx.xxx', 75 | * buildIcon: 'xxx', 76 | * buildDescription: '', 77 | * buildUpdateDescription: '', 78 | * buildScreenshots: '', 79 | * buildShortcutUrl: 'xxxx', 80 | * buildCreated: 'xxxx-xx-xx xx:xx:xx', 81 | * buildUpdated: 'xxxx-xx-xx xx:xx:xx', 82 | * buildQRCodeURL: 'https://www.pgyer.com/app/qrcodeHistory/xxxx' 83 | * } 84 | * } 85 | * 86 | */ 87 | 88 | const PGYERAppUploader = require('./PGYERAppUploader'); 89 | const API_KEY = ''; 90 | const APP_PATH = ''; 91 | 92 | const uploader = new PGYERAppUploader(API_KEY); 93 | 94 | const uploadOptions = { 95 | filePath: APP_PATH, // 上传文件路径 96 | log: true, // 显示 log 97 | buildInstallType: 2, // 安装方式: 2 为密码安装 98 | buildPassword: '123456' // 安装密码 99 | } 100 | 101 | // 调用方式 1: 使用回调函数调用 102 | uploader.upload(uploadOptions, function (error, result) { 103 | error ? console.error(error): console.log(result); 104 | }); 105 | 106 | // 调用方式 2: 使用 Promise 调用 107 | uploader.upload(uploadOptions).then(console.log).catch(console.error); 108 | -------------------------------------------------------------------------------- /nodejs-demo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-demo", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "form-data": "^4.0.2" 9 | } 10 | }, 11 | "node_modules/asynckit": { 12 | "version": "0.4.0", 13 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 14 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 15 | }, 16 | "node_modules/call-bind-apply-helpers": { 17 | "version": "1.0.2", 18 | "resolved": "https://mirrors.cloud.tencent.com/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 19 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 20 | "dependencies": { 21 | "es-errors": "^1.3.0", 22 | "function-bind": "^1.1.2" 23 | }, 24 | "engines": { 25 | "node": ">= 0.4" 26 | } 27 | }, 28 | "node_modules/combined-stream": { 29 | "version": "1.0.8", 30 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 31 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 32 | "dependencies": { 33 | "delayed-stream": "~1.0.0" 34 | }, 35 | "engines": { 36 | "node": ">= 0.8" 37 | } 38 | }, 39 | "node_modules/delayed-stream": { 40 | "version": "1.0.0", 41 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 42 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 43 | "engines": { 44 | "node": ">=0.4.0" 45 | } 46 | }, 47 | "node_modules/dunder-proto": { 48 | "version": "1.0.1", 49 | "resolved": "https://mirrors.cloud.tencent.com/npm/dunder-proto/-/dunder-proto-1.0.1.tgz", 50 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 51 | "dependencies": { 52 | "call-bind-apply-helpers": "^1.0.1", 53 | "es-errors": "^1.3.0", 54 | "gopd": "^1.2.0" 55 | }, 56 | "engines": { 57 | "node": ">= 0.4" 58 | } 59 | }, 60 | "node_modules/es-define-property": { 61 | "version": "1.0.1", 62 | "resolved": "https://mirrors.cloud.tencent.com/npm/es-define-property/-/es-define-property-1.0.1.tgz", 63 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 64 | "engines": { 65 | "node": ">= 0.4" 66 | } 67 | }, 68 | "node_modules/es-errors": { 69 | "version": "1.3.0", 70 | "resolved": "https://mirrors.cloud.tencent.com/npm/es-errors/-/es-errors-1.3.0.tgz", 71 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 72 | "engines": { 73 | "node": ">= 0.4" 74 | } 75 | }, 76 | "node_modules/es-object-atoms": { 77 | "version": "1.1.1", 78 | "resolved": "https://mirrors.cloud.tencent.com/npm/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 79 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 80 | "dependencies": { 81 | "es-errors": "^1.3.0" 82 | }, 83 | "engines": { 84 | "node": ">= 0.4" 85 | } 86 | }, 87 | "node_modules/es-set-tostringtag": { 88 | "version": "2.1.0", 89 | "resolved": "https://mirrors.cloud.tencent.com/npm/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 90 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 91 | "dependencies": { 92 | "es-errors": "^1.3.0", 93 | "get-intrinsic": "^1.2.6", 94 | "has-tostringtag": "^1.0.2", 95 | "hasown": "^2.0.2" 96 | }, 97 | "engines": { 98 | "node": ">= 0.4" 99 | } 100 | }, 101 | "node_modules/form-data": { 102 | "version": "4.0.2", 103 | "resolved": "https://mirrors.cloud.tencent.com/npm/form-data/-/form-data-4.0.2.tgz", 104 | "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", 105 | "dependencies": { 106 | "asynckit": "^0.4.0", 107 | "combined-stream": "^1.0.8", 108 | "es-set-tostringtag": "^2.1.0", 109 | "mime-types": "^2.1.12" 110 | }, 111 | "engines": { 112 | "node": ">= 6" 113 | } 114 | }, 115 | "node_modules/function-bind": { 116 | "version": "1.1.2", 117 | "resolved": "https://mirrors.cloud.tencent.com/npm/function-bind/-/function-bind-1.1.2.tgz", 118 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 119 | "funding": { 120 | "url": "https://github.com/sponsors/ljharb" 121 | } 122 | }, 123 | "node_modules/get-intrinsic": { 124 | "version": "1.3.0", 125 | "resolved": "https://mirrors.cloud.tencent.com/npm/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 126 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 127 | "dependencies": { 128 | "call-bind-apply-helpers": "^1.0.2", 129 | "es-define-property": "^1.0.1", 130 | "es-errors": "^1.3.0", 131 | "es-object-atoms": "^1.1.1", 132 | "function-bind": "^1.1.2", 133 | "get-proto": "^1.0.1", 134 | "gopd": "^1.2.0", 135 | "has-symbols": "^1.1.0", 136 | "hasown": "^2.0.2", 137 | "math-intrinsics": "^1.1.0" 138 | }, 139 | "engines": { 140 | "node": ">= 0.4" 141 | }, 142 | "funding": { 143 | "url": "https://github.com/sponsors/ljharb" 144 | } 145 | }, 146 | "node_modules/get-proto": { 147 | "version": "1.0.1", 148 | "resolved": "https://mirrors.cloud.tencent.com/npm/get-proto/-/get-proto-1.0.1.tgz", 149 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 150 | "dependencies": { 151 | "dunder-proto": "^1.0.1", 152 | "es-object-atoms": "^1.0.0" 153 | }, 154 | "engines": { 155 | "node": ">= 0.4" 156 | } 157 | }, 158 | "node_modules/gopd": { 159 | "version": "1.2.0", 160 | "resolved": "https://mirrors.cloud.tencent.com/npm/gopd/-/gopd-1.2.0.tgz", 161 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 162 | "engines": { 163 | "node": ">= 0.4" 164 | }, 165 | "funding": { 166 | "url": "https://github.com/sponsors/ljharb" 167 | } 168 | }, 169 | "node_modules/has-symbols": { 170 | "version": "1.1.0", 171 | "resolved": "https://mirrors.cloud.tencent.com/npm/has-symbols/-/has-symbols-1.1.0.tgz", 172 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 173 | "engines": { 174 | "node": ">= 0.4" 175 | }, 176 | "funding": { 177 | "url": "https://github.com/sponsors/ljharb" 178 | } 179 | }, 180 | "node_modules/has-tostringtag": { 181 | "version": "1.0.2", 182 | "resolved": "https://mirrors.cloud.tencent.com/npm/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 183 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 184 | "dependencies": { 185 | "has-symbols": "^1.0.3" 186 | }, 187 | "engines": { 188 | "node": ">= 0.4" 189 | }, 190 | "funding": { 191 | "url": "https://github.com/sponsors/ljharb" 192 | } 193 | }, 194 | "node_modules/hasown": { 195 | "version": "2.0.2", 196 | "resolved": "https://mirrors.cloud.tencent.com/npm/hasown/-/hasown-2.0.2.tgz", 197 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 198 | "dependencies": { 199 | "function-bind": "^1.1.2" 200 | }, 201 | "engines": { 202 | "node": ">= 0.4" 203 | } 204 | }, 205 | "node_modules/math-intrinsics": { 206 | "version": "1.1.0", 207 | "resolved": "https://mirrors.cloud.tencent.com/npm/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 208 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 209 | "engines": { 210 | "node": ">= 0.4" 211 | } 212 | }, 213 | "node_modules/mime-db": { 214 | "version": "1.52.0", 215 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 216 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 217 | "engines": { 218 | "node": ">= 0.6" 219 | } 220 | }, 221 | "node_modules/mime-types": { 222 | "version": "2.1.35", 223 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 224 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 225 | "dependencies": { 226 | "mime-db": "1.52.0" 227 | }, 228 | "engines": { 229 | "node": ">= 0.6" 230 | } 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /nodejs-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "form-data": "^4.0.2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /php-demo/PGYERAppUploader.php: -------------------------------------------------------------------------------- 1 | ); 14 | * 15 | * 在上传器实例化以后, 通过调用 upload 方法即可完成 App 上传。 16 | * 17 | * $uploader->upload($config); 18 | * 19 | * 示例: 20 | * $uploader = new PGYERAppUploader('apikey'); 21 | * $uploader->upload(['buildType' => 'ios', 'filePath' => './app.ipa']); 22 | 23 | * 24 | * uploadOptions 参数说明: (https://www.pgyer.com/doc/view/api#fastUploadApp) 25 | * 26 | * 对象成员名 是否必选 含义 27 | * filePath Y App 文件的路径,可以是相对路径 28 | * buildInstallType N 应用安装方式,值为(1,2,3,默认为1 公开安装)。1:公开安装,2:密码安装,3:邀请安装 29 | * buildPassword N 设置App安装密码,密码为空时默认公开安装 30 | * buildUpdateDescription N 版本更新描述,请传空字符串,或不传。 31 | * buildInstallDate N 是否设置安装有效期,值为:1 设置有效时间, 2 长期有效,如果不填写不修改上一次的设置 32 | * buildInstallStartDate N 安装有效期开始时间,字符串型,如:2018-01-01 33 | * buildInstallEndDate N 安装有效期结束时间,字符串型,如:2018-12-31 34 | * buildChannelShortcut N 所需更新的指定渠道的下载短链接,只可指定一个渠道,字符串型,如:abcd 35 | * 36 | * 37 | * 返回结果 38 | * 39 | * 返回结果是一个数组, 主要返回 API 调用的结果, 示例如下: 40 | * 41 | * { 42 | * code: 0, 43 | * message: '', 44 | * data: { 45 | * buildKey: 'xxx', 46 | * buildType: '1', 47 | * buildIsFirst: '0', 48 | * buildIsLastest: '1', 49 | * buildFileKey: 'xxx.ipa', 50 | * buildFileName: '', 51 | * buildFileSize: '40095060', 52 | * buildName: 'xxx', 53 | * buildVersion: '2.2.0', 54 | * buildVersionNo: '1.0.1', 55 | * buildBuildVersion: '9', 56 | * buildIdentifier: 'xxx.xxx.xxx', 57 | * buildIcon: 'xxx', 58 | * buildDescription: '', 59 | * buildUpdateDescription: '', 60 | * buildScreenshots: '', 61 | * buildShortcutUrl: 'xxxx', 62 | * buildCreated: 'xxxx-xx-xx xx:xx:xx', 63 | * buildUpdated: 'xxxx-xx-xx xx:xx:xx', 64 | * buildQRCodeURL: 'https://www.pgyer.com/app/qrcodeHistory/xxxx' 65 | * } 66 | * } 67 | * 68 | */ 69 | 70 | class PGYERAppUploader 71 | { 72 | 73 | private $apikey = ''; 74 | public $log = false; 75 | 76 | public function __construct($apikey) 77 | { 78 | $this->apikey = $apikey; 79 | } 80 | 81 | public function upload($config) 82 | { 83 | $filePath = $config['filePath']; 84 | 85 | // step 1: get app upload token 86 | $ext = pathinfo($filePath, PATHINFO_EXTENSION); 87 | if (!in_array($ext, ['ipa', 'apk'])) { 88 | throw new Exception('Invalid file type'); 89 | } 90 | 91 | $params = [ 92 | "_api_key" => $this->apikey, 93 | "buildType" => strtolower($ext) 94 | ]; 95 | 96 | $otherParams = ["buildInstallType", "buildPassword", "buildUpdateDescription", "buildInstallDate", "buildInstallStartDate", "buildInstallEndDate", "buildChannelShortcut"]; 97 | foreach ($otherParams as $key) { 98 | if (isset($config[$key])) { 99 | $params[$key] = $config[$key]; 100 | } 101 | } 102 | 103 | $this->log("get upload token with params: " . json_encode($params)); 104 | 105 | $res = $this->sendRequest("http://api.pgyer.com/apiv2/app/getCOSToken", $params); 106 | $this->log($res); 107 | $res = json_decode($res, true); 108 | 109 | if ($res['code'] != 0 || empty($res['data'])) { 110 | throw new Exception('Failed to get upload token: ' . $res['message']); 111 | } 112 | $key = $res['data']['key']; 113 | 114 | // step 2: upload app to bucket 115 | $params = $res['data']['params']; 116 | $params['x-cos-meta-file-name'] = pathinfo($filePath, PATHINFO_BASENAME); 117 | $params['file'] = new CURLFile($filePath); 118 | $this->log("upload app to bucket with params: " . json_encode($params)); 119 | $httpcode = 0; 120 | $result = $this->sendRequest($res['data']['endpoint'], $params, $httpcode); 121 | if ($httpcode == 204) { 122 | $this->log("upload success"); 123 | } else { 124 | $this->log("upload failed"); 125 | $this->log($result); 126 | throw new Exception('Failed to upload app'); 127 | } 128 | 129 | // step 3: get uploaded app data 130 | $url = "http://api.pgyer.com/apiv2/app/buildInfo?_api_key=" . $this->apikey . "&buildKey=$key"; 131 | $this->log("get build info from: " . $url); 132 | for ($i = 0; $i < 60; $i++) { 133 | $resp = $this->sendRequest($url); 134 | $res = json_decode($resp, true); 135 | if ($res['code'] != 0) { 136 | sleep(1); 137 | $this->log("[$i] get app build info..."); 138 | continue; 139 | } 140 | 141 | $this->log($resp); 142 | return $res['data']; 143 | } 144 | 145 | return false; 146 | } 147 | 148 | public function sendRequest($url, $params = [], &$httpcode = 0) 149 | { 150 | $ch = curl_init($url); 151 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 152 | 153 | if (!empty($params)) { 154 | curl_setopt($ch, CURLOPT_POST, true); 155 | curl_setopt($ch, CURLOPT_POSTFIELDS, $params); 156 | } 157 | 158 | $result = curl_exec($ch); 159 | $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 160 | 161 | curl_close($ch); 162 | return $result; 163 | } 164 | 165 | public function log($message) 166 | { 167 | if (!$this->log) { 168 | return; 169 | } 170 | 171 | if (is_array($message) || is_object($message)) { 172 | $message = var_export($message, true); 173 | } 174 | 175 | echo date("Y-m-d H:i:s") . " " . $message . "\n"; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /php-demo/demo.php: -------------------------------------------------------------------------------- 1 | ); 14 | * 15 | * 在上传器实例化以后, 通过调用 upload 方法即可完成 App 上传。 16 | * 17 | * $uploader->upload($config); 18 | * 19 | * 示例: 20 | * $uploader = new PGYERAppUploader('apikey'); 21 | * $uploader->upload(['buildType' => 'ios', 'filePath' => './app.ipa']); 22 | 23 | * 24 | * uploadOptions 参数说明: (https://www.pgyer.com/doc/view/api#fastUploadApp) 25 | * 26 | * 对象成员名 是否必选 含义 27 | * filePath Y App 文件的路径,可以是相对路径 28 | * buildInstallType N 应用安装方式,值为(1,2,3,默认为1 公开安装)。1:公开安装,2:密码安装,3:邀请安装 29 | * buildPassword N 设置App安装密码,密码为空时默认公开安装 30 | * buildUpdateDescription N 版本更新描述,请传空字符串,或不传。 31 | * buildInstallDate N 是否设置安装有效期,值为:1 设置有效时间, 2 长期有效,如果不填写不修改上一次的设置 32 | * buildInstallStartDate N 安装有效期开始时间,字符串型,如:2018-01-01 33 | * buildInstallEndDate N 安装有效期结束时间,字符串型,如:2018-12-31 34 | * buildChannelShortcut N 所需更新的指定渠道的下载短链接,只可指定一个渠道,字符串型,如:abcd 35 | * 36 | * 37 | * 返回结果 38 | * 39 | * 返回结果是一个数组, 主要返回 API 调用的结果, 示例如下: 40 | * 41 | * { 42 | * code: 0, 43 | * message: '', 44 | * data: { 45 | * buildKey: 'xxx', 46 | * buildType: '1', 47 | * buildIsFirst: '0', 48 | * buildIsLastest: '1', 49 | * buildFileKey: 'xxx.ipa', 50 | * buildFileName: '', 51 | * buildFileSize: '40095060', 52 | * buildName: 'xxx', 53 | * buildVersion: '2.2.0', 54 | * buildVersionNo: '1.0.1', 55 | * buildBuildVersion: '9', 56 | * buildIdentifier: 'xxx.xxx.xxx', 57 | * buildIcon: 'xxx', 58 | * buildDescription: '', 59 | * buildUpdateDescription: '', 60 | * buildScreenshots: '', 61 | * buildShortcutUrl: 'xxxx', 62 | * buildCreated: 'xxxx-xx-xx xx:xx:xx', 63 | * buildUpdated: 'xxxx-xx-xx xx:xx:xx', 64 | * buildQRCodeURL: 'https://www.pgyer.com/app/qrcodeHistory/xxxx' 65 | * } 66 | * } 67 | * 68 | */ 69 | 70 | require_once 'PGYERAppUploader.php'; 71 | 72 | try { 73 | $uploader = new PGYERAppUploader(''); 74 | $uploader->log = true; 75 | $info = $uploader->upload([ 76 | 'filePath' => '', 77 | 'buildInstallType' => 2, 78 | 'buildPassword' => '123456', 79 | 'buildUpdateDescription' => 'update by api', 80 | ]); 81 | 82 | print_r($info); 83 | } catch (Exception $e) { 84 | echo $e->getMessage(); 85 | } -------------------------------------------------------------------------------- /python-demo/README.md: -------------------------------------------------------------------------------- 1 | ## `Python` 脚本使用说明 2 | 3 | ## 环境 4 | 5 | 此为 `Python3` 脚本 6 | 7 | 使用前,请使用下方命令安装依赖 8 | 9 | ```shell 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | ## 使用 14 | 15 | 导入工具包 16 | 17 | ```python 18 | from utils import upload_pgyer as PgyerUtil 19 | ``` 20 | 21 | 调用 `upload_to_pgyer` 方法即可 22 | 23 | ```python 24 | # 上传完成回调 25 | def upload_complete_callback(isSuccess, result): 26 | if isSuccess: 27 | print('上传完成') 28 | else: 29 | print('上传失败') 30 | 31 | app_path = '' # App包路径 32 | pgyer_api_key = '' # API KEY 33 | pgyer_password = '' # 安装密码 34 | 35 | PgyerUtil.upload_to_pgyer( 36 | path = app_path, 37 | api_key = pgyer_api_key, 38 | password=pgyer_password, 39 | callback=upload_complete_callback 40 | ) 41 | ``` -------------------------------------------------------------------------------- /python-demo/demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # -*- author: LinXunFeng -*- 3 | 4 | from utils import upload_pgyer as PgyerUtil 5 | 6 | if __name__ == "__main__": 7 | 8 | # 上传完成回调 9 | def upload_complete_callback(isSuccess, result): 10 | if isSuccess: 11 | print('上传完成') 12 | _data = result['data'] 13 | _url = _data['buildShortcutUrl'].strip() # 去除首尾空格 14 | _appVer = _data['buildVersion'] 15 | _buildVer = _data['buildBuildVersion'] 16 | print('链接: https://www.pgyer.com/%s'%_url) 17 | print('版本: %s (build %s)'%(_appVer, _buildVer)) 18 | else: 19 | print('上传失败') 20 | 21 | app_path = '' # App包路径 22 | pgyer_api_key = '' # API KEY 23 | 24 | PgyerUtil.upload_to_pgyer( 25 | path = app_path, 26 | api_key = pgyer_api_key, 27 | install_type = 1, 28 | callback=upload_complete_callback 29 | ) 30 | -------------------------------------------------------------------------------- /python-demo/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.28.1 2 | -------------------------------------------------------------------------------- /python-demo/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PGYER/upload-app-api-example/afcb1de3943da7f3f2d337ea57605cf5beaa758d/python-demo/utils/__init__.py -------------------------------------------------------------------------------- /python-demo/utils/upload_pgyer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # -*- author: LinXunFeng -*- 3 | 4 | import time 5 | import requests 6 | 7 | # 官方文档 8 | # https://www.pgyer.com/doc/view/api#fastUploadApp 9 | 10 | def _getCOSToken( 11 | api_key, 12 | install_type, 13 | password='', 14 | update_description='', 15 | callback=None 16 | ): 17 | """ 18 | 获取上传的 token 19 | """ 20 | headers = {'enctype': 'multipart/form-data'} 21 | payload = { 22 | '_api_key': api_key, # API Key 23 | 'buildType': 'ios', # 需要上传的应用类型,ios 或 android 24 | 'buildInstallType': install_type, # (选填)应用安装方式,值为(1,2,3,默认为1 公开安装)。1:公开安装,2:密码安装,3:邀请安装 25 | 'buildPassword': password, # (选填) 设置App安装密码,密码为空时默认公开安装 26 | 'buildUpdateDescription': update_description, # (选填) 版本更新描述,请传空字符串,或不传。 27 | } 28 | try: 29 | r = requests.post('https://api.pgyer.com/apiv2/app/getCOSToken', data=payload, headers=headers) 30 | if r.status_code == requests.codes.ok: 31 | result = r.json() 32 | # print(result) 33 | if callback is not None: 34 | callback(True, result) 35 | else: 36 | if callback is not None: 37 | callback(False, None) 38 | except Exception as e: 39 | print('服务器暂时无法为您服务') 40 | 41 | 42 | def upload_to_pgyer(path, api_key, install_type=2, password='', update_description='', callback=None): 43 | """ 44 | 上传到蒲公英 45 | :param path: 文件路径 46 | :param api_key: API Key 47 | :param install_type: 应用安装方式,值为(1,2,3)。1:公开,2:密码安装,3:邀请安装。默认为1公开 48 | :param password: App安装密码 49 | :param update_description: 50 | :return: 版本更新描述 51 | """ 52 | 53 | def getCOSToken_callback(isSuccess, json): 54 | if isSuccess: 55 | _upload_url = json['data']['endpoint'] 56 | 57 | files = {'file': open(path, 'rb')} 58 | headers = {'enctype': 'multipart/form-data'} 59 | payload = json['data']['params'] 60 | print("上传中...") 61 | 62 | try: 63 | r = requests.post(_upload_url, data=payload, files=files, headers=headers) 64 | if r.status_code == 204: 65 | # result = r.json() 66 | # print(result) 67 | print("上传成功,正在获取包处理信息,请稍等...") 68 | _getBuildInfo(api_key=api_key, json=json, callback=callback) 69 | else: 70 | print('HTTPError,Code:'+ str(r.status_code)) 71 | if callback is not None: 72 | callback(False, None) 73 | except Exception as e: 74 | print('服务器暂时无法为您服务') 75 | else: 76 | pass 77 | 78 | _getCOSToken( 79 | api_key=api_key, 80 | install_type=install_type, 81 | password=password, 82 | update_description=update_description, 83 | callback=getCOSToken_callback, 84 | ) 85 | 86 | def _getBuildInfo(api_key, json, callback=None): 87 | """ 88 | 检测应用是否发布完成,并获取发布应用的信息 89 | """ 90 | time.sleep(3) # 先等个几秒,上传完直接获取肯定app是还在处理中~ 91 | response = requests.get('https://api.pgyer.com/apiv2/app/buildInfo', params={ 92 | '_api_key': api_key, 93 | 'buildKey': json['data']['params']['key'], 94 | }) 95 | if response.status_code == requests.codes.ok: 96 | result = response.json() 97 | code = result['code'] 98 | if code == 1247 or code == 1246: # 1246 应用正在解析、1247 应用正在发布中 99 | _getBuildInfo(api_key=api_key, json=json, callback=callback) 100 | else: 101 | if callback is not None: 102 | callback(True, result) 103 | else: 104 | if callback is not None: 105 | callback(False, None) 106 | 107 | -------------------------------------------------------------------------------- /shell-demo/README.md: -------------------------------------------------------------------------------- 1 | # shell 脚本使用说明 2 | 3 | 使用 curl 命令上传 app 安装包到蒲公英平台。 4 | 5 | 默认支持 Linux、Mac 平台。如需在 Windows 上使用,请安装 [git bash](https://gitforwindows.org)。 6 | 7 | ## 使用说明 8 | 9 | 为脚本赋予执行权限: 10 | 11 | chmod +x ./pgyer_upload.sh 12 | 13 | 执行命令: 14 | 15 | ./pgyer_upload.sh -k 16 | 17 | ## 输出 18 | 19 | 上传成功后,默认情况下直接输出 App 下载页面的 URL。如需额外输出完整 JSON 结果,请增加 `-j` 参数。 20 | 21 | ## 显示帮助 22 | 23 | $ ./pgyer_upload.sh -h 24 | 25 | Usage: ./pgyer_upload.sh -k [OPTION]... file 26 | Upload iOS or Android app package file to PGYER. 27 | Example: ./pgyer_upload.sh -k xxxxxxxxxxxxxxx /data/app.ipa 28 | 29 | Description: 30 | -k api_key (required) api key from PGYER 31 | -t buildInstallType build install type, 1=public, 2=password, 3=invite 32 | -p buildPassword build password, required if buildInstallType=2 33 | -d buildUpdateDescription build update description 34 | -e buildInstallDate build install date, 1=buildInstallStartDate~buildInstallEndDate, 2=forever 35 | -s buildInstallStartDate build install start date, format: yyyy-MM-dd 36 | -e buildInstallEndDate build install end date, format: yyyy-MM-dd 37 | -c buildChannelShortcut build channel shortcut 38 | -q quiet mode, disable progress bar 39 | -j output full JSON response after completion 40 | -h help show this help 41 | 42 | Report bugs to: 43 | Project home page: 44 | 45 | ## 日志 46 | 47 | 默认为关闭状态。您可以修改文件中的 `LOG_ENABLE=1` 来开启日志,这样可以在遇到错误时方便调试 48 | 49 | ## Windows 用户 50 | 51 | 1. 安装 [git bash](https://gitforwindows.org),以便让 windows 具备 bash 环境 52 | 2. 以`bash.exe`来执行 `pgyer_upload.sh` 脚本 53 | 54 | 命令如下(注意您的安装目录可能有所不同,请相应替换): 55 | 56 | D:\> & 'C:\Program Files\Git\bin\bash.exe' .\pgyer_upload.sh -k 57 | 58 | 完成后,就会直接返回 App 的上传结果 59 | 60 | ## 其他 61 | 62 | [显示上传进度](https://github.com/PGYER/pgyer_api_example/issues/19) 63 | 64 | 65 | -------------------------------------------------------------------------------- /shell-demo/pgyer_upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Shell script to upload local app files to PGYER via API 4 | # https://www.pgyer.com/doc/view/api#fastUploadApp 5 | # 6 | 7 | # --------------------------------------------------------------- 8 | # Constants 9 | # --------------------------------------------------------------- 10 | readonly API_BASE_URL="http://api.pgyer.com/apiv2" 11 | readonly SUPPORTED_TYPES=("ipa" "apk") 12 | 13 | # --------------------------------------------------------------- 14 | # Configuration 15 | # --------------------------------------------------------------- 16 | LOG_ENABLE=1 17 | PROGRESS_ENABLE=1 18 | JSON_OUTPUT=0 19 | 20 | # --------------------------------------------------------------- 21 | # Utility Functions 22 | # --------------------------------------------------------------- 23 | log() { 24 | [ $LOG_ENABLE -eq 1 ] && echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" 25 | } 26 | 27 | logTitle() { 28 | log "-------------------------------- $* --------------------------------" 29 | } 30 | 31 | execCommand() { 32 | log "$@" 33 | result=$(eval $@) 34 | } 35 | 36 | # --------------------------------------------------------------- 37 | # Parameter Processing Functions 38 | # --------------------------------------------------------------- 39 | printHelp() { 40 | cat << EOF 41 | Usage: $0 -k [OPTION]... file 42 | Upload iOS or Android app package file to PGYER. 43 | Example: $0 -k xxxxxxxxxxxxxxx /data/app.ipa 44 | 45 | Description: 46 | -k api_key (required) api key from PGYER 47 | -t buildInstallType build install type, 1=public, 2=password, 3=invite 48 | -p buildPassword build password, required if buildInstallType=2 49 | -d buildUpdateDescription build update description 50 | -e buildInstallDate build install date, 1=buildInstallStartDate~buildInstallEndDate, 2=forever 51 | -s buildInstallStartDate build install start date, format: yyyy-MM-dd 52 | -e buildInstallEndDate build install end date, format: yyyy-MM-dd 53 | -c buildChannelShortcut build channel shortcut 54 | -q quiet mode, disable progress bar 55 | -j output full JSON response after completion 56 | -h help show this help 57 | 58 | Report bugs to: 59 | Project home page: 60 | EOF 61 | exit 1 62 | } 63 | 64 | parseArguments() { 65 | while getopts 'k:t:p:d:s:e:c:qjh' OPT; do 66 | case $OPT in 67 | k) api_key="$OPTARG";; 68 | t) buildInstallType="$OPTARG";; 69 | p) buildPassword="$OPTARG";; 70 | d) buildUpdateDescription="$OPTARG";; 71 | e) buildInstallDate="$OPTARG";; 72 | s) buildInstallStartDate="$OPTARG";; 73 | e) buildInstallEndDate="$OPTARG";; 74 | c) buildChannelShortcut="$OPTARG";; 75 | q) PROGRESS_ENABLE=0;; 76 | j) JSON_OUTPUT=1;; 77 | ?) printHelp;; 78 | esac 79 | done 80 | 81 | shift $(($OPTIND - 1)) 82 | readonly file=$1 83 | } 84 | 85 | validateInputs() { 86 | # Check API key 87 | if [ -z "$api_key" ]; then 88 | echo "Error: api_key is empty" 89 | printHelp 90 | fi 91 | 92 | # Check if file exists 93 | if [ ! -f "$file" ]; then 94 | echo "Error: file not exists" 95 | printHelp 96 | fi 97 | 98 | # Check if file type is supported 99 | buildType=${file##*.} 100 | if [[ ! " ${SUPPORTED_TYPES[@]} " =~ " ${buildType} " ]]; then 101 | echo "Error: file extension '${buildType}' is not supported" 102 | printHelp 103 | fi 104 | } 105 | 106 | # --------------------------------------------------------------- 107 | # Business Logic Functions 108 | # --------------------------------------------------------------- 109 | getUploadToken() { 110 | logTitle "Step 1: Get Token" 111 | 112 | local command="curl -s" 113 | [ -n "$api_key" ] && command="${command} --form-string '_api_key=${api_key}'" 114 | [ -n "$buildType" ] && command="${command} --form-string 'buildType=${buildType}'" 115 | [ -n "$buildInstallType" ] && command="${command} --form-string 'buildInstallType=${buildInstallType}'" 116 | [ -n "$buildPassword" ] && command="${command} --form-string 'buildPassword=${buildPassword}'" 117 | [ -n "$buildUpdateDescription" ] && command="${command} --form-string $'buildUpdateDescription=${buildUpdateDescription}'" 118 | [ -n "$buildInstallDate" ] && command="${command} --form-string 'buildInstallDate=${buildInstallDate}'" 119 | [ -n "$buildInstallStartDate" ] && command="${command} --form-string 'buildInstallStartDate=${buildInstallStartDate}'" 120 | [ -n "$buildInstallEndDate" ] && command="${command} --form-string 'buildInstallEndDate=${buildInstallEndDate}'" 121 | [ -n "$buildChannelShortcut" ] && command="${command} --form-string 'buildChannelShortcut=${buildChannelShortcut}'" 122 | command="${command} ${API_BASE_URL}/app/getCOSToken" 123 | 124 | execCommand "$command" 125 | 126 | # Parse response 127 | [[ "${result}" =~ \"endpoint\":\"([\:\_\.\/\\A-Za-z0-9\-]+)\" ]] && endpoint=`echo ${BASH_REMATCH[1]} | sed 's!\\\/!/!g'` 128 | [[ "${result}" =~ \"key\":\"([\.a-z0-9]+)\" ]] && key=`echo ${BASH_REMATCH[1]}` 129 | [[ "${result}" =~ \"signature\":\"([\=\&\_\;A-Za-z0-9\-]+)\" ]] && signature=`echo ${BASH_REMATCH[1]}` 130 | [[ "${result}" =~ \"x-cos-security-token\":\"([\_A-Za-z0-9\-]+)\" ]] && x_cos_security_token=`echo ${BASH_REMATCH[1]}` 131 | 132 | if [ -z "$key" ] || [ -z "$signature" ] || [ -z "$x_cos_security_token" ] || [ -z "$endpoint" ]; then 133 | log "Error: Failed to get upload token" 134 | exit 1 135 | fi 136 | } 137 | 138 | uploadFile() { 139 | logTitle "Step 2: Upload File" 140 | 141 | local file_name=${file##*/} 142 | local progress_option="--progress-bar" 143 | [ $PROGRESS_ENABLE -eq 0 ] && progress_option="-s" 144 | 145 | execCommand "curl -o /dev/null -w '%{http_code}' \ 146 | ${progress_option} \ 147 | --form-string 'key=${key}' \ 148 | --form-string 'signature=${signature}' \ 149 | --form-string 'x-cos-security-token=${x_cos_security_token}' \ 150 | --form-string 'x-cos-meta-file-name=${file_name}' \ 151 | -F 'file=@${file}' ${endpoint}" 152 | 153 | if [ $result -ne 204 ]; then 154 | log "Error: Upload failed" 155 | exit 1 156 | fi 157 | } 158 | 159 | checkResult() { 160 | logTitle "Step 3: Check Result" 161 | 162 | local max_retries=60 163 | local url_printed=0 164 | local final_result="" 165 | 166 | for i in $(seq 1 $max_retries); do 167 | execCommand "curl -s ${API_BASE_URL}/app/buildInfo?_api_key=${api_key}\&buildKey=${key}" 168 | final_result="${result}" 169 | 170 | # Parse the result 171 | [[ "${result}" =~ \"code\":([0-9]+) ]] && code=`echo ${BASH_REMATCH[1]}` 172 | 173 | if [ $code -eq 0 ]; then 174 | # Extract and print URL only once 175 | if [ $url_printed -eq 0 ]; then 176 | [[ "${result}" =~ \"buildShortcutUrl\":\"([^\"]+)\" ]] && shortcut_url=`echo ${BASH_REMATCH[1]}` 177 | if [ -n "$shortcut_url" ]; then 178 | log "Upload successful! App URL: https://www.pgyer.com/${shortcut_url}" 179 | fi 180 | url_printed=1 181 | fi 182 | break 183 | else 184 | log "Checking build status... (Attempt ${i}/${max_retries})" 185 | sleep 1 186 | fi 187 | done 188 | 189 | if [ $code -ne 0 ]; then 190 | log "Error: Build check failed after ${max_retries} attempts" 191 | exit 1 192 | fi 193 | 194 | # Output full JSON response if requested 195 | if [ $JSON_OUTPUT -eq 1 ]; then 196 | log "Full response JSON:" 197 | echo "${final_result}" 198 | fi 199 | } 200 | 201 | # --------------------------------------------------------------- 202 | # Main Function 203 | # --------------------------------------------------------------- 204 | main() { 205 | parseArguments "$@" 206 | validateInputs 207 | getUploadToken 208 | uploadFile 209 | checkResult 210 | } 211 | 212 | # Execute main function 213 | main "$@" 214 | --------------------------------------------------------------------------------