Add goproxy local repositories (#143)
Private Go modules have nowhere to live: goproxy is remote-only, so internal modules need VCS access or a hand-rolled proxy. - serve local goproxy repos over the GOPROXY protocol (`@v/list`, `@latest`, `.info`/`.mod`/`.zip`) with case-encoded module paths - accept PUT uploads of `.zip`/`.mod`/`.info` per version; synthesize `.info` when absent - validate zips with `golang.org/x/mod/zip.CheckZip`, `.mod` module path and `.info` version before storing - add a ContentValidator hook so providers can check upload bytes - document publishing in the UI and `docs/goproxy/local.md`; add an e2e-docker case Reviewed-on: #143 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #143.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@@ -83,7 +84,22 @@ func (h *LocalHandler) uploadValidated(w http.ResponseWriter, r *http.Request, r
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.cas.Store(r.Context(), r.Body, contentType)
|
||||
var body io.Reader = r.Body
|
||||
if cv, ok := prov.(provider.ContentValidator); ok {
|
||||
f, err := spoolBody(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("read upload: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer discard(f)
|
||||
if err := cv.ValidateContent(storagePath, f.Name()); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
body = f
|
||||
}
|
||||
|
||||
result, err := h.cas.Store(r.Context(), body, contentType)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("store failed: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -110,6 +126,27 @@ func (h *LocalHandler) uploadValidated(w http.ResponseWriter, r *http.Request, r
|
||||
writeJSON(w, http.StatusCreated, uploader.UploadResponse(storagePath, result.ContentHash, result.SizeBytes))
|
||||
}
|
||||
|
||||
func spoolBody(r io.Reader) (*os.File, error) {
|
||||
f, err := os.CreateTemp("", "artifactapi-upload-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.Copy(f, r); err != nil {
|
||||
discard(f)
|
||||
return nil, err
|
||||
}
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
discard(f)
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func discard(f *os.File) {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(f.Name())
|
||||
}
|
||||
|
||||
func (h *LocalHandler) uploadGeneric(w http.ResponseWriter, r *http.Request, remote *models.Remote, filePath string) {
|
||||
existing, err := h.db.GetLocalFile(r.Context(), remote.Name, filePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/database"
|
||||
_ "git.unkin.net/unkin/artifactapi/internal/provider/goproxy"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
// TestLocalUploadContentRejected verifies a ContentValidator provider rejects
|
||||
// bad bytes with 400 before anything is stored.
|
||||
func TestLocalUploadContentRejected(t *testing.T) {
|
||||
if testDSN == "" {
|
||||
t.Skip("Docker unavailable")
|
||||
}
|
||||
ctx := context.Background()
|
||||
db, err := database.New(testDSN)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
const repo = "go-local-validate"
|
||||
if err := db.CreateRemote(ctx, &models.Remote{Name: repo, PackageType: models.PackageGoProxy, RepoType: models.RepoTypeLocal}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Route("/remotes/{name}/files", func(r chi.Router) {
|
||||
r.Put("/*", NewLocalHandler(db, nil).Routes().ServeHTTP)
|
||||
})
|
||||
|
||||
cases := map[string]string{
|
||||
"example.unkin.net/hello/@v/v0.1.0.zip": "not a zip",
|
||||
"example.unkin.net/hello/@v/v0.1.0.mod": "module example.unkin.net/other\n",
|
||||
"example.unkin.net/hello/@v/v0.1.0.info": `{"Version":"v9.9.9"}`,
|
||||
"example.unkin.net/Hello/@v/v0.1.0.mod": "module example.unkin.net/Hello\n",
|
||||
}
|
||||
for path, body := range cases {
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, httptest.NewRequest("PUT", "/remotes/"+repo+"/files/"+path, strings.NewReader(body)))
|
||||
if w.Code != 400 {
|
||||
t.Errorf("%s: status %d, want 400 (%s)", path, w.Code, w.Body.String())
|
||||
}
|
||||
if f, _ := db.GetLocalFile(ctx, repo, path); f != nil {
|
||||
t.Errorf("%s: rejected upload was recorded", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user