Skip to content

Commit 6e6b642

Browse files
FIX (restores): Validate PG version via DB and do not allow to restore from lower version
1 parent cf6a88e commit 6e6b642

File tree

7 files changed

+100
-21
lines changed

7 files changed

+100
-21
lines changed

backend/internal/features/databases/databases/postgresql/model.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"log/slog"
88
"postgresus-backend/internal/util/tools"
9+
"regexp"
910
"time"
1011

1112
"github.com/google/uuid"
@@ -93,6 +94,11 @@ func testSingleDatabaseConnection(
9394
}
9495
}()
9596

97+
// Check version after successful connection
98+
if err := verifyDatabaseVersion(ctx, conn, postgresDb.Version); err != nil {
99+
return err
100+
}
101+
96102
// Test if we can perform basic operations (like pg_dump would need)
97103
if err := testBasicOperations(ctx, conn, *postgresDb.Database); err != nil {
98104
return fmt.Errorf(
@@ -105,6 +111,37 @@ func testSingleDatabaseConnection(
105111
return nil
106112
}
107113

114+
// verifyDatabaseVersion checks if the actual database version matches the specified version
115+
func verifyDatabaseVersion(
116+
ctx context.Context,
117+
conn *pgx.Conn,
118+
expectedVersion tools.PostgresqlVersion,
119+
) error {
120+
var versionStr string
121+
err := conn.QueryRow(ctx, "SELECT version()").Scan(&versionStr)
122+
if err != nil {
123+
return fmt.Errorf("failed to query database version: %w", err)
124+
}
125+
126+
// Parse version from string like "PostgreSQL 14.2 on x86_64-pc-linux-gnu..."
127+
re := regexp.MustCompile(`PostgreSQL (\d+)\.`)
128+
matches := re.FindStringSubmatch(versionStr)
129+
if len(matches) < 2 {
130+
return fmt.Errorf("could not parse version from: %s", versionStr)
131+
}
132+
133+
actualVersion := tools.GetPostgresqlVersionEnum(matches[1])
134+
if actualVersion != expectedVersion {
135+
return fmt.Errorf(
136+
"you specified wrong version. Real version is %s, but you specified %s",
137+
actualVersion,
138+
expectedVersion,
139+
)
140+
}
141+
142+
return nil
143+
}
144+
108145
// testBasicOperations tests basic operations that backup tools need
109146
func testBasicOperations(ctx context.Context, conn *pgx.Conn, dbName string) error {
110147
var hasCreatePriv bool

backend/internal/features/notifiers/models/discord/model.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
type DiscordNotifier struct {
16-
NotifierID uuid.UUID `json:"notifierId" gorm:"primaryKey;column:notifier_id"`
16+
NotifierID uuid.UUID `json:"notifierId" gorm:"primaryKey;column:notifier_id"`
1717
ChannelWebhookURL string `json:"channelWebhookUrl" gorm:"not null;column:channel_webhook_url"`
1818
}
1919

backend/internal/features/restores/di.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package restores
33
import (
44
"postgresus-backend/internal/features/backups/backups"
55
backups_config "postgresus-backend/internal/features/backups/config"
6+
"postgresus-backend/internal/features/databases"
67
"postgresus-backend/internal/features/restores/usecases"
78
"postgresus-backend/internal/features/storages"
89
"postgresus-backend/internal/features/users"
@@ -16,6 +17,7 @@ var restoreService = &RestoreService{
1617
storages.GetStorageService(),
1718
backups_config.GetBackupConfigService(),
1819
usecases.GetRestoreBackupUsecase(),
20+
databases.GetDatabaseService(),
1921
logger.GetLogger(),
2022
}
2123
var restoreController = &RestoreController{

backend/internal/features/restores/service.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package restores
22

33
import (
44
"errors"
5+
"fmt"
56
"log/slog"
67
"postgresus-backend/internal/features/backups/backups"
78
backups_config "postgresus-backend/internal/features/backups/config"
@@ -11,6 +12,7 @@ import (
1112
"postgresus-backend/internal/features/restores/usecases"
1213
"postgresus-backend/internal/features/storages"
1314
users_models "postgresus-backend/internal/features/users/models"
15+
"postgresus-backend/internal/util/tools"
1416
"time"
1517

1618
"github.com/google/uuid"
@@ -22,6 +24,7 @@ type RestoreService struct {
2224
storageService *storages.StorageService
2325
backupConfigService *backups_config.BackupConfigService
2426
restoreBackupUsecase *usecases.RestoreBackupUsecase
27+
databaseService *databases.DatabaseService
2528
logger *slog.Logger
2629
}
2730

@@ -76,6 +79,26 @@ func (s *RestoreService) RestoreBackupWithAuth(
7679
return errors.New("user does not have access to this backup")
7780
}
7881

82+
backupDatabase, err := s.databaseService.GetDatabase(user, backup.DatabaseID)
83+
if err != nil {
84+
return err
85+
}
86+
87+
fmt.Printf(
88+
"restore from %s to %s\n",
89+
backupDatabase.Postgresql.Version,
90+
requestDTO.PostgresqlDatabase.Version,
91+
)
92+
93+
if tools.IsBackupDbVersionHigherThanRestoreDbVersion(
94+
backupDatabase.Postgresql.Version,
95+
requestDTO.PostgresqlDatabase.Version,
96+
) {
97+
return errors.New(`backup database version is higher than restore database version. ` +
98+
`Should be restored to the same version as the backup database or higher. ` +
99+
`For example, you can restore PG 15 backup to PG 15, 16 or higher. But cannot restore to 14 and lower`)
100+
}
101+
79102
go func() {
80103
if err := s.RestoreBackup(backup, requestDTO); err != nil {
81104
s.logger.Error("Failed to restore backup", "error", err)

backend/internal/util/tools/enums.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package tools
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
"strconv"
6+
)
47

58
type PostgresqlVersion string
69

@@ -35,3 +38,19 @@ func GetPostgresqlVersionEnum(version string) PostgresqlVersion {
3538
panic(fmt.Sprintf("invalid postgresql version: %s", version))
3639
}
3740
}
41+
42+
func IsBackupDbVersionHigherThanRestoreDbVersion(
43+
backupDbVersion, restoreDbVersion PostgresqlVersion,
44+
) bool {
45+
backupDbVersionInt, err := strconv.Atoi(string(backupDbVersion))
46+
if err != nil {
47+
return false
48+
}
49+
50+
restoreDbVersionInt, err := strconv.Atoi(string(restoreDbVersion))
51+
if err != nil {
52+
return false
53+
}
54+
55+
return backupDbVersionInt > restoreDbVersionInt
56+
}

frontend/src/features/databases/ui/edit/EditDatabaseSpecificDataComponent.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ interface Props {
2626

2727
isShowDbVersionHint?: boolean;
2828
isShowDbName?: boolean;
29-
isBlockDbName?: boolean;
3029
}
3130

3231
export const EditDatabaseSpecificDataComponent = ({
@@ -44,8 +43,6 @@ export const EditDatabaseSpecificDataComponent = ({
4443

4544
isShowDbVersionHint = true,
4645
isShowDbName = true,
47-
48-
isBlockDbName = false,
4946
}: Props) => {
5047
const [editingDatabase, setEditingDatabase] = useState<Database>();
5148
const [isSaving, setIsSaving] = useState(false);
@@ -266,7 +263,6 @@ export const EditDatabaseSpecificDataComponent = ({
266263
size="small"
267264
className="max-w-[200px] grow"
268265
placeholder="Enter PG database name (optional)"
269-
disabled={isBlockDbName}
270266
/>
271267
</div>
272268
)}

frontend/src/features/restores/ui/RestoresComponent.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ export const RestoresComponent = ({ database, backup }: Props) => {
8888
return (
8989
<>
9090
<div className="my-5 text-sm">
91-
Enter info of the database we will restore backup to. During the restore,{' '}
92-
<u>all the current data will be cleared</u>
91+
Enter info of the database we will restore backup to.{' '}
92+
<u>The empty database for restore should be created before the restore</u>. During the
93+
restore, all the current data will be cleared
9394
<br />
9495
<br />
9596
Make sure the database is not used right now (most likely you do not want to restore the
@@ -108,7 +109,6 @@ export const RestoresComponent = ({ database, backup }: Props) => {
108109
restore(database);
109110
}}
110111
isShowDbVersionHint={false}
111-
isBlockDbName
112112
/>
113113
</>
114114
);
@@ -203,20 +203,22 @@ export const RestoresComponent = ({ database, backup }: Props) => {
203203
</div>
204204
</div>
205205

206-
<div className="flex">
207-
<div className="w-[75px] min-w-[75px]">Duration</div>
208-
<div>
209-
<div>{duration}</div>
210-
<div className="mt-2 text-xs text-gray-500">
211-
Expected restoration time usually 3x-5x longer than the backup duration
212-
(sometimes less, sometimes more depending on data type)
213-
<br />
214-
<br />
215-
So it is expected to take up to {expectedRestoreDuration} (usually
216-
significantly faster)
206+
{restore.status === RestoreStatus.IN_PROGRESS && (
207+
<div className="flex">
208+
<div className="w-[75px] min-w-[75px]">Duration</div>
209+
<div>
210+
<div>{duration}</div>
211+
<div className="mt-2 text-xs text-gray-500">
212+
Expected restoration time usually 3x-5x longer than the backup duration
213+
(sometimes less, sometimes more depending on data type)
214+
<br />
215+
<br />
216+
So it is expected to take up to {expectedRestoreDuration} (usually
217+
significantly faster)
218+
</div>
217219
</div>
218220
</div>
219-
</div>
221+
)}
220222
</div>
221223
);
222224
})}

0 commit comments

Comments
 (0)