mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
file-server example: use a custom template for listing dirs/files
Former-commit-id: 5b9bb0be4ac3f5d463f0957a3074aa6e7b1a71f7
This commit is contained in:
parent
3ec94b9e4a
commit
dd72a1e398
|
@ -3,22 +3,31 @@ package main
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/middleware/basicauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
os.Mkdir("./uploads", 0700)
|
os.Mkdir("./uploads", 0700)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxSize = 1 * iris.GB
|
||||||
|
uploadDir = "./uploads"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
|
||||||
app.RegisterView(iris.HTML("./views", ".html"))
|
app.RegisterView(iris.HTML("./views", ".html"))
|
||||||
// Serve assets (e.g. javascript, css).
|
// Serve assets (e.g. javascript, css).
|
||||||
// app.HandleDir("/public", "./public")
|
// app.HandleDir("/public", "./public")
|
||||||
|
@ -28,11 +37,25 @@ func main() {
|
||||||
app.Get("/upload", uploadView)
|
app.Get("/upload", uploadView)
|
||||||
app.Post("/upload", upload)
|
app.Post("/upload", upload)
|
||||||
|
|
||||||
app.HandleDir("/files", "./uploads", iris.DirOptions{
|
filesRouter := app.Party("/files")
|
||||||
Gzip: true,
|
{
|
||||||
ShowList: true,
|
filesRouter.HandleDir("/", uploadDir, iris.DirOptions{
|
||||||
DirList: iris.DirListRich,
|
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")
|
app.Listen(":8080")
|
||||||
}
|
}
|
||||||
|
@ -50,12 +73,10 @@ func uploadView(ctx iris.Context) {
|
||||||
ctx.View("upload.html", token)
|
ctx.View("upload.html", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxSize = 1 * iris.GB
|
|
||||||
|
|
||||||
func upload(ctx iris.Context) {
|
func upload(ctx iris.Context) {
|
||||||
ctx.SetMaxRequestBodySize(maxSize)
|
ctx.SetMaxRequestBodySize(maxSize)
|
||||||
|
|
||||||
_, err := ctx.UploadFormFiles("./uploads", beforeSave)
|
_, err := ctx.UploadFormFiles(uploadDir, beforeSave)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.StopWithError(iris.StatusPayloadTooRage, err)
|
ctx.StopWithError(iris.StatusPayloadTooRage, err)
|
||||||
return
|
return
|
||||||
|
@ -71,3 +92,151 @@ func beforeSave(ctx iris.Context, file *multipart.FileHeader) {
|
||||||
|
|
||||||
file.Filename = ip + "-" + file.Filename
|
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>
|
||||||
|
`))
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
# Jet Engine Example
|
# 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
|
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.
|
||||||
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.
|
> The Iris Jet View Engine fixes some bugs that the underline [jet template parser](https://github.com/CloudyKit/jet) currently has.
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,11 @@ type (
|
||||||
// `FileServer` and `Party#HandleDir` can use to serve files and assets.
|
// `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.
|
// A shortcut for the `router.DirOptions`, useful when `FileServer` or `HandleDir` is being used.
|
||||||
DirOptions = router.DirOptions
|
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.
|
// ExecutionRules gives control to the execution of the route handlers outside of the handlers themselves.
|
||||||
// Usage:
|
// Usage:
|
||||||
// Party#SetExecutionRules(ExecutionRules {
|
// Party#SetExecutionRules(ExecutionRules {
|
||||||
|
|
|
@ -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
|
// 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) {
|
func (api *APIBuilder) HandleDir(requestPath, directory string, opts ...DirOptions) (routes []*Route) {
|
||||||
options := getDirOptions(opts...)
|
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)
|
h := FileServer(directory, options)
|
||||||
description := directory
|
description := directory
|
||||||
fileName, lineNumber := context.HandlerFileLine(h) // take those before StripPrefix.
|
fileName, lineNumber := context.HandlerFileLine(h) // take those before StripPrefix.
|
||||||
|
|
|
@ -395,6 +395,7 @@ func FileServer(directory string, opts ...DirOptions) context.Handler {
|
||||||
ctx.SetLastModified(info.ModTime())
|
ctx.SetLastModified(info.ModTime())
|
||||||
err = dirList(ctx, info.Name(), f)
|
err = dirList(ctx, info.Name(), f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
ctx.Application().Logger().Errorf("FileServer: dirList: %v", err)
|
||||||
plainStatusCode(ctx, http.StatusInternalServerError)
|
plainStatusCode(ctx, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -565,54 +566,72 @@ func DirectoryExists(dir string) bool {
|
||||||
return true
|
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
|
// DirListRich is a `DirListFunc` which can be passed to `DirOptions.DirList` field
|
||||||
// to override the default file listing appearance.
|
// to override the default file listing appearance.
|
||||||
// See `DirListRichTemplate` to modify the template, if necessary.
|
// See `DirListRichTemplate` to modify the template, if necessary.
|
||||||
func DirListRich(ctx context.Context, dirName string, dir http.File) error {
|
func DirListRich(opts ...DirListRichOptions) DirListFunc {
|
||||||
dirs, err := dir.Readdir(-1)
|
var options DirListRichOptions
|
||||||
if err != nil {
|
if len(opts) > 0 {
|
||||||
return err
|
options = opts[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
sortBy := ctx.URLParam("sort")
|
if options.Tmpl == nil {
|
||||||
switch sortBy {
|
options.Tmpl = DirListRichTemplate
|
||||||
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()) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pageData := listPageData{
|
return func(ctx context.Context, dirName string, dir http.File) error {
|
||||||
Title: fmt.Sprintf("List of %d files", len(dirs)),
|
dirs, err := dir.Readdir(-1)
|
||||||
Files: make([]fileInfoData, 0, len(dirs)),
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
|
||||||
for _, d := range dirs {
|
|
||||||
name := d.Name()
|
|
||||||
if d.IsDir() {
|
|
||||||
name += "/"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
upath := ""
|
sortBy := ctx.URLParam("sort")
|
||||||
if ctx.Path() == "/" {
|
switch sortBy {
|
||||||
upath = ctx.GetCurrentRoute().StaticPath() + "/" + name
|
case "name":
|
||||||
} else {
|
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
|
||||||
upath = "./" + dirName + "/" + 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{
|
for _, d := range dirs {
|
||||||
Info: d,
|
name := d.Name()
|
||||||
ModTime: d.ModTime().UTC().Format(http.TimeFormat),
|
if d.IsDir() {
|
||||||
Path: url.String(),
|
name += "/"
|
||||||
Name: html.EscapeString(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 (
|
type (
|
||||||
|
@ -625,13 +644,14 @@ type (
|
||||||
Info os.FileInfo
|
Info os.FileInfo
|
||||||
ModTime string // format-ed time.
|
ModTime string // format-ed time.
|
||||||
Path string // the request path.
|
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.
|
Name string // the html-escaped name.
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// DirListRichTemplate is the html template the `DirListRich` function is using to render
|
// DirListRichTemplate is the html template the `DirListRich` function is using to render
|
||||||
// the directories and files.
|
// the directories and files.
|
||||||
var DirListRichTemplate = template.Must(template.New("").
|
var DirListRichTemplate = template.Must(template.New("dirlist").
|
||||||
Funcs(template.FuncMap{
|
Funcs(template.FuncMap{
|
||||||
"formatBytes": func(b int64) string {
|
"formatBytes": func(b int64) string {
|
||||||
const unit = 1000
|
const unit = 1000
|
||||||
|
@ -721,18 +741,22 @@ var DirListRichTemplate = template.Must(template.New("").
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{ range $idx, $file := .Files }}
|
{{ range $idx, $file := .Files }}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $idx }}</td>
|
<td>{{ $idx }}</td>
|
||||||
<td><a href="{{ $file.Path }}" title="{{ $file.ModTime }}">{{ $file.Name }}</a></td>
|
<td><a href="{{ $file.Path }}" title="{{ $file.ModTime }}">{{ $file.Name }}</a></td>
|
||||||
<td>{{ formatBytes $file.Info.Size }}</td>
|
{{ if $file.Info.IsDir }}
|
||||||
|
<td>Dir</td>
|
||||||
|
{{ else }}
|
||||||
|
<td>{{ formatBytes $file.Info.Size }}</td>
|
||||||
|
{{ end }}
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</body></html>
|
</body></html>
|
||||||
`))
|
`))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user