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 (*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["nospace"].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 } fHash := 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 } typ, _ := utils.NetworkType(r.Host) h.logUpload.Printf("| %s | %s | %s | SHA256 %s | %s | %d | %s", r.Header.Get("X-Real-IP"), typ, fHandler.Filename, fHash, 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") path := path.Join(h.conf.Uploads.Directory, saltedHash) stat, err := os.Stat(path) if os.IsNotExist(err) { h.NotFound(w, r) return } name := server.GetURLParam(r, "name") 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 }