教你搭建App内测下载平台

作者: 知傲 | 来源:发表于2017-01-08 14:27 被阅读4327次

    前言

    App开发测试过程中,我们会把安装包传到各种第三方的内测分发平台方便下载。这些平台或多或少有这样那样的限制,比如下载量啊、付费啊、不能方便找到历史版本啊。还有一方面,我们经常会打Debug版本的包方便调试,又不希望Debug包流传到外部去,这样就很有必要自己搭一个下载平台,于是就有了这个项目(github地址)。

    技术调研

    怎么下载

    先说安卓,apk文件通过最简单的http/ftp下载就可以安装了,略过。
    iOS稍微复杂一点,需要两步才能完成。
    第一,下载链接必须是这样的格式

    itms-services://?action=download-manifest&url=一个plist文件的地址
    

    第二,plist内容如下

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>items</key>
        <array>
            <dict>
                <key>assets</key>
                <array>
                    <dict>
                        <key>kind</key>
                        <string>software-package</string>
                        <key>url</key>
                        <string>ipa文件的地址</string>
                    </dict>
                </array>
                <key>metadata</key>
                <dict>
                    <key>bundle-identifier</key>
                    <string>bundleID</string>
                    <key>bundle-version</key>
                    <string>1.0</string>
                    <key>kind</key>
                    <string>software</string>
                    <key>title</key>
                    <string>AppTitle</string>
                </dict>
            </dict>
        </array>
    </dict>
    </plist>
    

    其中,最重要的就是ipa文件的地址,要求必须是https协议,那就需要SSL证书,幸运的是我们可以信任自签名的证书。下载的过程就是这样,当然我们希望这个链接和plist的生成是自动完成的。

    自签名证书

    参考如何创建一个自签名的SSL证书(X509)

    包信息提取

    单单只能下载还不够,我们希望看到更多的信息:App名字、版本号、build号、更新时间、图标等。这些信息虽然可以留给上传者在上传的时候一并带上,但是作为有追求的程序员,把方便留给别人的最基本的,因此我们要从ipa/apk中提取这些信息。
    无论是ipa还是apk,本质都是zip压缩文件。
    对于iOS的ipa,包信息都放在Info.plist中,主要有CFBundleVersion、CFBundleIdentifier、CFBundleShortVersionString、CFBundleName等。图标文件的名字也是固定的,只要解压就可以得到。不过,苹果对png图片进行了了自定义的pngcrush压缩,有压缩自然就有还原工具pngdefry
    对于Android的apk,解压后还能看到AndroidManifest.xml,但是里面的内容经过编码显示为乱码,不方便查看,需要借助开发工具aapt(Android Asset Packaging Tool),方法如下
    aapt dump badging apkPath
    输出的文本格式如下,不是标准的歌声,需要手动转换一下。

    package: name='com.jianshu.haruki' versionCode='16070101' versionName='1.11.2'
    sdkVersion:'14'
    targetSdkVersion:'22'
    ...
    application: label='简书' icon='res/drawable-hdpi-v4/icon_jianshu_new.png'
    ...
    

    找轮子

    程序员有一个习惯,需要某个东西的时候会先一番搜索,直接用别人写好的,用着用着发现别人写的东西有这样那样的不足,然后撸起袖子自己造一个。这次也不例外,我在github上找到了一个ios-ipa-server,它的特点是简单,ipa文件存储在一个目录下,没有数据库,包信息只有上传时间(其实就是文件更新时间),不能对app归类,只靠文件名区别,不支持上传,如下图:


    浏览器访问下载页面时,后端实时解析包信息、解压icon图片,这样做效率是非常低的。
    这么多不足我们就有了造轮子的理由了。

    自己造一个

    既然ios-ipa-server是基于node-express写的,正好我没写过nodejs,那就在它的基础上继续写吧,借机学(zhuang)习(bi)一下。
    整个项目的结构是这样的,提供四个API:包上传、获取所有App最新版本、获取某个App的所有版本、动态生成plist文件,数据存储使用sqlite3。

    包上传

    接口设计如下:

    path:
    POST /upload
    
    param: 
    package:安装包文件
    
    response:
    {
            id: 6,
            guid: "46269d71-9fda-76fc-3442-a118d6b08bf1",
            bundleID: "com.jianshu.Hugo",
            version: "2.11.4",
            build: "1608051045",
            icon: "https://10.20.30.233:1234/icon/46269d71-9fda-76fc-3442-a118d6b08bf1.png",
            name: "Hugo",
            uploadTime: "2016-12-01 20:50:05",
            platform: "ios",
            url: "itms-services://?action=download-manifest&url=https://10.20.30.233:1234/plist/46269d71-9fda-76fc-3442-a118d6b08bf1"
    }
    

    后端需要拿到安装包,提取出包信息和png图标图片,然后插入到数据库中,最后存储安装包文件和png图片,这也是最关键、最复杂的一个API。

      app.post('/upload', function(req, res) {
        var form = new multiparty.Form();
        form.parse(req, function(err, fields, files) {
          var obj = files.package[0];
          var tmp_path = obj.path;
          parseAppAndInsertToDb(tmp_path, info => {
            storeApp(tmp_path, info["guid"], error => {
              if (error) {
                errorHandler(error,res)
              }
            })
            console.log(info)
            res.send(info)
          }, error => {
            errorHandler(error,res)
          });
        });
      });
    

    接收表单信息用到了multiparty模块,parseAppAndInsertToDb内部完成了包信息的提取和存储,storeApp存储包文件。
    parseAppAndInsertToDb的实现如下,

    function parseAppAndInsertToDb(filePath, callback, errorCallback) {
      var guid = Guid.create().toString();
      var parse, extract
      if (path.extname(filePath) === ".ipa") {
        parse = parseIpa
        extract = extractIpaIcon
      } else if (path.extname(filePath) === ".apk") {
        parse = parseApk
        extract = extractApkIcon
      }
      Promise.all([parse(filePath),extract(filePath,guid)]).then(values => {
        var info = values[0]
        info["guid"] = guid
        excuteDB("INSERT INTO info (guid, platform, build, bundleID, version, name) VALUES (?, ?, ?, ?, ?, ?);",
        [info["guid"], info["platform"], info["build"], info["bundleID"], info["version"], info["name"]],function(error){
            if (!error){
              callback(info)
            } else {
              errorCallback(error)
            }
        });
      }, reason => {
        errorCallback(reason)
      })
    }
    

    首先根据文件后缀名判断安装包类型,因为ipa和apk的处理逻辑不一样,所以分别对应两个方法,包信息的提取和icon提取可以同时进行,所以这里用了Promise.allparseIpaparseApk就是包信息的提取。extractApkIconextractIpaIcon则是icon的提取,extractIpaIcon多了一步还原png图片的处理。
    parseIpa用到了ipa-extract-info模块,parseApk则使用了apk-parser3,代码都非常简单。详细可进入github地址

    其他

    其他三个API则比较简单了,无非就是根据参数取数据,不再赘述。

    集成和使用

    安装步骤非常简单,首先需要安装node,有了node之后只要一行命令

    npm install -g ipapk-server
    

    安装完成之后输入命令

    ipapk-server
    

    手机浏览器访问https://ip:port 即可打开下载页面



    App的信息获取都设计成了API,提供给开发者更灵活的接入方式,可以做web页面,也可以做成App,我的好朋友mask(人格分裂术)贡献了不少工作,完成默认的web下载页面。
    更详细的内容请参考github

    写在最后

    简书作为一个优质原创内容社区,拥有大量优质原创内容,提供了极佳的阅读和书写体验,吸引了大量文字爱好者和程序员。简书技术团队在这里分享技术心得体会,是希望抛砖引玉,吸引更多的程序员大神来简书记录、分享、交流自己的心得体会。这个专题以后会不定期更新简书技术团队的文章,包括Android、iOS、前端、后端等等,欢迎大家关注。

    相关文章

      网友评论

      • boboliu123:打开这个网页之后,页面上的按钮都不能点击是什么情况
      • 九龙:上传第二版的时候就出现bug了
        如下:
        pngdefry : seen 1 file(s), wrote 1 file(s)
        /usr/local/lib/node_modules/ipapk-server/ipapk-server.js:302
        var data = info[0];
        ^

        TypeError: Cannot read property '0' of undefined
        at /usr/local/lib/node_modules/ipapk-server/ipapk-server.js:302:22
        at f (/usr/local/lib/node_modules/ipapk-server/node_modules/once/once.js:25:25)
        at ZipFile.<anonymous> (/usr/local/lib/node_modules/ipapk-server/node_modules/ipa-extract-info/index.js:52:33)
        at emitNone (events.js:106:13)
        at ZipFile.emit (events.js:208:7)
        at Immediate._onImmediate (/usr/local/lib/node_modules/ipapk-server/node_modules/yauzl/index.js:246:12)
        at runCallback (timers.js:789:20)
        at tryOnImmediate (timers.js:751:5)
        at processImmediate [as _immediateCallback] (timers.js:722:5)
      • 九龙:必须给个赞
      • Laki只是想做一个程序猿:NSLog(@"兄弟 就这简书,真是辛苦你了,尤其有这顶头的CEO,简书的初心走远了!");
        exit(1)
      • nenhall:這個好
      • 菊上一枝梅:最后感谢你女朋友在你写这篇文章的时候喂你吃水果。
      • zhoutq::+1: :+1: 附上原作者和地址 转啦。
      • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/d07uq5 欢迎点赞支持!
        欢迎订阅《iOS技术分享》https://toutiao.io/subjects/58931

      本文标题:教你搭建App内测下载平台

      本文链接:https://www.haomeiwen.com/subject/txnpmttx.html