mirror of
https://github.com/azukaar/Cosmos-Server.git
synced 2026-04-30 08:09:32 -05:00
[release] v0.15.9
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
- Added Storage management (MergeFS, SnapRAID, RClone, etc...)
|
||||
- Overwrite all docker networks size to prevent Cosmos from running out of IP addresses
|
||||
- Added optional subnet input to the network creation
|
||||
- Fix issue with Sysctl not being applied
|
||||
|
||||
## Version 0.14.6
|
||||
- Fix custom back-up folder logic
|
||||
|
||||
@@ -28,7 +28,17 @@ const mounts = {
|
||||
},
|
||||
body: JSON.stringify({mountPoint, permanent})
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
merge: (args) => {
|
||||
return wrap(fetch('/cosmos/api/merge', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(args)
|
||||
}))
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ const LogsInModal = ({title, request, OnSuccess, OnError, closeAnytime, OnClose,
|
||||
setLogs((old) => smartDockerLogConcat(old, err.message));
|
||||
OnError && OnError(err);
|
||||
};
|
||||
}, [request]);
|
||||
}, []);
|
||||
|
||||
return <>
|
||||
<Dialog open={openModal} onClose={() => close()}>
|
||||
|
||||
+5
-5
@@ -6,7 +6,7 @@ import { Box, CardActions, Collapse, Divider, IconButton, Tooltip } from '@mui/m
|
||||
|
||||
// third-party
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
const reactElementToJSXString = lazy(() => import('react-element-to-jsx-string'));
|
||||
// const reactElementToJSXString = ('react-element-to-jsx-string'));
|
||||
|
||||
// project import
|
||||
import SyntaxHighlight from '../../utils/SyntaxHighlight';
|
||||
@@ -23,13 +23,13 @@ const Highlighter = ({ children }) => {
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<CardActions sx={{ justifyContent: 'flex-end', p: 1, mb: highlight ? 1 : 0 }}>
|
||||
<Box sx={{ display: 'flex', position: 'inherit', right: 0, top: 6 }}>
|
||||
<CopyToClipboard text={reactElementToJSXString(children, { showFunctions: true, maxInlineAttributesLineLength: 100 })}>
|
||||
{/* <CopyToClipboard text={reactElementToJSXString(children, { showFunctions: true, maxInlineAttributesLineLength: 100 })}>
|
||||
<Tooltip title="Copy the source" placement="top-end">
|
||||
<IconButton color="secondary" size="small" sx={{ fontSize: '0.875rem' }}>
|
||||
<CopyOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</CopyToClipboard>
|
||||
</CopyToClipboard> */}
|
||||
<Divider orientation="vertical" variant="middle" flexItem sx={{ mx: 1 }} />
|
||||
<Tooltip title="Show the source" placement="top-end">
|
||||
<IconButton
|
||||
@@ -46,11 +46,11 @@ const Highlighter = ({ children }) => {
|
||||
<Collapse in={highlight}>
|
||||
{highlight && (
|
||||
<SyntaxHighlight>
|
||||
{reactElementToJSXString(children, {
|
||||
{/* {reactElementToJSXString(children, {
|
||||
showFunctions: true,
|
||||
showDefaultProps: false,
|
||||
maxInlineAttributesLineLength: 100
|
||||
})}
|
||||
})} */}
|
||||
</SyntaxHighlight>
|
||||
)}
|
||||
</Collapse>
|
||||
|
||||
@@ -25,7 +25,7 @@ import MainCard from '../../../components/MainCard';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
// third-party
|
||||
const ReactApexChart = lazy(() => import('react-apexcharts'));
|
||||
import Chart from 'react-apexcharts';
|
||||
import { FormaterForMetric, formatDate, toUTC } from './utils';
|
||||
|
||||
import * as API from '../../../api';
|
||||
@@ -238,7 +238,7 @@ const _MiniPlotComponent = ({metrics, labels, noLabels, noBackground, agglo, tit
|
||||
margin: '-10px 0px -20px 10px',
|
||||
flexGrow: 1,
|
||||
}} ref={ref}>
|
||||
<ReactApexChart options={chartOptions} series={series} type="line" height={90} />
|
||||
<Chart options={chartOptions} series={series} type="line" height={90} />
|
||||
</div>
|
||||
</Stack>
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import MainCard from '../../../components/MainCard';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
// third-party
|
||||
const ReactApexChart = lazy(() => import('react-apexcharts'));
|
||||
import Chart from 'react-apexcharts';
|
||||
import { FormaterForMetric, toUTC } from './utils';
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, z
|
||||
<MainCard content={false} sx={{ mt: 1.5 }} >
|
||||
<Box sx={{ pt: 1, pr: 2}}>
|
||||
|
||||
<ReactApexChart options={options} series={series} type="area" height={450} />
|
||||
<Chart options={options} series={series} type="area" height={450} />
|
||||
</Box>
|
||||
</MainCard>
|
||||
</>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getFaviconURL } from "../../utils/routes";
|
||||
import { Link } from "react-router-dom";
|
||||
import { getFullOrigin } from "../../utils/routes";
|
||||
import { ServAppIcon } from "../../utils/servapp-icon";
|
||||
const ReactApexChart = lazy(() => import('react-apexcharts'));
|
||||
import Chart from 'react-apexcharts';
|
||||
import { useClientInfos } from "../../utils/hooks";
|
||||
import { FormaterForMetric, formatDate } from "../dashboard/components/utils";
|
||||
import MiniPlotComponent from "../dashboard/components/mini-plot";
|
||||
@@ -399,7 +399,7 @@ const HomePage = () => {
|
||||
<div>{coStatus.AVX ? "AVX Supported" : "No AVX Support"}</div>
|
||||
</Stack>
|
||||
<div style={{height: '97px'}}>
|
||||
<ReactApexChart
|
||||
<Chart
|
||||
options={optionsRadial}
|
||||
// series={[parseInt(
|
||||
// coStatus.resources.ram / (coStatus.resources.ram + coStatus.resources.ramFree) * 100
|
||||
@@ -422,7 +422,7 @@ const HomePage = () => {
|
||||
<div>used: <strong>{latestRAM}</strong></div>
|
||||
</Stack>
|
||||
<div style={{height: '97px'}}>
|
||||
<ReactApexChart
|
||||
<Chart
|
||||
options={optionsRadial}
|
||||
// series={[parseInt(
|
||||
// coStatus.resources.ram / (coStatus.resources.ram + coStatus.resources.ramFree) * 100
|
||||
|
||||
@@ -196,7 +196,7 @@ const NewInstall = () => {
|
||||
}}>
|
||||
{(formik) => (
|
||||
<form noValidate onSubmit={formik.handleSubmit}>
|
||||
<LogsInModal
|
||||
{pullRequest && <LogsInModal
|
||||
request={pullRequest}
|
||||
title="Installing Database..."
|
||||
OnSuccess={() => {
|
||||
@@ -215,7 +215,10 @@ const NewInstall = () => {
|
||||
formik.setSubmitting(false);
|
||||
pullRequestOnSuccess();
|
||||
}}
|
||||
/>
|
||||
OnClose={() => {
|
||||
setPullRequest(null);
|
||||
}}
|
||||
/>}
|
||||
<Stack item xs={12} spacing={2}>
|
||||
<CosmosSelect
|
||||
name="DBMode"
|
||||
|
||||
@@ -138,13 +138,17 @@ const GetActions = ({
|
||||
];
|
||||
|
||||
return <>
|
||||
<LogsInModal
|
||||
{pullRequest && <LogsInModal
|
||||
request={pullRequest}
|
||||
title="Updating ServApp..."
|
||||
OnSuccess={() => {
|
||||
refreshServApps();
|
||||
setPullRequest(null);
|
||||
}}
|
||||
/>
|
||||
OnClose={() => {
|
||||
setPullRequest(null);
|
||||
}}
|
||||
/>}
|
||||
|
||||
{!isUpdating && actions.filter((action) => {
|
||||
return action.if.includes(state) || (updateAvailable && action.if.includes('update_available')) || (!updateAvailable && action.if.includes('update_not_available'));
|
||||
|
||||
@@ -308,11 +308,7 @@ const convertDockerCompose = (config, serviceName, dockerCompose, setYmlError) =
|
||||
doc.networks = {}
|
||||
}
|
||||
|
||||
doc.networks['cosmos-' + serviceName + '-default'] = {
|
||||
Labels: {
|
||||
'cosmos.stack': serviceName,
|
||||
}
|
||||
}
|
||||
doc.networks['cosmos-' + serviceName + '-default'] = {}
|
||||
}
|
||||
|
||||
// stack up
|
||||
|
||||
@@ -129,14 +129,17 @@ const DockerContainerSetup = ({ noCard, containerInfo, installer, OnChange, refr
|
||||
>
|
||||
{(formik) => (
|
||||
<form noValidate onSubmit={formik.handleSubmit}>
|
||||
<LogsInModal
|
||||
{pullRequest && <LogsInModal
|
||||
request={pullRequest}
|
||||
title="Pulling New Image..."
|
||||
OnSuccess={() => {
|
||||
setPullRequest(null);
|
||||
setLatestImage(formik.values.image);
|
||||
}}
|
||||
/>
|
||||
OnClose={() => {
|
||||
setPullRequest(null);
|
||||
}}
|
||||
/>}
|
||||
<Stack spacing={2}>
|
||||
{wrapCard(<>
|
||||
{containerInfo.State && containerInfo.State.Status !== 'running' && (
|
||||
|
||||
@@ -47,7 +47,6 @@ const FormatButton = ({disk, refresh}) => {
|
||||
const [formatting, setFormatting] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [passwordConfirm, setPasswordConfirm] = useState(false);
|
||||
const [values, setValues] = useState("");
|
||||
|
||||
return <>
|
||||
<LoadingButton
|
||||
@@ -197,13 +196,16 @@ export const StorageDisks = () => {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [disks, setDisks] = useState([]);
|
||||
const [containerized, setContainerized] = useState(false);
|
||||
|
||||
const refresh = async () => {
|
||||
let disksData = await API.storage.disks.list();
|
||||
let configAsync = await API.config.get();
|
||||
let status = await API.getStatus();
|
||||
setConfig(configAsync.data);
|
||||
setIsAdmin(configAsync.isAdmin);
|
||||
setDisks(disksData.data);
|
||||
setContainerized(status.data.containerized);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -213,6 +215,7 @@ export const StorageDisks = () => {
|
||||
return <>
|
||||
{(config) ? <>
|
||||
<Stack spacing={2} style={{maxWidth: "1000px"}}>
|
||||
{containerized && <Alert severity="warning">You are running Cosmos inside a Docker container. As such, it will only have limited access to your disks and their informations.</Alert>}
|
||||
<div>
|
||||
<Button variant="contained" color="primary" onClick={refresh}>Refresh</Button>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import PrettyTabbedView from '../../components/tabbedView/tabbedView';
|
||||
import { useClientInfos } from '../../utils/hooks';
|
||||
import { StorageMounts } from './mounts';
|
||||
import { StorageDisks } from './disks';
|
||||
import { StorageMerges } from './merges';
|
||||
|
||||
const StorageIndex = () => {
|
||||
const {role} = useClientInfos();
|
||||
@@ -17,16 +18,41 @@ const StorageIndex = () => {
|
||||
|
||||
return <div>
|
||||
<PrettyTabbedView path="/cosmos-ui/storage/:tab" tabs={[
|
||||
{
|
||||
title: 'Files',
|
||||
children: <StorageMounts />,
|
||||
path: 'files'
|
||||
},
|
||||
{
|
||||
title: 'Disks',
|
||||
children: <StorageDisks />,
|
||||
path: 'disks'
|
||||
},
|
||||
{
|
||||
title: 'Mounts',
|
||||
children: <StorageMounts />,
|
||||
path: 'mounts'
|
||||
},
|
||||
{
|
||||
title: 'External Storages',
|
||||
children: <StorageMounts />,
|
||||
path: 'external'
|
||||
},
|
||||
{
|
||||
title: 'Shares',
|
||||
children: <StorageMounts />,
|
||||
path: 'shares'
|
||||
},
|
||||
{
|
||||
title: 'Merge Disks',
|
||||
children: <StorageMerges />,
|
||||
path: 'mergerfs'
|
||||
},
|
||||
{
|
||||
title: 'Parity',
|
||||
children: <StorageMounts />,
|
||||
path: 'parity'
|
||||
},
|
||||
{
|
||||
title: 'RAID',
|
||||
children: <StorageMounts />,
|
||||
path: 'raid'
|
||||
},
|
||||
]}/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { Alert, Grid, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormHelperText, Stack, TextField } from "@mui/material";
|
||||
import React, {useState} from "react";
|
||||
import { CosmosCheckbox } from "../config/users/formShortcuts";
|
||||
import { Formik, FormikProvider, useFormik } from "formik";
|
||||
import * as yup from "yup";
|
||||
|
||||
import * as API from '../../api';
|
||||
|
||||
const MergerDialog = ({ refresh }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
path: '/mnt/storage',
|
||||
permanent: false,
|
||||
},
|
||||
validationSchema: yup.object({
|
||||
// should start with /mnt/ or /var/mnt
|
||||
path: yup.string().required('Required').matches(/^\/(mnt|var\/mnt)\/.{1,}$/, 'Path should start with /mnt/ or /var/mnt'),
|
||||
}),
|
||||
onSubmit: (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
return API.storage.mounts.merge({
|
||||
branches: ['/mnt/sda1'],
|
||||
mountPoint: values.path,
|
||||
chown: '',
|
||||
permanent: values.permanent,
|
||||
opts: '',
|
||||
}).then((res) => {
|
||||
setStatus({ success: true });
|
||||
setSubmitting(false);
|
||||
setOpen(false);
|
||||
refresh && refresh();
|
||||
}).catch((err) => {
|
||||
setStatus({ success: false });
|
||||
setErrors({ submit: err.message });
|
||||
setSubmitting(false);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return <>
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<FormikProvider value={formik}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogTitle>Merge Disks</DialogTitle>
|
||||
{open && <>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<Stack spacing={2} style={{ marginTop: '10px', width: '500px', maxWidth: '100%' }}>
|
||||
<div>
|
||||
<Alert severity="info">
|
||||
You are about to merge disks together. <strong>This operation is safe and reversible</strong>.
|
||||
It will not affect the data on the disks, but will make the content available to be viewed in the file explorer as a single disk.
|
||||
</Alert>
|
||||
</div>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="path"
|
||||
name="path"
|
||||
label="Path to mount to"
|
||||
value={formik.values.path}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.touched.path && Boolean(formik.errors.path)}
|
||||
helperText={formik.touched.path && formik.errors.path}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="chown"
|
||||
name="chown"
|
||||
label="Change mount folder owner (optional, ex. 1000:1000)"
|
||||
value={formik.values.chown}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.touched.chown && Boolean(formik.errors.chown)}
|
||||
helperText={formik.touched.chown && formik.errors.chown}
|
||||
/>
|
||||
<div>
|
||||
<Checkbox
|
||||
name="permanent"
|
||||
checked={formik.values.permanent}
|
||||
onChange={formik.handleChange}
|
||||
/> Permanent {'Mount'}
|
||||
</div>
|
||||
</Stack>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{formik.errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
||||
<LoadingButton color="primary" variant="contained" type="submit" onClick={() => {
|
||||
formik.handleSubmit();
|
||||
}}>Merge</LoadingButton>
|
||||
</DialogActions>
|
||||
</>}
|
||||
</form>
|
||||
</FormikProvider>
|
||||
</Dialog>
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
onClick={() => {setOpen(true);}}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>Create Merge</LoadingButton>
|
||||
</>
|
||||
}
|
||||
|
||||
export default MergerDialog;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import * as API from "../../api";
|
||||
import PrettyTableView from "../../components/tableView/prettyTableView";
|
||||
import { DeleteButton } from "../../components/delete";
|
||||
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, FolderOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, CircularProgress, InputLabel, Stack } from "@mui/material";
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
|
||||
import MainCard from "../../components/MainCard";
|
||||
import { Formik } from "formik";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import ApiModal from "../../components/apiModal";
|
||||
import ConfirmModal from "../../components/confirmModal";
|
||||
import { isDomain } from "../../utils/indexs";
|
||||
import UploadButtons from "../../components/fileUpload";
|
||||
import MergerDialog from "./mergerDialog";
|
||||
|
||||
export const StorageMerges = () => {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [mounts, setMounts] = useState([]);
|
||||
|
||||
const refresh = async () => {
|
||||
let mountsData = await API.storage.mounts.list();
|
||||
let configAsync = await API.config.get();
|
||||
setConfig(configAsync.data);
|
||||
setIsAdmin(configAsync.isAdmin);
|
||||
setMounts(mountsData.data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return <>
|
||||
{(config) ? <>
|
||||
<Stack spacing={2} style={{maxWidth: "1000px"}}>
|
||||
<div>
|
||||
<MergerDialog disk={{name: '/dev/sda'}} refresh={refresh}/>
|
||||
</div>
|
||||
<div>
|
||||
{mounts && mounts
|
||||
.filter((mount) => mount.type === 'fuse.mergerfs')
|
||||
.map((mount, index) => {
|
||||
return <div>
|
||||
<FolderOutlined/> {mount.device} - {mount.path} ({mount.type}) ({JSON.stringify(mount.opts)})
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</Stack>
|
||||
</> : <center>
|
||||
<CircularProgress color="inherit" size={20} />
|
||||
</center>}
|
||||
</>
|
||||
};
|
||||
@@ -17,7 +17,8 @@ const MountDialog = ({disk, unmount, refresh }) => {
|
||||
permanent: false,
|
||||
},
|
||||
validationSchema: yup.object({
|
||||
path: yup.string().required('Required'),
|
||||
// should start with /mnt/ or /var/mnt
|
||||
path: yup.string().required('Required').matches(/^\/(mnt|var\/mnt)\/.{1,}$/, 'Path should start with /mnt/ or /var/mnt'),
|
||||
}),
|
||||
onSubmit: (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
@@ -73,7 +74,7 @@ const MountDialog = ({disk, unmount, refresh }) => {
|
||||
fullWidth
|
||||
id="chown"
|
||||
name="chown"
|
||||
label="Change mount folder owner to (optional, ex. 1000:1000)"
|
||||
label="Change mount folder owner (optional, ex. 1000:1000)"
|
||||
value={formik.values.chown}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.touched.chown && Boolean(formik.errors.chown)}
|
||||
|
||||
@@ -37,7 +37,7 @@ export const StorageMounts = () => {
|
||||
<div>
|
||||
{mounts && mounts.map((mount, index) => {
|
||||
return <div>
|
||||
<FolderOutlined/> {mount.device} - {mount.path} ({mount.type})
|
||||
<FolderOutlined/> {mount.device} - {mount.path} ({mount.type}) ({JSON.stringify(mount.opts)})
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -4,16 +4,15 @@ import { lazy } from 'react';
|
||||
import Loadable from '../components/Loadable';
|
||||
import { NewMFA, MFALogin } from '../pages/authentication/newMFA';
|
||||
|
||||
const MinimalLayout = Loadable(lazy(() => import('../layout/MinimalLayout')));
|
||||
const Logout = Loadable(lazy(() => import('../pages/authentication/Logoff')));
|
||||
const NewInstall = Loadable(lazy(() => import('../pages/newInstall/newInstall')))
|
||||
import MinimalLayout from '../layout/MinimalLayout';
|
||||
import Logout from '../pages/authentication/Logoff';
|
||||
import NewInstall from '../pages/newInstall/newInstall';
|
||||
|
||||
const ForgotPassword = Loadable(lazy(() => import('../pages/authentication/forgotPassword')));
|
||||
const OpenID = Loadable(lazy(() => import('../pages/authentication/openid')));
|
||||
import ForgotPassword from '../pages/authentication/forgotPassword';
|
||||
import OpenID from '../pages/authentication/openid';
|
||||
|
||||
// render - login
|
||||
const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login')));
|
||||
const AuthRegister = Loadable(lazy(() => import('../pages/authentication/Register')));
|
||||
import AuthLogin from '../pages/authentication/login';
|
||||
import AuthRegister from '../pages/authentication/register';
|
||||
|
||||
// ==============================|| AUTH ROUTING ||============================== //
|
||||
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import { lazy } from 'react';
|
||||
|
||||
// project import
|
||||
import Loadable from '../components/Loadable';
|
||||
import MainLayout from '../layout/MainLayout';
|
||||
import logo from '../assets/images/icons/cosmos.png';
|
||||
import PrivateRoute from '../PrivateRoute';
|
||||
import { Navigate } from 'react-router';
|
||||
|
||||
const UserManagement = Loadable(lazy(() => import('../pages/config/users/usermanagement')));
|
||||
const ConfigManagement = Loadable(lazy(() => import('../pages/config/users/configman')));
|
||||
const ProxyManagement = Loadable(lazy(() => import('../pages/config/users/proxyman')));
|
||||
const ServAppsIndex = Loadable(lazy(() => import('../pages/servapps/')));
|
||||
const RouteConfigPage = Loadable(lazy(() => import('../pages/config/routeConfigPage')));
|
||||
const HomePage = Loadable(lazy(() => import('../pages/home')));
|
||||
const ContainerIndex = Loadable(lazy(() => import('../pages/servapps/containers')));
|
||||
const NewDockerServiceForm = Loadable(lazy(() => import('../pages/servapps/containers/newServiceForm')));
|
||||
const OpenIdList = Loadable(lazy(() => import('../pages/openid/openid-list')));
|
||||
const MarketPage = Loadable(lazy(() => import('../pages/market/listing')));
|
||||
const ConstellationIndex = Loadable(lazy(() => import('../pages/constellation')));
|
||||
const StorageIndex = Loadable(lazy(() => import('../pages/storage')));
|
||||
|
||||
// render - dashboard
|
||||
const DashboardDefault = Loadable(lazy(() => import('../pages/dashboard')));
|
||||
import UserManagement from '../pages/config/users/usermanagement';
|
||||
import ConfigManagement from '../pages/config/users/configman';
|
||||
import ProxyManagement from '../pages/config/users/proxyman';
|
||||
import ServAppsIndex from '../pages/servapps/';
|
||||
import RouteConfigPage from '../pages/config/routeConfigPage';
|
||||
import HomePage from '../pages/home';
|
||||
import ContainerIndex from '../pages/servapps/containers';
|
||||
import NewDockerServiceForm from '../pages/servapps/containers/newServiceForm';
|
||||
import OpenIdList from '../pages/openid/openid-list';
|
||||
import MarketPage from '../pages/market/listing';
|
||||
import ConstellationIndex from '../pages/constellation';
|
||||
import StorageIndex from '../pages/storage';
|
||||
import DashboardDefault from '../pages/dashboard';
|
||||
|
||||
// ==============================|| MAIN ROUTING ||============================== //
|
||||
|
||||
@@ -99,10 +92,7 @@ const MainRoutes = {
|
||||
path: '/cosmos-ui/market-listing/:appStore/:appName',
|
||||
element: <MarketPage />
|
||||
}
|
||||
].map(children => ({
|
||||
...children,
|
||||
element: PrivateRoute({ children: children.element })
|
||||
}))
|
||||
]
|
||||
};
|
||||
|
||||
export default MainRoutes;
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ EXPOSE 443 80
|
||||
VOLUME /config
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y ca-certificates openssl fdisk \
|
||||
&& apt-get install -y ca-certificates openssl fdisk mergerfs \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ EXPOSE 443 80
|
||||
VOLUME /config
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y ca-certificates openssl fdisk \
|
||||
&& apt-get install -y ca-certificates openssl fdisk mergerfs \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ WORKDIR /app
|
||||
|
||||
ENV PATH=$PATH:/usr/local/go/bin
|
||||
|
||||
RUN apt-get update && apt-get install -y ca-certificates openssl fdisk && \
|
||||
RUN apt-get update && apt-get install -y ca-certificates openssl fdisk mergerfs && \
|
||||
apt-get install -y --no-install-recommends wget curl && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
wget https://golang.org/dl/go1.20.2.linux-amd64.tar.gz && \
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.15.0-unstable8",
|
||||
"version": "0.15.0-unstable9",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
||||
@@ -80,7 +80,6 @@ type ContainerCreateRequestContainer struct {
|
||||
|
||||
CapAdd []string `json:"cap_add,omitempty"`
|
||||
CapDrop []string `json:"cap_drop,omitempty"`
|
||||
SysctlsMap map[string]string `json:"sysctls,omitempty"`
|
||||
|
||||
PostInstall []string `json:"post_install,omitempty"`
|
||||
}
|
||||
@@ -487,6 +486,10 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
|
||||
OpenStdin: container.StdinOpen,
|
||||
}
|
||||
|
||||
if container.StopGracePeriod == 0 {
|
||||
containerConfig.StopTimeout = nil
|
||||
}
|
||||
|
||||
// check if there's an empty TZ env, if so, replace it with the host's TZ
|
||||
if containerConfig.Env != nil {
|
||||
for i, env := range containerConfig.Env {
|
||||
|
||||
@@ -57,7 +57,6 @@ func ExportContainer(containerID string) (ContainerCreateRequestContainer, error
|
||||
Isolation: string(detailedInfo.HostConfig.Isolation),
|
||||
CapAdd: detailedInfo.HostConfig.CapAdd,
|
||||
CapDrop: detailedInfo.HostConfig.CapDrop,
|
||||
SysctlsMap: detailedInfo.HostConfig.Sysctls,
|
||||
Privileged: detailedInfo.HostConfig.Privileged,
|
||||
|
||||
// StopGracePeriod: int(detailedInfo.HostConfig.StopGracePeriod.Seconds()),
|
||||
|
||||
@@ -431,6 +431,7 @@ func InitServer() *mux.Router {
|
||||
srapiAdmin.HandleFunc("/api/mounts", storage.ListMountsRoute)
|
||||
srapiAdmin.HandleFunc("/api/mount", storage.MountRoute)
|
||||
srapiAdmin.HandleFunc("/api/unmount", storage.UnmountRoute)
|
||||
srapiAdmin.HandleFunc("/api/merge", storage.MergeRoute)
|
||||
|
||||
srapiAdmin.Use(utils.Restrictions(config.AdminConstellationOnly, config.AdminWhitelistIPs))
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ func StatusRoute(w http.ResponseWriter, req *http.Request) {
|
||||
// "disk": utils.GetDiskUsage(),
|
||||
// "network": utils.GetNetworkUsage(),
|
||||
},
|
||||
"containerized": os.Getenv("HOSTNAME") != "",
|
||||
"hostmode": utils.IsHostNetwork || os.Getenv("HOSTNAME") == "" || utils.GetMainConfig().DisableHostModeWarning,
|
||||
"database": utils.DBStatus,
|
||||
"docker": docker.DockerIsConnected,
|
||||
|
||||
@@ -3,6 +3,8 @@ package storage
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
@@ -53,4 +55,117 @@ func ListMountsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Assuming the structure for the mount/unmount request
|
||||
type MountRequest struct {
|
||||
Path string `json:"path"`
|
||||
MountPoint string `json:"mountPoint"`
|
||||
Permanent bool `json:"permanent"`
|
||||
Chown string `json:"chown"`
|
||||
}
|
||||
|
||||
// MountRoute handles mounting filesystem requests
|
||||
func MountRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
var request MountRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||
utils.Error("MountRoute: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request: " + err.Error(), http.StatusBadRequest, "MNT001")
|
||||
return
|
||||
}
|
||||
|
||||
if err := Mount(request.Path, request.MountPoint, request.Permanent, request.Chown); err != nil {
|
||||
utils.Error("MountRoute: Error mounting", err)
|
||||
utils.HTTPError(w, "Error mounting filesystem:" + err.Error(), http.StatusInternalServerError, "MNT002")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log(fmt.Sprintf("Mounted %s at %s", request.Path, request.MountPoint))
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"message": fmt.Sprintf("Mounted %s at %s", request.Path, request.MountPoint),
|
||||
})
|
||||
} else {
|
||||
utils.Error("MountRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// UnmountRoute handles unmounting filesystem requests
|
||||
func UnmountRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
var request MountRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||
utils.Error("UnmountRoute: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request: " + err.Error(), http.StatusBadRequest, "UMNT001")
|
||||
return
|
||||
}
|
||||
|
||||
if err := Unmount(request.MountPoint, request.Permanent); err != nil {
|
||||
utils.Error("UnmountRoute: Error unmounting", err)
|
||||
utils.HTTPError(w, "Error unmounting filesystem:" + err.Error(), http.StatusInternalServerError, "UMNT002")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log(fmt.Sprintf("Unmounted %s", request.MountPoint))
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"message": fmt.Sprintf("Unmounted %s", request.MountPoint),
|
||||
})
|
||||
} else {
|
||||
utils.Error("UnmountRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Assuming the structure for the mount/unmount request
|
||||
type MergeRequest struct {
|
||||
Branches []string `json:"branches"`
|
||||
MountPoint string `json:"mountPoint"`
|
||||
Permanent bool `json:"permanent"`
|
||||
Chown string `json:"chown"`
|
||||
Opts string `json:"opts"`
|
||||
}
|
||||
|
||||
// MergeRoute handles merging filesystem requests
|
||||
func MergeRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
var request MergeRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||
utils.Error("MergeRoute: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request: " + err.Error(), http.StatusBadRequest, "M001")
|
||||
return
|
||||
}
|
||||
|
||||
if err := MountMergerFS(request.Branches, request.MountPoint, request.Opts, request.Permanent, request.Chown); err != nil {
|
||||
utils.Error("MergeRoute: Error merging", err)
|
||||
utils.HTTPError(w, "Error merging filesystem:" + err.Error(), http.StatusInternalServerError, "M002")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log(fmt.Sprintf("Merged %s at %s", strings.Join(request.Branches, ":"), request.MountPoint))
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"message": fmt.Sprintf("Merged %s at %s", strings.Join(request.Branches, ":"), request.MountPoint),
|
||||
})
|
||||
} else {
|
||||
utils.Error("MergeRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -139,75 +139,3 @@ func FormatDiskRoute(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Assuming the structure for the mount/unmount request
|
||||
type MountRequest struct {
|
||||
Path string `json:"path"`
|
||||
MountPoint string `json:"mountPoint"`
|
||||
Permanent bool `json:"permanent"`
|
||||
Chown string `json:"chown"`
|
||||
}
|
||||
|
||||
// MountRoute handles mounting filesystem requests
|
||||
func MountRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
var request MountRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||
utils.Error("MountRoute: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request: " + err.Error(), http.StatusBadRequest, "MNT001")
|
||||
return
|
||||
}
|
||||
|
||||
if err := Mount(request.Path, request.MountPoint, request.Permanent, request.Chown); err != nil {
|
||||
utils.Error("MountRoute: Error mounting", err)
|
||||
utils.HTTPError(w, "Error mounting filesystem:" + err.Error(), http.StatusInternalServerError, "MNT002")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log(fmt.Sprintf("Mounted %s at %s", request.Path, request.MountPoint))
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"message": fmt.Sprintf("Mounted %s at %s", request.Path, request.MountPoint),
|
||||
})
|
||||
} else {
|
||||
utils.Error("MountRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// UnmountRoute handles unmounting filesystem requests
|
||||
func UnmountRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
var request MountRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||
utils.Error("UnmountRoute: Invalid request", err)
|
||||
utils.HTTPError(w, "Invalid request: " + err.Error(), http.StatusBadRequest, "UMNT001")
|
||||
return
|
||||
}
|
||||
|
||||
if err := Unmount(request.MountPoint, request.Permanent); err != nil {
|
||||
utils.Error("UnmountRoute: Error unmounting", err)
|
||||
utils.HTTPError(w, "Error unmounting filesystem:" + err.Error(), http.StatusInternalServerError, "UMNT002")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log(fmt.Sprintf("Unmounted %s", request.MountPoint))
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"message": fmt.Sprintf("Unmounted %s", request.MountPoint),
|
||||
})
|
||||
} else {
|
||||
utils.Error("UnmountRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"os"
|
||||
"errors"
|
||||
"strings"
|
||||
"io"
|
||||
"fmt"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// Mount mounts a filesystem located at 'path' to 'mountpoint'.
|
||||
func MountMergerFS(paths []string, mountpoint string, opts string, permanent bool, chown string) error {
|
||||
utils.Log("[STORAGE] Merging " + strings.Join(paths, ":") + " into " + mountpoint)
|
||||
|
||||
// Check if mountpoint exists
|
||||
if _, err := os.Stat(mountpoint); os.IsNotExist(err) {
|
||||
utils.Log("[STORAGE] Mountpoint does not exist, creating " + mountpoint)
|
||||
// Create the mountpoint if it does not exist
|
||||
if err := os.Mkdir(mountpoint, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
utils.Log("[STORAGE] Mountpoint exists, checking if it is empty")
|
||||
// Check if mountpoint is empty
|
||||
dir, _ := os.Open(mountpoint)
|
||||
defer dir.Close()
|
||||
|
||||
_, err := dir.Readdirnames(1) // Or use Readdir to get FileInfo
|
||||
if err != io.EOF {
|
||||
return errors.New("mountpoint is not empty")
|
||||
}
|
||||
}
|
||||
|
||||
// chown the mountpoint
|
||||
if chown != "" {
|
||||
utils.Log("[STORAGE] Chowning " + mountpoint + " to " + chown)
|
||||
cmd := exec.Command("chown", chown, mountpoint)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(opts) > 0 && opts[0] != ',' {
|
||||
opts = "," + opts
|
||||
}
|
||||
|
||||
// Execute the mount command
|
||||
cmd := exec.Command("mergerfs", "-o", "use_ino,cache.files=partial,dropcacheonclose=true,allow_other,category.create=mfs" + opts, strings.Join(paths, ":"), mountpoint)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Log("[STORAGE] command: mergerfs -o use_ino,cache.files=partial,dropcacheonclose=true,allow_other,category.create=mfs" + opts + " " + strings.Join(paths, ":") + " " + mountpoint)
|
||||
|
||||
if permanent {
|
||||
utils.Log("[STORAGE] Adding mountpoint to /etc/fstab")
|
||||
|
||||
// Check if mountpoint is already in /etc/fstab
|
||||
exists, err := isMountPointInFstab(mountpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Format the fstab entry
|
||||
fstabEntry := fmt.Sprintf("%s %s mergerfs use_ino,cache.files=partial,dropcacheonclose=true,allow_other,category.create=mfs%s 0 0\n", strings.Join(paths, ":"), mountpoint, opts)
|
||||
|
||||
// Append to /etc/fstab
|
||||
file, err := os.OpenFile("/etc/fstab", os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := file.WriteString(fstabEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Log("[STORAGE] Added mountpoint to /etc/fstab")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -43,8 +43,10 @@ func ListMounts() ([]MountPoint, error) {
|
||||
path = strings.Replace(path, "/mnt/host", "", 1)
|
||||
}
|
||||
|
||||
utils.Debug("[STORAGE] Checking if " + path + " is a disk")
|
||||
|
||||
// if not proc or sys or dev or run
|
||||
if !strings.HasPrefix(path, "/mnt") ||
|
||||
if !strings.HasPrefix(path, "/mnt") &&
|
||||
!strings.HasPrefix(path, "/var/mnt") {
|
||||
continue
|
||||
}
|
||||
@@ -243,4 +245,5 @@ func IsDiskMounted(diskPath string) (bool, error) {
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user