file-server example: use a custom template for listing dirs/files

Former-commit-id: 5b9bb0be4ac3f5d463f0957a3074aa6e7b1a71f7
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-07-06 16:06:48 +03:00
parent 3ec94b9e4a
commit dd72a1e398
5 changed files with 249 additions and 55 deletions

View File

@ -3,22 +3,31 @@ package main
import (
"crypto/md5"
"fmt"
"html/template"
"io"
"mime/multipart"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/basicauth"
)
func init() {
os.Mkdir("./uploads", 0700)
}
const (
maxSize = 1 * iris.GB
uploadDir = "./uploads"
)
func main() {
app := iris.New()
app.RegisterView(iris.HTML("./views", ".html"))
// Serve assets (e.g. javascript, css).
// app.HandleDir("/public", "./public")
@ -28,11 +37,25 @@ func main() {
app.Get("/upload", uploadView)
app.Post("/upload", upload)
app.HandleDir("/files", "./uploads", iris.DirOptions{
Gzip: true,
ShowList: true,
DirList: iris.DirListRich,
})
filesRouter := app.Party("/files")
{
filesRouter.HandleDir("/", uploadDir, iris.DirOptions{
Gzip: true,
ShowList: true,
DirList: iris.DirListRich(iris.DirListRichOptions{
// Optionally, use a custom template for listing:
Tmpl: dirListRichTemplate,
}),
})
auth := basicauth.New(basicauth.Config{
Users: map[string]string{
"myusername": "mypassword",
},
})
filesRouter.Delete("/{file:path}", auth, deleteFile)
}
app.Listen(":8080")
}
@ -50,12 +73,10 @@ func uploadView(ctx iris.Context) {
ctx.View("upload.html", token)
}
const maxSize = 1 * iris.GB
func upload(ctx iris.Context) {
ctx.SetMaxRequestBodySize(maxSize)
_, err := ctx.UploadFormFiles("./uploads", beforeSave)
_, err := ctx.UploadFormFiles(uploadDir, beforeSave)
if err != nil {
ctx.StopWithError(iris.StatusPayloadTooRage, err)
return
@ -71,3 +92,151 @@ func beforeSave(ctx iris.Context, file *multipart.FileHeader) {
file.Filename = ip + "-" + file.Filename
}
func deleteFile(ctx iris.Context) {
// It does not contain the system path,
// as we are not exposing it to the user.
fileName := ctx.Params().Get("file")
filePath := path.Join(uploadDir, fileName)
if err := os.RemoveAll(filePath); err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
ctx.Redirect("/files")
}
var dirListRichTemplate = template.Must(template.New("dirlist").
Funcs(template.FuncMap{
"formatBytes": func(b int64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp])
},
}).Parse(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<style>
a {
padding: 8px 8px;
text-decoration:none;
cursor:pointer;
color: #10a2ff;
}
table {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: 100%;
width: 100%;
border-collapse: collapse;
border-spacing: 0;
empty-cells: show;
border: 1px solid #cbcbcb;
}
table caption {
color: #000;
font: italic 85%/1 arial, sans-serif;
padding: 1em 0;
text-align: center;
}
table td,
table th {
border-left: 1px solid #cbcbcb;
border-width: 0 0 0 1px;
font-size: inherit;
margin: 0;
overflow: visible;
padding: 0.5em 1em;
}
table thead {
background-color: #10a2ff;
color: #fff;
text-align: left;
vertical-align: bottom;
}
table td {
background-color: transparent;
}
.table-odd td {
background-color: #f2f2f2;
}
.table-bordered td {
border-bottom: 1px solid #cbcbcb;
}
.table-bordered tbody > tr:last-child > td {
border-bottom-width: 0;
}
</style>
</head>
<body>
<table class="table-bordered table-odd">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Size</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{ range $idx, $file := .Files }}
<tr>
<td>{{ $idx }}</td>
<td><a href="{{ $file.Path }}" title="{{ $file.ModTime }}">{{ $file.Name }}</a></td>
{{ if $file.Info.IsDir }}
<td>Dir</td>
{{ else }}
<td>{{ formatBytes $file.Info.Size }}</td>
{{ end }}
<td><input type="button" style="background-color:transparent;border:0px;cursor:pointer;" value="❌" onclick="deleteFile({{ $file.RelPath }})"/></td>
</tr>
{{ end }}
</tbody>
</table>
<script type="text/javascript">
function deleteFile(filename) {
if (!confirm("Are you sure you want to delete "+filename+" ?")) {
return;
}
fetch('/files/'+filename,
{
method: "DELETE",
// If you don't want server to prompt for username/password:
// credentials:"include",
headers: {
// "Authorization": "Basic " + btoa("myusername:mypassword")
"X-Requested-With": "XMLHttpRequest",
},
}).
then(data => location.reload()).
catch(e => alert(e));
}
</script>
</body></html>
`))

View File

@ -1,9 +1,8 @@
# Jet Engine Example
This example is a fork of <https://github.com/CloudyKit/jet/tree/master/examples/todos> to work with Iris, so you can
notice the differences side by side.
This example is a fork of <https://github.com/CloudyKit/jet/tree/master/examples/todos> to work with Iris, so you can notice the differences side by side.
Read more at: https://github.com/kataras/iris/issues/1281
Read more at: https://github.com/CloudyKit/jet/blob/master/docs/syntax.md
> The Iris Jet View Engine fixes some bugs that the underline [jet template parser](https://github.com/CloudyKit/jet) currently has.

View File

@ -98,6 +98,11 @@ type (
// `FileServer` and `Party#HandleDir` can use to serve files and assets.
// A shortcut for the `router.DirOptions`, useful when `FileServer` or `HandleDir` is being used.
DirOptions = router.DirOptions
// DirListRichOptions the options for the `DirListRich` helper function.
// The Tmpl's "dirlist" template will be executed.
// A shortcut for the `router.DirListRichOptions`.
// Useful when `DirListRich` function is passed to `DirOptions.DirList` field.
DirListRichOptions = router.DirListRichOptions
// ExecutionRules gives control to the execution of the route handlers outside of the handlers themselves.
// Usage:
// Party#SetExecutionRules(ExecutionRules {

View File

@ -395,10 +395,7 @@ func (api *APIBuilder) HandleMany(methodOrMulti string, relativePathorMulti stri
// Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/file-server
func (api *APIBuilder) HandleDir(requestPath, directory string, opts ...DirOptions) (routes []*Route) {
options := getDirOptions(opts...)
// TODO(@kataras): Add option(s) to enable file & directory deletion (DELETE wildcard route)
// and integrade it with the new `DirListRich` helper
// (either context menu override on right-click of the file name or to a new table column)
// Note that, an auth middleware can be already registered, so no need to add options for that here.
h := FileServer(directory, options)
description := directory
fileName, lineNumber := context.HandlerFileLine(h) // take those before StripPrefix.

View File

@ -395,6 +395,7 @@ func FileServer(directory string, opts ...DirOptions) context.Handler {
ctx.SetLastModified(info.ModTime())
err = dirList(ctx, info.Name(), f)
if err != nil {
ctx.Application().Logger().Errorf("FileServer: dirList: %v", err)
plainStatusCode(ctx, http.StatusInternalServerError)
return
}
@ -565,54 +566,72 @@ func DirectoryExists(dir string) bool {
return true
}
// DirListRichOptions the options for the `DirListRich` helper function.
// The Tmpl's "dirlist" template will be executed.
type DirListRichOptions struct {
Tmpl *template.Template
}
// DirListRich is a `DirListFunc` which can be passed to `DirOptions.DirList` field
// to override the default file listing appearance.
// See `DirListRichTemplate` to modify the template, if necessary.
func DirListRich(ctx context.Context, dirName string, dir http.File) error {
dirs, err := dir.Readdir(-1)
if err != nil {
return err
func DirListRich(opts ...DirListRichOptions) DirListFunc {
var options DirListRichOptions
if len(opts) > 0 {
options = opts[0]
}
sortBy := ctx.URLParam("sort")
switch sortBy {
case "name":
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
case "size":
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Size() < dirs[j].Size() })
default:
sort.Slice(dirs, func(i, j int) bool { return dirs[i].ModTime().After(dirs[j].ModTime()) })
if options.Tmpl == nil {
options.Tmpl = DirListRichTemplate
}
pageData := listPageData{
Title: fmt.Sprintf("List of %d files", len(dirs)),
Files: make([]fileInfoData, 0, len(dirs)),
}
for _, d := range dirs {
name := d.Name()
if d.IsDir() {
name += "/"
return func(ctx context.Context, dirName string, dir http.File) error {
dirs, err := dir.Readdir(-1)
if err != nil {
return err
}
upath := ""
if ctx.Path() == "/" {
upath = ctx.GetCurrentRoute().StaticPath() + "/" + name
} else {
upath = "./" + dirName + "/" + name
sortBy := ctx.URLParam("sort")
switch sortBy {
case "name":
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
case "size":
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Size() < dirs[j].Size() })
default:
sort.Slice(dirs, func(i, j int) bool { return dirs[i].ModTime().After(dirs[j].ModTime()) })
}
url := url.URL{Path: upath}
pageData := listPageData{
Title: fmt.Sprintf("List of %d files", len(dirs)),
Files: make([]fileInfoData, 0, len(dirs)),
}
pageData.Files = append(pageData.Files, fileInfoData{
Info: d,
ModTime: d.ModTime().UTC().Format(http.TimeFormat),
Path: url.String(),
Name: html.EscapeString(name),
})
for _, d := range dirs {
name := d.Name()
if d.IsDir() {
name += "/"
}
upath := ""
if ctx.Path() == "/" {
upath = ctx.GetCurrentRoute().StaticPath() + "/" + name
} else {
upath = "./" + dirName + "/" + name
}
url := url.URL{Path: upath}
pageData.Files = append(pageData.Files, fileInfoData{
Info: d,
ModTime: d.ModTime().UTC().Format(http.TimeFormat),
Path: url.String(),
RelPath: path.Join(ctx.Path(), name),
Name: html.EscapeString(name),
})
}
return options.Tmpl.ExecuteTemplate(ctx, "dirlist", pageData)
}
return DirListRichTemplate.Execute(ctx, pageData)
}
type (
@ -625,13 +644,14 @@ type (
Info os.FileInfo
ModTime string // format-ed time.
Path string // the request path.
RelPath string // file path without the system directory itself (we are not exposing it to the user).
Name string // the html-escaped name.
}
)
// DirListRichTemplate is the html template the `DirListRich` function is using to render
// the directories and files.
var DirListRichTemplate = template.Must(template.New("").
var DirListRichTemplate = template.Must(template.New("dirlist").
Funcs(template.FuncMap{
"formatBytes": func(b int64) string {
const unit = 1000
@ -721,18 +741,22 @@ var DirListRichTemplate = template.Must(template.New("").
<tr>
<th>#</th>
<th>Name</th>
<th>Size</th>
<th>Size</th>
</tr>
</thead>
<tbody>
{{ range $idx, $file := .Files }}
<tr>
<td>{{ $idx }}</td>
<td><a href="{{ $file.Path }}" title="{{ $file.ModTime }}">{{ $file.Name }}</a></td>
<td>{{ formatBytes $file.Info.Size }}</td>
<td><a href="{{ $file.Path }}" title="{{ $file.ModTime }}">{{ $file.Name }}</a></td>
{{ if $file.Info.IsDir }}
<td>Dir</td>
{{ else }}
<td>{{ formatBytes $file.Info.Size }}</td>
{{ end }}
</tr>
{{ end }}
</tbody>
</table>
</table>
</body></html>
`))