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/ioutil"
"log"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/gorilla/mux"
)
type HTTPStaticServer struct {
Root string
Theme string
Upload bool
Root string
Theme string
Upload bool
PlistProxy string
m *mux.Router
}
@@ -34,6 +37,7 @@ func NewHTTPStaticServer(root string) *HTTPStaticServer {
m.HandleFunc("/-/status", s.hStatus)
m.HandleFunc("/-/raw/{path:.*}", s.hFileOrDirectory)
m.HandleFunc("/-/zip/{path:.*}", s.hZip)
m.HandleFunc("/-/unzip/{zip_path:.*}/-/{path:.*}", s.hUnzip)
m.HandleFunc("/-/json/{path:.*}", s.hJSONList)
// routers for Apple *.ipa
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)
finfo, err := os.Stat(relPath)
if err == nil && finfo.IsDir() {
tmpl.Execute(w, s)
tmpl.ExecuteTemplate(w, "index", s)
// tmpl.Execute(w, s)
} else {
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))
}
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) {
// Useless now.
path := mux.Vars(r)["path"]
relPath := filepath.Join(s.Root, path)
data, err := parseIpaIcon(relPath)
@@ -124,6 +144,18 @@ func (s *HTTPStaticServer) hIpaIcon(w http.ResponseWriter, r *http.Request) {
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) {
path := mux.Vars(r)["path"]
// rename *.plist to *.ipa
@@ -137,22 +169,16 @@ func (s *HTTPStaticServer) hPlist(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 500)
return
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
ipaURL := url.URL{
baseURL := &url.URL{
Scheme: scheme,
Host: r.Host,
Path: path,
}
imgURL := url.URL{
Scheme: scheme,
Host: r.Host,
Path: filepath.Join("/-/ipa/icon", path),
}
// TODO: image ignore here.
data, err := generateDownloadPlist(ipaURL.String(), imgURL.String(), plinfo)
data, err := generateDownloadPlist(baseURL, path, plinfo)
if err != nil {
http.Error(w, err.Error(), 500)
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) {
// need ua_parser
w.Write([]byte("redirect to itms service")) // url need urjJoin if setted plist proxy
path := mux.Vars(r)["path"]
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
// Browser just tell not supported.
w.Header().Set("Content-Type", "text/html")
tmpl.ExecuteTemplate(w, "ipa-install", map[string]string{
"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) {

276
ipa.go
View File

@@ -1,133 +1,143 @@
package main
import (
"archive/zip"
"bytes"
"errors"
"io"
"io/ioutil"
"path/filepath"
"regexp"
goplist "github.com/DHowett/go-plist"
)
type plistBundle struct {
CFBundleIdentifier string `plist:"CFBundleIdentifier"`
CFBundleVersion string `plist:"CFBundleVersion"`
CFBundleDisplayName string `plist:"CFBundleDisplayName"`
CFBundleIconFile string `plist:"CFBundleIconFile"`
}
func parseIpaIcon(path string) (data []byte, err error) {
iconPattern := regexp.MustCompile(`(?i)^Payload/[^/]*/icon\.png$`)
r, err := zip.OpenReader(path)
if err != nil {
return
}
defer r.Close()
var zfile *zip.File
for _, file := range r.File {
if iconPattern.MatchString(file.Name) {
zfile = file
break
}
}
if zfile == nil {
err = errors.New("icon.png file not found")
return
}
plreader, err := zfile.Open()
if err != nil {
return
}
defer plreader.Close()
return ioutil.ReadAll(plreader)
}
func parseIPA(path string) (plinfo *plistBundle, err error) {
plistre := regexp.MustCompile(`^Payload/[^/]*/Info\.plist$`)
r, err := zip.OpenReader(path)
if err != nil {
return
}
defer r.Close()
var plfile *zip.File
for _, file := range r.File {
if plistre.MatchString(file.Name) {
plfile = file
break
}
}
if plfile == nil {
err = errors.New("Info.plist file not found")
return
}
plreader, err := plfile.Open()
if err != nil {
return
}
defer plreader.Close()
buf := make([]byte, plfile.FileInfo().Size())
_, err = io.ReadFull(plreader, buf)
if err != nil {
return
}
dec := goplist.NewDecoder(bytes.NewReader(buf))
plinfo = new(plistBundle)
err = dec.Decode(plinfo)
return
}
// ref: https://gist.github.com/frischmilch/b15d81eabb67925642bd#file_manifest.plist
type plAsset struct {
Kind string `plist:"kind"`
URL string `plist:"url"`
}
type plItem struct {
Assets []*plAsset `plist:"assets"`
Metadata struct {
BundleIdentifier string `plist:"bundle-identifier"`
BundleVersion string `plist:"bundle-version"`
Kind string `plist:"kind"`
Title string `plist:"title"`
} `plist:"metadata"`
}
type downloadPlist struct {
Items []*plItem `plist:"items"`
}
func generateDownloadPlist(ipaUrl, imgUrl string, plinfo *plistBundle) ([]byte, error) {
dp := new(downloadPlist)
item := new(plItem)
item.Assets = append(item.Assets, &plAsset{
Kind: "software-package",
URL: ipaUrl,
})
// FIXME(ssx): find icon from CFBundleIconFile
_ = imgUrl
// , &plAsset{
// Kind: "display-image",
// URL: imgUrl,
// })
item.Metadata.Kind = "software"
item.Metadata.BundleIdentifier = plinfo.CFBundleIdentifier
item.Metadata.BundleVersion = plinfo.CFBundleVersion
item.Metadata.Title = plinfo.CFBundleDisplayName
if item.Metadata.Title == "" {
item.Metadata.Title = filepath.Base(ipaUrl)
}
dp.Items = append(dp.Items, item)
data, err := goplist.MarshalIndent(dp, goplist.XMLFormat, " ")
// fmt.Println(string(data))
// fmt.Println(err)
return data, err
}
package main
import (
"archive/zip"
"bytes"
"errors"
"io"
"io/ioutil"
"net/url"
"path/filepath"
"regexp"
goplist "github.com/DHowett/go-plist"
)
func parseIpaIcon(path string) (data []byte, err error) {
iconPattern := regexp.MustCompile(`(?i)^Payload/[^/]*/icon\.png$`)
r, err := zip.OpenReader(path)
if err != nil {
return
}
defer r.Close()
var zfile *zip.File
for _, file := range r.File {
if iconPattern.MatchString(file.Name) {
zfile = file
break
}
}
if zfile == nil {
err = errors.New("icon.png file not found")
return
}
plreader, err := zfile.Open()
if err != nil {
return
}
defer plreader.Close()
return ioutil.ReadAll(plreader)
}
func parseIPA(path string) (plinfo *plistBundle, err error) {
plistre := regexp.MustCompile(`^Payload/[^/]*/Info\.plist$`)
r, err := zip.OpenReader(path)
if err != nil {
return
}
defer r.Close()
var plfile *zip.File
for _, file := range r.File {
if plistre.MatchString(file.Name) {
plfile = file
break
}
}
if plfile == nil {
err = errors.New("Info.plist file not found")
return
}
plreader, err := plfile.Open()
if err != nil {
return
}
defer plreader.Close()
buf := make([]byte, plfile.FileInfo().Size())
_, err = io.ReadFull(plreader, buf)
if err != nil {
return
}
dec := goplist.NewDecoder(bytes.NewReader(buf))
plinfo = new(plistBundle)
err = dec.Decode(plinfo)
return
}
type plistBundle struct {
CFBundleIdentifier string `plist:"CFBundleIdentifier"`
CFBundleVersion string `plist:"CFBundleVersion"`
CFBundleDisplayName string `plist:"CFBundleDisplayName"`
CFBundleName string `plist:"CFBundleName"`
CFBundleIconFile string `plist:"CFBundleIconFile"`
CFBundleIcons struct {
CFBundlePrimaryIcon struct {
CFBundleIconFiles []string `plist:"CFBundleIconFiles"`
} `plist:"CFBundlePrimaryIcon"`
} `plist:"CFBundleIcons"`
}
// ref: https://gist.github.com/frischmilch/b15d81eabb67925642bd#file_manifest.plist
type plAsset struct {
Kind string `plist:"kind"`
URL string `plist:"url"`
}
type plItem struct {
Assets []*plAsset `plist:"assets"`
Metadata struct {
BundleIdentifier string `plist:"bundle-identifier"`
BundleVersion string `plist:"bundle-version"`
Kind string `plist:"kind"`
Title string `plist:"title"`
} `plist:"metadata"`
}
type downloadPlist struct {
Items []*plItem `plist:"items"`
}
func generateDownloadPlist(baseURL *url.URL, ipaPath string, plinfo *plistBundle) ([]byte, error) {
dp := new(downloadPlist)
item := new(plItem)
baseURL.Path = ipaPath
ipaUrl := baseURL.String()
item.Assets = append(item.Assets, &plAsset{
Kind: "software-package",
URL: ipaUrl,
})
iconFiles := plinfo.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles
if iconFiles != nil && len(iconFiles) > 0 {
baseURL.Path = "/-/unzip/" + ipaPath + "/-/**/" + iconFiles[0] + ".png"
imgUrl := baseURL.String()
item.Assets = append(item.Assets, &plAsset{
Kind: "display-image",
URL: imgUrl,
})
}
item.Metadata.Kind = "software"
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
}
var gcfg = Configure{}
var (
gcfg = Configure{}
)
func parseFlags() {
kingpin.HelpFlag.Short('h')
@@ -52,6 +54,9 @@ func main() {
if gcfg.Upload {
ss.EnableUpload()
}
if gcfg.PlistProxy != nil {
ss.PlistProxy = gcfg.PlistProxy.String()
}
var hdlr http.Handler = ss
// 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;
},
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-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({
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
package main
import (
"html/template"
"net/http"
)
var tmpl *template.Template
func init() {
http.Handle("/-/res/", http.StripPrefix("/-/res/", http.FileServer(assetFS())))
indexContent, _ := Asset("res/index.tmpl.html")
tmpl = template.Must(template.New("t").Delims("[[", "]]").Parse(string(indexContent)))
}
// +build bindata
package main
import (
"log"
"net/http"
)
func init() {
http.Handle("/-/res/", http.StripPrefix("/-/res/", http.FileServer(assetFS())))
for name, path := range templates {
data, err := Asset(path)
if err != nil {
log.Fatal(err)
}
ParseTemplate(name, string(data))
}
}

View File

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

221
zip.go
View File

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