├── .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 | --------------------------------------------------------------------------------