Compare commits

...

12 Commits

Author SHA1 Message Date
Alborz Jafari d99c819049
Merge 7ee1aec3b7 into 41e1989345 2024-04-09 14:39:44 +03:00
Matt Baer 41e1989345
Merge pull request #982 from writefreely/dependabot/go_modules/golang.org/x/net-0.22.0
Bump golang.org/x/net from 0.20.0 to 0.22.0
2024-04-03 14:25:17 -04:00
Matt Baer 34d902062f
Merge pull request #927 from writefreely/dependabot/go_modules/github.com/stretchr/testify-1.9.0
Bump github.com/stretchr/testify from 1.8.4 to 1.9.0
2024-04-03 14:23:13 -04:00
dependabot[bot] ed9ff51b68
Bump golang.org/x/net from 0.20.0 to 0.22.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.20.0 to 0.22.0.
- [Commits](https://github.com/golang/net/compare/v0.20.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-01 22:46:41 +00:00
dependabot[bot] 3dd0a9b8dc
Bump github.com/stretchr/testify from 1.8.4 to 1.9.0
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 22:40:53 +00:00
Alborz Jafari 7ee1aec3b7 Delete media files/directories along with corresponding posts/users 2023-08-31 22:12:06 +03:00
Alborz Jafari 6fbb51eb63 Add slug to drafts because uploading media needs slug 2023-08-30 22:07:35 +03:00
Alborz Jafari 7b537e3d4e Iterate over uploaded files in edit mode 2023-08-30 19:49:27 +03:00
Alborz Jafari eac4cbf390 Fix uploading file in draft mode with slug ID available 2023-08-30 18:37:07 +03:00
Alborz Jafari 9f8a1730e6 Add config for space limiting 2023-08-10 19:54:08 +03:00
Alborz Jafari ccde5ce6c9 Add space limit for uploading media files 2023-08-06 13:44:30 +03:00
Alborz Jafari 8a3daf7343 Add file uploading and file sharing functionality 2023-07-29 23:54:39 +03:00
11 changed files with 400 additions and 14 deletions

View File

@ -12,6 +12,7 @@ package writefreely
import (
"database/sql"
"path/filepath"
"fmt"
"html/template"
"net/http"
@ -19,6 +20,7 @@ import (
"strconv"
"strings"
"time"
"os"
"github.com/gorilla/mux"
"github.com/writeas/impart"
@ -345,10 +347,20 @@ func handleAdminDeleteUser(app *App, u *User, w http.ResponseWriter, r *http.Req
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not delete user account for '%s': %v", username, err)}
}
deleteMediaFilesOfUser(app, username)
_ = addSessionFlash(app, w, r, fmt.Sprintf("User \"%s\" was deleted successfully.", username), nil)
return impart.HTTPError{http.StatusFound, "/admin/users"}
}
func deleteMediaFilesOfUser(app *App, username string) {
mediaDirectoryPath := filepath.Join(app.cfg.Server.MediaParentDir, mediaDir,
username)
err := os.RemoveAll(mediaDirectoryPath)
if err != nil {
log.Error("Deleting media directory of %s failed: %v", username, err)
}
}
func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
username := vars["username"]

1
app.go
View File

@ -47,6 +47,7 @@ import (
const (
staticDir = "static"
mediaDir = "media"
assumedTitleLen = 80
postsPerPage = 10

View File

@ -43,6 +43,7 @@ type (
TemplatesParentDir string `ini:"templates_parent_dir"`
StaticParentDir string `ini:"static_parent_dir"`
MediaParentDir string `ini:"media_parent_dir"`
PagesParentDir string `ini:"pages_parent_dir"`
KeysParentDir string `ini:"keys_parent_dir"`
@ -168,6 +169,10 @@ type (
// Disable password authentication if use only Oauth
DisablePasswordAuth bool `ini:"disable_password_auth"`
AllowUploadMedia bool `ini:"allow_upload_media"`
MediaMaxSize int64 `ini:"media_max_size"`
TotalMediaSpace int64 `ini:"total_media_size"`
}
EmailCfg struct {
@ -203,8 +208,11 @@ func New() *Config {
SingleUser: true,
MinUsernameLen: 3,
MaxBlogs: 1,
MediaMaxSize: 10,
TotalMediaSpace: 100,
Federation: true,
PublicStats: true,
AllowUploadMedia: false,
},
}
c.UseMySQL(true)

View File

@ -670,7 +670,12 @@ func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Pos
ownerCollID := sql.NullInt64{
Valid: false,
}
slug := sql.NullString{"", false}
slugString := *post.Slug
slugValid := true
if slugString == "" {
slugValid = false
}
slug := sql.NullString{slugString, slugValid}
// If an alias was supplied, we'll add this to the collection as well.
if userID > 0 {

View File

@ -43,6 +43,7 @@ var (
ErrCollectionGone = impart.HTTPError{http.StatusGone, "This blog was unpublished."}
ErrCollectionPageNotFound = impart.HTTPError{http.StatusNotFound, "Collection page doesn't exist."}
ErrPostNotFound = impart.HTTPError{Status: http.StatusNotFound, Message: "Post not found."}
ErrFileNotFound = impart.HTTPError{Status: http.StatusNotFound, Message: "File not found."}
ErrPostBanned = impart.HTTPError{Status: http.StatusGone, Message: "Post removed."}
ErrPostUnpublished = impart.HTTPError{Status: http.StatusGone, Message: "Post unpublished by author."}
ErrPostFetchError = impart.HTTPError{Status: http.StatusInternalServerError, Message: "We encountered an error getting the post. The humans have been alerted."}

8
go.mod
View File

@ -34,7 +34,7 @@ require (
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.27.1
github.com/writeas/activity v0.1.2
github.com/writeas/activityserve v0.0.0-20230428180247-dc13a4f4d835
@ -49,8 +49,8 @@ require (
github.com/writeas/web-core v1.6.1-0.20231003013047-d81124d45431
github.com/writefreely/go-gopher v0.0.0-20220429181814-40127126f83b
github.com/writefreely/go-nodeinfo v1.2.0
golang.org/x/crypto v0.18.0
golang.org/x/net v0.20.0
golang.org/x/crypto v0.21.0
golang.org/x/net v0.22.0
)
require (
@ -83,7 +83,7 @@ require (
github.com/writeas/go-writeas/v2 v2.0.2 // indirect
github.com/writeas/openssl-go v1.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

16
go.sum
View File

@ -166,8 +166,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/writeas/activity v0.1.2 h1:Y12B5lIrabfqKE7e7HFCWiXrlfXljr9tlkFm2mp7DgY=
@ -213,8 +213,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@ -233,8 +233,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -261,8 +261,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

246
pad.go
View File

@ -11,9 +11,18 @@
package writefreely
import (
"os"
"io"
"fmt"
"strconv"
"io/ioutil"
"path/filepath"
"encoding/json"
"net/http"
"strings"
uuid "github.com/nu7hatch/gouuid"
"github.com/gorilla/mux"
"github.com/writeas/impart"
"github.com/writeas/web-core/log"
@ -120,6 +129,89 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
return nil
}
func okToEdit(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
action := vars["action"]
slug := vars["slug"]
collAlias := vars["collection"]
appData := &struct {
page.StaticPage
Post *RawPost
User *User
EditCollection *Collection // Collection of the post we're editing, if any
Flashes []string
NeedsToken bool
Silenced bool
}{
StaticPage: pageForReq(app, r),
Post: &RawPost{Font: "norm"},
User: getUserSession(app, r),
}
var err error
appData.Silenced, err = app.db.IsUserSilenced(appData.User.ID)
if err != nil {
log.Error("view meta: get user status: %v", err)
return ErrInternalGeneral
}
if action == "" && slug == "" {
return ErrPostNotFound
}
// Make sure this isn't cached, so user doesn't accidentally lose data
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Expires", "Thu, 28 Jul 1989 12:00:00 GMT")
if slug != "" {
appData.Post = getRawCollectionPost(app, slug, collAlias)
if appData.Post.OwnerID != appData.User.ID {
// TODO: add ErrForbiddenEditPost message to flashes
return impart.HTTPError{
http.StatusFound, r.URL.Path[:strings.LastIndex(r.URL.Path, "/meta")]}
}
if app.cfg.App.SingleUser {
// TODO: optimize this query just like we do in GetCollectionForPad (?)
appData.EditCollection, err = app.db.GetCollectionByID(1)
} else {
appData.EditCollection, err = app.db.GetCollectionForPad(collAlias)
}
if err != nil {
return err
}
appData.EditCollection.hostName = app.cfg.App.Host
} else {
// Editing a floating article
appData.Post = getRawPost(app, action)
appData.Post.Id = action
}
appData.NeedsToken = appData.User == nil || appData.User.ID != appData.Post.OwnerID
if appData.Post.Gone {
return ErrPostUnpublished
} else if appData.Post.Found && appData.Post.Content != "" {
// Got the post
} else if appData.Post.Found {
return ErrPostFetchError
} else {
return ErrPostNotFound
}
return nil
}
func handleDeleteFile(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
fileName := vars["filename"]
slug := vars["slug"]
user := getUserSession(app, r)
filePath := filepath.Join(app.cfg.Server.MediaParentDir, mediaDir, user.Username, slug) + "/" + fileName
err := os.Remove(filePath)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return ErrFileNotFound
}
w.WriteHeader(http.StatusOK)
return nil
}
func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
action := vars["action"]
@ -185,9 +277,163 @@ func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error {
return ErrPostNotFound
}
appData.Flashes, _ = getSessionFlashes(app, w, r, nil)
user := getUserSession(app, r)
if slug == "" {
slug, _ = getSlugFromActionId(app, action)
}
mediaDirectoryPath := filepath.Join(app.cfg.Server.MediaParentDir, mediaDir,
user.Username, slug)
appData.Post.MediaFilesList, _ = getFilesListInPath(mediaDirectoryPath)
if err = templates["edit-meta"].ExecuteTemplate(w, "edit-meta", appData); err != nil {
log.Error("Unable to execute template: %v", err)
}
return nil
}
func getNewFileName(path string, originalFieName string) (string, error) {
u, err := uuid.NewV4()
if err != nil {
log.Error("Unable to generate uuid: %v", err)
return "", err
}
extension := filepath.Ext(originalFieName)
return u.String() + extension, nil
}
func getFilesListInPath(path string) ([]string, error) {
var files []string
entries, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
for _, entry := range entries {
if !entry.IsDir() {
files = append(files, "/" + path + "/" + entry.Name())
}
}
return files, nil
}
func handleGetFile(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
slug := vars["slug"]
author := vars["author"]
filename := vars["filename"]
mediaDirectoryPath := filepath.Join(app.cfg.Server.MediaParentDir, mediaDir,
author, slug, filename)
filePath := mediaDirectoryPath
file, err := http.Dir("").Open(filePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return nil
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
http.Error(w, "Failed to get file information", http.StatusInternalServerError)
return nil
}
w.Header().Set("Content-Disposition", "attachment; filename="+fileInfo.Name())
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file)
return nil
}
func calculateDirectoryTotalSize(dirPath string) (int64, error) {
var totalSize int64
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
totalSize += info.Size()
}
return nil
})
return totalSize, err
}
func handleUploadMedia(app *App, w http.ResponseWriter, r *http.Request) error {
maxUploadSize := app.cfg.App.MediaMaxSize * 1024 * 1024
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
errMsg := fmt.Sprintf("File size limit exceeded. The limit is: %d MB",
app.cfg.App.MediaMaxSize)
http.Error(w, errMsg, http.StatusBadRequest)
return nil
}
fileSize := r.ContentLength
if err := okToEdit(app, w, r); err != nil {
return err
}
vars := mux.Vars(r)
slug := vars["slug"]
if slug == "" {
actionId := vars["action"]
if actionId == "" {
return ErrPostNotFound
}
var err error
slug, err = getSlugFromActionId(app, actionId)
if slug == "" || err != nil {
return ErrPostNotFound
}
}
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return nil
}
file, handler, err := r.FormFile("file")
if err != nil {
http.Error(w, "Error retrieving the file", http.StatusInternalServerError)
return nil
}
defer file.Close()
user := getUserSession(app, r)
mediaDirectoryPath := filepath.Join(app.cfg.Server.MediaParentDir, mediaDir,
user.Username, slug)
err = os.MkdirAll(mediaDirectoryPath, 0755)
if err != nil {
return err
}
totalSize, err := calculateDirectoryTotalSize(mediaDirectoryPath)
totalMediaSpace := app.cfg.App.TotalMediaSpace * 1024 * 1024
if totalSize + fileSize > totalMediaSpace {
errMsg := fmt.Sprintf("Your upload space limit has been exceeded. Your limit is: %d MB",
app.cfg.App.TotalMediaSpace)
http.Error(w, errMsg, http.StatusBadRequest)
return nil
}
newFileName, _ := getNewFileName(mediaDirectoryPath, handler.Filename)
newFilePath := filepath.Join(mediaDirectoryPath, newFileName)
dst, err := os.Create(newFilePath)
if err != nil {
http.Error(w, "Error saving the file", http.StatusInternalServerError)
return nil
}
defer dst.Close()
_, err = io.Copy(dst, file)
if err != nil {
http.Error(w, "Error copying the file", http.StatusInternalServerError)
return nil
}
response := map[string]string{
"message": "File uploaded successfully!",
"path": newFilePath,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
return nil
}

View File

@ -13,6 +13,8 @@ package writefreely
import (
"database/sql"
"encoding/json"
"path/filepath"
"errors"
"fmt"
"github.com/writefreely/writefreely/spam"
"html/template"
@ -21,6 +23,7 @@ import (
"regexp"
"strings"
"time"
"os"
"github.com/gorilla/mux"
"github.com/guregu/null"
@ -163,6 +166,7 @@ type (
Language sql.NullString
OwnerID int64
CollectionID sql.NullInt64
MediaFilesList []string
Found bool
Gone bool
@ -650,6 +654,8 @@ func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
}
collID = coll.ID
}
slug := getSlugFromPost(*p.Title, *p.Content, p.Language.String)
p.Slug = &slug
// TODO: return PublicPost from createPost
newPost.Post, err = app.db.CreatePost(userID, collID, p)
}
@ -821,6 +827,13 @@ func deletePost(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
friendlyID := vars["post"]
editToken := r.FormValue("token")
var err error
user := getUserSession(app, r)
slug, err := getSlugFromActionId(app, friendlyID)
if err == nil {
deleteMediaFilesOfPost(app, user.Username, slug)
}
var ownerID int64
var u *User
@ -834,7 +847,6 @@ func deletePost(app *App, w http.ResponseWriter, r *http.Request) error {
var res sql.Result
var t *sql.Tx
var err error
var collID sql.NullInt64
var coll *Collection
var pp *PublicPost
@ -933,6 +945,15 @@ func deletePost(app *App, w http.ResponseWriter, r *http.Request) error {
return impart.HTTPError{Status: http.StatusNoContent}
}
func deleteMediaFilesOfPost(app *App, username, slug string) {
mediaDirectoryPath := filepath.Join(app.cfg.Server.MediaParentDir, mediaDir,
username, slug)
err := os.RemoveAll(mediaDirectoryPath)
if err != nil {
log.Error("Deleting media directory of %s failed: %v", username, err)
}
}
// addPost associates a post with the authenticated user.
func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
var ownerID int64
@ -1325,6 +1346,18 @@ func (p *SubmittedPost) isFontValid() bool {
return valid
}
func getSlugFromActionId(app *App, actionID string) (string, error) {
var slug string
err := app.db.QueryRow("SELECT slug FROM posts WHERE id = ?", actionID).Scan(&slug)
switch {
case err == sql.ErrNoRows:
return "", errors.New("Post not found")
case err != nil:
return "", errors.New("Unable to fetch post")
}
return slug, nil
}
func getRawPost(app *App, friendlyID string) *RawPost {
var content, font, title string
var isRTL sql.NullBool

View File

@ -113,6 +113,10 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
me.HandleFunc("/invites", handler.User(handleViewUserInvites)).Methods("GET")
me.HandleFunc("/logout", handler.Web(viewLogout, UserLevelNone)).Methods("GET")
if apper.App().cfg.App.AllowUploadMedia {
write.HandleFunc("/media/{author}/{slug}/{filename}", handler.All(handleGetFile)).Methods("GET")
}
write.HandleFunc("/api/me", handler.All(viewMeAPI)).Methods("GET")
apiMe := write.PathPrefix("/api/me/").Subrouter()
apiMe.HandleFunc("/", handler.All(viewMeAPI)).Methods("GET")
@ -203,6 +207,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
// All the existing stuff
write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelUser)).Methods("GET")
write.HandleFunc(draftEditPrefix+"/{action}/meta/mediafile/", handler.Web(handleUploadMedia, UserLevelUser)).Methods("POST")
// Collections
if apper.App().cfg.App.SingleUser {
RouteCollections(handler, write.PathPrefix("/").Subrouter())
@ -233,6 +238,8 @@ func RouteCollections(handler *Handler, r *mux.Router) {
r.HandleFunc("/{slug}", handler.CollectionPostOrStatic)
r.HandleFunc("/{slug}/edit", handler.Web(handleViewPad, UserLevelUser))
r.HandleFunc("/{slug}/edit/meta", handler.Web(handleViewMeta, UserLevelUser))
r.HandleFunc("/{slug}/edit/meta/mediafile/{filename}", handler.Web(handleDeleteFile, UserLevelUser)).Methods("DELETE")
r.HandleFunc("/{slug}/edit/meta/mediafile/", handler.Web(handleUploadMedia, UserLevelUser)).Methods("POST")
r.HandleFunc("/{slug}/", handler.Web(handleCollectionPostRedirect, UserLevelReader)).Methods("GET")
}

View File

@ -265,10 +265,83 @@
</dl>
<input type="hidden" name="web" value="true" />
</form>
<dt><label>Media Files List</label></dt></br>
<ul id="mediaFilesList"></ul>
<form id="uploadForm">
<input type="file" id="fileInput" />
<button type="button" onclick="uploadFile()">Upload</button>
</form>
</div>
<script src="/js/h.js"></script>
<script>
function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('Please select a file to upload.');
return;
}
const formData = new FormData();
formData.append('file', file);
const url = "meta/mediafile/"
fetch(url, {
method: 'POST',
body: formData,
}).then(response => response.json())
.then(data => {
alert('File uploaded successfully, path: /' + data.path);
location.reload();
}).catch(error => {
alert('An error occurred during file upload.');
});
}
</script>
<script>
function getFileNameFromPath(filePath) {
const parts = filePath.split("/");
return parts.pop();
}
function postDeleteMediafile(fileName) {
const url = "meta/mediafile/" + fileName;
fetch(url, {
method: 'DELETE',
headers: {},
})
.then(response => {
if (!response.ok) {
console.error(response);
} else {
location.reload();
}
});
}
const valueListElement = document.getElementById("mediaFilesList");
{{.Post.MediaFilesList}}.forEach((value) => {
const listItemElement = document.createElement("li");
listItemElement.textContent = value;
valueListElement.appendChild(listItemElement);
const buttonElement = document.createElement("button");
buttonElement.textContent = "Remove";
buttonElement.addEventListener("click", () => {
const confirmResult = confirm("Are you sure to remove: " + value + "?");
if (confirmResult) {
fileName = getFileNameFromPath(value)
postDeleteMediafile(fileName);
} else {}
});
listItemElement.appendChild(buttonElement);
});
</script>
<script>
function updateMeta() {
if ({{.Silenced}}) {
alert("Your account is silenced, so you can't edit posts.");