rewrite html generator with html/template
This commit is contained in:
@@ -4,3 +4,8 @@ bin/
|
|||||||
__debug_bin*
|
__debug_bin*
|
||||||
graphify-out/
|
graphify-out/
|
||||||
graphify_out/
|
graphify_out/
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
TEMP/
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package disk_usage
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrNotSupported = errors.New("disk_usage: not supported on this OS")
|
||||||
|
|
||||||
|
// Used + Free can be less than Total: space reserved for root/admin
|
||||||
|
// is not free for this process, but not used either.
|
||||||
|
type Usage struct {
|
||||||
|
// size of the file system (available to this user, if there are quotas)
|
||||||
|
Total int64
|
||||||
|
// Total minus all free space, including reserved
|
||||||
|
Used int64
|
||||||
|
// available to this process
|
||||||
|
Free int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiskUsage returns usage of the file system that contains path.
|
||||||
|
func DiskUsage(path string) (Usage, error) {
|
||||||
|
return diskUsage(path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !(linux || darwin || freebsd || windows)
|
||||||
|
|
||||||
|
// Fallback, so the program still builds on other OSes.
|
||||||
|
|
||||||
|
package disk_usage
|
||||||
|
|
||||||
|
func diskUsage(path string) (Usage, error) {
|
||||||
|
return Usage{}, ErrNotSupported
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//go:build linux || darwin || freebsd
|
||||||
|
|
||||||
|
// "_unix" is not a special file name suffix for Go, so the OS list is in the //go:build line.
|
||||||
|
// Not "unix": openbsd and netbsd have different Statfs_t or no syscall.Statfs.
|
||||||
|
|
||||||
|
package disk_usage
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
func diskUsage(path string) (Usage, error) {
|
||||||
|
var st syscall.Statfs_t
|
||||||
|
if err := syscall.Statfs(path, &st); err != nil {
|
||||||
|
return Usage{}, err
|
||||||
|
}
|
||||||
|
// Field types differ between OSes, int64() works for all of them.
|
||||||
|
bsize := int64(st.Bsize)
|
||||||
|
return Usage{
|
||||||
|
Total: int64(st.Blocks) * bsize,
|
||||||
|
// Bfree: all free blocks, including reserved for root
|
||||||
|
Used: (int64(st.Blocks) - int64(st.Bfree)) * bsize,
|
||||||
|
// Bavail: free blocks available to unprivileged users
|
||||||
|
Free: int64(st.Bavail) * bsize,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package disk_usage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Loaded on first call. kernel32.dll is a KnownDLL,
|
||||||
|
// Windows always loads it from the system directory.
|
||||||
|
var procGetDiskFreeSpaceExW = syscall.NewLazyDLL("kernel32.dll").NewProc("GetDiskFreeSpaceExW")
|
||||||
|
|
||||||
|
func diskUsage(path string) (Usage, error) {
|
||||||
|
pathPtr, err := syscall.UTF16PtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return Usage{}, err
|
||||||
|
}
|
||||||
|
// BOOL GetDiskFreeSpaceExW(LPCWSTR lpDirectoryName,
|
||||||
|
// PULARGE_INTEGER lpFreeBytesAvailableToCaller, // respects user disk quotas
|
||||||
|
// PULARGE_INTEGER lpTotalNumberOfBytes, // respects user disk quotas
|
||||||
|
// PULARGE_INTEGER lpTotalNumberOfFreeBytes); // whole disk
|
||||||
|
var freeToCaller, total, totalFree uint64
|
||||||
|
ok, _, callErr := procGetDiskFreeSpaceExW.Call(
|
||||||
|
uintptr(unsafe.Pointer(pathPtr)),
|
||||||
|
uintptr(unsafe.Pointer(&freeToCaller)),
|
||||||
|
uintptr(unsafe.Pointer(&total)),
|
||||||
|
uintptr(unsafe.Pointer(&totalFree)))
|
||||||
|
if ok == 0 {
|
||||||
|
// Call always returns non-nil error, it is meaningful only if the function failed
|
||||||
|
return Usage{}, callErr
|
||||||
|
}
|
||||||
|
return Usage{
|
||||||
|
Total: int64(total),
|
||||||
|
// with quotas totalFree can be > total
|
||||||
|
Used: max(int64(total)-int64(totalFree), 0),
|
||||||
|
Free: int64(freeToCaller),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
{{- /* Directory listing page.
|
||||||
|
Shows directory content and has upload form. */ -}}
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; margin: 2em; }
|
||||||
|
td { padding: .2em 1em .2em 0; }
|
||||||
|
td.size, td.date { color: #666; white-space: nowrap; }
|
||||||
|
caption { text-align: left; color: #666; padding-bottom: .5em; }
|
||||||
|
#status p { margin: .3em 0; }
|
||||||
|
.error { color: #c00; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{.Path}}</h1>
|
||||||
|
|
||||||
|
{{- /* upload form, visible only if share has write_password */}}
|
||||||
|
{{if .Writable}}
|
||||||
|
<form id="upload" data-base="{{.PathUrl}}">
|
||||||
|
<input type="file" id="files" multiple required>
|
||||||
|
<input type="password" id="password" placeholder="write password" autocomplete="current-password" required>
|
||||||
|
<button>Upload</button>
|
||||||
|
</form>
|
||||||
|
<div id="status"></div>
|
||||||
|
{{- /* dialog that appears if file with such name exists in the directory */}}
|
||||||
|
<dialog id="replace">
|
||||||
|
<p></p>
|
||||||
|
<form method="dialog">
|
||||||
|
<button value="yes">Yes</button>
|
||||||
|
<button value="no">No</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
|
||||||
|
{{- /* listing element is replaced by id after file upload */}}
|
||||||
|
<table id="listing">
|
||||||
|
<caption>Free space: {{.FreeSpace}}</caption>
|
||||||
|
<tr><td><a href="{{.ParentUrl}}">../</a></td></tr>
|
||||||
|
{{- range .Entries}}
|
||||||
|
<tr>
|
||||||
|
{{- if .IsDir}}
|
||||||
|
{{- /*Directory links end with '/', because browser remembers password for directory with trailing slash.*/}}
|
||||||
|
<td><a href="{{.Url}}/">{{.Name}}/</a></td>
|
||||||
|
{{- else}}
|
||||||
|
{{- /* download attribute: clicking a file link saves the file instead of opening it in the browser. */}}
|
||||||
|
<td><a href="{{.Url}}" download>{{.Name}}</a></td>
|
||||||
|
{{- end}}
|
||||||
|
<td class="size">{{.Size}}</td>
|
||||||
|
<td class="date">{{.Date}}</td>
|
||||||
|
</tr>
|
||||||
|
{{- end}}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{- /*
|
||||||
|
Upload: for each file asks the server whether the upload would be accepted
|
||||||
|
(OPTIONS with X-Upload-Size), asks before replacing an existing file,
|
||||||
|
uploads with progress, then reloads the file table.
|
||||||
|
The OPTIONS check comes first, because a PUT rejected while the browser is still sending the file
|
||||||
|
ends with a connection reset, and the browser reports only "NetworkError" without reading response error.
|
||||||
|
Error messages are the response bodies.
|
||||||
|
The file is sent as the raw PUT body, the server doesn't need to parse multipart forms.
|
||||||
|
*/}}
|
||||||
|
{{if .Writable}}
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById("upload");
|
||||||
|
const filesInput = document.getElementById("files");
|
||||||
|
const passwordInput = document.getElementById("password");
|
||||||
|
const uploadButton = form.querySelector("button");
|
||||||
|
const statusBox = document.getElementById("status");
|
||||||
|
const replaceDialog = document.getElementById("replace");
|
||||||
|
const base = form.dataset.base;
|
||||||
|
|
||||||
|
function formatSize(n) {
|
||||||
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
let i = 0;
|
||||||
|
for (; n >= 1024 && i < units.length - 1; i++)
|
||||||
|
n /= 1024;
|
||||||
|
return (i === 0 ? n : n.toFixed(1)) + units[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
function authorization() {
|
||||||
|
const bytes = new TextEncoder().encode(":" + passwordInput.value);
|
||||||
|
return "Basic " + btoa(String.fromCharCode(...bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
// the server explains errors in the response body
|
||||||
|
function errorText(status, body) {
|
||||||
|
return body.trim() || "HTTP " + status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function askReplace(name) {
|
||||||
|
replaceDialog.querySelector("p").textContent = 'File "' + name + '" already exists. Replace it?';
|
||||||
|
replaceDialog.returnValue = "";
|
||||||
|
replaceDialog.showModal();
|
||||||
|
return new Promise(resolve => replaceDialog.addEventListener("close",
|
||||||
|
() => resolve(replaceDialog.returnValue === "yes"), { once: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function put(url, file, line) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("PUT", url);
|
||||||
|
xhr.setRequestHeader("Authorization", authorization());
|
||||||
|
const start = performance.now();
|
||||||
|
xhr.upload.onprogress = (e) => {
|
||||||
|
const seconds = (performance.now() - start) / 1000;
|
||||||
|
const speed = seconds > 0 ? e.loaded / seconds : 0;
|
||||||
|
line.textContent = 'Uploading file "' + file.name + '" '
|
||||||
|
+ formatSize(e.loaded) + "/" + formatSize(e.total) + " (" + formatSize(speed) + "/s)";
|
||||||
|
};
|
||||||
|
xhr.onload = () => xhr.status >= 200 && xhr.status < 300
|
||||||
|
? resolve() : reject(new Error(errorText(xhr.status, xhr.responseText)));
|
||||||
|
xhr.onerror = () => reject(new Error("connection failed"));
|
||||||
|
xhr.send(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload(file, line) {
|
||||||
|
const url = base + encodeURIComponent(file.name);
|
||||||
|
line.textContent = 'Checking file "' + file.name + '"...';
|
||||||
|
const check = await fetch(url, {
|
||||||
|
method: "OPTIONS",
|
||||||
|
headers: { "Authorization": authorization(), "X-Upload-Size": String(file.size) },
|
||||||
|
});
|
||||||
|
// 200: the file exists and would be replaced, 201: it would be created,
|
||||||
|
// anything else: the upload would be rejected
|
||||||
|
if (check.status === 200) {
|
||||||
|
if (!await askReplace(file.name)) {
|
||||||
|
line.textContent = 'File "' + file.name + '" skipped';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (check.status !== 201) {
|
||||||
|
throw new Error(errorText(check.status, await check.text()));
|
||||||
|
}
|
||||||
|
await put(url, file, line);
|
||||||
|
line.textContent = 'File "' + file.name + '" uploaded';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshListing() {
|
||||||
|
const res = await fetch(location.href, { cache: "no-store" });
|
||||||
|
if (!res.ok)
|
||||||
|
return;
|
||||||
|
const page = new DOMParser().parseFromString(await res.text(), "text/html");
|
||||||
|
const table = page.getElementById("listing");
|
||||||
|
if (table)
|
||||||
|
document.getElementById("listing").replaceWith(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadButton.disabled = true;
|
||||||
|
statusBox.replaceChildren();
|
||||||
|
const notUploaded = new DataTransfer();
|
||||||
|
for (const file of filesInput.files) {
|
||||||
|
const line = document.createElement("p");
|
||||||
|
statusBox.append(line);
|
||||||
|
try {
|
||||||
|
if (!await upload(file, line))
|
||||||
|
notUploaded.items.add(file);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
line.textContent = 'Error: file "' + file.name + '": ' + err.message;
|
||||||
|
line.className = "error";
|
||||||
|
notUploaded.items.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// uploaded files are removed from the selection, the rest stays for another try
|
||||||
|
filesInput.files = notUploaded.files;
|
||||||
|
uploadButton.disabled = false;
|
||||||
|
await refreshListing();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package html
|
|
||||||
|
|
||||||
import sh "serveg/internal/string_helper"
|
|
||||||
|
|
||||||
const DateTimeTZ = "2006-01-02 15:04:05 UTC-07"
|
|
||||||
|
|
||||||
func htmlEscape(s string) string {
|
|
||||||
sb := sh.StringBuilder{Buf: make([]byte, 0, len(s))}
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
b := s[i]
|
|
||||||
switch b {
|
|
||||||
case '&':
|
|
||||||
sb.AddS("&")
|
|
||||||
case '<':
|
|
||||||
sb.AddS("<")
|
|
||||||
case '>':
|
|
||||||
sb.AddS(">")
|
|
||||||
case '"':
|
|
||||||
sb.AddS(""")
|
|
||||||
case '\'':
|
|
||||||
sb.AddS("'")
|
|
||||||
default:
|
|
||||||
sb.AddB(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.StringUnsafe()
|
|
||||||
}
|
|
||||||
+76
-78
@@ -1,108 +1,106 @@
|
|||||||
package html
|
package html
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"html/template"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"serveg/internal/disk_usage"
|
||||||
"serveg/internal/http"
|
"serveg/internal/http"
|
||||||
"serveg/internal/log"
|
|
||||||
sh "serveg/internal/string_helper"
|
sh "serveg/internal/string_helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
type listingRow struct {
|
const DateTimeTZ = "2006-01-02 15:04:05 UTC-07"
|
||||||
name string // html-escaped
|
|
||||||
url string // url-encoded
|
// The template file is embedded into the binary at build time.
|
||||||
mtime string // formatted with DateTimeTZ
|
//
|
||||||
size string // formatted with HumanBytes
|
//go:embed ListingPage.gohtml
|
||||||
isDir bool
|
var listingPageSrc string
|
||||||
|
|
||||||
|
// Parsed once at startup, Execute() is safe for concurrent use.
|
||||||
|
// Must() panics on syntax error, so a broken template fails at start, not on request.
|
||||||
|
var listingPageTemplate = template.Must(template.New("ListingPage").Parse(listingPageSrc))
|
||||||
|
|
||||||
|
// Data for ListingPage.gohtml. Fields must be exported to be visible in the template.
|
||||||
|
// Values are not escaped: html/template does it.
|
||||||
|
type listingPage struct {
|
||||||
|
Title string
|
||||||
|
Path string // directory url, ends with '/'
|
||||||
|
PathUrl string // percent-encoded Path
|
||||||
|
ParentUrl string // percent-encoded, ends with '/'
|
||||||
|
Writable bool // show the upload form
|
||||||
|
FreeSpace string // formatted with HumanBytes
|
||||||
|
Entries []listingEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
func (row *listingRow) genHtml(sb *sh.StringBuilder) {
|
type listingEntry struct {
|
||||||
sb.AddS(`<tr>`)
|
Name string
|
||||||
sb.AddS(`<td><a href="`).AddS(row.url)
|
Url string // percent-encoded, without trailing '/'
|
||||||
if row.isDir {
|
Size string // formatted with HumanBytes, empty for directories
|
||||||
// add slash to directory url
|
Date string // formatted with DateTimeTZ
|
||||||
// because browser remembers password for directory with trailing slash
|
IsDir bool
|
||||||
sb.AddS(`/">`)
|
|
||||||
} else {
|
|
||||||
// tell browser to download file instead of opening in new tab
|
|
||||||
sb.AddS(`" download>`)
|
|
||||||
}
|
|
||||||
sb.AddS(row.name).AddS(`</a></td>`)
|
|
||||||
sb.AddS(`<td class="size">`).AddS(row.size).AddS(`</td>`)
|
|
||||||
sb.AddS(`<td class="date">`).AddS(row.mtime).AddS(`</td>`)
|
|
||||||
sb.AddS(`</tr>`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenDirListingHtml(logger *log.LoggerContext, dirPath, reqUrl, pageTitle string) ([]byte, error) {
|
// reqUrl is a clean url without trailing slash (except "/"), see sh.ResolveSafePath
|
||||||
|
func GenDirListingHtml(dirPath, reqUrl, pageTitle string, writable bool) ([]byte, error) {
|
||||||
dirEntries, err := os.ReadDir(dirPath)
|
dirEntries, err := os.ReadDir(dirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var dirs, files []listingRow
|
dirUrl := withTrailingSlash(reqUrl)
|
||||||
var estimatedLen = 460 // html for page without entries
|
page := listingPage{
|
||||||
|
Title: pageTitle + " " + dirUrl,
|
||||||
|
Path: dirUrl,
|
||||||
|
PathUrl: http.PercentEcodeUrl(dirUrl),
|
||||||
|
ParentUrl: http.PercentEcodeUrl(withTrailingSlash(path.Dir(reqUrl))),
|
||||||
|
Writable: writable,
|
||||||
|
Entries: make([]listingEntry, 0, len(dirEntries)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// listing is still useful without free space, so error is shown on the page
|
||||||
|
if usage, err := disk_usage.DiskUsage(dirPath); err == nil {
|
||||||
|
page.FreeSpace = sh.HumanBytes(usage.Free)
|
||||||
|
} else {
|
||||||
|
page.FreeSpace = "unknown (" + err.Error() + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// directories first, then files, both sorted by name (os.ReadDir sorts)
|
||||||
|
var files []listingEntry
|
||||||
for _, e := range dirEntries {
|
for _, e := range dirEntries {
|
||||||
einf, err := e.Info()
|
einf, err := e.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
entry := listingEntry{
|
||||||
row := listingRow{
|
Name: e.Name(),
|
||||||
name: htmlEscape(e.Name()),
|
Url: http.PercentEcodeUrl(dirUrl + e.Name()),
|
||||||
url: http.PercentEcodeUrl(path.Join(reqUrl, e.Name())),
|
Date: einf.ModTime().Format(DateTimeTZ),
|
||||||
mtime: einf.ModTime().Format(DateTimeTZ),
|
IsDir: e.IsDir(),
|
||||||
isDir: e.IsDir(),
|
|
||||||
}
|
}
|
||||||
if row.isDir {
|
if entry.IsDir {
|
||||||
row.size = "DIR"
|
page.Entries = append(page.Entries, entry)
|
||||||
dirs = append(dirs, row)
|
|
||||||
} else {
|
} else {
|
||||||
row.size = sh.HumanBytes(einf.Size())
|
entry.Size = sh.HumanBytes(einf.Size())
|
||||||
files = append(files, row)
|
files = append(files, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
estimatedLen += len(row.name) + len(row.url) + len(row.mtime) + len(row.size)
|
|
||||||
estimatedLen += 100 // html for row
|
|
||||||
}
|
}
|
||||||
|
page.Entries = append(page.Entries, files...)
|
||||||
|
|
||||||
sb := sh.StringBuilder{Buf: make([]byte, 0, estimatedLen)}
|
// render to buffer, not to socket: content-length must be known,
|
||||||
sb.AddS(`<!doctype html>
|
// and an error in the middle must not send a half page
|
||||||
<html>
|
var buf bytes.Buffer
|
||||||
<head>
|
buf.Grow(len(listingPageSrc) + len(page.Entries)*200)
|
||||||
<meta charset="utf-8">
|
if err := listingPageTemplate.Execute(&buf, &page); err != nil {
|
||||||
<title>`)
|
return nil, err
|
||||||
sb.AddS(htmlEscape(pageTitle + " " + reqUrl))
|
|
||||||
sb.AddS(`
|
|
||||||
</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; margin: 2em; }
|
|
||||||
td { padding: .2em 1em .2em 0; }
|
|
||||||
td.size, td.date { color: #666; white-space: nowrap; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>`)
|
|
||||||
sb.AddS("<h1>").AddS(htmlEscape(reqUrl)).AddS("</h1>")
|
|
||||||
sb.AddS(`<table id="listing">`)
|
|
||||||
sb.AddS(`<tr><td><a href="`)
|
|
||||||
sb.AddS(http.PercentEcodeUrl(path.Dir(reqUrl)))
|
|
||||||
sb.AddS(`">../</a></td></tr>`)
|
|
||||||
|
|
||||||
for i := 0; i < len(dirs); i++ {
|
|
||||||
dirs[i].genHtml(&sb)
|
|
||||||
}
|
}
|
||||||
for i := 0; i < len(files); i++ {
|
return buf.Bytes(), nil
|
||||||
files[i].genHtml(&sb)
|
}
|
||||||
}
|
|
||||||
|
func withTrailingSlash(s string) string {
|
||||||
sb.AddS(`
|
if len(s) > 0 && s[len(s)-1] == '/' {
|
||||||
</table>
|
return s
|
||||||
</body>
|
}
|
||||||
</html>`)
|
return s + "/"
|
||||||
|
|
||||||
realLen := sb.Len()
|
|
||||||
if estimatedLen < realLen {
|
|
||||||
logger.Warn("genDirListingHtml(): size prediction failed. estimatedLen: ",
|
|
||||||
estimatedLen, " realLen: ", realLen)
|
|
||||||
}
|
|
||||||
return sb.Buf, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,16 +41,19 @@ func NewConnection(
|
|||||||
logger *log.LoggerContext,
|
logger *log.LoggerContext,
|
||||||
callback ConnCallback,
|
callback ConnCallback,
|
||||||
) *Connection {
|
) *Connection {
|
||||||
|
r := bufio.NewReaderSize(socket, maxHeaderBytes)
|
||||||
return &Connection{
|
return &Connection{
|
||||||
Timeout: defaultTimeout,
|
Timeout: defaultTimeout,
|
||||||
log: logger,
|
log: logger,
|
||||||
callback: callback,
|
callback: callback,
|
||||||
socket: socket,
|
socket: socket,
|
||||||
r: bufio.NewReaderSize(socket, maxHeaderBytes),
|
r: r,
|
||||||
wbuf: sh.StringBuilder{Buf: make([]byte, 0, initialWriteBufCap)},
|
wbuf: sh.StringBuilder{Buf: make([]byte, 0, initialWriteBufCap)},
|
||||||
_req: Request{
|
_req: Request{
|
||||||
QueryParams: make(mapStrStr),
|
QueryParams: make(mapStrStr),
|
||||||
Headers: make(HeadersMap),
|
Headers: make(HeadersMap),
|
||||||
|
// body is read from the same buffered reader as headers
|
||||||
|
bodyReader: io.LimitedReader{R: r},
|
||||||
},
|
},
|
||||||
_res: Response{
|
_res: Response{
|
||||||
Headers: make(HeadersMap),
|
Headers: make(HeadersMap),
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ func (conn *TcpConnection) GET_HEAD(req *http.Request) *http.Response {
|
|||||||
case UrlFile:
|
case UrlFile:
|
||||||
return conn.serveFile(req, path)
|
return conn.serveFile(req, path)
|
||||||
case UrlDir:
|
case UrlDir:
|
||||||
return conn.serveDirListing(req, path)
|
// show upload form if the share accepts uploads
|
||||||
|
writable := share != nil && share.WritePassword != ""
|
||||||
|
return conn.serveDirListing(req, path, writable)
|
||||||
case UrlShareNotFound, UrlDoesntExistInShare:
|
case UrlShareNotFound, UrlDoesntExistInShare:
|
||||||
return conn.createErrorResponse(req, false, http.StatusNotFound, req.Url)
|
return conn.createErrorResponse(req, false, http.StatusNotFound, req.Url)
|
||||||
case UrlForbidden:
|
case UrlForbidden:
|
||||||
@@ -39,9 +41,9 @@ func (conn *TcpConnection) GET_HEAD(req *http.Request) *http.Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *TcpConnection) serveDirListing(req *http.Request, dirPath string) *http.Response {
|
func (conn *TcpConnection) serveDirListing(req *http.Request, dirPath string, writable bool) *http.Response {
|
||||||
htmlBytes, err := html.GenDirListingHtml(&conn.log, dirPath,
|
htmlBytes, err := html.GenDirListingHtml(dirPath,
|
||||||
req.Url, conn.srv.Conf.HtmlTitle)
|
req.Url, conn.srv.Conf.HtmlTitle, writable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return conn.createErrorResponse(req, false, http.StatusInternalServerError, err.Error())
|
return conn.createErrorResponse(req, false, http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user