refac: decouple persistence and image storage behind driver registries
This commit is contained in:
1 parent
421cfafec3
commit
81e299f09a
46 files changed
+1036
-374
No files matched your search
@@ -191,6 +191,15 @@ type ImageInput struct {
|
||||
UploadedByID uint32
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Filename string
|
||||
Size int64
|
||||
ContentType string
|
||||
Width int
|
||||
Height int
|
||||
SHA256 string
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
ID uint32 `json:"id"`
|
||||
Path string `json:"path"`
|
||||
|
||||
@@ -6,7 +6,7 @@ type SearchQuery struct {
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
Create(input ImageInput) (Image, error)
|
||||
Create(image Image) (Image, error)
|
||||
Get(id uint32) (Image, error)
|
||||
Delete(id uint32) error
|
||||
List(query SearchQuery) ([]Image, error)
|
||||
@@ -14,4 +14,6 @@ type Repository interface {
|
||||
Rename(id uint32, filename string) (Image, error)
|
||||
ListDuplicates() ([]Image, error)
|
||||
Counts() (Counts, error)
|
||||
ListMissingMetadata() ([]Image, error)
|
||||
UpdateMetadata(id uint32, metadata Metadata) error
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrBlobNotFound = errors.New("stored image content not found")
|
||||
|
||||
const PublicPathPrefix = "/static/uploadedImages/"
|
||||
|
||||
func PublicPath(storedName string) string {
|
||||
return PublicPathPrefix + storedName
|
||||
}
|
||||
|
||||
type Blob struct {
|
||||
Content io.ReadCloser
|
||||
Size int64
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
Put(contentType string, content io.Reader) (string, error)
|
||||
Open(storedName string) (Blob, error)
|
||||
Delete(storedName string) error
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -36,6 +37,10 @@ type Settings struct {
|
||||
Container bool
|
||||
}
|
||||
|
||||
func (s Settings) DataDir() string {
|
||||
return filepath.Dir(s.DBFile)
|
||||
}
|
||||
|
||||
func (s Settings) IsProduction() bool {
|
||||
return s.Environment == "production"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
domainUpdate "RenewCMS/internal/domain/update"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Driver string
|
||||
Source string
|
||||
}
|
||||
|
||||
type Driver interface {
|
||||
Open(source string) (*gorm.DB, error)
|
||||
NewBackup(db *gorm.DB, directory string, now func() time.Time) domainUpdate.DatabaseBackup
|
||||
}
|
||||
|
||||
var drivers = map[string]Driver{
|
||||
DriverSQLite: sqliteDriver{},
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
DB *gorm.DB
|
||||
driver Driver
|
||||
}
|
||||
|
||||
func Connect(config Config) (*Connection, error) {
|
||||
driver, ok := drivers[config.Driver]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported database driver %q", config.Driver)
|
||||
}
|
||||
db, err := driver.Open(config.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Connection{DB: db, driver: driver}, nil
|
||||
}
|
||||
|
||||
func (c *Connection) NewBackup(directory string, now func() time.Time) domainUpdate.DatabaseBackup {
|
||||
return c.driver.NewBackup(c.DB, directory, now)
|
||||
}
|
||||
|
||||
func (c *Connection) Close() error {
|
||||
sqlDb, err := c.DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDb.Close()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Migrate(db *gorm.DB) error {
|
||||
err := db.AutoMigrate(&models.Article{}, &models.Image{}, &models.User{}, &models.Invite{}, &models.APIKey{}, &models.PurgeSettings{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return BackfillUserRoles(db)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
domainUpdate "RenewCMS/internal/domain/update"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const DriverSQLite = "sqlite"
|
||||
|
||||
const sqliteBusyTimeoutMs = 5000
|
||||
|
||||
type sqliteDriver struct{}
|
||||
|
||||
func SQLiteDSN(dbName string) string {
|
||||
return fmt.Sprintf("%s?_pragma=busy_timeout(%d)", dbName, sqliteBusyTimeoutMs)
|
||||
}
|
||||
|
||||
func (sqliteDriver) Open(file string) (*gorm.DB, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(file), os.ModePerm); err != nil {
|
||||
return nil, fmt.Errorf("unable to create necessary subdirectories: %w", err)
|
||||
}
|
||||
return gorm.Open(sqlite.Open(SQLiteDSN(file)), &gorm.Config{})
|
||||
}
|
||||
|
||||
func (sqliteDriver) NewBackup(db *gorm.DB, directory string, now func() time.Time) domainUpdate.DatabaseBackup {
|
||||
return NewSQLiteBackup(db, directory, now)
|
||||
}
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
package update
|
||||
package database
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
@@ -21,19 +21,19 @@ const (
|
||||
backupTimeLayout = "20060102T150405.000000000Z"
|
||||
)
|
||||
|
||||
type SQLiteDatabaseBackup struct {
|
||||
type SQLiteBackup struct {
|
||||
db *gorm.DB
|
||||
directory string
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
var _ domain.DatabaseBackup = &SQLiteDatabaseBackup{}
|
||||
var _ domain.DatabaseBackup = &SQLiteBackup{}
|
||||
|
||||
func NewSQLiteDatabaseBackup(db *gorm.DB, directory string, now func() time.Time) *SQLiteDatabaseBackup {
|
||||
return &SQLiteDatabaseBackup{db: db, directory: directory, now: now}
|
||||
func NewSQLiteBackup(db *gorm.DB, directory string, now func() time.Time) *SQLiteBackup {
|
||||
return &SQLiteBackup{db: db, directory: directory, now: now}
|
||||
}
|
||||
|
||||
func (b *SQLiteDatabaseBackup) Backup() (string, error) {
|
||||
func (b *SQLiteBackup) Backup() (string, error) {
|
||||
if err := os.MkdirAll(b.directory, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -56,7 +56,7 @@ type backupFile struct {
|
||||
modified time.Time
|
||||
}
|
||||
|
||||
func (b *SQLiteDatabaseBackup) prune() error {
|
||||
func (b *SQLiteBackup) prune() error {
|
||||
entries, err := os.ReadDir(b.directory)
|
||||
if err != nil {
|
||||
return err
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package persistence
|
||||
package database
|
||||
|
||||
import (
|
||||
domainUser "RenewCMS/internal/domain/user"
|
||||
@@ -126,7 +126,7 @@ func (a *ArticleRepository) Delete(id uint32) error {
|
||||
|
||||
func (a *ArticleRepository) FindByBodyContaining(filename string) ([]domain.Article, error) {
|
||||
var entityArticles []entity.Article
|
||||
err := a.db.Model(&entity.Article{}).Where("instr(body, ?) > 0", filename).Order("id asc").Find(&entityArticles).Error
|
||||
err := a.db.Model(&entity.Article{}).Where("body LIKE ? ESCAPE '!'", containsPattern(filename)).Order("id asc").Find(&entityArticles).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
stdimage "image"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "golang.org/x/image/webp"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var extensionContentTypes = map[string]string{
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
".svg": "image/svg+xml",
|
||||
}
|
||||
|
||||
func BackfillImageMetadata(db *gorm.DB, uploadDir string) error {
|
||||
var pending []entity.Image
|
||||
err := db.Where("filename = '' OR filename IS NULL").Order("id asc").Find(&pending).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range pending {
|
||||
metadata, ok := readStoredImage(uploadDir, row.Path)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
err := db.Model(&entity.Image{}).Where("id = ?", row.ID).UpdateColumns(metadata).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readStoredImage(uploadDir string, storedPath string) (map[string]any, bool) {
|
||||
fileName := filepath.Base(storedPath)
|
||||
contentType, known := extensionContentTypes[filepath.Ext(fileName)]
|
||||
if !known {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(uploadDir, fileName))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
width, height := 0, 0
|
||||
if contentType != "image/svg+xml" {
|
||||
config, _, err := stdimage.DecodeConfig(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
width, height = config.Width, config.Height
|
||||
}
|
||||
|
||||
checksum := sha256.Sum256(data)
|
||||
return map[string]any{
|
||||
"filename": fileName,
|
||||
"size": int64(len(data)),
|
||||
"content_type": contentType,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"sha256": hex.EncodeToString(checksum[:]),
|
||||
}, true
|
||||
}
|
||||
@@ -4,67 +4,24 @@ import (
|
||||
domain "RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/infrastructure/persistence/mappers"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const uploadedImagesURLPrefix = "/static/uploadedImages/"
|
||||
|
||||
type ImageRepository struct {
|
||||
db *gorm.DB
|
||||
uploadDir string
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewImageRepository(db *gorm.DB, uploadDir string) *ImageRepository {
|
||||
return &ImageRepository{db, uploadDir}
|
||||
func NewImageRepository(db *gorm.DB) *ImageRepository {
|
||||
return &ImageRepository{db}
|
||||
}
|
||||
|
||||
var contentTypeExtensions = map[string]string{
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpeg",
|
||||
"image/webp": ".webp",
|
||||
"image/svg+xml": ".svg",
|
||||
}
|
||||
|
||||
func (i ImageRepository) Create(input domain.ImageInput) (domain.Image, error) {
|
||||
fileBytes, err := io.ReadAll(input.Content)
|
||||
if err != nil {
|
||||
return domain.Image{}, err
|
||||
}
|
||||
|
||||
err = os.MkdirAll(i.uploadDir, 0755)
|
||||
if err != nil {
|
||||
return domain.Image{}, err
|
||||
}
|
||||
|
||||
newName := uuid.NewString() + contentTypeExtensions[input.ContentType]
|
||||
finalPath := filepath.Join(i.uploadDir, newName)
|
||||
err = os.WriteFile(finalPath, fileBytes, 0666)
|
||||
if err != nil {
|
||||
return domain.Image{}, err
|
||||
}
|
||||
|
||||
model := mappers.ImageToModel(domain.Image{
|
||||
Path: uploadedImagesURLPrefix + newName,
|
||||
Filename: input.Filename,
|
||||
Size: input.Size,
|
||||
ContentType: input.ContentType,
|
||||
Width: input.Width,
|
||||
Height: input.Height,
|
||||
SHA256: input.SHA256,
|
||||
UploadedByID: input.UploadedByID,
|
||||
})
|
||||
func (i ImageRepository) Create(image domain.Image) (domain.Image, error) {
|
||||
model := mappers.ImageToModel(image)
|
||||
if err := i.db.Create(&model).Error; err != nil {
|
||||
_ = os.Remove(finalPath)
|
||||
return domain.Image{}, err
|
||||
}
|
||||
|
||||
return mappers.ImageToDomain(model), nil
|
||||
}
|
||||
|
||||
@@ -78,24 +35,13 @@ func (i ImageRepository) Get(id uint32) (domain.Image, error) {
|
||||
}
|
||||
|
||||
func (i ImageRepository) Delete(id uint32) error {
|
||||
var model entity.Image
|
||||
err := i.db.First(&model, id).Error
|
||||
if err != nil {
|
||||
return mapNotFoundErr(err, domain.ErrImageNotFound)
|
||||
}
|
||||
|
||||
err = os.Remove(filepath.Join(i.uploadDir, filepath.Base(model.Path)))
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
return i.db.Delete(&entity.Image{}, id).Error
|
||||
return deleteOrNotFound(i.db.Delete(&entity.Image{}, id), domain.ErrImageNotFound)
|
||||
}
|
||||
|
||||
func (i ImageRepository) List(query domain.SearchQuery) ([]domain.Image, error) {
|
||||
statement := i.db.Model(&entity.Image{})
|
||||
if query.Search != "" {
|
||||
statement = statement.Where("instr(lower(filename), ?) > 0", strings.ToLower(query.Search))
|
||||
statement = statement.Where("LOWER(filename) LIKE ? ESCAPE '!'", containsPattern(strings.ToLower(query.Search)))
|
||||
}
|
||||
if query.Hash != "" {
|
||||
statement = statement.Where("sha256 = ?", query.Hash)
|
||||
@@ -171,4 +117,24 @@ func imagesToDomain(models []entity.Image) []domain.Image {
|
||||
return images
|
||||
}
|
||||
|
||||
func (i ImageRepository) ListMissingMetadata() ([]domain.Image, error) {
|
||||
var models []entity.Image
|
||||
err := i.db.Where("filename = '' OR filename IS NULL").Order("id asc").Find(&models).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return imagesToDomain(models), nil
|
||||
}
|
||||
|
||||
func (i ImageRepository) UpdateMetadata(id uint32, metadata domain.Metadata) error {
|
||||
return i.db.Model(&entity.Image{}).Where("id = ?", id).UpdateColumns(map[string]any{
|
||||
"filename": metadata.Filename,
|
||||
"size": metadata.Size,
|
||||
"content_type": metadata.ContentType,
|
||||
"width": metadata.Width,
|
||||
"height": metadata.Height,
|
||||
"sha256": metadata.SHA256,
|
||||
}).Error
|
||||
}
|
||||
|
||||
var _ domain.Repository = &ImageRepository{}
|
||||
@@ -2,6 +2,7 @@ package persistence
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -22,3 +23,8 @@ func deleteOrNotFound(result *gorm.DB, sentinel error) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsPattern(fragment string) string {
|
||||
escaped := strings.NewReplacer("!", "!!", "%", "!%", "_", "!_").Replace(fragment)
|
||||
return "%" + escaped + "%"
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/image"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var contentTypeExtensions = map[string]string{
|
||||
"image/png": ".png",
|
||||
"image/jpeg": ".jpeg",
|
||||
"image/webp": ".webp",
|
||||
"image/svg+xml": ".svg",
|
||||
}
|
||||
|
||||
type LocalStorage struct {
|
||||
directory string
|
||||
}
|
||||
|
||||
var _ domain.Storage = &LocalStorage{}
|
||||
|
||||
func NewLocalStorage(directory string) *LocalStorage {
|
||||
return &LocalStorage{directory: directory}
|
||||
}
|
||||
|
||||
func (s *LocalStorage) Put(contentType string, content io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.MkdirAll(s.directory, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
storedName := uuid.NewString() + contentTypeExtensions[contentType]
|
||||
if err := os.WriteFile(filepath.Join(s.directory, storedName), data, 0666); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return storedName, nil
|
||||
}
|
||||
|
||||
func (s *LocalStorage) Open(storedName string) (domain.Blob, error) {
|
||||
if !isPlainName(storedName) {
|
||||
return domain.Blob{}, domain.ErrBlobNotFound
|
||||
}
|
||||
file, err := os.Open(filepath.Join(s.directory, storedName))
|
||||
if err != nil {
|
||||
return domain.Blob{}, domain.ErrBlobNotFound
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil || info.IsDir() {
|
||||
_ = file.Close()
|
||||
return domain.Blob{}, domain.ErrBlobNotFound
|
||||
}
|
||||
return domain.Blob{Content: file, Size: info.Size(), ModTime: info.ModTime()}, nil
|
||||
}
|
||||
|
||||
func (s *LocalStorage) Delete(storedName string) error {
|
||||
if !isPlainName(storedName) {
|
||||
return nil
|
||||
}
|
||||
err := os.Remove(filepath.Join(s.directory, storedName))
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isPlainName(name string) bool {
|
||||
return name != "" && name != "." && name != ".." && filepath.Base(name) == name && filepath.ToSlash(name) == name
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/image"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const DriverLocal = "local"
|
||||
|
||||
type Config struct {
|
||||
Driver string
|
||||
Location string
|
||||
}
|
||||
|
||||
func Open(config Config) (domain.Storage, error) {
|
||||
switch config.Driver {
|
||||
case DriverLocal:
|
||||
return NewLocalStorage(config.Location), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported storage driver %q", config.Driver)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/image"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
stdimage "image"
|
||||
"io"
|
||||
"path"
|
||||
|
||||
_ "golang.org/x/image/webp"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
)
|
||||
|
||||
var extensionContentTypes = map[string]string{
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
".svg": "image/svg+xml",
|
||||
}
|
||||
|
||||
type BackfillImageMetadataUseCase struct {
|
||||
imageRepository image.Repository
|
||||
storage image.Storage
|
||||
}
|
||||
|
||||
func NewBackfillImageMetadataUseCase(imageRepository image.Repository, storage image.Storage) *BackfillImageMetadataUseCase {
|
||||
return &BackfillImageMetadataUseCase{imageRepository, storage}
|
||||
}
|
||||
|
||||
func (u *BackfillImageMetadataUseCase) Run() error {
|
||||
pending, err := u.imageRepository.ListMissingMetadata()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range pending {
|
||||
metadata, ok := u.readStoredImage(row.Path)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := u.imageRepository.UpdateMetadata(row.ID, metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *BackfillImageMetadataUseCase) readStoredImage(storedPath string) (image.Metadata, bool) {
|
||||
fileName := image.StoredName(storedPath)
|
||||
contentType, known := extensionContentTypes[path.Ext(fileName)]
|
||||
if !known {
|
||||
return image.Metadata{}, false
|
||||
}
|
||||
|
||||
data, err := u.read(fileName)
|
||||
if err != nil {
|
||||
return image.Metadata{}, false
|
||||
}
|
||||
|
||||
width, height := 0, 0
|
||||
if contentType != "image/svg+xml" {
|
||||
config, _, err := stdimage.DecodeConfig(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return image.Metadata{}, false
|
||||
}
|
||||
width, height = config.Width, config.Height
|
||||
}
|
||||
|
||||
checksum := sha256.Sum256(data)
|
||||
return image.Metadata{
|
||||
Filename: fileName,
|
||||
Size: int64(len(data)),
|
||||
ContentType: contentType,
|
||||
Width: width,
|
||||
Height: height,
|
||||
SHA256: hex.EncodeToString(checksum[:]),
|
||||
}, true
|
||||
}
|
||||
|
||||
func (u *BackfillImageMetadataUseCase) read(storedName string) ([]byte, error) {
|
||||
blob, err := u.storage.Open(storedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer blob.Content.Close()
|
||||
return io.ReadAll(blob.Content)
|
||||
}
|
||||
@@ -24,10 +24,11 @@ var rasterFormats = map[string]string{
|
||||
|
||||
type CreateImageUseCase struct {
|
||||
imageRepository image.Repository
|
||||
storage image.Storage
|
||||
}
|
||||
|
||||
func NewCreateImageUseCase(imageRepository image.Repository) *CreateImageUseCase {
|
||||
return &CreateImageUseCase{imageRepository}
|
||||
func NewCreateImageUseCase(imageRepository image.Repository, storage image.Storage) *CreateImageUseCase {
|
||||
return &CreateImageUseCase{imageRepository, storage}
|
||||
}
|
||||
|
||||
func (g *CreateImageUseCase) CreateImage(input image.ImageInput) (image.Image, error) {
|
||||
@@ -46,8 +47,13 @@ func (g *CreateImageUseCase) CreateImage(input image.ImageInput) (image.Image, e
|
||||
}
|
||||
|
||||
checksum := sha256.Sum256(data)
|
||||
return g.imageRepository.Create(image.ImageInput{
|
||||
Content: bytes.NewReader(data),
|
||||
storedName, err := g.storage.Put(input.ContentType, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return image.Image{}, err
|
||||
}
|
||||
|
||||
created, err := g.imageRepository.Create(image.Image{
|
||||
Path: image.PublicPath(storedName),
|
||||
Filename: image.NormalizeUploadFilename(input.Filename, input.ContentType),
|
||||
Size: int64(len(data)),
|
||||
ContentType: input.ContentType,
|
||||
@@ -56,6 +62,11 @@ func (g *CreateImageUseCase) CreateImage(input image.ImageInput) (image.Image, e
|
||||
SHA256: hex.EncodeToString(checksum[:]),
|
||||
UploadedByID: input.UploadedByID,
|
||||
})
|
||||
if err != nil {
|
||||
_ = g.storage.Delete(storedName)
|
||||
return image.Image{}, err
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func readWithinLimit(content io.Reader) ([]byte, error) {
|
||||
|
||||
@@ -4,12 +4,17 @@ import "RenewCMS/internal/domain/image"
|
||||
|
||||
type DeleteMediaUseCase struct {
|
||||
imageRepository image.Repository
|
||||
remover mediaRemover
|
||||
}
|
||||
|
||||
func NewDeleteMediaUseCase(imageRepository image.Repository) *DeleteMediaUseCase {
|
||||
return &DeleteMediaUseCase{imageRepository}
|
||||
func NewDeleteMediaUseCase(imageRepository image.Repository, storage image.Storage) *DeleteMediaUseCase {
|
||||
return &DeleteMediaUseCase{imageRepository, mediaRemover{imageRepository, storage}}
|
||||
}
|
||||
|
||||
func (u *DeleteMediaUseCase) Delete(id uint32) error {
|
||||
return u.imageRepository.Delete(id)
|
||||
img, err := u.imageRepository.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return u.remover.remove(img)
|
||||
}
|
||||
@@ -18,10 +18,11 @@ type MergeMediaResult struct {
|
||||
type MergeMediaUseCase struct {
|
||||
imageRepository image.Repository
|
||||
articleRepository article.Repository
|
||||
remover mediaRemover
|
||||
}
|
||||
|
||||
func NewMergeMediaUseCase(imageRepository image.Repository, articleRepository article.Repository) *MergeMediaUseCase {
|
||||
return &MergeMediaUseCase{imageRepository, articleRepository}
|
||||
func NewMergeMediaUseCase(imageRepository image.Repository, articleRepository article.Repository, storage image.Storage) *MergeMediaUseCase {
|
||||
return &MergeMediaUseCase{imageRepository, articleRepository, mediaRemover{imageRepository, storage}}
|
||||
}
|
||||
|
||||
type bodyRewrite struct {
|
||||
@@ -53,7 +54,7 @@ func (u *MergeMediaUseCase) Merge(keepId uint32, mergeIds []uint32) (MergeMediaR
|
||||
}
|
||||
|
||||
for _, duplicate := range copies {
|
||||
if err := u.imageRepository.Delete(duplicate.ID); err != nil {
|
||||
if err := u.remover.remove(duplicate); err != nil {
|
||||
return MergeMediaResult{}, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package useCases
|
||||
|
||||
import "RenewCMS/internal/domain/image"
|
||||
|
||||
type OpenMediaUseCase struct {
|
||||
storage image.Storage
|
||||
}
|
||||
|
||||
func NewOpenMediaUseCase(storage image.Storage) *OpenMediaUseCase {
|
||||
return &OpenMediaUseCase{storage}
|
||||
}
|
||||
|
||||
func (u *OpenMediaUseCase) Open(storedName string) (image.Blob, error) {
|
||||
return u.storage.Open(storedName)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package useCases
|
||||
|
||||
import "RenewCMS/internal/domain/image"
|
||||
|
||||
type mediaRemover struct {
|
||||
imageRepository image.Repository
|
||||
storage image.Storage
|
||||
}
|
||||
|
||||
func (r mediaRemover) remove(img image.Image) error {
|
||||
if err := r.storage.Delete(image.StoredName(img.Path)); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.imageRepository.Delete(img.ID)
|
||||
}
|
||||
Reference in new issue
Block a user