リファクタリングと再設計

Signed-off-by: Izuru Yakumo <yakumo.izuru@chaotic.ninja>

git-svn-id: file:///srv/svn/repo/marisa/trunk@67 d6811dac-2434-b64a-9ddc-f563ab233461
This commit is contained in:
yakumo.izuru 2024-02-11 02:08:18 +00:00
parent 0f18a4e48e
commit c83cad8164
19 changed files with 431 additions and 530 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/marisa
/marisa-trash

View File

@ -1,27 +1,15 @@
package main package main
import ( import (
"encoding/json"
"flag" "flag"
"fmt"
"html/template"
"io"
"io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
"net/http/fcgi" "net/http/fcgi"
"os" "os"
"os/signal" "os/signal"
"os/user"
"path"
"path/filepath"
"strconv"
"syscall" "syscall"
"time"
"github.com/dustin/go-humanize"
"gopkg.in/ini.v1"
"marisa.chaotic.ninja/marisa" "marisa.chaotic.ninja/marisa"
) )
@ -54,276 +42,6 @@ var conf struct {
var verbose bool var verbose bool
func writefile(f *os.File, s io.ReadCloser, contentlength int64) error {
buffer := make([]byte, 4096)
eof := false
sz := int64(0)
defer f.Sync()
for !eof {
n, err := s.Read(buffer)
if err != nil && err != io.EOF {
return err
} else if err == io.EOF {
eof = true
}
/* ensure we don't write more than expected */
r := int64(n)
if sz+r > contentlength {
r = contentlength - sz
eof = true
}
_, err = f.Write(buffer[:r])
if err != nil {
return err
}
sz += r
}
return nil
}
func writemeta(filename string, expiry int64) error {
f, _ := os.Open(filename)
stat, _ := f.Stat()
size := stat.Size()
f.Close()
if expiry < 0 {
expiry = conf.expiry
}
meta := metadata{
Filename: filepath.Base(filename),
Size: size,
Expiry: time.Now().Unix() + expiry,
}
if verbose {
log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
}
f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
if err != nil {
return err
}
defer f.Close()
j, err := json.Marshal(meta)
if err != nil {
return err
}
_, err = f.Write(j)
return err
}
func servetemplate(w http.ResponseWriter, f string, d templatedata) {
t, err := template.ParseFiles(conf.tmplpath + "/" + f)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
if verbose {
log.Printf("Serving template %s", t.Name())
}
err = t.Execute(w, d)
if err != nil {
fmt.Println(err)
}
}
func uploaderPut(w http.ResponseWriter, r *http.Request) {
/* limit upload size */
if r.ContentLength > conf.maxsize {
http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
}
tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
f, err := os.Create(tmp.Name())
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
if verbose {
log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
}
if err = writefile(f, r.Body, r.ContentLength); err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
defer os.Remove(tmp.Name())
return
}
writemeta(tmp.Name(), conf.expiry)
resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
w.Write([]byte(resp + "\r\n"))
}
func uploaderPost(w http.ResponseWriter, r *http.Request) {
/* read 32Mb at a time */
r.ParseMultipartForm(32 << 20)
links := []string{}
for _, h := range r.MultipartForm.File["file"] {
if h.Size > conf.maxsize {
http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
return
}
post, err := h.Open()
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
defer post.Close()
tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
f, err := os.Create(tmp.Name())
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
defer f.Close()
if err = writefile(f, post, h.Size); err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
defer os.Remove(tmp.Name())
return
}
expiry, err := strconv.Atoi(r.PostFormValue("expiry"))
if err != nil || expiry < 0 {
expiry = int(conf.expiry)
}
writemeta(tmp.Name(), int64(expiry))
link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
links = append(links, link)
}
switch r.PostFormValue("output") {
case "html":
data := templatedata{
Maxsize: humanize.IBytes(uint64(conf.maxsize)),
Links: links,
}
servetemplate(w, "/index.html", data)
case "json":
data, _ := json.Marshal(links)
w.Write(data)
default:
for _, link := range links {
w.Write([]byte(link + "\r\n"))
}
}
}
func uploaderGet(w http.ResponseWriter, r *http.Request) {
// r.URL.Path is sanitized regarding "." and ".."
filename := r.URL.Path
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
servetemplate(w, "/index.html", data)
return
}
if verbose {
log.Printf("Serving file %s", conf.rootdir+filename)
}
http.ServeFile(w, r, conf.rootdir+filename)
}
func uploaderDelete(w http.ResponseWriter, r *http.Request) {
// r.URL.Path is sanitized regarding "." and ".."
filename := r.URL.Path
filepath := conf.filepath + filename
if verbose {
log.Printf("Deleting file %s", filepath)
}
f, err := os.Open(filepath)
if err != nil {
http.NotFound(w, r)
return
}
f.Close()
// Force file expiration
writemeta(filepath, 0)
w.WriteHeader(http.StatusNoContent)
}
func uploader(w http.ResponseWriter, r *http.Request) {
if verbose {
log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
}
switch r.Method {
case "DELETE":
uploaderDelete(w, r)
case "POST":
uploaderPost(w, r)
case "PUT":
uploaderPut(w, r)
case "GET":
uploaderGet(w, r)
}
}
func parseconfig(file string) error {
cfg, err := ini.Load(file)
if err != nil {
return err
}
conf.listen = cfg.Section("").Key("listen").String()
conf.user = cfg.Section("").Key("user").String()
conf.group = cfg.Section("").Key("group").String()
conf.baseuri = cfg.Section("").Key("baseuri").String()
conf.filepath = cfg.Section("").Key("filepath").String()
conf.metapath = cfg.Section("").Key("metapath").String()
conf.filectx = cfg.Section("").Key("filectx").String()
conf.rootdir = cfg.Section("").Key("rootdir").String()
conf.chroot = cfg.Section("").Key("chroot").String()
conf.tmplpath = cfg.Section("").Key("tmplpath").String()
conf.maxsize, _ = cfg.Section("").Key("maxsize").Int64()
conf.expiry, _ = cfg.Section("").Key("expiry").Int64()
return nil
}
func usergroupids(username string, groupname string) (int, int, error) {
u, err := user.Lookup(username)
if err != nil {
return -1, -1, err
}
uid, _ := strconv.Atoi(u.Uid)
gid, _ := strconv.Atoi(u.Gid)
if conf.group != "" {
g, err := user.LookupGroup(groupname)
if err != nil {
return uid, -1, err
}
gid, _ = strconv.Atoi(g.Gid)
}
return uid, gid, nil
}
func main() { func main() {
var err error var err error
var configfile string var configfile string

27
cmd/marisa/parseconfig.go Normal file
View File

@ -0,0 +1,27 @@
package main
import (
"gopkg.in/ini.v1"
)
func parseconfig(file string) error {
cfg, err := ini.Load(file)
if err != nil {
return err
}
conf.listen = cfg.Section("marisa").Key("listen").String()
conf.user = cfg.Section("marisa").Key("user").String()
conf.group = cfg.Section("marisa").Key("group").String()
conf.baseuri = cfg.Section("www").Key("baseuri").String()
conf.filepath = cfg.Section("www").Key("filepath").String()
conf.metapath = cfg.Section("www").Key("metapath").String()
conf.filectx = cfg.Section("www").Key("filectx").String()
conf.rootdir = cfg.Section("www").Key("rootdir").String()
conf.chroot = cfg.Section("marisa").Key("chroot").String()
conf.tmplpath = cfg.Section("www").Key("tmplpath").String()
conf.maxsize, _ = cfg.Section("www").Key("maxsize").Int64()
conf.expiry, _ = cfg.Section("www").Key("expiry").Int64()
return nil
}

View File

@ -0,0 +1,25 @@
package main
import (
"fmt"
"html/template"
"log"
"net/http"
)
func servetemplate(w http.ResponseWriter, f string, d templatedata) {
t, err := template.ParseFiles(conf.tmplpath + "/" + f)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
if verbose {
log.Printf("Serving template %s", t.Name())
}
err = t.Execute(w, d)
if err != nil {
fmt.Println(err)
}
}

23
cmd/marisa/uploader.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"log"
"net/http"
)
func uploader(w http.ResponseWriter, r *http.Request) {
if verbose {
log.Printf("%s: <%s> %s %s %s", r.Host, r.RemoteAddr, r.Method, r.RequestURI, r.Proto)
}
switch r.Method {
case "DELETE":
uploaderDelete(w, r)
case "POST":
uploaderPost(w, r)
case "PUT":
uploaderPut(w, r)
case "GET":
uploaderGet(w, r)
}
}

View File

@ -0,0 +1,28 @@
package main
import (
"log"
"net/http"
"os"
)
func uploaderDelete(w http.ResponseWriter, r *http.Request) {
// r.URL.Path is sanitized regarding "." and ".."
filename := r.URL.Path
filepath := conf.filepath + filename
if verbose {
log.Printf("Deleting file %s", filepath)
}
f, err := os.Open(filepath)
if err != nil {
http.NotFound(w, r)
return
}
f.Close()
// Force file expiration
writemeta(filepath, 0)
w.WriteHeader(http.StatusNoContent)
}

24
cmd/marisa/uploaderget.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"log"
"net/http"
"github.com/dustin/go-humanize"
)
func uploaderGet(w http.ResponseWriter, r *http.Request) {
// r.URL.Path is sanitized regarding "." and ".."
filename := r.URL.Path
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
data := templatedata{Maxsize: humanize.IBytes(uint64(conf.maxsize))}
servetemplate(w, "/index.html", data)
return
}
if verbose {
log.Printf("Serving file %s", conf.rootdir+filename)
}
http.ServeFile(w, r, conf.rootdir+filename)
}

View File

@ -0,0 +1,71 @@
package main
import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"github.com/dustin/go-humanize"
)
func uploaderPost(w http.ResponseWriter, r *http.Request) {
/* read 32Mb at a time */
r.ParseMultipartForm(32 << 20)
links := []string{}
for _, h := range r.MultipartForm.File["file"] {
if h.Size > conf.maxsize {
http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
return
}
post, err := h.Open()
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
defer post.Close()
tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(h.Filename))
f, err := os.Create(tmp.Name())
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
defer f.Close()
if err = writefile(f, post, h.Size); err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
defer os.Remove(tmp.Name())
return
}
expiry, err := strconv.Atoi(r.PostFormValue("expiry"))
if err != nil || expiry < 0 {
expiry = int(conf.expiry)
}
writemeta(tmp.Name(), int64(expiry))
link := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
links = append(links, link)
}
switch r.PostFormValue("output") {
case "html":
data := templatedata{
Maxsize: humanize.IBytes(uint64(conf.maxsize)),
Links: links,
}
servetemplate(w, "/index.html", data)
case "json":
data, _ := json.Marshal(links)
w.Write(data)
default:
for _, link := range links {
w.Write([]byte(link + "\r\n"))
}
}
}

40
cmd/marisa/uploaderput.go Normal file
View File

@ -0,0 +1,40 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"path/filepath"
)
func uploaderPut(w http.ResponseWriter, r *http.Request) {
/* limit upload size */
if r.ContentLength > conf.maxsize {
http.Error(w, "File is too big", http.StatusRequestEntityTooLarge)
}
tmp, _ := ioutil.TempFile(conf.filepath, "*"+path.Ext(r.URL.Path))
f, err := os.Create(tmp.Name())
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
if verbose {
log.Printf("Writing %d bytes to %s", r.ContentLength, tmp.Name())
}
if err = writefile(f, r.Body, r.ContentLength); err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
defer os.Remove(tmp.Name())
return
}
writemeta(tmp.Name(), conf.expiry)
resp := conf.baseuri + conf.filectx + filepath.Base(tmp.Name())
w.Write([]byte(resp + "\r\n"))
}

View File

@ -0,0 +1,26 @@
package main
import (
"os/user"
"strconv"
)
func usergroupids(username string, groupname string) (int, int, error) {
u, err := user.Lookup(username)
if err != nil {
return -1, -1, err
}
uid, _ := strconv.Atoi(u.Uid)
gid, _ := strconv.Atoi(u.Gid)
if conf.group != "" {
g, err := user.LookupGroup(groupname)
if err != nil {
return uid, -1, err
}
gid, _ = strconv.Atoi(g.Gid)
}
return uid, gid, nil
}

38
cmd/marisa/writefile.go Normal file
View File

@ -0,0 +1,38 @@
package main
import (
"io"
"os"
)
func writefile(f *os.File, s io.ReadCloser, contentlength int64) error {
buffer := make([]byte, 4096)
eof := false
sz := int64(0)
defer f.Sync()
for !eof {
n, err := s.Read(buffer)
if err != nil && err != io.EOF {
return err
} else if err == io.EOF {
eof = true
}
/* ensure we don't write more than expected */
r := int64(n)
if sz+r > contentlength {
r = contentlength - sz
eof = true
}
_, err = f.Write(buffer[:r])
if err != nil {
return err
}
sz += r
}
return nil
}

46
cmd/marisa/writemeta.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"encoding/json"
"log"
"os"
"path/filepath"
"time"
)
func writemeta(filename string, expiry int64) error {
f, _ := os.Open(filename)
stat, _ := f.Stat()
size := stat.Size()
f.Close()
if expiry < 0 {
expiry = conf.expiry
}
meta := metadata{
Filename: filepath.Base(filename),
Size: size,
Expiry: time.Now().Unix() + expiry,
}
if verbose {
log.Printf("Saving metadata for %s in %s", meta.Filename, conf.metapath+"/"+meta.Filename+".json")
}
f, err := os.Create(conf.metapath + "/" + meta.Filename + ".json")
if err != nil {
return err
}
defer f.Close()
j, err := json.Marshal(meta)
if err != nil {
return err
}
_, err = f.Write(j)
return err
}

View File

@ -1,36 +1,35 @@
# TCP or unix Socket to listen on. [marisa]
# When unix sockets are used, the content will be served over FastCGI. # TCP or Unix socket to listen on.
#listen = /var/run/marisa-fcgi.sock # When the Unix socket is used, the content will be served through FastCGI
listen = 127.0.0.1:9000 # listen = /var/run/marisa.sock
# listen = 127.0.0.1:9000
# Base to use when constructing URI to files uploaded.
# The full URI must be specified, in the form SCHEME://HOST[:PORT]
baseuri = http://127.0.0.1:9000
# Drop privilege to the user and group specified. # Drop privilege to the user and group specified.
# When only the user is specified, the default group of the user will # When only the user is specified, the default group of the user
# be used. # will be used.
# user = www # user = www
#group = daemon # group = www
# Change the root directory to the following directory. # Change the root directory to the following directory.
# When a chroot is set, all path must be given according to the chroot. # When a chroot(2) is set, all paths must be given according to it.
# Note: the configuration file is read before chrooting. # Note: the configuration file is read before it happens
#chroot = /var/www # chroot =
[www]
# baseuri = http://127.0.0.1:9000
# Path to the different path used by the server. Must take into account # Path to the resources used by the server, must take into account
# the chroot if set. # the chroot is set
rootdir = static # rootdir = ./static
tmplpath = templates # tmplpath = ./templates
filepath = files # filepath = ./files
metapath = meta # metapath = ./meta
# URI context that files will be served on # URI context that files will be served on
filectx = /f/ # filectx = /f/
# Maximum per-file upload size (in bytes) # Maximum per-file upload size (in bytes)
maxsize = 536870912 # 512Mib # maxsize = 536870912 # 512 MiB
# Default expiration time (in seconds). An expiration time of 0 seconds # Default expiration time (in seconds).
# means no expiration. # An expiration time of 0 seconds means no expiration.
expiry = 86400 # 24 hours # expiry = 86400 # 24 hours

View File

@ -1,98 +0,0 @@
// Handle drag and drop into a dropzone_element div:
// send the files as a POST request to the server
"use strict";
// Only start once the DOM tree is ready
if(document.readyState === "complete") {
setupzone();
} else {
document.addEventListener("DOMContentLoaded", setupzone);
}
function setupzone() {
let dropzone = document.getElementById("dropzone");
let fileinput = document.getElementById("filebox");
let fallbackform = document.getElementById("fallbackform");
fallbackform.style.display = "none";
dropzone.className = "dropzone";
dropzone.innerHTML = "Click or drop file(s)";
dropzone.onclick = function() {
fileinput.click()
return false;
}
dropzone.ondragover = function() {
this.className = "dropzone dragover";
return false;
}
dropzone.ondragleave = function() {
this.className = "dropzone";
return false;
}
dropzone.ondrop = function(e) {
// Stop browser from simply opening that was just dropped
e.preventDefault();
// Restore original dropzone appearance
this.className = "dropzone";
sendfiles(e.dataTransfer.files)
}
fileinput.onchange = function(e) {
sendfiles(this.files)
}
}
function sendfiles(files) {
let uploads = document.getElementById("uploads");
let progressbar = document.createElement("progress");
let uploadlist = document.createElement("ul");
let uploadtext = document.createElement("textarea");
let formData = new FormData(), xhr = new XMLHttpRequest();
// used for clipboard only
uploadtext.style.display = "none";
uploads.appendChild(progressbar);
uploads.appendChild(uploadlist);
uploads.appendChild(uploadtext);
formData.append("expiry", 10);
for(let i=0; i < files.length; i++) {
formData.append("file", files[i]);
}
// triggers periodically
xhr.upload.onprogress = function(e) {
// e.loaded - how many bytes downloaded
// e.lengthComputable = true if the server sent Content-Length header
// e.total - total number of bytes (if lengthComputable)
}
xhr.onreadystatechange = function() {
if(xhr.readyState === XMLHttpRequest.DONE) {
progressbar.remove();
this.response.split(/\r?\n/).forEach(function(link) {
let li = document.createElement("li");
li.innerHTML = `<a href="${link}">${link}</a>`;
uploadlist.appendChild(li);
});
let clippy = document.createElement("button");
uploads.appendChild(clippy);
clippy.innerText = " 📋 copy ";
clippy.onclick = function(e) {
uploadtext.select();
document.execCommand("copy");
}
}
}
xhr.open('POST', window.location.href, true); // async = true
xhr.send(formData);
}

BIN
example/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

17
example/static/marisa.css Normal file
View File

@ -0,0 +1,17 @@
body {
background-color: #282c37;
color: #f8f8f2;
font-family: sans-serif;
}
a {
color: #272822;
}
a:hover, a:link {
color: #e6db74;
}
a:visited {
color: #66d9ef;
}
table {
border-color: rgb(128, 0, 0);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,90 +0,0 @@
body {
padding: 5%;
margin: auto;
max-width: 540px;
font-family: sans-serif;
font-size: 1.5rem;
text-align: center;
background-color: #550000;
color: #ff4444;
}
header {
display: flex;
flex-direction: column;
flex-wrap: wrap-reverse;
align-items: center;
align-content: center;
}
section {
display: flex;
justify-content: flex-end;
font-size: initial;
}
section#formsettings > * {
margin-top: 20px;
margin-left: 20px;
}
img#logo {
height: 100%;
max-height: 30vh;
}
h1 {
font-size: 4.0rem;
}
#uploads {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#uploads > ul {
list-style: none;
text-align: left;
padding: 0;
}
#uploads > button {
align-self: flex-end;
margin-right: 10%;
}
.dropzone {
padding-top: 60px;
padding-bottom: 60px;
border: 2px dashed #888888;
border-radius: 8px;
text-align: center;
margin: auto;
color: #888888;
}
.dropzone.dragover {
color: #222222;
border-color: #222222;
}
/* font attributes are not inherited by default */
input, input::file-selector-button {
text-align: inherit;
font-family: inherit;
font-size: inherit;
}
@media (min-aspect-ratio: 18/9) {
header {
flex-direction: row;
}
h1 { font-size: 3rem; margin-right: 10px; }
img#logo {
height: 50%;
max-height: 20vh;
order: 2;
}
}

View File

@ -1,42 +1,47 @@
<!DOCTYPE HTML> <!DOCTYPE HTML PUBLIC "//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html> <html>
<head> <head>
<meta charset="utf-8"> <link rel="icon" href="/favicon.ico">
<meta name="author" content="z3bra, Izuru Yakumo"> <link rel="stylesheet" href="/marisa.css">
<meta name="robots" content="noindex,nofollow" > <meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta name="author" content="Izuru Yakumo">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<link rel="stylesheet" type="text/css" href="/marisa_98.css" >
<link rel="icon" href="/marisa.png">
<title>Marisa</title> <title>Marisa</title>
</head> </head>
<body> <body>
<header> <table border="1" align="center">
<img id="logo" src="/marisa.png" > <thead>
<h1>marisa</h1> <img class="logo" src="/marisa.png">
</header> <br>
<form enctype="multipart/form-data" method="post"> <h1>Marisa</h1>
<div id="dropzone"></div> </thead>
<div id="fallbackform" class="dropzone"> <tbody>
<input id="filebox" name="file" type="file" multiple> <form enctype="multipart/form-data" method="POST">
<input id="output" name="output" type="hidden" value='html' > <input class="file" name="file" type="file"><br>
<input type="submit" value="Upload"> <input name="output" type="hidden" value="html"><br>
</div> <input type="submit"><br>
<section id="formsettings">
<label for="expiry">Destroy after</label> <label for="expiry">Destroy after</label>
<select id="expiry" name="expiry"> <select name="expiry">
<option value="900">15 minutes</option> <option value="900">15 minutes</option>
<option value="3600">1 hour</option> <option value="3600">1 hour</option>
<option value="28800">8 hours</option> <option value="28800">8 hours</option>
<option value="86400" selected> 1 day </option> <option value="86400">1 day</option>
<option value="604800">1 week</option> <option value="604800">1 week</option>
</select> </select>
</section>
</form> </form>
<p>File size limited to {{.Maxsize}}.</p> <p>
<div id="uploads">{{if .Links}} File size limited to {{.Maxsize}}.
<ul> </p>
{{range .Links}}<li><a href="{{.}}">{{.}}</a></li>{{end}} </tbody>
</ul> <tfoot>
{{end}}</div> {{if .Links}}
<tr>
{{range .Links}}<td><a href="{{.}}">{{.}}</a></td>{{end}}
</tr>
{{end}}
<br><hr>
<p>&copy; 2024 Izuru Yakumo</p>
</tfoot>
</table>
</body> </body>
</html> </html>