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 @@ + + +
+