package http import ( "crypto/sha256" "dwelling-upload/internal/configuration" "dwelling-upload/pkg/logging" "dwelling-upload/pkg/utils" "dwelling-upload/web" "encoding/base64" "encoding/hex" "fmt" "io" "net/http" "net/url" "os" "path" "strings" "time" ) type UploadHandlers struct { conf *configuration.Configuration logErr *logging.Logger logUpload *logging.Logger logDownload *logging.Logger logDelete *logging.Logger uploadDirSize *int64 } func NewUploadHandlers(conf *configuration.Configuration, lErr, lUp, lDown, lDel *logging.Logger, uploadDirSize *int64) *UploadHandlers { return &UploadHandlers{ conf: conf, logErr: lErr, logUpload: lUp, logDownload: lDown, logDelete: lDel, uploadDirSize: uploadDirSize} } func (*UploadHandlers) AssetsFS() http.FileSystem { return web.Assets() } 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) web.Index(utils.MainSite(r.Host), storCapacity, *h.uploadDirSize, h.conf.Uploads.Limits.KeepForHours, fMaxSzStr, usedStr, capStr, availStr, w) } 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, err.Error(), 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) http.Error(w, "cannot read incoming file", 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) web.ErrorNoSpace(utils.MainSite(r.Host), w) } s256 := sha256.New() if _, err := io.Copy(s256, f); err != nil { h.logErr.Println("failed to compute SHA-256 hash:", err) http.Error(w, "cannot compute hash for a file", 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) http.Error(w, "cannot create your file", 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) http.Error(w, "cannot copy file's content", 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) _, scheme := utils.NetworkType(r.Host) site := scheme + "://" + r.Host if strings.Contains(r.UserAgent(), "curl") { w.Write([]byte(site + downloadURLParsed.String() + "\r\n")) return } web.Uploaded(utils.MainSite(r.Host), site, downloadURLParsed.String(), h.conf.Uploads.Limits.KeepForHours, w) } func (h *UploadHandlers) Download(w http.ResponseWriter, r *http.Request) { saltedHash := GetURLParam(r, "hash") path := path.Join(h.conf.Uploads.Directory, saltedHash) stat, err := os.Stat(path) if os.IsNotExist(err) { NotFound(w, r) return } name := 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) Delete(w http.ResponseWriter, r *http.Request) { var saltedHash string if r.Method == "DELETE" { saltedHash = GetURLParam(r, "hash") } else { r.ParseForm() saltedHash = r.FormValue("hash") } path := path.Join(h.conf.Uploads.Directory, saltedHash) _, err := os.Stat(path) if os.IsNotExist(err) { NotFound(w, r) return } err = os.Remove(path) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, err) return } netTyp, _ := utils.NetworkType(r.Host) h.logDelete.Printf("| %s | %s | %s | %s", r.Header.Get("X-Real-IP"), netTyp, saltedHash, r.UserAgent()) w.WriteHeader(http.StatusNoContent) } func NotFound(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) if !strings.Contains(r.UserAgent(), "curl") { web.Error404(utils.MainSite(r.Host), w) } else { fmt.Fprintln(w, "deleted") } } func RobotsTxt(w http.ResponseWriter, r *http.Request) { data, _ := web.AssetsGetFile("robots.txt") w.Write(data) }