├── .gitignore
├── LICENSE
├── README.md
├── config
└── user.config
├── htmls
├── .gitkeep
└── NewT66y.html
├── icons
├── .gitkeep
└── NewT66y
│ └── Payload
│ └── caoliu.app
│ └── AppIcon60x60@2x.png
├── ipas
├── .gitkeep
└── Target.ipa
├── plists
├── .gitkeep
└── NewT66y.plist
├── template.html
├── template.plist
└── upload.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Alone_Monkey
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OnlineIpaInstall
2 | Fast upload ipa online to install
3 |
4 | # Usage
5 |
6 | Set config in `config/user.config` file
7 |
8 | ```
9 | {
10 | //七牛的ak和sk
11 | "access_key" : "xxxxx",
12 | "secret_key" : "xxxxx",
13 | //七牛的bucket名字和链接
14 | "bucket_name" : "xxxxx",
15 | "bucket_url" : "xxxxx",
16 | //当前repo的文件访问地址
17 | "repo_url" : "https://raw.githubusercontent.com/AloneMonkey/OnlineIpaInstall/master/"
18 | }
19 | ```
20 |
21 | ```
22 | ➜ OnlineIpaInstall git:(master) ✗ ./upload.py ipas/Target.ipa
23 | [NewT66y.ipa] uploading......
24 | [NewT66y-AppIcon60x60@2x.png] uploading......
25 | [NewT66y.html] uploading......
26 | Please push local branch to remote, Then open http://o9slcszsf.bkt.clouddn.com/NewT66y.html install
27 | ➜ OnlineIpaInstall git:(master) ✗ git add .
28 | ➜ OnlineIpaInstall git:(master) ✗ git commit -m "[new] add t66y"
29 | [master 917abb5] [new] add t66y
30 | 7 files changed, 130 insertions(+), 27 deletions(-)
31 | create mode 100644 htmls/NewT66y.html
32 | create mode 100644 icons/NewT66y/Payload/caoliu.app/AppIcon60x60@2x.png
33 | create mode 100644 ipas/Target.ipa
34 | create mode 100644 plists/NewT66y.plist
35 | ➜ OnlineIpaInstall git:(master) git push origin master
36 | Counting objects: 16, done.
37 | Delta compression using up to 4 threads.
38 | Compressing objects: 100% (12/12), done.
39 | Writing objects: 100% (16/16), 550.63 KiB | 22.02 MiB/s, done.
40 | Total 16 (delta 1), reused 0 (delta 0)
41 | remote: Resolving deltas: 100% (1/1), completed with 1 local object.
42 | To https://github.com/AloneMonkey/OnlineIpaInstall.git
43 | 5e1f254..917abb5 master -> master
44 | ➜ OnlineIpaInstall git:(master) open http://o9slcszsf.bkt.clouddn.com/NewT66y.html
45 | ```
46 |
47 | # issues
48 |
49 | Q: html文件上传失败
50 |
51 | A: 需要先实名认证才能上传html
--------------------------------------------------------------------------------
/config/user.config:
--------------------------------------------------------------------------------
1 | {
2 | "access_key" : "xxxxx",
3 | "secret_key" : "xxxxx",
4 | "bucket_name" : "xxxxx",
5 | "bucket_url" : "xxxxx",
6 | "repo_url" : "https://raw.githubusercontent.com/AloneMonkey/OnlineIpaInstall/master/"
7 | }
--------------------------------------------------------------------------------
/htmls/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AloneMonkey/OnlineIpaInstall/de64ac0a897e73cc259cd6aff100a15734802845/htmls/.gitkeep
--------------------------------------------------------------------------------
/htmls/NewT66y.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Online Ipa Install
6 |
39 |
40 |
41 |
42 |

43 |
小草
44 |
2.2.2(Build 1) - 537.94KB
45 |
一键安装
46 |
47 |
48 |
--------------------------------------------------------------------------------
/icons/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AloneMonkey/OnlineIpaInstall/de64ac0a897e73cc259cd6aff100a15734802845/icons/.gitkeep
--------------------------------------------------------------------------------
/icons/NewT66y/Payload/caoliu.app/AppIcon60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AloneMonkey/OnlineIpaInstall/de64ac0a897e73cc259cd6aff100a15734802845/icons/NewT66y/Payload/caoliu.app/AppIcon60x60@2x.png
--------------------------------------------------------------------------------
/ipas/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AloneMonkey/OnlineIpaInstall/de64ac0a897e73cc259cd6aff100a15734802845/ipas/.gitkeep
--------------------------------------------------------------------------------
/ipas/Target.ipa:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AloneMonkey/OnlineIpaInstall/de64ac0a897e73cc259cd6aff100a15734802845/ipas/Target.ipa
--------------------------------------------------------------------------------
/plists/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AloneMonkey/OnlineIpaInstall/de64ac0a897e73cc259cd6aff100a15734802845/plists/.gitkeep
--------------------------------------------------------------------------------
/plists/NewT66y.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | items
6 |
7 |
8 | assets
9 |
10 |
11 | kind
12 | software-package
13 | url
14 | http://o9slcszsf.bkt.clouddn.com/NewT66y.ipa
15 |
16 |
17 | kind
18 | full-size-image
19 | needs-shine
20 |
21 | url
22 | http://o9slcszsf.bkt.clouddn.com/NewT66y-AppIcon60x60@2x.png
23 |
24 |
25 | kind
26 | full-size-image
27 | needs-shine
28 |
29 | url
30 | http://o9slcszsf.bkt.clouddn.com/NewT66y-AppIcon60x60@2x.png
31 |
32 |
33 | metadata
34 |
35 | bundle-identifier
36 | com.cl.NewT66y.---
37 | bundle-version
38 | 2.2.2
39 | kind
40 | software
41 | subtitle
42 | 小草
43 | title
44 | 小草
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Online Ipa Install
6 |
39 |
40 |
41 |
42 |

43 |
{{ name }}
44 |
{{ version }}(Build {{ build }}) - {{ size }}
45 |
一键安装
46 |
47 |
48 |
--------------------------------------------------------------------------------
/template.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | items
6 |
7 |
8 | assets
9 |
10 |
11 | kind
12 | software-package
13 | url
14 | {{ ipa }}
15 |
16 |
17 | kind
18 | full-size-image
19 | needs-shine
20 |
21 | url
22 | {{ large }}
23 |
24 |
25 | kind
26 | full-size-image
27 | needs-shine
28 |
29 | url
30 | {{ small }}
31 |
32 |
33 | metadata
34 |
35 | bundle-identifier
36 | {{ bundleid }}
37 | bundle-version
38 | {{ version }}
39 | kind
40 | software
41 | subtitle
42 | {{ name }}
43 | title
44 | {{ name }}
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/upload.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | # Author : AloneMonkey
5 | # blog: www.alonemonkey.com
6 |
7 | import os
8 | import sys
9 | import re
10 | import json
11 | import plistlib
12 | import argparse
13 | import zipfile
14 | import shutil
15 | import subprocess
16 | from qiniu import Auth, put_file, etag
17 | import qiniu.config
18 | from jinja2 import Environment, FileSystemLoader
19 |
20 | # Read user config file
21 | script_dir = os.path.dirname(os.path.realpath(__file__))
22 | user_config = os.path.join(script_dir, 'config', 'user.config')
23 |
24 | with open(user_config, 'r') as read:
25 | config = json.load(read)
26 |
27 | q = Auth(config['access_key'], config['secret_key'])
28 |
29 | def render_and_write(template_file, path, context):
30 | env = Environment(loader=FileSystemLoader(os.path.dirname(template_file)))
31 | template = env.get_template(os.path.basename(template_file))
32 | rendered_content = template.render(context)
33 | if not rendered_content:
34 | print("Render returned None - skipping '%s'" % path)
35 | return
36 |
37 | with open(path, 'wb') as out:
38 | out.write(rendered_content.encode('utf-8').strip())
39 |
40 | def get_file_size(filePath):
41 | fsize = os.path.getsize(filePath)
42 | unit = 'unknow'
43 | if fsize < 1024*1024:
44 | fsize = fsize/float(1024)
45 | unit = 'KB'
46 | elif fsize >= 1024*1024 and fsize < 1024*1024*1024:
47 | fsize = fsize/float(1024*1024)
48 | unit = 'MB'
49 | else:
50 | fsize = fsize/float(1024*1024*1024)
51 | unit = 'GB'
52 | return str(round(fsize,2)) + unit
53 |
54 | class IPAParser(object):
55 |
56 | plist_file_rx = re.compile(r'Payload/.+?\.app/Info.plist$')
57 |
58 | def __init__(self, ipa_file_path):
59 | self.ipa_file_path = ipa_file_path
60 | self.zip_obj = zipfile.ZipFile(self.ipa_file_path, 'r')
61 |
62 | def get_files_from_ipa(self, file_regx):
63 | filenames = [file for file in self.zip_obj.namelist() if file_regx.search(file)]
64 | return filenames
65 |
66 | def get_info_plist_data(self):
67 | info_plists = self.get_files_from_ipa(self.plist_file_rx)
68 | if len(info_plists):
69 | info_plist_name = info_plists[0]
70 | info_plist_data = self.zip_obj.read(info_plist_name)
71 | plist_root = plistlib.loads(info_plist_data)
72 | return plist_root
73 | return None
74 |
75 | def main_icon_name(self):
76 | result = []
77 | icon_name = None
78 | plist_root = self.get_info_plist_data()
79 | if 'CFBundleIcons' in plist_root \
80 | and 'CFBundlePrimaryIcon' in plist_root['CFBundleIcons'] \
81 | and 'CFBundleIconFiles' in plist_root['CFBundleIcons']['CFBundlePrimaryIcon']:
82 | result = plist_root['CFBundleIcons']['CFBundlePrimaryIcon']['CFBundleIconFiles']
83 |
84 | if not len(result) and 'CFBundleIconFiles' in plist_root :
85 | result = plist_root['CFBundleIconFiles']
86 |
87 | if len(result):
88 | filter_result = list(filter(lambda x: '@2x' in x, result))
89 | if not len(filter_result):
90 | filter_result = list(filter(lambda x: '60' in x, result))
91 | if len(filter_result) and os.path.splitext(filter_result[0])[1] == '' and (not filter_result[0].endswith('@2x')):
92 | icon_name = filter_result[0] + '@2x'
93 | else:
94 | icon_name = filter_result[0]
95 | else:
96 | icon_name = plist_root['CFBundleIconFile']
97 |
98 | if icon_name and os.path.splitext(icon_name)[1] == '':
99 | icon_name += '.png'
100 |
101 | return icon_name
102 |
103 | def extra_file_to_path(self, filename, path):
104 | if not filename:
105 | return None
106 |
107 | filter_list = list(filter(lambda x : x.endswith(filename) ,self.zip_obj.namelist()))
108 | if len(filter_list):
109 | file_path = filter_list[0]
110 | self.zip_obj.extractall(path, members=[file_path])
111 | return os.path.join(path, file_path)
112 |
113 | return None
114 |
115 | def is_valid_zip_file(self):
116 | return zipfile.is_zipfile(self.ipa_file_path)
117 |
118 | def upload_file_to_qiniu(file_path, file_name):
119 | print("[%s] uploading......" % file_name)
120 | token = q.upload_token(config['bucket_name'], file_name)
121 | ret, _ = put_file(token, file_name, file_path)
122 | assert ret['key'] == file_name
123 | assert ret['hash'] == etag(file_path)
124 | return os.path.join(config['bucket_url'], ret['key'])
125 |
126 | if __name__ == '__main__':
127 | parser = argparse.ArgumentParser(description='ipa-upload-script (by AloneMonkey v1.0)')
128 | parser.add_argument('target', nargs='?', help='The path of target ipa file')
129 | args = parser.parse_args()
130 |
131 | exit_code = 0
132 | failed_code = -1
133 | errors = []
134 |
135 | if not len(sys.argv[1:]):
136 | parser.print_help()
137 | sys.exit(exit_code)
138 |
139 | ipa_file_path = args.target
140 |
141 | if not os.path.exists(ipa_file_path):
142 | print("[%s] is not existed!" % ipa_file_path)
143 | exit(failed_code)
144 |
145 | ipa_parser = IPAParser(ipa_file_path)
146 |
147 | if not ipa_parser.is_valid_zip_file():
148 | print('[%s] is not a valid zip file' % ipa_file_path)
149 | sys.exit(failed_code)
150 |
151 | plist_root = ipa_parser.get_info_plist_data()
152 | bundle_id = plist_root.get('CFBundleIdentifier')
153 | bundle_version = plist_root.get('CFBundleShortVersionString')
154 | bundle_version_build = plist_root.get('CFBundleVersion', 'build')
155 | display_name = plist_root.get('CFBundleDisplayName') or plist_root.get('CFBundleName')
156 | executable_name = plist_root.get('CFBundleExecutable')
157 |
158 | # upload ipa file
159 | upload_ipa_url = upload_file_to_qiniu(ipa_file_path, executable_name + ".ipa")
160 |
161 | icon_name = ipa_parser.main_icon_name()
162 |
163 | upload_icon_url = 'http://o9slcszsf.bkt.clouddn.com/largeImage.png'
164 |
165 | local_icon_path = ipa_parser.extra_file_to_path(icon_name, os.path.join(script_dir, 'icons', executable_name))
166 |
167 | if local_icon_path:
168 | pngcrush_args = ('/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/pngcrush', '-q', '-e', '.fixed', '-revert-iphone-optimizations', local_icon_path)
169 | try:
170 | subprocess.check_call(pngcrush_args)
171 | except subprocess.CalledProcessError as err:
172 | print(err)
173 | shutil.move(os.path.splitext(local_icon_path)[0] + '.fixed', local_icon_path)
174 | upload_icon_url = upload_file_to_qiniu(local_icon_path, executable_name + '-' + icon_name)
175 |
176 | plist_name = executable_name + '.plist'
177 | html_name = executable_name + '.html'
178 |
179 | write_plist_path = os.path.join(script_dir, 'plists', plist_name)
180 | write_html_path = os.path.join(script_dir, 'htmls', html_name)
181 |
182 | render_and_write(os.path.join(script_dir, 'template.plist'), write_plist_path,
183 | {
184 | 'ipa' : upload_ipa_url,
185 | 'large' : upload_icon_url,
186 | 'small' : upload_icon_url,
187 | 'bundleid' : bundle_id,
188 | 'version' : bundle_version,
189 | 'name' : display_name
190 | })
191 |
192 | render_and_write(os.path.join(script_dir, 'template.html'), write_html_path,
193 | {
194 | 'icon' : upload_icon_url,
195 | 'name' : display_name,
196 | 'version' : bundle_version,
197 | 'build' : bundle_version_build,
198 | 'size' : get_file_size(ipa_file_path),
199 | 'plisturl' : os.path.join(config['repo_url'], 'plists', plist_name)
200 | })
201 |
202 | html_url = upload_file_to_qiniu(write_html_path, html_name)
203 |
204 | print("Please push local branch to remote, Then open %s install" % html_url)
205 |
206 |
207 |
208 |
209 |
--------------------------------------------------------------------------------