461 lines
12 KiB
Go
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(¤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
|
|
}
|