feat: update button (download, verify, stage)
This commit is contained in:
1 parent
834493a4f5
commit
70460e4170
41 files changed
+5345
-97
No files matched your search
+63
-15
@@ -14,9 +14,12 @@ import (
|
||||
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
||||
"RenewCMS/internal/infrastructure/update"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
@@ -56,21 +59,32 @@ type UseCases struct {
|
||||
GetSettingsUseCase *useCases.GetSettingsUseCase
|
||||
GetUpdateStatusUseCase *useCases.GetUpdateStatusUseCase
|
||||
CheckForUpdateUseCase *useCases.CheckForUpdateUseCase
|
||||
StartUpdateDownloadUseCase *useCases.StartUpdateDownloadUseCase
|
||||
}
|
||||
|
||||
type Repositories struct {
|
||||
ArticleRepo domainArticle.Repository
|
||||
UserRepo domainUser.Repository
|
||||
ImageRepo domainImage.Repository
|
||||
InviteRepo domainInvite.Repository
|
||||
MailRepo domainMail.Repository
|
||||
ReleaseFeed domainUpdate.ReleaseFeed
|
||||
UpdateStore domainUpdate.StatusStore
|
||||
ArticleRepo domainArticle.Repository
|
||||
UserRepo domainUser.Repository
|
||||
ImageRepo domainImage.Repository
|
||||
InviteRepo domainInvite.Repository
|
||||
MailRepo domainMail.Repository
|
||||
ReleaseFeed domainUpdate.ReleaseFeed
|
||||
UpdateStore domainUpdate.StatusStore
|
||||
UpdateJobs domainUpdate.JobStore
|
||||
UpdateHelper domainUpdate.HelperDetector
|
||||
AssetFetcher domainUpdate.AssetFetcher
|
||||
SignatureVerifier domainUpdate.SignatureVerifier
|
||||
StagedRunner domainUpdate.StagedBinaryRunner
|
||||
DatabaseBackup domainUpdate.DatabaseBackup
|
||||
}
|
||||
|
||||
const (
|
||||
sqliteBusyTimeoutMs = 5000
|
||||
updateStatusFileName = "update-status.json"
|
||||
updateJobFileName = "update-job.json"
|
||||
updateHelperFileName = "update-helper.json"
|
||||
updatesDirName = "updates"
|
||||
backupsDirName = "backups"
|
||||
)
|
||||
|
||||
func SQLiteDSN(dbName string) string {
|
||||
@@ -101,15 +115,30 @@ func getDb(dbName string, uploadDir string) *gorm.DB {
|
||||
return db
|
||||
}
|
||||
|
||||
func releasePublicKey() ed25519.PublicKey {
|
||||
key, err := update.EmbeddedPublicKey()
|
||||
if err != nil {
|
||||
panic("Unable to read the embedded release key: " + err.Error())
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func getRepositories(db *gorm.DB, settings config.Settings) *Repositories {
|
||||
dataDir := filepath.Dir(settings.DBFile)
|
||||
return &Repositories{
|
||||
ArticleRepo: persistence.NewArticleRepository(db),
|
||||
UserRepo: persistence.NewUserRepository(db),
|
||||
ImageRepo: persistence.NewImageRepository(db, settings.UploadDir),
|
||||
InviteRepo: persistence.NewInviteRepository(db),
|
||||
MailRepo: mailer.NewMailRepository(settings.Smtp),
|
||||
ReleaseFeed: update.NewGiteaReleaseFeed(settings.UpdateFeedURL),
|
||||
UpdateStore: update.NewJSONStatusStore(filepath.Join(filepath.Dir(settings.DBFile), updateStatusFileName)),
|
||||
ArticleRepo: persistence.NewArticleRepository(db),
|
||||
UserRepo: persistence.NewUserRepository(db),
|
||||
ImageRepo: persistence.NewImageRepository(db, settings.UploadDir),
|
||||
InviteRepo: persistence.NewInviteRepository(db),
|
||||
MailRepo: mailer.NewMailRepository(settings.Smtp),
|
||||
ReleaseFeed: update.NewGiteaReleaseFeed(settings.UpdateFeedURL),
|
||||
UpdateStore: update.NewJSONStatusStore(filepath.Join(dataDir, updateStatusFileName)),
|
||||
UpdateJobs: update.NewJSONJobStore(filepath.Join(dataDir, updateJobFileName)),
|
||||
UpdateHelper: update.NewFileHelperDetector(filepath.Join(dataDir, updateHelperFileName)),
|
||||
AssetFetcher: update.NewHTTPAssetFetcher(settings.UpdateFeedURL),
|
||||
SignatureVerifier: update.NewEd25519SignatureVerifier(releasePublicKey()),
|
||||
StagedRunner: update.NewExecStagedBinaryRunner(),
|
||||
DatabaseBackup: update.NewSQLiteDatabaseBackup(db, filepath.Join(dataDir, backupsDirName), time.Now),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,11 +175,30 @@ func getUseCases(repos *Repositories, settings config.Settings) *UseCases {
|
||||
MergeMediaUseCase: useCases.NewMergeMediaUseCase(repos.ImageRepo, repos.ArticleRepo),
|
||||
GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(repos.ImageRepo, repos.ArticleRepo),
|
||||
GetSettingsUseCase: useCases.NewGetSettingsUseCase(settings.Version),
|
||||
GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(repos.UpdateStore, settings.Version),
|
||||
GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(repos.UpdateStore, repos.UpdateJobs, repos.UpdateHelper, settings.Version),
|
||||
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(repos.ReleaseFeed, repos.UpdateStore, settings.Version, time.Now),
|
||||
StartUpdateDownloadUseCase: startUpdateDownloadUseCase(repos, settings),
|
||||
}
|
||||
}
|
||||
|
||||
func startUpdateDownloadUseCase(repos *Repositories, settings config.Settings) *useCases.StartUpdateDownloadUseCase {
|
||||
return useCases.NewStartUpdateDownloadUseCase(useCases.StartUpdateDownloadDependencies{
|
||||
Store: repos.UpdateStore,
|
||||
Jobs: repos.UpdateJobs,
|
||||
Helper: repos.UpdateHelper,
|
||||
Fetcher: repos.AssetFetcher,
|
||||
Verifier: repos.SignatureVerifier,
|
||||
Runner: repos.StagedRunner,
|
||||
Backup: repos.DatabaseBackup,
|
||||
Staging: update.NewOSStagingArea(filepath.Join(filepath.Dir(settings.DBFile), updatesDirName)),
|
||||
Logger: log.Default(),
|
||||
CurrentVersion: settings.Version,
|
||||
Platform: domainUpdate.Platform{OS: runtime.GOOS, Arch: runtime.GOARCH},
|
||||
FeedURL: settings.UpdateFeedURL,
|
||||
Now: time.Now,
|
||||
})
|
||||
}
|
||||
|
||||
func InitContainer(settings config.Settings) (*UseCases, func() error) {
|
||||
db := getDb(settings.DBFile, settings.UploadDir)
|
||||
repos := getRepositories(db, settings)
|
||||
|
||||
@@ -1,39 +1,100 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"RenewCMS/api/pkg/claims"
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
GetUpdateStatusUseCase *useCases.GetUpdateStatusUseCase
|
||||
CheckForUpdateUseCase *useCases.CheckForUpdateUseCase
|
||||
GetUpdateStatusUseCase *useCases.GetUpdateStatusUseCase
|
||||
CheckForUpdateUseCase *useCases.CheckForUpdateUseCase
|
||||
StartUpdateDownloadUseCase *useCases.StartUpdateDownloadUseCase
|
||||
GetUserUseCase *useCases.GetUserUseCase
|
||||
}
|
||||
|
||||
func NewHandler(getStatus *useCases.GetUpdateStatusUseCase, checkForUpdate *useCases.CheckForUpdateUseCase) *Handler {
|
||||
return &Handler{GetUpdateStatusUseCase: getStatus, CheckForUpdateUseCase: checkForUpdate}
|
||||
func NewHandler(getStatus *useCases.GetUpdateStatusUseCase, checkForUpdate *useCases.CheckForUpdateUseCase, startDownload *useCases.StartUpdateDownloadUseCase, getUser *useCases.GetUserUseCase) *Handler {
|
||||
return &Handler{
|
||||
GetUpdateStatusUseCase: getStatus,
|
||||
CheckForUpdateUseCase: checkForUpdate,
|
||||
StartUpdateDownloadUseCase: startDownload,
|
||||
GetUserUseCase: getUser,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) GetStatus(w http.ResponseWriter, _ *http.Request) {
|
||||
writeStatus(w, h.GetUpdateStatusUseCase.GetStatus())
|
||||
func (h *Handler) GetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
isOwner, ok := h.isOwner(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
writeStatus(w, http.StatusOK, h.GetUpdateStatusUseCase.GetStatus(isOwner))
|
||||
}
|
||||
|
||||
func (h *Handler) Check(w http.ResponseWriter, _ *http.Request) {
|
||||
status, err := h.CheckForUpdateUseCase.Check()
|
||||
if err != nil {
|
||||
func (h *Handler) Check(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := h.CheckForUpdateUseCase.Check(); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeStatus(w, status)
|
||||
isOwner, ok := h.isOwner(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
writeStatus(w, http.StatusOK, h.GetUpdateStatusUseCase.GetStatus(isOwner))
|
||||
}
|
||||
|
||||
func writeStatus(w http.ResponseWriter, status domain.UpdateStatus) {
|
||||
func (h *Handler) Download(w http.ResponseWriter, _ *http.Request) {
|
||||
status, err := h.StartUpdateDownloadUseCase.Start()
|
||||
if err != nil {
|
||||
writeDownloadError(w, err)
|
||||
return
|
||||
}
|
||||
writeStatus(w, http.StatusAccepted, status)
|
||||
}
|
||||
|
||||
func (h *Handler) isOwner(w http.ResponseWriter, r *http.Request) (bool, bool) {
|
||||
userId, ok := claims.UserId(r)
|
||||
if !ok {
|
||||
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
|
||||
return false, false
|
||||
}
|
||||
currentUser, err := h.GetUserUseCase.GetUser(userId)
|
||||
if err != nil {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
|
||||
return false, false
|
||||
}
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return false, false
|
||||
}
|
||||
return currentUser.Role == user.RoleOwner, true
|
||||
}
|
||||
|
||||
func writeDownloadError(w http.ResponseWriter, err error) {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrNotPrepared):
|
||||
http.Error(w, apperr.UpdateNotPreparedErrorMessage, http.StatusConflict)
|
||||
case errors.Is(err, domain.ErrJobInProgress):
|
||||
http.Error(w, apperr.UpdateAlreadyRunningErrorMessage, http.StatusConflict)
|
||||
case errors.Is(err, domain.ErrNothingToUpdate):
|
||||
http.Error(w, apperr.NothingToUpdateErrorMessage, http.StatusBadRequest)
|
||||
case errors.Is(err, domain.ErrPreflightRefused):
|
||||
http.Error(w, apperr.UpdateRefusedErrorMessage, http.StatusUnprocessableEntity)
|
||||
default:
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func writeStatus(w http.ResponseWriter, code int, status domain.UpdateStatus) {
|
||||
body, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
@@ -29,3 +29,7 @@ const MediaQueryErrorMessage = "The media query is invalid."
|
||||
const MediaFilenameErrorMessage = "Enter a filename."
|
||||
const MediaMergeConflictErrorMessage = "Some of the images to merge changed. Refresh and try again."
|
||||
const MediaMergeRequestErrorMessage = "Choose an image to keep and at least one other image to merge into it."
|
||||
const UpdateNotPreparedErrorMessage = "This server cannot update itself yet."
|
||||
const UpdateAlreadyRunningErrorMessage = "An update is already running or waiting to be installed."
|
||||
const NothingToUpdateErrorMessage = "There is nothing to update."
|
||||
const UpdateRefusedErrorMessage = "This update cannot be downloaded."
|
||||
+10
-3
@@ -132,14 +132,16 @@ func NewSettingsRouter(container *UseCases) http.Handler {
|
||||
}
|
||||
|
||||
const (
|
||||
updateCheckMaxAttempts = 5
|
||||
updateCheckWindow = time.Minute
|
||||
updateCheckMaxAttempts = 5
|
||||
updateCheckWindow = time.Minute
|
||||
updateDownloadMaxAttempts = 5
|
||||
updateDownloadWindow = time.Minute
|
||||
)
|
||||
|
||||
func NewSystemRouter(container *UseCases) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
updateHandler := update.NewHandler(container.GetUpdateStatusUseCase, container.CheckForUpdateUseCase)
|
||||
updateHandler := update.NewHandler(container.GetUpdateStatusUseCase, container.CheckForUpdateUseCase, container.StartUpdateDownloadUseCase, container.GetUserUseCase)
|
||||
|
||||
r.Use(jwtauth.Verifier(container.Token))
|
||||
r.Use(jwtauth.Authenticator(container.Token))
|
||||
@@ -151,6 +153,11 @@ func NewSystemRouter(container *UseCases) http.Handler {
|
||||
r.Use(middleware.NewUserRateLimiter(updateCheckMaxAttempts, updateCheckWindow))
|
||||
r.Post("/update/check", updateHandler.Check)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.RequireOwner(container.GetUserUseCase))
|
||||
r.Use(middleware.NewUserRateLimiter(updateDownloadMaxAttempts, updateDownloadWindow))
|
||||
r.Post("/update/download", updateHandler.Download)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user