mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
docs: update README
This commit is contained in:
84
README.md
84
README.md
@@ -1,5 +1,83 @@
|
||||
# ironmount
|
||||
<div align="center">
|
||||
<h1>Ironmount</h1>
|
||||
<h3>Keep your volumes in check!<br />One interface to manage all your storage</h3>
|
||||
<a href="https://github.com/nicotsx/ironmount/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/nicotsx/ironmount" />
|
||||
</a>
|
||||
<br />
|
||||
<figure>
|
||||
<img src="https://github.com/nicotsx/ironmount/blob/main/screenshots/volume-details.png?raw=true" alt="Demo" />
|
||||
<figcaption>
|
||||
<p align="center">
|
||||
Volume details view with usage statistics and health check status
|
||||
</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
docker run --rm -it -v nicolas:/data alpine sh -lc 'echo hello > /data/hi && cat /data/hi'
|
||||
<br />
|
||||
|
||||
mount -t davfs http://192.168.2.42 /mnt/webdav
|
||||
## Intro
|
||||
|
||||
Ironmount is an easy to use web interface to manage your remote storage and mount them as local volumes on your server. Docker as a first class citizen, Ironmount allows you to easily mount your remote storage directly into your containers with few lines of code.
|
||||
|
||||
### Features
|
||||
|
||||
https://github.com/nicotsx/ironmount/blob/main/screenshots/volume-creation.png?raw=true
|
||||
|
||||
- ✅ Support for multiple protocols: NFS, SMB, FTP, Directory
|
||||
- 📡 Mount your remote storage as local folders
|
||||
- 🐳 Docker integration: mount your remote storage directly into your containers via a docker volume syntax
|
||||
- 🔍 Keep an eye on your mounts with health checks and automatic remounting on error
|
||||
- 📊 Monitor your mounts usage with detailed statistics and graphs
|
||||
|
||||
### Coming soon
|
||||
|
||||
- 🔐 User authentication and role management
|
||||
- 💾 Automated backups and snapshots with encryption, strategies and retention policies
|
||||
- 🔄 Re-exporting your mounts to other protocols (e.g. mount an FTP server as an SMB share with fine-grained permissions)
|
||||
- ☁️ Integration with cloud storage providers (e.g. AWS S3, Google Drive, Dropbox)
|
||||
- 🔀 Storage sharding and replication for high availability and performance
|
||||
|
||||
## Installation
|
||||
|
||||
In order to run Ironmount, you need to have Docker and Docker Compose installed on your server. Then, you can use the provided `docker-compose.yml` file to start the application.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ironmount:
|
||||
image: nicotsx/ironmount:v0.0.1
|
||||
container_name: ironmount
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- SYS_ADMIN
|
||||
ports:
|
||||
- "4096:4096"
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /run/docker/plugins:/run/docker/plugins
|
||||
- /var/lib/docker/volumes/:/var/lib/docker/volumes:rshared
|
||||
- ironmount_data:/data
|
||||
|
||||
volumes:
|
||||
ironmount_data:
|
||||
driver: local
|
||||
```
|
||||
|
||||
Then, run the following command to start Ironmount:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Once the container is running, you can access the web interface at `http://<your-server-ip>:4096`.
|
||||
|
||||
## Docker volume usage
|
||||
|
||||

|
||||
|
||||
## Volume creation
|
||||
|
||||

|
||||
|
||||
@@ -52,25 +52,20 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
form.reset({ name: watchedName, ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType] });
|
||||
}, [watchedBackend, watchedName, form.reset]);
|
||||
|
||||
const [testStatus, setTestStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
||||
const [testMessage, setTestMessage] = useState<string>("");
|
||||
|
||||
const testBackendConnection = useMutation({
|
||||
...testConnectionMutation(),
|
||||
onMutate: () => {
|
||||
setTestMessage("");
|
||||
setTestStatus("loading");
|
||||
},
|
||||
onError: () => {
|
||||
setTestStatus("error");
|
||||
setTestMessage("Failed to test connection. Please try again.");
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data?.success) {
|
||||
setTestStatus("success");
|
||||
setTestMessage(data.message);
|
||||
} else {
|
||||
setTestStatus("error");
|
||||
setTestMessage(data?.message || "Connection test failed");
|
||||
}
|
||||
},
|
||||
@@ -435,30 +430,24 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={
|
||||
testStatus === "loading" ||
|
||||
!form.watch("server") ||
|
||||
!form.watch("share") ||
|
||||
!form.watch("username") ||
|
||||
!form.watch("password")
|
||||
}
|
||||
disabled={testBackendConnection.isPending}
|
||||
className="flex-1"
|
||||
>
|
||||
{testStatus === "loading" && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{testStatus === "success" && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
||||
{testStatus === "error" && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
||||
{testStatus === "idle" && "Test Connection"}
|
||||
{testStatus === "loading" && "Testing..."}
|
||||
{testStatus === "success" && "Connection Successful"}
|
||||
{testStatus === "error" && "Test Failed"}
|
||||
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{testBackendConnection.isSuccess && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
||||
{testBackendConnection.isError && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
||||
{testBackendConnection.isIdle && "Test Connection"}
|
||||
{testBackendConnection.isPending && "Testing..."}
|
||||
{testBackendConnection.isSuccess && "Connection Successful"}
|
||||
{testBackendConnection.isError && "Test Failed"}
|
||||
</Button>
|
||||
</div>
|
||||
{testMessage && (
|
||||
<div
|
||||
className={`text-sm p-2 rounded-md ${
|
||||
testStatus === "success"
|
||||
testBackendConnection.isSuccess
|
||||
? "bg-green-50 text-green-700 border border-green-200"
|
||||
: testStatus === "error"
|
||||
: testBackendConnection.isError
|
||||
? "bg-red-50 text-red-700 border border-red-200"
|
||||
: "bg-gray-50 text-gray-700 border border-gray-200"
|
||||
}`}
|
||||
@@ -476,24 +465,24 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={testStatus === "loading" || !form.watch("server") || !form.watch("exportPath")}
|
||||
disabled={testBackendConnection.isPending}
|
||||
className="flex-1"
|
||||
>
|
||||
{testStatus === "loading" && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{testStatus === "success" && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
||||
{testStatus === "error" && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
||||
{testStatus === "idle" && "Test Connection"}
|
||||
{testStatus === "loading" && "Testing..."}
|
||||
{testStatus === "success" && "Connection Successful"}
|
||||
{testStatus === "error" && "Test Failed"}
|
||||
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{testBackendConnection.isSuccess && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
||||
{testBackendConnection.isError && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
||||
{testBackendConnection.isIdle && "Test Connection"}
|
||||
{testBackendConnection.isPending && "Testing..."}
|
||||
{testBackendConnection.isSuccess && "Connection Successful"}
|
||||
{testBackendConnection.isError && "Test Failed"}
|
||||
</Button>
|
||||
</div>
|
||||
{testMessage && (
|
||||
<div
|
||||
className={`text-sm p-2 rounded-md ${
|
||||
testStatus === "success"
|
||||
testBackendConnection.isSuccess
|
||||
? "bg-green-50 text-green-700 border border-green-200"
|
||||
: testStatus === "error"
|
||||
: testBackendConnection.isError
|
||||
? "bg-red-50 text-red-700 border border-red-200"
|
||||
: "bg-gray-50 text-gray-700 border border-gray-200"
|
||||
}`}
|
||||
@@ -511,24 +500,24 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={testStatus === "loading" || !form.watch("server") || !form.watch("path")}
|
||||
disabled={testBackendConnection.isPending}
|
||||
className="flex-1"
|
||||
>
|
||||
{testStatus === "loading" && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{testStatus === "success" && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
||||
{testStatus === "error" && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
||||
{testStatus === "idle" && "Test Connection"}
|
||||
{testStatus === "loading" && "Testing..."}
|
||||
{testStatus === "success" && "Connection Successful"}
|
||||
{testStatus === "error" && "Test Failed"}
|
||||
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{testBackendConnection.isSuccess && <CheckCircle className="mr-2 h-4 w-4 text-green-500" />}
|
||||
{testBackendConnection.isError && <XCircle className="mr-2 h-4 w-4 text-red-500" />}
|
||||
{testBackendConnection.isIdle && "Test Connection"}
|
||||
{testBackendConnection.isPending && "Testing..."}
|
||||
{testBackendConnection.isSuccess && "Connection Successful"}
|
||||
{testBackendConnection.isError && "Test Failed"}
|
||||
</Button>
|
||||
</div>
|
||||
{testMessage && (
|
||||
<div
|
||||
className={`text-sm p-2 rounded-md ${
|
||||
testStatus === "success"
|
||||
testBackendConnection.isSuccess
|
||||
? "bg-green-50 text-green-700 border border-green-200"
|
||||
: testStatus === "error"
|
||||
: testBackendConnection.isError
|
||||
? "bg-red-50 text-red-700 border border-red-200"
|
||||
: "bg-gray-50 text-gray-700 border border-gray-200"
|
||||
}`}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MutationCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
|
||||
import { Toaster } from "~/components/ui/sonner";
|
||||
|
||||
@@ -52,7 +51,6 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</html>
|
||||
);
|
||||
|
||||
4
notes.md
Normal file
4
notes.md
Normal file
@@ -0,0 +1,4 @@
|
||||
docker run --rm -it -v nicolas:/data alpine sh -lc 'echo hello > /data/hi && cat /data/hi'
|
||||
|
||||
mount -t davfs http://192.168.2.42 /mnt/webdav
|
||||
|
||||
BIN
screenshots/docker-instructions.png
Normal file
BIN
screenshots/docker-instructions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/volume-creation.png
Normal file
BIN
screenshots/volume-creation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
screenshots/volume-details.png
Normal file
BIN
screenshots/volume-details.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Reference in New Issue
Block a user