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
Этот коммит содержится в:
коммит произвёл
Harrison Healey
родитель
fc158fce90
Коммит
ecefa6cdd1
@@ -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 {
|
||||
|
||||
27
app/user.go
27
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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Ссылка в новой задаче
Block a user