major refactoring
This commit is contained in:
parent
91aa8371e9
commit
78b5e5f796
14 changed files with 712 additions and 576 deletions
|
|
@ -139,7 +139,9 @@ func getPreloads(path string, fs *fs.FS) (dc dirContents) {
|
|||
Running: p.Running,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(dc.Files, dc.Files.Less)
|
||||
slices.SortFunc(dc.Files, func(i, _ file) bool {
|
||||
return i.Running
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"io"
|
||||
stdfs "io/fs"
|
||||
"math"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
|
@ -19,8 +20,16 @@ const (
|
|||
csp = "Content-Security-Policy"
|
||||
indexCSP = "default-src 'none';style-src 'unsafe-inline';frame-ancestors 'none'"
|
||||
videoCSP = indexCSP + ";script-src 'sha256-ZLSc/s5/US9uVMMZOJ/yWiS1tj9nEoi+5Qoohy3QetU=';media-src 'self'"
|
||||
|
||||
gib = 1024 * 1024 * 1024
|
||||
)
|
||||
|
||||
type data struct {
|
||||
QuotaCur float64
|
||||
QuotaMax float64
|
||||
Paths dirContents
|
||||
}
|
||||
|
||||
type FileServer struct {
|
||||
log logr.Logger
|
||||
fs *fs.FS
|
||||
|
|
@ -32,7 +41,8 @@ func NewFileServer(fs *fs.FS, log logr.Logger) http.Handler {
|
|||
}
|
||||
|
||||
func skipLog(path string) bool {
|
||||
return strings.HasSuffix(path, "favicon.ico")
|
||||
return path == "/?o=preloads" ||
|
||||
strings.HasSuffix(path, "favicon.ico")
|
||||
}
|
||||
|
||||
func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -67,7 +77,7 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
h := w.Header()
|
||||
if option == "v" {
|
||||
h.Set(csp, videoCSP)
|
||||
err = video.Execute(w, r.URL.Path)
|
||||
err = video.Execute(w, data{Paths: dirContents{Base: r.URL.Path}})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -82,6 +92,10 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
fs.fs.CancelPreload(p)
|
||||
}
|
||||
rdir := "/"
|
||||
if r.FormValue("r") == "preloads" {
|
||||
http.Redirect(w, r, rdir+"?o=preloads", http.StatusFound)
|
||||
return
|
||||
}
|
||||
if i := strings.LastIndex(p, "/"); i > 0 {
|
||||
rdir = p[:i]
|
||||
}
|
||||
|
|
@ -97,7 +111,12 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
if option == "preloads" {
|
||||
h.Set(csp, indexCSP)
|
||||
err = preloads.Execute(w, getPreloads(p, fs.fs))
|
||||
cur, max := fs.fs.QuotaUsage()
|
||||
err = preloads.Execute(w, data{
|
||||
QuotaCur: math.Round(float64(cur)/gib*100) / 100,
|
||||
QuotaMax: float64(max) / gib,
|
||||
Paths: getPreloads(p, fs.fs),
|
||||
})
|
||||
if err != nil {
|
||||
fs.log.Error(err, "error rendering preloads")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
@ -124,7 +143,7 @@ func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
h.Set(csp, indexCSP)
|
||||
|
||||
w.WriteHeader(i.Status())
|
||||
err = index.Execute(w, paths)
|
||||
err = index.Execute(w, data{Paths: paths})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2,231 +2,39 @@
|
|||
|
||||
package srv
|
||||
|
||||
import "html/template"
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
var templates embed.FS
|
||||
|
||||
func subMust(fsys fs.FS, dir string) fs.FS {
|
||||
fsys, err := fs.Sub(fsys, dir)
|
||||
if err != nil {
|
||||
panic("sub: " + err.Error())
|
||||
}
|
||||
return fsys
|
||||
}
|
||||
|
||||
var tsub = subMust(templates, "templates")
|
||||
|
||||
var (
|
||||
indexHead = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#222222">
|
||||
<meta name="description" content="cachefs">
|
||||
<style>
|
||||
body {
|
||||
font: 20px Helvetica, sans-serif;
|
||||
background-color: #111;
|
||||
color: #e6e6e6;
|
||||
margin: 0px;
|
||||
padding: 0 150px 0 150px;
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
article {
|
||||
background-color: #222;
|
||||
}
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 10px 0 14px 0;
|
||||
}
|
||||
a:link {
|
||||
color: #e6e6e6;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:visited {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.listpath a:visited {
|
||||
color: #b2b2b2;
|
||||
}
|
||||
.up a:visited {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
a:hover, .listpath a:hover {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:active {
|
||||
color: #a6a6a6;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
td {
|
||||
padding: 0;
|
||||
}
|
||||
.listitem {
|
||||
background-color: #303030;
|
||||
}
|
||||
.listitem:hover {
|
||||
background-color: #404040;
|
||||
}
|
||||
.listitem:active {
|
||||
background-color: #363636;
|
||||
}
|
||||
.listpath {
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.listpath a {
|
||||
display: block;
|
||||
min-height: 45px;
|
||||
padding: 0.55em 0 0 5px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
word-break: break-all;
|
||||
}
|
||||
.listoptions {
|
||||
padding-right: 5px;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
.listoptions span, .listoptions a {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.spacer {
|
||||
height: 3px;
|
||||
}
|
||||
.red {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
.yellow {
|
||||
background-color: #f39c12;
|
||||
}
|
||||
.green {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
.status-yellow {
|
||||
color: #f39c12;
|
||||
}
|
||||
.status-green {
|
||||
color: #4caf50;
|
||||
}
|
||||
span {
|
||||
color: #b2b2b2;
|
||||
font-size: 15px;
|
||||
}
|
||||
</style>
|
||||
<title>cachefs | {{.Base}}</title>
|
||||
</head>`
|
||||
|
||||
index = template.Must(template.New("index").Parse(indexHead + `<body>
|
||||
<article>
|
||||
<pre>[v]: show video
|
||||
[n]: skip file caching
|
||||
[p]: preload file
|
||||
<a href="/?o=preloads">[Preloads]</a></pre>
|
||||
<table>
|
||||
<tr class="listitem">
|
||||
<td class="listpath up"><a href="../">../</a></td>
|
||||
<td class="listoptions">[dir]</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2"></td></tr>
|
||||
{{range $s := .Dirs -}}
|
||||
<tr class="listitem">
|
||||
<td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td>
|
||||
<td class="listoptions">[dir]</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2"></td></tr>
|
||||
{{end -}}
|
||||
{{range $s := .Files -}}
|
||||
<tr class="listitem">
|
||||
<td class="listpath"><a id="{{$s.Anchor}}" href="{{$s.URI}}">{{$s.Name}}</a></td>
|
||||
<td class="listoptions">
|
||||
{{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<a href="{{$s.URI}}?o=v">[v]</a><a href="{{$s.URI}}?o=n">[n]</a><a href="{{$s.URI}}?o=p">[p]</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2">
|
||||
{{if ge $s.Status 0}}
|
||||
<div class="spacer {{if le $s.Status 35}}red{{else if le $s.Status 65}}yellow{{else}}green{{end}}" style="width: {{$s.Status}}%;"></div>
|
||||
{{end -}}
|
||||
</td></tr>
|
||||
{{end -}}
|
||||
</table>
|
||||
</article>
|
||||
</body>
|
||||
</html>`))
|
||||
preloads = template.Must(template.New("preloads").Parse(indexHead + `<body>
|
||||
<article>
|
||||
<pre>[v]: show video
|
||||
[s]: stop preloading</pre>
|
||||
<table>
|
||||
<tr class="listitem">
|
||||
<td class="listpath up"><a href="/">/</a></td>
|
||||
<td class="listoptions">[dir]</td>
|
||||
</tr>
|
||||
{{range $s := .Files -}}
|
||||
<tr class="listitem">
|
||||
<td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td>
|
||||
<td class="listoptions">
|
||||
{{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<span class="status-{{if $s.Running}}green">[running{{else}}yellow">[queued{{end}}]</span><a href="{{$s.URI}}?o=v">[v]</a><a href="{{$s.Name}}?o=s">[s]</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2">
|
||||
{{if ge $s.Status 0}}
|
||||
<div class="spacer {{if le $s.Status 35}}red{{else if le $s.Status 65}}yellow{{else}}green{{end}}" style="width: {{$s.Status}}%;"></div>
|
||||
{{end -}}
|
||||
</td></tr>
|
||||
{{end -}}
|
||||
</table>
|
||||
</article>
|
||||
</body>
|
||||
</html>`))
|
||||
cache = template.Must(template.New("cache").Parse(indexHead + `<body>
|
||||
<article>
|
||||
<pre>[v]: show video</pre>
|
||||
<table>
|
||||
<tr class="listitem">
|
||||
<td class="listpath up"><a href="../">../</a></td>
|
||||
<td class="listoptions">[dir]</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2"></td></tr>
|
||||
{{range $s := .Dirs -}}
|
||||
<tr class="listitem">
|
||||
<td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td>
|
||||
<td class="listoptions">[dir]</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2"></td></tr>
|
||||
{{end -}}
|
||||
{{range $s := .Files -}}
|
||||
<tr class="listitem">
|
||||
<td class="listpath"><a id="{{$s.Anchor}}" href="{{$s.URI}}">{{$s.Name}}</a></td>
|
||||
<td class="listoptions">
|
||||
{{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<a href="{{$s.URI}}?o=v">[v]</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2">
|
||||
{{if ge $s.Status 0}}
|
||||
<div class="spacer {{if le $s.Status 35}}red{{else if le $s.Status 65}}yellow{{else}}green{{end}}" style="width: {{$s.Status}}%;"></div>
|
||||
{{end -}}
|
||||
</td></tr>
|
||||
{{end -}}
|
||||
</table>
|
||||
</article>
|
||||
</body>
|
||||
</html>`))
|
||||
video = template.Must(template.New("video").Parse(`<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#111111">
|
||||
<meta name="description" content="cachefs">
|
||||
<title>cachefs | {{.}}</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #111;
|
||||
margin: 0px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="video" style="width: 100%; height: 100%;" src="{{.}}" controls=""></video>
|
||||
<script>document.getElementById("video").volume=0.5;</script>
|
||||
</body>
|
||||
</html>`))
|
||||
index = template.Must(template.ParseFS(tsub,
|
||||
"head.html",
|
||||
"index.html",
|
||||
))
|
||||
preloads = template.Must(template.ParseFS(tsub,
|
||||
"head.html",
|
||||
"preloads.html",
|
||||
))
|
||||
cache = template.Must(template.ParseFS(tsub,
|
||||
"head.html",
|
||||
"cache.html",
|
||||
))
|
||||
video = template.Must(template.ParseFS(tsub,
|
||||
"video.html",
|
||||
))
|
||||
)
|
||||
|
|
|
|||
30
pkg/srv/templates/cache.html
Normal file
30
pkg/srv/templates/cache.html
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{{define "body" -}}
|
||||
<pre>[v]: show video</pre>
|
||||
<table>
|
||||
<tr class="listitem">
|
||||
<td class="listpath up"><a href="../">../</a></td>
|
||||
<td class="listoptions">[dir]</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2"></td></tr>
|
||||
{{range $s := .Dirs -}}
|
||||
<tr class="listitem">
|
||||
<td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td>
|
||||
<td class="listoptions">[dir]</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2"></td></tr>
|
||||
{{end -}}
|
||||
{{range $s := .Files -}}
|
||||
<tr class="listitem">
|
||||
<td class="listpath"><a id="{{$s.Anchor}}" href="{{$s.URI}}">{{$s.Name}}</a></td>
|
||||
<td class="listoptions">
|
||||
{{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<a href="{{$s.URI}}?o=v">[v]</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2">
|
||||
{{if ge $s.Status 0 -}}
|
||||
<div class="spacer {{if le $s.Status 35}}red{{else if le $s.Status 65}}yellow{{else}}green{{end}}" style="width: {{$s.Status}}%;"></div>
|
||||
{{end -}}
|
||||
</td></tr>
|
||||
{{end -}}
|
||||
</table>
|
||||
{{end -}}
|
||||
115
pkg/srv/templates/head.html
Normal file
115
pkg/srv/templates/head.html
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#222222">
|
||||
<meta name="description" content="cachefs">
|
||||
<style>
|
||||
body {
|
||||
font: 20px Helvetica, sans-serif;
|
||||
background-color: #111;
|
||||
color: #e6e6e6;
|
||||
margin: 0px;
|
||||
padding: 0 150px 0 150px;
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
article {
|
||||
background-color: #222;
|
||||
}
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 10px 0 14px 0;
|
||||
}
|
||||
a:link {
|
||||
color: #e6e6e6;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:visited {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.listpath a:visited {
|
||||
color: #b2b2b2;
|
||||
}
|
||||
.up a:visited {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
a:hover, .listpath a:hover {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:active {
|
||||
color: #a6a6a6;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
td {
|
||||
padding: 0;
|
||||
}
|
||||
.listitem {
|
||||
background-color: #303030;
|
||||
}
|
||||
.listitem:hover {
|
||||
background-color: #404040;
|
||||
}
|
||||
.listitem:active {
|
||||
background-color: #363636;
|
||||
}
|
||||
.listpath {
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.listpath a {
|
||||
display: block;
|
||||
min-height: 45px;
|
||||
padding: 0.55em 0 0 5px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
word-break: break-all;
|
||||
}
|
||||
.listoptions {
|
||||
padding-right: 5px;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
.listoptions span, .listoptions a {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.spacer {
|
||||
height: 3px;
|
||||
}
|
||||
.red {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
.yellow {
|
||||
background-color: #f39c12;
|
||||
}
|
||||
.green {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
.status-yellow {
|
||||
color: #f39c12;
|
||||
}
|
||||
.status-green {
|
||||
color: #4caf50;
|
||||
}
|
||||
span {
|
||||
color: #b2b2b2;
|
||||
font-size: 15px;
|
||||
}
|
||||
</style>
|
||||
<title>cachefs | {{.Paths.Base}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<article>
|
||||
{{template "body" .}}
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
33
pkg/srv/templates/index.html
Normal file
33
pkg/srv/templates/index.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{{define "body" -}}
|
||||
<pre>[v]: show video
|
||||
[n]: skip file caching
|
||||
[p]: preload file
|
||||
<a href="/?o=preloads">[Preloads]</a></pre>
|
||||
<table>
|
||||
<tr class="listitem">
|
||||
<td class="listpath up"><a href="../">../</a></td>
|
||||
<td class="listoptions">[dir]</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2"></td></tr>
|
||||
{{range $s := .Paths.Dirs -}}
|
||||
<tr class="listitem">
|
||||
<td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td>
|
||||
<td class="listoptions">[dir]</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2"></td></tr>
|
||||
{{end -}}
|
||||
{{range $s := .Paths.Files -}}
|
||||
<tr class="listitem">
|
||||
<td class="listpath"><a id="{{$s.Anchor}}" href="{{$s.URI}}">{{$s.Name}}</a></td>
|
||||
<td class="listoptions">
|
||||
{{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<a href="{{$s.URI}}?o=v">[v]</a><a href="{{$s.URI}}?o=n">[n]</a><a href="{{$s.URI}}?o=p">[p]</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2">
|
||||
{{if ge $s.Status 0 -}}
|
||||
<div class="spacer {{if le $s.Status 35}}red{{else if le $s.Status 65}}yellow{{else}}green{{end}}" style="width: {{$s.Status}}%;"></div>
|
||||
{{end -}}
|
||||
</td></tr>
|
||||
{{end -}}
|
||||
</table>
|
||||
{{end -}}
|
||||
25
pkg/srv/templates/preloads.html
Normal file
25
pkg/srv/templates/preloads.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{{define "body" -}}
|
||||
<pre>[v]: show video
|
||||
[s]: stop preloading
|
||||
Quota: {{.QuotaCur}} / {{.QuotaMax}} GiB
|
||||
</pre>
|
||||
<table>
|
||||
<tr class="listitem">
|
||||
<td class="listpath up"><a href="/">/</a></td>
|
||||
<td class="listoptions">[dir]</td>
|
||||
</tr>
|
||||
{{range $s := .Paths.Files -}}
|
||||
<tr class="listitem">
|
||||
<td class="listpath"><a href="{{$s.URI}}">{{$s.Name}}</a></td>
|
||||
<td class="listoptions">
|
||||
{{if ge $s.Status 0}}<span>{{$s.Status}}%</span>{{end}}<span class="status-{{if $s.Running}}green">[running{{else}}yellow">[queued{{end}}]</span><a href="{{$s.URI}}?o=v">[v]</a><a href="{{$s.Name}}?o=s&r=preloads">[s]</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="spacer"><td colspan="2">
|
||||
{{if ge $s.Status 0 -}}
|
||||
<div class="spacer {{if le $s.Status 35}}red{{else if le $s.Status 65}}yellow{{else}}green{{end}}" style="width: {{$s.Status}}%;"></div>
|
||||
{{end -}}
|
||||
</td></tr>
|
||||
{{end -}}
|
||||
</table>
|
||||
{{end -}}
|
||||
19
pkg/srv/templates/video.html
Normal file
19
pkg/srv/templates/video.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#111111">
|
||||
<meta name="description" content="cachefs">
|
||||
<title>cachefs | {{.Paths.Base}}</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #111;
|
||||
margin: 0px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="video" style="width: 100%; height: 100%;" src="{{.Paths.Base}}" controls=""></video>
|
||||
<script>document.getElementById("video").volume=0.5;</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue