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 0000000..8694257 Binary files /dev/null and b/res/imgs/wx.png differ diff --git a/res/ipa-install.tmpl.html b/res/ipa-install.tmpl.html new file mode 100644 index 0000000..f9413a1 --- /dev/null +++ b/res/ipa-install.tmpl.html @@ -0,0 +1,71 @@ + + + + [[.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()) +}