280 lines
7.4 KiB
Go
280 lines
7.4 KiB
Go
package services
|
|
|
|
import (
|
|
"cmbone/internal/models"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
imageTaskStatusSucceeded = "succeeded"
|
|
imageTaskStatusCanceled = "canceled"
|
|
|
|
defaultImageOperation = "enhance"
|
|
defaultTaskLimit = 50
|
|
maxTaskLimit = 200
|
|
)
|
|
|
|
var (
|
|
errImageTaskSourcePathRequired = errors.New("source path is required")
|
|
errImageTaskNotFound = errors.New("image task not found")
|
|
errImageTaskCannotCancel = errors.New("image task cannot be canceled")
|
|
)
|
|
|
|
type ImageOptimizationService struct {
|
|
store *SuiStore
|
|
}
|
|
|
|
func NewImageOptimizationService(store *SuiStore) *ImageOptimizationService {
|
|
return &ImageOptimizationService{store: store}
|
|
}
|
|
|
|
func (s *ImageOptimizationService) CreateTask(input models.ImageTaskInput) (models.ImageTaskDetail, error) {
|
|
sourcePath := strings.TrimSpace(input.SourcePath)
|
|
if sourcePath == "" {
|
|
_ = recordAppLog(s.store, appLogLevelWarn, "image_optimization", "create task failed", errImageTaskSourcePathRequired.Error())
|
|
return models.ImageTaskDetail{}, errImageTaskSourcePathRequired
|
|
}
|
|
|
|
operation := strings.TrimSpace(input.Operation)
|
|
if operation == "" {
|
|
operation = defaultImageOperation
|
|
}
|
|
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
beforeSize := fileSizeOrZero(sourcePath)
|
|
afterSize := simulatedAfterSize(beforeSize)
|
|
|
|
tx, err := s.store.DB.Begin()
|
|
if err != nil {
|
|
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "begin task transaction failed", err.Error())
|
|
return models.ImageTaskDetail{}, err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
result, err := tx.Exec(`
|
|
INSERT INTO image_tasks (source_path, status, operation, duration_ms, error_message, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, '', ?, ?)
|
|
`, sourcePath, imageTaskStatusSucceeded, operation, 1200, now, now)
|
|
if err != nil {
|
|
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "insert image task failed", err.Error())
|
|
return models.ImageTaskDetail{}, err
|
|
}
|
|
|
|
taskID, err := result.LastInsertId()
|
|
if err != nil {
|
|
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "read image task id failed", err.Error())
|
|
return models.ImageTaskDetail{}, err
|
|
}
|
|
|
|
outputPath := simulatedOutputPath(sourcePath, operation)
|
|
result, err = tx.Exec(`
|
|
INSERT INTO image_task_results (task_id, output_path, before_size, after_size, created_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`, taskID, outputPath, beforeSize, afterSize, now)
|
|
if err != nil {
|
|
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "insert image task result failed", err.Error())
|
|
return models.ImageTaskDetail{}, err
|
|
}
|
|
|
|
resultID, err := result.LastInsertId()
|
|
if err != nil {
|
|
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "read image task result id failed", err.Error())
|
|
return models.ImageTaskDetail{}, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
_ = recordAppLog(s.store, appLogLevelError, "image_optimization", "commit image task failed", err.Error())
|
|
return models.ImageTaskDetail{}, err
|
|
}
|
|
|
|
detail := models.ImageTaskDetail{
|
|
Task: models.ImageTask{
|
|
ID: taskID,
|
|
SourcePath: sourcePath,
|
|
Status: imageTaskStatusSucceeded,
|
|
Operation: operation,
|
|
DurationMs: 1200,
|
|
ErrorMessage: "",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
Result: models.ImageTaskResult{
|
|
ID: resultID,
|
|
TaskID: taskID,
|
|
OutputPath: outputPath,
|
|
BeforeSize: beforeSize,
|
|
AfterSize: afterSize,
|
|
CreatedAt: now,
|
|
},
|
|
}
|
|
_ = recordAuditLog(s.store, currentAuditActor(s.store), auditActionImageTaskCreate, "image_task", fmt.Sprint(taskID), fmt.Sprintf("source=%s operation=%s", sourcePath, operation))
|
|
return detail, nil
|
|
}
|
|
|
|
func (s *ImageOptimizationService) ListTasks(filter models.ImageTaskFilter) ([]models.ImageTask, error) {
|
|
limit := filter.Limit
|
|
if limit <= 0 {
|
|
limit = defaultTaskLimit
|
|
}
|
|
if limit > maxTaskLimit {
|
|
limit = maxTaskLimit
|
|
}
|
|
offset := filter.Offset
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
|
|
query := strings.Builder{}
|
|
query.WriteString(`
|
|
SELECT id, source_path, status, operation, duration_ms, error_message, created_at, updated_at
|
|
FROM image_tasks
|
|
WHERE 1 = 1
|
|
`)
|
|
|
|
args := make([]any, 0, 4)
|
|
if strings.TrimSpace(filter.Status) != "" {
|
|
query.WriteString(" AND status = ?")
|
|
args = append(args, strings.TrimSpace(filter.Status))
|
|
}
|
|
if strings.TrimSpace(filter.Operation) != "" {
|
|
query.WriteString(" AND operation = ?")
|
|
args = append(args, strings.TrimSpace(filter.Operation))
|
|
}
|
|
query.WriteString(" ORDER BY id DESC LIMIT ? OFFSET ?")
|
|
args = append(args, limit, offset)
|
|
|
|
rows, err := s.store.DB.Query(query.String(), args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
tasks := make([]models.ImageTask, 0)
|
|
for rows.Next() {
|
|
task, err := scanImageTask(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tasks = append(tasks, task)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return tasks, nil
|
|
}
|
|
|
|
func (s *ImageOptimizationService) GetTask(id int64) (models.ImageTaskDetail, error) {
|
|
row := s.store.DB.QueryRow(`
|
|
SELECT
|
|
t.id, t.source_path, t.status, t.operation, t.duration_ms, t.error_message, t.created_at, t.updated_at,
|
|
r.id, r.task_id, r.output_path, r.before_size, r.after_size, r.created_at
|
|
FROM image_tasks t
|
|
LEFT JOIN image_task_results r ON r.task_id = t.id
|
|
WHERE t.id = ?
|
|
ORDER BY r.id DESC
|
|
LIMIT 1
|
|
`, id)
|
|
|
|
detail, err := scanImageTaskDetail(row)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return models.ImageTaskDetail{}, errImageTaskNotFound
|
|
}
|
|
return detail, err
|
|
}
|
|
|
|
func (s *ImageOptimizationService) CancelTask(id int64) error {
|
|
result, err := s.store.DB.Exec(`
|
|
UPDATE image_tasks
|
|
SET status = ?, updated_at = ?
|
|
WHERE id = ? AND status IN ('pending', 'processing')
|
|
`, imageTaskStatusCanceled, time.Now().UTC().Format(time.RFC3339), id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
affected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if affected == 0 {
|
|
return errImageTaskCannotCancel
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type rowScanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanImageTask(scanner rowScanner) (models.ImageTask, error) {
|
|
var task models.ImageTask
|
|
err := scanner.Scan(
|
|
&task.ID,
|
|
&task.SourcePath,
|
|
&task.Status,
|
|
&task.Operation,
|
|
&task.DurationMs,
|
|
&task.ErrorMessage,
|
|
&task.CreatedAt,
|
|
&task.UpdatedAt,
|
|
)
|
|
return task, err
|
|
}
|
|
|
|
func scanImageTaskDetail(scanner rowScanner) (models.ImageTaskDetail, error) {
|
|
var detail models.ImageTaskDetail
|
|
err := scanner.Scan(
|
|
&detail.Task.ID,
|
|
&detail.Task.SourcePath,
|
|
&detail.Task.Status,
|
|
&detail.Task.Operation,
|
|
&detail.Task.DurationMs,
|
|
&detail.Task.ErrorMessage,
|
|
&detail.Task.CreatedAt,
|
|
&detail.Task.UpdatedAt,
|
|
&detail.Result.ID,
|
|
&detail.Result.TaskID,
|
|
&detail.Result.OutputPath,
|
|
&detail.Result.BeforeSize,
|
|
&detail.Result.AfterSize,
|
|
&detail.Result.CreatedAt,
|
|
)
|
|
return detail, err
|
|
}
|
|
|
|
func fileSizeOrZero(path string) int64 {
|
|
info, err := os.Stat(path)
|
|
if err != nil || info.IsDir() {
|
|
return 0
|
|
}
|
|
return info.Size()
|
|
}
|
|
|
|
func simulatedAfterSize(beforeSize int64) int64 {
|
|
if beforeSize <= 0 {
|
|
return 0
|
|
}
|
|
return beforeSize * 82 / 100
|
|
}
|
|
|
|
func simulatedOutputPath(sourcePath string, operation string) string {
|
|
dir := filepath.Dir(sourcePath)
|
|
ext := filepath.Ext(sourcePath)
|
|
name := strings.TrimSuffix(filepath.Base(sourcePath), ext)
|
|
if name == "" || name == "." {
|
|
name = "image"
|
|
}
|
|
if ext == "" {
|
|
ext = ".jpg"
|
|
}
|
|
return filepath.Join(dir, fmt.Sprintf("%s_%s_optimized%s", name, operation, ext))
|
|
}
|