Files
fm_be/internal/repository/mysql/file_manager_folder_repo.go
2026-07-16 22:16:45 +07:00

461 lines
12 KiB
Go

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(&current).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(&current).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
}