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" "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 } 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, }); 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 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() }() 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", r.RemoteAddr, netTyp, fHandler.Filename, fSha256, fSaltedHash, fHandler.Size) w.WriteHeader(http.StatusCreated) } else { os.Chtimes(fPath, time.Now(), time.Now()) w.WriteHeader(http.StatusFound) } downloadURL := path.Join("/f", fSaltedHash, fHandler.Filename) if strings.Contains(r.UserAgent(), "curl") { _, scheme := utils.NetworkType(r.Host) w.Write([]byte(path.Join(scheme, "://", r.Host, downloadURL) + "\r\n")) return } if err := compiledTemplates["uploaded"].Execute(w, &UploadedData{ MainSite: utils.MainSite(r.Host), DownloadURL: downloadURL, KeepForHours: h.conf.Uploads.Limits.KeepForHours, }); err != nil { w.WriteHeader(http.StatusInternalServerError) h.logErr.Fatalln("failed to execute Index 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", r.RemoteAddr, netTyp, name, saltedHash) 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 }