show info from directories
This commit is contained in:
parent
17e7153be7
commit
e6ad98babd
@ -1,8 +1,9 @@
|
|||||||
|
# syntax=docker/dockerfile:1.6.0
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
from mcluseau/golang-builder:1.21.6 as build
|
from mcluseau/golang-builder:1.23.2 as build
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
from alpine:3.19
|
from alpine:3.20
|
||||||
volume /srv/dkl-store
|
volume /srv/dkl-store
|
||||||
entrypoint ["/bin/dkl-store"]
|
entrypoint ["/bin/dkl-store"]
|
||||||
copy --from=build /go/bin/ /bin/
|
copy --from=build /go/bin/ /bin/
|
||||||
|
@ -3,15 +3,20 @@ package main
|
|||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-semver/semver"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -31,23 +36,56 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleHTTP(w http.ResponseWriter, req *http.Request) {
|
func handleHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
filePath := filepath.Join(*storeDir, req.URL.Path)
|
filePath := filepath.Join(*storeDir, path.Clean(req.URL.Path))
|
||||||
|
|
||||||
l := fmt.Sprintf("%s %s", req.Method, filePath)
|
l := fmt.Sprintf("%s %s", req.Method, filePath)
|
||||||
log.Print(l)
|
log.Print(l)
|
||||||
defer log.Print(l, " done")
|
defer log.Print(l, " done")
|
||||||
|
|
||||||
stat, err := os.Stat(filePath)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
writeErr(err, w)
|
|
||||||
return
|
|
||||||
} else if err == nil && stat.Mode().IsDir() {
|
|
||||||
http.NotFound(w, req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case "GET", "HEAD":
|
case "GET", "HEAD":
|
||||||
|
stat, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
writeErr(err, w)
|
||||||
|
} else {
|
||||||
|
http.NotFound(w, req)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if stat.Mode().IsDir() {
|
||||||
|
entries, err := os.ReadDir(filePath)
|
||||||
|
if err != nil {
|
||||||
|
writeErr(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp := struct {
|
||||||
|
Versions map[string]*VersionInfo `json:",omitempty"`
|
||||||
|
Names []string
|
||||||
|
}{
|
||||||
|
Versions: make(map[string]*VersionInfo, len(entries)),
|
||||||
|
Names: make([]string, 0, len(entries)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
name := e.Name()
|
||||||
|
if strings.HasSuffix(name, ".sha1") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Names = append(resp.Names, e.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Versions = aggregateVersions(resp.Names)
|
||||||
|
|
||||||
|
sort.Strings(resp.Names)
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
sha1Hex, err := hashOf(filePath)
|
sha1Hex, err := hashOf(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErr(err, w)
|
writeErr(err, w)
|
||||||
@ -106,7 +144,7 @@ func handleHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
os.Rename(tmpOut, filePath)
|
os.Rename(tmpOut, filePath)
|
||||||
|
|
||||||
if err := ioutil.WriteFile(filePath+".sha1", []byte(sha1Hex), 0644); err != nil {
|
if err := os.WriteFile(filePath+".sha1", []byte(sha1Hex), 0644); err != nil {
|
||||||
writeErr(err, w)
|
writeErr(err, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -144,7 +182,7 @@ func hashOf(filePath string) (sha1Hex string, err error) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
if sha1Stat.ModTime().After(fileStat.ModTime()) {
|
if sha1Stat.ModTime().After(fileStat.ModTime()) {
|
||||||
// cached value is up-to-date
|
// cached value is up-to-date
|
||||||
sha1HexBytes, readErr := ioutil.ReadFile(sha1Path)
|
sha1HexBytes, readErr := os.ReadFile(sha1Path)
|
||||||
|
|
||||||
if readErr == nil {
|
if readErr == nil {
|
||||||
sha1Hex = string(sha1HexBytes)
|
sha1Hex = string(sha1HexBytes)
|
||||||
@ -178,9 +216,97 @@ func hashOf(filePath string) (sha1Hex string, err error) {
|
|||||||
|
|
||||||
log.Print("hashing ", filePath, " took ", time.Since(start).Truncate(time.Millisecond))
|
log.Print("hashing ", filePath, " took ", time.Since(start).Truncate(time.Millisecond))
|
||||||
|
|
||||||
if writeErr := ioutil.WriteFile(sha1Path, []byte(sha1Hex), 0644); writeErr != nil {
|
if writeErr := os.WriteFile(sha1Path, []byte(sha1Hex), 0644); writeErr != nil {
|
||||||
log.Printf("WARNING: failed to cache SHA1: %v", writeErr)
|
log.Printf("WARNING: failed to cache SHA1: %v", writeErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func aggregateVersions(names []string) map[string]*VersionInfo {
|
||||||
|
versions := make([]VersionName, 0, len(names))
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
rem := name
|
||||||
|
|
||||||
|
segments := make([]semver.Version, 0, 5)
|
||||||
|
for len(rem) != 0 {
|
||||||
|
var s string
|
||||||
|
s, rem, _ = strings.Cut(rem, "_")
|
||||||
|
|
||||||
|
// remove non-number prefix chars
|
||||||
|
s = strings.TrimFunc(s, func(c rune) bool {
|
||||||
|
return !('0' <= c && c <= '9')
|
||||||
|
})
|
||||||
|
|
||||||
|
ver, err := semver.NewVersion(s)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
segments = append(segments, *ver)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(segments) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
versions = append(versions, VersionName{segments, name})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(versions, func(i, j int) bool {
|
||||||
|
return versions[i].LessThan(versions[j])
|
||||||
|
})
|
||||||
|
|
||||||
|
ret := make(map[string]*VersionInfo, len(versions))
|
||||||
|
for _, vi := range versions {
|
||||||
|
v := vi.segments[0]
|
||||||
|
name := vi.name
|
||||||
|
for _, key := range []string{
|
||||||
|
fmt.Sprintf("%d", v.Major),
|
||||||
|
fmt.Sprintf("%d.%d", v.Major, v.Minor),
|
||||||
|
} {
|
||||||
|
agg, ok := ret[key]
|
||||||
|
if !ok {
|
||||||
|
agg = &VersionInfo{}
|
||||||
|
ret[key] = agg
|
||||||
|
}
|
||||||
|
agg.Latest = name
|
||||||
|
agg.All = append(agg.All, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
type VersionInfo struct {
|
||||||
|
Latest string
|
||||||
|
All []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type VersionName struct {
|
||||||
|
segments []semver.Version
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a VersionName) LessThan(b VersionName) bool {
|
||||||
|
n := len(a.segments)
|
||||||
|
if l := len(b.segments); l > n {
|
||||||
|
n = l
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i != n; i++ {
|
||||||
|
if i >= len(a.segments) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if i >= len(b.segments) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
va, vb := a.segments[i], b.segments[i]
|
||||||
|
if !va.Equal(vb) {
|
||||||
|
return va.LessThan(vb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.name < b.name
|
||||||
|
}
|
||||||
|
88
cmd/dkl-store/versions_test.go
Normal file
88
cmd/dkl-store/versions_test.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleAggregateVersions() {
|
||||||
|
names := []string{
|
||||||
|
"dlkjsgljk",
|
||||||
|
"v1.20.16",
|
||||||
|
"v1.19.8_containerd.1.4.4",
|
||||||
|
"v1.20.15_containerd.1.4.13",
|
||||||
|
"v1.20.1_containerd.1.4.3",
|
||||||
|
"lkjzsfgj",
|
||||||
|
"v1.20.9_containerd.1.4.8",
|
||||||
|
"v1.21.0_containerd.1.4.4",
|
||||||
|
"v1.21.10_containerd.1.4.12",
|
||||||
|
"v1.21.10_containerd.1.4.13",
|
||||||
|
"v1.21.10_containerd.1.5.10",
|
||||||
|
"v1.21.10_containerd.1.5.9",
|
||||||
|
"v1.21.11_containerd.1.4.13",
|
||||||
|
"v1.21.14_containerd.1.5.16",
|
||||||
|
"v1.21.9_containerd.1.5.9",
|
||||||
|
}
|
||||||
|
|
||||||
|
rand.Shuffle(len(names), func(i, j int) {
|
||||||
|
names[i], names[j] = names[j], names[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
agg := aggregateVersions(names)
|
||||||
|
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
enc.Encode(agg)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// {
|
||||||
|
// "1": {
|
||||||
|
// "Latest": "v1.21.14_containerd.1.5.16",
|
||||||
|
// "All": [
|
||||||
|
// "v1.19.8_containerd.1.4.4",
|
||||||
|
// "v1.20.1_containerd.1.4.3",
|
||||||
|
// "v1.20.9_containerd.1.4.8",
|
||||||
|
// "v1.20.15_containerd.1.4.13",
|
||||||
|
// "v1.20.16",
|
||||||
|
// "v1.21.0_containerd.1.4.4",
|
||||||
|
// "v1.21.9_containerd.1.5.9",
|
||||||
|
// "v1.21.10_containerd.1.4.12",
|
||||||
|
// "v1.21.10_containerd.1.4.13",
|
||||||
|
// "v1.21.10_containerd.1.5.9",
|
||||||
|
// "v1.21.10_containerd.1.5.10",
|
||||||
|
// "v1.21.11_containerd.1.4.13",
|
||||||
|
// "v1.21.14_containerd.1.5.16"
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// "1.19": {
|
||||||
|
// "Latest": "v1.19.8_containerd.1.4.4",
|
||||||
|
// "All": [
|
||||||
|
// "v1.19.8_containerd.1.4.4"
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// "1.20": {
|
||||||
|
// "Latest": "v1.20.16",
|
||||||
|
// "All": [
|
||||||
|
// "v1.20.1_containerd.1.4.3",
|
||||||
|
// "v1.20.9_containerd.1.4.8",
|
||||||
|
// "v1.20.15_containerd.1.4.13",
|
||||||
|
// "v1.20.16"
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// "1.21": {
|
||||||
|
// "Latest": "v1.21.14_containerd.1.5.16",
|
||||||
|
// "All": [
|
||||||
|
// "v1.21.0_containerd.1.4.4",
|
||||||
|
// "v1.21.9_containerd.1.5.9",
|
||||||
|
// "v1.21.10_containerd.1.4.12",
|
||||||
|
// "v1.21.10_containerd.1.4.13",
|
||||||
|
// "v1.21.10_containerd.1.5.9",
|
||||||
|
// "v1.21.10_containerd.1.5.10",
|
||||||
|
// "v1.21.11_containerd.1.4.13",
|
||||||
|
// "v1.21.14_containerd.1.5.16"
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
4
go.mod
4
go.mod
@ -1,3 +1,5 @@
|
|||||||
module novit.nc/direktil/store
|
module novit.nc/direktil/store
|
||||||
|
|
||||||
go 1.13
|
go 1.23
|
||||||
|
|
||||||
|
require github.com/coreos/go-semver v0.3.1
|
||||||
|
6
go.sum
6
go.sum
@ -0,0 +1,6 @@
|
|||||||
|
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||||
|
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
Loading…
Reference in New Issue
Block a user