diff --git a/handler.go b/handler.go index c059176..5be7a9a 100644 --- a/handler.go +++ b/handler.go @@ -3,6 +3,7 @@ package main import ( "archive/zip" "context" + "crypto/md5" "encoding/json" "errors" "fmt" @@ -26,6 +27,7 @@ var ( infoP = regexp.MustCompile(`^/dl/(.+)/@v/(.+)\.info$`) modP = regexp.MustCompile(`^/dl/(.+)/@v/(.+)\.mod$`) zipP = regexp.MustCompile(`^/dl/(.+)/@v/(.+)\.zip$`) + uploadP = regexp.MustCompile(`^/ul/(.+)/@v/(.+)\.zip$`) ) type handler struct { @@ -138,6 +140,13 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + if matches := uploadP.FindStringSubmatch(r.URL.Path); matches != nil { + modpath := matches[1] + modversion := matches[2] + h.upload(modpath, modversion, w, r) + return + } + w.WriteHeader(http.StatusNotFound) w.Write([]byte("not found")) return @@ -168,7 +177,7 @@ func writeError(w http.ResponseWriter, err error) { } w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "internal server error") + fmt.Fprintf(w, "internal server error: %v", err) log_error.Printf("500 %v", err) return } @@ -273,6 +282,12 @@ func (h handler) zipPath(modpath, version string) string { return filepath.Join(absdir, fmt.Sprintf("%s@%s.zip", basename, version)) } +func (h handler) uploadPath(modpath, version string) string { + hash := md5.Sum([]byte(fmt.Sprintf("%s@%s", modpath, version))) + fname := fmt.Sprintf("%x.zip", hash) + return filepath.Join(h.root, "uploads", fname) +} + func (h handler) openZip(modpath, version string) (io.ReadCloser, error) { return os.Open(h.zipPath(modpath, version)) } @@ -314,6 +329,59 @@ func (h handler) zipfile(modpath, modversion string, w http.ResponseWriter, r *h } } +func (h handler) upload(modpath, modversion string, w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + writeError(w, apiError(http.StatusMethodNotAllowed)) + return + } + + p, err := h.doUpload(modpath, modversion, r) + if err != nil { + writeError(w, err) + return + } + + if err := h.verifyUpload(modpath, modversion, p); err != nil { + writeError(w, err) + return + } + + if err := os.Rename(p, h.zipPath(modpath, modversion)); err != nil { + writeError(w, fmt.Errorf("unable to move upload into place: %w", err)) + return + } + w.Write([]byte("ok")) +} + +func (h handler) doUpload(modpath, modversion string, r *http.Request) (string, error) { + p := h.uploadPath(modpath, modversion) + f, err := os.Create(p) + if err != nil { + return "", fmt.Errorf("unable to open destination path: %w", err) + } + defer f.Close() + + if _, err := io.Copy(f, r.Body); err != nil { + return "", fmt.Errorf("failed to write upload file locally: %w", err) + } + return p, nil +} + +func (h handler) verifyUpload(modpath, modversion, fpath string) error { + rc, err := zip.OpenReader(fpath) + if err != nil { + return fmt.Errorf("unable to verify upload: %w", err) + } + + prefix := fmt.Sprintf("%s@%s/", modpath, modversion) + for _, f := range rc.File { + if !strings.HasPrefix(f.Name, prefix) { + return fmt.Errorf("zip contains file with bad name: %w", err) + } + } + return nil +} + type versionInfo struct { Version string // version string Time time.Time // commit time