first commit

This commit is contained in:
yizhaorong
2019-06-30 17:22:53 +08:00
commit e31aff5ffb
71 changed files with 11619 additions and 0 deletions

98
README.md Normal file
View File

@@ -0,0 +1,98 @@
# 安装指南
#### 介绍
本项目使用 Spring Boot 开发的,用于企业内网 APP 分发,解决下载限制,实名认证等繁琐过程。
#### 效果
样式与 fir 一致,直接扒的,未进行无用样式清理。
![首页](images/index.jpg)
![首页](images/list.jpg)
![首页](images/install.jpg)
#### 安装教程
项目使用 JAVA 开发,需要 JDK 1.8 运行环境,数据库使用的是 Mysql需要安装 Mysql。JDK 安装直接找网上教程。
##### 数据库
> Mac 下安装 Mysql
```shell
brew install mysql
# 后台运行 mysql
mysqld &
# 登录 mysql
mysql -u root -p
```
> 建库
```shell
# 创建库
create database app_manager;
```
##### HTTPS 证书
参考 [Spring Boot Https 证书](Spring_Boot_Https_证书.md) 创建证书,本项目使用的是 `pckcs12`,密码使用的是 `123456`,部署项目时证书需要自己创建。
##### 配置
[下载](bin/bin.zip),解压包。
> 配置 HTTPS
将上一步生成的 ca.crt 放入 `/static/crt/` 目录中,替换掉里面的 ca.crt将上一步生成的 `server.pckcs12` 文件替换掉包中的原有文件。
如果生成的证书密码不是 `123456`,需要修改`/config/application.properties` 中的 `server.ssl.key-store-password`字段的值为自已设定的密码
> 修改域名
使用文本编辑器打开 `/config/application.properties`,将 `server.domain`字段修改为部署服务器的 IP 或域名。
#### 部署
本项目使用的是 80 和 443 端口,确保端口未被占用。
> 启动服务
```shell
java -jar intranet_app_manager-1.0.0.jar
```
服务启动后即可输入你的 IP 或域名来访问。
> 上传与安装
可以将 ipa 或 apk 拖入上传块中进行上传上传完成后会在列表中展示。iOS 安装需要使用 https 协议,由于内网部署是用的自建证书,需要将 ca 添加到设备的信用列表中才可正常进行安装。
#### Jenkins 集成
集成会用上 Jenkins 展示 HTML需要在 Jenkins 配置中打开 HTML 展示
![html](images/jenkins.jpg)
> 上传脚本
```shell
# 上传到APP管理平台
result=$(curl -F "file=@$WORKSPACE/build/Ewt360_debug/Ewt360.ipa" http://172.16.241.203/app/upload)
code_url=$(echo $result | sed 's/.*\(http.*\)",.*/\1/g')
echo "code_url="$code_url > $WORKSPACE/code.txt
```
> 注入变量
Properties File Path:`$WORKSPACE/code.txt`
> 展示二维码
Description: `<a href="${code_url}" target="_blank"><img src='${code_url}' height="160" width="160" /></a>`
![shell](images/shell.jpg)
![code](images/code.jpg)

168
Spring_Boot_Https_证书.md Normal file
View File

@@ -0,0 +1,168 @@
# Spring Boot Https 证书
## 创建目录和文件
```shell
mkdir -p CA/{certs,crl,newcerts,private}
touch CA/index.txt
touch CA/certs.db
touch openssl.cnf
echo 00 > CA/serial
```
## 设置配置
> openssl.cnf
```shell
[ req ]
distinguished_name=req_distinguished_name
req_extensions=v3_req
[ req_distinguished_name ]
countryName=Country Name (2 letter code)
countryName_default=CN
stateOrProvinceName=State or Province Name (full name)
stateOrProvinceName_default=ZheJiang
localityName=Locality Name (eg, city)
localityName_default=HangZhou
organizationalUnitName=Organizational Unit Name (eg, section)
organizationalUnitName_default=Domain Control Validated
commonName=Internet Widgits Ltd
commonName_default=192.168.0.*
commonName_max=64
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = 192.168.0.110
DNS.2 = 192.168.0.111
# section for the "default_ca" option
[ca]
default_ca=my_ca_default
# default section for "ca" command options
[my_ca_default]
new_certs_dir=./CA/certs
database=./CA/certs.db
default_md = sha256
policy=my_ca_policy
serial = ./CA/serial
default_days = 365
# section for DN field validation and order
[my_ca_policy]
commonName = supplied
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
emailAddress = optional
```
**注意**
```shell
[ alt_names ]
DNS.1 = 192.168.0.110
DNS.2 = 192.168.0.111
```
这里配置需要部署的域名或 IP 地址列表。
## 创建 CA
### 生成ca.key并自签署
```shell
openssl req -new -x509 -days 3650 -keyout ca.key -out ca.crt -config openssl.cnf
```
## 创建服务器证书
### 生成server.key(名字不重要)
```shell
openssl genrsa -out server.key 2048
```
### 生成证书签名请求
```shell
openssl req -new -key server.key -out server.csr -config openssl.cnf
```
Common Name 这个写主要域名就好了(注意这个域名也要在openssl.cnf的DNS.x里)
### 使用自签署的CA签署server.scr
```shell
openssl ca -in server.csr -out server.crt -cert ca.crt -keyfile ca.key -extensions v3_req -config openssl.cnf
```
## 创建 Spring Boot 所需证书
### 导出 pckcs12格式
```shell
openssl pkcs12 -export -in server.crt -inkey server.key -out server.pkcs12
```
### 导出 jks 格式
```shell
keytool -importkeystore -srckeystore server.pkcs12 -destkeystore server.jks -srcstoretype pkcs12
```
## Spring Boot 配置
```properties
# 证书
server.port=443
server.ssl.key-store=classpath:server.pkcs12
server.ssl.key-store-password=123456
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=1
```
### SpringBootApplication
```java
@Bean
public TomcatServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint constraint = new SecurityConstraint();
constraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
constraint.addCollection(collection);
context.addConstraint(constraint);
}
};
tomcat.addAdditionalTomcatConnectors(httpConnector());
return tomcat;
}
@Bean
public Connector httpConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(9090);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
```

BIN
bin/bin.zip Normal file

Binary file not shown.

41
build.gradle Normal file
View File

@@ -0,0 +1,41 @@
plugins {
id 'org.springframework.boot' version '2.1.6.RELEASE'
id 'java'
}
apply plugin: 'io.spring.dependency-management'
group = 'org.yzr'
version = '1.0.0'
sourceCompatibility = '1.8'
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.16'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
compile group: 'com.googlecode.plist', name: 'dd-plist', version: '1.21'
compile group: 'net.dongliu', name: 'apk-parser', version: '2.6.9'
compile group: 'net.glxn.qrgen', name: 'javase', version: '2.0'
compile group: 'commons-io', name: 'commons-io', version: '2.6'
compile group: 'com.jcraft', name: 'jzlib', version: '1.1.3'
compile group: 'org.freemarker', name: 'freemarker', version: '2.3.28'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Fri Jun 28 17:18:31 CST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

172
gradlew vendored Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

BIN
images/code.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
images/index.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
images/install.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
images/jenkins.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
images/list.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
images/shell.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

6
settings.gradle Normal file
View File

@@ -0,0 +1,6 @@
pluginManagement {
repositories {
gradlePluginPortal()
}
}
rootProject.name = 'intranet_app_manager'

BIN
src/main/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/main/java/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/main/java/org/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/main/java/org/yzr/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,57 @@
package org.yzr;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import javax.annotation.Resource;
@SpringBootApplication
//@EnableJpaRepositories("org.yzr.dao") // JPA扫描该包路径下的Repositorie
//@EntityScan("org.yzr.model") // 扫描Entity实体类
public class Application {
@Resource
private Environment environment;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public TomcatServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint constraint = new SecurityConstraint();
constraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
constraint.addCollection(collection);
context.addConstraint(constraint);
}
};
tomcat.addAdditionalTomcatConnectors(httpConnector());
return tomcat;
}
@Bean
public Connector httpConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
int httpPort = Integer.parseInt(environment.getProperty("server.http.port"));
int httpsPort = Integer.parseInt(environment.getProperty("server.port"));
connector.setScheme("http");
connector.setPort(httpPort);
connector.setSecure(true);
connector.setRedirectPort(httpsPort);
return connector;
}
}

View File

@@ -0,0 +1,45 @@
package org.yzr.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.ResourceUtils;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import javax.annotation.Resource;
@Configuration
public class WebAppConfigurer extends WebMvcConfigurationSupport {
@Resource
private Environment environment;
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
try {
String path = ResourceUtils.getURL("").getPath();
String prefix = ResourceUtils.FILE_URL_PREFIX + path;
String debug = environment.getProperty("config.debug");
if (debug !=null && debug.equals("debug")) {
prefix = "classpath:/";
}
registry.addResourceHandler("/android/**").addResourceLocations(prefix+ "static/upload/android/");
registry.addResourceHandler("/ios/**").addResourceLocations(prefix + "static/upload/ios/");
registry.addResourceHandler("/crt/**").addResourceLocations(prefix + "static/crt/");
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
registry.addResourceHandler("/images/**").addResourceLocations("classpath:/static/images/");
super.addResourceHandlers(registry);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController( "/" ).setViewName( "forward:/apps" );
super.addViewControllers(registry);
}
}

View File

@@ -0,0 +1,61 @@
package org.yzr.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.yzr.service.AppService;
import org.yzr.utils.PathManager;
import org.yzr.vo.AppViewModel;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
public class AppController {
@Resource
private AppService appService;
@Resource
private PathManager pathManager;
@GetMapping("/apps")
public String apps(HttpServletRequest request) {
try{
List<AppViewModel> apps = this.appService.findAll();
request.setAttribute("apps", apps);
request.setAttribute("baseURL", this.pathManager.getBaseURL(false));
} catch (Exception e) {
e.printStackTrace();
}
return "index";
}
@GetMapping("/apps/{appID}")
public String getAppById(@PathVariable("appID") String appID, HttpServletRequest request) {
AppViewModel appViewModel = this.appService.getById(appID);
request.setAttribute("package", appViewModel);
request.setAttribute("apps", appViewModel.getPackageList());
return "list";
}
@RequestMapping("/app/delete/{id}")
@ResponseBody
public Map<String, Object> deleteById(@PathVariable("id") String id) {
Map<String, Object> map = new HashMap<>();
try {
this.appService.deleteById(id);
map.put("success", true);
} catch (Exception e) {
map.put("success", false);
}
return map;
}
}

View File

@@ -0,0 +1,26 @@
package org.yzr.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Controller
public class ErrorController implements org.springframework.boot.web.servlet.error.ErrorController {
/**
* 所有错误都转到首页
* @param request
* @param response
* @throws Exception
*/
@RequestMapping("/error")
public void handleError(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.sendRedirect("/apps");
}
@Override
public String getErrorPath() {
return "/error";
}
}

View File

@@ -0,0 +1,194 @@
package org.yzr.controller;
import net.glxn.qrgen.javase.QRCode;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.yzr.model.App;
import org.yzr.model.Package;
import org.yzr.service.AppService;
import org.yzr.service.PackageService;
import org.yzr.utils.PathManager;
import org.yzr.utils.ipa.PlistGenerator;
import org.yzr.vo.AppViewModel;
import org.yzr.vo.PackageViewModel;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Controller
public class PackageController {
@Resource
private AppService appService;
@Resource
private PackageService packageService;
@Resource
private PathManager pathManager;
/**
* 预览页
* @param code
* @param request
* @return
*/
@GetMapping("/s/{code}")
public String get(@PathVariable("code") String code, HttpServletRequest request) {
String id = request.getParameter("id");
AppViewModel viewModel = this.appService.findByCode(code, id);
request.setAttribute("app", viewModel);
request.setAttribute("ca_path", this.pathManager.getCAPath());
return "install";
}
/**
* 上传包
* @param file
* @param request
* @return
*/
@RequestMapping("/app/upload")
@ResponseBody
public Map<String, Object> upload(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
try {
String filePath = transfer(file);
Package aPackage = this.packageService.buildPackage(filePath);
App app = this.appService.getByPackage(aPackage);
app.getPackageList().add(aPackage);
app.setCurrentPackage(aPackage);
aPackage.setApp(app);
app = this.appService.save(app);
// URL
String codeURL = this.pathManager.getBaseURL(false) + "p/code/" + app.getCurrentPackage().getId();
map.put("code", codeURL);
map.put("success", true);
} catch (Exception e) {
map.put("success", false);
e.printStackTrace();
}
return map;
}
/**
* 下载文件源文件(ipa 或 apk)
* @param id
* @param response
*/
@RequestMapping("/p/{id}")
public void download(@PathVariable("id") String id, HttpServletResponse response) {
try {
Package aPackage = this.packageService.get(id);
String path = PathManager.getFullPath(aPackage) + aPackage.getFileName();
File file = new File(path);
if(file.exists()){ //判断文件父目录是否存在
response.setContentType("application/force-download");
// 文件名称转换
String fileName = aPackage.getName() + "_" + aPackage.getVersion();
String ext = "." + FilenameUtils.getExtension(aPackage.getFileName());
String appName = new String(fileName.getBytes("UTF-8"), "iso-8859-1");
response.setHeader("Content-Disposition", "attachment;fileName=" + appName + ext);
byte[] buffer = new byte[1024];
OutputStream os = response.getOutputStream();
FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis);
int i = bis.read(buffer);
while(i != -1){
os.write(buffer);
i = bis.read(buffer);
}
bis.close();
fis.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取 manifest
* @param id
* @param response
*/
@RequestMapping("/m/{id}")
public void getManifest(@PathVariable("id") String id, HttpServletResponse response) {
try {
PackageViewModel viewModel = this.packageService.findById(id);
if (viewModel != null && viewModel.isIOS()) {
response.setContentType("application/force-download");
response.setHeader("Content-Disposition", "attachment;fileName=manifest.plist");
Writer writer = new OutputStreamWriter(response.getOutputStream());
PlistGenerator.generate(viewModel, writer);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取包二维码
* @param id
* @param response
*/
@RequestMapping("/p/code/{id}")
public void getQrCode(@PathVariable("id") String id, HttpServletResponse response) {
try {
PackageViewModel viewModel = this.packageService.findById(id);
if (viewModel != null) {
response.setContentType("image/png");
QRCode.from(viewModel.getPreviewURL()).withSize(250, 250).writeTo(response.getOutputStream());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 删除包
* @param id
* @return
*/
@RequestMapping("/p/delete/{id}")
@ResponseBody
public Map<String, Object> deleteById(@PathVariable("id") String id) {
Map<String, Object> map = new HashMap<>();
try {
this.packageService.deleteById(id);
map.put("success", true);
} catch (Exception e) {
map.put("success", false);
}
return map;
}
/**
* 转存文件
* @param srcFile
* @return
*/
private String transfer(MultipartFile srcFile) {
try {
// 获取文件后缀
String fileName = srcFile.getOriginalFilename();
String ext = FilenameUtils.getExtension(fileName);
// 生成文件名
String newFileName = UUID.randomUUID().toString() + "." + ext;
// 转存到 tmp
String destPath = FileUtils.getTempDirectory() + newFileName;
srcFile.transferTo(new File(destPath));
return destPath;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View File

@@ -0,0 +1,19 @@
package org.yzr.dao;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.yzr.model.App;
public interface AppDao extends CrudRepository<App, String> {
@Query("select a from App a where a.bundleID=:bundleID and a.platform=:platform")
public App get(@Param("bundleID") String bundleID, @Param("platform") String platform);
@Query("select a from App a where a.shortCode=:shortCode")
public App findByShortCode(@Param("shortCode") String shortCode);
@Override
@Query("select a from App a order by a.currentPackage.createTime desc ")
Iterable<App> findAll();
}

View File

@@ -0,0 +1,8 @@
package org.yzr.dao;
import org.springframework.data.repository.CrudRepository;
import org.yzr.model.Package;
public interface PackageDao extends CrudRepository <Package, String > {
}

View File

@@ -0,0 +1,44 @@
package org.yzr.model;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.List;
@Entity
@Table(name = "tb_app", uniqueConstraints = {@UniqueConstraint(columnNames={"platform", "bundleID"})})
@Setter
@Getter
public class App {
// 主键
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid")
@Column(length = 32)
private String id;
// APP ID
private String bundleID;
// 应用名称
private String name;
// 短链接码
@Column(unique = true)
private String shortCode;
// 平台
private String platform;
// 简介
private String description;
// 创建时间
private long createTime;
// 包列表
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "app")
private List<Package> packageList;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
// 当前包
@JoinColumn(name = "currentID",referencedColumnName = "id")
private Package currentPackage;
}

View File

@@ -0,0 +1,45 @@
package org.yzr.model;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.ManyToAny;
import javax.persistence.*;
@Entity
@Table(name="tb_package")
@Setter
@Getter
public class Package {
// 主键
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid")
@Column(length = 32)
private String id;
// 应用ID
private String bundleID;
// 名称
private String name;
// 版本
private String version;
// 构建版本
private String buildVersion;
// 创建时间
private long createTime;
// 包大小
private long size;
// 最低支持版本
private String minVersion;
// 平台(Android 或 iOS)
private String platform;
// 文件名
private String fileName;
@ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
@JoinColumn(name="appId")
private App app;
}

View File

@@ -0,0 +1,105 @@
package org.yzr.service;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.yzr.dao.AppDao;
import org.yzr.model.App;
import org.yzr.model.Package;
import org.yzr.utils.CodeGenerator;
import org.yzr.utils.PathManager;
import org.yzr.vo.AppViewModel;
import org.yzr.vo.PackageViewModel;
import javax.annotation.Resource;
import javax.transaction.Transactional;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class AppService {
@Resource
private AppDao appDao;
@Resource
private PathManager pathManager;
@Transactional
public App save(App app) {
App app1 = this.appDao.save(app);
app1.getCurrentPackage();
return app1;
}
@Transactional
public List<AppViewModel> findAll() {
Iterable<App> apps = this.appDao.findAll();
List<AppViewModel> list = new ArrayList<>();
for (App app : apps) {
AppViewModel appViewModel = new AppViewModel(app, this.pathManager, false);
list.add(appViewModel);
}
return list;
}
@Transactional
public AppViewModel getById(String appID) {
Optional<App> optionalApp = this.appDao.findById(appID);
App app = optionalApp.get();
if (app != null) {
app.getPackageList().forEach(aPackage -> {});
AppViewModel appViewModel = new AppViewModel(app, this.pathManager, true);
return appViewModel;
}
return null;
}
@Transactional
public App getByPackage(Package aPackage) {
App app = this.appDao.get(aPackage.getBundleID(), aPackage.getPlatform());
if (app == null) {
app = new App();
String shortCode = CodeGenerator.generate(4);
while (this.appDao.findByShortCode(shortCode) != null) {
shortCode = CodeGenerator.generate(4);
}
BeanUtils.copyProperties(aPackage, app);
app.setShortCode(shortCode);
} else {
app.setName(aPackage.getName());
// 触发级联查询
app.getPackageList().forEach(p->{});
}
if (app.getPackageList() == null) {
app.setPackageList(new ArrayList<>());
}
return app;
}
@Transactional
public void deleteById(String id) {
App app = this.appDao.findById(id).get();
if (app != null) {
this.appDao.deleteById(id);
// 消除整个 APP 目录
String path = PathManager.getAppPath(app);
PathManager.deleteDirectory(path);
}
}
/**
* 通过 code 和 packageId 查询
* @param code
* @param packageId
* @return
*/
@Transactional
public AppViewModel findByCode(String code, String packageId) {
App app = this.appDao.findByShortCode(code);
AppViewModel viewModel = new AppViewModel(app, pathManager, packageId);
return viewModel;
}
}

View File

@@ -0,0 +1,84 @@
package org.yzr.service;
import lombok.SneakyThrows;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Service;
import org.yzr.dao.AppDao;
import org.yzr.dao.PackageDao;
import org.yzr.model.App;
import org.yzr.model.Package;
import org.yzr.utils.PathManager;
import org.yzr.utils.ipa.PlistGenerator;
import org.yzr.utils.parser.ParserClient;
import org.yzr.vo.PackageViewModel;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.transaction.Transactional;
import java.io.File;
@Service
public class PackageService {
@Resource
private PackageDao packageDao;
@Resource
private PathManager pathManager;
public Package buildPackage(String filePath) {
Package aPackage = ParserClient.parse(filePath);
try {
String fileName = aPackage.getPlatform() + "." + FilenameUtils.getExtension(filePath);
// 更新文件名
aPackage.setFileName(fileName);
String packagePath = PathManager.getFullPath(aPackage);
String tempIconPath = PathManager.getTempIconPath(aPackage);
String iconPath = packagePath + File.separator + "icon.png";
String sourcePath = packagePath + File.separator + fileName;
// 拷贝图标
FileUtils.copyFile(new File(tempIconPath), new File(iconPath));
// 源文件
FileUtils.copyFile(new File(filePath), new File(sourcePath));
// 删除临时图标
FileUtils.forceDelete(new File(tempIconPath));
// 源文件
FileUtils.forceDelete(new File(filePath));
} catch (Exception e) {
e.printStackTrace();
}
return aPackage;
}
@Transactional
public Package save(Package aPackage) {
return this.packageDao.save(aPackage);
}
@Transactional
public Package get(String id) {
Package aPackage = this.packageDao.findById(id).get();
return aPackage;
}
@Transactional
public PackageViewModel findById(String id) {
Package aPackage = this.packageDao.findById(id).get();
PackageViewModel viewModel = new PackageViewModel(aPackage, this.pathManager);
return viewModel;
}
@Transactional
public void deleteById(String id) {
Package aPackage = this.packageDao.findById(id).get();
if (aPackage != null) {
this.packageDao.deleteById(id);
String path = PathManager.getFullPath(aPackage);
PathManager.deleteDirectory(path);
}
}
}

BIN
src/main/java/org/yzr/utils/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,28 @@
package org.yzr.utils;
import java.util.Random;
public class CodeGenerator {
// 所有编码
private static final String ALL_CODE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
// 随机种子
private static Random random = new Random();
public static String generate(int length) {
if (length < 1) {
return null;
}
StringBuffer code = new StringBuffer();
for (int i = 0; i < length; i++) {
code.append(randomCode());
}
return code.toString();
}
private static char randomCode() {
int count = ALL_CODE.length();
int index = random.nextInt(count) % count;
return ALL_CODE.charAt(index);
}
}

View File

@@ -0,0 +1,450 @@
package org.yzr.utils;
import com.jcraft.jzlib.Deflater;
import com.jcraft.jzlib.GZIPException;
import com.jcraft.jzlib.Inflater;
import com.jcraft.jzlib.JZlib;
import java.io.*;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.SimpleFormatter;
import java.util.logging.StreamHandler;
import java.util.zip.CRC32;
public class PNGConverter {
private final File source;
private final File target;
private ArrayList<PNGTrunk> trunks = null;
/**
* iOS PNG 图标转换
* @param srcPath
* @param destPath
* @throws Exception
*/
public static void convert(String srcPath, String destPath) {
try {
PNGConverter converter = new PNGConverter(new File(srcPath), new File(destPath));
converter.convert();
} catch (Exception e) {
e.printStackTrace();
}
}
private PNGConverter(File source, File target) {
if (source == null) throw new NullPointerException("'source' cannot be null");
if (target == null) throw new NullPointerException("'target' cannot be null");
this.source = source;
this.target = target;
}
private File getTargetFile(File convertedFile) throws IOException {
if (source.isFile()) {
if (target.isDirectory()) {
return new File(target, source.getName());
} else {
return target;
}
} else { // source is a directory
if (target.isFile()) { // single existing target
return target;
} else { // otherwise reconstruct a similar directory structure
if (!target.isDirectory() && !target.mkdirs()) {
throw new IOException("failed to create folder " + target.getAbsolutePath());
}
Path relativeConvertedPath = source.toPath().relativize(convertedFile.toPath());
File targetFile = new File(target, relativeConvertedPath.toString());
File targetFileDir = targetFile.getParentFile();
if (targetFileDir != null && !targetFileDir.exists() && !targetFileDir.mkdirs()) {
throw new IOException("unable to create folder " + targetFileDir.getAbsolutePath());
}
return targetFile;
}
}
}
public void convert() throws IOException {
convert(source);
}
private boolean isPngFileName(File file) {
return file.getName().toLowerCase().endsWith(".png");
}
private PNGTrunk getTrunk(String szName) {
if (trunks == null) {
return null;
}
PNGTrunk trunk;
for (int n = 0; n < trunks.size(); n++) {
trunk = trunks.get(n);
if (trunk.getName().equalsIgnoreCase(szName)) {
return trunk;
}
}
return null;
}
private void convertPngFile(File pngFile, File targetFile) throws IOException {
readTrunks(pngFile);
if (getTrunk("CgBI") != null) {
// Convert data
PNGIHDRTrunk ihdrTrunk = (PNGIHDRTrunk) getTrunk("IHDR");
int nMaxInflateBuffer = 4 * (ihdrTrunk.m_nWidth + 1) * ihdrTrunk.m_nHeight;
byte[] outputBuffer = new byte[nMaxInflateBuffer];
convertDataTrunk(ihdrTrunk, outputBuffer, nMaxInflateBuffer);
writePng(targetFile);
} else {
// Likely a standard PNG: just copy
byte[] buffer = new byte[1024];
int bytesRead;
InputStream inputStream = new FileInputStream(pngFile);
try {
OutputStream outputStream = new FileOutputStream(targetFile);
try {
while ((bytesRead = inputStream.read(buffer)) >= 0) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
} finally {
outputStream.close();
}
} finally {
inputStream.close();
}
}
}
private long inflate(byte[] conversionBuffer, int nMaxInflateBuffer) throws GZIPException {
Inflater inflater = new Inflater(-15);
for (PNGTrunk dataTrunk : trunks) {
if (!"IDAT".equalsIgnoreCase(dataTrunk.getName())) continue;
inflater.setInput(dataTrunk.getData(), true);
}
inflater.setOutput(conversionBuffer);
int nResult;
try {
nResult = inflater.inflate(JZlib.Z_NO_FLUSH);
checkResultStatus(nResult);
} finally {
inflater.inflateEnd();
}
if (inflater.getTotalOut() > nMaxInflateBuffer) {
}
return inflater.getTotalOut();
}
private Deflater deflate(byte[] buffer, int length, int nMaxInflateBuffer) throws GZIPException {
Deflater deflater = new Deflater();
deflater.setInput(buffer, 0, length, false);
int nMaxDeflateBuffer = nMaxInflateBuffer + 1024;
byte[] deBuffer = new byte[nMaxDeflateBuffer];
deflater.setOutput(deBuffer);
deflater.deflateInit(JZlib.Z_BEST_COMPRESSION);
int nResult = deflater.deflate(JZlib.Z_FINISH);
checkResultStatus(nResult);
if (deflater.getTotalOut() > nMaxDeflateBuffer) {
throw new GZIPException("deflater output buffer was too small");
}
return deflater;
}
private void checkResultStatus(int nResult) throws GZIPException {
switch (nResult) {
case JZlib.Z_OK:
case JZlib.Z_STREAM_END:
break;
case JZlib.Z_NEED_DICT:
throw new GZIPException("Z_NEED_DICT - " + nResult);
case JZlib.Z_DATA_ERROR:
throw new GZIPException("Z_DATA_ERROR - " + nResult);
case JZlib.Z_MEM_ERROR:
throw new GZIPException("Z_MEM_ERROR - " + nResult);
case JZlib.Z_STREAM_ERROR:
throw new GZIPException("Z_STREAM_ERROR - " + nResult);
case JZlib.Z_BUF_ERROR:
throw new GZIPException("Z_BUF_ERROR - " + nResult);
default:
throw new GZIPException("inflater error: " + nResult);
}
}
private boolean convertDataTrunk(PNGIHDRTrunk ihdrTrunk, byte[] conversionBuffer, int nMaxInflateBuffer)
throws IOException {
long inflatedSize = inflate(conversionBuffer, nMaxInflateBuffer);
// Switch the color
int nIndex = 0;
byte nTemp;
for (int y = 0; y < ihdrTrunk.m_nHeight; y++) {
nIndex++;
for (int x = 0; x < ihdrTrunk.m_nWidth; x++) {
nTemp = conversionBuffer[nIndex];
conversionBuffer[nIndex] = conversionBuffer[nIndex + 2];
conversionBuffer[nIndex + 2] = nTemp;
nIndex += 4;
}
}
Deflater deflater = deflate(conversionBuffer, (int) inflatedSize, nMaxInflateBuffer);
// Put the result in the first IDAT chunk (the only one to be written out)
PNGTrunk firstDataTrunk = getTrunk("IDAT");
CRC32 crc32 = new CRC32();
crc32.update(firstDataTrunk.getName().getBytes());
crc32.update(deflater.getNextOut(), 0, (int) deflater.getTotalOut());
long lCRCValue = crc32.getValue();
firstDataTrunk.m_nData = deflater.getNextOut();
firstDataTrunk.m_nCRC[0] = (byte) ((lCRCValue & 0xFF000000) >> 24);
firstDataTrunk.m_nCRC[1] = (byte) ((lCRCValue & 0xFF0000) >> 16);
firstDataTrunk.m_nCRC[2] = (byte) ((lCRCValue & 0xFF00) >> 8);
firstDataTrunk.m_nCRC[3] = (byte) (lCRCValue & 0xFF);
firstDataTrunk.m_nSize = (int) deflater.getTotalOut();
return false;
}
private void writePng(File newFileName) throws IOException {
FileOutputStream outStream = new FileOutputStream(newFileName);
try {
byte[] pngHeader = {-119, 80, 78, 71, 13, 10, 26, 10};
outStream.write(pngHeader);
boolean dataWritten = false;
for (PNGTrunk trunk : trunks) {
// Skip Apple specific and misplaced CgBI chunk
if (trunk.getName().equalsIgnoreCase("CgBI")) {
continue;
}
// Only write the first IDAT chunk as they have all been put together now
if ("IDAT".equalsIgnoreCase(trunk.getName())) {
if (dataWritten) {
continue;
} else {
dataWritten = true;
}
}
trunk.writeToStream(outStream);
}
outStream.flush();
} finally {
outStream.close();
}
}
private void readTrunks(File pngFile) throws IOException {
DataInputStream input = new DataInputStream(new FileInputStream(pngFile));
try {
byte[] nPNGHeader = new byte[8];
input.readFully(nPNGHeader);
boolean bWithCgBI = false;
trunks = new ArrayList<PNGTrunk>();
if ((nPNGHeader[0] == -119) && (nPNGHeader[1] == 0x50) && (nPNGHeader[2] == 0x4e) && (nPNGHeader[3] == 0x47)
&& (nPNGHeader[4] == 0x0d) && (nPNGHeader[5] == 0x0a) && (nPNGHeader[6] == 0x1a) && (nPNGHeader[7] == 0x0a)) {
PNGTrunk trunk;
do {
trunk = PNGTrunk.generateTrunk(input);
trunks.add(trunk);
if (trunk.getName().equalsIgnoreCase("CgBI")) {
bWithCgBI = true;
}
}
while (!trunk.getName().equalsIgnoreCase("IEND"));
}
} finally {
input.close();
}
}
private void convertDirectory(File dir) throws IOException {
for (File file : dir.listFiles()) {
convert(file);
}
}
private void convert(File sourceFile) throws IOException {
if (sourceFile.isDirectory()) {
convertDirectory(sourceFile);
} else if (isPngFileName(sourceFile)) {
File targetFile = getTargetFile(sourceFile);
convertPngFile(sourceFile, targetFile);
}
}
public static void main(String[] args) throws Exception {
SimpleFormatter fmt = new SimpleFormatter();
StreamHandler sh = new StreamHandler(System.out, fmt);
sh.setLevel(Level.FINE);
new PNGConverter(new File(args[0]), new File(args[1])).convert();
}
}
class PNGTrunk {
protected int m_nSize;
protected String m_szName;
protected byte[] m_nData;
protected byte[] m_nCRC;
public static PNGTrunk generateTrunk(DataInputStream input) throws IOException {
int nSize = readPngInt(input);
byte[] nData = new byte[4];
input.readFully(nData);
String szName = new String(nData, "ASCII");
byte[] nDataBuffer = new byte[nSize];
input.readFully(nDataBuffer);
byte[] nCRC = new byte[4];
input.readFully(nCRC);
if (szName.equalsIgnoreCase("IHDR")) {
return new PNGIHDRTrunk(nSize, szName, nDataBuffer, nCRC);
}
return new PNGTrunk(nSize, szName, nDataBuffer, nCRC);
}
protected PNGTrunk(int nSize, String szName, byte[] nCRC) {
m_nSize = nSize;
m_szName = szName;
m_nCRC = nCRC;
}
protected PNGTrunk(int nSize, String szName, byte[] nData, byte[] nCRC) {
this(nSize, szName, nCRC);
m_nData = nData;
}
public int getSize() {
return m_nSize;
}
public String getName() {
return m_szName;
}
public byte[] getData() {
return m_nData;
}
public byte[] getCRC() {
return m_nCRC;
}
public void writeToStream(FileOutputStream outStream) throws IOException {
byte nSize[] = new byte[4];
nSize[0] = (byte) ((m_nSize & 0xFF000000) >> 24);
nSize[1] = (byte) ((m_nSize & 0xFF0000) >> 16);
nSize[2] = (byte) ((m_nSize & 0xFF00) >> 8);
nSize[3] = (byte) (m_nSize & 0xFF);
outStream.write(nSize);
outStream.write(m_szName.getBytes("ASCII"));
outStream.write(m_nData, 0, m_nSize);
outStream.write(m_nCRC);
}
public static void writeInt(byte[] nDes, int nPos, int nVal) {
nDes[nPos] = (byte) ((nVal & 0xff000000) >> 24);
nDes[nPos + 1] = (byte) ((nVal & 0xff0000) >> 16);
nDes[nPos + 2] = (byte) ((nVal & 0xff00) >> 8);
nDes[nPos + 3] = (byte) (nVal & 0xff);
}
public static int readPngInt(DataInputStream input) throws IOException {
final byte[] buffer = new byte[4];
input.readFully(buffer);
return readInt(buffer, 0);
}
public static int readInt(byte[] nDest, int nPos) { //读一个int
return ((nDest[nPos++] & 0xFF) << 24)
| ((nDest[nPos++] & 0xFF) << 16)
| ((nDest[nPos++] & 0xFF) << 8)
| (nDest[nPos] & 0xFF);
}
public static void writeCRC(byte[] nData, int nPos) {
int chunklen = readInt(nData, nPos);
int sum = CRCChecksum(nData, nPos + 4, 4 + chunklen) ^ 0xffffffff;
writeInt(nData, nPos + 8 + chunklen, sum);
}
public static int[] crc_table = null;
public static int CRCChecksum(byte[] nBuffer, int nOffset, int nLength) {
int c = 0xffffffff;
int n;
if (crc_table == null) {
int mkc;
int mkn, mkk;
crc_table = new int[256];
for (mkn = 0; mkn < 256; mkn++) {
mkc = mkn;
for (mkk = 0; mkk < 8; mkk++) {
if ((mkc & 1) == 1) {
mkc = 0xedb88320 ^ (mkc >>> 1);
} else {
mkc = mkc >>> 1;
}
}
crc_table[mkn] = mkc;
}
}
for (n = nOffset; n < nLength + nOffset; n++) {
c = crc_table[(c ^ nBuffer[n]) & 0xff] ^ (c >>> 8);
}
return c;
}
}
class PNGIHDRTrunk extends PNGTrunk {
public int m_nWidth;
public int m_nHeight;
public PNGIHDRTrunk(int nSize, String szName, byte[] nData, byte[] nCRC) {
super(nSize, szName, nData, nCRC);
m_nWidth = readInt(nData, 0);
m_nHeight = readInt(nData, 4);
}
}

View File

@@ -0,0 +1,166 @@
package org.yzr.utils;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.net.InetAddress;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.util.ResourceUtils;
import org.yzr.model.App;
import org.yzr.model.Package;
import javax.annotation.Resource;
@Component
public class PathManager {
@Resource
private Environment environment;
private String httpsBaseURL;
private String httpBaseURL;
/**
* 获取基础路径
* @param isHttps
* @return
*/
public String getBaseURL(boolean isHttps) {
if (isHttps) {
if (httpsBaseURL != null) {
return httpsBaseURL;
}
} else {
if (httpBaseURL != null) {
return httpBaseURL;
}
}
try {
// URL
InetAddress address = InetAddress.getLocalHost();
String domain=environment.getProperty("server.domain");
if (domain == null) {
domain = address.getHostAddress();
}
int httpPort = Integer.parseInt(environment.getProperty("server.http.port"));
int httpsPort = Integer.parseInt(environment.getProperty("server.port"));
int port = isHttps ? httpPort : httpsPort;
String protocol = isHttps ? "https" : "http";
String portString = ":" + port;
if (port == 80 || port == 443) {
portString = "";
}
String baseURL = protocol + "://" + domain + portString + "/";
return baseURL;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取包所在路径
* @param aPackage
* @param isHttps
* @return
*/
public String getPackageResourceURL(Package aPackage, boolean isHttps) {
String baseURL = getBaseURL(isHttps);
String resourceURL = baseURL + aPackage.getPlatform() + "/" + aPackage.getBundleID() + "/" + aPackage.getCreateTime() + "/";
return resourceURL;
}
/**
* 获取证书路径
* @return
*/
public String getCAPath() {
return getBaseURL(false) + "crt/ca.crt";
}
/**
* 获取图标的临时路径
* @param aPackage
* @return
*/
public static String getTempIconPath(Package aPackage) {
if (aPackage == null) return null;
StringBuilder path = new StringBuilder();
path.append(FileUtils.getTempDirectoryPath()).append(aPackage.getPlatform());
path.append(File.separator).append(aPackage.getBundleID());
// 如果目录不存在,创建目录
File dir = new File(path.toString());
if (!dir.exists()) dir.mkdirs();
path.append(File.separator).append(aPackage.getCreateTime()).append(".png");
return path.toString();
}
/**
* 获取上传路径
* @return
*/
public static String getUploadPath() {
try {
//获取跟目录
File path = new File(ResourceUtils.getURL("classpath:").getPath());
if(!path.exists()) path = new File("");
//如果上传目录为/static/upload/,则可以如下获取:
File upload = new File(path.getAbsolutePath(),"static/upload/");
if(!upload.exists()) upload.mkdirs();
return upload.getPath();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取 APP 路径
* @param app
* @return
*/
public static String getAppPath(App app) {
return getUploadPath() + File.separator + app.getPlatform() + File.separator + app.getBundleID() + File.separator;
}
/**
* 获取包的完整路径
* @param aPackage
* @return
*/
public static String getFullPath(Package aPackage) {
return getUploadPath() + File.separator + getRelativePath(aPackage);
}
/**
* 获取包的相对路径
* @param aPackage
* @return
*/
public static String getRelativePath(Package aPackage) {
if (aPackage == null) return null;
StringBuilder path = new StringBuilder();
path.append(aPackage.getPlatform()).append(File.separator);
path.append(aPackage.getBundleID()).append(File.separator);
path.append(aPackage.getCreateTime()).append(File.separator);
return path.toString();
}
/**
* 清除目录
* @param path
*/
public static void deleteDirectory(String path) {
File dir = new File(path);
if (dir.exists()) {
try {
FileUtils.deleteDirectory(dir);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,50 @@
package org.yzr.utils;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class ZipUtils {
public static String unzip(String path) {
try {
long start = System.currentTimeMillis();
String destDirPath = System.getProperty("java.io.tmpdir") + start;
ZipFile zipFile = new ZipFile(path);
Enumeration<?> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = (ZipEntry)entries.nextElement();
if (entry.isDirectory()) {
String dirPath = destDirPath + File.separator + entry.getName();
File dir = new File(dirPath);
dir.mkdirs();
} else {
File targetFile = new File(destDirPath + File.separator + entry.getName());
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
targetFile.createNewFile();
InputStream is = zipFile.getInputStream(entry);
FileOutputStream fos = new FileOutputStream(targetFile);
int len;
byte[] buf = new byte[1024];
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
}
fos.close();
is.close();
}
}
zipFile.close();
return destDirPath;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View File

@@ -0,0 +1,98 @@
package org.yzr.utils.ipa;
import com.dd.plist.NSArray;
import com.dd.plist.NSDictionary;
import com.dd.plist.NSObject;
import com.dd.plist.PropertyListParser;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class Plist {
private NSDictionary dictionary;
public Plist(NSDictionary dictionary) {
this.dictionary = dictionary;
}
public static Plist parseWithFile(File file) {
try {
NSDictionary dictionary = (NSDictionary)PropertyListParser.parse(file);
return new Plist(dictionary);
}catch (Exception e){}
return null;
}
public static Plist parseWithString(String plist) {
try {
NSDictionary dictionary = (NSDictionary)PropertyListParser.parse(plist.getBytes());
return new Plist(dictionary);
}catch (Exception e){}
return null;
}
// 通过 keyPath 获取值
public NSObject valueForKeyPath(String keyPath) {
String[] values = keyPath.split("\\.");
try {
if (values.length > 0) {
int i = 0;
NSObject value = null;
NSDictionary dictionary = this.dictionary;
while (i < values.length) {
value = dictionary.objectForKey(values[i]);
if (value instanceof NSDictionary) {
dictionary = (NSDictionary)value;
}
i++;
}
return value;
}
}catch (Exception e) {
e.printStackTrace();
}
return null;
}
public String stringValueForKeyPath(String keyPath) {
Object object = valueForKeyPath(keyPath);
if (object != null) {
return object.toString();
}
return null;
}
public NSObject valueForPath(String path) {
try {
return this.dictionary.objectForKey(path);
}catch (Exception e) {
e.printStackTrace();
}
return null;
}
public String stringValueForPath(String keyPath) {
Object object = valueForKeyPath(keyPath);
if (object != null) {
return object.toString();
}
return null;
}
public List<String> arrayValueForPath(String path) {
Object object = valueForKeyPath(path);
if (object != null) {
NSArray deviceArray = (NSArray)object;
List<String> devices = new ArrayList<>();
if (deviceArray != null && deviceArray.count() > 0) {
for (int i = 0; i < deviceArray.count(); i++) {
devices.add(deviceArray.objectAtIndex(i).toString());
}
}
return devices;
}
return null;
}
}

View File

@@ -0,0 +1,54 @@
package org.yzr.utils.ipa;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.springframework.util.ResourceUtils;
import org.yzr.vo.PackageViewModel;
import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class PlistGenerator {
public static void generate(PackageViewModel aPackage, String destPath) {
try {
Writer out = new FileWriter(new File(destPath));
generate(aPackage, out);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 生成 manifest
* @param aPackage
* @param out
*/
public static void generate(PackageViewModel aPackage, Writer out) {
try {
//1.0 创建配置对象
//创建Configuration实例指定FreeMarker的版本
Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);
//指定模板所在的目录
cfg.setClassLoaderForTemplateLoading(PlistGenerator.class.getClassLoader(), "/freemarker");
//设置默认字符集
cfg.setDefaultEncoding(StandardCharsets.UTF_8.name());
//2.0 创建数据模型
Map<String, Object> root = new HashMap<>();
root.put("aPackage", aPackage);
//3.0 获取模板
Template template = cfg.getTemplate("manifest.plist");
//4.0 给模板绑定数据模型
template.process(root, out);
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,56 @@
package org.yzr.utils.ipa;
import com.dd.plist.NSDate;
import lombok.Getter;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.Date;
import java.util.List;
@Getter
public class Provision {
private String teamName;
private String teamID;
private Date createDate;
private Date expirationDate;
private String UUID;
private List<String> devices;
private int deviceCount;
private String type;
public Provision(String appPath) {
String profile = appPath + File.separator + "embedded.mobileprovision";
try {
boolean started = false;
boolean ended = false;
BufferedReader reader = new BufferedReader(new FileReader(profile));
StringBuffer plist = new StringBuffer();
String str = null;
while ((str = reader.readLine()) != null) {
if (str.contains("</plist>")) {
ended = true;
plist.append("</plist>").append("\n");
} else if (started && !ended) {
plist.append(str).append("\n");
} else if (str.contains("<?xml")) {
started = true;
plist.append(str.substring(str.indexOf("<?xml"))).append("\n");
}
}
reader.close();
Plist provisionFile = Plist.parseWithString(plist.toString());
this.devices = provisionFile.arrayValueForPath("ProvisionedDevices");
this.deviceCount = this.devices.size();
this.teamName = provisionFile.stringValueForPath("TeamName");
this.teamID = provisionFile.arrayValueForPath("TeamIdentifier").get(0);
this.createDate = ((NSDate)provisionFile.valueForKeyPath("CreationDate")).getDate();
this.expirationDate = ((NSDate)provisionFile.valueForKeyPath("ExpirationDate")).getDate();
this.UUID = provisionFile.stringValueForPath("UUID");
this.type = this.deviceCount > 0 ? "Ad-hoc" : "Release";
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,45 @@
package org.yzr.utils.parser;
import net.dongliu.apk.parser.ApkFile;
import net.dongliu.apk.parser.bean.ApkMeta;
import net.dongliu.apk.parser.bean.IconFace;
import org.apache.commons.io.FileUtils;
import org.yzr.model.Package;
import org.yzr.utils.PathManager;
import java.io.File;
import java.util.UUID;
public class APKParser implements PackageParser {
@Override
public Package parse(String filePath) {
try {
File file = new File(filePath);
if (!file.exists()) return null;
ApkFile apkFile = new ApkFile(file);
long currentTimeMillis = System.currentTimeMillis();
Package aPackage = new Package();
aPackage.setSize(file.length());
ApkMeta meta = apkFile.getApkMeta();
aPackage.setName(meta.getName());
aPackage.setVersion(meta.getPlatformBuildVersionName());
aPackage.setBuildVersion(meta.getPlatformBuildVersionCode());
aPackage.setBundleID(meta.getPackageName());
aPackage.setMinVersion(meta.getMinSdkVersion());
aPackage.setPlatform("android");
aPackage.setCreateTime(currentTimeMillis);
int iconCount = apkFile.getAllIcons().size();
if (iconCount > 0) {
IconFace icon = apkFile.getAllIcons().get(iconCount - 1);
String iconPath = PathManager.getTempIconPath(aPackage);
File iconFile = new File(iconPath);
FileUtils.writeByteArrayToFile(iconFile, icon.getData());
}
apkFile.close();
return aPackage;
}catch (Exception e){
}
return null;
}
}

View File

@@ -0,0 +1,103 @@
package org.yzr.utils.parser;
import org.apache.commons.io.FileUtils;
import org.yzr.model.Package;
import org.yzr.utils.PNGConverter;
import org.yzr.utils.PathManager;
import org.yzr.utils.ZipUtils;
import org.yzr.utils.ipa.Plist;
import org.yzr.utils.ipa.Provision;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
public class IPAParser implements PackageParser {
@Override
public Package parse(String filePath) {
try {
Package aPackage = new Package();
// 解压 IPA 包
String targetPath = ZipUtils.unzip(filePath);
String appPath = appPath(targetPath);
String infoPlistPath = appPath + File.separator + "Info.plist";
File infoPlistFile = new File(infoPlistPath);
// Plist 文件获取失败
if (!infoPlistFile.exists()) return null;
// 获取 infoPlist
Plist infoPlist = Plist.parseWithFile(infoPlistFile);
File ipaFile = new File(filePath);
long currentTimeMillis = System.currentTimeMillis();
aPackage.setSize(ipaFile.length());
aPackage.setName(infoPlist.stringValueForPath("CFBundleDisplayName"));
if (aPackage.getName() == null) {
aPackage.setName(infoPlist.stringValueForPath("CFBundleName"));
}
aPackage.setVersion(infoPlist.stringValueForPath("CFBundleShortVersionString"));
aPackage.setBuildVersion(infoPlist.stringValueForPath("CFBundleVersion"));
aPackage.setBundleID(infoPlist.stringValueForPath("CFBundleIdentifier"));
aPackage.setMinVersion(infoPlist.stringValueForPath("MinimumOSVersion"));
aPackage.setCreateTime(currentTimeMillis);
aPackage.setPlatform("ios");
// 获取应用图标
String iconPath = appIcon(appPath, infoPlist.stringValueForKeyPath("CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconName"));
String iconTempPath = PathManager.getTempIconPath(aPackage);
PNGConverter.convert(iconPath, iconTempPath);
// 解析 Provision
Provision provision = new Provision(appPath);
// 清除目录
FileUtils.deleteDirectory(new File(targetPath));
return aPackage;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/*获取 APP 路径*/
private static String appPath(String path) {
try {
String payloadPath = path + File.separator + "Payload";
File payloadFile = new File(payloadPath);
if (!payloadFile.exists()) return null;
if (!payloadFile.isDirectory()) return null;
File[] listFiles = payloadFile.listFiles();
String appName = null;
for (File file : listFiles) {
if (file.getName().contains(".app")) {
appName = file.getName();
break;
}
}
if (appName == null) return null;
return payloadPath + File.separator + appName;
} catch (Exception e) {
}
return null;
}
// 获取 APP 图标
private static String appIcon(String appPath, String iconName) {
List<String> iconNames = new ArrayList<>();
File appFile = new File(appPath);
File[] listFiles = appFile.listFiles();
for (File file : listFiles) {
String pattern = iconName + "[4,6]0x[4,6]0@[2,3]?x.png";
boolean isMatch = Pattern.matches(pattern, file.getName());
if (isMatch) {
iconNames.add(file.getName());
}
}
if (iconNames.size() > 0) {
return appPath + File.separator + iconNames.get(iconNames.size() - 1);
}
return null;
}
}

View File

@@ -0,0 +1,8 @@
package org.yzr.utils.parser;
import org.yzr.model.Package;
public interface PackageParser {
// 解析包
public Package parse(String filePath);
}

View File

@@ -0,0 +1,38 @@
package org.yzr.utils.parser;
import org.apache.commons.io.FilenameUtils;
import org.yzr.model.Package;
public class ParserClient {
/**
* 解析包
* @param filePath 文件路径
* @return
*/
public static Package parse(String filePath) {
PackageParser parser = getParser(filePath);
if (parser != null) {
return parser.parse(filePath);
}
return null;
}
/**
* 根据文件后缀名获取解析器
* @param filePath
* @return
*/
private static PackageParser getParser(String filePath) {
String extension = FilenameUtils.getExtension(filePath);
try {
// 动态获取解析器
Class aClass = Class.forName("org.yzr.utils.parser." + extension.toUpperCase()+"Parser");
PackageParser packageParser = (PackageParser)aClass.newInstance();
return packageParser;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View File

@@ -0,0 +1,109 @@
package org.yzr.vo;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.yzr.model.App;
import org.yzr.model.Package;
import org.yzr.utils.PathManager;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@Getter
public class AppViewModel {
private String id;
private String name;
private String platform;
private String bundleID;
private String icon;
private String version;
private String buildVersion;
private String minVersion;
private String shortCode;
private String installPath;
private List<PackageViewModel> packageList;
private PackageViewModel currentPackage;
/***
* 初始化是否加载列表
* @param app
* @param pathManager
* @param loadList
*/
public AppViewModel(App app, PathManager pathManager, boolean loadList) {
this.id = app.getId();
this.platform = app.getPlatform();
this.bundleID = app.getBundleID();
this.icon = PathManager.getRelativePath(app.getCurrentPackage()) + "icon.png";
Package aPackage = findPackageById(app, null);
this.version = aPackage.getVersion();
this.buildVersion = aPackage.getBuildVersion();
this.shortCode = app.getShortCode();
this.name = app.getName();
this.installPath = pathManager.getBaseURL(false) + "s/" + app.getShortCode();
this.minVersion = aPackage.getMinVersion();
this.currentPackage = new PackageViewModel(aPackage, pathManager);
if (loadList) {
// 排序
this.packageList = sortPackages(app.getPackageList(), pathManager);
}
}
public AppViewModel(App app, PathManager pathManager, String packageId) {
this.id = app.getId();
this.platform = app.getPlatform();
this.bundleID = app.getBundleID();
this.icon = PathManager.getRelativePath(app.getCurrentPackage()) + "icon.png";
Package aPackage = findPackageById(app, packageId);
this.version = aPackage.getVersion();
this.buildVersion = aPackage.getBuildVersion();
this.shortCode = app.getShortCode();
this.name = app.getName();
this.installPath = pathManager.getBaseURL(false) + "s/" + app.getShortCode();
this.minVersion = aPackage.getMinVersion();
this.currentPackage = new PackageViewModel(aPackage, pathManager);
}
private static Package findPackageById(App app, String id) {
if (id != null) {
for (Package aPackage : app.getPackageList()) {
if (aPackage.getId().equals(id)) {
return aPackage;
}
}
}
return app.getCurrentPackage();
}
private static List<PackageViewModel> sortPackages(List<Package> packages, PathManager pathManager) {
// 排序
List<PackageViewModel> packageViewModels = new ArrayList<>();
for (Package aPackage : packages) {
PackageViewModel packageViewModel = new PackageViewModel(aPackage, pathManager);
packageViewModels.add(packageViewModel);
}
packageViewModels.sort((o1, o2) -> {
if (o1.getCreateTime() > o2.getCreateTime()) {
return -1;
}
return 1;
});
return packageViewModels;
}
}

View File

@@ -0,0 +1,56 @@
package org.yzr.vo;
import lombok.Getter;
import org.apache.commons.io.FileUtils;
import org.yzr.model.Package;
import org.yzr.utils.PathManager;
import java.net.URLEncoder;
import java.util.Date;
@Getter
public class PackageViewModel {
private String downloadURL;
private String safeDownloadURL;
private String iconURL;
private String installURL;
private String previewURL;
private String id;
private String version;
private String bundleID;
private String name;
private long createTime;
private String buildVersion;
private String displaySize;
private String displayTime;
private boolean iOS;
public PackageViewModel(Package aPackage, PathManager pathManager) {
this.downloadURL = pathManager.getBaseURL(false) + "p/" + aPackage.getId();
this.safeDownloadURL = pathManager.getBaseURL(true) + "p/" + aPackage.getId();
this.iconURL = pathManager.getPackageResourceURL(aPackage, true) + "icon.png";
this.id = aPackage.getId();
this.version = aPackage.getVersion();
this.bundleID = aPackage.getBundleID();
this.name = aPackage.getName();
this.createTime = aPackage.getCreateTime();
this.buildVersion = aPackage.getBuildVersion();
this.displaySize = String.format("%.2f MB", aPackage.getSize() / (1.0F * FileUtils.ONE_MB));
Date updateTime = new Date(this.createTime);
String displayTime = (new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(updateTime);
this.displayTime = displayTime;
if (aPackage.getPlatform().equals("ios")) {
this.iOS = true;
String url = pathManager.getBaseURL(true) + "m/" + aPackage.getId();
try {
this.installURL = "itms-services://?action=download-manifest&url=" + URLEncoder.encode(url, "utf-8");
} catch (Exception e){e.printStackTrace();}
} else if (aPackage.getPlatform().equals("android")) {
this.iOS = false;
this.installURL = pathManager.getPackageResourceURL(aPackage, false) + aPackage.getFileName();
}
this.previewURL = pathManager.getBaseURL(false) + "s/" + aPackage.getApp().getShortCode() + "?id=" + aPackage.getId();
}
}

BIN
src/main/resources/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,44 @@
########################################################
### Mysql
########################################################
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/app_manager
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
########################################################
### Java Persistence Api
########################################################
# Specify the DBMS
spring.jpa.database = MYSQL
# Show or not log for each sql query
spring.jpa.show-sql = true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto = update
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5InnoDBDialect
############################################################
#
# 上传文件大小
#
############################################################
spring.servlet.multipart.max-file-size=300MB
spring.servlet.multipart.max-request-size=300MB
############################################################
#
# ssl
#
############################################################
server.ssl.key-store=classpath:server.pkcs12
server.ssl.key-store-password=123456
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=1
# 自定义配置
server.port=443
server.http.port=80
config.debug=debug
server.domain=192.168.0.108

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<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><![CDATA[${aPackage.safeDownloadURL}]]></string>
</dict>
<dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<integer>0</integer>
<key>url</key>
<string><![CDATA[${aPackage.iconURL}]]></string>
</dict>
<dict>
<key>kind</key>
<string>full-size-image</string>
<key>needs-shine</key>
<true/>
<key>url</key>
<string><![CDATA[${aPackage.iconURL}]]></string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>${aPackage.bundleID}</string>
<key>bundle-version</key>
<string><![CDATA[${aPackage.version}]]></string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string><![CDATA[${aPackage.name}]]></string>
</dict>
</dict>
</array>
</dict></plist>

Binary file not shown.

BIN
src/main/resources/static/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/main/resources/static/crt/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDVDCCAjwCCQCZ0rtCbbUBAjANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJD
TjERMA8GA1UECAwIWmhlSmlhbmcxETAPBgNVBAcMCEhhbmdaaG91MSEwHwYDVQQL
DBhEb21haW4gQ29udHJvbCBWYWxpZGF0ZWQxFDASBgNVBAMMCzE5Mi4xNjguMC4q
MB4XDTE5MDYzMDA4MjI0NFoXDTI5MDYyNzA4MjI0NFowbDELMAkGA1UEBhMCQ04x
ETAPBgNVBAgMCFpoZUppYW5nMREwDwYDVQQHDAhIYW5nWmhvdTEhMB8GA1UECwwY
RG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMRQwEgYDVQQDDAsxOTIuMTY4LjAuKjCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMIEYFPCU5m4kP3/IqAzX21q
8rRzwpwe0FMxef3Q3NI64H6Y+Hi064WZuX5W9PuAHyyBuxUYTObMC+AI6rmtV8na
5+oGrpnJH0dBjtLL/04unFs5ArSd2ob5hQDZ+bukJMo8ffiOw4T882P9wEiRBNXw
RQOJBKuG4zAhDLyAgcpZ96g4Is+3TtgfQnVfbSWIcT6B+mS7yVKJM+Ov0twWb60q
94Fk8NlPyq5AhIiM8O7W74IMr9Uu/woMMJ91wJoBnp5MqDl79ebGQzuzOHY0E1VX
qyp2RkAZgzEgGOpwLXWOzYfGu2A+HdaMleCZQ2d/xF81w17x6hue3CCS0AIqvMUC
AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAlEeut6xHWX7H58c2uqUtak7AYS1P0KmB
hN1zuFQ/lwn87TQ9JtLJK8Ock+2mIDadnRzKPPwGfKlKgNRT6kj+MYixnL+xeiE8
0bIPs2RAPu0XGbbPFIX9VeHC7gTbixbjPQY8MmrvgTizwnooAdrFg9XrkN9BNgj/
f43kwlEA9lw0L8hjcr1EM86IQCFXTUzLdJAW13WrVMQsO7ZVz/09mtHnH0GEVfTv
jn/j7Sykr9sEwBNBtKSmbCahSj2CBft4WZl0PyE1LnEbeu29e6xN0XUh9C6OUyXZ
wTAwy+aaBE2XID9nHHH0G9EVHlDgm9SnvjNRN8nXag1/z2iESkqK/A==
-----END CERTIFICATE-----

BIN
src/main/resources/static/css/.DS_Store vendored Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,459 @@
@font-face {
font-family: partner;
src: url(./fonts/ipartner.eot?rkodm6);
src: url(./fonts/ipartner.eot?rkodm6#iefix) format("embedded-opentype"), url(./fonts/ipartner.ttf?rkodm6) format("truetype"), url(./fonts/ipartner.woff?rkodm6) format("woff"), url(./fonts/ipartner.svg?rkodm6#ipartner) format("svg");
font-weight: 400;
font-style: normal
}
[class*=" ipartner-"], [class^=ipartner-] {
font-family: partner!important;
speak: none;
font-style: normal;
font-weight: 400;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale
}
.ipartner-swift:before {
content: "\e900"
}
.ipartner-qingcloud:before {
content: "\e902"
}
.ipartner-devlink:before {
content: "\e920"
}
.ipartner-logo-pingxx:before {
content: "\e921"
}
.ipartner-logo-qiniu:before {
content: "\e922"
}
.ipartner-logo-leancloud:before {
content: "\e923"
}
.ipartner-logo-flowci:before {
content: "\e924"
}
.ipartner-deveco:before {
content: "\e925"
}
.ipartner-wilddog:before {
content: "\e926"
}
.ipartner-xitu:before {
content: "\e927"
}
.ipartner-36kr:before {
content: "\e928"
}
@font-face {
font-family: icomoon;
src: url(./fonts/icomoon.eot?wcusdg);
src: url(./fonts/icomoon.eot?wcusdg#iefix) format("embedded-opentype"), url(./fonts/icomoon.ttf?wcusdg) format("truetype"), url(./fonts/icomoon.woff?wcusdg) format("woff"), url(./fonts/icomoon.svg?wcusdg#icomoon) format("svg");
font-weight: 400;
font-style: normal
}
[class*=" icon-"], [class^=icon-] {
font-family: icomoon!important;
speak: none;
font-style: normal;
font-weight: 400;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale
}
.icon-cake:before {
content: "\e900"
}
.icon-weibo:before {
content: "\e600"
}
.icon-wechat:before {
content: "\e601"
}
.icon-firim:before {
content: "\e602"
}
.icon-bug:before {
content: "\e603"
}
.icon-ion:before {
content: "\e604"
}
.icon-angle-right:before {
content: "\e606"
}
.icon-cloud-download:before {
content: "\e607"
}
.icon-question:before {
content: "\e608"
}
.icon-optimize-upload:before {
content: "\e609"
}
.icon-console:before {
content: "\e60a"
}
.icon-microscope:before {
content: "\e60b"
}
.icon-user-access:before {
content: "\e60c"
}
.icon-logo-jiecao:before {
content: "\e60d"
}
.icon-m:before {
content: "\e60e"
}
.icon-f:before {
content: "\e60f"
}
.icon-plugin:before {
content: "\e610"
}
.icon-launch:before {
content: "\e611"
}
.icon-brace-left:before {
content: "\e613"
}
.icon-logo:before {
content: "\e620"
}
.icon-thumbsup:before {
content: "\e615"
}
.icon-webhooks:before {
content: "\e616"
}
.icon-brace-right:before {
content: "\e617"
}
.icon-comma-eye:before {
content: "\e618"
}
.icon-mouth:before {
content: "\e619"
}
.icon-brace-hor:before {
content: "\e61a"
}
.icon-brace-right-b:before {
content: "\e61b"
}
.icon-brace-left-b:before {
content: "\e61c"
}
.icon-brace-box:before {
content: "\e61d"
}
.icon-menu:before {
content: "\e61e"
}
.icon-app:before {
content: "\e61f"
}
.icon-box:before {
content: "\e620"
}
.icon-combo:before {
content: "\e621"
}
.icon-device:before {
content: "\e622"
}
.icon-face:before {
content: "\e623"
}
.icon-file:before {
content: "\e624"
}
.icon-msg:before {
content: "\e625"
}
.icon-pen:before {
content: "\e626"
}
.icon-ipa:before {
content: "\e627"
}
.icon-lock2:before {
content: "\e628"
}
.icon-qrcode:before {
content: "\e629"
}
.icon-rollback:before {
content: "\e62a"
}
.icon-eye:before {
content: "\e62b"
}
.icon-plus:before {
content: "\e62c"
}
.icon-chart:before {
content: "\e62d"
}
.icon-idea:before {
content: "\e62e"
}
.icon-owner:before {
content: "\e62f"
}
.icon-search:before {
content: "\e630"
}
.icon-studio:before {
content: "\e631"
}
.icon-upload-cloud2:before {
content: "\e632"
}
.icon-android:before {
content: "\e633"
}
.icon-trash:before {
content: "\e634"
}
.icon-attachment:before {
content: "\e635"
}
.icon-apple:before {
content: "\e636"
}
.icon-calendar:before {
content: "\e637"
}
.icon-calendar2:before {
content: "\e638"
}
.icon-eye-close:before {
content: "\e639"
}
.icon-reply:before {
content: "\e63a"
}
.icon-email:before {
content: "\e63b"
}
.icon-error2:before {
content: "\e63c"
}
.icon-cross:before {
content: "\e63d"
}
.icon-user-plus:before {
content: "\e63e"
}
.icon-ios:before {
content: "\e63f"
}
.icon-filter:before {
content: "\e640"
}
.icon-test-speed:before {
content: "\e641"
}
.icon-udid:before {
content: "\e642"
}
.icon-update:before {
content: "\e643"
}
.icon-comma:before {
content: "\e644"
}
.icon-i:before {
content: "\e645"
}
.icon-r:before {
content: "\e646"
}
.icon-f-dot:before {
content: "\e647"
}
.icon-layers:before {
content: "\e648"
}
.icon-news:before {
content: "\e649"
}
.icon-percent:before {
content: "\e64a"
}
.icon-bughd:before {
content: "\e64b"
}
.icon-incode:before {
content: "\e64c"
}
.icon-message:before {
content: "\e64d"
}
.icon-eclipse:before {
content: "\e64e"
}
.icon-turkey:before {
content: "\e901"
}
.icon-jenkins:before {
content: "\e902"
}
.icon-gradle:before {
content: "\e903"
}
.icon-statistics:before {
content: "\e904"
}
.icon-done:before {
content: "\e905"
}
.icon-qiniu:before {
content: "\e906"
}
.icon-logo-leancloud:before {
content: "\e907"
}
.icon-logo-jd:before {
content: "\e908"
}
.icon-logo-xiachufang:before {
content: "\e909"
}
.icon-logo-ebaoyang:before {
content: "\e90a"
}
.icon-logo-jumei:before {
content: "\e90b"
}
.icon-cart:before {
content: "\e90c"
}
.icon-users:before {
content: "\e90d"
}
.icon-spinner3:before {
content: "\e97c"
}
.icon-infinite:before {
content: "\ea2f"
}
.icon-spinner:before {
content: "\f110"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/main/resources/static/js/.DS_Store vendored Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,183 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="renderer" content="webkit">
<title>应用列表</title>
<link rel="icon" type="image/x-icon" th:href="@{/images/favicon.ico}" />
<link rel="stylesheet" th:href="@{/css/icons.css}">
<link rel="stylesheet" th:href="@{/css/bootstrap.css}">
<link rel="stylesheet" th:href="@{/css/manage.css}">
<link rel="stylesheet" th:href="@{/css/index.css}">
<script type="text/javascript" th:src="@{/js/jquery-1.11.0.min.js}"></script>
</head>
<body class="ng-scope">
<nav class="navbar navbar-transparent fade-out navbar-black" role="navigation">
<div class="navbar-header"><a class="navbar-brand" href="/apps"><i class="icon-logo"></i> </a></div>
<div class="collapse navbar-collapse navbar-ex1-collapse ng-scope" ng-controller="NavbarController">
<div class="dropdown">
<div></div>
</div>
</div>
</nav>
<div class="menu-toggle fade-out"><i class="icon-menu"></i></div>
<div class="navbar-wrapper ng-scope">
<div ng-controller="NavbarController" class="ng-scope">
<div class="navbar-header-wrap">
<div class="middle-wrapper">
<nav>
<h1 class="navbar-title logo">
<i class="icon-logo"></i>
</h1>
<i class="icon-angle-right"></i>
<div class="navbar-title primary-title">
<a class="ng-binding" th:href="${baseURL} + 'apps'">我的应用</a>
</div>
<i class="icon-angle-right ng-hide"></i>
</nav>
</div>
</div>
</div>
</div><!-- ngInclude: '/templates_manage/upload_modal.html' -->
<section data-ui-view="" class="ng-scope" style="">
<div class="page-apps ng-scope">
<div class="middle-wrapper">
</div><!-- ngIf: !appsReady -->
<div class="middle-wrapper container-fluid" ng-show="appsReady">
<div class="apps row">
<upload-card id="uploadCard" class="components-upload-card col-xs-4 col-sm-4 col-md-4 app-animator" >
<div class="card text-center">
<div class="dashed-space">
<table>
<tbody>
<tr>
<td>
<i class="icon-upload-cloud2"></i>
<div class="text drag-state">
<span id="upload-progress" translate="DRAG_TO_UPLOAD" class="ng-scope">拖拽到这里上传</span>
<span translate="DROP_TO_UPLOAD" class="ng-scope">快松手</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</upload-card>
<div th:each="app : ${apps}" class="col-xs-4 col-sm-4 col-md-4 app-animator ng-scope">
<div th:class="'card app ' + @{'card-' + ${app.platform}}">
<i th:class="'type-icon ' + @{'icon-' + ${app.platform}}"></i>
<div class="type-mark"></div>
<a class="appicon" th:href="'/apps/' + ${app.id}" target="_blank">
<img class="icon ng-isolate-scope" width="100" height="100" th:src="@{'/' + ${app.icon}}" />
</a>
<!-- ngIf: app.has_combo --><br>
<p class="appname" th:data="@{${baseURL} + 'apps/' + ${app.id}}">
<i class="icon-owner"></i>
<span class="ng-binding">[[${app.name}]]</span></p>
<table>
<tbody>
<tr>
<td class="ng-binding">短链接:</td>
<td><span class="ng-binding">[[${baseURL}]]s/[[${app.shortCode}]]</span></td>
</tr>
<tr>
<td class="ng-binding">包名:</td>
<td>
<span title="com.mistong.ewt360" class="ng-binding">[[${app.bundleID}]]</span>
</td>
</tr>
<tr>
<td class="ng-binding">版本:</td>
<td>
<span class="ng-binding">[[${app.version}]] ( Build [[${app.buildVersion}]] )</span>
</td>
</tr>
</tbody>
</table>
<div class="action">
<a class="ng-binding" th:href="'/apps/' + ${app.id}">
<i class="icon-pen"></i> 编辑</a>
<a th:href="@{${baseURL}+'s/'+${app.shortCode}}" target="_blank" class="ng-binding">
<i class="icon-eye"></i> 预览</a>
<button class="btn btn-remove ng-scope" th:data="${app.id}">
<i class="icon icon-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<div alert-bar="" class="alert-bar ng-hide" ng-show="anyErrors">
<div class="action" ng-click="close()"></div>
<div class="inner">
<p ng-bind="errors" class="ng-binding"></p>
</div>
</div>
<script type="text/javascript">
var dashboard = document.getElementById("uploadCard")
dashboard.addEventListener("dragover", function (e) {
e.preventDefault()
e.stopPropagation()
})
dashboard.addEventListener("dragenter", function (e) {
e.preventDefault()
e.stopPropagation()
})
dashboard.addEventListener("drop", function (e) {
// 必须要禁用浏览器默认事件
e.preventDefault()
e.stopPropagation()
var files = this.files || e.dataTransfer.files
var file = files[0]
//上传
var xhr = new XMLHttpRequest();
xhr.open("post", "/app/upload", true);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
// 获取上传进度
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
var percent = Math.floor(event.loaded / event.total * 100);
var uploadText = "拖拽到这里上传"
var uploadElement = document.getElementById("upload-progress")
if (percent < 100) {
uploadElement.innerText="正在上传:" + percent + "%"
} else {
uploadElement.innerText=uploadText
}
}
};
// 上传完成
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
window.location.href=window.location.href
window.location.reload
}
}
var fd = new FormData();
fd.append('file', file);
xhr.send(fd);
})
$(".btn-remove").click(function () {
var url = "/app/delete/" + $(this).attr("data");
$.ajax({url:url,success:function(result){
window.location.href=window.location.href
window.location.reload
}
});
});
$(".appname").click(function () {
window.open($(this).attr("data"))
});
</script>
</body>
</html>

View File

@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport"
content="minimal-ui,width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="white">
<meta name="format-detection" content="telephone=no">
<link rel="icon" type="image/x-icon" th:href="@{/images/favicon.ico}" />
<link rel="stylesheet" th:href="@{/css/download.css}">
<script type="text/javascript" th:src="@{/js/qrcode.js}"></script>
<script type="text/javascript" th:src="@{/js/jquery-1.11.0.min.js}"></script>
<title>[[${app.name}]]</title>
</head>
<body class="app">
<div class="masklayer" id="MaskLayer" style="display:none"></div>
<span class="pattern left">
<img th:src="@{/images/download_pattern_left.png}" />
</span>
<span class="pattern right">
<img th:src="@{/images/download_pattern_right.png}" />
</span>
<div class="wechat_tip_content"></div>
<div class="out-container">
<div class="main">
<header itemscope="" itemtype="http://schema.org/SoftwareApplication">
<div class="table-container">
<div class="cell-container">
<div class="app-brief">
<div class="icon-container wrapper">
<i class="icon-icon_path bg-path"></i>
<span class="icon">
<img th:src="@{'/' + ${app.icon}}" itemprop="image">
</span>
<span class="qrcode" id="qrcode" th:data="${app.currentPackage.previewURL}">
</span>
</div>
<p class="release-type wrapper">内测版</p>
<h1 class="name wrapper">
<span class="icon-warp">
<i th:class="'icon-'+ ${app.platform}"></i>
[[${app.name}]]
</span>
</h1>
<p class="scan-tips">扫描二维码下载<br>或用手机浏览器输入这个网址:&nbsp;&nbsp;<span
class="text-black">[[${app.installPath}]]</span></p>
<div class="release-info">
<p>内测版 -
<span itemprop="softwareVersion">[[${app.version}]] (Build [[${app.buildVersion}]]) -
[[${app.currentPackage.displaySize}]]</span></p>
<p>更新于: <span itemprop="datePublished">[[${app.currentPackage.displayTime}]]</span></p>
</div>
<div class="action-animate">
<input id="installURL" th:value="${app.currentPackage.installURL}" style="display: none" />
<div class="action-animate-text" id="install">下载安装</div>
<div class="action-animate-active"></div>
</div>
<div class="action-animate">
<input id="crtURL" th:value="${ca_path}" style="display: none"/>
<div class="action-animate-text" id="installCRT">安装证书</div>
<div class="action-animate-active"></div>
</div>
</div>
</div>
</div>
</header>
<!-- Release list -->
</div>
</div>
<script type="application/javascript">
$("#install").click(function () {
window.location.href = ($("#installURL").val())
})
$("#installCRT").click(function () {
window.open($("#crtURL").val())
})
var codeData = $("#qrcode").attr("data");
new QRCode("qrcode", {
text: codeData,
width: 200,
height: 200,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.L
});
</script>
</body>
</html>

View File

@@ -0,0 +1,161 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Stict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="renderer" content="webkit">
<title class="ng-binding">[[${package.name}]] - 应用动态</title>
<link rel="icon" type="image/x-icon" th:href="@{/images/favicon.ico}" />
<link rel="stylesheet" th:href="@{/css/icons.css}">
<link rel="stylesheet" th:href="@{/css/bootstrap.css}">
<link rel="stylesheet" th:href="@{/css/manage.css}">
<link rel="stylesheet" th:href="@{/css/index.css}">
<script type="text/javascript" th:src="@{/js/jquery-1.11.0.min.js}"></script>
</head>
<body class="ng-scope">
<nav class="navbar navbar-transparent fade-out navbar-black">
<div class="navbar-header">
<a class="navbar-brand" href="/apps"><i class="icon-logo"></i> </a>
</div>
<div class="collapse navbar-collapse navbar-ex1-collapse ng-scope">
<div class="dropdown">
<div></div>
</div>
</div>
</nav>
<div class="menu-toggle fade-out"><i class="icon-menu"></i></div>
<div class="navbar-wrapper ng-scope">
<div ng-controller="NavbarController" class="ng-scope">
<div class="navbar-header-wrap">
<div class="middle-wrapper">
<nav>
<h1 class="navbar-title logo">
<i class="icon-logo"></i>
</h1>
<i class="icon-angle-right"></i>
<div class="navbar-title primary-title">
<a class="ng-binding" href="/apps">我的应用</a>
</div>
<i class="icon-angle-right"></i>
<div class="navbar-title secondary-title ng-binding"
style="">[[${package.name}]]</div>
</nav>
</div>
</div>
</div>
</div><!-- ngInclude: '/templates_manage/upload_modal.html' -->
<section data-ui-view="" class="ng-scope" style="">
<div class="page-app app-activities">
<div class="banner has-devices">
<div class="middle-wrapper clearfix">
<div class="pull-left icon-container appicon">
<img th:src="'/' + ${package.icon}" width="100" height="100" class="change_icon ng-isolate-scope"/>
</div>
<div class="badges">
<span tooltip-top="" tooltip="复制到剪贴板" id="js-app-short-copy-trigger"
class="short tooltip-top ng-binding ng-isolate-scope" th:value="${package.installPath}"
copy-trigger="">[[${package.installPath}]]</span>
<span class="apptype ng-binding" th:if="${#strings.containsIgnoreCase(package.platform,'ios')}">iOS</span>
<span class="apptype ng-binding" th:if="${#strings.containsIgnoreCase(package.platform,'android')}">Android</span>
<span class="bundleid ng-binding">BundleID<b class="ng-binding">&nbsp;&nbsp;[[${package.bundleID}]]</b></span>
<span class="version ng-scope" th:if="${#strings.containsIgnoreCase(package.platform,'ios')}" >iOS&nbsp;[[${package.minVersion}]]&nbsp;或者高版本</span>
</div>
<div class="actions">
<a class="download ng-binding"th:href="${package.installPath}" target="_blank">
<i class="icon-eye"></i>
预览
</a>
</div>
<div class="tabs-container">
<ul class="list-inline">
<li>
<a class="ng-binding"><i class="icon-file"></i> 基本信息</a></li>
<li>
<a class="ng-binding"><i class="icon-device"></i> 设备列表</a></li>
</ul>
</div>
</div>
</div><!-- uiView: -->
<div data-ui-view="" class="ng-scope">
<div class="page-app-activities page-tabcontent ng-scope">
<!-- ngIf: !activitiesReady -->
<div class="middle-wrapper" ng-show="activitiesReady">
<ul class="list-unstyled time-line">
<li>
<span class="dot"></span>
<span class="filter ng-binding">版本更新</span>
<span class="filter version-rollback ng-scope"></span>
</li>
<li>
<div class="market-app-info">
</div>
</li>
<li th:each="app,appStat : ${apps}">
<div>
<div class="directive-view-release">
<i class="icon-upload-cloud2"></i>
<b class="ng-binding">[[${app.version}]] (Build [[${app.buildVersion}]])</b>
<div class="release-metainfo ng-hide">
<small>
<i class="icon-calendar"></i>
<span class="ng-binding">[[${app.displayTime}]]</span>
</small>
</div>
<div class="release-metainfo">
<small>
<i class="icon-calendar"></i>
<span class="ng-binding">[[${app.displayTime}]]</span></small> &nbsp;&nbsp;·&nbsp;&nbsp;
<small class="ng-scope">内测版</small>
<i class="ng-hide">&nbsp;&nbsp;·&nbsp;&nbsp;</i>
<small class="ng-binding ng-hide"></small>
</div>
<div class="release-actions">
<button class="tooltip-top download-action" tooltip="下载原文件" th:value="${app.downloadURL}">
<i class="icon-cloud-download"></i>
<span class="ng-binding">[[${app.displaySize}]]</span>
</button>
<button class="preview" th:value="${app.previewURL}">
<i class="icon-eye"></i>
<span class="ng-binding">预览</span>
</button>
<button class="ng-scope app-delete" th:data="${app.id}" th:if="${appStat.index > 0}">
<i class="icon-trash"></i>
<span class="ng-binding">删除</span>
</button>
</div>
</div>
</div>
</li>
<li class="more ng-hide" ng-show="currentApp.releases.current_page &lt; currentApp.releases.total_pages">
<button ng-click="moreRelease()" class="ng-binding">显示更多版本</button></li>
</ul>
</div>
</div>
</div>
</div>
</section>
<script type="application/javascript">
$(".download-action").click(function(){
window.open($(this).val())
})
$(".preview").click(function () {
window.open($(this).val())
})
$(".app-delete").click(function () {
var url = "/p/delete/" + $(this).attr("data");
$.ajax({url:url,success:function(result){
window.location.href=window.location.href
window.location.reload
}
});
})
</script>
</body>
</html>

View File

@@ -0,0 +1,43 @@
package org.yzr.main;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.yzr.model.Package;
import org.yzr.service.PackageService;
import org.yzr.utils.ipa.PlistGenerator;
import org.yzr.utils.parser.ParserClient;
import org.yzr.vo.PackageViewModel;
import javax.annotation.Resource;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
@Resource
private PackageService packageService;
@Test
public void contextLoads() {
}
@Test
public void testSave() {
Package aPackage = new Package();
aPackage.setName("升学e网通");
aPackage.setBundleID("org.yzr.test");
aPackage.setVersion("6.9.7");
this.packageService.save(aPackage);
}
// @Test
// public void testFileName() {
// Package aPackage = new Package();
// aPackage.setName("升学e网通");
// aPackage.setBundleID("org.yzr.test");
// aPackage.setVersion("6.9.7");
// PackageViewModel viewModel = new PackageViewModel(aPackage);
// PlistGenerator.generate(viewModel, "/Users/zhaorongyi/Documents/Learn/intranet_app_manager/out/test.plist");
// }
}