mirror of
https://github.com/azukaar/Cosmos-Server.git
synced 2026-01-06 04:09:53 -06:00
[release] v0.18.0-unstable91
This commit is contained in:
@@ -8,6 +8,7 @@ interface BackupConfig {
|
||||
crontab?: string;
|
||||
tags?: string[];
|
||||
exclude?: string[];
|
||||
autoStopContainers?: boolean;
|
||||
}
|
||||
|
||||
interface RestoreConfig {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { PlusCircleOutlined } from "@ant-design/icons";
|
||||
import { crontabToText } from "../../utils/indexs";
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { FilePickerButton } from '../../components/filePicker';
|
||||
import { CosmosInputText } from "../config/users/formShortcuts";
|
||||
import { CosmosCheckbox, CosmosInputText } from "../config/users/formShortcuts";
|
||||
|
||||
const isAbsolutePath = (path) => path.startsWith('/') || path.startsWith('rclone:') || /^[a-zA-Z]:\\/.test(path); // Unix & Windows support
|
||||
|
||||
@@ -23,7 +23,8 @@ const BackupDialogInternal = ({ refresh, open, setOpen, preSource, preName, data
|
||||
repository: data.Repository || '/backups',
|
||||
crontab: data.Crontab || '0 0 4 * * *',
|
||||
crontabForget: data.CrontabForget || '0 0 12 * * *',
|
||||
retentionPolicy: data.RetentionPolicy || '--keep-last 3 --keep-daily 7 --keep-weekly 8 --keep-yearly 3'
|
||||
retentionPolicy: data.RetentionPolicy || '--keep-last 3 --keep-daily 7 --keep-weekly 8 --keep-yearly 3',
|
||||
autoStopContainers: isEdit ? data.AutoStopContainers : true,
|
||||
},
|
||||
validationSchema: yup.object({
|
||||
name: yup
|
||||
@@ -156,6 +157,13 @@ const BackupDialogInternal = ({ refresh, open, setOpen, preSource, preName, data
|
||||
helperText={formik.touched.retentionPolicy && formik.errors.retentionPolicy}
|
||||
/>
|
||||
|
||||
<CosmosCheckbox
|
||||
name="autoStopContainers"
|
||||
label={t('mgmt.backup.autoStopContainers')}
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
|
||||
{formik.errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
|
||||
@@ -88,7 +88,7 @@ const RestoreDialogInternal = ({ refresh, open, setOpen, candidatePaths, origina
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpen(false)}>{t('global.cancelAction')}</Button>
|
||||
<Button onClick={() => setOpen(false)}>{done ? t('global.close') : t('global.cancelAction')}</Button>
|
||||
{!done && <LoadingButton
|
||||
color="primary"
|
||||
variant="contained"
|
||||
|
||||
@@ -325,7 +325,7 @@ const VolumeContainerSetup = ({
|
||||
>
|
||||
{t('global.unmount')}
|
||||
</Button>
|
||||
{r.Target ? <BackupDialog preName={`${containerInfo.Name.replace("/", "").replace("/", "-")}-${r.Target.replace("/", "").replaceAll("/", "_")}`} preSource={formatSource(r.Source)} refresh={() => setTimeout(refreshAll, 1500)} /> : null}
|
||||
{!newContainer && containerInfo.Name && (r.Target ? <BackupDialog preName={`${containerInfo.Name.replace("/", "").replace("/", "-")}-${r.Target.replace("/", "").replaceAll("/", "_")}`} preSource={formatSource(r.Source)} refresh={() => setTimeout(refreshAll, 1500)} /> : null)}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
@@ -374,19 +374,19 @@ const VolumeContainerSetup = ({
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
<div
|
||||
{!newContainer && <div
|
||||
style={{
|
||||
maxWidth: "1000px",
|
||||
width: "100%",
|
||||
margin: "",
|
||||
position: "relative",
|
||||
}}>
|
||||
<MainCard title={t('mgmt.backup.backups')}>
|
||||
{containerInfo && containerInfo.HostConfig && containerInfo.HostConfig.Mounts && <MainCard title={t('mgmt.backup.backups')}>
|
||||
<Backups pathFilters={
|
||||
containerInfo.HostConfig.Mounts.map((r) => formatSource(r.Source))
|
||||
} />
|
||||
</MainCard>
|
||||
</div>
|
||||
</MainCard>}
|
||||
</div>}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -912,6 +912,7 @@
|
||||
"mgmt.backup.files": "Files in Snapshot",
|
||||
"mgmt.backup.restore.success": "Restore operation has been queued. You can monitor the progress in the tasks log on the top right.",
|
||||
"mgmt.backup.restore.summary": "You are about to restore {count} file(s)/folder(s). This will overwrite existing files.",
|
||||
"mgmt.backup.autoStopContainers": "Automatically Stop Containers using that folder before backup/restore operations",
|
||||
"global.noData": "No Data To Show",
|
||||
"global.open": "Open",
|
||||
"global.absolutePath": "Must be an absolute path",
|
||||
|
||||
@@ -143,6 +143,7 @@ func EditBackupRoute(w http.ResponseWriter, req *http.Request) {
|
||||
current.Crontab = request.Crontab
|
||||
current.CrontabForget = request.CrontabForget
|
||||
current.RetentionPolicy = request.RetentionPolicy
|
||||
current.AutoStopContainers = request.AutoStopContainers
|
||||
|
||||
config.Backup.Backups[request.Name] = current
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"context"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/azukaar/cosmos-server/src/cron"
|
||||
"github.com/azukaar/cosmos-server/src/docker"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
@@ -262,7 +264,42 @@ func CreateBackupJob(config BackupConfig, crontab string) {
|
||||
Scheduler: "Restic",
|
||||
Name: fmt.Sprintf("Restic backup %s", config.Name),
|
||||
Cancellable: true,
|
||||
Job: cron.JobFromCommandWithEnv(env, "./restic", prependResticArgs(args)...),
|
||||
Job: func(OnLog func(string), OnFail func(error), OnSuccess func(), ctx context.Context, cancel context.CancelFunc) {
|
||||
var containers []string
|
||||
var err error
|
||||
|
||||
if config.AutoStopContainers {
|
||||
containers, err = docker.GetContainersUsingPath(config.Source)
|
||||
if err != nil {
|
||||
OnFail(err)
|
||||
return
|
||||
}
|
||||
|
||||
OnLog("Found container(s) using path: " + strings.Join(containers, ", "))
|
||||
|
||||
|
||||
// Stop all containers
|
||||
err = docker.StopContainers(containers)
|
||||
if err != nil {
|
||||
docker.StartContainers(containers)
|
||||
OnFail(err)
|
||||
return
|
||||
}
|
||||
|
||||
OnLog("Stopped containers, starting backup")
|
||||
}
|
||||
|
||||
cron.JobFromCommandWithEnv(env, "./restic", prependResticArgs(args)...)(OnLog, OnFail, OnSuccess, ctx, cancel)
|
||||
|
||||
if config.AutoStopContainers {
|
||||
// Start all containers
|
||||
err = docker.StartContainers(containers)
|
||||
if err != nil {
|
||||
OnFail(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
Crontab: crontab,
|
||||
Resource: "backup@" + config.Name,
|
||||
})
|
||||
@@ -337,7 +374,42 @@ func CreateRestoreJob(config RestoreConfig) {
|
||||
Scheduler: "Restic",
|
||||
Name: fmt.Sprintf("Restic restore %s", config.Name),
|
||||
Cancellable: true,
|
||||
Job: cron.JobFromCommandWithEnv(env, "./restic", prependResticArgs(args)...),
|
||||
Job: func(OnLog func(string), OnFail func(error), OnSuccess func(), ctx context.Context, cancel context.CancelFunc) {
|
||||
var containers []string
|
||||
var err error
|
||||
|
||||
if config.AutoStopContainers {
|
||||
containers, err = docker.GetContainersUsingPath(config.OriginalSource)
|
||||
if err != nil {
|
||||
OnFail(err)
|
||||
return
|
||||
}
|
||||
|
||||
OnLog("Found container(s) using path: " + strings.Join(containers, ", "))
|
||||
|
||||
|
||||
// Stop all containers
|
||||
err = docker.StopContainers(containers)
|
||||
if err != nil {
|
||||
docker.StartContainers(containers)
|
||||
OnFail(err)
|
||||
return
|
||||
}
|
||||
|
||||
OnLog("Stopped containers, starting backup")
|
||||
}
|
||||
|
||||
cron.JobFromCommandWithEnv(env, "./restic", prependResticArgs(args)...)(OnLog, OnFail, OnSuccess, ctx, cancel)
|
||||
|
||||
if config.AutoStopContainers {
|
||||
// Start all containers
|
||||
err = docker.StartContainers(containers)
|
||||
if err != nil {
|
||||
OnFail(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
Resource: "backup@" + config.Name,
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -173,14 +173,7 @@ func setTerminalSize(ptmx *os.File, rows, cols uint16) error {
|
||||
})
|
||||
}
|
||||
|
||||
func WrapJobExecution(
|
||||
jobExecuter func(OnLog func(string), OnFail func(error), OnSuccess func(), ctx context.Context, cancel context.CancelFunc),
|
||||
wrapFunc func(func(OnLog func(string), OnFail func(error), OnSuccess func(), ctx context.Context, cancel context.CancelFunc)) func(OnLog func(string), OnFail func(error), OnSuccess func(), ctx context.Context, cancel context.CancelFunc),
|
||||
) func(OnLog func(string), OnFail func(error), OnSuccess func(), ctx context.Context, cancel context.CancelFunc) {
|
||||
return func(OnLog func(string), OnFail func(error), OnSuccess func(), ctx context.Context, cancel context.CancelFunc) {
|
||||
wrapFunc(jobExecuter)(OnLog, OnFail, OnSuccess, ctx, cancel)
|
||||
}
|
||||
}
|
||||
type ExecuterFn func(OnLog func(string), OnFail func(error), OnSuccess func(), ctx context.Context, cancel context.CancelFunc)
|
||||
|
||||
func JobFromContainerCommand(containerID string, command string, args ...string) func(OnLog func(string), OnFail func(error), OnSuccess func(), ctx context.Context, cancel context.CancelFunc) {
|
||||
return func(OnLog func(string), OnFail func(error), OnSuccess func(), ctx context.Context, cancel context.CancelFunc) {
|
||||
|
||||
233
src/docker/backups.go
Normal file
233
src/docker/backups.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"path/filepath"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
contstuff "github.com/docker/docker/api/types/container"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// arePathsRelated checks if either path is a parent of the other or if they are the same
|
||||
func arePathsRelated(path1, path2 string) bool {
|
||||
// Clean and normalize both paths
|
||||
path1 = filepath.Clean(path1)
|
||||
path2 = filepath.Clean(path2)
|
||||
|
||||
// If paths are equal, return true
|
||||
if path1 == path2 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if path1 is a parent of path2
|
||||
rel1, err1 := filepath.Rel(path1, path2)
|
||||
if err1 == nil && !strings.HasPrefix(rel1, "..") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if path2 is a parent of path1
|
||||
rel2, err2 := filepath.Rel(path2, path1)
|
||||
if err2 == nil && !strings.HasPrefix(rel2, "..") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getStackContainers returns all containers that are part of the same stack as the given container
|
||||
func getStackContainers(containerID string) ([]string, error) {
|
||||
var stackContainers []string
|
||||
|
||||
// Get the container details to find stack labels
|
||||
container, err := DockerClient.ContainerInspect(DockerContext, containerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check for compose project name (stack name)
|
||||
stackName := ""
|
||||
for label, value := range container.Config.Labels {
|
||||
if label == "com.docker.compose.project" || label == "cosmos-stack" {
|
||||
stackName = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no stack found, return just this container
|
||||
if stackName == "" {
|
||||
return []string{containerID}, nil
|
||||
}
|
||||
|
||||
// List all containers
|
||||
containers, err := ListContainers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find all containers with the same stack name
|
||||
for _, cont := range containers {
|
||||
fullContainer, err := DockerClient.ContainerInspect(DockerContext, cont.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for label, value := range fullContainer.Config.Labels {
|
||||
if (label == "com.docker.compose.project" || label == "cosmos-stack") && value == stackName {
|
||||
stackContainers = append(stackContainers, cont.ID)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stackContainers, nil
|
||||
}
|
||||
|
||||
// GetContainersUsingPath returns a list of container IDs that have volumes or binds
|
||||
// that are either equal to or contained within the specified path
|
||||
func GetContainersUsingPath(path string) ([]string, error) {
|
||||
var affectedContainers []string
|
||||
seenContainers := make(map[string]bool)
|
||||
|
||||
// Connect to Docker if not already connected
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
return nil, errD
|
||||
}
|
||||
|
||||
// List all containers
|
||||
containers, err := ListContainers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check each container
|
||||
for _, container := range containers {
|
||||
// Skip containers that are stopped
|
||||
if container.State == "exited" || container.State == "dead" || container.State == "created" || container.State == "removing" || container.State == "paused" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get detailed container info
|
||||
fullContainer, err := DockerClient.ContainerInspect(DockerContext, container.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check mounts (both volumes and binds)
|
||||
isAffected := false
|
||||
for _, mount := range fullContainer.Mounts {
|
||||
var sourcePath string
|
||||
|
||||
switch mount.Type {
|
||||
case "bind":
|
||||
sourcePath = mount.Source
|
||||
case "volume":
|
||||
// For volumes, we need to check in /var/lib/docker/volumes
|
||||
sourcePath = filepath.Join("/var/lib/docker/volumes", mount.Name, "_data")
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
utils.Debug("Checking container " + container.ID + " mount " + sourcePath + " against " + path)
|
||||
|
||||
if arePathsRelated(path, sourcePath) {
|
||||
isAffected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isAffected {
|
||||
// Get all containers in the same stack
|
||||
stackContainers, err := getStackContainers(container.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add all stack containers to the result list (avoiding duplicates)
|
||||
for _, stackContainer := range stackContainers {
|
||||
if !seenContainers[stackContainer] {
|
||||
affectedContainers = append(affectedContainers, stackContainer)
|
||||
seenContainers[stackContainer] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return affectedContainers, nil
|
||||
}
|
||||
|
||||
|
||||
// StopContainers stops the given containers and waits for them to stop
|
||||
func StopContainers(containerIDs []string) error {
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
return errD
|
||||
}
|
||||
|
||||
for _, containerID := range containerIDs {
|
||||
// Get container state
|
||||
container, err := DockerClient.ContainerInspect(DockerContext, containerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect container %s: %v", containerID, err)
|
||||
}
|
||||
|
||||
// Skip if already stopped
|
||||
if !container.State.Running {
|
||||
continue
|
||||
}
|
||||
|
||||
// Stop the container with a timeout
|
||||
err = DockerClient.ContainerStop(DockerContext, containerID, contstuff.StopOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop container %s: %v", containerID, err)
|
||||
}
|
||||
|
||||
// Wait until container is actually stopped
|
||||
for {
|
||||
container, err := DockerClient.ContainerInspect(DockerContext, containerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect container %s: %v", containerID, err)
|
||||
}
|
||||
|
||||
if !container.State.Running {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartContainers starts the given containers and waits for them to be running
|
||||
func StartContainers(containerIDs []string) error {
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
return errD
|
||||
}
|
||||
|
||||
for _, containerID := range containerIDs {
|
||||
// Get container state
|
||||
container, err := DockerClient.ContainerInspect(DockerContext, containerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect container %s: %v", containerID, err)
|
||||
}
|
||||
|
||||
// Skip if already running
|
||||
if container.State.Running {
|
||||
continue
|
||||
}
|
||||
|
||||
// Start the container
|
||||
err = DockerClient.ContainerStart(DockerContext, containerID, contstuff.StartOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start container %s: %v", containerID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user