[release] v0.15.9

This commit is contained in:
Yann Stepienik
2024-02-26 22:36:44 +00:00
parent 4e5645f9bb
commit f27e710aea
31 changed files with 488 additions and 146 deletions
+1
View File
@@ -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
+11 -1
View File
@@ -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)
}))
},
};
+1 -1
View File
@@ -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
View File
@@ -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>
</>
+3 -3
View File
@@ -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
+5 -2
View File
@@ -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"
+6 -2
View File
@@ -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' && (
+4 -1
View File
@@ -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>
+31 -5
View File
@@ -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>;
}
+113
View File
@@ -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;
+55
View File
@@ -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>}
</>
};
+3 -2
View File
@@ -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)}
+1 -1
View File
@@ -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>
+7 -8
View File
@@ -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 ||============================== //
+14 -24
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.15.0-unstable8",
"version": "0.15.0-unstable9",
"description": "",
"main": "test-server.js",
"bugs": {
+4 -1
View File
@@ -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 {
-1
View File
@@ -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()),
+1
View File
@@ -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))
+1
View File
@@ -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,
+115
View File
@@ -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
}
}
-72
View File
@@ -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
}
}
+88
View File
@@ -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
}
+5 -2
View File
@@ -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
}
}