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
+30
-45
@@ -13,13 +13,13 @@ import (
|
||||
"RenewCMS/internal/infrastructure/hashing"
|
||||
"RenewCMS/internal/infrastructure/mailer"
|
||||
"RenewCMS/internal/infrastructure/markdown"
|
||||
"RenewCMS/internal/infrastructure/persistence/models"
|
||||
"RenewCMS/internal/infrastructure/persistence/database"
|
||||
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
||||
"RenewCMS/internal/infrastructure/scheduler"
|
||||
"RenewCMS/internal/infrastructure/storage"
|
||||
"RenewCMS/internal/infrastructure/update"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -28,9 +28,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UseCases struct {
|
||||
@@ -60,6 +58,7 @@ type UseCases struct {
|
||||
AcceptInviteUseCase *useCases.AcceptInviteUseCase
|
||||
SendMailUseCase *useCases.SendMailUseCase
|
||||
CreateImageUseCase *useCases.CreateImageUseCase
|
||||
OpenMediaUseCase *useCases.OpenMediaUseCase
|
||||
ListMediaUseCase *useCases.ListMediaUseCase
|
||||
RenameMediaUseCase *useCases.RenameMediaUseCase
|
||||
DeleteMediaUseCase *useCases.DeleteMediaUseCase
|
||||
@@ -103,7 +102,6 @@ type Repositories struct {
|
||||
}
|
||||
|
||||
const (
|
||||
sqliteBusyTimeoutMs = 5000
|
||||
updateStatusFileName = "update-status.json"
|
||||
updateJobFileName = "update-job.json"
|
||||
updateHelperFileName = "update-helper.json"
|
||||
@@ -111,32 +109,23 @@ const (
|
||||
backupsDirName = "backups"
|
||||
)
|
||||
|
||||
func SQLiteDSN(dbName string) string {
|
||||
return fmt.Sprintf("%s?_pragma=busy_timeout(%d)", dbName, sqliteBusyTimeoutMs)
|
||||
func openImageStorage(settings config.Settings) domainImage.Storage {
|
||||
imageStorage, err := storage.Open(storage.Config{Driver: storage.DriverLocal, Location: settings.UploadDir})
|
||||
if err != nil {
|
||||
panic("Unable to open the image storage: " + err.Error())
|
||||
}
|
||||
return imageStorage
|
||||
}
|
||||
|
||||
func getDb(dbName string, uploadDir string) *gorm.DB {
|
||||
if err := os.MkdirAll(filepath.Dir(dbName), os.ModePerm); err != nil {
|
||||
panic("Unable to create necessary subdirectories: " + err.Error())
|
||||
}
|
||||
db, err := gorm.Open(sqlite.Open(SQLiteDSN(dbName)), &gorm.Config{})
|
||||
func openDatabase(settings config.Settings) *database.Connection {
|
||||
connection, err := database.Connect(database.Config{Driver: database.DriverSQLite, Source: settings.DBFile})
|
||||
if err != nil {
|
||||
panic("Unable to open the database: " + err.Error())
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&models.Article{}, &models.Image{}, &models.User{}, &models.Invite{}, &models.APIKey{}, &models.PurgeSettings{}); err != nil {
|
||||
if err := database.Migrate(connection.DB); err != nil {
|
||||
panic("Failed to migrate database: " + err.Error())
|
||||
}
|
||||
|
||||
if err := persistence.BackfillUserRoles(db); err != nil {
|
||||
panic("Failed to backfill user roles: " + err.Error())
|
||||
}
|
||||
|
||||
if err := persistence.BackfillImageMetadata(db, uploadDir); err != nil {
|
||||
panic("Failed to backfill image metadata: " + err.Error())
|
||||
}
|
||||
|
||||
return db
|
||||
return connection
|
||||
}
|
||||
|
||||
func releasePublicKey() ed25519.PublicKey {
|
||||
@@ -147,12 +136,13 @@ func releasePublicKey() ed25519.PublicKey {
|
||||
return key
|
||||
}
|
||||
|
||||
func getRepositories(db *gorm.DB, settings config.Settings) *Repositories {
|
||||
dataDir := filepath.Dir(settings.DBFile)
|
||||
func getRepositories(connection *database.Connection, settings config.Settings) *Repositories {
|
||||
db := connection.DB
|
||||
dataDir := settings.DataDir()
|
||||
return &Repositories{
|
||||
ArticleRepo: persistence.NewArticleRepository(db),
|
||||
UserRepo: persistence.NewUserRepository(db),
|
||||
ImageRepo: persistence.NewImageRepository(db, settings.UploadDir),
|
||||
ImageRepo: persistence.NewImageRepository(db),
|
||||
InviteRepo: persistence.NewInviteRepository(db),
|
||||
APIKeyRepo: persistence.NewAPIKeyRepository(db),
|
||||
PurgeSettingsRepo: persistence.NewPurgeSettingsRepository(db),
|
||||
@@ -164,18 +154,18 @@ func getRepositories(db *gorm.DB, settings config.Settings) *Repositories {
|
||||
AssetFetcher: update.NewHTTPAssetFetcher(settings.UpdateFeedURL),
|
||||
SignatureVerifier: update.NewEd25519SignatureVerifier(releasePublicKey()),
|
||||
StagedRunner: update.NewExecStagedBinaryRunner(),
|
||||
DatabaseBackup: update.NewSQLiteDatabaseBackup(db, filepath.Join(dataDir, backupsDirName), time.Now),
|
||||
DatabaseBackup: connection.NewBackup(filepath.Join(dataDir, backupsDirName), time.Now),
|
||||
UpdateResults: update.NewJSONResultStore(filepath.Join(dataDir, domainUpdate.ResultFileName)),
|
||||
RestartRequests: update.NewJSONRestartRequestStore(filepath.Join(dataDir, domainUpdate.RestartRequestFileName)),
|
||||
RunningInfo: update.NewJSONRunningInfoStore(filepath.Join(dataDir, domainUpdate.RunningInfoFileName)),
|
||||
}
|
||||
}
|
||||
|
||||
func getUseCases(repos *Repositories, settings config.Settings) *UseCases {
|
||||
func getUseCases(repos *Repositories, imageStorage domainImage.Storage, settings config.Settings) *UseCases {
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
renderer := markdown.NewConverter()
|
||||
keyGenerator := hashing.NewSHA256KeyGenerator()
|
||||
mergeMedia := useCases.NewMergeMediaUseCase(repos.ImageRepo, repos.ArticleRepo)
|
||||
mergeMedia := useCases.NewMergeMediaUseCase(repos.ImageRepo, repos.ArticleRepo, imageStorage)
|
||||
|
||||
runPurge := useCases.NewRunPurgeUseCase(repos.ImageRepo, repos.ArticleRepo, mergeMedia, repos.PurgeSettingsRepo, time.Now, log.Default())
|
||||
purgeScheduler := useCases.NewPurgeSchedulerUseCase(repos.PurgeSettingsRepo, runPurge)
|
||||
@@ -204,10 +194,11 @@ func getUseCases(repos *Repositories, settings config.Settings) *UseCases {
|
||||
GetInviteUseCase: useCases.NewGetInviteUseCase(repos.InviteRepo, hasher),
|
||||
AcceptInviteUseCase: useCases.NewAcceptInviteUseCase(repos.UserRepo, repos.InviteRepo, hasher),
|
||||
SendMailUseCase: useCases.NewSendMailUseCase(repos.MailRepo),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(repos.ImageRepo),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(repos.ImageRepo, imageStorage),
|
||||
OpenMediaUseCase: useCases.NewOpenMediaUseCase(imageStorage),
|
||||
ListMediaUseCase: useCases.NewListMediaUseCase(repos.ImageRepo, repos.ArticleRepo, repos.UserRepo),
|
||||
RenameMediaUseCase: useCases.NewRenameMediaUseCase(repos.ImageRepo),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(repos.ImageRepo),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(repos.ImageRepo, imageStorage),
|
||||
MergeMediaUseCase: mergeMedia,
|
||||
GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(repos.ImageRepo, repos.ArticleRepo),
|
||||
GetPurgeSettingsUseCase: useCases.NewGetPurgeSettingsUseCase(repos.PurgeSettingsRepo),
|
||||
@@ -238,7 +229,7 @@ func startUpdateDownloadUseCase(repos *Repositories, settings config.Settings) *
|
||||
Verifier: repos.SignatureVerifier,
|
||||
Runner: repos.StagedRunner,
|
||||
Backup: repos.DatabaseBackup,
|
||||
Staging: update.NewOSStagingArea(filepath.Join(filepath.Dir(settings.DBFile), updatesDirName)),
|
||||
Staging: update.NewOSStagingArea(filepath.Join(settings.DataDir(), updatesDirName)),
|
||||
Logger: log.Default(),
|
||||
CurrentVersion: settings.Version,
|
||||
Platform: domainUpdate.Platform{OS: runtime.GOOS, Arch: runtime.GOARCH},
|
||||
@@ -248,19 +239,13 @@ func startUpdateDownloadUseCase(repos *Repositories, settings config.Settings) *
|
||||
}
|
||||
|
||||
func InitContainer(settings config.Settings) (*UseCases, func() error) {
|
||||
db := getDb(settings.DBFile, settings.UploadDir)
|
||||
repos := getRepositories(db, settings)
|
||||
return getUseCases(repos, settings), closeDb(db)
|
||||
}
|
||||
|
||||
func closeDb(db *gorm.DB) func() error {
|
||||
return func() error {
|
||||
sqlDb, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDb.Close()
|
||||
imageStorage := openImageStorage(settings)
|
||||
connection := openDatabase(settings)
|
||||
repos := getRepositories(connection, settings)
|
||||
if err := useCases.NewBackfillImageMetadataUseCase(repos.ImageRepo, imageStorage).Run(); err != nil {
|
||||
panic("Failed to backfill image metadata: " + err.Error())
|
||||
}
|
||||
return getUseCases(repos, imageStorage, settings), connection.Close
|
||||
}
|
||||
|
||||
type BrowserUpdateCommands struct {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package staticimages
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -24,13 +26,13 @@ var contentTypeExtensions = map[string]string{
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
uploadDir fs.FS
|
||||
openMedia *useCases.OpenMediaUseCase
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func NewHandler(uploadDir string) *Handler {
|
||||
func NewHandler(openMedia *useCases.OpenMediaUseCase) *Handler {
|
||||
return &Handler{
|
||||
uploadDir: os.DirFS(uploadDir),
|
||||
openMedia: openMedia,
|
||||
modTime: time.Now(),
|
||||
}
|
||||
}
|
||||
@@ -39,18 +41,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
cleaned := cleanPath(requestedPath(r))
|
||||
file, stat, ok := h.openFile(cleaned)
|
||||
if !ok {
|
||||
blob, err := h.openMedia.Open(cleaned)
|
||||
if errors.Is(err, image.ErrBlobNotFound) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
content, ok := file.(io.ReadSeeker)
|
||||
if !ok {
|
||||
http.Error(w, "File not seekable", http.StatusInternalServerError)
|
||||
if err != nil {
|
||||
http.Error(w, "Unable to read the file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer blob.Content.Close()
|
||||
|
||||
contentType := contentTypeFor(cleaned)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
@@ -59,7 +59,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Security-Policy", svgContentSecurityPolicy)
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, cleaned, stat.ModTime(), content)
|
||||
if content, ok := blob.Content.(io.ReadSeeker); ok {
|
||||
http.ServeContent(w, r, cleaned, blob.ModTime, content)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(blob.Size, 10))
|
||||
if !blob.ModTime.IsZero() {
|
||||
w.Header().Set("Last-Modified", blob.ModTime.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if r.Method != http.MethodHead {
|
||||
_, _ = io.Copy(w, blob.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func requestedPath(r *http.Request) string {
|
||||
@@ -73,22 +84,6 @@ func cleanPath(p string) string {
|
||||
return strings.TrimPrefix(path.Clean("/"+p), "/")
|
||||
}
|
||||
|
||||
func (h *Handler) openFile(p string) (fs.File, fs.FileInfo, bool) {
|
||||
if p == "" || p == "." {
|
||||
return nil, nil, false
|
||||
}
|
||||
file, err := h.uploadDir.Open(p)
|
||||
if err != nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
stat, err := file.Stat()
|
||||
if err != nil || stat.IsDir() {
|
||||
file.Close()
|
||||
return nil, nil, false
|
||||
}
|
||||
return file, stat, true
|
||||
}
|
||||
|
||||
func contentTypeFor(p string) string {
|
||||
ext := path.Ext(p)
|
||||
if contentType, ok := contentTypeExtensions[ext]; ok {
|
||||
|
||||
+1
-1
@@ -292,7 +292,7 @@ func InitRoutes(container *UseCases, settings config.Settings) *chi.Mux {
|
||||
apiRouter.Use(chimiddleware.GetHead)
|
||||
apiRouter.Get("/healthz", GetHealth)
|
||||
apiRouter.Mount("/v1", backend)
|
||||
apiRouter.Mount("/static/uploadedImages", staticimages.NewHandler(settings.UploadDir))
|
||||
apiRouter.Mount("/static/uploadedImages", staticimages.NewHandler(container.OpenMediaUseCase))
|
||||
apiRouter.Mount("/", frontend)
|
||||
|
||||
return apiRouter
|
||||
|
||||
Reference in new issue
Block a user