init push
This commit is contained in:
460
internal/repository/mysql/file_manager_folder_repo.go
Normal file
460
internal/repository/mysql/file_manager_folder_repo.go
Normal file
@@ -0,0 +1,460 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
filemanager "wucher/internal/domain/file_manager"
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type FileManagerFolderRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewFileManagerFolderRepository(db *gorm.DB) *FileManagerFolderRepository {
|
||||
return &FileManagerFolderRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) Create(ctx context.Context, row *filemanager.Folder) error {
|
||||
return r.db.WithContext(ctx).Create(row).Error
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) Update(ctx context.Context, row *filemanager.Folder) error {
|
||||
return r.db.WithContext(ctx).Save(row).Error
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) MoveSubtree(ctx context.Context, id []byte, targetParentID []byte, targetName string, targetNameNormalized string, updatedBy []byte) (*filemanager.Folder, error) {
|
||||
var moved filemanager.Folder
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var current filemanager.Folder
|
||||
if err := tx.Model(&filemanager.Folder{}).
|
||||
Where("id = ? AND deleted_at IS NULL", id).
|
||||
First(¤t).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetDepth := 0
|
||||
targetParentPath := ""
|
||||
if len(targetParentID) != 0 {
|
||||
if folderRepoBytesEqual16(targetParentID, current.ID) {
|
||||
return filemanager.ErrInvalidMove
|
||||
}
|
||||
|
||||
var parent filemanager.Folder
|
||||
if err := tx.Model(&filemanager.Folder{}).
|
||||
Where("id = ? AND deleted_at IS NULL", targetParentID).
|
||||
First(&parent).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isDescendant, err := folderRepoIsDescendantFolder(tx, targetParentID, current.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isDescendant {
|
||||
return filemanager.ErrInvalidMove
|
||||
}
|
||||
|
||||
targetDepth = parent.Depth + 1
|
||||
targetParentPath = parent.PathCache
|
||||
}
|
||||
|
||||
var dupTotal int64
|
||||
if err := folderParentScope(
|
||||
tx.Model(&filemanager.Folder{}),
|
||||
targetParentID,
|
||||
).Where("name_normalized = ? AND name_slot = 'live' AND deleted_at IS NULL AND id <> ?", targetNameNormalized, id).Count(&dupTotal).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if dupTotal > 0 {
|
||||
return gorm.ErrDuplicatedKey
|
||||
}
|
||||
|
||||
oldPath := current.PathCache
|
||||
oldDepth := current.Depth
|
||||
newPath := folderRepoBuildPathCache(targetParentPath, targetName)
|
||||
|
||||
res := tx.Model(&filemanager.Folder{}).
|
||||
Where("id = ? AND deleted_at IS NULL", id).
|
||||
Updates(map[string]any{
|
||||
"parent_id": targetParentID,
|
||||
"name": targetName,
|
||||
"name_normalized": targetNameNormalized,
|
||||
"depth": targetDepth,
|
||||
"path_cache": newPath,
|
||||
"updated_by": updatedBy,
|
||||
"updated_at": time.Now().UTC(),
|
||||
})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
depthDelta := targetDepth - oldDepth
|
||||
if oldPath != newPath || depthDelta != 0 {
|
||||
if err := folderRepoRewriteDescendantTree(tx, current.ID, oldPath, newPath, depthDelta, updatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
current.ParentID = targetParentID
|
||||
current.Name = targetName
|
||||
current.NameNormalized = targetNameNormalized
|
||||
current.Depth = targetDepth
|
||||
current.PathCache = newPath
|
||||
current.UpdatedBy = updatedBy
|
||||
moved = current
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &moved, nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) Delete(ctx context.Context, id []byte, deletedBy []byte, purgeAt *time.Time) error {
|
||||
now := time.Now().UTC()
|
||||
nameSlot := deletedNameSlot(id)
|
||||
updates := map[string]any{
|
||||
"deleted_at": now,
|
||||
"deleted_by": deletedBy,
|
||||
"updated_by": deletedBy,
|
||||
"name_slot": nameSlot,
|
||||
}
|
||||
if purgeAt != nil {
|
||||
v := purgeAt.UTC()
|
||||
updates["purge_at"] = v
|
||||
}
|
||||
|
||||
res := r.db.WithContext(ctx).
|
||||
Model(&filemanager.Folder{}).
|
||||
Where("id = ? AND deleted_at IS NULL", id).
|
||||
Updates(updates)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) Restore(ctx context.Context, id []byte, restoredBy []byte) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var row filemanager.Folder
|
||||
if err := tx.Model(&filemanager.Folder{}).Where("id = ?", id).First(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if row.DeletedAt == nil {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := folderParentScope(
|
||||
tx.Model(&filemanager.Folder{}),
|
||||
row.ParentID,
|
||||
).Where("name_normalized = ? AND name_slot = 'live' AND deleted_at IS NULL", row.NameNormalized).Count(&total).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if total > 0 {
|
||||
return gorm.ErrDuplicatedKey
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
"deleted_at": nil,
|
||||
"deleted_by": nil,
|
||||
"purge_at": nil,
|
||||
"updated_by": restoredBy,
|
||||
"name_slot": "live",
|
||||
}
|
||||
res := tx.Model(&filemanager.Folder{}).
|
||||
Where("id = ? AND deleted_at IS NOT NULL", id).
|
||||
Updates(updates)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) Purge(ctx context.Context, id []byte) error {
|
||||
res := r.db.WithContext(ctx).
|
||||
Where("id = ? AND deleted_at IS NOT NULL", id).
|
||||
Delete(&filemanager.Folder{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) GetByID(ctx context.Context, id []byte) (*filemanager.Folder, error) {
|
||||
var row filemanager.Folder
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("id = ? AND deleted_at IS NULL", id).
|
||||
First(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return &row, err
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) GetByIDAny(ctx context.Context, id []byte) (*filemanager.Folder, error) {
|
||||
var row filemanager.Folder
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("id = ?", id).
|
||||
First(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return &row, err
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) GetByParentAndName(ctx context.Context, parentID []byte, nameNormalized string) (*filemanager.Folder, error) {
|
||||
var row filemanager.Folder
|
||||
q := folderParentScope(r.db.WithContext(ctx).Model(&filemanager.Folder{}), parentID).
|
||||
Where("name_normalized = ? AND name_slot = 'live' AND deleted_at IS NULL", nameNormalized)
|
||||
|
||||
err := q.First(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return &row, err
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) ListByParent(ctx context.Context, parentID []byte, sort string, limit, offset int) ([]filemanager.Folder, int64, error) {
|
||||
var rows []filemanager.Folder
|
||||
var total int64
|
||||
|
||||
base := folderParentScope(
|
||||
r.db.WithContext(ctx).
|
||||
Model(&filemanager.Folder{}).
|
||||
Where("deleted_at IS NULL AND name_slot = 'live'"),
|
||||
parentID,
|
||||
)
|
||||
|
||||
query := base
|
||||
if sort != "" {
|
||||
query = query.Order(sort)
|
||||
}
|
||||
if err := base.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit).Offset(offset)
|
||||
}
|
||||
if err := query.Find(&rows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) SearchByName(ctx context.Context, name string, sort string, limit, offset int) ([]filemanager.Folder, int64, error) {
|
||||
var rows []filemanager.Folder
|
||||
var total int64
|
||||
|
||||
base := r.db.WithContext(ctx).
|
||||
Model(&filemanager.Folder{}).
|
||||
Where("deleted_at IS NULL AND name_slot = 'live' AND name LIKE ?", "%"+name+"%")
|
||||
|
||||
query := base
|
||||
if sort != "" {
|
||||
query = query.Order(sort)
|
||||
}
|
||||
if err := base.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit).Offset(offset)
|
||||
}
|
||||
if err := query.Find(&rows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) ListTrash(ctx context.Context, sort string, limit, offset int) ([]filemanager.Folder, int64, error) {
|
||||
var rows []filemanager.Folder
|
||||
var total int64
|
||||
|
||||
base := r.db.WithContext(ctx).
|
||||
Model(&filemanager.Folder{}).
|
||||
Where("deleted_at IS NOT NULL")
|
||||
|
||||
query := base
|
||||
if sort != "" {
|
||||
query = query.Order(sort)
|
||||
}
|
||||
if err := base.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit).Offset(offset)
|
||||
}
|
||||
if err := query.Find(&rows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFolderRepository) ListTrashByParent(ctx context.Context, parentID []byte, sort string, limit, offset int) ([]filemanager.Folder, int64, error) {
|
||||
var rows []filemanager.Folder
|
||||
var total int64
|
||||
|
||||
base := folderParentScope(
|
||||
r.db.WithContext(ctx).
|
||||
Model(&filemanager.Folder{}).
|
||||
Where("deleted_at IS NOT NULL"),
|
||||
parentID,
|
||||
)
|
||||
|
||||
query := base
|
||||
if sort != "" {
|
||||
query = query.Order(sort)
|
||||
}
|
||||
if err := base.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit).Offset(offset)
|
||||
}
|
||||
if err := query.Find(&rows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
func folderParentScope(tx *gorm.DB, parentID []byte) *gorm.DB {
|
||||
if len(parentID) == 0 {
|
||||
return tx.Where("parent_id IS NULL")
|
||||
}
|
||||
return tx.Where("parent_id = ?", parentID)
|
||||
}
|
||||
|
||||
func deletedNameSlot(id []byte) string {
|
||||
if len(id) == 16 {
|
||||
if v, err := uuidv7.BytesToString(id); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
if v, err := uuidv7.BytesToString(uuidv7.MustBytes()); err == nil {
|
||||
return v
|
||||
}
|
||||
return "deleted"
|
||||
}
|
||||
|
||||
func folderRepoIsDescendantFolder(tx *gorm.DB, candidateID []byte, ancestorID []byte) (bool, error) {
|
||||
currentID := append([]byte(nil), candidateID...)
|
||||
for len(currentID) != 0 {
|
||||
if folderRepoBytesEqual16(currentID, ancestorID) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var current filemanager.Folder
|
||||
err := tx.Model(&filemanager.Folder{}).
|
||||
Where("id = ? AND deleted_at IS NULL", currentID).
|
||||
First(¤t).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(current.ParentID) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
currentID = append([]byte(nil), current.ParentID...)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func folderRepoRewriteDescendantTree(tx *gorm.DB, rootID []byte, oldPrefix string, newPrefix string, depthDelta int, updatedBy []byte) error {
|
||||
queue := [][]byte{append([]byte(nil), rootID...)}
|
||||
for len(queue) > 0 {
|
||||
parentID := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
var children []filemanager.Folder
|
||||
if err := tx.Model(&filemanager.Folder{}).
|
||||
Where("parent_id = ?", parentID).
|
||||
Find(&children).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range children {
|
||||
child := children[i]
|
||||
pathCache := child.PathCache
|
||||
if pathCache != "" {
|
||||
pathCache = folderRepoReplacePathPrefix(pathCache, oldPrefix, newPrefix)
|
||||
}
|
||||
depth := child.Depth + depthDelta
|
||||
if depth < 0 {
|
||||
depth = 0
|
||||
}
|
||||
|
||||
if err := tx.Model(&filemanager.Folder{}).
|
||||
Where("id = ?", child.ID).
|
||||
Updates(map[string]any{
|
||||
"path_cache": pathCache,
|
||||
"depth": depth,
|
||||
"updated_by": updatedBy,
|
||||
"updated_at": time.Now().UTC(),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
queue = append(queue, append([]byte(nil), child.ID...))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func folderRepoReplacePathPrefix(pathCache string, oldPrefix string, newPrefix string) string {
|
||||
if oldPrefix == "" {
|
||||
return pathCache
|
||||
}
|
||||
if pathCache == oldPrefix {
|
||||
return newPrefix
|
||||
}
|
||||
prefixWithSlash := oldPrefix + "/"
|
||||
if strings.HasPrefix(pathCache, prefixWithSlash) {
|
||||
return newPrefix + strings.TrimPrefix(pathCache, oldPrefix)
|
||||
}
|
||||
return pathCache
|
||||
}
|
||||
|
||||
func folderRepoBuildPathCache(parentPath, name string) string {
|
||||
parentPath = strings.TrimSpace(parentPath)
|
||||
if parentPath == "" {
|
||||
return "/" + name
|
||||
}
|
||||
parentPath = strings.TrimRight(parentPath, "/")
|
||||
return parentPath + "/" + name
|
||||
}
|
||||
|
||||
func folderRepoBytesEqual16(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user