Skip to content

Commit

Permalink
feat(app): Filter out available IPs using fetch testing
Browse files Browse the repository at this point in the history
  • Loading branch information
meowtec committed Apr 26, 2023
1 parent 7cdbcd3 commit 6c620e1
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 8 deletions.
19 changes: 12 additions & 7 deletions packages/launcher/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import Icon from '@meowtec/lansend-shared/components/icon';
import IC_DONE from '#/assets/icons/done.svg';
import IC_DOWN from '#/assets/icons/expand-down.svg';
import { invokes } from './bridge/invoke';
import { filterIP } from './utils/ip';
import { useDelaySwitch } from './utils/use-delay-switch';
import { safeLocalStorage } from './utils/storage';
import { useLocalIpList } from './utils/use-available-ip';

const FORM_VALUE_STORAGE_KEY = 'form-value';

Expand All @@ -34,7 +34,7 @@ function App() {
});
const [copied, setCopied] = useDelaySwitch(1000);

const { data: networkInterfaces } = useSWR('tauri:network_interfaces', () => invokes.get_netifas().then((list) => list.filter(filterIP)));
const ipList = useLocalIpList(formValue.port, isRunning ?? false);

const { ip, port } = formValue;
const numberPort = Number(port);
Expand Down Expand Up @@ -78,11 +78,16 @@ function App() {
};

useEffect(() => {
// if ip not in the list, set it to the first
if (networkInterfaces && !networkInterfaces.some((item) => item.ip === ip)) {
setIp(networkInterfaces[0].ip);
// if ip is empty or not available, set it to the first available ip
if (!ipList?.length) return;

if (
!ip
|| !ipList.some((item) => item.ip === ip)
) {
setIp(ipList[0].ip);
}
}, [ip, networkInterfaces]);
}, [ip, ipList]);

useEffect(() => {
safeLocalStorage.set(FORM_VALUE_STORAGE_KEY, formValue);
Expand Down Expand Up @@ -128,7 +133,7 @@ function App() {
onChange={(e) => setIp(e.target.value)}
>
<option value="" disabled>Select IP</option>
{networkInterfaces?.map((item) => (
{ipList?.map((item) => (
<option value={item.ip}>
{item.ip}
</option>
Expand Down
67 changes: 67 additions & 0 deletions packages/launcher/src/utils/use-available-ip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { invokes } from '#/bridge/invoke';
import { useEffect, useMemo, useState } from 'react';
import useSWR from 'swr';
import { filterIP } from './ip';

/**
* detect if the ip is available by sending a request to the server.
*/
const isIpAvailable = (ip: string, port: number | string) => fetch(`http://${ip}:${port}/api/ping`, {
credentials: 'omit',
}).then(() => true).catch(() => false);

/**
* cache the result or promise<result> of isIpAvailable.
* - if the result is true, cache the result.
* - if the result is pending, cache the promise.
*/
const ipAvailableCache = new Map<string, true | Promise<boolean>>();

/**
* cached version of isIpAvailable.
* Only cache when the result is true or the check is pending.
*/
const isIpAvailableCached = async (ip: string, port: number | string): Promise<boolean> => {
const cached = ipAvailableCache.get(ip);
if (cached) {
return cached;
}

const promise = isIpAvailable(ip, port).then((isAvailable) => {
if (isAvailable) {
ipAvailableCache.set(ip, true);
} else {
ipAvailableCache.delete(ip);
}
return isAvailable;
});

ipAvailableCache.set(ip, promise);

return promise;
};

export function useLocalIpList(port: number | string, isRunning: boolean) {
const { data: networkInterfaces } = useSWR(
'tauri:network_interfaces',
() => invokes.get_netifas().then((list) => list.filter(filterIP)),
);
const ipList = useMemo(() => networkInterfaces?.map((item) => item.ip) ?? [], [networkInterfaces]);

const [availableSet, setAvailableSet] = useState<Set<string> | null>(null);

useEffect(() => {
if (!isRunning) {
return;
}

void Promise.all(ipList.map((ip) => isIpAvailableCached(ip, port))).then((isAvailableList) => {
const availableList = ipList.filter((_, index) => isAvailableList[index]);
setAvailableSet(new Set(availableList));
});
}, [ipList, port, isRunning]);

return useMemo(() => networkInterfaces?.filter(
(ifa) => (availableSet ? availableSet.has(ifa.ip) : true),
), [availableSet, networkInterfaces]);
}
9 changes: 8 additions & 1 deletion packages/server/src/controllers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
use actix::Addr;
use actix_files::NamedFile;
use actix_session::Session;
use actix_web::{get, post, web, HttpRequest, HttpResponse};
use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
use actix_web_actors::ws;
use serde::Deserialize;

Expand All @@ -20,6 +20,13 @@ pub struct UploadQuery {
pub filename: String,
}

#[get("/ping")]
pub async fn ping() -> impl Responder {
HttpResponse::Ok()
.append_header(("Access-Control-Allow-Origin", "*"))
.body("pong")
}

#[post("/file/upload")]
pub async fn file_upload(
req: HttpRequest,
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ impl LansendServer {
.service(
web::scope("/api")
.app_data(web::Data::new(file_manager.clone()))
.service(controllers::ping)
.service(controllers::user_info)
.service(controllers::user_list)
.service(controllers::file_upload)
Expand Down

0 comments on commit 6c620e1

Please sign in to comment.