diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index c1fd52e8..f979aec3 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -7,11 +7,13 @@ import ( "io" "log" "net/http" + "path" "time" "github.com/go-chi/chi" "github.com/go-chi/render" "github.com/umputun/remark/backend/app/rest/auth" + "github.com/umputun/remark/backend/app/rest/proxy" "github.com/umputun/remark/backend/app/migrator" "github.com/umputun/remark/backend/app/rest" @@ -27,6 +29,7 @@ type admin struct { cache cache.LoadingCache authenticator auth.Authenticator readOnlyAge int + avatarProxy *proxy.Avatar } func (a *admin) routes(middlewares ...func(http.Handler) http.Handler) chi.Router { @@ -118,6 +121,14 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user") return } + + if claims.User.Picture != "" { + if err := a.avatarProxy.Store.Remove(path.Base(claims.User.Picture)); err != nil { + rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user's avatar") + return + } + } + a.cache.Flush(cache.Flusher(claims.SiteID).Scopes(claims.SiteID, claims.User.ID, "last")) render.Status(r, http.StatusOK) render.JSON(w, r, JSON{"user_id": claims.User.ID, "site_id": claims.SiteID}) diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index 8d27e2a5..8b6bd5a1 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -115,6 +115,7 @@ func (s *Rest) routes() chi.Router { cache: s.Cache, authenticator: s.Authenticator, readOnlyAge: s.ReadOnlyAge, + avatarProxy: s.AvatarProxy, } ipFn := func(ip string) string { return store.HashValue(ip, s.DataService.Secret)[:12] } // logger uses it for anonymization diff --git a/backend/app/store/avatar/gridfs.go b/backend/app/store/avatar/gridfs.go index 33106f5d..f6103f1c 100644 --- a/backend/app/store/avatar/gridfs.go +++ b/backend/app/store/avatar/gridfs.go @@ -73,7 +73,7 @@ func (gf *GridFS) ID(avatar string) (id string) { return errors.Wrapf(e, "can't open avatar %s", avatar) } id = fh.MD5() - return nil + return errors.Wrapf(fh.Close(), "can't close avatar") }) if err != nil { log.Printf("[DEBUG] can't get file info '%s', %s", avatar, err) @@ -81,3 +81,15 @@ func (gf *GridFS) ID(avatar string) (id string) { } return id } + +// Remove avatar from gridfs +func (gf *GridFS) Remove(avatar string) error { + return gf.Connection.WithDB(func(dbase *mgo.Database) error { + fh, e := dbase.GridFS("fs").Open(avatar) + if e != nil { + return errors.Wrapf(e, "can't get avatar %s", avatar) + } + _ = fh.Close() + return dbase.GridFS("fs").Remove(avatar) + }) +} diff --git a/backend/app/store/avatar/gridfs_test.go b/backend/app/store/avatar/gridfs_test.go index bcc6baad..0ba335f0 100644 --- a/backend/app/store/avatar/gridfs_test.go +++ b/backend/app/store/avatar/gridfs_test.go @@ -34,6 +34,21 @@ func TestGridFS_PutAndGet(t *testing.T) { assert.Equal(t, "70c881d4a26984ddce795f6f71817c9cf4480e79", p.ID("aaaa"), "no data, encode avatar id") } +func TestGridFS_Remove(t *testing.T) { + p, skip := prepGFStore(t) + if skip { + return + } + + assert.NotNil(t, p.Remove("no-such-thing.image")) + + avatar, err := p.Put("user1", strings.NewReader("some picture bin data")) + require.Nil(t, err) + assert.Equal(t, "b3daa77b4c04a9551b8781d03191fe098f325e67.image", avatar) + assert.NoError(t, p.Remove("b3daa77b4c04a9551b8781d03191fe098f325e67.image"), "remove real one") + assert.NotNil(t, p.Remove("b3daa77b4c04a9551b8781d03191fe098f325e67.image"), "already removed") +} + func prepGFStore(t *testing.T) (Store, bool) { conn, err := mongo.MakeTestConnection(t) if err != nil { diff --git a/backend/app/store/avatar/localfs.go b/backend/app/store/avatar/localfs.go index 735fa1bf..1c077b0f 100644 --- a/backend/app/store/avatar/localfs.go +++ b/backend/app/store/avatar/localfs.go @@ -88,6 +88,13 @@ func (fs *LocalFS) ID(avatar string) (id string) { return store.EncodeID(avatar + strconv.FormatInt(fi.ModTime().Unix(), 10)) } +// Remove avatar file +func (fs *LocalFS) Remove(avatar string) error { + location := fs.location(strings.TrimSuffix(avatar, imgSfx)) + avFile := path.Join(location, avatar) + return os.Remove(avFile) +} + // get location (directory) for user id by adding partition to final path in order to keep files // in different subdirectories and avoid too many files in a single place. // the end result is a full path like this - /tmp/avatars.test/92 diff --git a/backend/app/store/avatar/localfs_test.go b/backend/app/store/avatar/localfs_test.go index f60bd9ed..51833f30 100644 --- a/backend/app/store/avatar/localfs_test.go +++ b/backend/app/store/avatar/localfs_test.go @@ -110,6 +110,22 @@ func TestAvatarStoreFS_ID(t *testing.T) { assert.Equal(t, "325d5b451f32c2f8e7f30a9fd65bff6a42954d9a", id) // store.EncodeID("b3daa77b4c04a9551b8781d03191fe098f325e67.image1500000000") } +func TestAvatarStoreFS_Remove(t *testing.T) { + p := NewLocalFS("/tmp/avatars.test", 300) + err := os.MkdirAll("/tmp/avatars.test/30", 0700) + require.NoError(t, err) + defer os.RemoveAll("/tmp/avatars.test") + + assert.NotNil(t, p.Remove("no-such-avatar"), "remove non-existing avatar") + err = ioutil.WriteFile("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image", []byte("something"), 0666) + require.NoError(t, err) + + assert.NoError(t, p.Remove("b3daa77b4c04a9551b8781d03191fe098f325e67.image")) + _, err = os.Stat("/tmp/avatars.test/30/b3daa77b4c04a9551b8781d03191fe098f325e67.image") + assert.NotNil(t, err, "removed for real") + t.Log(err) +} + func BenchmarkAvatarStoreFS_ID(b *testing.B) { p := NewLocalFS("/tmp/avatars.test", 300) os.MkdirAll("/tmp/avatars.test/30", 0700) diff --git a/backend/app/store/avatar/store.go b/backend/app/store/avatar/store.go index 1b612bcf..94392405 100644 --- a/backend/app/store/avatar/store.go +++ b/backend/app/store/avatar/store.go @@ -22,9 +22,11 @@ const imgSfx = ".image" // Store defines interface to store and and load avatars type Store interface { - Put(userID string, reader io.Reader) (avatarID string, err error) // save avatar data from the given reader and return base name + Put(userID string, reader io.Reader) (avatarID string, err error) // save avatar data from the reader and return base name Get(avatarID string) (reader io.ReadCloser, size int, err error) // load avatar via reader ID(avatarID string) (id string) // unique id of stored avatar's data + Remove(avatarID string) error // remove avatar data + } // resize an image of supported format (PNG, JPG, GIF) to the size of "limit" px of the biggest side