git.sophuwu.com > cdn   
              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
}