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
|
||||
entrypoint ["/bin/dkl-store"]
|
||||
copy --from=build /go/bin/ /bin/
|
||||
|
@ -3,15 +3,20 @@ package main
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -31,23 +36,56 @@ func main() {
|
||||
}
|
||||
|
||||
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)
|
||||
log.Print(l)
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
writeErr(err, w)
|
||||
@ -106,7 +144,7 @@ func handleHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -144,7 +182,7 @@ func hashOf(filePath string) (sha1Hex string, err error) {
|
||||
if err == nil {
|
||||
if sha1Stat.ModTime().After(fileStat.ModTime()) {
|
||||
// cached value is up-to-date
|
||||
sha1HexBytes, readErr := ioutil.ReadFile(sha1Path)
|
||||
sha1HexBytes, readErr := os.ReadFile(sha1Path)
|
||||
|
||||
if readErr == nil {
|
||||
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))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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