Modify: use pure tree structure in file system scheme

This commit is contained in:
HFO4
2019-12-07 19:47:22 +08:00
parent 10a2ef4267
commit 56b1ae9f31
13 changed files with 1728 additions and 1620 deletions

View File

@@ -21,6 +21,9 @@ type File struct {
// 关联模型
Policy Policy `gorm:"PRELOAD:false,association_autoupdate:false"`
// 数据库忽略字段
PositionTemp string `gorm:"-"`
}
// Create 创建文件记录
@@ -32,20 +35,31 @@ func (file *File) Create() (uint, error) {
return file.ID, nil
}
// GetFileByPathAndName 给定路径(s)、文件名、用户ID查找文件
func GetFileByPathAndName(path string, name string, uid uint) (File, error) {
// GetChildFile 查找目录下名为name的子文件
func (folder *Folder) GetChildFile(name string) (*File, error) {
var file File
result := DB.Where("user_id = ? AND dir = ? AND name=?", uid, path, name).First(&file)
return file, result.Error
result := DB.Where("folder_id = ? AND name = ?", folder.ID, name).Find(&file)
if result.Error == nil {
file.PositionTemp = path.Join(folder.PositionTemp, folder.Name, file.Name)
}
return &file, result.Error
}
// GetChildFile 查找目录下子文件
func (folder *Folder) GetChildFile() ([]File, error) {
// GetChildFiles 查找目录下子文件
func (folder *Folder) GetChildFiles() ([]File, error) {
var files []File
result := DB.Where("folder_id = ?", folder.ID).Find(&files)
return files, result.Error
}
// GetFilesByIDs 根据文件ID批量获取文件
func GetFilesByIDs(ids []uint, uid uint) ([]File, error) {
var files []File
result := DB.Where("id in (?) AND user_id = ?", ids, uid).Find(&files)
return files, result.Error
}
// GetChildFilesOfFolders 批量检索目录子文件
func GetChildFilesOfFolders(folders *[]Folder) ([]File, error) {
// 将所有待删除目录ID抽离以便检索文件
@@ -68,19 +82,6 @@ func (file *File) GetPolicy() *Policy {
return &file.Policy
}
// GetFileByPaths 根据给定的文件路径(s)查找文件
func GetFileByPaths(paths []string, uid uint) ([]File, error) {
var files []File
tx := DB
for _, value := range paths {
base := path.Base(value)
dir := path.Dir(value)
tx = tx.Or("dir = ? and name = ? and user_id = ?", dir, base, uid)
}
result := tx.Find(&files)
return files, result.Error
}
// RemoveFilesWithSoftLinks 去除给定的文件列表中有软链接的文件
func RemoveFilesWithSoftLinks(files []File) ([]File, error) {
// 结果值
@@ -127,14 +128,6 @@ func DeleteFileByIDs(ids []uint) error {
return result.Error
}
//// GetRecursiveByPaths 根据给定的文件路径(s)递归查找文件
//func GetRecursiveByPaths(paths []string, uid uint) ([]File, error) {
// files := make([]File, 0, len(paths))
// search := util.BuildRegexp(paths, "^", "/", "|")
// result := DB.Where("(user_id = ? and dir REGEXP ?) or (user_id = and dir in (?))", uid, search, uid, paths).Find(&files)
// return files, result.Error
//}
// GetFilesByParentIDs 根据父目录ID查找文件
func GetFilesByParentIDs(ids []uint, uid uint) ([]File, error) {
files := make([]File, 0, len(ids))

View File

@@ -8,17 +8,6 @@ import (
"testing"
)
func TestGetFileByPathAndName(t *testing.T) {
asserts := assert.New(t)
fileRows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "1.cia")
mock.ExpectQuery("SELECT(.+)").WillReturnRows(fileRows)
file, _ := GetFileByPathAndName("/", "1.cia", 1)
asserts.Equal("1.cia", file.Name)
asserts.NoError(mock.ExpectationsWereMet())
}
func TestFile_Create(t *testing.T) {
asserts := assert.New(t)
file := File{
@@ -44,6 +33,32 @@ func TestFile_Create(t *testing.T) {
}
func TestFolder_GetChildFile(t *testing.T) {
asserts := assert.New(t)
folder := Folder{Model: gorm.Model{ID: 1}, Name: "/"}
// 存在
{
mock.ExpectQuery("SELECT(.+)").
WithArgs(1, "1.txt").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "1.txt"))
file, err := folder.GetChildFile("1.txt")
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
asserts.Equal("1.txt", file.Name)
asserts.Equal("/1.txt", file.PositionTemp)
}
// 不存在
{
mock.ExpectQuery("SELECT(.+)").
WithArgs(1, "1.txt").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}))
_, err := folder.GetChildFile("1.txt")
asserts.NoError(mock.ExpectationsWereMet())
asserts.Error(err)
}
}
func TestFolder_GetChildFiles(t *testing.T) {
asserts := assert.New(t)
folder := &Folder{
Model: gorm.Model{
@@ -53,20 +68,46 @@ func TestFolder_GetChildFile(t *testing.T) {
// 找不到
mock.ExpectQuery("SELECT(.+)folder_id(.+)").WithArgs(1).WillReturnError(errors.New("error"))
files, err := folder.GetChildFile()
files, err := folder.GetChildFiles()
asserts.Error(err)
asserts.Len(files, 0)
asserts.NoError(mock.ExpectationsWereMet())
// 找到了
mock.ExpectQuery("SELECT(.+)folder_id(.+)").WithArgs(1).WillReturnRows(sqlmock.NewRows([]string{"name", "id"}).AddRow("1.txt", 1).AddRow("2.txt", 2))
files, err = folder.GetChildFile()
files, err = folder.GetChildFiles()
asserts.NoError(err)
asserts.Len(files, 2)
asserts.NoError(mock.ExpectationsWereMet())
}
func TestGetFilesByIDs(t *testing.T) {
asserts := assert.New(t)
// 出错
{
mock.ExpectQuery("SELECT(.+)").
WithArgs(1, 2, 3, 1).
WillReturnError(errors.New("error"))
folders, err := GetFilesByIDs([]uint{1, 2, 3}, 1)
asserts.NoError(mock.ExpectationsWereMet())
asserts.Error(err)
asserts.Len(folders, 0)
}
// 部分找到
{
mock.ExpectQuery("SELECT(.+)").
WithArgs(1, 2, 3, 1).
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "1"))
folders, err := GetFilesByIDs([]uint{1, 2, 3}, 1)
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
asserts.Len(folders, 1)
}
}
func TestGetChildFilesOfFolders(t *testing.T) {
asserts := assert.New(t)
testFolder := []Folder{
@@ -149,46 +190,6 @@ func TestFile_GetPolicy(t *testing.T) {
}
}
func TestGetFileByPaths(t *testing.T) {
asserts := assert.New(t)
paths := []string{"/我的目录/文件.txt", "/根目录文件.txt"}
// 正常情况
{
mock.ExpectQuery("SELECT(.+)files(.+)").
WithArgs("/我的目录", "文件.txt", 1, "/", "根目录文件.txt", 1).
WillReturnRows(
sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "文件.txt").
AddRow(2, "根目录文件.txt"),
)
files, err := GetFileByPaths(paths, 1)
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
asserts.Equal([]File{
File{
Model: gorm.Model{ID: 1},
Name: "文件.txt",
},
File{
Model: gorm.Model{ID: 2},
Name: "根目录文件.txt",
},
}, files)
}
// 出错
{
mock.ExpectQuery("SELECT(.+)files(.+)").
WithArgs("/我的目录", "文件.txt", 1, "/", "根目录文件.txt", 1).
WillReturnError(errors.New("error"))
files, err := GetFileByPaths(paths, 1)
asserts.NoError(mock.ExpectationsWereMet())
asserts.Error(err)
asserts.Len(files, 0)
}
}
func TestRemoveFilesWithSoftLinks(t *testing.T) {
asserts := assert.New(t)
files := []File{

View File

@@ -2,11 +2,9 @@ package model
import (
"errors"
"github.com/HFO4/cloudreve/pkg/conf"
"github.com/HFO4/cloudreve/pkg/util"
"github.com/jinzhu/gorm"
"path"
"strings"
)
// Folder 目录
@@ -18,6 +16,9 @@ type Folder struct {
Position string `gorm:"size:65536"`
OwnerID uint `gorm:"index:owner_id"`
PositionAbsolute string `gorm:"size:65536"`
// 数据库忽略字段
PositionTemp string `gorm:"-"`
}
// Create 创建目录
@@ -29,11 +30,18 @@ func (folder *Folder) Create() (uint, error) {
return folder.ID, nil
}
// GetFolderByPath 根据绝对路径和UID查找目录
func GetFolderByPath(path string, uid uint) (Folder, error) {
var folder Folder
result := DB.Where("owner_id = ? AND position_absolute = ?", uid, path).First(&folder)
return folder, result.Error
// GetChild 返回folder下名为name的子目录不存在则返回错误
func (folder *Folder) GetChild(name string) (*Folder, error) {
var resFolder Folder
err := DB.
Where("parent_id = ? AND owner_id = ? AND name = ?", folder.ID, folder.OwnerID, name).
First(&resFolder).Error
// 将子目录的路径传递下去
if err == nil {
resFolder.PositionTemp = path.Join(folder.PositionTemp, folder.Name)
}
return &resFolder, err
}
// GetChildFolder 查找子目录
@@ -44,59 +52,48 @@ func (folder *Folder) GetChildFolder() ([]Folder, error) {
}
// GetRecursiveChildFolder 查找所有递归子目录,包括自身
func GetRecursiveChildFolder(dirs []string, uid uint, includeSelf bool) ([]Folder, error) {
func GetRecursiveChildFolder(dirs []uint, uid uint, includeSelf bool) ([]Folder, error) {
folders := make([]Folder, 0, len(dirs))
var err error
if conf.DatabaseConfig.Type == "mysql" {
// SQLite 下使用递归查询
var parFolders []Folder
result := DB.Where("owner_id = ? and id in (?)", uid, dirs).Find(&parFolders)
if result.Error != nil {
return folders, err
}
// MySQL 下使用正则查询
search := util.BuildRegexp(dirs, "^", "/", "|")
result := DB.Where("(owner_id = ? and position_absolute REGEXP ?) or (owner_id = ? and position_absolute in (?))", uid, search, uid, dirs).Find(&folders)
err = result.Error
// 整理父目录的ID
var parentIDs = make([]uint, 0, len(parFolders))
for _, folder := range parFolders {
parentIDs = append(parentIDs, folder.ID)
}
} else {
if includeSelf {
// 合并至最终结果
folders = append(folders, parFolders...)
}
parFolders = []Folder{}
// SQLite 下使用递归查询
var parFolders []Folder
result := DB.Where("owner_id = ? and position_absolute in (?)", uid, dirs).Find(&parFolders)
if result.Error != nil {
return folders, err
// 递归查询子目录,最大递归65535次
for i := 0; i < 65535; i++ {
result = DB.Where("owner_id = ? and parent_id in (?)", uid, parentIDs).Find(&parFolders)
// 查询结束条件
if len(parFolders) == 0 {
break
}
// 整理父目录的ID
var parentIDs = make([]uint, 0, len(parFolders))
parentIDs = make([]uint, 0, len(parFolders))
for _, folder := range parFolders {
parentIDs = append(parentIDs, folder.ID)
}
if includeSelf {
// 合并至最终结果
folders = append(folders, parFolders...)
}
// 合并至最终结果
folders = append(folders, parFolders...)
parFolders = []Folder{}
// 递归查询子目录,最大递归65535次
for i := 0; i < 65535; i++ {
result = DB.Where("owner_id = ? and parent_id in (?)", uid, parentIDs).Find(&parFolders)
// 查询结束条件
if len(parFolders) == 0 {
break
}
// 整理父目录的ID
parentIDs = make([]uint, 0, len(parFolders))
for _, folder := range parFolders {
parentIDs = append(parentIDs, folder.ID)
}
// 合并至最终结果
folders = append(folders, parFolders...)
parFolders = []Folder{}
}
}
return folders, err
@@ -108,9 +105,16 @@ func DeleteFolderByIDs(ids []uint) error {
return result.Error
}
// GetFoldersByIDs 根据ID和用户查找所有目录
func GetFoldersByIDs(ids []uint, uid uint) ([]Folder, error) {
var folders []Folder
result := DB.Where("id in (?) AND owner_id = ?", ids, uid).Find(&folders)
return folders, result.Error
}
// MoveOrCopyFileTo 将此目录下的files移动或复制至dstFolder
// 返回此操作新增的容量
func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy bool) (uint64, error) {
func (folder *Folder) MoveOrCopyFileTo(files []uint, dstFolder *Folder, isCopy bool) (uint64, error) {
// 已复制文件的总大小
var copiedSize uint64
@@ -118,10 +122,10 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy
// 检索出要复制的文件
var originFiles = make([]File, 0, len(files))
if err := DB.Where(
"name in (?) and user_id = ? and dir = ?",
"id in (?) and user_id = ? and folder_id = ?",
files,
folder.OwnerID,
folder.PositionAbsolute,
folder.ID,
).Find(&originFiles).Error; err != nil {
return 0, err
}
@@ -130,7 +134,6 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy
for _, oldFile := range originFiles {
oldFile.Model = gorm.Model{}
oldFile.FolderID = dstFolder.ID
oldFile.Dir = dstFolder.PositionAbsolute
if err := DB.Create(&oldFile).Error; err != nil {
return copiedSize, err
@@ -142,14 +145,13 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy
} else {
// 更改顶级要移动文件的父目录指向
err := DB.Model(File{}).Where(
"name in (?) and user_id = ? and dir = ?",
"id in (?) and user_id = ? and folder_id = ?",
files,
folder.OwnerID,
folder.PositionAbsolute,
folder.ID,
).
Update(map[string]interface{}{
"folder_id": dstFolder.ID,
"dir": dstFolder.PositionAbsolute,
}).
Error
if err != nil {
@@ -162,209 +164,88 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy
}
// MoveOrCopyFolderTo 将folder目录下的dirs子目录复制或移动到dstFolder
// 返回此过程中增加的容量
func (folder *Folder) MoveOrCopyFolderTo(dirs []string, dstFolder *Folder, isCopy bool) (uint64, error) {
// 生成绝对路径
fullDirs := make([]string, len(dirs))
for i := 0; i < len(dirs); i++ {
fullDirs[i] = path.Join(
folder.PositionAbsolute,
path.Base(dirs[i]),
)
}
var subFolders = make([][]Folder, len(fullDirs))
// 更新被移动的目录递归的子目录和文件
for key, parentDir := range fullDirs {
// 检索被移动的目录的所有子目录
toBeMoved, err := GetRecursiveChildFolder([]string{parentDir}, folder.OwnerID, true)
if err != nil {
return 0, err
}
subFolders[key] = toBeMoved
}
// 记录复制要用到的父目录源路径和新的ID
var copyCache = make(map[string]uint)
// 记录已复制文件的容量
var newUsedStorage uint64
var err error
if isCopy {
// 复制
// TODO:支持多目录
origin := Folder{}
if DB.Where(
"position_absolute in (?) and owner_id = ?",
fullDirs,
folder.OwnerID,
).Find(&origin).Error != nil {
return 0, errors.New("找不到原始目录")
}
oldPosition := origin.PositionAbsolute
// 更新复制后的相关属性
origin.PositionAbsolute = util.FillSlash(dstFolder.PositionAbsolute) + origin.Name
origin.Position = dstFolder.PositionAbsolute
origin.ParentID = dstFolder.ID
// 清空主键
origin.Model = gorm.Model{}
if err := DB.Create(&origin).Error; err != nil {
return 0, err
}
// 记录新的主键
copyCache[oldPosition] = origin.Model.ID
} else {
// 移动
// 更改顶级要移动目录的父目录指向
err = DB.Model(Folder{}).
Where("position_absolute in (?) and owner_id = ?",
fullDirs,
folder.OwnerID,
).
Update(map[string]interface{}{
"parent_id": dstFolder.ID,
"position": dstFolder.PositionAbsolute,
"position_absolute": gorm.Expr(
util.BuildConcat("?",
"name",
conf.DatabaseConfig.Type,
),
util.FillSlash(dstFolder.PositionAbsolute),
),
}).Error
}
// CopyFolderTo 将此目录及其子目录及文件递归复制至dstFolder
// 返回此操作新增的容量
func (folder *Folder) CopyFolderTo(folderID uint, dstFolder *Folder) (size uint64, err error) {
// 列出所有子目录
subFolders, err := GetRecursiveChildFolder([]uint{folderID}, folder.OwnerID, true)
if err != nil {
return 0, err
}
// 更新被移动的目录递归的子目录和文件
for parKey, toBeMoved := range subFolders {
ignorePath := fullDirs[parKey]
// TODO 找到更好的修改办法
// 抽离所有子目录的ID
var subFolderIDs = make([]uint, len(toBeMoved))
for key, subFolder := range toBeMoved {
subFolderIDs[key] = subFolder.ID
}
if isCopy {
index := 0
for len(toBeMoved) != 0 {
innerIndex := index % len(toBeMoved)
index++
// 限制循环次数
if index > 65535 {
return 0, errors.New("循环超出限制")
}
// 如果是顶级父目录,直接删除,不需要复制
if toBeMoved[innerIndex].PositionAbsolute == ignorePath {
toBeMoved = append(toBeMoved[:innerIndex], toBeMoved[innerIndex+1:]...)
continue
}
// 如果缓存中存在父目录ID执行复制,并删除
if newID, ok := copyCache[toBeMoved[innerIndex].Position]; ok {
// 记录目录原来的路径
oldPosition := toBeMoved[innerIndex].PositionAbsolute
// 设置目录i虚拟的路径
newPosition := path.Join(
dstFolder.PositionAbsolute, strings.Replace(
toBeMoved[innerIndex].Position,
folder.PositionAbsolute, "", 1),
)
toBeMoved[innerIndex].Position = newPosition
toBeMoved[innerIndex].PositionAbsolute = path.Join(
newPosition,
toBeMoved[innerIndex].Name,
)
toBeMoved[innerIndex].ParentID = newID
toBeMoved[innerIndex].Model = gorm.Model{}
if err := DB.Create(&toBeMoved[innerIndex]).Error; err != nil {
return 0, err
}
// 将当前目录老路径和新ID保存以便后续待处理目录文件使用
copyCache[oldPosition] = toBeMoved[innerIndex].Model.ID
toBeMoved = append(toBeMoved[:innerIndex], toBeMoved[innerIndex+1:]...)
}
}
// 抽离所有子目录的ID
var subFolderIDs = make([]uint, len(subFolders))
for key, value := range subFolders {
subFolderIDs[key] = value.ID
}
// 复制子目录
var newIDCache = make(map[uint]uint)
for _, folder := range subFolders {
// 新的父目录指向
var newID uint
// 顶级目录直接指向新的目的目录
if folder.ID == folderID {
newID = dstFolder.ID
} else if IDCache, ok := newIDCache[folder.ParentID]; ok {
newID = IDCache
} else {
for _, subFolder := range toBeMoved {
// 每个分组的第一个目录已经变更指向,直接跳过
if subFolder.PositionAbsolute != ignorePath {
newPosition := path.Join(dstFolder.PositionAbsolute,
strings.Replace(subFolder.Position,
folder.PositionAbsolute,
"",
1,
),
)
// 移动
DB.Model(&subFolder).Updates(map[string]interface{}{
"position": newPosition,
"position_absolute": path.Join(newPosition, subFolder.Name),
})
}
}
util.Log().Warning("无法取得新的父目录:%d", folder.ParentID)
return size, errors.New("无法取得新的父目录")
}
// 获取子目录下的所有子文件
toBeMovedFile, err := GetFilesByParentIDs(subFolderIDs, folder.OwnerID)
if err != nil {
return 0, err
}
// 开始复制或移动子文件
for _, subFile := range toBeMovedFile {
newPosition := path.Join(dstFolder.PositionAbsolute,
strings.Replace(
subFile.Dir,
folder.PositionAbsolute,
"",
1,
),
)
if isCopy {
// 复制
if newID, ok := copyCache[subFile.Dir]; ok {
subFile.FolderID = newID
} else {
util.Log().Debug("无法找到文件的父目录ID原始路径%s", subFile.Dir)
}
subFile.Dir = newPosition
subFile.Model = gorm.Model{}
// 复制文件记录
if err := DB.Create(&subFile).Error; err != nil {
util.Log().Warning("无法复制子文件:%s", err)
} else {
// 记录此文件容量
newUsedStorage += subFile.Size
}
} else {
DB.Model(&subFile).Updates(map[string]interface{}{
"dir": newPosition,
})
}
// 插入新的目录记录
oldID := folder.ID
folder.Model = gorm.Model{}
folder.ParentID = newID
if err = DB.Create(&folder).Error; err != nil {
return size, err
}
// 记录新的ID以便其子目录使用
newIDCache[oldID] = folder.ID
}
return newUsedStorage, nil
// 复制文件
var originFiles = make([]File, 0, len(subFolderIDs))
if err := DB.Where(
"user_id = ? and folder_id in (?)",
folder.OwnerID,
subFolderIDs,
).Find(&originFiles).Error; err != nil {
return 0, err
}
// 复制文件记录
for _, oldFile := range originFiles {
oldFile.Model = gorm.Model{}
oldFile.FolderID = newIDCache[oldFile.FolderID]
if err := DB.Create(&oldFile).Error; err != nil {
return size, err
}
size += oldFile.Size
}
return size, nil
}
// MoveFolderTo 将folder目录下的dirs子目录复制或移动到dstFolder
// 返回此过程中增加的容量
func (folder *Folder) MoveFolderTo(dirs []uint, dstFolder *Folder) error {
// 更改顶级要移动目录的父目录指向
err := DB.Model(Folder{}).Where(
"id in (?) and owner_id = ? and parent_id = ?",
dirs,
folder.OwnerID,
folder.ID,
).Update(map[string]interface{}{
"parent_id": dstFolder.ID,
}).Error
return err
}

File diff suppressed because it is too large Load Diff

View File

@@ -55,6 +55,13 @@ type UserOption struct {
PreferredTheme string `json:"preferred_theme"`
}
// Root 获取用户的根目录
func (user *User) Root() (*Folder, error) {
var folder Folder
err := DB.Where("parent_id = 0 AND owner_id = ?", user.ID).First(&folder).Error
return &folder, err
}
// DeductionStorage 减少用户已用容量
func (user *User) DeductionStorage(size uint64) bool {
if size == 0 {
@@ -160,10 +167,8 @@ func (user *User) BeforeSave() (err error) {
func (user *User) AfterCreate(tx *gorm.DB) (err error) {
// 创建用户的默认根目录
defaultFolder := &Folder{
Name: "根目录",
Position: ".",
OwnerID: user.ID,
PositionAbsolute: "/",
Name: "/",
OwnerID: user.ID,
}
tx.Create(defaultFolder)
return err

View File

@@ -306,3 +306,25 @@ func TestUser_AfterCreate(t *testing.T) {
asserts.NoError(err)
asserts.NoError(mock.ExpectationsWereMet())
}
func TestUser_Root(t *testing.T) {
asserts := assert.New(t)
user := User{Model: gorm.Model{ID: 1}}
// 根目录存在
{
mock.ExpectQuery("SELECT(.+)").WithArgs(1).WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "根目录"))
root, err := user.Root()
asserts.NoError(mock.ExpectationsWereMet())
asserts.NoError(err)
asserts.Equal("根目录", root.Name)
}
// 根目录不存在
{
mock.ExpectQuery("SELECT(.+)").WithArgs(1).WillReturnRows(sqlmock.NewRows([]string{"id", "name"}))
_, err := user.Root()
asserts.NoError(mock.ExpectationsWereMet())
asserts.Error(err)
}
}