专栏文章 jenkins 打包产物生成二维码

jb · 2019年02月12日 · 最后由 lifangyi 回复于 2019年02月15日 · 6739 次阅读

JB 的测试之旅-jenkins 打包产物生成二维码

标签(空格分隔): 测试


前言

无风不起浪,为什么会做这个事情,就要由前几天讲起了。。
image.png-26.1kB

悲剧了

小公司没有资源,因为很多内测都是用第三方的,这边用的是蒲公英;

在某日早上,开发提测,打包,上传pgy,准备给业务方体验的时候,结果点击查看下载页按钮,弹出这货;
image.png-98.1kB

一开始以为是自己手误,然后再上传几次,依然显示这个界面,也没有任何报错信息,懵逼啊,之前都用的好好的,什么鬼?
image.png-67.1kB

折腾半天,无望,拿起手机,看到有短信,点开一开,显示这个:
image.png-32.9kB

这里面说到不再接受金融类应用在该平台分发,我司产品虽然是资讯类产品,但内容的确是金融相关的,好像没毛病;

操作起来有点麻烦

公司某项目的打包产品是一个 zip,是当打包完成后把 apk 跟 ipa 压缩成一个 zip 输出,而使用者需要下载这个 zip,解压,电脑连手机/模拟器安装,方可体验;
image.png-77kB

整个链路过长,也比较麻烦,因此就想着两点:

  • 打包拆分,支持安卓、ios 分开打包,不然有时候验证一个平台的问题需要打两个包,打包时长成本问题;
  • 打包产物显示二维码便捷下载

在正式开始之前,先说明下,testerhome 其实有类似的文章,如下图:
在此输入正文

对应的文章都写的挺好的,但是轮子嘛,还是要亲力亲为印象才深刻,而且会针对对应文章缺乏的内容进行相应补充,尽可能更加详细把过程写出来;

jenkins 显示图片

这里不会再讲述 jenkins 是什么,怎么安装之类的内容,如果有疑问,请点击此处第二处查看;

想要做成的效果是这样的:

  • 支持修改文件描述
  • 支持显示二维码 image.png-94.9kB

插件安装

jenkins 不支持上面两个操作的,因此需要安装插件来使用;

  • Build Name Setter ,用于修改 Build 名称
  • description setter,用于在修改 Build 描述信息,在描述信息中增加显示 QRCode(二维码)

直接在 Jenkins 的插件管理页面搜索上述插件,点击安装即可
image.png-16.6kB
image.png-37.5kB

怎么用

点击对应 job 的设置界面;

Build Name Setter

点击Build Environment,找到set build name,默认是#${BUILD_NUMBER},这里可以自定义,如下修改成#${BUILD_NUMBER}jbtest,执行任务后的结果是这样的;
image.png-63.8kB

description setter

这个是在Post-build Actions里面,将<img src='qr_code_url'>写入到 build 描述信息中即可;

但填写完发现跟预想的不一致,这是因为 Jenkins 出于安全的考虑,所有描述信息的Markup Formatter默认都是采用Plain text模式,在这种模式下是不会对 build 描述信息中的 HTML 编码进行解析的。

要改变也很容易,Manage Jenkins -> Configure Global Security,将Markup Formatter的设置更改为Safe HTML即可。
image.png-25.4kB
更改配置后,我们就可以在 build 描述信息中采用 HTML 的 img 标签插入图片了。

保存后,执行任务,会就会显示 url 对应的图片了;
image.png-174.2kB

到这里,jenkins 上显示图片的问题,就这样解决啦~
image.png-4.8kB

小小结

jenkins 显示图片及修改任务描述,需要安装两个插件,并且需要传一个图片的 img 标签过来即可;

jenkins 任务结果收集产物

这里额外提及一个点,如果 job 里面是有产物的,比如 apk 等文件,默认构建后是不会显示出来的,如下图:
image.png-330.2kB

那怎样让其在右侧显示出来?还是打开 job 的设置项,Post-build Actions,选择归档成品/Archives build artifacts,在Files to archive里面输入内容就好啦;

定位文件时,可以通过正则表达式进行匹配,也可以调用项目的环境变量;多个文件通过逗号进行分隔;

${OUTPUT_FOLDER}/*.ipa,*.txt,QRCode.png

添加后的配置页面如下图所示:
image.png-19.2kB

重新构建任务,就可以看到对应的产物啦;
image.png-49.1kB

分发平台

首先说明,非广告贴,非广告贴,这节除了介绍分发平台,也会介绍不使用分发平台时怎么搞,任君选择;

上网搜了下,目前国内比较有名且还能用的分发平台,就是蒲公英fir.im

蒲公英

点击上面的地址打开官网,注册登录,点击文档,会有 API 说明;

简单看了下,支持的功能蛮多的,好像可以,而本章的重点是上传 APP,可以搜索框输入上传 APP,也可以点击链接直接跳转;
image.png-23.8kB

仔细看了下 response,有二维码地址,good,就是你啦;

常规参数说明

参数 别称 说明
_api_key API Key API Key,用来识别 API 调用者的身份,如不特别说明,每个接口中都需要含有此参数。对于同一个蒲公英的注册用户来说,这个值在固定的;
userKey User Key 用户 Key,用来标识当前用户的身份,对于同一个蒲公英的注册用户来说,这个值在固定的;
appKey App Key 表示一个 App 组的唯一 Key。例如,名称为'微信'的 App 上传了三个版本,那么这三个版本为一个 App 组,该参数表示这个组的 Key。这个值显示在应用详情 -- 应用概述--App Key。
buildKey Build Key Build Key 是唯一标识应用的索引 ID,可以通过获取 App 所有版本取得

_api_keyuserKey在登录状态下,点击网页的按钮即可获取;

上传 App

参数太多了,懒的贴了,直接上代码吧;

Linux

这是官网给的例子,Linux下直接使用curl命令上传即可;

curl -F 'file=@/c/Users/jb/Desktop/jb-android-3.4.1.30402-release-1812251912.apk' -F '_api_key=你的key' https://www.pgyer.com/apiv2/app/upload

执行后等待上传完即可:
image.png-52.7kB
从返回的结果来看,是有一个buildQRCodeURL,就是拿这个给到 jenkins 那边的;

Python
环境,py3


import requests
import sys
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

upload_url = "https://www.pgyer.com/apiv2/app/upload"
_api_key = "你的key"
apk_path = "你要上传的文件路径"
def pgy_uploadFile():
    # 获取运行传递过来的参数
    # _apk_path = sys.argv[1]
    # 上传apk
    try:
        file = {'file': open(apk_path, 'rb')}
        param = {'_api_key': _api_key}
        req=requests.post(url=upload_url,files=file,data=param,verify=False)
        if (req.status_code == 200):
            print(req.json().get("data")["buildQRCodeURL"])
        else:
            print("上传失败,状态码: "+req.status_code)
    except Exception as e:
        print("upload:" + e)
if __name__ == '__main__':
    pgy_uploadFile()

如果是需要传参给脚本,就直接用sys.argv来获取,脚本本来没做太多兼容处理,将就用吧;

最后会输出二维码地址,拿这个地址传给 jenkins 就好啦;

image.png-12.8kB

结合 jenkins 玩玩

上面提及到,jenkins 显示二维码是利用 img src 来处理,但是这个蒲公英返回的二维码地址是每次都不同的呢,那怎么搞?按照常理来说,是把 src 的值写成变量就好啦;
image.png-40.6kB

其实就是写成一个变量就好了,但是也因为 url 本身每次都变化,因此不能直接贴 url,而是把 url 下载下来,固定下来一个名称,变量直接取这个路径即可;

那上面的 img 标签就会变成这样啦:

<img src="${BUILD_URL}/artifact/QRCode.png" style="background-color: width: 286px; height: 189px;">
# 上面有多余的样式,因为图片是随便复制的,比较大,因此做了下限制,非必选;

这里可能有同学会问题,这个${BUILD_URL}是怎么来的,代表什么意思;

${BUILD_URL}是 jenkins 的内置变量,代表着显示当前构建的 URL 地址,文章尾部会列出常用的 jenkins 变量;

比如上,假如二维码的链接是这样:

jenkineUrl/job/jobName/34/artifact/QRCode.png

那么,jenkineUrl/job/jobName/34这一串就是${BUILD_URL}

既然需要图片,那我们就下载图片吧,反正都有 url 了;

def pgy_doanloadQRCode(QRCodeURL):
    print("准备下载二维码")
    filename = os.getcwd()+"/QRCode.png"
    with open(filename, 'wb') as f:
        # 以二进制写入的模式在本地构建新文件
        header = {
            'User-Agent': '"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",'
            , 'Referer': QRCodeURL}
        f.write(requests.get(QRCodeURL, headers=header).content)
        print("%s下载完成" % filename)

结果截图

因 pgy 需要上传 apk 或 ipa,因为为了方便,直接 hardcore 了一个图片 url 来演示结果;
image.png-51.2kB
image.png-138.7kB

fir.im

点击这里跳转到官网,看了下,实名认证用户有 100 次/日的免费下载限额,未实名,仅有 10 次/日的免费下载限额;

一般来说,小公司,每天 100 次够用啦,除非产品够多,或者打包频繁;

实名好麻烦,还要手持证件照,没关系,反正有 10 次,够用啦;

然后去看 api 文章,咦,response 居然没有二维码字段?那手动上传一个应用试试看,结果。。

image.png-6.3kB
当时心里的疑问就如下图一样,好吧,再见;
image.png-89.4kB

小小结

网上找了下分发平台,国内比较有名且还能用的只剩下蒲公英跟 fir.im,然而 fir.im 需要实名才能玩,那就剩下蒲公英了,亲自接入下蒲公英,接入比较简单,而且支持的字段也不少,目前来看,比较推荐,省去不少事;

造轮子

此时有同学可能会有疑问,我司产品比较机密,不想用第三方,自己可以造一个轮子吗?
image.png-36.6kB

这个问题非常好,没错,可以的,简单想想了,这个轮子需要啥?

  • 一个界面,提供上传文件按钮;
  • 文件支持点击下载,支持鼠标移动到文件时显示对应的二维码;
  • 一台服务器;

想的界面很简单,本来一开始是想着安装个 phpstudy 就好啦,但是突然想起,前几天看到大佬发了个截图:
image.png-119.4kB

想着差不多,也顺便弄一个仓库呗,比较方便;

但 jb 不懂这个怎么弄,就跑去问老大了;
image.png-94.4kB

听老大说挺简单的,但是啊,jb 只听过 ng,没真正玩过啊,哪里简单的了,哭;
image.png-9.4kB

于是乎就去简单了解 ng 下了,漫漫人生路;

nginx 简单介绍

也许没听过 nginx,但是没关系,Apache 肯定是听过的,这两者都属于 http server,因此,nginx 同样是一款开源的 HTTP 服务器软件;

主要拿来干嘛

  • 反向代理
  • 负载均衡
  • HTTP 服务器(包含动静分离)
  • 正向代理

反向代理

反向代理应该是 Nginx 做的最多的一件事了;

什么是反向代理呢,以下是百度百科的说法:

反向代理(Reverse Proxy)方式是指以代理服务器来接受 internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。简单来说就是真实的服务器不能直接被外部网络访问,所以需要一台代理服务器,而代理服务器能被外部网络访问的同时又跟真实服务器在同一个网络环境,当然也可能是同一台服务器,端口不同而已。
下面贴上一段简单的实现反向代理的代码

server {
        listen       80;                                                         
        server_name  localhost;                                               
        client_max_body_size 1024M;

        location / {
            proxy_pass http://localhost:8080;
            proxy_set_header Host $host:$server_port;
        }
    }

保存配置文件后启动 Nginx,这样当我们访问 localhost 的时候,就相当于访问 localhost:8080 了;

负载均衡

负载均衡其意思就是分摊到多个操作单元上进行执行,例如 Web 服务器、FTP 服务器、企业关键应用服务器和其它关键任务服务器等,从而共同完成工作任务。

简单而言就是当有 2 台或以上服务器时,根据规则随机的将请求分发到指定的服务器上处理,负载均衡配置一般都需要同时配置反向代理,通过反向代理跳转到负载均衡。

HTTP 服务器

Nginx 本身也是一个静态资源的服务器,当只有静态资源的时候,就可以使用 Nginx 来做服务器,同时现在也很流行动静分离,就可以通过 Nginx 来实现,首先看看 Nginx 做静态资源服务器;

server {
        listen       80;                                                         
        server_name  localhost;                                               
        client_max_body_size 1024M;


        location / {
               root   e:\wwwroot;
               index  index.html;
           }
    }

这样如果访问http://localhost 就会默认访问到 E 盘 wwwroot 目录下面的 index.html,如果一个网站只是静态页面的话,那么就可以通过这种方式来实现部署。

动静分离

动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路;

upstream test{  
       server localhost:8080;  
       server localhost:8081;  
    }   

    server {  
        listen       80;  
        server_name  localhost;  

        location / {  
            root   e:\wwwroot;  
            index  index.html;  
        }  

        # 所有静态请求都由nginx处理,存放目录为html  
        location ~ \.(gif|jpg|jpeg|png|bmp|swf|css|js)$ {  
            root    e:\wwwroot;  
        }  

        # 所有动态请求都转发给tomcat处理  
        location ~ \.(jsp|do)$ {  
            proxy_pass  http://test;  
        }  

        error_page   500 502 503 504  /50x.html;  
        location = /50x.html {  
            root   e:\wwwroot;  
        }  
    }

这样就可以吧 HTML 以及图片和 css 以及 js 放到 wwwroot 目录下,而 tomcat 只负责处理 jsp 和请求;

例如当我们后缀为 gif 的时候,Nginx 默认会从 wwwroot 获取到当前请求的动态图文件返回,当然这里的静态文件跟 Nginx 是同一台服务器,我们也可以在另外一台服务器,然后通过反向代理和负载均衡配置过去就好了,只要搞清楚了最基本的流程,很多配置就很简单了,另外 localtion 后面其实是一个正则表达式,所以非常灵活;

正向代理

意思是一个位于客户端和原始服务器 (origin server) 之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标 (原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。

nginx 常用命令

**启动nginx**

    service nginx start

**停止nginx**

    nginx -s stop

**查看nginx进程**

    ps -ef | grep nginx 

**平滑启动nginx**

    nginx -s reload

平滑启动的意思是在不停止nginx的情况下,重启nginx,重新加载配置文件,启动新的工作线程,完美停止旧的工作线程。

**强制停止nginx**

    pkill -9 nginx

**检查对nginx.conf文件的修改是否正确**

    nginx -t -c /etc/nginx/nginx.conf
    or 
    nginx -t

**查看nginx的版本**

    nginx -v
    or
    nginx -V

端口开放

因阿里云默认是安装了 nginx 1.6 版本,因此这块不说明;

直接在阿里云找到安全组规则,添加对应的对口,就可以用公网 IP 访问啦;
image.png-36.1kB

修改默认端口

因 nginx 默认是使用 80 端口的,如果需要修改,需要去配置文件修改;

nginx 安装文件在/etc/nginx,打开后发现里面有个nginx.conf,查看发现里面没有端口信息,但是最后一行插入了*.conf文件,那我们就跟着这目录找;
image.png-81kB

cd到 conf.d 目录,发现里面只有一个default.conf文件,编辑查看,发现里面有个 listen 端口,这个就是了,修改成像要的端口,保存即可;
image.png-51.2kB

然后输入nginx -s reload重启服务器,然后再用公网 IP+ 端口访问下,也会显示Welcome to nginx!

增加端口

有同学可能问,那想加多几个端口可以吗?

没问题的,还是来到default.conf文件,在原来的 server 下新增一个就好啦,如下:

server {
    listen       8083;
    server_name  location;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
    root /home/file_dir;
        autoindex on;   #开启nginx目录浏览功能
        autoindex_exact_size off;   #文件大小从KB开始显示
        autoindex_localtime on;   #显示文件修改时间为服务器本地时间

        add_after_body /autoindex.html;
        charset utf-8;
    }

autoindex

nginx 有一个目录浏览功能 (autoindex),但是呢,默认是不允许列出整个目录的,如果有需求,就用上面的新增端口的方式来操作就好啦;

而上面这个 autoindex.html 文件点击下发链接下载即可;

链接:https://pan.baidu.com/s/1oiukkMAILzq9lHwCKzy-0w
提取码:7ytc

最后,整个效果如下:
image.png-70kB

还可以解析 README.md,骚啊;
image.png-113.9kB

root&alias

在弄 ng 的配置文件时,看到过别人是这样弄的;

location /ware {
    alias /lvdata/warehouse/;

而自己基本上只会这么弄的:

location / {  
    root   e:\wwwroot;  

当时心里就想,这两者有什么区别?

简介

nginx 指定文件路径有两种方式rootalias;

root 与 alias 主要区别在于nginx 如何解释 location 后面的 uri,这会使两者分别以不同的方式将请求映射到服务器文件上。

语法

**root的用法**

    句法:   root path;
    默认:   root html;
    语境:   http,server,location,if in location

示例 1:

location ^~ /request_path/dirt/ {
    root /local_path/dirt/;
  }

当客户端请求 /request_path/image/file.ext的时候,Nginx 把请求解析映射为/local_path/dirt/request_path/dirt/file.ext

实例 2:

location ^~ /t/ {
 root /www/root/html/;
}

如果一个请求的 URI 是/t/a.html时,web 服务器将会返回服务器上的/www/root/html/t/a.html的文件;

**alias的用法**

    句法:   alias path;
    默认:   -
    语境:   location

示例 1:

location /request_path/dirt/ {
    alias /local_path/dirt/file/;
}

当客户端请求 /request_path/dirt/file.ext 的时候,Nginx 把请求映射为/local_path/dirt/file/file.ext
注意这里是 file 目录,因为alias会把location后面配置的路径丢弃掉(比如/request_path/dirt/one.html,到alias那里就剩one.html了),把当前匹配到的目录指向到指定的目录。

示例 2:

location ^~ /t/ {
 alias /www/root/html/new_t/;
}

如果一个请求的 URI 是/t/a.html时,web 服务器将会返回服务器上的/www/root/html/new_t/a.html的文件;

综合例子

location /abc/ {
    alias /home/html/abc/;
}

在这段配置下,http://test/abc/a.html 就指定的是 /home/html/abc/a.html;

这段配置亦可改成使用 root 标签:

location /abc/ {
    root /home/html/;
}

这样,nginx 就会去找 /home/html/ 目录下的 abc 目录了,得到的结果是相同的。

但是,如果把 alias 的配置改成

location /abc/ {
    alias /home/html/def/;
}

那么 nginx 将会直接从 /home/html/def/ 取数据,例如访问 http://test/abc/a.html 指向的是 /home/html/def/a.html;

这段配置还不能直接使用 root 配置,如果非要配置,只有在 /home/html/ 下建立一个 def->abc软 link(快捷方式)了。

一般情况下,在 location / 中配置 root,在 location /other 中配置 alias 是一个好习惯。

其他

  1. 使用 alias 时,目录名后面一定要加"/",不然会认为是个文件。
  2. alias 在使用正则匹配时,location 后 uri 中捕捉到要匹配的内容后,并在指定的 alias 规则内容处使用。
location ~ ^/users/(.+\.(?:gif|jpe?g|png))$ {
    alias /data/w3/images/$1;
}
  1. alias 只能位于 location 块中,而 root 的权限不限于 location。

举个例子

你提供地址,当你在家时:

1.朋友想去找你,看到阿姨,阿姨说在家,那朋友得到的是你提供的地址 + 阿姨说的,这就是 root,会把两个地址串起来;
2.班主任想去找你,看到阿姨,阿姨说,你直接跟我说就好了,那班主任得到的就是阿姨说的,这就是 alias,会把 location 后面配置的路径弃掉,把当前匹配到的目录指向到指定目录;

当你不在家时 (动静分离):

1.(alias) 班主任找你的结果不变,结果依然是以阿姨说的为主;
2.(root) 因为 root 是把连个地址串一起的原因,这种情况不太适用,如果非要用 root,要在阿姨身上加一个电话,直接 call 你(软 link)

上传文件

既然可以访问了,那就写个简单的 HTML 上传文件吧,关于后端的话,本来想用php,毕竟这块网上例子很多,比如这里,都有源码了;

但是呢,毕竟不懂php,即使它是最棒的语言,考虑到后面维护麻烦,还是选择用flask吧;

直接通过 pip 命令进行安装即可:

pip install flask
官方的一个最简单示例:

# coding=utf-8
from flask import Flask

app = Flask(__name__)


@app.route("/")
def hello():
    return "Hello Flask!"


if __name__ == "__main__":
    app.run()
    #如果想在公网访问,就修改如下:
    #app.run((host='0.0.0.0',port=5000,debug=True))
    #直接打开ip:5000就可以了,记得开放端口权限

image.png-21.7kB

这里不会详细介绍 flask,感兴趣的同学可以來官网看看;

对于简单的上传,一般只需要 3 个步骤:

1. 创建上传表单

<form method="POST" enctype="multipart/form-data">
      <input type="file" name="file">
      <input type="submit" value="Upload">
</form>

2. 获取文件

当点击上传/提交按钮,要获取到上传的文件,通过 requests 对象中的 files 就可以获取到啦~

file = request.files['file']

3. 保存文件
获取到文件,接着就是保存了,指定路径和文件名;

file.save(path + filename)

配置文件

实际在上传文件的时候,会做下限制,比如限制文件大小、文件夹地址、上传文件扩展名等,而在实际项目,还会有密钥、数据库地址等等,这些都是属于配置项;

一般有 3 种方式:

直接写入脚本

当你脚本是轻量,配置项不多的情况下,可以直接写到脚本里面;

from flask import Flask

app = Flask(__name__)
app.config['name'] = 'jb'
app.config['DEBUG'] = True
app.config['age'] = 18

当然也可以用字典来简化代码:

from flask import Flask

app = Flask(__name__)
app.config.update(
    DEBUG=True,
    name='jb',
    age=18
)

单独配置文件

这种适用于配置项多离的情况,可以创建一个独立的配置文件,如config.py

name = 'jb'
DEBUG = True
age = 18

然后导入配置:

import config

...
app = Flask(__name__)
app.config.from_object(config)
...

或者:

...
app = Flask(__name__)
app.config.from_pyfile('config.py')
...

不同的配置类

当需要多个配置配合,比如测试配置、开发配置、运营配置,这时候就需要在配置文件中创建不同的配置类,然后在创建程序实例时引入相应的配置类;

这里继续以config.py为例子,创建一个存储通用配置的类;

import os
basedir = os.path.abspath(os.path.dirname(__file__))


class BaseConfig:  # 基本配置类
    SECRET_KEY = os.getenv('SECRET_KEY', 'some secret words')
    ITEMS_PER_PAGE = 10


class DevelopmentConfig(BaseConfig):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.getenv('DEV_DATABASE_URL', 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')


class TestingConfig(BaseConfig):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.getenv('TEST_DATABASE_URL', 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
    WTF_CSRF_ENABLED = False


config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,

    'default': DevelopmentConfig
}

这里说明下,上面是把配置写入系统环境变量,然后使用 os 模块的 getenv() 方法获取,第二个参数作为默认值;

通过 from_object() 方法导入配置:

from config import config  # 导入存储配置的字典

...
app = Flask(__name__)
app.config.from_object(config['development'])  # 获取相应的配置类
...

那回到这次的功能上,我们只需要写到脚本里面即可;

app.config['UPLOAD_FOLDER'] = os.getcwd()
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024

当然还要考虑安全问题,如文件名校验之类的,具体的话,看源码:

import os
from flask import Flask, request, url_for, send_from_directory
from werkzeug import secure_filename

# 文件扩展名
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.getcwd()+"/file_upload"  #上传地址
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 #文件大小,单位N


html = '''
    <!DOCTYPE html>
    <title>Upload File</title>
    <h1>图片上传</h1>
    <form method=post enctype=multipart/form-data>
         <input type=file name=file>
         <input type=submit value=上传>
    </form>
    '''

# 检查文件类型
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

# 获取上传后的文件
@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'],
                               filename)

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        file = request.files['file']
        #判断上传文件名
        if file and allowed_file(file.filename):
            # 检查文件名
            filename = secure_filename(file.filename)


            #如果目录不存在则创建
            if not os.path.exists(app.config['UPLOAD_FOLDER']):
                os.mkdir(app.config['UPLOAD_FOLDER'])

            # 保存图片
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))

            # 获取url
            file_url = url_for('uploaded_file', filename=filename)
            return html + '<br><img src=' + file_url + '>'
    return html





if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8087,debug=True)

这里说个小事情,一开始执行上面的代码,会报错:
image.png-42.2kB

网上找了下,原因是在 Subline3 遇到的都是看似空格实则没有空格引起的::

解决方法:
就是打开 subline 的空格制表显示就可以清楚的显示出自己是否真的空格了。

第一次遇到这问题,详情请点击这里查看;

执行脚本,上传文件,上传的文件就是在file_upload目录下的;
image.png-10.1kB

功能是有的,就是界面 low 了点。。
image.png-6.5kB

身为一个测试同学,对 UI 肯定要有点追求,并且希望可以提供下载,因此就上网找了个插件,点击这里,看看 github 上的截图:
image.png-388kB

听漂亮的,这样上传就有了进度,并且支持多文件上传,但依然会有个问题,上传完去哪里看?想下载怎么办?
image.png-6.8kB

这里想说一个问题,上面的例子,如果大家有细心看的话,会发现上传的文件都会去到一个叫file_upload文件夹,上传是没问题的,但是下载就有问题了;

jb 在下载的时候,不管这个下载地址怎么拼,页面都会无情报错,说这个地址不存在;

但是呢,如果把上传目录修改成static,却是正常的;
包括如果这个static不在根目录,也一样有问题,那为什么当static是根目录且放到这里面就这样?

Flask 资源定位是依靠

app = Flask(__name__)

__name__参数(文件名或包名),所以相对定位一定要基于这个文件路径。

为什么会在 static 文件夹路径下会正确?Flask 默认静态文件在 static 文件下。

那如果一定要上传到file_upload文件夹,怎么办?

那就修改的 flask 默认的 static 文件夹只需要在创建 Flask 实例的时候,把static_folderstatic_url_path参数设置为空字符串即可。

app = Flask(__name__, static_folder='', static_url_path='')

访问的时候用 url_for 函数,res 文件夹和 static 文件夹同一级:

url_for('static', filename='res/sheeta.jpg')
最终就成了这样:

<img src ="/file_upload/a.png">
//或者
<img src = "{{ url_for('file_upload', filename = 'a.png') }}>

good,问题搞定了,体验下:
image.png-16.7kB
image.png-10.8kB
image.png-50.6kB

搞定,那接着就是生成二维码;

生成二维码

支持点击打开/下载,那如果是上传 apk/ipa 那就麻烦了,jenkins 那边,所以还是希望能生成一个二维码;

而 Python 生成二维码的话,有 qrcode,同时也需要处理下二维码图片,因此需要安装qrcodepillow

pip install qrcode
pip install pillow

最简单的 demo:

import qrcode

qr = qrcode.QRCode(version=2, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=1)

qr.add_data("我是jb啊~")
qr.make(fit=True)
img = qr.make_image()
img.save("jb_qrcode.png")

这里面是有一些参数的,但是边幅原因,请各自去了解;

这时候就生成一个二维码了,但是呢,上面的 demo 图片太小了,而且想弄个定制化的二维码,怎么破?不细说,直接源码拿走不谢~

# 生成二维码
def get_QRCode(filename):

    #检测目录的方法,不存在则创建
    checkdir(存放二维码目录路径)

    # 初步生成二维码图像
    qr = qrcode.QRCode(
        version=5,
        error_correction=qrcode.constants.ERROR_CORRECT_H,
        box_size=8,
        border=4
    )
    # 二维码存放的内容,可文案,可链接
    qr.add_data("二维码路径")
    qr.make(fit=True)

    # 获得Image实例并把颜色模式转换为RGBA
    img = qr.make_image()
    img = img.convert("RGBA")

    # 打开logo文件
    icon = Image.open("定制的logo")

    # 计算logo的尺寸
    img_w,img_h = img.size
    factor = 4
    size_w = int(img_w / factor)
    size_h = int(img_h / factor)

    # 比较并重新设置logo文件的尺寸
    icon_w,icon_h = icon.size
    if icon_w >size_w:
        icon_w = size_w
    if icon_h > size_h:
        icon_h = size_h
    icon = icon.resize((icon_w,icon_h),Image.ANTIALIAS)

    # 计算logo的位置,并复制到二维码图像中
    w = int((img_w - icon_w)/2)
    h = int((img_h - icon_h)/2)
    icon = icon.convert("RGBA")
    img.paste(icon,(w,h),icon)

    # 保存二维码
    img.save(os.path.join(二维码路径, filename))

可以直接拿来复用,需要修改的也就几个参数,简单便捷,效果图如下:

image.png-29.6kB

其他小系列

在处理过程,遇到一些小问题,简单罗列下;

中文名称被吃了

是这样的,如果一个文件里面有中文,比如消息.jpg,把这个地址塞到二维码,然后扫描,会这样的:

image.png-4.8kB
中文不见了。。如果是英文或字母,都很正常,另外,如果是用浏览器扫描,也都没问题,就微信会这样;

很简单,encode 下就好了;

编码:

from urllib.parse import quote
text = quote(text, 'utf-8')

解码:

from urllib.parse import unquote
text = unquote(text, 'utf-8')

这样后,就能在微信打开啦~

ios 不能直接下载 ipa 包

来到这里,主路径都是通的,二维码也是可以生成的,用 android 手机试下,没问题,可以在线预览图片或者下载文件;

但是用 ios 手机扫描ipa包的链接,结果不会像安卓那样下载 ipa 包安装,而是 load 半天,然后这样显示:
image.png-372.6kB

???这跟剧本不一样啊;

网上找了下,很多这样的例子,这里参考的是这里

大致的步骤就是:

  • 准备 plist、ipa 包、icon、HTMLdemo;
  • plist 上传到 https 地址;
  • HTML 里面加一个点击事件;

原理:

是通过 Safari 解析链接中的"itms-services://"来实现的。

例如:
Iphone Download

Safari 会去读取 installIPA.plist 中的信息,如:iOS 应用的名称、版本、安装地址等。

test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/layer/2.3/layer.js"></script>
    <title>File Manager</title>
</head>
<body>
<h1>File Manager</h1>
<div id="btnContainer">
        <a id="btnA" href="itms-services://?action=download-manifest&url=你的plist地址,必须要https,不然会提示签名无效">
            <span id="btnSpan2">v1.1.2</span>
        </a>
 </div>
</html>

这里的 href 是填入plist的地址,必须要https,原因是iOS7.1 以后, Apple 不再支持HTTP方式的 OTA ,所以需要开启HTTPS服务,不然会提示无效证书;

test.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>包名</string>
          <key>bundle-version</key>
          <string>版本号</string>
          <key>kind</key>
          <string>software</string>
          <key>subtitle</key>
          <string>应用名称</string>
          <key>title</key>
          <string>应用名称</string>
        </dict>
      </dict>
    </array>
  </dict>
</plist>

因篇幅问题,只留必备项,只输入填入ipa包地址,底部那块元数据信息即可;

最终的结果就是这样啦:
image.png-175.5kB

主流程总算通了,深呼一口气;

这里说明下,如果没有 https,测试的话,可以试试上传的github,这里是 https 的;
image.png-25.5kB

当长期考虑,还是要弄一个,公司有就用公司的,公司没有就自己买一个,jb 是用 aly,所以也在 aly 买了个,有兴趣的同学点击这里 ,购买、认证、解析、申请证书,就可以了,这块不说明,感兴趣的可自行上网查询,

这里演示的是 demo,因此 url 都是 hardcore 的,实际还要处理plist的路径等,上传一个ipa就生成一个plist,这块自行处理吧;

源码

链接:https://pan.baidu.com/s/1SFFtGJHmUHhpqq2MwwhpXw
提取码:2hdd

源码在此,就不再单独解释了,可能会有一些问题,但是模型大致就这样啦,因时间问题,年后再优化,反正就是缺什么就 import 什么就好了;

小结

本文折腾很久,主要是因为重新看回flask跟学习下ng,以及 ios 的解决方案,外加年底工作很繁忙,因此陆陆续续折腾了快半个月的时间,本来还想把所有优化都做好再放出来,但怕开年后更加忙了,那这文章就烂尾了,因此就先发出来了;

本文涉及到的内容比较多,包括jenkins如何显示图片pgy分发平台的使用自己搭一个文件上传的轮子,涉及到的只是有ngflask,都是比较简单的内容,但是是否做过是两回事,从小白的角度出发来落地;

如果有更好的方案,欢迎一起交流~

最后,谢谢大家~
1-140R3154U8.jpg-9kB

这里还有一节,主要是介绍下 jenkins 的内置变量,感兴趣的同学可以看看~

jenkins 内置变量

邮件的配置变量

变量名 说明
${GIT_BRANCH} build 的 Git 分支;
${FILE,path="xxx"} xxx 为指定的文件,文件内容可以在邮件中显示。注意:xxx 是工作区目录的相对路径;
${JOB_DESCRIPTION} 显示项目描述;
${BUILD_NUMBER} 显示当前构建的编号;
${SVN_REVISION} 显示 svn 版本号;
${CAUSE} 显示谁、通过什么渠道触发这次构建;
${CHANGES} 显示上一次构建之后的变化;
${BUILD_ID} 显示当前构建生成的 ID;
${PROJECT_NAME} 显示项目的全名;
${PROJECT_DISPLAY_NAME} 显示项目的显示名称;
${JENKINS_URL} 显示 Jenkins 服务器的 url 地址(可以在系统配置页更改);
${BUILD_LOG_MULTILINE_REGEX} 按正则表达式匹配并显示构建日志;
${BUILD_LOG} 显示最终构建日志;
${PROJECT_URL} 显示项目的 URL 地址;
${BUILD_STATUS} 显示当前构建的状态 (失败、成功等等);
${BUILD_URL} 显示当前构建的 URL 地址;
${CHANGES_SINCE_LAST_SUCCESS} 显示上一次成功构建之后的变化;
${CHANGES_SINCE_LAST_UNSTABLE} 显示显示上一次不稳固或者成功的构建之后的变化;
${ENV} 显示一个环境变量;
${FAILED_TESTS} 如果有失败的测试,显示这些失败的单元测试信息;
${PROJECT_URL} 显示项目的 URL;
${TEST_COUNTS} 显示测试的数量;

环境变量

变量名 说明
BRANCH_NAME 设置为正在构建的分支的名称;
CHANGE_ID 更改 ID,例如拉取请求号;
CHANGE_URL 设置为更改 URL;
CHANGE_TITLE 设置为更改的标题;
CHANGE_AUTHOR 设置为拟议更改的作者的用户名;
CHANGE_AUTHOR_DISPLAY_NAME 设置为作者的人名;
CHANGE_AUTHOR_EMAIL 设置为作者的电子邮件地址;
CHANGE_TARGET 设置为可以合并更改的目标或基本分支;
BUILD_NUMBER 目前的编号,如 “153”;
BUILD_ID 当前版本 ID,与 BUILD_NUMBER 相同;
BUILD_DISPLAY_NAME 当前版本的显示名称;
JOB_NAME 此构建项目的名称,如 “foo”;
JOB_BASE_NAME 此建立项目的名称将剥离文件夹路径,例如 “bar”。
BUILD_TAG jenkins- JOBNAME− {BUILD_NUMBER}的字符串;
EXECUTOR_NUMBER 识别执行此构建的当前执行程序(在同一台计算机的执行程序中)的唯一编号;
NODE_NAME 代理的名称;
NODE_LABELS 空格分隔的节点分配的标签列表;
WORKSPACE 分配给构建作为工作区的目录的绝对路径;
JENKINS_HOME Jenkins 主节点上分配的目录绝对路径存储数据;
JENKINS_URL 完整的 Jenkins 网址,如http://server:port/jenkins/
BUILD_URL 此构建的完整 URL,如http://server:port/jenkins/job/foo/15/
JOB_URL 此作业的完整 URL,如http://server:port/jenkins/ job/foo/
SVN_REVISION Subversion 版本号,当前已被检出到工作区,如 “12345”;
SVN_URL 当前已经检出到工作空间的 Subversion URL;
共收到 10 条回复 时间 点赞

请问一下,iOS 和安卓都可以在 Linux 下 jenkins 构建打包实现以上需求么?貌似 iOS 的 ipa 只能在 mac 机下打包?

猫星人 回复

IOS 打包需要 xcode 組件,所以只能在 Mac 下打包。

81—1 回复

嗯,感谢~

jb #4 · 2019年02月12日 Author
猫星人 回复

打包就找到 mini 当服务器吧,这个真没办法

一个帖子搞这么多表情,也是服了

jb #6 · 2019年02月12日 Author
胖虎 回复

刚认真看了下,刚好 10 个表情,如果给同学在阅读时带来干扰,先说下对不起;
加表情的原因是考虑到全文比较长,阅读起来可能会有疲劳,因此在觉得适当的地方加了点表情,抱歉,如有干扰,抱歉~🙏

jb 回复

没事,一看就是一位有激情的 90 后

很用心 👍

在 CI 里面用额外的代码去生成二维码可能不太优雅,推荐一个命令 myqr ,一个命令就能生成二维码 https://hoxis.github.io/python-myqr.html

llliuyx 回复

谢谢分享。是个便捷的方式,但感觉一般,不支持中文。 reportlab 也不错

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册