As it did Warehouse and PyPI.
cd packages
for pkg in * ; do
# Assume running:
- # GOCHEESE_NO_SYNC=1 gocheese -bind "[::1]:8080" -gpgupdate /gpgupdate/
- curl http://localhost:8080/gpgupdate/$pkg/ > /dev/null
+ # GOCHEESE_NO_SYNC=1 gocheese -bind "[::1]:8080" -refresh /simple/
+ curl http://localhost:8080/simple/$pkg/ > /dev/null
done
########################################################################
@item Supports uploading of internal packages through the standard
Warehouse API, including signatures, metadata and checksums.
-@item Supports @url{https://pythonwheels.com/, wheels}, GPG signatures,
+@item Supports @url{https://pythonwheels.com/, wheels},
@url{https://packaging.python.org/specifications/core-metadata/, Metadata}
with @url{https://www.python.org/dev/peps/pep-0566/, PEP-0566} compatible
conversion to JSON, multiple (MD5, SHA256, SHA512, BLAKE2b-256) integrity
Same as above, but does not refresh data from the upstream, completely
read only mode.
-@item @code{/gpgupdate/} (@option{-gpgupdate} option)
-Refresh the package state from the upstream as above, but additionally
-check and download missing GPG signatures. Intended to be used only
-manually, for example after database migration.
-It is probably useful to set @env{$GOCHEESE_NO_SYNC=1} environment
-variable to turn off filesystem synchronization calls.
-
@item @code{/pypi/} (@option{-json} option)
Read only (non refreshing) JSON API entrypoint, giving metadata for the
packages and releases.
| +- public-package-0.1.tar.gz.blake2_256
| +- public-package-0.1.1.tar.gz.blake2_256
| +- public-package-0.2.tar.gz
- | +- public-package-0.2.tar.gz.asc
| +- public-package-0.2.tar.gz.sha256
| +- public-package-0.2.tar.gz.blake2_256
+-- private-package
| +- .internal
| +- .metadata.rec
| +- private-package-0.1.tar.gz
- | +- private-package-0.1.tar.gz.asc
| +- private-package-0.1.tar.gz.sha256
| +- private-package-0.1.tar.gz.blake2_256
|...
long time ago with MD5 checksum. @code{0.1.1} version is downloaded more
recently with BLAKE2b-256 checksum, also storing that checksum for
@code{0.1}. @code{0.2} version is downloaded tarball, having forced
-SHA256 and BLAKE2b-256 recalculated checksums. Also upstream has
-corresponding @file{.asc} signature file.
+SHA256 and BLAKE2b-256 recalculated checksums.
@file{private-package} is private package, because it contains
@file{.internal} file. It can be uploaded and queries to it are not
All metadata information sent by @command{twine} is stored on the disk.
Package creation time will be server's current time. If @command{twine}
-send package checksums, then they are checked against. GPG signature
-file is also saved.
+send package checksums, then they are checked against.
-module go.cypherpunks.ru/gocheese/v3
+module go.cypherpunks.ru/gocheese/v4
go 1.17
// Version format is too complicated: https://www.python.org/dev/peps/pep-0386/
// So here is very simple parser working good enough for most packages
func filenameToVersion(fn string) string {
- fn = strings.TrimSuffix(fn, GPGSigExt)
var trimmed string
for _, ext := range KnownExts {
trimmed = strings.TrimSuffix(fn, ext)
}
delete(files, fnClean)
}
- if _, exists := files[fnClean+GPGSigExt]; exists {
- release.HasSig = true
- delete(files, fnClean+GPGSigExt)
- }
}
release.Digests[algo] = hex.EncodeToString(digest)
}
w http.ResponseWriter,
r *http.Request,
pkgName string,
- autorefresh, gpgUpdate bool,
+ autorefresh bool,
) {
dirPath := filepath.Join(Root, pkgName)
if autorefresh {
- if !refreshDir(w, r, pkgName, "", gpgUpdate) {
+ if !refreshDir(w, r, pkgName, "") {
return
}
} else if _, err := os.Stat(dirPath); os.IsNotExist(err) &&
- !refreshDir(w, r, pkgName, "", false) {
+ !refreshDir(w, r, pkgName, "") {
return
}
serial, releases, err := listDir(pkgName, false)
</head>
<body>{{$Refresh := .RefreshURLPath}}{{$PkgName := .PkgName}}{{range .Releases}}
<a href="{{$Refresh}}{{$PkgName}}/{{.Filename -}}
- #{{range $a, $d := .Digests}}{{$a}}={{$d}}{{end -}}"
- {{- with .HasSig}} data-gpg-sig="true"{{end}}>{{.Filename}}</a><br/>
+ #{{range $a, $d := .Digests}}{{$a}}={{$d}}{{end -}}">{{.Filename}}</a><br/>
{{- end}}
</body>
</html>
)
const (
- Version = "3.7.1"
+ Version = "4.0.0"
UserAgent = "GoCheese/" + Version
)
NoRefreshURLPath = flag.String("norefresh", DefaultNoRefreshURLPath, "")
RefreshURLPath = flag.String("refresh", DefaultRefreshURLPath, "")
- GPGUpdateURLPath = flag.String("gpgupdate", DefaultGPGUpdateURLPath, "")
JSONURLPath = flag.String("json", DefaultJSONURLPath, "")
PyPIURL = flag.String("pypi", DefaultPyPIURL, "")
log.Println(r.RemoteAddr, "get", filename)
path := filepath.Join(Root, pkgName, filename)
if _, err := os.Stat(path); os.IsNotExist(err) {
- if !refreshDir(w, r, pkgName, filename, false) {
+ if !refreshDir(w, r, pkgName, filename) {
return
}
}
case "GET":
var path string
var autorefresh bool
- var gpgUpdate bool
if strings.HasPrefix(r.URL.Path, *NoRefreshURLPath) {
path = strings.TrimPrefix(r.URL.Path, *NoRefreshURLPath)
} else if strings.HasPrefix(r.URL.Path, *RefreshURLPath) {
path = strings.TrimPrefix(r.URL.Path, *RefreshURLPath)
autorefresh = true
- } else if strings.HasPrefix(r.URL.Path, *GPGUpdateURLPath) {
- path = strings.TrimPrefix(r.URL.Path, *GPGUpdateURLPath)
- autorefresh = true
- gpgUpdate = true
} else {
http.Error(w, "unknown action", http.StatusBadRequest)
return
if parts[0] == "" {
listRoot(w, r)
} else {
- serveListDir(w, r, parts[0], autorefresh, gpgUpdate)
+ serveListDir(w, r, parts[0], autorefresh)
}
} else {
servePkg(w, r, parts[0], parts[1])
http.HandleFunc(*JSONURLPath, serveJSON)
http.HandleFunc(*NoRefreshURLPath, handler)
http.HandleFunc(*RefreshURLPath, handler)
- if *GPGUpdateURLPath != "" {
- http.HandleFunc(*GPGUpdateURLPath, handler)
- }
if *DoUCSPI {
server.SetKeepAlivesEnabled(false)
HashAlgoBLAKE2b256 = "blake2_256"
HashAlgoSHA512 = "sha512"
HashAlgoMD5 = "md5"
- GPGSigExt = ".asc"
InternalFlag = ".internal"
)
w http.ResponseWriter,
r *http.Request,
pkgName, filenameGet string,
- gpgUpdate bool,
) bool {
if _, err := os.Stat(filepath.Join(Root, pkgName, InternalFlag)); err == nil {
return true
}
}
- if filename == filenameGet || gpgUpdate {
- resp, err := c.Do(agentedReq(uri + GPGSigExt))
- if err != nil {
- goto GPGSigSkip
- }
- if resp.StatusCode != http.StatusOK {
- resp.Body.Close()
- goto GPGSigSkip
- }
- sig, err := io.ReadAll(resp.Body)
- resp.Body.Close()
- if err != nil {
- goto GPGSigSkip
- }
- if !bytes.HasPrefix(sig, []byte("-----BEGIN PGP SIGNATURE-----")) {
- log.Println(r.RemoteAddr, "pypi", filename+GPGSigExt, "non PGP")
- goto GPGSigSkip
- }
- if err = WriteFileSync(dirPath, path+GPGSigExt, sig, mtime); err != nil {
- log.Println("error", r.RemoteAddr, "pypi", filename+GPGSigExt, err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return false
- }
- log.Println(r.RemoteAddr, "pypi", filename+GPGSigExt, "downloaded")
- }
- if mtimeExists {
- stat, err := os.Stat(path + GPGSigExt)
- if err == nil && !stat.ModTime().Truncate(time.Second).Equal(mtime) {
- log.Println(r.RemoteAddr, "pypi", filename+GPGSigExt, "touch")
- if err = os.Chtimes(path+GPGSigExt, mtime, mtime); err != nil {
- log.Println("error", r.RemoteAddr, "pypi", filename, err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
- }
- }
-
- GPGSigSkip:
if digest == nil {
continue
}
}
pkgName := strings.ToLower(NormalizationRe.ReplaceAllString(pkgNames[0], "-"))
dirPath := filepath.Join(Root, pkgName)
- gpgSigsExpected := make(map[string]struct{})
now := time.Now().UTC()
var digestSHA256Expected []byte
for _, file := range r.MultipartForm.File["content"] {
filename := file.Filename
- gpgSigsExpected[filename+GPGSigExt] = struct{}{}
log.Println(r.RemoteAddr, "put", filename, "by", username)
path := filepath.Join(dirPath, filename)
if _, err = os.Stat(path); err == nil {
return
}
}
- for _, file := range r.MultipartForm.File["gpg_signature"] {
- filename := file.Filename
- if _, exists := gpgSigsExpected[filename]; !exists {
- log.Println(r.RemoteAddr, filename, "unexpected GPG signature filename")
- http.Error(w, "unexpected GPG signature filename", http.StatusBadRequest)
- return
- }
- delete(gpgSigsExpected, filename)
- log.Println(r.RemoteAddr, "put", filename, "by", username)
- path := filepath.Join(dirPath, filename)
- if _, err = os.Stat(path); err == nil {
- log.Println(r.RemoteAddr, filename, "already exists")
- http.Error(w, "already exists", http.StatusBadRequest)
- return
- }
- src, err := file.Open()
- if err != nil {
- log.Println("error", r.RemoteAddr, filename, err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- sig, err := io.ReadAll(src)
- src.Close()
- if err != nil {
- log.Println("error", r.RemoteAddr, filename, err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if err = WriteFileSync(dirPath, path, sig, now); err != nil {
- log.Println("error", r.RemoteAddr, path, err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
var buf bytes.Buffer
wr := recfile.NewWriter(&buf)
DefaultMaxClients = 128
DefaultNoRefreshURLPath = "/norefresh/"
DefaultRefreshURLPath = "/simple/"
- DefaultGPGUpdateURLPath = "/gpgupdate/"
DefaultJSONURLPath = "/pypi/"
DefaultPyPIURL = "https://pypi.org/simple/"
DefaultJSONURL = "https://pypi.org/pypi/"
HTTP endpoints:
-norefresh URLPATH -- Non-refreshing Simple API path (default: %s)
-refresh URLPATH -- Auto-refreshing Simple API path (default: %s)
- -gpgupdate URLPATH -- GPG forceful refreshing path (default: %s)
-json URLPATH -- JSON API path (default: %s)
Upstream PyPI:
DefaultMaxClients,
DefaultNoRefreshURLPath,
DefaultRefreshURLPath,
- DefaultGPGUpdateURLPath,
DefaultJSONURLPath,
DefaultPyPIURL,
DefaultJSONURLPath,