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) 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 { func (a *App) MoveFile(oldPath, newPath string) *model.AppError {
backend, err := a.FileBackend() backend, err := a.FileBackend()
if err != nil { if err != nil {

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

@@ -1288,6 +1288,33 @@ func (a *App) PermanentDeleteUser(user *model.User) *model.AppError {
return result.Err 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 { if result := <-a.Srv.Store.User().PermanentDelete(user.Id); result.Err != nil {
return result.Err 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 { func (fs SqlFileInfoStore) AttachToPost(fileId, postId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) { return store.Do(func(result *store.StoreResult) {
if _, err := fs.GetMaster().Exec( 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 Get(id string) StoreChannel
GetByPath(path string) StoreChannel GetByPath(path string) StoreChannel
GetForPost(postId string, readFromMaster bool, allowFromCache bool) StoreChannel GetForPost(postId string, readFromMaster bool, allowFromCache bool) StoreChannel
GetForUser(userId string) StoreChannel
InvalidateFileInfosForPostCache(postId string) InvalidateFileInfosForPostCache(postId string)
AttachToPost(fileId string, postId string) StoreChannel AttachToPost(fileId string, postId string) StoreChannel
DeleteForPost(postId string) StoreChannel DeleteForPost(postId string) StoreChannel
PermanentDelete(fileId string) StoreChannel PermanentDelete(fileId string) StoreChannel
PermanentDeleteBatch(endTime int64, limit int64) StoreChannel PermanentDeleteBatch(endTime int64, limit int64) StoreChannel
PermanentDeleteByUser(userId string) StoreChannel
ClearCaches() 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("FileInfoSaveGet", func(t *testing.T) { testFileInfoSaveGet(t, ss) })
t.Run("FileInfoSaveGetByPath", func(t *testing.T) { testFileInfoSaveGetByPath(t, ss) }) t.Run("FileInfoSaveGetByPath", func(t *testing.T) { testFileInfoSaveGetByPath(t, ss) })
t.Run("FileInfoGetForPost", func(t *testing.T) { testFileInfoGetForPost(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("FileInfoAttachToPost", func(t *testing.T) { testFileInfoAttachToPost(t, ss) })
t.Run("FileInfoDeleteForPost", func(t *testing.T) { testFileInfoDeleteForPost(t, ss) }) t.Run("FileInfoDeleteForPost", func(t *testing.T) { testFileInfoDeleteForPost(t, ss) })
t.Run("FileInfoPermanentDelete", func(t *testing.T) { testFileInfoPermanentDelete(t, ss) }) t.Run("FileInfoPermanentDelete", func(t *testing.T) { testFileInfoPermanentDelete(t, ss) })
t.Run("FileInfoPermanentDeleteBatch", func(t *testing.T) { testFileInfoPermanentDeleteBatch(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) { 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) { func testFileInfoAttachToPost(t *testing.T, ss store.Store) {
userId := model.NewId() userId := model.NewId()
postId := model.NewId() postId := model.NewId()
@@ -294,3 +344,18 @@ func testFileInfoPermanentDeleteBatch(t *testing.T, ss store.Store) {
t.Fatal("Expected 3 fileInfos") 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 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 // InvalidateFileInfosForPostCache provides a mock function with given fields: postId
func (_m *FileInfoStore) InvalidateFileInfosForPostCache(postId string) { func (_m *FileInfoStore) InvalidateFileInfosForPostCache(postId string) {
_m.Called(postId) _m.Called(postId)
@@ -135,6 +151,22 @@ func (_m *FileInfoStore) PermanentDeleteBatch(endTime int64, limit int64) store.
return r0 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 // Save provides a mock function with given fields: info
func (_m *FileInfoStore) Save(info *model.FileInfo) store.StoreChannel { func (_m *FileInfoStore) Save(info *model.FileInfo) store.StoreChannel {
ret := _m.Called(info) ret := _m.Called(info)

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

@@ -15,6 +15,7 @@ type FileBackend interface {
Reader(path string) (io.ReadCloser, *model.AppError) Reader(path string) (io.ReadCloser, *model.AppError)
ReadFile(path string) ([]byte, *model.AppError) ReadFile(path string) ([]byte, *model.AppError)
FileExists(path string) (bool, *model.AppError)
CopyFile(oldPath, newPath string) *model.AppError CopyFile(oldPath, newPath string) *model.AppError
MoveFile(oldPath, newPath string) *model.AppError MoveFile(oldPath, newPath string) *model.AppError
WriteFile(fr io.Reader, path string) (int64, *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 { func (b *LocalFileBackend) CopyFile(oldPath, newPath string) *model.AppError {
if err := CopyFile(filepath.Join(b.directory, oldPath), filepath.Join(b.directory, newPath)); err != nil { 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) 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 { func (b *S3FileBackend) CopyFile(oldPath, newPath string) *model.AppError {
s3Clnt, err := b.s3New() s3Clnt, err := b.s3New()
if err != nil { if err != nil {

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

@@ -124,6 +124,23 @@ func (s *FileBackendTestSuite) TestReadWriteFileImage() {
s.EqualValues(readString, "testimage") 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() { func (s *FileBackendTestSuite) TestCopyFile() {
b := []byte("test") b := []byte("test")
path1 := "tests/" + model.NewId() path1 := "tests/" + model.NewId()