From 255fcf684d3d8336f5a18251da1537ab282e405c Mon Sep 17 00:00:00 2001 From: codeskyblue Date: Thu, 28 Jul 2016 15:24:41 +0800 Subject: [PATCH] add ipa qrcode generate support --- httpstaticserver.go | 70 +++++++--- ipa.go | 276 ++++++++++++++++++++------------------ main.go | 7 +- res.go | 24 ++++ res/imgs/wx.png | Bin 0 -> 16609 bytes res/ipa-install.tmpl.html | 71 ++++++++++ res/js/index.js | 7 +- res/js/ua-parser.min.js | 9 ++ res_bindata.go | 37 ++--- res_nobindata.go | 39 +++--- zip.go | 221 +++++++++++++++++------------- zip_test.go | 15 +++ 12 files changed, 496 insertions(+), 280 deletions(-) create mode 100644 res.go create mode 100644 res/imgs/wx.png create mode 100644 res/ipa-install.tmpl.html create mode 100644 res/js/ua-parser.min.js create mode 100644 zip_test.go diff --git a/httpstaticserver.go b/httpstaticserver.go index 8872662..1e95577 100644 --- a/httpstaticserver.go +++ b/httpstaticserver.go @@ -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( + // `Click this link to install`, + // plistUrl))) } func (s *HTTPStaticServer) hFileOrDirectory(w http.ResponseWriter, r *http.Request) { diff --git a/ipa.go b/ipa.go index c2b2279..6d8f36c 100644 --- a/ipa.go +++ b/ipa.go @@ -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 +} diff --git a/main.go b/main.go index 428c535..8105246 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/res.go b/res.go new file mode 100644 index 0000000..708dc4e --- /dev/null +++ b/res.go @@ -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)) +} diff --git a/res/imgs/wx.png b/res/imgs/wx.png new file mode 100644 index 0000000000000000000000000000000000000000..8694257d0482229746b54ab63891c754764123cd GIT binary patch literal 16609 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

+ + + [[.Name]] install + + + + + + + + + +

+ + + + + + + diff --git a/res/js/index.js b/res/js/index.js index 73ca1b1..16b28f5 100644 --- a/res/js/index.js +++ b/res/js/index.js @@ -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 }); diff --git a/res/js/ua-parser.min.js b/res/js/ua-parser.min.js new file mode 100644 index 0000000..8dee15b --- /dev/null +++ b/res/js/ua-parser.min.js @@ -0,0 +1,9 @@ +/** + * UAParser.js v0.7.3 + * Lightweight JavaScript-based User-Agent string parser + * https://github.com/faisalman/ua-parser-js + * + * Copyright © 2012-2014 Faisal Salman + * Dual licensed under GPLv2 & MIT + */ +(function(window,undefined){"use strict";var LIBVERSION="0.7.3",EMPTY="",UNKNOWN="?",FUNC_TYPE="function",UNDEF_TYPE="undefined",OBJ_TYPE="object",MAJOR="major",MODEL="model",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",EMBEDDED="embedded";var util={extend:function(regexes,extensions){for(var i in extensions){if("browser cpu device engine os".indexOf(i)!==-1&&extensions[i].length%2===0){regexes[i]=extensions[i].concat(regexes[i])}}return regexes},has:function(str1,str2){if(typeof str1==="string"){return str2.toLowerCase().indexOf(str1.toLowerCase())!==-1}},lowerize:function(str){return str.toLowerCase()}};var mapper={rgx:function(){var result,i=0,j,k,p,q,matches,match,args=arguments;while(i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){result[q[0]]=q[1].call(this,match)}else{result[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){result[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{result[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){result[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{result[q]=match?match:undefined}}}}i+=2}return result},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j 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)) +} diff --git a/zip_test.go b/zip_test.go new file mode 100644 index 0000000..d0ca490 --- /dev/null +++ b/zip_test.go @@ -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()) +}