Skip to content

Commit

Permalink
Merge pull request #20 from eduardolat/feat/restore-backups
Browse files Browse the repository at this point in the history
Feat/restore backups
  • Loading branch information
eduardolat authored Aug 5, 2024
2 parents 6688a8c + 5f69043 commit 59da65b
Show file tree
Hide file tree
Showing 29 changed files with 1,045 additions and 9 deletions.
4 changes: 3 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ ENV DEBIAN_FRONTEND=noninteractive

# Install system dependencies
RUN apt-get update && \
apt-get install -y wget git gnupg2 lsb-release && \
apt-get install -y \
wget=1.21.4-1ubuntu4.1 unzip=6.0-28ubuntu4 git=1:2.43.0-1ubuntu7.1 \
gnupg2=2.4.4-2ubuntu17 lsb-release=12.0-2 && \
rm -rf /var/lib/apt/lists/*

# Add PostgreSQL repository and key
Expand Down
4 changes: 3 additions & 1 deletion docker/Dockerfile.cicd
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ ENV DEBIAN_FRONTEND=noninteractive

# Install system dependencies
RUN apt-get update && \
apt-get install -y wget git gnupg2 lsb-release && \
apt-get install -y \
wget=1.21.4-1ubuntu4.1 unzip=6.0-28ubuntu4 git=1:2.43.0-1ubuntu7.1 \
gnupg2=2.4.4-2ubuntu17 lsb-release=12.0-2 && \
rm -rf /var/lib/apt/lists/*

# Add PostgreSQL repository and key
Expand Down
4 changes: 3 additions & 1 deletion docker/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ ENV DEBIAN_FRONTEND=noninteractive

# Install system dependencies
RUN apt-get update && \
apt-get install -y wget git gnupg2 lsb-release && \
apt-get install -y \
wget=1.21.4-1ubuntu4.1 unzip=6.0-28ubuntu4 git=1:2.43.0-1ubuntu7.1 \
gnupg2=2.4.4-2ubuntu17 lsb-release=12.0-2 && \
rm -rf /var/lib/apt/lists/*

# Add PostgreSQL repository and key
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS restorations (
id UUID NOT NULL DEFAULT uuid_generate_v4() PRIMARY KEY,
execution_id UUID NOT NULL REFERENCES executions(id) ON DELETE CASCADE,
database_id UUID REFERENCES databases(id) ON DELETE CASCADE,

status TEXT NOT NULL CHECK (
status IN ('running', 'success', 'failed')
) DEFAULT 'running',
message TEXT,

started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ
);

CREATE TRIGGER restorations_change_updated_at
BEFORE UPDATE ON restorations FOR EACH ROW EXECUTE FUNCTION change_updated_at();

CREATE INDEX IF NOT EXISTS
idx_restorations_execution_id ON restorations(execution_id);
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS restorations;
-- +goose StatementEnd
64 changes: 64 additions & 0 deletions internal/integration/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"bytes"
"fmt"
"io"
"os"
"os/exec"

"github.com/eduardolat/pgbackweb/internal/util/strutil"
"github.com/orsinium-labs/enum"
)

Expand Down Expand Up @@ -195,3 +197,65 @@ func (c *Client) DumpZip(

return reader
}

// RestoreZip downloads or copies the ZIP from the given url or path, unzips it,
// and runs the psql command to restore the database.
//
// The ZIP file must contain a dump.sql file with the SQL dump to restore.
//
// - version: PostgreSQL version to use for the restore
// - connString: connection string to the database
// - isLocal: whether the ZIP file is local or a URL
// - zipURLOrPath: URL or path to the ZIP file
func (Client) RestoreZip(
version PGVersion, connString string, isLocal bool, zipURLOrPath string,
) error {
workDir, err := os.MkdirTemp("", "pbw-restore-*")
if err != nil {
return fmt.Errorf("error creating temp dir: %w", err)
}
defer os.RemoveAll(workDir)
zipPath := strutil.CreatePath(true, workDir, "dump.zip")
dumpPath := strutil.CreatePath(true, workDir, "dump.sql")

if isLocal {
cmd := exec.Command("cp", zipURLOrPath, zipPath)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error copying ZIP file to temp dir: %s", output)
}
}

if !isLocal {
cmd := exec.Command("wget", "--no-verbose", "-O", zipPath, zipURLOrPath)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error downloading ZIP file: %s", output)
}
}

if _, err := os.Stat(zipPath); os.IsNotExist(err) {
return fmt.Errorf("zip file not found: %s", zipPath)
}

cmd := exec.Command("unzip", "-o", zipPath, "dump.sql", "-d", workDir)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error unzipping ZIP file: %s", output)
}

if _, err := os.Stat(dumpPath); os.IsNotExist(err) {
return fmt.Errorf("dump.sql file not found in ZIP file: %s", zipPath)
}

cmd = exec.Command(version.Value.psql, connString, "-f", dumpPath)
output, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf(
"error running psql v%s command: %s",
version.Value.version, output,
)
}

return nil
}
2 changes: 1 addition & 1 deletion internal/service/executions/get_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ import (

func (s *Service) GetExecution(
ctx context.Context, id uuid.UUID,
) (dbgen.Execution, error) {
) (dbgen.ExecutionsServiceGetExecutionRow, error) {
return s.dbgen.ExecutionsServiceGetExecution(ctx, id)
}
10 changes: 8 additions & 2 deletions internal/service/executions/get_execution.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-- name: ExecutionsServiceGetExecution :one
SELECT * FROM executions
WHERE id = @id;
SELECT
executions.*,
databases.id AS database_id,
databases.pg_version AS database_pg_version
FROM executions
INNER JOIN backups ON backups.id = executions.backup_id
INNER JOIN databases ON databases.id = backups.database_id
WHERE executions.id = @id;
1 change: 1 addition & 0 deletions internal/service/executions/paginate_executions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ SELECT
executions.*,
backups.name AS backup_name,
databases.name AS database_name,
databases.pg_version AS database_pg_version,
destinations.name AS destination_name,
backups.is_local AS backup_is_local
FROM executions
Expand Down
13 changes: 13 additions & 0 deletions internal/service/restorations/create_restoration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package restorations

import (
"context"

"github.com/eduardolat/pgbackweb/internal/database/dbgen"
)

func (s *Service) CreateRestoration(
ctx context.Context, params dbgen.RestorationsServiceCreateRestorationParams,
) (dbgen.Restoration, error) {
return s.dbgen.RestorationsServiceCreateRestoration(ctx, params)
}
4 changes: 4 additions & 0 deletions internal/service/restorations/create_restoration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- name: RestorationsServiceCreateRestoration :one
INSERT INTO restorations (execution_id, database_id, status, message)
VALUES (@execution_id, @database_id, @status, @message)
RETURNING *;
7 changes: 7 additions & 0 deletions internal/service/restorations/get_restorations_qty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package restorations

import "context"

func (s *Service) GetRestorationsQty(ctx context.Context) (int64, error) {
return s.dbgen.RestorationsServiceGetRestorationsQty(ctx)
}
2 changes: 2 additions & 0 deletions internal/service/restorations/get_restorations_qty.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- name: RestorationsServiceGetRestorationsQty :one
SELECT COUNT(*) FROM restorations;
54 changes: 54 additions & 0 deletions internal/service/restorations/paginate_restorations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package restorations

import (
"context"

"github.com/eduardolat/pgbackweb/internal/database/dbgen"
"github.com/eduardolat/pgbackweb/internal/util/paginateutil"
"github.com/google/uuid"
)

type PaginateRestorationsParams struct {
Page int
Limit int
ExecutionFilter uuid.NullUUID
DatabaseFilter uuid.NullUUID
}

func (s *Service) PaginateRestorations(
ctx context.Context, params PaginateRestorationsParams,
) (paginateutil.PaginateResponse, []dbgen.RestorationsServicePaginateRestorationsRow, error) {
page := max(params.Page, 1)
limit := min(max(params.Limit, 1), 100)

count, err := s.dbgen.RestorationsServicePaginateRestorationsCount(
ctx, dbgen.RestorationsServicePaginateRestorationsCountParams{
ExecutionID: params.ExecutionFilter,
DatabaseID: params.DatabaseFilter,
},
)
if err != nil {
return paginateutil.PaginateResponse{}, nil, err
}

paginateParams := paginateutil.PaginateParams{
Page: page,
Limit: limit,
}
offset := paginateutil.CreateOffsetFromParams(paginateParams)
paginateResponse := paginateutil.CreatePaginateResponse(paginateParams, int(count))

restorations, err := s.dbgen.RestorationsServicePaginateRestorations(
ctx, dbgen.RestorationsServicePaginateRestorationsParams{
ExecutionID: params.ExecutionFilter,
DatabaseID: params.DatabaseFilter,
Limit: int32(params.Limit),
Offset: int32(offset),
},
)
if err != nil {
return paginateutil.PaginateResponse{}, nil, err
}

return paginateResponse, restorations, nil
}
42 changes: 42 additions & 0 deletions internal/service/restorations/paginate_restorations.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- name: RestorationsServicePaginateRestorationsCount :one
SELECT COUNT(restorations.*)
FROM restorations
INNER JOIN executions ON executions.id = restorations.execution_id
INNER JOIN backups ON backups.id = executions.backup_id
LEFT JOIN databases ON databases.id = restorations.database_id
WHERE
(
sqlc.narg('execution_id')::UUID IS NULL
OR
restorations.execution_id = sqlc.narg('execution_id')::UUID
)
AND
(
sqlc.narg('database_id')::UUID IS NULL
OR
restorations.database_id = sqlc.narg('database_id')::UUID
);

-- name: RestorationsServicePaginateRestorations :many
SELECT
restorations.*,
databases.name AS database_name,
backups.name AS backup_name
FROM restorations
INNER JOIN executions ON executions.id = restorations.execution_id
INNER JOIN backups ON backups.id = executions.backup_id
LEFT JOIN databases ON databases.id = restorations.database_id
WHERE
(
sqlc.narg('execution_id')::UUID IS NULL
OR
restorations.execution_id = sqlc.narg('execution_id')::UUID
)
AND
(
sqlc.narg('database_id')::UUID IS NULL
OR
restorations.database_id = sqlc.narg('database_id')::UUID
)
ORDER BY restorations.started_at DESC
LIMIT sqlc.arg('limit') OFFSET sqlc.arg('offset');
31 changes: 31 additions & 0 deletions internal/service/restorations/restorations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package restorations

import (
"github.com/eduardolat/pgbackweb/internal/database/dbgen"
"github.com/eduardolat/pgbackweb/internal/integration"
"github.com/eduardolat/pgbackweb/internal/service/databases"
"github.com/eduardolat/pgbackweb/internal/service/destinations"
"github.com/eduardolat/pgbackweb/internal/service/executions"
)

type Service struct {
dbgen *dbgen.Queries
ints *integration.Integration
executionsService *executions.Service
databasesService *databases.Service
destinationsService *destinations.Service
}

func New(
dbgen *dbgen.Queries, ints *integration.Integration,
executionsService *executions.Service, databasesService *databases.Service,
destinationsService *destinations.Service,
) *Service {
return &Service{
dbgen: dbgen,
ints: ints,
executionsService: executionsService,
databasesService: databasesService,
destinationsService: destinationsService,
}
}
Loading

0 comments on commit 59da65b

Please sign in to comment.