init push
This commit is contained in:
492
internal/repository/mysql/file_manager_file_repo.go
Normal file
492
internal/repository/mysql/file_manager_file_repo.go
Normal file
@@ -0,0 +1,492 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
filemanager "wucher/internal/domain/file_manager"
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type FileManagerFileRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewFileManagerFileRepository(db *gorm.DB) *FileManagerFileRepository {
|
||||
return &FileManagerFileRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) Create(ctx context.Context, row *filemanager.File) error {
|
||||
return r.db.WithContext(ctx).Create(row).Error
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) Update(ctx context.Context, row *filemanager.File) error {
|
||||
return r.db.WithContext(ctx).Save(row).Error
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) TransitionStatus(ctx context.Context, params filemanager.FileStatusTransitionParams) error {
|
||||
expectedFrom := filemanager.NormalizeFileStatus(params.ExpectedFrom)
|
||||
toStatus := filemanager.NormalizeFileStatus(params.To)
|
||||
if expectedFrom == "" || toStatus == "" {
|
||||
return filemanager.ErrInvalidFileStatusTransition
|
||||
}
|
||||
if !filemanager.CanTransitionFileStatus(expectedFrom, toStatus) {
|
||||
return filemanager.ErrInvalidFileStatusTransition
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
updates := map[string]any{
|
||||
"status": toStatus,
|
||||
"updated_by": params.UpdatedBy,
|
||||
}
|
||||
switch toStatus {
|
||||
case filemanager.FileStatusProcessing:
|
||||
startedAt := now
|
||||
if params.ProcessingStartedAt != nil {
|
||||
startedAt = params.ProcessingStartedAt.UTC()
|
||||
}
|
||||
updates["processing_started_at"] = startedAt
|
||||
updates["processed_at"] = nil
|
||||
updates["validated_at"] = nil
|
||||
updates["failed_at"] = nil
|
||||
updates["failure_reason"] = nil
|
||||
updates["upload_error"] = ""
|
||||
case filemanager.FileStatusValidated:
|
||||
processedAt := now
|
||||
if params.ProcessedAt != nil {
|
||||
processedAt = params.ProcessedAt.UTC()
|
||||
}
|
||||
validatedAt := processedAt
|
||||
if params.ValidatedAt != nil {
|
||||
validatedAt = params.ValidatedAt.UTC()
|
||||
}
|
||||
updates["processed_at"] = processedAt
|
||||
updates["validated_at"] = validatedAt
|
||||
updates["failed_at"] = nil
|
||||
updates["failure_reason"] = nil
|
||||
updates["upload_error"] = ""
|
||||
case filemanager.FileStatusFailed:
|
||||
processedAt := now
|
||||
if params.ProcessedAt != nil {
|
||||
processedAt = params.ProcessedAt.UTC()
|
||||
}
|
||||
failedAt := now
|
||||
if params.FailedAt != nil {
|
||||
failedAt = params.FailedAt.UTC()
|
||||
}
|
||||
reason := "file processing failed"
|
||||
if params.FailureReason != nil {
|
||||
clean := strings.TrimSpace(*params.FailureReason)
|
||||
if clean != "" {
|
||||
reason = clean
|
||||
}
|
||||
}
|
||||
updates["processed_at"] = processedAt
|
||||
updates["failed_at"] = failedAt
|
||||
updates["validated_at"] = nil
|
||||
updates["failure_reason"] = reason
|
||||
updates["upload_error"] = reason
|
||||
}
|
||||
|
||||
res := r.db.WithContext(ctx).
|
||||
Model(&filemanager.File{}).
|
||||
Where("id = ? AND deleted_at IS NULL AND status = ?", params.ID, expectedFrom).
|
||||
Updates(updates)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) Finalize(ctx context.Context, params filemanager.FileFinalizeParams) error {
|
||||
expectedFrom := filemanager.NormalizeFileStatus(params.ExpectedFrom)
|
||||
if expectedFrom == "" {
|
||||
expectedFrom = filemanager.FileStatusValidated
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
"folder_id": params.FolderID,
|
||||
"name": params.Name,
|
||||
"name_normalized": params.NameNormalized,
|
||||
"extension": params.Extension,
|
||||
"status": filemanager.FileStatusReady,
|
||||
"name_slot": "live",
|
||||
"upload_error": "",
|
||||
"failure_reason": nil,
|
||||
"updated_by": params.UpdatedBy,
|
||||
}
|
||||
|
||||
res := r.db.WithContext(ctx).
|
||||
Model(&filemanager.File{}).
|
||||
Where("id = ? AND deleted_at IS NULL AND status = ?", params.ID, expectedFrom).
|
||||
Updates(updates)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) Delete(ctx context.Context, id []byte, deletedBy []byte, purgeAt *time.Time) error {
|
||||
now := time.Now().UTC()
|
||||
nameSlot := deletedNameSlot(id)
|
||||
updates := map[string]any{
|
||||
"status": filemanager.FileStatusTrashed,
|
||||
"trashed_at": now,
|
||||
"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.File{}).
|
||||
Where("id = ? AND deleted_at IS NULL AND status = ?", id, filemanager.FileStatusReady).
|
||||
Updates(updates)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) Restore(ctx context.Context, id []byte, restoredBy []byte) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var row filemanager.File
|
||||
if err := tx.Model(&filemanager.File{}).Where("id = ?", id).First(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if row.DeletedAt == nil {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := tx.Model(&filemanager.File{}).
|
||||
Where("folder_id = ? AND name_normalized = ? AND name_slot = 'live' AND deleted_at IS NULL", row.FolderID, row.NameNormalized).
|
||||
Count(&total).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if total > 0 {
|
||||
return gorm.ErrDuplicatedKey
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
"status": filemanager.FileStatusReady,
|
||||
"trashed_at": nil,
|
||||
"deleted_at": nil,
|
||||
"deleted_by": nil,
|
||||
"purge_at": nil,
|
||||
"updated_by": restoredBy,
|
||||
"name_slot": "live",
|
||||
}
|
||||
res := tx.Model(&filemanager.File{}).
|
||||
Where("id = ? AND deleted_at IS NOT NULL AND status = ?", id, filemanager.FileStatusTrashed).
|
||||
Updates(updates)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) Purge(ctx context.Context, id []byte) error {
|
||||
res := r.db.WithContext(ctx).
|
||||
Where("id = ?", id).
|
||||
Delete(&filemanager.File{})
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) GetByID(ctx context.Context, id []byte) (*filemanager.File, error) {
|
||||
var row filemanager.File
|
||||
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 *FileManagerFileRepository) GetByIDAny(ctx context.Context, id []byte) (*filemanager.File, error) {
|
||||
var row filemanager.File
|
||||
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 *FileManagerFileRepository) GetByObjectKey(ctx context.Context, objectKey string) (*filemanager.File, error) {
|
||||
objectKey = strings.TrimSpace(objectKey)
|
||||
if objectKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var row filemanager.File
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("object_key = ? AND deleted_at IS NULL", objectKey).
|
||||
First(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return &row, err
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) GetByFolderAndName(ctx context.Context, folderID []byte, nameNormalized string) (*filemanager.File, error) {
|
||||
var row filemanager.File
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("folder_id = ? AND name_normalized = ? AND name_slot = 'live' AND deleted_at IS NULL", folderID, nameNormalized).
|
||||
First(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return &row, err
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) GetByTemplateAndTakeover(ctx context.Context, templateUUID, takeoverID []byte) (*filemanager.File, error) {
|
||||
if len(templateUUID) != 16 || len(takeoverID) != 16 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var row filemanager.File
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("template_uuid = ? AND takeover_id = ? AND deleted_at IS NULL", templateUUID, takeoverID).
|
||||
Order("updated_at DESC, created_at DESC, id DESC").
|
||||
First(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return &row, err
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) ListEditedTemplatesByTakeoverID(ctx context.Context, takeoverID []byte) ([]filemanager.File, error) {
|
||||
if len(takeoverID) != 16 {
|
||||
return nil, nil
|
||||
}
|
||||
rows := make([]filemanager.File, 0)
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("takeover_id = ? AND template_uuid IS NOT NULL AND deleted_at IS NULL AND lifecycle_deleted_at IS NULL AND status != ?", takeoverID, filemanager.FileStatusTrashed).
|
||||
Order("created_at ASC, id ASC").
|
||||
Find(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) ListByFolder(ctx context.Context, folderID []byte, sort string, limit, offset int) ([]filemanager.File, int64, error) {
|
||||
var rows []filemanager.File
|
||||
var total int64
|
||||
|
||||
base := r.db.WithContext(ctx).
|
||||
Model(&filemanager.File{}).
|
||||
Where("folder_id = ? AND deleted_at IS NULL AND name_slot = 'live'", folderID)
|
||||
|
||||
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 *FileManagerFileRepository) SearchByName(ctx context.Context, name string, sort string, limit, offset int) ([]filemanager.File, int64, error) {
|
||||
var rows []filemanager.File
|
||||
var total int64
|
||||
|
||||
base := r.db.WithContext(ctx).
|
||||
Model(&filemanager.File{}).
|
||||
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 *FileManagerFileRepository) ListTrashByFolder(ctx context.Context, folderID []byte, sort string, limit, offset int) ([]filemanager.File, int64, error) {
|
||||
var rows []filemanager.File
|
||||
var total int64
|
||||
|
||||
base := r.db.WithContext(ctx).
|
||||
Model(&filemanager.File{}).
|
||||
Where("deleted_at IS NOT NULL")
|
||||
if len(folderID) != 0 {
|
||||
base = base.Where("folder_id = ?", folderID)
|
||||
}
|
||||
|
||||
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 *FileManagerFileRepository) UpdateLifecycle(ctx context.Context, params filemanager.FileLifecycleTransitionParams) error {
|
||||
status := filemanager.NormalizeFileLifecycleStatus(params.Status)
|
||||
if !filemanager.IsKnownFileLifecycleStatus(status) {
|
||||
return fmt.Errorf("invalid file lifecycle status: %s", params.Status)
|
||||
}
|
||||
updates := map[string]any{
|
||||
"lifecycle_status": status,
|
||||
"updated_by": params.UpdatedBy,
|
||||
}
|
||||
if params.AttachedAt != nil {
|
||||
updates["attached_at"] = params.AttachedAt.UTC()
|
||||
}
|
||||
if params.OrphanedAt != nil {
|
||||
updates["orphaned_at"] = params.OrphanedAt.UTC()
|
||||
}
|
||||
if params.ClearOrphanedAt {
|
||||
updates["orphaned_at"] = nil
|
||||
}
|
||||
if params.LifecycleDelete != nil {
|
||||
updates["lifecycle_deleted_at"] = params.LifecycleDelete.UTC()
|
||||
}
|
||||
res := r.db.WithContext(ctx).
|
||||
Model(&filemanager.File{}).
|
||||
Where("id = ?", params.ID).
|
||||
Updates(updates)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) ListFilesByLifecycle(ctx context.Context, filter filemanager.FileLifecycleListFilter) ([]filemanager.File, error) {
|
||||
status := filemanager.NormalizeFileLifecycleStatus(filter.Status)
|
||||
if !filemanager.IsKnownFileLifecycleStatus(status) {
|
||||
return []filemanager.File{}, nil
|
||||
}
|
||||
query := r.db.WithContext(ctx).
|
||||
Model(&filemanager.File{}).
|
||||
Where("lifecycle_status = ?", status)
|
||||
if filter.OrphanedBeforeEq != nil {
|
||||
query = query.Where("orphaned_at IS NOT NULL AND orphaned_at <= ?", filter.OrphanedBeforeEq.UTC())
|
||||
}
|
||||
if filter.CreatedBeforeEq != nil {
|
||||
query = query.Where("created_at <= ?", filter.CreatedBeforeEq.UTC())
|
||||
}
|
||||
if filter.ExcludeInTrash {
|
||||
query = query.Where("deleted_at IS NULL")
|
||||
}
|
||||
if filter.ExcludeLifecycleDeleted {
|
||||
query = query.Where("lifecycle_deleted_at IS NULL")
|
||||
}
|
||||
if filter.Limit > 0 {
|
||||
query = query.Limit(filter.Limit)
|
||||
}
|
||||
rows := make([]filemanager.File, 0)
|
||||
if err := query.Order("orphaned_at ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (r *FileManagerFileRepository) IsFileReferenced(ctx context.Context, fileID []byte) (bool, error) {
|
||||
if len(fileID) != 16 {
|
||||
return false, nil
|
||||
}
|
||||
var total int64
|
||||
queries := []struct {
|
||||
sql string
|
||||
args []any
|
||||
}{
|
||||
{sql: "SELECT COUNT(1) FROM attachments WHERE file_id = ?", args: []any{fileID}},
|
||||
{sql: "SELECT COUNT(1) FROM users u JOIN attachments a ON a.id = u.image_attachment_id WHERE a.file_id = ?", args: []any{fileID}},
|
||||
{sql: "SELECT COUNT(1) FROM bases b JOIN attachments a ON a.id = b.foto_attachment_id WHERE a.file_id = ? AND b.deleted_at IS NULL", args: []any{fileID}},
|
||||
{sql: "SELECT COUNT(1) FROM helicopters h JOIN attachments a ON a.id = h.foto_attachment_id WHERE a.file_id = ?", args: []any{fileID}},
|
||||
{sql: "SELECT COUNT(1) FROM helicopter_files hf JOIN attachments a ON a.id = hf.file_attachment_id WHERE a.file_id = ?", args: []any{fileID}},
|
||||
{sql: "SELECT COUNT(1) FROM hems_operational_files hof JOIN attachments a ON a.id = hof.file_attachment_id JOIN hems_operational_data hod ON hod.id = hof.operational_data_id WHERE a.file_id = ? AND hod.deleted_at IS NULL", args: []any{fileID}},
|
||||
{sql: "SELECT COUNT(1) FROM master_settings ms JOIN file_files f ON f.id = ms.logo_file_id WHERE f.id = ? AND ms.deleted_at IS NULL", args: []any{fileID}},
|
||||
{sql: "SELECT COUNT(1) FROM master_settings ms JOIN file_files f ON f.id = ms.cover_file_id WHERE f.id = ? AND ms.deleted_at IS NULL", args: []any{fileID}},
|
||||
}
|
||||
for i := range queries {
|
||||
total = 0
|
||||
if err := r.db.WithContext(ctx).Raw(queries[i].sql, queries[i].args...).Scan(&total).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
if total > 0 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
fileUUID, err := uuidv7.BytesToString(fileID)
|
||||
if err == nil && strings.TrimSpace(fileUUID) != "" {
|
||||
legacyQueries := []string{
|
||||
"SELECT COUNT(1) FROM pilot_profiles WHERE TRIM(photo_file_id) = ?",
|
||||
"SELECT COUNT(1) FROM doctor_profiles WHERE TRIM(photo_file_id) = ?",
|
||||
"SELECT COUNT(1) FROM air_rescuer_profiles WHERE TRIM(photo_file_id) = ?",
|
||||
"SELECT COUNT(1) FROM technician_profiles WHERE TRIM(photo_file_id) = ?",
|
||||
"SELECT COUNT(1) FROM flight_assistant_profiles WHERE TRIM(photo_file_id) = ?",
|
||||
"SELECT COUNT(1) FROM staff_profiles WHERE TRIM(photo_file_id) = ?",
|
||||
}
|
||||
for i := range legacyQueries {
|
||||
total = 0
|
||||
if err := r.db.WithContext(ctx).Raw(legacyQueries[i], fileUUID).Scan(&total).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
if total > 0 {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user