mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
allow mdns cifs shares (#933)
* allow mdns cifs shares * regex and messaging for cifs * Update install-wizard.component.ts * refactor * always include leading slash when saving path * add avahi-utils Co-authored-by: Matt Hill <matthewonthemoon@gmail.com> Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
@@ -47,7 +47,7 @@ pub async fn add(
|
|||||||
let guard = TmpMountGuard::mount(&cifs).await?;
|
let guard = TmpMountGuard::mount(&cifs).await?;
|
||||||
let embassy_os = recovery_info(&guard).await?;
|
let embassy_os = recovery_info(&guard).await?;
|
||||||
guard.unmount().await?;
|
guard.unmount().await?;
|
||||||
let path_string = cifs.path.display().to_string();
|
let path_string = Path::new("/").join(&cifs.path).display().to_string();
|
||||||
let id: u32 = sqlx::query!(
|
let id: u32 = sqlx::query!(
|
||||||
"INSERT INTO cifs_shares (hostname, path, username, password) VALUES (?, ?, ?, ?) RETURNING id AS \"id: u32\"",
|
"INSERT INTO cifs_shares (hostname, path, username, password) VALUES (?, ?, ?, ?) RETURNING id AS \"id: u32\"",
|
||||||
cifs.hostname,
|
cifs.hostname,
|
||||||
@@ -95,7 +95,7 @@ pub async fn update(
|
|||||||
let guard = TmpMountGuard::mount(&cifs).await?;
|
let guard = TmpMountGuard::mount(&cifs).await?;
|
||||||
let embassy_os = recovery_info(&guard).await?;
|
let embassy_os = recovery_info(&guard).await?;
|
||||||
guard.unmount().await?;
|
guard.unmount().await?;
|
||||||
let path_string = cifs.path.display().to_string();
|
let path_string = Path::new("/").join(&cifs.path).display().to_string();
|
||||||
if sqlx::query!(
|
if sqlx::query!(
|
||||||
"UPDATE cifs_shares SET hostname = ?, path = ?, username = ?, password = ? WHERE id = ?",
|
"UPDATE cifs_shares SET hostname = ?, path = ?, username = ?, password = ? WHERE id = ?",
|
||||||
cifs.hostname,
|
cifs.hostname,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::os::unix::ffi::OsStrExt;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use color_eyre::eyre::eyre;
|
||||||
use digest::generic_array::GenericArray;
|
use digest::generic_array::GenericArray;
|
||||||
use digest::Digest;
|
use digest::Digest;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -12,6 +13,7 @@ use tracing::instrument;
|
|||||||
|
|
||||||
use super::FileSystem;
|
use super::FileSystem;
|
||||||
use crate::disk::mount::guard::TmpMountGuard;
|
use crate::disk::mount::guard::TmpMountGuard;
|
||||||
|
use crate::net::mdns::resolve_mdns;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
@@ -24,16 +26,21 @@ pub async fn mount_cifs(
|
|||||||
mountpoint: impl AsRef<Path>,
|
mountpoint: impl AsRef<Path>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
||||||
let ip: IpAddr = String::from_utf8(
|
let ip: IpAddr = if hostname.ends_with(".local") {
|
||||||
Command::new("nmblookup")
|
resolve_mdns(hostname).await?
|
||||||
.arg(hostname)
|
} else {
|
||||||
.invoke(crate::ErrorKind::Network)
|
String::from_utf8(
|
||||||
.await?,
|
Command::new("nmblookup")
|
||||||
)?
|
.arg(hostname)
|
||||||
.split(" ")
|
.invoke(crate::ErrorKind::Network)
|
||||||
.next()
|
.await?,
|
||||||
.unwrap()
|
)?
|
||||||
.parse()?;
|
.split(" ")
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.trim()
|
||||||
|
.parse()?
|
||||||
|
};
|
||||||
let absolute_path = Path::new("/").join(path.as_ref());
|
let absolute_path = Path::new("/").join(path.as_ref());
|
||||||
Command::new("mount")
|
Command::new("mount")
|
||||||
.arg("-t")
|
.arg("-t")
|
||||||
|
|||||||
@@ -1,16 +1,40 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use avahi_sys::{
|
use avahi_sys::{
|
||||||
self, avahi_client_errno, avahi_entry_group_add_service, avahi_entry_group_commit,
|
self, avahi_client_errno, avahi_entry_group_add_service, avahi_entry_group_commit,
|
||||||
avahi_entry_group_free, avahi_entry_group_reset, avahi_free, avahi_strerror, AvahiClient,
|
avahi_entry_group_free, avahi_entry_group_reset, avahi_free, avahi_strerror, AvahiClient,
|
||||||
AvahiEntryGroup,
|
AvahiEntryGroup,
|
||||||
};
|
};
|
||||||
|
use color_eyre::eyre::eyre;
|
||||||
use libc::c_void;
|
use libc::c_void;
|
||||||
|
use tokio::process::Command;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use torut::onion::TorSecretKeyV3;
|
use torut::onion::TorSecretKeyV3;
|
||||||
|
|
||||||
use super::interface::InterfaceId;
|
use super::interface::InterfaceId;
|
||||||
use crate::s9pk::manifest::PackageId;
|
use crate::s9pk::manifest::PackageId;
|
||||||
|
use crate::util::Invoke;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
pub async fn resolve_mdns(hostname: &str) -> Result<IpAddr, Error> {
|
||||||
|
Ok(String::from_utf8(
|
||||||
|
Command::new("avahi-resolve-host-name")
|
||||||
|
.arg(hostname)
|
||||||
|
.invoke(crate::ErrorKind::Network)
|
||||||
|
.await?,
|
||||||
|
)?
|
||||||
|
.split_once("\t")
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::new(
|
||||||
|
eyre!("Failed to resolve hostname: {}", hostname),
|
||||||
|
crate::ErrorKind::Network,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.1
|
||||||
|
.trim()
|
||||||
|
.parse()?)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MdnsController(Mutex<MdnsControllerInner>);
|
pub struct MdnsController(Mutex<MdnsControllerInner>);
|
||||||
impl MdnsController {
|
impl MdnsController {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ apt-get install -y \
|
|||||||
nginx \
|
nginx \
|
||||||
libavahi-client3 \
|
libavahi-client3 \
|
||||||
avahi-daemon \
|
avahi-daemon \
|
||||||
|
avahi-utils \
|
||||||
iotop \
|
iotop \
|
||||||
bmon \
|
bmon \
|
||||||
exfat-utils \
|
exfat-utils \
|
||||||
|
|||||||
@@ -16,11 +16,12 @@
|
|||||||
[(ngModel)]="cifs.hostname"
|
[(ngModel)]="cifs.hostname"
|
||||||
name="hostname"
|
name="hostname"
|
||||||
#hostname="ngModel"
|
#hostname="ngModel"
|
||||||
placeholder="e.g. My Computer, Bob's Laptop"
|
placeholder="e.g. 'My Computer' OR 'my-computer.local'"
|
||||||
|
pattern="^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$"
|
||||||
></ion-input>
|
></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<p [hidden]="hostname.valid || hostname.pristine">
|
<p [hidden]="hostname.valid || hostname.pristine">
|
||||||
<ion-text color="danger">Hostname is required</ion-text>
|
<ion-text color="danger">Hostname is required. e.g. 'My Computer' OR 'my-computer.local'</ion-text>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>Path *</p>
|
<p>Path *</p>
|
||||||
|
|||||||
@@ -77,8 +77,3 @@ export class CifsModal {
|
|||||||
alert.present()
|
alert.present()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MappedCifs {
|
|
||||||
hasValidBackup: boolean
|
|
||||||
cifs: CifsBackupTarget
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<p class="ion-padding-bottom">
|
<p class="ion-padding-bottom">
|
||||||
Using a shared folder is the recommended way to recover from backup, since it works with all Embassy hardware configurations.
|
Using a shared folder is the recommended way to recover from backup, since it works with all Embassy hardware configurations.
|
||||||
To restore from a shared folder, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html" target="blank" noreferrer>instructions</a>.
|
To restore from a shared folder, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html#shared-network-folder" target="_blank" noreferrer>instructions</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- connect -->
|
<!-- connect -->
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<p class="ion-padding-bottom">
|
<p class="ion-padding-bottom">
|
||||||
Warning! Plugging in more than one physical drive to Embassy can lead to power failure and data corruption.
|
Warning! Plugging in more than one physical drive to Embassy can lead to power failure and data corruption.
|
||||||
To restore from a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html" target="blank" noreferrer>instructions</a>.
|
To restore from a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html#physical-drive" target="_blank" noreferrer>instructions</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ng-container *ngFor="let mapped of mappedDrives">
|
<ng-container *ngFor="let mapped of mappedDrives">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>
|
<h2>
|
||||||
Shared folders are the recommended way to create Embassy backups.
|
Shared folders are the recommended way to create Embassy backups. View the <a href="https://docs.start9.com/user-manual/general/backups.html#shared-network-folder" target="_blank" noreferrer>Instructions</a>
|
||||||
</h2>
|
</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<br />
|
<br />
|
||||||
<h2>
|
<h2>
|
||||||
To backup to a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html" target="blank" noreferrer>instructions</a>.
|
To backup to a physical drive, please follow the <a href="https://docs.start9.com/user-manual/general/backups.html#physical-drive" target="_blank" noreferrer>instructions</a>.
|
||||||
</h2>
|
</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|||||||
@@ -238,8 +238,10 @@ const CifsSpec: ConfigSpec = {
|
|||||||
hostname: {
|
hostname: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'Hostname',
|
name: 'Hostname',
|
||||||
description: 'The local URL of the shared folder.',
|
description: 'The hostname of your target device on the Local Area Network.',
|
||||||
placeholder: `e.g. My Computer, Bob's Laptop`,
|
placeholder: `e.g. 'My Computer' OR 'my-computer.local'`,
|
||||||
|
pattern: '^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$',
|
||||||
|
'pattern-description': `Must be a valid hostname. e.g. 'My Computer' OR 'my-computer.local'`,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
masked: false,
|
masked: false,
|
||||||
copyable: false,
|
copyable: false,
|
||||||
@@ -247,7 +249,7 @@ const CifsSpec: ConfigSpec = {
|
|||||||
path: {
|
path: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'Path',
|
name: 'Path',
|
||||||
description: 'The path to the shared folder on the target device.',
|
description: 'The directory path to the shared folder on your target device.',
|
||||||
placeholder: 'e.g. /Desktop/my-folder',
|
placeholder: 'e.g. /Desktop/my-folder',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
masked: false,
|
masked: false,
|
||||||
|
|||||||
@@ -277,7 +277,8 @@ export class WizardBaker {
|
|||||||
whileLoading: { },
|
whileLoading: { },
|
||||||
afterLoading: { text: 'Cancel' },
|
afterLoading: { text: 'Cancel' },
|
||||||
},
|
},
|
||||||
next: 'Stop Service' },
|
next: 'Stop Service',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slide: {
|
slide: {
|
||||||
|
|||||||
@@ -6,14 +6,16 @@
|
|||||||
>
|
>
|
||||||
<span *ngIf= "!installProgress">
|
<span *ngIf= "!installProgress">
|
||||||
{{ disconnected ? 'Unknown' : rendering.display }}
|
{{ disconnected ? 'Unknown' : rendering.display }}
|
||||||
<span *ngIf="rendering.showDots">...</span>
|
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="installProgress">
|
<span *ngIf="installProgress">
|
||||||
<span *ngIf="installProgress < 99">
|
<span *ngIf="installProgress < 99">
|
||||||
Installing... {{ installProgress }}%
|
Installing
|
||||||
|
<span class="loading-dots"></span>{{ installProgress }}%
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="installProgress >= 99">
|
<span *ngIf="installProgress >= 99">
|
||||||
Finalizing install. This could take a minute...
|
Finalizing install. This could take a minute
|
||||||
|
<span class="loading-dots"></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -109,13 +109,16 @@
|
|||||||
<ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1" color="warning">Update Available</ion-text>
|
<ion-text *ngIf="(pkg.manifest.version | compareEmver : localPkg.manifest.version) === 1" color="warning">Update Available</ion-text>
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
|
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
|
||||||
<ion-text color="primary">
|
<ion-text color="primary" *ngIf="(localPkg['install-progress'] | installState) as progress">
|
||||||
<span>{{ (localPkg['install-progress'] | installState).display }}</span>
|
Installing
|
||||||
|
<span class="loading-dots"></span>{{ progress.totalProgress < 99 ? progress.totalProgress + '%' : 'finalizing' }}
|
||||||
</ion-text>
|
</ion-text>
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="localPkg.state === PackageState.Removing">
|
<p *ngIf="localPkg.state === PackageState.Removing">
|
||||||
<ion-text color="warning">{{ localPkg.state | titlecase }}</ion-text>
|
<ion-text color="danger">
|
||||||
<ion-spinner name="dots" color="warning"></ion-spinner>
|
Removing
|
||||||
|
<span class="loading-dots"></span>
|
||||||
|
</ion-text>
|
||||||
</p>
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #none>
|
<ng-template #none>
|
||||||
|
|||||||
@@ -32,12 +32,17 @@
|
|||||||
</p>
|
</p>
|
||||||
<!-- installing, updating -->
|
<!-- installing, updating -->
|
||||||
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
|
<p *ngIf="[PackageState.Installing, PackageState.Updating] | includes : localPkg.state">
|
||||||
<ion-text color="primary">{{ (localPkg['install-progress'] | installState).display }}</ion-text>
|
<ion-text color="primary" *ngIf="(localPkg['install-progress'] | installState) as progress">
|
||||||
|
Installing
|
||||||
|
<span class="loading-dots"></span>{{ progress.totalProgress < 99 ? progress.totalProgress + '%' : 'finalizing' }}
|
||||||
|
</ion-text>
|
||||||
</p>
|
</p>
|
||||||
<!-- removing -->
|
<!-- removing -->
|
||||||
<p *ngIf="localPkg.state === PackageState.Removing">
|
<p *ngIf="localPkg.state === PackageState.Removing">
|
||||||
<ion-text color="warning">{{ localPkg.state | titlecase }}</ion-text>
|
<ion-text color="danger">
|
||||||
<ion-spinner name="dots" color="warning"></ion-spinner>
|
Removing
|
||||||
|
<span class="loading-dots"></span>
|
||||||
|
</ion-text>
|
||||||
</p>
|
</p>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,9 +119,6 @@ const sshSpec = {
|
|||||||
name: 'SSH Key',
|
name: 'SSH Key',
|
||||||
description: 'Enter the SSH public key of you would like to authorize for root access to your Embassy.',
|
description: 'Enter the SSH public key of you would like to authorize for root access to your Embassy.',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
// @TODO regex for SSH Key
|
|
||||||
// pattern: '',
|
|
||||||
'pattern-description': 'Must be a valid SSH key',
|
|
||||||
masked: false,
|
masked: false,
|
||||||
copyable: false,
|
copyable: false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export function packageLoadingProgress (
|
|||||||
validateProgress: Math.floor((100 * validated) / size),
|
validateProgress: Math.floor((100 * validated) / size),
|
||||||
unpackProgress: Math.floor((100 * unpacked) / size),
|
unpackProgress: Math.floor((100 * unpacked) / size),
|
||||||
isComplete: downloadComplete && validationComplete && unpackComplete,
|
isComplete: downloadComplete && validationComplete && unpackComplete,
|
||||||
display: totalProgress > 98 ? 'Finalizing...' : `Installing... ${totalProgress}%`,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,5 +49,4 @@ export interface ProgressData {
|
|||||||
validateProgress: number
|
validateProgress: number
|
||||||
unpackProgress: number
|
unpackProgress: number
|
||||||
isComplete: boolean
|
isComplete: boolean
|
||||||
display: string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,3 +311,28 @@ ion-loading {
|
|||||||
h2 {
|
h2 {
|
||||||
line-height: unset;
|
line-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-dots:after {
|
||||||
|
content: "...";
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
animation: ellipsis-dot 1s infinite .3s;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ellipsis-dot {
|
||||||
|
25% {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
content: ".";
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
content: "..";
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
content: "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user