mirror of
https://github.com/ivabus/gui
synced 2025-04-23 14:07:14 +03:00
watch directory for newly installed packages (#624)
This commit is contained in:
parent
6f4725d21c
commit
ec166132c8
9 changed files with 163 additions and 126 deletions
|
@ -1,5 +1,12 @@
|
|||
import { ipcMain, app, BrowserWindow } from "electron";
|
||||
import { deletePackageFolder, getInstalledPackages, cacheImage } from "./tea-dir";
|
||||
import {
|
||||
deletePackageFolder,
|
||||
getInstalledPackages,
|
||||
getInstalledVersionsForPackage,
|
||||
cacheImage,
|
||||
startMonitoringTeaDir,
|
||||
stopMonitoringTeaDir
|
||||
} from "./tea-dir";
|
||||
import { readSessionData, writeSessionData, pollAuth } from "./auth";
|
||||
import type { Packages, Session } from "../../src/libs/types";
|
||||
import log from "./logger";
|
||||
|
@ -37,6 +44,16 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions)
|
|||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-installed-package-versions", async (_, fullName: string) => {
|
||||
try {
|
||||
log.info(`getting installed versions for package: ${fullName}`);
|
||||
return await getInstalledVersionsForPackage(fullName);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-session", async () => {
|
||||
try {
|
||||
log.info("getting session");
|
||||
|
@ -230,4 +247,22 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions)
|
|||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("monitor-tea-dir", async () => {
|
||||
try {
|
||||
await startMonitoringTeaDir(notifyMainWindow);
|
||||
} catch (err) {
|
||||
log.error("Failed to monitor tea dir", err);
|
||||
return err;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("stop-monitor-tea-dir", async () => {
|
||||
try {
|
||||
await stopMonitoringTeaDir();
|
||||
} catch (err) {
|
||||
log.error("Failed to stop monitoring tea dir", err);
|
||||
return err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// import { readDir, BaseDirectory } from '@tauri-apps/api/fs';
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { app } from "electron";
|
||||
|
@ -8,12 +7,8 @@ import { mkdirp } from "mkdirp";
|
|||
import fetch from "node-fetch";
|
||||
import { SemVer, isValidSemVer } from "@tea/libtea";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
type Dir = {
|
||||
name: string;
|
||||
path: string;
|
||||
children?: Dir[];
|
||||
};
|
||||
import chokidar from "chokidar";
|
||||
import { MainWindowNotifier } from "./types";
|
||||
|
||||
type ParsedVersion = { full_name: string; semVer: SemVer };
|
||||
|
||||
|
@ -38,9 +33,18 @@ export const getGuiPath = () => {
|
|||
return path.join(getTeaPath(), "tea.xyz/gui");
|
||||
};
|
||||
|
||||
export async function getInstalledPackages(): Promise<InstalledPackage[]> {
|
||||
const pkgsPath = getTeaPath();
|
||||
export async function getInstalledVersionsForPackage(fullName: string): Promise<InstalledPackage> {
|
||||
const pkgsPath = path.join(getTeaPath(), fullName);
|
||||
const result = await findInstalledVersions(pkgsPath);
|
||||
const pkg = result.find((v) => v.full_name === fullName);
|
||||
return pkg ?? { full_name: fullName, installed_versions: [] };
|
||||
}
|
||||
|
||||
export async function getInstalledPackages(): Promise<InstalledPackage[]> {
|
||||
return findInstalledVersions(getTeaPath());
|
||||
}
|
||||
|
||||
async function findInstalledVersions(pkgsPath: string): Promise<InstalledPackage[]> {
|
||||
if (!fs.existsSync(pkgsPath)) {
|
||||
log.info(`packages path ${pkgsPath} does not exist, no installed packages`);
|
||||
return [];
|
||||
|
@ -89,33 +93,6 @@ const parseVersionFromPath = (versionPath: string): ParsedVersion | null => {
|
|||
}
|
||||
};
|
||||
|
||||
const semverTest =
|
||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g;
|
||||
|
||||
export const getPkgBottles = (packageDir: Dir): string[] => {
|
||||
log.info("getting installed bottle for ", packageDir);
|
||||
const bottles: string[] = [];
|
||||
|
||||
const pkg = packageDir.path.split(".tea/")[1];
|
||||
const version = pkg.split("/v")[1];
|
||||
|
||||
const isVersion = semverTest.test(version) || !isNaN(+version) || version === "*";
|
||||
|
||||
if (version && isVersion) {
|
||||
bottles.push(pkg);
|
||||
} else if (packageDir?.children?.length) {
|
||||
const childBottles = packageDir.children
|
||||
.map(getPkgBottles)
|
||||
.reduce((arr, bottles) => [...arr, ...bottles], []);
|
||||
bottles.push(...childBottles);
|
||||
}
|
||||
|
||||
const foundBottles = bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
|
||||
|
||||
log.info(`Found ${foundBottles.length} bottles from `, packageDir);
|
||||
return foundBottles;
|
||||
};
|
||||
|
||||
export const deepReadDir = async ({
|
||||
dir,
|
||||
continueDeeper,
|
||||
|
@ -181,7 +158,7 @@ export async function deletePackageFolder(fullName, version) {
|
|||
try {
|
||||
const foldPath = path.join(getTeaPath(), fullName, `v${version}`);
|
||||
log.info("rm:", foldPath);
|
||||
await fs.rmdirSync(foldPath, { recursive: true });
|
||||
await fs.rmSync(foldPath, { recursive: true });
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
@ -217,3 +194,51 @@ export async function cacheImage(url: string): Promise<string> {
|
|||
|
||||
return `file://${imagePath}`;
|
||||
}
|
||||
|
||||
let watcher: chokidar.FSWatcher | null = null;
|
||||
|
||||
export async function startMonitoringTeaDir(mainWindowNotifier: MainWindowNotifier) {
|
||||
if (watcher) {
|
||||
log.info("Watcher already started");
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = path.join(getTeaPath(), "**/v*");
|
||||
log.info(`Start monitoring tea dir: ${dir}}`);
|
||||
|
||||
watcher = chokidar.watch(dir, {
|
||||
ignoreInitial: true,
|
||||
persistent: true,
|
||||
followSymlinks: false,
|
||||
depth: 5,
|
||||
ignored: ["**/var/pantry/projects/**", "**/local/tmp/**", "**/share/**"]
|
||||
});
|
||||
|
||||
watcher
|
||||
.on("addDir", (pth) => {
|
||||
const dir = path.dirname(pth);
|
||||
const version = path.basename(pth);
|
||||
if (isValidSemVer(version) && !fs.lstatSync(pth).isSymbolicLink()) {
|
||||
const full_name = dir.split(".tea/")[1];
|
||||
log.info(`Monitor - Added Package: ${full_name} v${version}`);
|
||||
mainWindowNotifier("pkg-modified", { full_name, version, type: "add" });
|
||||
}
|
||||
})
|
||||
.on("unlinkDir", (pth) => {
|
||||
// FIXME: unlinkDir does not always fire, this is a bug in chokidar
|
||||
const dir = path.dirname(pth);
|
||||
const version = path.basename(pth);
|
||||
if (isValidSemVer(version)) {
|
||||
const full_name = dir.split(".tea/")[1];
|
||||
log.info(`Monitor - Removed Package: ${full_name} v${version}`);
|
||||
mainWindowNotifier("pkg-modified", { full_name, version, type: "remove" });
|
||||
}
|
||||
})
|
||||
.on("error", (error) => log.error(`Watcher error: ${error}`));
|
||||
}
|
||||
|
||||
export async function stopMonitoringTeaDir() {
|
||||
log.info("Stop monitoring tea dir");
|
||||
await watcher?.close();
|
||||
watcher = null;
|
||||
}
|
||||
|
|
|
@ -95,6 +95,7 @@
|
|||
"axios": "^1.3.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"buffer": "^6.0.3",
|
||||
"chokidar": "^3.5.3",
|
||||
"custom-electron-titlebar": "4.2.0-beta.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"electron-context-menu": "^3.6.1",
|
||||
|
|
|
@ -48,6 +48,15 @@ export async function getInstalledPackages(): Promise<InstalledPackage[]> {
|
|||
return pkgs;
|
||||
}
|
||||
|
||||
export async function getInstalledVersionsForPackage(fullName: string): Promise<InstalledPackage> {
|
||||
log.info("getting installed versions for package: ", fullName);
|
||||
const result = await ipcRenderer.invoke("get-installed-package-versions", fullName);
|
||||
if (result instanceof Error) {
|
||||
throw result;
|
||||
}
|
||||
return result as InstalledPackage;
|
||||
}
|
||||
|
||||
export async function getPackages(): Promise<GUIPackage[]> {
|
||||
const [packages, installedPackages] = await Promise.all([
|
||||
getDistPackages(),
|
||||
|
@ -278,3 +287,17 @@ export const isDev = async () => {
|
|||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const monitorTeaDir = async () => {
|
||||
const result = await ipcRenderer.invoke("monitor-tea-dir");
|
||||
if (result instanceof Error) {
|
||||
throw result;
|
||||
}
|
||||
};
|
||||
|
||||
export const stopMonitoringTeaDir = async () => {
|
||||
const result = await ipcRenderer.invoke("stop-monitor-tea-dir");
|
||||
if (result instanceof Error) {
|
||||
throw result;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -202,6 +202,13 @@ export async function getDistPackages(): Promise<Package[]> {
|
|||
return packages;
|
||||
}
|
||||
|
||||
export async function getInstalledVersionsForPackage(full_name: string): Promise<Package> {
|
||||
return (packages.find((pkg) => pkg.full_name === full_name) ?? {
|
||||
full_name,
|
||||
installed_versions: []
|
||||
}) as Package;
|
||||
}
|
||||
|
||||
export async function getPackages(): Promise<GUIPackage[]> {
|
||||
return packages.map((pkg) => {
|
||||
return {
|
||||
|
@ -399,3 +406,11 @@ export async function openPackageEntrypointInTerminal(pkg: string) {
|
|||
export const pollDeviceSession = async () => {
|
||||
console.log("do nothing");
|
||||
};
|
||||
|
||||
export const monitorTeaDir = async () => {
|
||||
console.log("do nothing");
|
||||
};
|
||||
|
||||
export const stopMonitoringTeaDir = async () => {
|
||||
console.log("do nothing");
|
||||
};
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import { getPkgBottles } from "../tea-dir";
|
||||
|
||||
describe("tea-dir module", () => {
|
||||
it("should getPkgBottles from nested Dir object/s", () => {
|
||||
const results = getPkgBottles({
|
||||
name: "kkos",
|
||||
path: "/Users/x/.tea/github.com/kkos",
|
||||
children: [
|
||||
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/.DS_Store" },
|
||||
{
|
||||
name: "oniguruma",
|
||||
path: "/Users/x/.tea/github.com/kkos/oniguruma",
|
||||
children: [
|
||||
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/.DS_Store" },
|
||||
{
|
||||
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6",
|
||||
name: "v6",
|
||||
children: [
|
||||
{ name: ".DS_Store", path: "/Users/x/.tea/github.com/kkos/oniguruma/v6/.DS_Store" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "v*",
|
||||
path: "/Users/x/.tea/github.com/kkos/oniguruma/v*",
|
||||
children: []
|
||||
},
|
||||
{
|
||||
name: "v6.9.8",
|
||||
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9.8",
|
||||
children: []
|
||||
},
|
||||
{
|
||||
name: "v6.9",
|
||||
path: "/Users/x/.tea/github.com/kkos/oniguruma/v6.9",
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
"github.com/kkos/oniguruma/v*",
|
||||
"github.com/kkos/oniguruma/v6",
|
||||
"github.com/kkos/oniguruma/v6.9",
|
||||
"github.com/kkos/oniguruma/v6.9.8"
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
// import { app } from 'electron';
|
||||
// import fs from 'fs';
|
||||
// import { join } from 'upath';
|
||||
|
||||
type Dir = {
|
||||
name: string;
|
||||
path: string;
|
||||
children?: Dir[];
|
||||
};
|
||||
|
||||
const semverTest =
|
||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g;
|
||||
|
||||
export const getPkgBottles = (packageDir: Dir): string[] => {
|
||||
const bottles: string[] = [];
|
||||
|
||||
const pkg = packageDir.path.split(".tea/")[1];
|
||||
const version = pkg.split("/v")[1];
|
||||
|
||||
const isVersion = semverTest.test(version) || !isNaN(+version) || version === "*";
|
||||
|
||||
if (version && isVersion) {
|
||||
bottles.push(pkg);
|
||||
} else if (packageDir?.children?.length) {
|
||||
const childBottles = packageDir.children
|
||||
.map(getPkgBottles)
|
||||
.reduce((arr, bottles) => [...arr, ...bottles], []);
|
||||
bottles.push(...childBottles);
|
||||
}
|
||||
|
||||
return bottles.filter((b) => b !== undefined).sort(); // ie: ["gohugo.io/v*", "gohugo.io/v0", "gohugo.io/v0.108", "gohugo.io/v0.108.0"]
|
||||
};
|
|
@ -12,7 +12,10 @@ import {
|
|||
writePackageCache,
|
||||
syncPantry,
|
||||
cacheImageURL,
|
||||
listenToChannel
|
||||
listenToChannel,
|
||||
getInstalledVersionsForPackage,
|
||||
monitorTeaDir,
|
||||
stopMonitoringTeaDir
|
||||
} from "@native";
|
||||
|
||||
import { getReadme, getContributors, getRepoAsPackage } from "$libs/github";
|
||||
|
@ -26,7 +29,6 @@ import withRetry from "$libs/utils/retry";
|
|||
|
||||
import log from "$libs/logger";
|
||||
import { isPackageUpToDate } from "../packages/pkg-utils";
|
||||
import withDelay from "$libs/utils/delay";
|
||||
|
||||
import { indexPackages, searchPackages } from "$libs/search-index";
|
||||
|
||||
|
@ -150,6 +152,7 @@ const init = async function () {
|
|||
packageMap.set(cachedPkgs);
|
||||
|
||||
await refreshPackages();
|
||||
await monitorTeaDir();
|
||||
initialized = true;
|
||||
}
|
||||
log.info("packages store: initialized!");
|
||||
|
@ -201,12 +204,20 @@ const refreshPackages = async () => {
|
|||
refreshTimeoutId = setTimeout(() => refreshPackages(), packageRefreshInterval); // refresh every hour
|
||||
};
|
||||
|
||||
const refreshSinglePackage = async (fullName: string) => {
|
||||
log.info(`refreshing single package: ${fullName}`);
|
||||
const result = await getInstalledVersionsForPackage(fullName);
|
||||
log.info(`package: ${fullName} has installed versions:`, result.installed_versions);
|
||||
updatePackage(fullName, { installed_versions: result.installed_versions });
|
||||
};
|
||||
|
||||
// Destructor for the package store
|
||||
const destroy = () => {
|
||||
const destroy = async () => {
|
||||
isDestroyed = true;
|
||||
if (refreshTimeoutId) {
|
||||
clearTimeout(refreshTimeoutId);
|
||||
}
|
||||
await stopMonitoringTeaDir();
|
||||
log.info("packages store: destroyed");
|
||||
};
|
||||
|
||||
|
@ -218,7 +229,7 @@ const installPkg = async (pkg: GUIPackage, version?: string) => {
|
|||
await installPackage(pkg, versionToInstall);
|
||||
trackInstall(pkg.full_name);
|
||||
|
||||
await refreshPackages(); // helps e2e tests might not be the most efficient but helps
|
||||
await refreshSinglePackage(pkg.full_name);
|
||||
|
||||
notificationStore.add({
|
||||
message: `Package ${pkg.full_name} v${versionToInstall} has been installed.`
|
||||
|
@ -252,9 +263,7 @@ const uninstallPkg = async (pkg: GUIPackage) => {
|
|||
await deletePkg(pkg, v);
|
||||
}
|
||||
|
||||
updatePackage(pkg.full_name, {
|
||||
installed_versions: []
|
||||
});
|
||||
await refreshSinglePackage(pkg.full_name);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
notificationStore.add({
|
||||
|
@ -300,6 +309,7 @@ listenToChannel("install-progress", ({ full_name, progress }: any) => {
|
|||
updatePackage(full_name, { install_progress_percentage: progress });
|
||||
});
|
||||
|
||||
// TODO: perhaps this can be combined with pkg-modified?
|
||||
listenToChannel("pkg-installed", ({ full_name, version }: any) => {
|
||||
if (!full_name) {
|
||||
return;
|
||||
|
@ -307,6 +317,13 @@ listenToChannel("pkg-installed", ({ full_name, version }: any) => {
|
|||
updatePackage(full_name, {}, version);
|
||||
});
|
||||
|
||||
listenToChannel("pkg-modified", ({ full_name }: any) => {
|
||||
if (!full_name) {
|
||||
return;
|
||||
}
|
||||
refreshSinglePackage(full_name);
|
||||
});
|
||||
|
||||
// This is only used for uninstall now
|
||||
export const withFakeLoader = (
|
||||
pkg: GUIPackage,
|
||||
|
|
|
@ -52,6 +52,7 @@ importers:
|
|||
axios: ^1.3.2
|
||||
bcryptjs: ^2.4.3
|
||||
buffer: ^6.0.3
|
||||
chokidar: ^3.5.3
|
||||
concurrently: ^7.6.0
|
||||
cross-env: ^7.0.3
|
||||
custom-electron-titlebar: 4.2.0-beta.0
|
||||
|
@ -115,6 +116,7 @@ importers:
|
|||
axios: 1.4.0
|
||||
bcryptjs: 2.4.3
|
||||
buffer: 6.0.3
|
||||
chokidar: 3.5.3
|
||||
custom-electron-titlebar: 4.2.0-beta.0_electron@22.1.0
|
||||
dayjs: 1.11.7
|
||||
electron-context-menu: 3.6.1
|
||||
|
|
Loading…
Reference in a new issue