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
Этот коммит содержится в:
Daniel Schalla
2018-06-25 18:12:59 +02:00
коммит произвёл Harrison Healey
родитель fc158fce90
Коммит ecefa6cdd1
11 изменённых файлов: 270 добавлений и 0 удалений

Просмотреть файл

@@ -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()