add ipa qrcode generate support

This commit is contained in:
codeskyblue
2016-07-28 15:24:41 +08:00
parent 3f33e1723a
commit 255fcf684d
12 changed files with 496 additions and 280 deletions

View File

@@ -5,18 +5,21 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"mime"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
type HTTPStaticServer struct { type HTTPStaticServer struct {
Root string Root string
Theme string Theme string
Upload bool Upload bool
PlistProxy string
m *mux.Router m *mux.Router
} }
@@ -34,6 +37,7 @@ func NewHTTPStaticServer(root string) *HTTPStaticServer {
m.HandleFunc("/-/status", s.hStatus) m.HandleFunc("/-/status", s.hStatus)
m.HandleFunc("/-/raw/{path:.*}", s.hFileOrDirectory) m.HandleFunc("/-/raw/{path:.*}", s.hFileOrDirectory)
m.HandleFunc("/-/zip/{path:.*}", s.hZip) m.HandleFunc("/-/zip/{path:.*}", s.hZip)
m.HandleFunc("/-/unzip/{zip_path:.*}/-/{path:.*}", s.hUnzip)
m.HandleFunc("/-/json/{path:.*}", s.hJSONList) m.HandleFunc("/-/json/{path:.*}", s.hJSONList)
// routers for Apple *.ipa // routers for Apple *.ipa
m.HandleFunc("/-/ipa/icon/{path:.*}", s.hIpaIcon) m.HandleFunc("/-/ipa/icon/{path:.*}", s.hIpaIcon)
@@ -101,7 +105,8 @@ func (s *HTTPStaticServer) hIndex(w http.ResponseWriter, r *http.Request) {
relPath := filepath.Join(s.Root, path) relPath := filepath.Join(s.Root, path)
finfo, err := os.Stat(relPath) finfo, err := os.Stat(relPath)
if err == nil && finfo.IsDir() { if err == nil && finfo.IsDir() {
tmpl.Execute(w, s) tmpl.ExecuteTemplate(w, "index", s)
// tmpl.Execute(w, s)
} else { } else {
http.ServeFile(w, r, relPath) http.ServeFile(w, r, relPath)
} }
@@ -112,7 +117,22 @@ func (s *HTTPStaticServer) hZip(w http.ResponseWriter, r *http.Request) {
CompressToZip(w, filepath.Join(s.Root, path)) CompressToZip(w, filepath.Join(s.Root, path))
} }
func (s *HTTPStaticServer) hUnzip(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
zipPath, path := vars["zip_path"], vars["path"]
ctype := mime.TypeByExtension(filepath.Ext(path))
if ctype != "" {
w.Header().Set("Content-Type", ctype)
}
err := ExtractFromZip(filepath.Join(s.Root, zipPath), path, w)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
func (s *HTTPStaticServer) hIpaIcon(w http.ResponseWriter, r *http.Request) { func (s *HTTPStaticServer) hIpaIcon(w http.ResponseWriter, r *http.Request) {
// Useless now.
path := mux.Vars(r)["path"] path := mux.Vars(r)["path"]
relPath := filepath.Join(s.Root, path) relPath := filepath.Join(s.Root, path)
data, err := parseIpaIcon(relPath) data, err := parseIpaIcon(relPath)
@@ -124,6 +144,18 @@ func (s *HTTPStaticServer) hIpaIcon(w http.ResponseWriter, r *http.Request) {
w.Write(data) w.Write(data)
} }
func genURLStr(r *http.Request, path string) *url.URL {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
return &url.URL{
Scheme: scheme,
Host: r.Host,
Path: path,
}
}
func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) { func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
path := mux.Vars(r)["path"] path := mux.Vars(r)["path"]
// rename *.plist to *.ipa // rename *.plist to *.ipa
@@ -137,22 +169,16 @@ func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
scheme := "http" scheme := "http"
if r.TLS != nil { if r.TLS != nil {
scheme = "https" scheme = "https"
} }
ipaURL := url.URL{ baseURL := &url.URL{
Scheme: scheme, Scheme: scheme,
Host: r.Host, Host: r.Host,
Path: path,
} }
imgURL := url.URL{ data, err := generateDownloadPlist(baseURL, path, plinfo)
Scheme: scheme,
Host: r.Host,
Path: filepath.Join("/-/ipa/icon", path),
}
// TODO: image ignore here.
data, err := generateDownloadPlist(ipaURL.String(), imgURL.String(), plinfo)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
@@ -162,12 +188,20 @@ func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
} }
func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) { func (s *HTTPStaticServer) hIpaLink(w http.ResponseWriter, r *http.Request) {
// need ua_parser path := mux.Vars(r)["path"]
w.Write([]byte("redirect to itms service")) // url need urjJoin if setted plist proxy plistUrl := genURLStr(r, "/-/ipa/plist/"+path).String()
if s.PlistProxy != "" {
plistUrl = strings.TrimSuffix(s.PlistProxy, "/") + "/" + r.Host + "/-/ipa/plist/" + path
}
// WeChat need to tell to open with Safari w.Header().Set("Content-Type", "text/html")
tmpl.ExecuteTemplate(w, "ipa-install", map[string]string{
// Browser just tell not supported. "Name": filepath.Base(path),
"PlistLink": plistUrl,
})
// w.Write([]byte(fmt.Sprintf(
// `<a href='itms-services://?action=download-manifest&url=%s'>Click this link to install</a>`,
// plistUrl)))
} }
func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Request) { func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Request) {

276
ipa.go
View File

@@ -1,133 +1,143 @@
package main package main
import ( import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"errors" "errors"
"io" "io"
"io/ioutil" "io/ioutil"
"path/filepath" "net/url"
"regexp" "path/filepath"
"regexp"
goplist "github.com/DHowett/go-plist"
) goplist "github.com/DHowett/go-plist"
)
type plistBundle struct {
CFBundleIdentifier string `plist:"CFBundleIdentifier"` func parseIpaIcon(path string) (data []byte, err error) {
CFBundleVersion string `plist:"CFBundleVersion"` iconPattern := regexp.MustCompile(`(?i)^Payload/[^/]*/icon\.png$`)
CFBundleDisplayName string `plist:"CFBundleDisplayName"` r, err := zip.OpenReader(path)
CFBundleIconFile string `plist:"CFBundleIconFile"` if err != nil {
} return
}
func parseIpaIcon(path string) (data []byte, err error) { defer r.Close()
iconPattern := regexp.MustCompile(`(?i)^Payload/[^/]*/icon\.png$`)
r, err := zip.OpenReader(path) var zfile *zip.File
if err != nil { for _, file := range r.File {
return if iconPattern.MatchString(file.Name) {
} zfile = file
defer r.Close() break
}
var zfile *zip.File }
for _, file := range r.File { if zfile == nil {
if iconPattern.MatchString(file.Name) { err = errors.New("icon.png file not found")
zfile = file return
break }
} plreader, err := zfile.Open()
} if err != nil {
if zfile == nil { return
err = errors.New("icon.png file not found") }
return defer plreader.Close()
} return ioutil.ReadAll(plreader)
plreader, err := zfile.Open() }
if err != nil {
return func parseIPA(path string) (plinfo *plistBundle, err error) {
} plistre := regexp.MustCompile(`^Payload/[^/]*/Info\.plist$`)
defer plreader.Close() r, err := zip.OpenReader(path)
return ioutil.ReadAll(plreader) if err != nil {
} return
}
func parseIPA(path string) (plinfo *plistBundle, err error) { defer r.Close()
plistre := regexp.MustCompile(`^Payload/[^/]*/Info\.plist$`)
r, err := zip.OpenReader(path) var plfile *zip.File
if err != nil { for _, file := range r.File {
return if plistre.MatchString(file.Name) {
} plfile = file
defer r.Close() break
}
var plfile *zip.File }
for _, file := range r.File { if plfile == nil {
if plistre.MatchString(file.Name) { err = errors.New("Info.plist file not found")
plfile = file return
break }
} plreader, err := plfile.Open()
} if err != nil {
if plfile == nil { return
err = errors.New("Info.plist file not found") }
return defer plreader.Close()
} buf := make([]byte, plfile.FileInfo().Size())
plreader, err := plfile.Open() _, err = io.ReadFull(plreader, buf)
if err != nil { if err != nil {
return return
} }
defer plreader.Close() dec := goplist.NewDecoder(bytes.NewReader(buf))
buf := make([]byte, plfile.FileInfo().Size()) plinfo = new(plistBundle)
_, err = io.ReadFull(plreader, buf) err = dec.Decode(plinfo)
if err != nil { return
return }
}
dec := goplist.NewDecoder(bytes.NewReader(buf)) type plistBundle struct {
plinfo = new(plistBundle) CFBundleIdentifier string `plist:"CFBundleIdentifier"`
err = dec.Decode(plinfo) CFBundleVersion string `plist:"CFBundleVersion"`
return CFBundleDisplayName string `plist:"CFBundleDisplayName"`
} CFBundleName string `plist:"CFBundleName"`
CFBundleIconFile string `plist:"CFBundleIconFile"`
// ref: https://gist.github.com/frischmilch/b15d81eabb67925642bd#file_manifest.plist CFBundleIcons struct {
type plAsset struct { CFBundlePrimaryIcon struct {
Kind string `plist:"kind"` CFBundleIconFiles []string `plist:"CFBundleIconFiles"`
URL string `plist:"url"` } `plist:"CFBundlePrimaryIcon"`
} } `plist:"CFBundleIcons"`
}
type plItem struct {
Assets []*plAsset `plist:"assets"` // ref: https://gist.github.com/frischmilch/b15d81eabb67925642bd#file_manifest.plist
Metadata struct { type plAsset struct {
BundleIdentifier string `plist:"bundle-identifier"` Kind string `plist:"kind"`
BundleVersion string `plist:"bundle-version"` URL string `plist:"url"`
Kind string `plist:"kind"` }
Title string `plist:"title"`
} `plist:"metadata"` type plItem struct {
} Assets []*plAsset `plist:"assets"`
Metadata struct {
type downloadPlist struct { BundleIdentifier string `plist:"bundle-identifier"`
Items []*plItem `plist:"items"` BundleVersion string `plist:"bundle-version"`
} Kind string `plist:"kind"`
Title string `plist:"title"`
func generateDownloadPlist(ipaUrl, imgUrl string, plinfo *plistBundle) ([]byte, error) { } `plist:"metadata"`
dp := new(downloadPlist) }
item := new(plItem)
item.Assets = append(item.Assets, &plAsset{ type downloadPlist struct {
Kind: "software-package", Items []*plItem `plist:"items"`
URL: ipaUrl, }
})
func generateDownloadPlist(baseURL *url.URL, ipaPath string, plinfo *plistBundle) ([]byte, error) {
// FIXME(ssx): find icon from CFBundleIconFile dp := new(downloadPlist)
_ = imgUrl item := new(plItem)
// , &plAsset{ baseURL.Path = ipaPath
// Kind: "display-image", ipaUrl := baseURL.String()
// URL: imgUrl, item.Assets = append(item.Assets, &plAsset{
// }) Kind: "software-package",
URL: ipaUrl,
item.Metadata.Kind = "software" })
item.Metadata.BundleIdentifier = plinfo.CFBundleIdentifier iconFiles := plinfo.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles
item.Metadata.BundleVersion = plinfo.CFBundleVersion if iconFiles != nil && len(iconFiles) > 0 {
item.Metadata.Title = plinfo.CFBundleDisplayName baseURL.Path = "/-/unzip/" + ipaPath + "/-/**/" + iconFiles[0] + ".png"
if item.Metadata.Title == "" { imgUrl := baseURL.String()
item.Metadata.Title = filepath.Base(ipaUrl) item.Assets = append(item.Assets, &plAsset{
} Kind: "display-image",
URL: imgUrl,
dp.Items = append(dp.Items, item) })
data, err := goplist.MarshalIndent(dp, goplist.XMLFormat, " ") }
// fmt.Println(string(data))
// fmt.Println(err) item.Metadata.Kind = "software"
return data, err
} item.Metadata.BundleIdentifier = plinfo.CFBundleIdentifier
item.Metadata.BundleVersion = plinfo.CFBundleVersion
item.Metadata.Title = plinfo.CFBundleName
if item.Metadata.Title == "" {
item.Metadata.Title = filepath.Base(ipaUrl)
}
dp.Items = append(dp.Items, item)
data, err := goplist.MarshalIndent(dp, goplist.XMLFormat, " ")
return data, err
}

View File

@@ -25,7 +25,9 @@ type Configure struct {
PlistProxy *url.URL PlistProxy *url.URL
} }
var gcfg = Configure{} var (
gcfg = Configure{}
)
func parseFlags() { func parseFlags() {
kingpin.HelpFlag.Short('h') kingpin.HelpFlag.Short('h')
@@ -52,6 +54,9 @@ func main() {
if gcfg.Upload { if gcfg.Upload {
ss.EnableUpload() ss.EnableUpload()
} }
if gcfg.PlistProxy != nil {
ss.PlistProxy = gcfg.PlistProxy.String()
}
var hdlr http.Handler = ss var hdlr http.Handler = ss
// HTTP Basic Authentication // HTTP Basic Authentication

24
res.go Normal file
View File

@@ -0,0 +1,24 @@
package main
import "html/template"
var (
tmpl *template.Template
templates = map[string]string{
"index": "res/index.tmpl.html",
"ipa-install": "res/ipa-install.tmpl.html",
}
)
func ParseTemplate(name string, content string) {
if tmpl == nil {
tmpl = template.New(name)
}
var t *template.Template
if tmpl.Name() == name {
t = tmpl
} else {
t = tmpl.New(name)
}
template.Must(t.New(name).Delims("[[", "]]").Parse(content))
}

BIN
res/imgs/wx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

71
res/ipa-install.tmpl.html Normal file
View File

@@ -0,0 +1,71 @@
<html>
<head>
<title>[[.Name]] install</title>
<meta http-equiv="Content-Type" content="text/HTML; charset=utf-8">
<meta content="target-densitydpi=device-dpi,width=640" name="viewport" id="viewport">
<link rel="shortcut icon" type="image/png" href="/-/res/favicon.png" />
<script type="text/javascript" src="/-/res/js/ua-parser.min.js"></script>
<script type="text/javascript">
function showById(name) {
document.getElementById(name).style.display = 'block';
}
function checkBrowerAndDownload() {
var parser = new UAParser();
var os_info = parser.getOS();
console.log(os_info)
if (navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == "micromessenger") {
showById('wechat');
return;
}
var plistLink = "[[.PlistLink]]";
var ipaInstallLink = 'itms-services://?action=download-manifest&url=' + plistLink;
document.getElementById('itms-link').href = ipaInstallLink;
if (os_info.name == 'Android') {
return;
} else if (os_info.name == 'iOS') {
showById('safari');
location.href = ipaInstallLink;
return;
} else {
showById('browser');
return;
}
}
</script>
</head>
<body>
<style>
#wechat {
position: relative;
width: 640px;
margin: 0 auto;
background: #fff;
overflow: hidden;
min-height: 777px;
}
</style>
<div id="wechat" style="display: none">
<img style='width: 100%;position: relative;' src='/-/res/imgs/wx.png' />
</div>
<div id="browser" style="display: none">
This is IPA install page, you should open this link with your iPhone.
</div>
<div id="safari" style="display: none">
If install not started soon, click <a id="itms-link" href="#">here</a>
</div>
<div id="android" style="display: none">
This is IPA install page, not for android.
</div>
<script type="text/javascript">
checkBrowerAndDownload();
</script>
</body>
</html>

View File

@@ -82,9 +82,14 @@ var vm = new Vue({
this.showHidden = !this.showHidden; this.showHidden = !this.showHidden;
}, },
genQrcode: function(text) { genQrcode: function(text) {
var urlPath = location.protocol + "//" + location.host + location.pathname + text; var urlPath = location.protocol + "//" + pathJoin([location.host, location.pathname, text]);
$("#qrcode-title").html(text); $("#qrcode-title").html(text);
$("#qrcode-link").attr("href", urlPath); $("#qrcode-link").attr("href", urlPath);
if (getExtention(text) == "ipa") {
urlPath = location.protocol + "//" + pathJoin([location.host, "/-/ipa/link", location.pathname, text]);
console.log(urlPath)
}
$('#qrcodeCanvas').empty().qrcode({ $('#qrcodeCanvas').empty().qrcode({
text: urlPath text: urlPath
}); });

9
res/js/ua-parser.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,17 +1,20 @@
// +build bindata // +build bindata
package main package main
import ( import (
"html/template" "log"
"net/http" "net/http"
) )
var tmpl *template.Template func init() {
http.Handle("/-/res/", http.StripPrefix("/-/res/", http.FileServer(assetFS())))
func init() {
http.Handle("/-/res/", http.StripPrefix("/-/res/", http.FileServer(assetFS()))) for name, path := range templates {
data, err := Asset(path)
indexContent, _ := Asset("res/index.tmpl.html") if err != nil {
tmpl = template.Must(template.New("t").Delims("[[", "]]").Parse(string(indexContent))) log.Fatal(err)
} }
ParseTemplate(name, string(data))
}
}

View File

@@ -1,18 +1,21 @@
// +build !bindata // +build !bindata
package main package main
import ( import (
"html/template" "io/ioutil"
"io/ioutil" "log"
"net/http" "net/http"
) )
var tmpl *template.Template func init() {
http.Handle("/-/res/", http.StripPrefix("/-/res/", http.FileServer(http.Dir("./res"))))
func init() {
http.Handle("/-/res/", http.StripPrefix("/-/res/", http.FileServer(http.Dir("./res")))) for name, path := range templates {
content, err := ioutil.ReadFile(path)
indexContent, _ := ioutil.ReadFile("./res/index.tmpl.html") if err != nil {
tmpl = template.Must(template.New("t").Delims("[[", "]]").Parse(string(indexContent))) log.Fatal(err)
} }
ParseTemplate(name, string(content))
}
}

221
zip.go
View File

@@ -1,92 +1,129 @@
package main package main
import ( import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"io" "fmt"
"io/ioutil" "io"
"net/http" "io/ioutil"
"os" "net/http"
"path/filepath" "os"
"runtime" "path/filepath"
"strings" "runtime"
) "strconv"
"strings"
type Zip struct {
*zip.Writer dkignore "github.com/codeskyblue/dockerignore"
} )
func sanitizedName(filename string) string { type Zip struct {
if len(filename) > 1 && filename[1] == ':' && *zip.Writer
runtime.GOOS == "windows" { }
filename = filename[2:]
} func sanitizedName(filename string) string {
filename = strings.TrimLeft(strings.Replace(filename, `\`, "/", -1), `/`) if len(filename) > 1 && filename[1] == ':' &&
filename = filepath.ToSlash(filename) runtime.GOOS == "windows" {
filename = filepath.Clean(filename) filename = filename[2:]
return filename }
} filename = strings.TrimLeft(strings.Replace(filename, `\`, "/", -1), `/`)
filename = filepath.ToSlash(filename)
func statFile(filename string) (info os.FileInfo, reader io.ReadCloser, err error) { filename = filepath.Clean(filename)
info, err = os.Lstat(filename) return filename
if err != nil { }
return
} func statFile(filename string) (info os.FileInfo, reader io.ReadCloser, err error) {
// content info, err = os.Lstat(filename)
if info.Mode()&os.ModeSymlink != 0 { if err != nil {
var target string return
target, err = os.Readlink(filename) }
if err != nil { // content
return if info.Mode()&os.ModeSymlink != 0 {
} var target string
reader = ioutil.NopCloser(bytes.NewBuffer([]byte(target))) target, err = os.Readlink(filename)
} else if !info.IsDir() { if err != nil {
reader, err = os.Open(filename) return
if err != nil { }
return reader = ioutil.NopCloser(bytes.NewBuffer([]byte(target)))
} } else if !info.IsDir() {
} else { reader, err = os.Open(filename)
reader = ioutil.NopCloser(bytes.NewBuffer(nil)) if err != nil {
} return
return }
} } else {
reader = ioutil.NopCloser(bytes.NewBuffer(nil))
func (z *Zip) Add(relpath, abspath string) error { }
info, rdc, err := statFile(abspath) return
if err != nil { }
return err
} func (z *Zip) Add(relpath, abspath string) error {
defer rdc.Close() info, rdc, err := statFile(abspath)
if err != nil {
hdr, err := zip.FileInfoHeader(info) return err
if err != nil { }
return err defer rdc.Close()
}
hdr.Name = sanitizedName(relpath) hdr, err := zip.FileInfoHeader(info)
if info.IsDir() { if err != nil {
hdr.Name += "/" return err
} }
hdr.Method = zip.Deflate // compress method hdr.Name = sanitizedName(relpath)
writer, err := z.CreateHeader(hdr) if info.IsDir() {
if err != nil { hdr.Name += "/"
return err }
} hdr.Method = zip.Deflate // compress method
_, err = io.Copy(writer, rdc) writer, err := z.CreateHeader(hdr)
return err if err != nil {
} return err
}
func CompressToZip(w http.ResponseWriter, rootDir string) { _, err = io.Copy(writer, rdc)
rootDir = filepath.Clean(rootDir) return err
zipFileName := filepath.Base(rootDir) + ".zip" }
w.Header().Set("Content-Type", "application/zip") func CompressToZip(w http.ResponseWriter, rootDir string) {
w.Header().Set("Content-Disposition", `attachment; filename="`+zipFileName+`"`) rootDir = filepath.Clean(rootDir)
zipFileName := filepath.Base(rootDir) + ".zip"
zw := &Zip{Writer: zip.NewWriter(w)}
defer zw.Close() w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", `attachment; filename="`+zipFileName+`"`)
filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
zipPath := path[len(rootDir):] zw := &Zip{Writer: zip.NewWriter(w)}
return zw.Add(zipPath, path) defer zw.Close()
})
} filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
zipPath := path[len(rootDir):]
return zw.Add(zipPath, path)
})
}
func ExtractFromZip(zipFile, path string, w io.Writer) (err error) {
cf, err := zip.OpenReader(zipFile)
if err != nil {
return
}
defer cf.Close()
rd := ioutil.NopCloser(bytes.NewBufferString(path))
patterns, err := dkignore.ReadIgnore(rd)
if err != nil {
return
}
for _, file := range cf.File {
matched, _ := dkignore.Matches(file.Name, patterns)
if !matched {
continue
}
rc, er := file.Open()
if er != nil {
err = er
return
}
defer rc.Close()
_, err = io.Copy(w, rc)
if err != nil {
return
}
return
}
return fmt.Errorf("File %s not found", strconv.Quote(path))
}

15
zip_test.go Normal file
View File

@@ -0,0 +1,15 @@
package main
import (
"bytes"
"testing"
)
func TestExtractFromZip(t *testing.T) {
buf := bytes.NewBuffer(nil)
err := ExtractFromZip("testdata/test.zip", "*/foo.txt", buf)
if err != nil {
t.Fatal(err)
}
t.Log("Content: " + buf.String())
}