2020-12-30 13:18:43 +08:00
|
|
|
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
2017-12-29 16:03:30 +08:00
|
|
|
//
|
|
|
|
// This Source Code Form is subject to the terms of the MIT License.
|
|
|
|
// If a copy of the MIT was not distributed with this file,
|
2019-02-02 16:18:25 +08:00
|
|
|
// You can obtain one at https://github.com/gogf/gf.
|
2017-12-31 18:19:58 +08:00
|
|
|
|
2017-11-23 10:21:28 +08:00
|
|
|
package ghttp
|
|
|
|
|
|
|
|
import (
|
2019-06-19 09:06:52 +08:00
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
2019-07-25 23:25:30 +08:00
|
|
|
|
2019-09-25 10:56:44 +08:00
|
|
|
"github.com/gogf/gf/text/gstr"
|
|
|
|
|
2019-09-14 22:53:28 +08:00
|
|
|
"github.com/gogf/gf/errors/gerror"
|
|
|
|
|
2019-08-14 22:03:52 +08:00
|
|
|
"github.com/gogf/gf/os/gres"
|
|
|
|
|
2019-07-29 21:01:19 +08:00
|
|
|
"github.com/gogf/gf/encoding/ghtml"
|
|
|
|
"github.com/gogf/gf/os/gfile"
|
|
|
|
"github.com/gogf/gf/os/gspath"
|
|
|
|
"github.com/gogf/gf/os/gtime"
|
2017-11-23 10:21:28 +08:00
|
|
|
)
|
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// ServeHTTP is the default handler for http request.
|
|
|
|
// It should not create new goroutine handling the request as
|
|
|
|
// it's called by am already created new goroutine from http.Server.
|
|
|
|
//
|
|
|
|
// This function also make serve implementing the interface of http.Handler.
|
2020-04-04 22:46:52 +08:00
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
2020-04-30 20:37:09 +08:00
|
|
|
// Max body size limit.
|
|
|
|
if s.config.ClientMaxBodySize > 0 {
|
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, s.config.ClientMaxBodySize)
|
|
|
|
}
|
2020-05-07 23:05:33 +08:00
|
|
|
|
|
|
|
// Rewrite feature checks.
|
2019-06-19 09:06:52 +08:00
|
|
|
if len(s.config.Rewrites) > 0 {
|
|
|
|
if rewrite, ok := s.config.Rewrites[r.URL.Path]; ok {
|
|
|
|
r.URL.Path = rewrite
|
|
|
|
}
|
|
|
|
}
|
2019-03-31 20:52:30 +08:00
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// Remove char '/' in the tail of URI.
|
2019-06-19 09:06:52 +08:00
|
|
|
if r.URL.Path != "/" {
|
2020-03-23 20:57:34 +08:00
|
|
|
for len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] == '/' {
|
2019-06-19 09:06:52 +08:00
|
|
|
r.URL.Path = r.URL.Path[:len(r.URL.Path)-1]
|
|
|
|
}
|
|
|
|
}
|
2018-04-16 16:23:34 +08:00
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// Default URI value if it's empty.
|
2020-03-23 20:57:34 +08:00
|
|
|
if r.URL.Path == "" {
|
|
|
|
r.URL.Path = "/"
|
|
|
|
}
|
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// Create a new request object.
|
2019-06-19 09:06:52 +08:00
|
|
|
request := newRequest(s, r, w)
|
2018-04-19 19:11:10 +08:00
|
|
|
|
2019-06-19 09:06:52 +08:00
|
|
|
defer func() {
|
2020-02-07 16:29:14 +08:00
|
|
|
request.LeaveTime = gtime.TimestampMilli()
|
2020-05-07 23:05:33 +08:00
|
|
|
// error log handling.
|
2019-09-14 22:53:28 +08:00
|
|
|
if request.error != nil {
|
|
|
|
s.handleErrorLog(request.error, request)
|
|
|
|
} else {
|
|
|
|
if exception := recover(); exception != nil {
|
|
|
|
request.Response.WriteStatus(http.StatusInternalServerError)
|
2020-12-30 13:18:43 +08:00
|
|
|
if err, ok := exception.(error); ok {
|
|
|
|
s.handleErrorLog(gerror.Wrap(err, ""), request)
|
|
|
|
} else {
|
|
|
|
s.handleErrorLog(gerror.Newf("%v", exception), request)
|
|
|
|
}
|
2019-09-14 22:53:28 +08:00
|
|
|
}
|
2019-06-19 09:06:52 +08:00
|
|
|
}
|
2020-05-07 23:05:33 +08:00
|
|
|
// access log handling.
|
2019-06-19 09:06:52 +08:00
|
|
|
s.handleAccessLog(request)
|
2020-05-07 23:05:33 +08:00
|
|
|
// Close the session, which automatically update the TTL
|
|
|
|
// of the session if it exists.
|
2019-11-04 21:26:16 +08:00
|
|
|
request.Session.Close()
|
2020-08-08 11:09:58 +08:00
|
|
|
|
|
|
|
// Close the request and response body
|
|
|
|
// to release the file descriptor in time.
|
2021-05-02 08:10:35 +08:00
|
|
|
_ = request.Request.Body.Close()
|
2020-08-08 11:09:58 +08:00
|
|
|
if request.Request.Response != nil {
|
2021-05-02 08:10:35 +08:00
|
|
|
_ = request.Request.Response.Body.Close()
|
2020-08-08 11:09:58 +08:00
|
|
|
}
|
2019-06-19 09:06:52 +08:00
|
|
|
}()
|
2018-04-19 19:11:10 +08:00
|
|
|
|
2019-06-19 09:06:52 +08:00
|
|
|
// ============================================================
|
2020-05-07 23:05:33 +08:00
|
|
|
// Priority:
|
|
|
|
// Static File > Dynamic Service > Static Directory
|
2019-06-19 09:06:52 +08:00
|
|
|
// ============================================================
|
2018-11-17 02:39:23 +08:00
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// Search the static file with most high priority,
|
|
|
|
// which also handle the index files feature.
|
2019-06-19 09:06:52 +08:00
|
|
|
if s.config.FileServerEnabled {
|
2020-01-17 21:12:52 +08:00
|
|
|
request.StaticFile = s.searchStaticFile(r.URL.Path)
|
|
|
|
if request.StaticFile != nil {
|
2019-06-19 09:06:52 +08:00
|
|
|
request.isFileRequest = true
|
|
|
|
}
|
|
|
|
}
|
2018-11-17 02:39:23 +08:00
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// Search the dynamic service handler.
|
2019-10-09 00:33:58 +08:00
|
|
|
request.handlers, request.hasHookHandler, request.hasServeHandler = s.getHandlersWithCache(request)
|
2018-08-24 14:57:49 +08:00
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// Check the service type static or dynamic for current request.
|
2020-01-17 21:12:52 +08:00
|
|
|
if request.StaticFile != nil && request.StaticFile.IsDir && request.hasServeHandler {
|
2019-06-19 09:06:52 +08:00
|
|
|
request.isFileRequest = false
|
|
|
|
}
|
2018-11-23 09:20:45 +08:00
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// HOOK - BeforeServe
|
2020-12-14 13:26:48 +08:00
|
|
|
s.callHookHandler(HookBeforeServe, request)
|
2018-08-16 18:17:47 +08:00
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// Core serving handling.
|
2019-06-19 09:06:52 +08:00
|
|
|
if !request.IsExited() {
|
2019-08-03 15:54:12 +08:00
|
|
|
if request.isFileRequest {
|
2020-05-07 23:05:33 +08:00
|
|
|
// Static file service.
|
2020-01-17 21:12:52 +08:00
|
|
|
s.serveFile(request, request.StaticFile)
|
2019-06-19 09:06:52 +08:00
|
|
|
} else {
|
2019-10-09 00:33:58 +08:00
|
|
|
if len(request.handlers) > 0 {
|
2020-05-07 23:05:33 +08:00
|
|
|
// Dynamic service.
|
2019-08-06 20:40:04 +08:00
|
|
|
request.Middleware.Next()
|
2019-06-19 09:06:52 +08:00
|
|
|
} else {
|
2020-01-17 21:12:52 +08:00
|
|
|
if request.StaticFile != nil && request.StaticFile.IsDir {
|
2020-05-07 23:05:33 +08:00
|
|
|
// Serve the directory.
|
2020-01-17 21:12:52 +08:00
|
|
|
s.serveFile(request, request.StaticFile)
|
2019-06-19 09:06:52 +08:00
|
|
|
} else {
|
|
|
|
if len(request.Response.Header()) == 0 &&
|
|
|
|
request.Response.Status == 0 &&
|
|
|
|
request.Response.BufferLength() == 0 {
|
2019-10-11 22:54:25 +08:00
|
|
|
request.Response.WriteHeader(http.StatusNotFound)
|
2019-06-19 09:06:52 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-05-31 12:07:31 +08:00
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// HOOK - AfterServe
|
2019-06-19 09:06:52 +08:00
|
|
|
if !request.IsExited() {
|
2020-12-14 13:26:48 +08:00
|
|
|
s.callHookHandler(HookAfterServe, request)
|
2019-06-19 09:06:52 +08:00
|
|
|
}
|
2019-09-04 20:22:31 +08:00
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// HOOK - BeforeOutput
|
2019-09-04 20:22:31 +08:00
|
|
|
if !request.IsExited() {
|
2020-12-14 13:26:48 +08:00
|
|
|
s.callHookHandler(HookBeforeOutput, request)
|
2019-09-04 20:22:31 +08:00
|
|
|
}
|
2019-10-09 00:33:58 +08:00
|
|
|
|
2019-10-11 22:54:25 +08:00
|
|
|
// HTTP status checking.
|
2019-10-09 00:33:58 +08:00
|
|
|
if request.Response.Status == 0 {
|
2020-01-17 21:12:52 +08:00
|
|
|
if request.StaticFile != nil || request.Middleware.served || request.Response.buffer.Len() > 0 {
|
2019-10-11 22:54:25 +08:00
|
|
|
request.Response.WriteHeader(http.StatusOK)
|
2019-10-09 00:33:58 +08:00
|
|
|
} else {
|
2019-10-11 22:54:25 +08:00
|
|
|
request.Response.WriteHeader(http.StatusNotFound)
|
2019-10-09 00:33:58 +08:00
|
|
|
}
|
|
|
|
}
|
2019-10-11 22:54:25 +08:00
|
|
|
// HTTP status handler.
|
|
|
|
if request.Response.Status != http.StatusOK {
|
2020-11-25 16:37:41 +08:00
|
|
|
statusFuncArray := s.getStatusHandler(request.Response.Status, request)
|
|
|
|
for _, f := range statusFuncArray {
|
2019-10-11 22:54:25 +08:00
|
|
|
// Call custom status handler.
|
|
|
|
niceCallFunc(func() {
|
|
|
|
f(request)
|
|
|
|
})
|
2020-11-25 16:37:41 +08:00
|
|
|
if request.IsExited() {
|
|
|
|
break
|
|
|
|
}
|
2019-10-11 22:54:25 +08:00
|
|
|
}
|
|
|
|
}
|
2019-10-09 00:33:58 +08:00
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// Automatically set the session id to cookie
|
2021-05-02 08:10:35 +08:00
|
|
|
// if it creates a new session id in this request
|
|
|
|
// and SessionCookieOutput is enabled.
|
2020-10-14 20:52:26 +08:00
|
|
|
if s.config.SessionCookieOutput &&
|
|
|
|
request.Session.IsDirty() &&
|
|
|
|
request.Session.Id() != request.GetSessionId() {
|
2019-09-11 21:19:45 +08:00
|
|
|
request.Cookie.SetSessionId(request.Session.Id())
|
|
|
|
}
|
2020-05-07 23:05:33 +08:00
|
|
|
// Output the cookie content to client.
|
2020-06-15 23:36:20 +08:00
|
|
|
request.Cookie.Flush()
|
2020-05-07 23:05:33 +08:00
|
|
|
// Output the buffer content to client.
|
2020-06-15 23:36:20 +08:00
|
|
|
request.Response.Flush()
|
2020-05-07 23:05:33 +08:00
|
|
|
// HOOK - AfterOutput
|
2019-09-04 20:22:31 +08:00
|
|
|
if !request.IsExited() {
|
2020-12-14 13:26:48 +08:00
|
|
|
s.callHookHandler(HookAfterOutput, request)
|
2019-09-04 20:22:31 +08:00
|
|
|
}
|
2018-04-11 12:05:25 +08:00
|
|
|
}
|
|
|
|
|
2020-01-15 21:23:40 +08:00
|
|
|
// searchStaticFile searches the file with given URI.
|
|
|
|
// It returns a file struct specifying the file information.
|
2021-01-19 19:33:21 +08:00
|
|
|
func (s *Server) searchStaticFile(uri string) *staticFile {
|
2019-08-14 22:03:52 +08:00
|
|
|
var file *gres.File
|
|
|
|
var path string
|
|
|
|
var dir bool
|
2019-10-12 23:56:03 +08:00
|
|
|
// Firstly search the StaticPaths mapping.
|
2019-06-19 09:06:52 +08:00
|
|
|
if len(s.config.StaticPaths) > 0 {
|
|
|
|
for _, item := range s.config.StaticPaths {
|
|
|
|
if len(uri) >= len(item.prefix) && strings.EqualFold(item.prefix, uri[0:len(item.prefix)]) {
|
2019-10-12 23:56:03 +08:00
|
|
|
// To avoid case like: /static/style -> /static/style.css
|
2019-06-19 09:06:52 +08:00
|
|
|
if len(uri) > len(item.prefix) && uri[len(item.prefix)] != '/' {
|
|
|
|
continue
|
|
|
|
}
|
2019-08-19 20:17:13 +08:00
|
|
|
file = gres.GetWithIndex(item.path+uri[len(item.prefix):], s.config.IndexFiles)
|
2019-08-14 22:03:52 +08:00
|
|
|
if file != nil {
|
2021-01-19 19:33:21 +08:00
|
|
|
return &staticFile{
|
2020-01-17 21:12:52 +08:00
|
|
|
File: file,
|
|
|
|
IsDir: file.FileInfo().IsDir(),
|
2019-08-14 22:03:52 +08:00
|
|
|
}
|
|
|
|
}
|
2019-08-19 20:17:13 +08:00
|
|
|
path, dir = gspath.Search(item.path, uri[len(item.prefix):], s.config.IndexFiles...)
|
|
|
|
if path != "" {
|
2021-01-19 19:33:21 +08:00
|
|
|
return &staticFile{
|
2020-01-17 21:12:52 +08:00
|
|
|
Path: path,
|
|
|
|
IsDir: dir,
|
2019-08-14 22:03:52 +08:00
|
|
|
}
|
|
|
|
}
|
2019-08-19 20:17:13 +08:00
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-01-15 21:23:40 +08:00
|
|
|
// Secondly search the root and searching paths.
|
2019-08-19 20:17:13 +08:00
|
|
|
if len(s.config.SearchPaths) > 0 {
|
|
|
|
for _, p := range s.config.SearchPaths {
|
|
|
|
file = gres.GetWithIndex(p+uri, s.config.IndexFiles)
|
|
|
|
if file != nil {
|
2021-01-19 19:33:21 +08:00
|
|
|
return &staticFile{
|
2020-01-17 21:12:52 +08:00
|
|
|
File: file,
|
|
|
|
IsDir: file.FileInfo().IsDir(),
|
2019-08-19 20:17:13 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if path, dir = gspath.Search(p, uri, s.config.IndexFiles...); path != "" {
|
2021-01-19 19:33:21 +08:00
|
|
|
return &staticFile{
|
2020-01-17 21:12:52 +08:00
|
|
|
Path: path,
|
|
|
|
IsDir: dir,
|
2019-08-19 20:17:13 +08:00
|
|
|
}
|
2019-06-19 09:06:52 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-01-15 21:23:40 +08:00
|
|
|
// Lastly search the resource manager.
|
2019-08-19 20:17:13 +08:00
|
|
|
if len(s.config.StaticPaths) == 0 && len(s.config.SearchPaths) == 0 {
|
|
|
|
if file = gres.GetWithIndex(uri, s.config.IndexFiles); file != nil {
|
2021-01-19 19:33:21 +08:00
|
|
|
return &staticFile{
|
2020-01-17 21:12:52 +08:00
|
|
|
File: file,
|
|
|
|
IsDir: file.FileInfo().IsDir(),
|
2019-08-14 22:03:52 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2018-11-30 09:48:57 +08:00
|
|
|
}
|
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// serveFile serves the static file for client.
|
|
|
|
// The optional parameter <allowIndex> specifies if allowing directory listing if <f> is directory.
|
2021-01-19 19:33:21 +08:00
|
|
|
func (s *Server) serveFile(r *Request, f *staticFile, allowIndex ...bool) {
|
2020-05-07 23:05:33 +08:00
|
|
|
// Use resource file from memory.
|
2020-01-17 21:12:52 +08:00
|
|
|
if f.File != nil {
|
|
|
|
if f.IsDir {
|
2019-08-14 22:03:52 +08:00
|
|
|
if s.config.IndexFolder || (len(allowIndex) > 0 && allowIndex[0]) {
|
2020-01-17 21:12:52 +08:00
|
|
|
s.listDir(r, f.File)
|
2019-08-14 22:03:52 +08:00
|
|
|
} else {
|
|
|
|
r.Response.WriteStatus(http.StatusForbidden)
|
|
|
|
}
|
|
|
|
} else {
|
2020-01-17 21:12:52 +08:00
|
|
|
info := f.File.FileInfo()
|
2019-10-12 23:56:03 +08:00
|
|
|
r.Response.wroteHeader = true
|
2020-01-17 21:12:52 +08:00
|
|
|
http.ServeContent(r.Response.Writer.RawWriter(), r.Request, info.Name(), info.ModTime(), f.File)
|
2019-08-14 22:03:52 +08:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2020-05-07 23:05:33 +08:00
|
|
|
// Use file from dist.
|
2020-01-17 21:12:52 +08:00
|
|
|
file, err := os.Open(f.Path)
|
2019-06-19 09:06:52 +08:00
|
|
|
if err != nil {
|
|
|
|
r.Response.WriteStatus(http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
2019-08-14 22:03:52 +08:00
|
|
|
defer file.Close()
|
2019-10-09 00:33:58 +08:00
|
|
|
|
|
|
|
// Clear the response buffer before file serving.
|
|
|
|
// It ignores all custom buffer content and uses the file content.
|
|
|
|
r.Response.ClearBuffer()
|
|
|
|
|
2019-08-14 22:03:52 +08:00
|
|
|
info, _ := file.Stat()
|
2019-06-19 09:06:52 +08:00
|
|
|
if info.IsDir() {
|
2019-07-24 22:48:43 +08:00
|
|
|
if s.config.IndexFolder || (len(allowIndex) > 0 && allowIndex[0]) {
|
2019-08-14 22:03:52 +08:00
|
|
|
s.listDir(r, file)
|
2019-06-19 09:06:52 +08:00
|
|
|
} else {
|
|
|
|
r.Response.WriteStatus(http.StatusForbidden)
|
|
|
|
}
|
|
|
|
} else {
|
2019-10-12 23:56:03 +08:00
|
|
|
r.Response.wroteHeader = true
|
2019-10-09 00:33:58 +08:00
|
|
|
http.ServeContent(r.Response.Writer.RawWriter(), r.Request, info.Name(), info.ModTime(), file)
|
2019-06-19 09:06:52 +08:00
|
|
|
}
|
2017-11-23 10:21:28 +08:00
|
|
|
}
|
|
|
|
|
2020-05-07 23:05:33 +08:00
|
|
|
// listDir lists the sub files of specified directory as HTML content to client.
|
2019-06-19 09:06:52 +08:00
|
|
|
func (s *Server) listDir(r *Request, f http.File) {
|
|
|
|
files, err := f.Readdir(-1)
|
|
|
|
if err != nil {
|
|
|
|
r.Response.WriteStatus(http.StatusInternalServerError, "Error reading directory")
|
|
|
|
return
|
|
|
|
}
|
2020-05-18 20:09:00 +08:00
|
|
|
// The folder type has the most priority than file.
|
|
|
|
sort.Slice(files, func(i, j int) bool {
|
|
|
|
if files[i].IsDir() && !files[j].IsDir() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if !files[i].IsDir() && files[j].IsDir() {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return files[i].Name() < files[j].Name()
|
|
|
|
})
|
2019-07-24 22:48:43 +08:00
|
|
|
if r.Response.Header().Get("Content-Type") == "" {
|
|
|
|
r.Response.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
}
|
|
|
|
r.Response.Write(`<html>`)
|
2019-09-25 10:56:44 +08:00
|
|
|
r.Response.Write(`<head>`)
|
|
|
|
r.Response.Write(`<style>`)
|
|
|
|
r.Response.Write(`body {font-family:Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;}`)
|
|
|
|
r.Response.Write(`</style>`)
|
|
|
|
r.Response.Write(`</head>`)
|
2019-07-24 22:48:43 +08:00
|
|
|
r.Response.Write(`<body>`)
|
2019-07-25 23:25:30 +08:00
|
|
|
r.Response.Writef(`<h1>Index of %s</h1>`, r.URL.Path)
|
2019-07-24 22:48:43 +08:00
|
|
|
r.Response.Writef(`<hr />`)
|
|
|
|
r.Response.Write(`<table>`)
|
2019-06-19 09:06:52 +08:00
|
|
|
if r.URL.Path != "/" {
|
2019-07-25 23:25:30 +08:00
|
|
|
r.Response.Write(`<tr>`)
|
|
|
|
r.Response.Writef(`<td><a href="%s">..</a></td>`, gfile.Dir(r.URL.Path))
|
|
|
|
r.Response.Write(`</tr>`)
|
2019-06-19 09:06:52 +08:00
|
|
|
}
|
2019-07-24 22:48:43 +08:00
|
|
|
name := ""
|
|
|
|
size := ""
|
2019-09-25 10:56:44 +08:00
|
|
|
prefix := gstr.TrimRight(r.URL.Path, "/")
|
2019-06-19 09:06:52 +08:00
|
|
|
for _, file := range files {
|
2019-07-24 22:48:43 +08:00
|
|
|
name = file.Name()
|
|
|
|
size = gfile.FormatSize(file.Size())
|
2019-06-19 09:06:52 +08:00
|
|
|
if file.IsDir() {
|
|
|
|
name += "/"
|
2019-07-24 22:48:43 +08:00
|
|
|
size = "-"
|
2019-06-19 09:06:52 +08:00
|
|
|
}
|
2019-07-24 22:48:43 +08:00
|
|
|
r.Response.Write(`<tr>`)
|
2019-09-25 10:56:44 +08:00
|
|
|
r.Response.Writef(`<td><a href="%s/%s">%s</a></td>`, prefix, name, ghtml.SpecialChars(name))
|
2019-09-01 22:22:08 +08:00
|
|
|
r.Response.Writef(`<td style="width:300px;text-align:center;">%s</td>`, gtime.New(file.ModTime()).ISO8601())
|
2019-09-25 10:56:44 +08:00
|
|
|
r.Response.Writef(`<td style="width:80px;text-align:right;">%s</td>`, size)
|
2019-07-24 22:48:43 +08:00
|
|
|
r.Response.Write(`</tr>`)
|
2019-06-19 09:06:52 +08:00
|
|
|
}
|
2019-07-24 22:48:43 +08:00
|
|
|
r.Response.Write(`</table>`)
|
|
|
|
r.Response.Write(`</body>`)
|
|
|
|
r.Response.Write(`</html>`)
|
2017-11-23 10:21:28 +08:00
|
|
|
}
|