mirror of
https://github.com/genuinetools/reg.git
synced 2024-09-28 11:46:20 -04:00
server things
Signed-off-by: Jess Frazelle <acidburn@google.com>
This commit is contained in:
parent
1252fed54f
commit
5b848b5b4f
10 changed files with 933 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -45,3 +45,4 @@ Icon
|
||||||
.Trashes
|
.Trashes
|
||||||
|
|
||||||
reg
|
reg
|
||||||
|
server/server
|
||||||
|
|
29
server/Dockerfile
Normal file
29
server/Dockerfile
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
FROM alpine:latest
|
||||||
|
MAINTAINER Jessica Frazelle <jess@linux.com>
|
||||||
|
|
||||||
|
ENV PATH /go/bin:/usr/local/go/bin:$PATH
|
||||||
|
ENV GOPATH /go
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
ca-certificates
|
||||||
|
|
||||||
|
COPY static /src/static
|
||||||
|
COPY templates /src/templates
|
||||||
|
|
||||||
|
RUN set -x \
|
||||||
|
&& apk add --no-cache --virtual .build-deps \
|
||||||
|
go \
|
||||||
|
git \
|
||||||
|
gcc \
|
||||||
|
libc-dev \
|
||||||
|
libgcc \
|
||||||
|
&& go get -v github.com/jessfraz/reg \
|
||||||
|
&& cd /go/src/github.com/jessfraz/reg \
|
||||||
|
&& go build -o /usr/bin/reg-server ./server \
|
||||||
|
&& apk del .build-deps \
|
||||||
|
&& rm -rf /go \
|
||||||
|
&& echo "Build complete."
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
ENTRYPOINT [ "reg-server" ]
|
286
server/server.go
Normal file
286
server/server.go
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/cliconfig"
|
||||||
|
"github.com/docker/engine-api/types"
|
||||||
|
"github.com/jessfraz/reg/registry"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// VERSION is the binary version.
|
||||||
|
VERSION = "v0.1.0"
|
||||||
|
|
||||||
|
dockerConfigPath = ".docker/config.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
updating = false
|
||||||
|
)
|
||||||
|
|
||||||
|
// preload initializes any global options and configuration
|
||||||
|
// before the main or sub commands are run.
|
||||||
|
func preload(c *cli.Context) (err error) {
|
||||||
|
if c.GlobalBool("debug") {
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "reg-server"
|
||||||
|
app.Version = VERSION
|
||||||
|
app.Author = "@jessfraz"
|
||||||
|
app.Email = "no-reply@butts.com"
|
||||||
|
app.Usage = "Docker registry v2 static UI server."
|
||||||
|
app.Before = preload
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "debug, d",
|
||||||
|
Usage: "run in debug mode",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "username, u",
|
||||||
|
Usage: "username for the registry",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "password, p",
|
||||||
|
Usage: "password for the registry",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "registry, r",
|
||||||
|
Usage: "URL to the provate registry (ex. r.j3ss.co)",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "port",
|
||||||
|
Value: "8080",
|
||||||
|
Usage: "port for server to run on",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "cert",
|
||||||
|
Usage: "path to ssl cert",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Usage: "path to ssl key",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "interval",
|
||||||
|
Value: "5m",
|
||||||
|
Usage: "interval to generate new index.html's at",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Action = func(c *cli.Context) error {
|
||||||
|
auth, err := getAuthConfig(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the registry client
|
||||||
|
r, err := registry.New(auth, c.GlobalBool("debug"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the path to the static directory
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
staticDir := filepath.Join(wd, "static")
|
||||||
|
|
||||||
|
// create the initial index
|
||||||
|
if err := createStaticIndex(r, staticDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the duration
|
||||||
|
dur, err := time.ParseDuration(c.String("interval"))
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("parsing %s as duration failed: %v", c.String("interval"), err)
|
||||||
|
}
|
||||||
|
ticker := time.NewTicker(dur)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// create more indexes every X minutes based off interval
|
||||||
|
for range ticker.C {
|
||||||
|
if !updating {
|
||||||
|
if err := createStaticIndex(r, staticDir); err != nil {
|
||||||
|
logrus.Warnf("creating static index failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// create mux server
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// static files handler
|
||||||
|
staticHandler := http.FileServer(http.Dir(staticDir))
|
||||||
|
mux.Handle("/", staticHandler)
|
||||||
|
// TODO: add handler for individual repos
|
||||||
|
|
||||||
|
// set up the server
|
||||||
|
port := c.String("port")
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: ":" + port,
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
logrus.Infof("Starting server on port %q", port)
|
||||||
|
if c.String("cert") != "" && c.String("key") != "" {
|
||||||
|
logrus.Fatal(server.ListenAndServeTLS(c.String("cert"), c.String("key")))
|
||||||
|
} else {
|
||||||
|
logrus.Fatal(server.ListenAndServe())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthConfig(c *cli.Context) (types.AuthConfig, error) {
|
||||||
|
if c.GlobalString("username") != "" && c.GlobalString("password") != "" && c.GlobalString("registry") != "" {
|
||||||
|
return types.AuthConfig{
|
||||||
|
Username: c.GlobalString("username"),
|
||||||
|
Password: c.GlobalString("password"),
|
||||||
|
ServerAddress: c.GlobalString("registry"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dcfg, err := cliconfig.Load(cliconfig.ConfigDir())
|
||||||
|
if err != nil {
|
||||||
|
return types.AuthConfig{}, fmt.Errorf("Loading config file failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return error early if there are no auths saved
|
||||||
|
if !dcfg.ContainsAuth() {
|
||||||
|
if c.GlobalString("registry") != "" {
|
||||||
|
return types.AuthConfig{
|
||||||
|
ServerAddress: c.GlobalString("registry"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return types.AuthConfig{}, fmt.Errorf("No auth was present in %s, please pass a registry, username, and password", cliconfig.ConfigDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
// if they passed a specific registry, return those creds _if_ they exist
|
||||||
|
if c.GlobalString("registry") != "" {
|
||||||
|
if creds, ok := dcfg.AuthConfigs[c.GlobalString("registry")]; ok {
|
||||||
|
return creds, nil
|
||||||
|
}
|
||||||
|
return types.AuthConfig{}, fmt.Errorf("No authentication credentials exist for %s", c.GlobalString("registry"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the auth config as the registryURL, username and Password
|
||||||
|
for _, creds := range dcfg.AuthConfigs {
|
||||||
|
return creds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.AuthConfig{}, fmt.Errorf("Could not find any authentication credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRepoAndRef(c *cli.Context) (repo, ref string, err error) {
|
||||||
|
if len(c.Args()) < 1 {
|
||||||
|
return "", "", errors.New("pass the name of the repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
arg := c.Args()[0]
|
||||||
|
parts := []string{}
|
||||||
|
if strings.Contains(arg, "@") {
|
||||||
|
parts = strings.Split(c.Args()[0], "@")
|
||||||
|
} else if strings.Contains(arg, ":") {
|
||||||
|
parts = strings.Split(c.Args()[0], ":")
|
||||||
|
} else {
|
||||||
|
parts = []string{arg}
|
||||||
|
}
|
||||||
|
|
||||||
|
repo = parts[0]
|
||||||
|
ref = "latest"
|
||||||
|
if len(parts) > 1 {
|
||||||
|
ref = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type data struct {
|
||||||
|
RegistryURL string
|
||||||
|
LastUpdated string
|
||||||
|
Repos []repository
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository struct {
|
||||||
|
Name string
|
||||||
|
Tags string
|
||||||
|
RegistryURL string
|
||||||
|
// TODO: add date last uploaded
|
||||||
|
}
|
||||||
|
|
||||||
|
func createStaticIndex(r *registry.Registry, staticDir string) error {
|
||||||
|
updating = true
|
||||||
|
logrus.Info("fetching catalog")
|
||||||
|
repoList, err := r.Catalog()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting catalog failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Info("fetching tags")
|
||||||
|
var repos []repository
|
||||||
|
for _, repo := range repoList {
|
||||||
|
tags, err := r.Tags(repo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting tags for %s failed: %v", repo, err)
|
||||||
|
}
|
||||||
|
repos = append(repos, repository{
|
||||||
|
Name: repo,
|
||||||
|
Tags: strings.Join(tags, "<br/>"),
|
||||||
|
RegistryURL: r.URL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// create temporoary file to save template to
|
||||||
|
logrus.Info("creating temporary file for template")
|
||||||
|
f, err := ioutil.TempFile("", "reg-server")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating temp file failed: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
|
||||||
|
// parse & execute the template
|
||||||
|
logrus.Info("parsing and executing the template")
|
||||||
|
templateDir := filepath.Join(staticDir, "../templates")
|
||||||
|
lp := filepath.Join(templateDir, "layout.html")
|
||||||
|
|
||||||
|
d := data{
|
||||||
|
RegistryURL: r.URL,
|
||||||
|
Repos: repos,
|
||||||
|
LastUpdated: time.Now().String(),
|
||||||
|
}
|
||||||
|
tmpl := template.Must(template.New("").ParseFiles(lp))
|
||||||
|
if err := tmpl.ExecuteTemplate(f, "layout", d); err != nil {
|
||||||
|
return fmt.Errorf("execute template failed: %v", err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
logrus.Info("renaming the temporary file to index.html")
|
||||||
|
index := filepath.Join(staticDir, "index.html")
|
||||||
|
if err := os.Rename(f.Name(), index); err != nil {
|
||||||
|
return fmt.Errorf("renaming result from %s to %s failed: %v", f.Name(), index, err)
|
||||||
|
}
|
||||||
|
updating = false
|
||||||
|
return nil
|
||||||
|
}
|
16
server/static/css/search.svg
Normal file
16
server/static/css/search.svg
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
|
||||||
|
<g id="search_1_">
|
||||||
|
<path fill="#CCCCCC" d="M20,0.005c-6.627,0-12,5.373-12,12c0,2.026,0.507,3.933,1.395,5.608l-8.344,8.342l0.007,0.006
|
||||||
|
C0.406,26.602,0,27.49,0,28.477c0,1.949,1.58,3.529,3.529,3.529c0.985,0,1.874-0.406,2.515-1.059l-0.002-0.002l8.341-8.34
|
||||||
|
c1.676,0.891,3.586,1.4,5.617,1.4c6.627,0,12-5.373,12-12S26.627,0.005,20,0.005z M4.795,29.697
|
||||||
|
c-0.322,0.334-0.768,0.543-1.266,0.543c-0.975,0-1.765-0.789-1.765-1.764c0-0.498,0.21-0.943,0.543-1.266l-0.009-0.008l8.066-8.066
|
||||||
|
c0.705,0.951,1.545,1.791,2.494,2.498L4.795,29.697z M20,22.006c-5.522,0-10-4.479-10-10c0-5.522,4.478-10,10-10
|
||||||
|
c5.521,0,10,4.478,10,10C30,17.527,25.521,22.006,20,22.006z"/>
|
||||||
|
<path fill="#CCCCCC" d="M20,5.005c-3.867,0-7,3.134-7,7c0,0.276,0.224,0.5,0.5,0.5s0.5-0.224,0.5-0.5c0-3.313,2.686-6,6-6
|
||||||
|
c0.275,0,0.5-0.224,0.5-0.5S20.275,5.005,20,5.005z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
220
server/static/css/styles.css
Normal file
220
server/static/css/styles.css
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,300');
|
||||||
|
/* Have to use @import for the font, as you can only specify a single stylesheet */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
min-height: 100%;
|
||||||
|
border-top: 10px solid #ECEEF1;
|
||||||
|
border-bottom: 10px solid #ECEEF1;
|
||||||
|
color: #61666c;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding: 20px;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: Inconsolata,monospace;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #61666c;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #2a2a2a;
|
||||||
|
}
|
||||||
|
/*------------------------------------*\
|
||||||
|
Wrapper
|
||||||
|
\*------------------------------------*/
|
||||||
|
.wrapper {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-top: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
/*------------------------------------*\
|
||||||
|
Demo block
|
||||||
|
\*------------------------------------*/
|
||||||
|
.block {
|
||||||
|
font-size: .875em;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
color: #9099A3;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-weight: 200;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.4em;
|
||||||
|
line-height: 3em;
|
||||||
|
font-family: 'Museo Slab', 'Open Sans', monospace;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
*overflow: visible;
|
||||||
|
line-height: normal;
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 18px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
padding: 4px 32px 4px 6px;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #555555;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 196px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
-webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
|
||||||
|
-moz-transition: border linear 0.2s, box-shadow linear 0.2s;
|
||||||
|
-o-transition: border linear 0.2s, box-shadow linear 0.2s;
|
||||||
|
transition: border linear 0.2s, box-shadow linear 0.2s;
|
||||||
|
background: url('search.svg') no-repeat 211px center;
|
||||||
|
background-size: auto 20px;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
border-color: rgba(82, 168, 236, 0.8);
|
||||||
|
outline: 0;
|
||||||
|
outline: thin dotted \9;
|
||||||
|
/* IE6-9 */
|
||||||
|
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
|
||||||
|
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
|
||||||
|
}
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
input[type="search"] {
|
||||||
|
margin-top: 20px;
|
||||||
|
-webkit-box-sizing: content-box;
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
-webkit-transition: all 300ms ease-in;
|
||||||
|
-moz-transition: all 300ms ease-in;
|
||||||
|
-ms-transition: all 300ms ease-in;
|
||||||
|
-o-transition: all 300ms ease-in;
|
||||||
|
transition: all 300ms ease-in;
|
||||||
|
}
|
||||||
|
input[type="search"]::-webkit-search-decoration,
|
||||||
|
input[type="search"]::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
a.clear,
|
||||||
|
a.clear:link,
|
||||||
|
a.clear:visited {
|
||||||
|
color: #666;
|
||||||
|
padding: 2px 0 2px 0;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0px 0 0 20px;
|
||||||
|
line-height: 14px;
|
||||||
|
display: inline-block;
|
||||||
|
border-bottom: transparent 1px solid;
|
||||||
|
vertical-align: -10px;
|
||||||
|
-webkit-transition: all 300ms ease-in;
|
||||||
|
-moz-transition: all 300ms ease-in;
|
||||||
|
-ms-transition: all 300ms ease-in;
|
||||||
|
-o-transition: all 300ms ease-in;
|
||||||
|
transition: all 300ms ease-in;
|
||||||
|
}
|
||||||
|
a.clear:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
/*------------------------------------*\
|
||||||
|
Table (directory listing)
|
||||||
|
\*------------------------------------*/
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: .875em;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 20px auto 0px auto;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
outline: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
tr:hover td {
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
font-size: .75em;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
/* 2nd Column: Filename */
|
||||||
|
th + th {
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
/* 3rd Column: Last Modified */
|
||||||
|
/* 4th Column: Size */
|
||||||
|
th + th + th + th {
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
tr td:first-of-type {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 5px 0;
|
||||||
|
outline: 0;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #edf1f5;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: left;
|
||||||
|
-webkit-transition: background 300ms ease-in;
|
||||||
|
-moz-transition: background 300ms ease-in;
|
||||||
|
-ms-transition: background 300ms ease-in;
|
||||||
|
-o-transition: background 300ms ease-in;
|
||||||
|
transition: background 300ms ease-in;
|
||||||
|
}
|
||||||
|
td:last-child,
|
||||||
|
th:last-child {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
td a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
tr.parent a {
|
||||||
|
color: #9099A3;
|
||||||
|
}
|
||||||
|
.parent a:hover {
|
||||||
|
color: #2a2a2a;
|
||||||
|
}
|
||||||
|
/*------------------------------------*\
|
||||||
|
Footer
|
||||||
|
\*------------------------------------*/
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: .75em;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
max-height: 16px;
|
||||||
|
}
|
226
server/static/css/styles.less
Normal file
226
server/static/css/styles.less
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
|
||||||
|
@import url('http://fonts.googleapis.com/css?family=Open+Sans:400,300');
|
||||||
|
/* Have to use @import for the font, as you can only specify a single stylesheet */
|
||||||
|
* {
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
-webkit-box-sizing:border-box;
|
||||||
|
-moz-box-sizing:border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
min-height:100%;
|
||||||
|
border-top:10px solid #ECEEF1;
|
||||||
|
border-bottom:10px solid #ECEEF1;
|
||||||
|
color:#61666c;
|
||||||
|
font-weight:300;
|
||||||
|
font-size:1em;
|
||||||
|
font-family:'Open Sans', sans-serif;
|
||||||
|
line-height:2em;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding:20px;
|
||||||
|
-webkit-backface-visibility:hidden;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family:Inconsolata,monospace;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color:#61666c;
|
||||||
|
text-decoration:none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color:#2a2a2a;
|
||||||
|
}
|
||||||
|
/*------------------------------------*\
|
||||||
|
Wrapper
|
||||||
|
\*------------------------------------*/
|
||||||
|
.wrapper {
|
||||||
|
margin:0 auto;
|
||||||
|
padding-top:20px;
|
||||||
|
max-width:800px;
|
||||||
|
}
|
||||||
|
/*------------------------------------*\
|
||||||
|
Demo block
|
||||||
|
\*------------------------------------*/
|
||||||
|
.block {
|
||||||
|
font-size:.875em;
|
||||||
|
margin:20px 0;
|
||||||
|
padding:20px;
|
||||||
|
color:#9099A3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight:200;
|
||||||
|
text-align:center;
|
||||||
|
font-size:1.4em;
|
||||||
|
line-height:3em;
|
||||||
|
font-family:'Museo Slab','Open Sans',monospace;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
*overflow: visible;
|
||||||
|
line-height: normal;
|
||||||
|
font-family:'Open Sans', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 18px;
|
||||||
|
color:#555555;
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
padding: 4px 32px 4px 6px;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #555555;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 196px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
-webkit-transition: border linear .2s, box-shadow linear .2s;
|
||||||
|
-moz-transition: border linear .2s, box-shadow linear .2s;
|
||||||
|
-o-transition: border linear .2s, box-shadow linear .2s;
|
||||||
|
transition: border linear .2s, box-shadow linear .2s;
|
||||||
|
background: url('search.svg') no-repeat 211px center;
|
||||||
|
background-size:auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: rgba(82, 168, 236, 0.8);
|
||||||
|
outline: 0;
|
||||||
|
outline: thin dotted \9;
|
||||||
|
/* IE6-9 */
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
|
||||||
|
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="search"] {
|
||||||
|
margin-top: 20px;
|
||||||
|
-webkit-box-sizing: content-box;
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
-webkit-transition:all 300ms ease-in;
|
||||||
|
-moz-transition:all 300ms ease-in;
|
||||||
|
-ms-transition:all 300ms ease-in;
|
||||||
|
-o-transition:all 300ms ease-in;
|
||||||
|
transition:all 300ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="search"]::-webkit-search-decoration,
|
||||||
|
input[type="search"]::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
a.clear, a.clear:link, a.clear:visited {
|
||||||
|
color:#666;
|
||||||
|
padding:2px 0 2px 0;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
margin:0px 0 0 20px;
|
||||||
|
line-height: 14px;
|
||||||
|
display: inline-block;
|
||||||
|
border-bottom: transparent 1px solid;
|
||||||
|
vertical-align: -10px;
|
||||||
|
-webkit-transition:all 300ms ease-in;
|
||||||
|
-moz-transition:all 300ms ease-in;
|
||||||
|
-ms-transition:all 300ms ease-in;
|
||||||
|
-o-transition:all 300ms ease-in;
|
||||||
|
transition:all 300ms ease-in;
|
||||||
|
}
|
||||||
|
a.clear:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color:#333;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
/*------------------------------------*\
|
||||||
|
Table (directory listing)
|
||||||
|
\*------------------------------------*/
|
||||||
|
table {
|
||||||
|
border-collapse:collapse;
|
||||||
|
font-size:.875em;
|
||||||
|
max-width:100%;
|
||||||
|
margin:20px auto 0px auto;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
outline:0;
|
||||||
|
border:0;
|
||||||
|
}
|
||||||
|
tr:hover td {
|
||||||
|
background:#f6f6f6;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align:left;
|
||||||
|
font-size:.75em;
|
||||||
|
padding-right:20px;
|
||||||
|
}
|
||||||
|
/* 2nd Column: Filename */
|
||||||
|
th + th {
|
||||||
|
width:65%;
|
||||||
|
}
|
||||||
|
/* 3rd Column: Last Modified */
|
||||||
|
th + th + th {
|
||||||
|
}
|
||||||
|
/* 4th Column: Size */
|
||||||
|
th + th + th + th {
|
||||||
|
width:5%;
|
||||||
|
}
|
||||||
|
tr td:first-of-type {
|
||||||
|
padding-left:10px;
|
||||||
|
padding-right:10px;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding:5px 0;
|
||||||
|
outline:0;
|
||||||
|
border:0;
|
||||||
|
border-bottom:1px solid #edf1f5;
|
||||||
|
vertical-align:middle;
|
||||||
|
text-align:left;
|
||||||
|
-webkit-transition:background 300ms ease-in;
|
||||||
|
-moz-transition:background 300ms ease-in;
|
||||||
|
-ms-transition:background 300ms ease-in;
|
||||||
|
-o-transition:background 300ms ease-in;
|
||||||
|
transition:background 300ms ease-in;
|
||||||
|
}
|
||||||
|
td:last-child, th:last-child {
|
||||||
|
text-align:right;
|
||||||
|
padding-right:0px;
|
||||||
|
}
|
||||||
|
td a{
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
tr.parent a {
|
||||||
|
color:#9099A3;
|
||||||
|
}
|
||||||
|
.parent a:hover {
|
||||||
|
color:#2a2a2a;
|
||||||
|
}
|
||||||
|
/*------------------------------------*\
|
||||||
|
Footer
|
||||||
|
\*------------------------------------*/
|
||||||
|
.footer {
|
||||||
|
text-align:center;
|
||||||
|
font-size:.75em;
|
||||||
|
margin-top:50px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
outline:none;
|
||||||
|
border:none;
|
||||||
|
max-height:16px;
|
||||||
|
}
|
BIN
server/static/favicon.ico
Normal file
BIN
server/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
5
server/static/js/lib/jquery-1.9.1.min.js
vendored
Normal file
5
server/static/js/lib/jquery-1.9.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
91
server/static/js/scripts.js
Normal file
91
server/static/js/scripts.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
// pretty date function
|
||||||
|
function prettyDate(time){
|
||||||
|
var date = new Date((time || "").replace(/-/g,"/").replace(/[TZ]/g," ")),
|
||||||
|
diff = (((new Date()).getTime() - date.getTime()) / 1000),
|
||||||
|
day_diff = Math.floor(diff / 86400);
|
||||||
|
|
||||||
|
if (isNaN(day_diff) || day_diff < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
return day_diff == 0 && (
|
||||||
|
diff < 60 && "just now" ||
|
||||||
|
diff < 120 && "1 minute ago" ||
|
||||||
|
diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
|
||||||
|
diff < 7200 && "1 hour ago" ||
|
||||||
|
diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
|
||||||
|
day_diff == 1 && "Yesterday" ||
|
||||||
|
day_diff < 7 && day_diff + " days ago" ||
|
||||||
|
day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago" ||
|
||||||
|
day_diff > 31 && Math.round(day_diff / 31) + " months ago";
|
||||||
|
}
|
||||||
|
|
||||||
|
// search function
|
||||||
|
function search(search_val){
|
||||||
|
var suche = search_val.toLowerCase();
|
||||||
|
var table = document.getElementById("directory");
|
||||||
|
var cellNr = 1;
|
||||||
|
var ele;
|
||||||
|
for (var r = 1; r < table.rows.length; r++){
|
||||||
|
ele = table.rows[r].cells[cellNr].innerHTML.replace(/<[^>]+>/g,"");
|
||||||
|
if (ele.toLowerCase().indexOf(suche)>=0 ) {
|
||||||
|
table.rows[r].style.display = '';
|
||||||
|
} else {
|
||||||
|
table.rows[r].style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var el = document.querySelectorAll('tr:nth-child(2)')[0].querySelectorAll('td:nth-child(2)')[0];
|
||||||
|
if (el.textContent == 'Parent Directory'){
|
||||||
|
var parent_row = document.querySelectorAll('tr:nth-child(2)')[0];
|
||||||
|
if (parent_row.classList){
|
||||||
|
parent_row.classList.add('parent');
|
||||||
|
} else {
|
||||||
|
parent_row.className += ' ' + 'parent';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// var rows = document.querySelectorAll('tr:not(.parent)');
|
||||||
|
// Array.prototype.forEach.call(rows, function(item, index){
|
||||||
|
// if (index !== 0) {
|
||||||
|
// var date_holder = item.querySelectorAll('td:nth-child(3)')[0];
|
||||||
|
// var date = date_holder.textContent;
|
||||||
|
// date = prettyDate(date);
|
||||||
|
// date_holder.innerHTML = date;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
var cells = document.querySelectorAll('td a');
|
||||||
|
Array.prototype.forEach.call(cells, function(item, index){
|
||||||
|
var link = item.getAttribute('href');
|
||||||
|
link = link.replace('.html', '');
|
||||||
|
item.setAttribute('href', link);
|
||||||
|
});
|
||||||
|
|
||||||
|
var our_table = document.querySelectorAll('table')[0];
|
||||||
|
our_table.setAttribute('id', 'directory');
|
||||||
|
|
||||||
|
// search script
|
||||||
|
var search_input = document.querySelectorAll('input[name="filter"]')[0];
|
||||||
|
var clear_button = document.querySelectorAll('a.clear')[0];
|
||||||
|
|
||||||
|
if (search_input.value !== ''){
|
||||||
|
search(search_input.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
search_input.addEventListener('keyup', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
search(search_input.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
search_input.addEventListener('keypress', function(e){
|
||||||
|
if ( e.which == 13 ) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clear_button.addEventListener('click', function(e){
|
||||||
|
search_input.value = '';
|
||||||
|
search('');
|
||||||
|
});
|
59
server/templates/layout.html
Normal file
59
server/templates/layout.html
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{{define "layout"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||||
|
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||||
|
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||||
|
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<base href="/" >
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
|
<title>{{ .RegistryURL }}</title>
|
||||||
|
<link rel="icon" type="image/ico" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ .RegistryURL }}<</h1>
|
||||||
|
<form>
|
||||||
|
<input name="filter" type="search"><a class="clear">clear</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Tags</th>
|
||||||
|
<th>Pull Command</th>
|
||||||
|
</tr>
|
||||||
|
{{ range $key, $value := .Repos }}
|
||||||
|
<tr>
|
||||||
|
<td valign="top">
|
||||||
|
{{ $value.Name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ $value.Tags }}
|
||||||
|
</td>
|
||||||
|
<td align="right">
|
||||||
|
<code>docker pull {{ $value.RegistryURL }}/{{ $value.Name }}</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<a href="https://twitter.com/jessfraz">@jessfraz</a>
|
||||||
|
<p>Last Updated: {{ .LastUpdated }}</p>
|
||||||
|
</div><!--/.footer-->
|
||||||
|
<script src="/js/scripts.js"></script>
|
||||||
|
<script>
|
||||||
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||||
|
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||||
|
ga('create', 'UA-29404280-12', 'jessfraz.com');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
Loading…
Reference in a new issue