306 lines
8.0 KiB
Go
306 lines
8.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"dwelling-upload/internal/configuration"
|
|
"dwelling-upload/pkg/logging"
|
|
"dwelling-upload/pkg/server"
|
|
"dwelling-upload/pkg/utils"
|
|
"embed"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Joker/jade"
|
|
)
|
|
|
|
var compiledTemplates map[string]*template.Template
|
|
|
|
//go:embed web/assets
|
|
var assetsDir embed.FS
|
|
|
|
//go:embed web/templates
|
|
var templatesDir embed.FS
|
|
|
|
type NotFoundData struct {
|
|
MainSite string
|
|
}
|
|
|
|
type IndexData struct {
|
|
MainSite string
|
|
FileMaxSz string
|
|
StorageCapacity int64
|
|
StorageUsed int64
|
|
StorageAvailable int64
|
|
StorageCapStr string
|
|
StorageUsedStr string
|
|
StorageAvailStr string
|
|
KeepForHours int
|
|
}
|
|
|
|
type UploadedData struct {
|
|
MainSite string
|
|
DownloadURL string
|
|
KeepForHours int
|
|
}
|
|
|
|
type UploadHandlers struct {
|
|
conf *configuration.Configuration
|
|
logErr *logging.Logger
|
|
logUpload *logging.Logger
|
|
logDownload *logging.Logger
|
|
|
|
uploadDirSize *int64
|
|
}
|
|
|
|
func NewUploadHandlers(conf *configuration.Configuration, lErr, lUp, lDown *logging.Logger, uploadDirSize *int64) *UploadHandlers {
|
|
compileTemplates(lErr)
|
|
|
|
return &UploadHandlers{
|
|
conf: conf,
|
|
logErr: lErr,
|
|
logUpload: lUp,
|
|
logDownload: lDown,
|
|
uploadDirSize: uploadDirSize}
|
|
}
|
|
|
|
func (h *UploadHandlers) AssetsFS() http.FileSystem {
|
|
f, _ := fs.Sub(assetsDir, "web/assets")
|
|
return http.FS(f)
|
|
}
|
|
|
|
func (h *UploadHandlers) Index(w http.ResponseWriter, r *http.Request) {
|
|
var storCapacity int64 = h.conf.Uploads.Limits.Storage << 20
|
|
var fMaxSize int64 = h.conf.Uploads.Limits.FileSize << 20
|
|
|
|
_, _, capStr := utils.ConvertFileSize(storCapacity)
|
|
_, _, usedStr := utils.ConvertFileSize(*h.uploadDirSize)
|
|
_, _, availStr := utils.ConvertFileSize(storCapacity - *h.uploadDirSize)
|
|
_, _, fMaxSzStr := utils.ConvertFileSize(fMaxSize)
|
|
|
|
if err := compiledTemplates["index"].Execute(w, &IndexData{
|
|
MainSite: utils.MainSite(r.Host),
|
|
FileMaxSz: fMaxSzStr,
|
|
StorageCapacity: storCapacity,
|
|
StorageCapStr: capStr,
|
|
StorageAvailable: storCapacity - *h.uploadDirSize,
|
|
StorageAvailStr: availStr,
|
|
StorageUsed: *h.uploadDirSize,
|
|
StorageUsedStr: usedStr,
|
|
KeepForHours: h.conf.Uploads.Limits.KeepForHours,
|
|
}); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
h.logErr.Fatalln("failed to execute Index template:", err)
|
|
}
|
|
}
|
|
|
|
func (h *UploadHandlers) Upload(w http.ResponseWriter, r *http.Request) {
|
|
var fMaxSizeBytes int64 = h.conf.Uploads.Limits.FileSize << 20
|
|
var storCapacity int64 = h.conf.Uploads.Limits.Storage << 20
|
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, fMaxSizeBytes)
|
|
|
|
if err := r.ParseMultipartForm(fMaxSizeBytes); err != nil {
|
|
h.logErr.Println("failed to parse form:", err)
|
|
http.Error(w, "request too big", http.StatusExpectationFailed)
|
|
return
|
|
}
|
|
|
|
f, fHandler, err := r.FormFile("file")
|
|
if err != nil {
|
|
h.logErr.Println("failed to open incoming file:", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer func() {
|
|
os.Remove(fHandler.Filename)
|
|
f.Close()
|
|
}()
|
|
|
|
var leftSpace int64 = storCapacity - *h.uploadDirSize
|
|
|
|
if leftSpace < fHandler.Size {
|
|
h.logErr.Println("not enough space left in storage, only", leftSpace>>20, "MiB left")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
if err := compiledTemplates["uploaded"].Execute(w, &NotFoundData{
|
|
MainSite: utils.MainSite(r.Host),
|
|
}); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
h.logErr.Fatalln("failed to execute NoSpace template:", err)
|
|
}
|
|
}
|
|
|
|
s256 := sha256.New()
|
|
if _, err := io.Copy(s256, f); err != nil {
|
|
h.logErr.Println("failed to compute SHA-256 hash:", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
fSha256 := hex.EncodeToString(s256.Sum(nil))
|
|
s256.Write([]byte(h.conf.HashSalt))
|
|
fSaltedHash := base64.RawURLEncoding.EncodeToString(s256.Sum(nil))
|
|
|
|
f.Seek(0, io.SeekStart)
|
|
|
|
fPath := path.Join(h.conf.Uploads.Directory, fSaltedHash)
|
|
|
|
_, err = os.Stat(fPath)
|
|
if os.IsNotExist(err) {
|
|
fDst, err := os.Create(fPath)
|
|
if err != nil {
|
|
h.logErr.Println("failed to open file for writing", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer fDst.Close()
|
|
|
|
// We initialy set a dst file size to occupy space equal to uploaded's size.
|
|
// This is called a sparse file, if you need to know.
|
|
// It allows us to have a relatively small buffer size for inotify watcher.
|
|
// And it really affects that. I tested it.
|
|
fDst.Seek(fHandler.Size-1, io.SeekStart)
|
|
fDst.Write([]byte{0})
|
|
fDst.Seek(0, io.SeekStart)
|
|
|
|
_, err = io.Copy(fDst, f)
|
|
if err != nil {
|
|
h.logErr.Println("failed to copy uploaded file to destination:", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
netTyp, _ := utils.NetworkType(r.Host)
|
|
|
|
h.logUpload.Printf("| %s | %s | %s | SHA256 %s | %s | %d | %s", r.Header.Get("X-Real-IP"), netTyp,
|
|
fHandler.Filename, fSha256, fSaltedHash, fHandler.Size, r.UserAgent())
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
} else {
|
|
os.Chtimes(fPath, time.Now(), time.Now())
|
|
w.WriteHeader(http.StatusFound)
|
|
}
|
|
|
|
downloadURL := path.Join("/f", fSaltedHash, fHandler.Filename)
|
|
downloadURLParsed, _ := url.Parse(downloadURL)
|
|
|
|
if strings.Contains(r.UserAgent(), "curl") {
|
|
_, scheme := utils.NetworkType(r.Host)
|
|
downloadURL = fmt.Sprint(scheme, "://", r.Host, downloadURLParsed, "\r\n")
|
|
w.Write([]byte(downloadURL))
|
|
return
|
|
}
|
|
|
|
if err := compiledTemplates["uploaded"].Execute(w, &UploadedData{
|
|
MainSite: utils.MainSite(r.Host),
|
|
DownloadURL: downloadURLParsed.String(),
|
|
KeepForHours: h.conf.Uploads.Limits.KeepForHours,
|
|
}); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
h.logErr.Fatalln("failed to execute Uploaded template:", err)
|
|
}
|
|
}
|
|
|
|
func (h *UploadHandlers) Download(w http.ResponseWriter, r *http.Request) {
|
|
saltedHash := server.GetURLParam(r, "hash")
|
|
name := server.GetURLParam(r, "name")
|
|
|
|
path := path.Join(h.conf.Uploads.Directory, saltedHash)
|
|
|
|
stat, err := os.Stat(path)
|
|
if os.IsNotExist(err) {
|
|
h.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", name))
|
|
|
|
fd, err := os.Open(path)
|
|
if err != nil {
|
|
h.logErr.Println("failed to open file to read:", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer fd.Close()
|
|
|
|
netTyp, _ := utils.NetworkType(r.Host)
|
|
|
|
h.logDownload.Printf("| %s | %s | %s | %s | %s", r.Header.Get("X-Real-IP"), netTyp, name, saltedHash, r.UserAgent())
|
|
|
|
http.ServeContent(w, r, path, stat.ModTime(), fd)
|
|
}
|
|
|
|
func (h *UploadHandlers) NotFound(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
if err := compiledTemplates["404"].Execute(w, NotFoundData{
|
|
MainSite: utils.MainSite(r.Host),
|
|
}); err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
h.logErr.Fatalln("failed to execute 404 template:", err)
|
|
}
|
|
}
|
|
|
|
func compileTemplates(lErr *logging.Logger) {
|
|
compiledTemplates = make(map[string]*template.Template)
|
|
|
|
t, _ := fs.Sub(templatesDir, "web/templates")
|
|
templatesFS := http.FS(t)
|
|
|
|
indexStr, err := jade.ParseFileFromFileSystem("index.jade", templatesFS)
|
|
if err != nil {
|
|
lErr.Fatalln(err)
|
|
}
|
|
|
|
indexTpl, err := template.New("index").Parse(indexStr)
|
|
if err != nil {
|
|
lErr.Fatalln(err)
|
|
}
|
|
|
|
compiledTemplates["index"] = indexTpl
|
|
|
|
uploadedStr, err := jade.ParseFileFromFileSystem("uploaded.jade", templatesFS)
|
|
if err != nil {
|
|
lErr.Fatalln(err)
|
|
}
|
|
|
|
uploadedTpl, err := template.New("uploaded").Parse(uploadedStr)
|
|
if err != nil {
|
|
lErr.Fatalln(err)
|
|
}
|
|
|
|
compiledTemplates["uploaded"] = uploadedTpl
|
|
|
|
notfoundStr, err := jade.ParseFileFromFileSystem("404.jade", templatesFS)
|
|
if err != nil {
|
|
lErr.Fatalln(err)
|
|
}
|
|
|
|
notfoundTpl, err := template.New("404").Parse(notfoundStr)
|
|
if err != nil {
|
|
lErr.Fatalln(err)
|
|
}
|
|
|
|
compiledTemplates["404"] = notfoundTpl
|
|
|
|
nospaceStr, err := jade.ParseFileFromFileSystem("nospace.jade", templatesFS)
|
|
if err != nil {
|
|
lErr.Fatalln(err)
|
|
}
|
|
|
|
nospaceTpl, err := template.New("nospace").Parse(nospaceStr)
|
|
if err != nil {
|
|
lErr.Fatalln(err)
|
|
}
|
|
|
|
compiledTemplates["nospace"] = nospaceTpl
|
|
}
|