238
package main
import (
"embed"
"errors"
"fmt"
"git.sophuwu.com/cdn/config"
"git.sophuwu.com/cdn/imgconv"
"github.com/asdine/storm/v3"
"go.etcd.io/bbolt"
"io/fs"
"time"
_ "embed"
"html/template"
"io"
"net/http"
"os"
"path/filepath"
"strings"
)
//go:embed html/*
var embedHtml embed.FS
type TimeStr struct {
Full string
Short string
}
type DirEntry struct {
Icon string
Name string
Size string
Url string
mod int64
Mod TimeStr
}
func DateToInt(t time.Time) int { // bit size: y 12, mon 4, day 5
return t.Year()<<(12+4+5) | int(t.Month())<<(4+5) | t.Day()
}
func FmtTime(t time.Time, today int) TimeStr {
var pt TimeStr
pt.Full = t.Format("Mon 02 Jan 2006 15:04")
if DateToInt(t) == today {
pt.Short = "today " + t.Format("15:04")
} else {
pt.Short = t.Format("02 Jan 2006")
}
return pt
}
func Si(d int64) string {
f := float64(d)
i := 0
for f > 1024 {
f /= 1024
i++
}
s := fmt.Sprintf("%.2f", f)
s = strings.TrimRight(s, ".0")
return fmt.Sprintf("%s %cB", s, " KMGTPEZY"[i])
}
type TemplateData struct {
Path string
Dirs []DirEntry
Items []DirEntry
}
func (t *TemplateData) add(a DirEntry, size int64, dir bool) {
if dir {
a.Size = func() string {
n, e := os.ReadDir(filepath.Join(config.HttpDir, a.Url))
if e == nil {
return fmt.Sprintf("%d items", len(n))
}
return "0 items"
}()
a.Icon = "F"
t.Dirs = append(t.Dirs, a)
} else {
a.Icon = "f"
a.Size = Si(size)
t.Items = append(t.Items, a)
}
}
func (t *TemplateData) sortNewest() {
for _, tt := range []*[]DirEntry{&t.Items, &t.Dirs} {
for i := 0; i < len(*tt); i++ {
for j := i + 1; j < len(*tt); j++ {
if (*tt)[i].mod < (*tt)[j].mod {
(*tt)[i], (*tt)[j] = (*tt)[j], (*tt)[i]
}
}
}
}
}
var Temp *template.Template
var HttpCodes = map[int]string{
404: "Not Found",
500: "Internal Server Error",
403: "Forbidden",
401: "Unauthorized",
400: "Bad Request",
200: "OK",
}
func FillError(w io.Writer, err error, code int) bool {
if err == nil {
return false
}
fmt.Fprintf(os.Stderr, "error: %s\n", err)
_ = Temp.ExecuteTemplate(w, "index", map[string]string{
"Error": fmt.Sprintf("%d: %s", code, HttpCodes[code]),
})
return true
}
type ImgIcon struct {
ImgPath string `storm:"id"`
ModTime string
PngData []byte
}
func customFileServer(root http.Dir) http.Handler {
iconFunc := func(w http.ResponseWriter, r *http.Request) {
qq := r.URL.Query()
var icon ImgIcon
if qq.Get("icon") == "" || qq.Get("mod") == "" {
FillError(w, fmt.Errorf("icon or mod not found"), 400)
return
}
err := DB.One("ImgPath", qq.Get("icon"), &icon)
if err != nil || icon.ModTime != qq.Get("mod") {
fn := DB.Update
if errors.Is(err, storm.ErrNotFound) {
fn = DB.Save
}
icon.ImgPath = qq.Get("icon")
icon.ModTime = qq.Get("mod")
var f http.File
f, err = root.Open(icon.ImgPath)
if FillError(w, err, 404) {
return
}
var fi fs.FileInfo
fi, err = f.Stat()
if err == nil && fi.IsDir() {
err = fmt.Errorf("icon is dir")
}
if FillError(w, err, 400) {
f.Close()
return
}
icon.PngData, err = imgconv.Media2Icon(icon.ImgPath, f)
f.Close()
if FillError(w, err, 404) {
return
}
err = fn(&icon)
if FillError(w, err, 500) {
return
}
}
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(200)
w.Write(icon.PngData)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Has("icon") {
iconFunc(w, r)
return
}
upath := r.URL.Path
f, err := root.Open(upath)
if FillError(w, err, 404) {
return
}
defer f.Close()
var fi fs.FileInfo
fi, err = f.Stat()
if FillError(w, err, 500) {
return
}
if fi.IsDir() {
var fi []fs.FileInfo
fi, err = f.Readdir(0)
if FillError(w, err, 500) {
return
}
t := TemplateData{Path: upath, Dirs: []DirEntry{}, Items: []DirEntry{}}
today := DateToInt(time.Now())
for _, d := range fi {
t.add(DirEntry{
Name: d.Name(),
Url: filepath.Join(upath, d.Name()),
mod: d.ModTime().Unix(),
Mod: FmtTime(d.ModTime(), today),
}, d.Size(), d.IsDir())
}
t.sortNewest()
Temp.ExecuteTemplate(w, "index", t)
return
}
http.FileServer(root).ServeHTTP(w, r)
})
}
var DB *storm.DB
func main() {
// Fs := os.DirFS(Config.HTTPDir)
http.Handle("/", customFileServer(http.Dir(config.HttpDir)))
// http.Handle("/", http.StripPrefix("/", FileServer(http.Dir(Config.HTTPDir))))
http.ListenAndServe(config.Addr+":"+config.Port, nil)
}
func init() {
Temp = template.Must(template.ParseFS(embedHtml, "html/*"))
config.Get()
db, err := storm.Open(config.DbPath, storm.BoltOptions(0600, &bbolt.Options{Timeout: 1 * time.Second}))
if err != nil {
fmt.Println("Failed to open database:", err)
os.Exit(1)
}
db.Init(&ImgIcon{})
DB = db
}