show info from directories

This commit is contained in:
Mikaël Cluseau 2024-10-13 12:32:58 +02:00
parent 17e7153be7
commit e6ad98babd
5 changed files with 240 additions and 17 deletions

View File

@ -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/

View File

@ -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
}

View 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
View File

@ -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
View File

@ -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=