From df86d5836a284741aa269d4bff7cf17778381afe Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Fri, 22 Nov 2019 18:41:54 +0100 Subject: [PATCH] Implement file deletion --- .../html/deletePasteSuccess.html.tmpl | 3 + .../templates/txt/deletePasteSuccess.txt.tmpl | 1 + .../txt/newFileUploadPasteSuccess.txt.tmpl | 3 +- .../txt/newRedirectPasteSuccess.txt.tmpl | 3 +- fileupload.go | 20 ++++-- handlers.go | 70 +++++++++---------- paste.go | 30 ++++++-- 7 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 assets/templates/html/deletePasteSuccess.html.tmpl create mode 100644 assets/templates/txt/deletePasteSuccess.txt.tmpl diff --git a/assets/templates/html/deletePasteSuccess.html.tmpl b/assets/templates/html/deletePasteSuccess.html.tmpl new file mode 100644 index 0000000..334f2cb --- /dev/null +++ b/assets/templates/html/deletePasteSuccess.html.tmpl @@ -0,0 +1,3 @@ +{{define "title"}} +Success - rushlink +{{end}} \ No newline at end of file diff --git a/assets/templates/txt/deletePasteSuccess.txt.tmpl b/assets/templates/txt/deletePasteSuccess.txt.tmpl new file mode 100644 index 0000000..0ffae3c --- /dev/null +++ b/assets/templates/txt/deletePasteSuccess.txt.tmpl @@ -0,0 +1 @@ +<{{.Request.Host}}/{{.Paste.Key}}> was succesfully deleted diff --git a/assets/templates/txt/newFileUploadPasteSuccess.txt.tmpl b/assets/templates/txt/newFileUploadPasteSuccess.txt.tmpl index bbceb50..dba998e 100644 --- a/assets/templates/txt/newFileUploadPasteSuccess.txt.tmpl +++ b/assets/templates/txt/newFileUploadPasteSuccess.txt.tmpl @@ -1,4 +1,5 @@ -https://{{.Request.Host}}/{{.Paste.Key}} {{if .Request.PostForm.deleteToken -}} https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}} +{{else -}} +https://{{.Request.Host}}/{{.Paste.Key}} {{end -}} diff --git a/assets/templates/txt/newRedirectPasteSuccess.txt.tmpl b/assets/templates/txt/newRedirectPasteSuccess.txt.tmpl index bbceb50..dba998e 100644 --- a/assets/templates/txt/newRedirectPasteSuccess.txt.tmpl +++ b/assets/templates/txt/newRedirectPasteSuccess.txt.tmpl @@ -1,4 +1,5 @@ -https://{{.Request.Host}}/{{.Paste.Key}} {{if .Request.PostForm.deleteToken -}} https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}} +{{else -}} +https://{{.Request.Host}}/{{.Paste.Key}} {{end -}} diff --git a/fileupload.go b/fileupload.go index bcef1e8..2925065 100644 --- a/fileupload.go +++ b/fileupload.go @@ -79,7 +79,7 @@ func (t fileUploadState) String() string { func OpenFileStore(path string) error { if path == "" { - return errors.New("file store path not set") + return errors.New("file-store not set") } // Try to create the file store directory if it does not yet exist @@ -158,11 +158,23 @@ func (fu *fileUpload) save(tx *bolt.Tx) error { } func (fu *fileUpload) delete(tx *bolt.Tx) error { - // Replace the old paste with a new empty paste - return (&fileUpload{ + // Remove the file in the backend + filePath := fileStorePath(fu.ID, fu.FileName) + if err := os.Remove(filePath); err != nil { + return err + } + + // Update the file in the server + if err := (&fileUpload{ ID: fu.ID, State: fileUploadStateDeleted, - }).save(tx) + }).save(tx); err != nil { + return err + } + + // Cleanup the parent directory + wrap := "deletion succeeded, but removing the file directory has failed" + return errors.Wrap(os.Remove(path.Dir(filePath)), wrap) } func (fu *fileUpload) url() *url.URL { diff --git a/handlers.go b/handlers.go index 6de1de3..ca8075c 100644 --- a/handlers.go +++ b/handlers.go @@ -173,47 +173,34 @@ func viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPas } func newPasteHandler(w http.ResponseWriter, r *http.Request) { - newPasteHandlerMultipart(w, r, func(w http.ResponseWriter, r *http.Request) { - newPasteHandlerURLEncoded(w, r, func(w http.ResponseWriter, r *http.Request) { - renderError(w, r, http.StatusBadRequest, "no form data in request\n") - }) - }) -} - -// Try to parse the new-paste request as multi-part form. -// -// If there is no multi-part form in the request, this handler will call next. -func newPasteHandlerMultipart(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - reader, err := r.MultipartReader() - if err != nil { - next(w, r) + file, fileHeader, err := r.FormFile("file") + if err == nil { + newFileUploadPasteHandler(w, r, file, *fileHeader) return - } - - part, err := reader.NextPart() - if err != nil { - if err == io.EOF { - renderError(w, r, http.StatusBadRequest, "multipart form is empty\n") - return - } - panic(errors.Wrap(err, "multipart.Reader.NextPart")) - } - if part.FormName() != "file" { - msg := fmt.Sprintf("invalid multipart form name: %v", part.FormName()) + } else if err == http.ErrMissingFile { + // Fallthrough + } else { + msg := fmt.Sprintf("could not parse form: %v\n", err) renderError(w, r, http.StatusBadRequest, msg) return } - newFileUploadPasteHandler(w, r, *part) + shorten := r.FormValue("shorten") + if shorten != "" { + newRedirectPasteHandler(w, r, shorten) + return + } + + renderError(w, r, http.StatusBadRequest, "no 'file' and no 'shorten' fields given in form\n") } -func newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, part multipart.Part) { +func newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) { var fu *fileUpload var paste *paste if err := DB.Update(func(tx *bolt.Tx) error { var err error // Create the fileUpload in the database - fu, err = newFileUpload(tx, &part, part.FileName(), part.Header.Get("Content-Type")) + fu, err = newFileUpload(tx, file, header.Filename, header.Header.Get("Content-Type")) if err != nil { panic(errors.Wrap(err, "creating fileUpload")) } @@ -240,8 +227,7 @@ func newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Request, next http newRedirectPasteHandler(w, r, shorten) } -func newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, shorten string) { - rawurl := r.PostForm.Get("shorten") +func newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, rawurl string) { userURL, err := url.ParseRequestURI(rawurl) if err != nil { msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl) @@ -259,7 +245,6 @@ func newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, shorten str var paste *paste if err := DB.Update(func(tx *bolt.Tx) error { - // Generate a new delete token for this paste var err error paste, err = shortenURL(tx, userURL) return err @@ -282,22 +267,35 @@ func deletePasteHandler(w http.ResponseWriter, r *http.Request) { } var errorCode int + var paste paste if err := DB.Update(func(tx *bolt.Tx) error { p, err := getPaste(tx, key) if err != nil { errorCode = http.StatusNotFound return err } - if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 { - p.delete(tx) + if p.State == pasteStateDeleted { + errorCode = http.StatusGone + return errors.New("already deleted") } - errorCode = http.StatusForbidden - return errors.New("invalid delete token") + if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 0 { + errorCode = http.StatusForbidden + return errors.New("invalid delete token") + } + if err := p.delete(tx); err != nil { + errorCode = http.StatusInternalServerError + return err + } + paste = *p + return nil }); err != nil { log.Printf("error: %v\n", err) renderError(w, r, errorCode, fmt.Sprintf("error: %v\n", err)) return } + + data := map[string]interface{}{"Paste": paste} + render(w, r, "deletePasteSuccess", data) } // Add a new fileUpload redirect to the database diff --git a/paste.go b/paste.go index 28c6fef..7455cbe 100644 --- a/paste.go +++ b/paste.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/google/uuid" "github.com/pkg/errors" bolt "go.etcd.io/bbolt" ) @@ -44,6 +45,8 @@ func (t pasteType) String() string { return "paste" case pasteTypeRedirect: return "redirect" + case pasteTypeFileUpload: + return "file" default: return "invalid" } @@ -94,12 +97,29 @@ func (p *paste) save(tx *bolt.Tx) error { } func (p *paste) delete(tx *bolt.Tx) error { + // Remove the (maybe) attached file + if p.Type == pasteTypeFileUpload { + fuID, err := uuid.FromBytes(p.Content) + if err != nil { + return errors.Wrap(err, "failed to parse uuid") + } + fu, err := getFileUpload(tx, fuID) + if err != nil { + return errors.Wrap(err, "failed to find file in database") + } + if err := fu.delete(tx); err != nil { + return errors.Wrap(err, "failed to remove file") + } + } + // Replace the old paste with a new empty paste - return (&paste{ - Key: p.Key, - State: pasteStateDeleted, - DeleteToken: p.DeleteToken, - }).save(tx) + p.Type = pasteTypeUndef + p.State = pasteStateDeleted + p.Content = []byte{} + if err := p.save(tx); err != nil { + return errors.Wrap(err, "failed to delete paste in database") + } + return nil } // Get the URL from this paste.