diff options
author | Daniel Schalla <daniel@schalla.me> | 2018-06-25 18:12:59 +0200 |
---|---|---|
committer | Harrison Healey <harrisonmhealey@gmail.com> | 2018-06-25 12:12:59 -0400 |
commit | ecefa6cdd1e7376046bbec82c1b47f7756fea646 (patch) | |
tree | d1cb2486e344c1bb93353020713b9eb2146a5b6f | |
parent | fc158fce907b602bbde3babfadfd1a04d1dde31e (diff) | |
download | chat-ecefa6cdd1e7376046bbec82c1b47f7756fea646.tar.gz chat-ecefa6cdd1e7376046bbec82c1b47f7756fea646.tar.bz2 chat-ecefa6cdd1e7376046bbec82c1b47f7756fea646.zip |
Implementation of File Exists Function; Delete FileInfos upon Permanent User Delete (#8958)
Check if file was deleted on FS
Warning message if file couldnt be removed
-rw-r--r-- | app/file.go | 8 | ||||
-rw-r--r-- | app/user.go | 27 | ||||
-rw-r--r-- | app/user_test.go | 44 | ||||
-rw-r--r-- | store/sqlstore/file_info_store.go | 43 | ||||
-rw-r--r-- | store/store.go | 2 | ||||
-rw-r--r-- | store/storetest/file_info_store.go | 65 | ||||
-rw-r--r-- | store/storetest/mocks/FileInfoStore.go | 32 | ||||
-rw-r--r-- | utils/file_backend.go | 1 | ||||
-rw-r--r-- | utils/file_backend_local.go | 12 | ||||
-rw-r--r-- | utils/file_backend_s3.go | 19 | ||||
-rw-r--r-- | utils/file_backend_test.go | 17 |
11 files changed, 270 insertions, 0 deletions
diff --git a/app/file.go b/app/file.go index aba09479a..add965fd7 100644 --- a/app/file.go +++ b/app/file.go @@ -70,6 +70,14 @@ func (a *App) ReadFile(path string) ([]byte, *model.AppError) { return backend.ReadFile(path) } +func (a *App) FileExists(path string) (bool, *model.AppError) { + backend, err := a.FileBackend() + if err != nil { + return false, err + } + return backend.FileExists(path) +} + func (a *App) MoveFile(oldPath, newPath string) *model.AppError { backend, err := a.FileBackend() if err != nil { diff --git a/app/user.go b/app/user.go index 27e6f347d..b00ef19ef 100644 --- a/app/user.go +++ b/app/user.go @@ -1288,6 +1288,33 @@ func (a *App) PermanentDeleteUser(user *model.User) *model.AppError { return result.Err } + fchan := a.Srv.Store.FileInfo().GetForUser(user.Id) + var infos []*model.FileInfo + if result := <-fchan; result.Err != nil { + mlog.Warn("Error getting file list for user from FileInfoStore") + } else { + infos = result.Data.([]*model.FileInfo) + for _, info := range infos { + res, err := a.FileExists(info.Path) + + if err != nil { + mlog.Warn(fmt.Sprintf("Error checking existence of file '%s': %s", info.Path, err)) + continue + } + + if res { + a.RemoveFile(info.Path) + } else { + mlog.Warn(fmt.Sprintf("Unable to remove file '%s': %s", info.Path, err)) + } + + } + } + + if result := <-a.Srv.Store.FileInfo().PermanentDeleteByUser(user.Id); result.Err != nil { + return result.Err + } + if result := <-a.Srv.Store.User().PermanentDelete(user.Id); result.Err != nil { return result.Err } diff --git a/app/user_test.go b/app/user_test.go index b557d296b..7952eaa1f 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -480,3 +480,47 @@ func TestCreateUserWithToken(t *testing.T) { } }) } + +func TestPermanentDeleteUser(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + b := []byte("testimage") + + finfo, err := th.App.DoUploadFile(time.Now(), th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, "testfile.txt", b) + + if err != nil { + t.Log(err) + t.Fatal("Unable to upload file") + } + + err = th.App.PermanentDeleteUser(th.BasicUser) + if err != nil { + t.Log(err) + t.Fatal("Unable to delete user") + } + + res, err := th.App.FileExists(finfo.Path) + + if err != nil { + t.Log(err) + t.Fatal("Unable to check whether file exists") + } + + if res { + t.Log(err) + t.Fatal("File was not deleted on FS") + } + + finfo, err = th.App.GetFileInfo(finfo.Id) + + if finfo != nil { + t.Log(err) + t.Fatal("Unable to find finfo") + } + + if err == nil { + t.Log(err) + t.Fatal("GetFileInfo after DeleteUser is nil") + } +} diff --git a/store/sqlstore/file_info_store.go b/store/sqlstore/file_info_store.go index 7559640c8..824e41583 100644 --- a/store/sqlstore/file_info_store.go +++ b/store/sqlstore/file_info_store.go @@ -177,6 +177,30 @@ func (fs SqlFileInfoStore) GetForPost(postId string, readFromMaster bool, allowF }) } +func (fs SqlFileInfoStore) GetForUser(userId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + var infos []*model.FileInfo + + dbmap := fs.GetReplica() + + if _, err := dbmap.Select(&infos, + `SELECT + * + FROM + FileInfo + WHERE + CreatorId = :CreatorId + AND DeleteAt = 0 + ORDER BY + CreateAt`, map[string]interface{}{"CreatorId": userId}); err != nil { + result.Err = model.NewAppError("SqlFileInfoStore.GetForPost", + "store.sql_file_info.get_for_user_id.app_error", nil, "creator_id="+userId+", "+err.Error(), http.StatusInternalServerError) + } else { + result.Data = infos + } + }) +} + func (fs SqlFileInfoStore) AttachToPost(fileId, postId string) store.StoreChannel { return store.Do(func(result *store.StoreResult) { if _, err := fs.GetMaster().Exec( @@ -246,3 +270,22 @@ func (s SqlFileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) store } }) } + +func (s SqlFileInfoStore) PermanentDeleteByUser(userId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + query := "DELETE from FileInfo WHERE CreatorId = :CreatorId" + + sqlResult, err := s.GetMaster().Exec(query, map[string]interface{}{"CreatorId": userId}) + if err != nil { + result.Err = model.NewAppError("SqlFileInfoStore.PermanentDeleteByUser", "store.sql_file_info.PermanentDeleteByUser.app_error", nil, ""+err.Error(), http.StatusInternalServerError) + } else { + rowsAffected, err1 := sqlResult.RowsAffected() + if err1 != nil { + result.Err = model.NewAppError("SqlFileInfoStore.PermanentDeleteByUser", "store.sql_file_info.PermanentDeleteByUser.app_error", nil, ""+err.Error(), http.StatusInternalServerError) + result.Data = int64(0) + } else { + result.Data = rowsAffected + } + } + }) +} diff --git a/store/store.go b/store/store.go index 3ec136f0b..bb967e614 100644 --- a/store/store.go +++ b/store/store.go @@ -430,11 +430,13 @@ type FileInfoStore interface { Get(id string) StoreChannel GetByPath(path string) StoreChannel GetForPost(postId string, readFromMaster bool, allowFromCache bool) StoreChannel + GetForUser(userId string) StoreChannel InvalidateFileInfosForPostCache(postId string) AttachToPost(fileId string, postId string) StoreChannel DeleteForPost(postId string) StoreChannel PermanentDelete(fileId string) StoreChannel PermanentDeleteBatch(endTime int64, limit int64) StoreChannel + PermanentDeleteByUser(userId string) StoreChannel ClearCaches() } diff --git a/store/storetest/file_info_store.go b/store/storetest/file_info_store.go index 9b3388db5..50b5cf059 100644 --- a/store/storetest/file_info_store.go +++ b/store/storetest/file_info_store.go @@ -15,10 +15,12 @@ func TestFileInfoStore(t *testing.T, ss store.Store) { t.Run("FileInfoSaveGet", func(t *testing.T) { testFileInfoSaveGet(t, ss) }) t.Run("FileInfoSaveGetByPath", func(t *testing.T) { testFileInfoSaveGetByPath(t, ss) }) t.Run("FileInfoGetForPost", func(t *testing.T) { testFileInfoGetForPost(t, ss) }) + t.Run("FileInfoGetForUser", func(t *testing.T) { testFileInfoGetForUser(t, ss) }) t.Run("FileInfoAttachToPost", func(t *testing.T) { testFileInfoAttachToPost(t, ss) }) t.Run("FileInfoDeleteForPost", func(t *testing.T) { testFileInfoDeleteForPost(t, ss) }) t.Run("FileInfoPermanentDelete", func(t *testing.T) { testFileInfoPermanentDelete(t, ss) }) t.Run("FileInfoPermanentDeleteBatch", func(t *testing.T) { testFileInfoPermanentDeleteBatch(t, ss) }) + t.Run("FileInfoPermanentDeleteByUser", func(t *testing.T) { testFileInfoPermanentDeleteByUser(t, ss) }) } func testFileInfoSaveGet(t *testing.T, ss store.Store) { @@ -153,6 +155,54 @@ func testFileInfoGetForPost(t *testing.T, ss store.Store) { } } +func testFileInfoGetForUser(t *testing.T, ss store.Store) { + userId := model.NewId() + userId2 := model.NewId() + postId := model.NewId() + + infos := []*model.FileInfo{ + { + PostId: postId, + CreatorId: userId, + Path: "file.txt", + }, + { + PostId: postId, + CreatorId: userId, + Path: "file.txt", + }, + { + PostId: postId, + CreatorId: userId, + Path: "file.txt", + }, + { + PostId: model.NewId(), + CreatorId: userId2, + Path: "file.txt", + }, + } + + for i, info := range infos { + infos[i] = store.Must(ss.FileInfo().Save(info)).(*model.FileInfo) + defer func(id string) { + <-ss.FileInfo().PermanentDelete(id) + }(infos[i].Id) + } + + if result := <-ss.FileInfo().GetForUser(userId); result.Err != nil { + t.Fatal(result.Err) + } else if returned := result.Data.([]*model.FileInfo); len(returned) != 3 { + t.Fatal("should've returned exactly 3 file infos") + } + + if result := <-ss.FileInfo().GetForUser(userId2); result.Err != nil { + t.Fatal(result.Err) + } else if returned := result.Data.([]*model.FileInfo); len(returned) != 1 { + t.Fatal("should've returned exactly 1 file infos") + } +} + func testFileInfoAttachToPost(t *testing.T, ss store.Store) { userId := model.NewId() postId := model.NewId() @@ -294,3 +344,18 @@ func testFileInfoPermanentDeleteBatch(t *testing.T, ss store.Store) { t.Fatal("Expected 3 fileInfos") } } + +func testFileInfoPermanentDeleteByUser(t *testing.T, ss store.Store) { + userId := model.NewId() + postId := model.NewId() + + store.Must(ss.FileInfo().Save(&model.FileInfo{ + PostId: postId, + CreatorId: userId, + Path: "file.txt", + })) + + if result := <-ss.FileInfo().PermanentDeleteByUser(userId); result.Err != nil { + t.Fatal(result.Err) + } +} diff --git a/store/storetest/mocks/FileInfoStore.go b/store/storetest/mocks/FileInfoStore.go index 4dddf0bd7..a33ca0453 100644 --- a/store/storetest/mocks/FileInfoStore.go +++ b/store/storetest/mocks/FileInfoStore.go @@ -98,6 +98,22 @@ func (_m *FileInfoStore) GetForPost(postId string, readFromMaster bool, allowFro return r0 } +// GetForUser provides a mock function with given fields: userId +func (_m *FileInfoStore) GetForUser(userId string) store.StoreChannel { + ret := _m.Called(userId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok { + r0 = rf(userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // InvalidateFileInfosForPostCache provides a mock function with given fields: postId func (_m *FileInfoStore) InvalidateFileInfosForPostCache(postId string) { _m.Called(postId) @@ -135,6 +151,22 @@ func (_m *FileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) store. return r0 } +// PermanentDeleteByUser provides a mock function with given fields: userId +func (_m *FileInfoStore) PermanentDeleteByUser(userId string) store.StoreChannel { + ret := _m.Called(userId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok { + r0 = rf(userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // Save provides a mock function with given fields: info func (_m *FileInfoStore) Save(info *model.FileInfo) store.StoreChannel { ret := _m.Called(info) diff --git a/utils/file_backend.go b/utils/file_backend.go index 9ed564592..368e1ba28 100644 --- a/utils/file_backend.go +++ b/utils/file_backend.go @@ -15,6 +15,7 @@ type FileBackend interface { Reader(path string) (io.ReadCloser, *model.AppError) ReadFile(path string) ([]byte, *model.AppError) + FileExists(path string) (bool, *model.AppError) CopyFile(oldPath, newPath string) *model.AppError MoveFile(oldPath, newPath string) *model.AppError WriteFile(fr io.Reader, path string) (int64, *model.AppError) diff --git a/utils/file_backend_local.go b/utils/file_backend_local.go index ec0c657a7..681ab9234 100644 --- a/utils/file_backend_local.go +++ b/utils/file_backend_local.go @@ -49,6 +49,18 @@ func (b *LocalFileBackend) ReadFile(path string) ([]byte, *model.AppError) { } } +func (b *LocalFileBackend) FileExists(path string) (bool, *model.AppError) { + _, err := os.Stat(filepath.Join(b.directory, path)) + + if os.IsNotExist(err) { + return false, nil + } else if err == nil { + return true, nil + } + + return false, model.NewAppError("ReadFile", "api.file.file_exists.exists_local.app_error", nil, err.Error(), http.StatusInternalServerError) +} + func (b *LocalFileBackend) CopyFile(oldPath, newPath string) *model.AppError { if err := CopyFile(filepath.Join(b.directory, oldPath), filepath.Join(b.directory, newPath)); err != nil { return model.NewAppError("copyFile", "api.file.move_file.rename.app_error", nil, err.Error(), http.StatusInternalServerError) diff --git a/utils/file_backend_s3.go b/utils/file_backend_s3.go index a0c46e5d3..dedcb2797 100644 --- a/utils/file_backend_s3.go +++ b/utils/file_backend_s3.go @@ -111,6 +111,25 @@ func (b *S3FileBackend) ReadFile(path string) ([]byte, *model.AppError) { } } +func (b *S3FileBackend) FileExists(path string) (bool, *model.AppError) { + s3Clnt, err := b.s3New() + + if err != nil { + return false, model.NewAppError("FileExists", "api.file.file_exists.s3.app_error", nil, err.Error(), http.StatusInternalServerError) + } + _, err = s3Clnt.StatObject(b.bucket, path, s3.StatObjectOptions{}) + + if err == nil { + return true, nil + } + + if err.(s3.ErrorResponse).Code == "NoSuchKey" { + return false, nil + } + + return false, model.NewAppError("FileExists", "api.file.file_exists.s3.app_error", nil, err.Error(), http.StatusInternalServerError) +} + func (b *S3FileBackend) CopyFile(oldPath, newPath string) *model.AppError { s3Clnt, err := b.s3New() if err != nil { diff --git a/utils/file_backend_test.go b/utils/file_backend_test.go index 7f8265d73..f7ce7ca61 100644 --- a/utils/file_backend_test.go +++ b/utils/file_backend_test.go @@ -124,6 +124,23 @@ func (s *FileBackendTestSuite) TestReadWriteFileImage() { s.EqualValues(readString, "testimage") } +func (s *FileBackendTestSuite) TestFileExists() { + b := []byte("testimage") + path := "tests/" + model.NewId() + ".png" + + _, err := s.backend.WriteFile(bytes.NewReader(b), path) + s.Nil(err) + defer s.backend.RemoveFile(path) + + res, err := s.backend.FileExists(path) + s.Nil(err) + s.True(res) + + res, err = s.backend.FileExists("tests/idontexist.png") + s.Nil(err) + s.False(res) +} + func (s *FileBackendTestSuite) TestCopyFile() { b := []byte("test") path1 := "tests/" + model.NewId() |