diff --git a/cmd/gochan/server.go b/cmd/gochan/server.go index c0cd8512..00a219e9 100755 --- a/cmd/gochan/server.go +++ b/cmd/gochan/server.go @@ -407,15 +407,35 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) { if fileOnly { fileName := post.Filename if fileName != "" && fileName != "deleted" { - if err = post.DeleteFiles(true); err != nil { - if wantsJSON { - serverutil.ServeJSON(writer, map[string]interface{}{ - "error": err, + var files []string + var errStr string + + if files, err = post.GetFilePaths(); err != nil { + errStr = gclog.Print(gclog.LErrorLog, "Error getting file upload info: ", err.Error()) + serverutil.ServeError(writer, errStr, wantsJSON, map[string]interface{}{ + "postid": post.ID, + }) + return + } + + if err = post.UnlinkUploads(true); err != nil { + gclog.Printf(gclog.LErrorLog, + "Error unlinking post uploads for #%d: %s", post.ID, err.Error()) + serverutil.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{ + "postid": post.ID, + }) + return + } + + for _, filePath := range files { + if err = os.Remove(filePath); err != nil { + fileBase := path.Base(filePath) + errStr = gclog.Printf(gclog.LErrorLog, "Error deleting %s: %s", fileBase, err.Error()) + serverutil.ServeError(writer, errStr, wantsJSON, map[string]interface{}{ "postid": post.ID, + "file": fileBase, }) - } else { - serverutil.ServeErrorPage(writer, gclog.Print(gclog.LErrorLog, - "Error deleting files from post: ", err.Error())) + return } } } diff --git a/pkg/gcsql/boards.go b/pkg/gcsql/boards.go index 8185d819..c68aca76 100644 --- a/pkg/gcsql/boards.go +++ b/pkg/gcsql/boards.go @@ -3,7 +3,6 @@ package gcsql import ( "database/sql" "net/http" - "os" "strconv" "github.com/gochan-org/gochan/pkg/gclog" @@ -82,6 +81,8 @@ func (board *Board) PopulateData(id int) error { return QueryRowSQL(sql, interfaceSlice(id), interfaceSlice(&board.ID, &board.Section, &board.Dir, &board.ListOrder, &board.Title, &board.Subtitle, &board.Description, &board.MaxFilesize, &board.DefaultStyle, &board.Locked, &board.CreatedOn, &board.Anonymous, &board.ForcedAnon, &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength, &board.EmbedsAllowed, &board.RedirectToThread, &board.RequireFile, &board.EnableCatalog)) } +// Delete deletes the board from the database (if a row with the struct's ID exists) and +// returns any errors. It does not remove the board directory or its files func (board *Board) Delete() error { exists := DoesBoardExistByID(board.ID) if !exists { @@ -92,13 +93,6 @@ func (board *Board) Delete() error { } const delSql = `DELETE FROM DBPREFIXboards WHERE id = ?` _, err := ExecSQL(delSql, board.ID) - if err != nil { - return err - } - absPath := board.AbsolutePath() - gclog.Printf(gclog.LStaffLog, - "Deleting board /%s/, absolute path: %s\n", board.Dir, absPath) - err = os.RemoveAll(absPath) return err } diff --git a/pkg/gcsql/posts.go b/pkg/gcsql/posts.go index c8dd2ac4..e294e0e5 100644 --- a/pkg/gcsql/posts.go +++ b/pkg/gcsql/posts.go @@ -1,8 +1,7 @@ package gcsql import ( - "fmt" - "os" + "database/sql" "path" "time" @@ -173,44 +172,47 @@ func GetPostsFromIP(ip string, limit int, onlyNotDeleted bool) ([]Post, error) { return posts, nil } -func (p *Post) DeleteFiles(leaveDeletedBox bool) error { - board, boardWasFound, err := GetBoardFromPostID(p.ID) +// GetFilePaths returns an array of absolute paths to uploaded files and thumbnails associated +// with this post, and any errors that occurred +func (p *Post) GetFilePaths() ([]string, error) { + boardDir, err := p.GetBoardDir() if err != nil { - return err - } - if !boardWasFound { - return fmt.Errorf("could not find board for post %v", p.ID) + return nil, err } const filenameSQL = `SELECT filename FROM DBPREFIXfiles WHERE post_id = ?` rows, err := QuerySQL(filenameSQL, p.ID) - if err != nil { - return err + var paths []string + if err == sql.ErrNoRows { + return paths, nil + } else if err != nil { + return nil, err } - var filenames []string + documentRoot := config.GetSystemCriticalConfig().DocumentRoot for rows.Next() { var filename string if err = rows.Scan(&filename); err != nil { - return err + return paths, err } - filenames = append(filenames, filename) - } - - systemCriticalCfg := config.GetSystemCriticalConfig() - //Remove files from disk - for _, filename := range filenames { _, filenameBase, fileExt := gcutil.GetFileParts(filename) thumbExt := fileExt if thumbExt == "gif" || thumbExt == "webm" || thumbExt == "mp4" { thumbExt = "jpg" } - uploadPath := path.Join(systemCriticalCfg.DocumentRoot, board, "/src/", filenameBase+"."+fileExt) - thumbPath := path.Join(systemCriticalCfg.DocumentRoot, board, "/thumb/", filenameBase+"t."+thumbExt) - catalogThumbPath := path.Join(systemCriticalCfg.DocumentRoot, board, "/thumb/", filenameBase+"c."+thumbExt) - os.Remove(uploadPath) - os.Remove(thumbPath) - os.Remove(catalogThumbPath) + paths = append(paths, + path.Join(documentRoot, boardDir, "/src/", filenameBase+"."+fileExt), + path.Join(documentRoot, boardDir, "/thumb/", filenameBase+"t."+thumbExt), // thumbnail path + ) + if p.ParentID == 0 { + paths = append(paths, path.Join(documentRoot, boardDir, "/thumb/", filenameBase+"c."+thumbExt)) // catalog thumbnail path + } } + return paths, nil +} +// UnlinkUploads disassociates the post with any uploads in DBPREFIXfiles +// that may have been uploaded with it, optionally leaving behind a "File Deleted" +// frame where the thumbnail appeared +func (p *Post) UnlinkUploads(leaveDeletedBox bool) error { var sqlStr string if leaveDeletedBox { // leave a "File Deleted" box @@ -218,6 +220,6 @@ func (p *Post) DeleteFiles(leaveDeletedBox bool) error { } else { sqlStr = `DELETE FROM DBPREFIXfiles WHERE post_id = ?` } - _, err = ExecSQL(sqlStr, p.ID) + _, err := ExecSQL(sqlStr, p.ID) return err } diff --git a/pkg/gcsql/staff.go b/pkg/gcsql/staff.go index 1dd7cf9e..62032bf3 100644 --- a/pkg/gcsql/staff.go +++ b/pkg/gcsql/staff.go @@ -257,14 +257,6 @@ func UpdatePost(postID int, email, subject string, message template.HTML, messag return err } -// DeleteFilesFromPost deletes all files belonging to a given post -func DeleteFilesFromPost(postID int, leaveDeletedBox bool) error { - post := &Post{ - ID: postID, - } - return post.DeleteFiles(leaveDeletedBox) -} - // DeletePost deletes a post with a given ID func DeletePost(postID int, checkIfTopPost bool) error { if checkIfTopPost { @@ -281,7 +273,6 @@ func DeletePost(postID int, checkIfTopPost bool) error { } } - DeleteFilesFromPost(postID, false) const sql = `UPDATE DBPREFIXposts SET is_deleted = TRUE, deleted_at = CURRENT_TIMESTAMP WHERE id = ?` _, err := ExecSQL(sql, postID) return err diff --git a/pkg/gcsql/tables.go b/pkg/gcsql/tables.go index f6613e9b..571d78a1 100644 --- a/pkg/gcsql/tables.go +++ b/pkg/gcsql/tables.go @@ -290,7 +290,7 @@ func (p *Post) GetBoardDir() (string, error) { SELECT board_id FROM DBPREFIXthreads WHERE id = ( SELECT thread_id FROM DBPREFIXposts WHERE id = ? LIMIT 1) LIMIT 1)` - err := QueryRowSQL(sql, []interface{}{p.ParentID}, interfaceSlice(&p.boardDir)) + err := QueryRowSQL(sql, []interface{}{p.ID}, interfaceSlice(&p.boardDir)) return p.boardDir, err } diff --git a/pkg/manage/actions.go b/pkg/manage/actions.go index d830a966..50a65737 100644 --- a/pkg/manage/actions.go +++ b/pkg/manage/actions.go @@ -8,6 +8,7 @@ import ( "html" "net" "net/http" + "os" "path" "regexp" "strconv" @@ -530,6 +531,13 @@ var actions = []Action{ Permissions: AdminPerms, JSONoutput: NoJSON, Callback: func(writer http.ResponseWriter, request *http.Request, wantsJSON bool) (output interface{}, err error) { + var currentUser string + currentUser, err = getCurrentStaff(request) + if err != nil { + return "", errors.New(gclog.Println(gclog.LErrorLog, + "Error parsing current user:", err.Error())) + } + pageBuffer := bytes.NewBufferString("") var board gcsql.Board requestType, boardID, err := boardsRequestType(request) @@ -550,6 +558,13 @@ var actions = []Action{ return "", err } err = board.Delete() + if err != nil { + return "", err + } + absPath := board.AbsolutePath() + gclog.Printf(gclog.LStaffLog, + "Board /%s/ deleted by %s, absolute path: %s\n", board.Dir, currentUser, absPath) + err = os.RemoveAll(absPath) case "edit": // edit button clicked, fill the input fields with board data to be edited board, err = gcsql.GetBoardFromID(boardID) diff --git a/pkg/serverutil/util.go b/pkg/serverutil/util.go index c79b0829..d56cc774 100644 --- a/pkg/serverutil/util.go +++ b/pkg/serverutil/util.go @@ -18,6 +18,20 @@ func ServeJSON(writer http.ResponseWriter, data map[string]interface{}) { MinifyWriter(writer, []byte(jsonStr), "application/json") } +// ServeError serves the given map as a JSON file (with the error string included) if wantsJSON is true, +// otherwise it serves a regular HTML error page +func ServeError(writer http.ResponseWriter, err string, wantsJSON bool, data map[string]interface{}) { + if wantsJSON { + servedMap := data + if _, ok := servedMap["error"]; !ok { + servedMap["error"] = err + } + ServeJSON(writer, servedMap) + } else { + ServeErrorPage(writer, err) + } +} + // ServeErrorPage shows a general error page if something goes wrong func ServeErrorPage(writer http.ResponseWriter, err string) { writer.Header().Set("Content-Type", "text/html; charset=utf-8")