Commit ecefa6cd authored by Daniel Schalla's avatar Daniel Schalla Committed by Harrison Healey

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
parent fc158fce
......@@ -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 {
......
......@@ -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
}
......
......@@ -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")
}
}
......@@ -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
}
}
})
}
......@@ -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()
}
......
......@@ -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)
}
}
......@@ -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)
......
......@@ -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)
......
......@@ -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)
......
......@@ -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 {
......
......@@ -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()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment