Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/lib/client/action/repositories/project.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ export class ProjectRepository extends BaseRepository {
getGatewayProjects(): Promise<GatewayProjectResult[]> {
return this.run(`/actions/project?/get-gateway-projects`);
}

syncGatewayProject(): Promise<GatewayProjectResult[]> {
return this.run(`/actions/project?/sync-gateway-project`);
}
}
20 changes: 20 additions & 0 deletions src/lib/client/project/save-handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type Writable, get, writable } from 'svelte/store';

import { getConfig } from '$lib/client/config';
import type { Project } from '$lib/client/project';

import type { Save } from '@utils/types';
Expand All @@ -15,6 +16,7 @@ export class SaveHandler {
private _readyToSync = true;
private _syncTimer?: Timer;
private _needSync: Writable<boolean> = writable(false);
private _syncGatewayEnable?: boolean = true;

constructor(project: Project) {
this._project = project;
Expand All @@ -28,6 +30,9 @@ export class SaveHandler {
this._save.subscribe(() => {
this.syncToServer();
});
if (getConfig().mode === 'online') {
this.initSyncGateway();
}
}

async fetchFromServer() {
Expand Down Expand Up @@ -102,4 +107,19 @@ export class SaveHandler {
void this.syncToServer();
});
}

private initSyncGateway() {
setInterval(
async () => {
if (this._syncGatewayEnable) {
await this._project.actions.project.syncGatewayProject();
}
},
1000 * 60 * 5,
);
window.addEventListener('beforeunload', (e) => {
const res = this._project.actions.project.syncGatewayProject();
if (!res) e.preventDefault();
});
}
}
11 changes: 9 additions & 2 deletions src/lib/components/Menu/MenuBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import { resolve } from '$app/paths';
import { goto } from '$app/navigation';
import type { Snippet } from 'svelte';
import { ProjectLoader } from '$lib/client/project';
import { ProjectLoader, useProject } from '$lib/client/project';
import { PUBLIC_DOCS_URL, PUBLIC_LANDING_URL } from '$env/static/public';
import { getConfig } from '$lib/client/config';

const { actions } = useProject();

let fileInput: HTMLInputElement;

Expand Down Expand Up @@ -38,13 +41,17 @@
input.value = '';
}

const handleSave = async () => {
if (getConfig().mode === 'online') await actions.project.syncGatewayProject();
};

const nullFunction = () => {};

const elements: Menu[] = [
{
name: 'File',
items: [
{ name: 'Save', icon: 'i-solar-cloud-download-bold-duotone', onClick: nullFunction },
{ name: 'Save', icon: 'i-solar-cloud-download-bold-duotone', onClick: handleSave },
{
snippet: fileImportSnippet,
icon: 'i-solar-download-bold-duotone',
Expand Down
4 changes: 2 additions & 2 deletions src/lib/server/actions/project/complete.action.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Expose } from 'class-transformer';
import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';

import { resolveSessionFunctions } from '$lib/server/actions/project/load.action';
import { loadProject } from '$lib/server/project';
Expand All @@ -8,7 +8,7 @@ import { useActionHandler } from '@utils-server/request-handler';

export class CompleteProjectBody {
@Expose()
@IsUUID(8)
@IsString()
@IsNotEmpty()
gatewayId!: string;

Expand Down
8 changes: 8 additions & 0 deletions src/lib/server/actions/project/gateway.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ export const getGatewayProjectsAction = useActionHandler(
},
{ onlineOnly: true, projectOptional: true },
);

export const syncGatewayProjectAction = useActionHandler(
async ({ git }) => {
console.log('Syncing gateway project');
return await git.push();
},
{ onlineOnly: true },
);
12 changes: 8 additions & 4 deletions src/lib/server/actions/project/load.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Expose } from 'class-transformer';
import { IsOptional, IsString } from 'class-validator';
import { join } from 'path';

import { Git } from '$lib/server/git';
import { loadProject } from '$lib/server/project';
import { loadProjectFromId } from '$lib/server/project/load-project';
import type { SessionProject } from '$lib/server/session';
Expand Down Expand Up @@ -34,18 +35,21 @@ export class LoadProjectBody {

const resolveSessionFromGatewayId = async (
gatewayId: string,
{ api, git, context }: Handler,
{ api, context }: Handler,
): Promise<SessionProject> => {
if (!context.online)
throw new Exception('Bad Request', 'Cannot load project from gatewayId while offline', 400);

const project = await api.projects.getProject(gatewayId);
const basePath = await git.clone(project.gatewayProjectRegistryUrl, {
sshKey: project.gatewayProjectRegistryMetadata.sshKey,

const git = new Git({
...context,
project: { path: '', gateway: { id: gatewayId, token: project.token } },
});
const basePath = await git.clone(project.gatewayProjectRegistryUrl);
return {
path: join(basePath, project.gatewayProjectRegistryMetadata.dir ?? ''),
gateway: { id: gatewayId, sshKey: project.gatewayProjectRegistryMetadata.sshKey },
gateway: { id: gatewayId, token: project.token },
};
};

Expand Down
2 changes: 1 addition & 1 deletion src/lib/server/api/types/project.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export interface ApiProject {
gatewayProjectRegistryUrl: string;
gatewayProjectRegistryMetadata: {
dir: string | null;
sshKey: string;
};
token: string;
}
49 changes: 21 additions & 28 deletions src/lib/server/git/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,43 @@ import { resolve } from 'path';

import { env } from '$env/dynamic/private';

import type { Context } from '@utils-server/request-handler';
import { generateKey } from '@utils-server/string';

export class Git {
private readonly _rootPath: string;
private readonly _token: string | null;
private readonly _path: string;

constructor() {
constructor(context: Context) {
this._rootPath = resolve(env.FS_ROOT ?? '');
this._token = context.project.gateway?.token ?? null;
this._path = context.project.path;
}

async clone(url: string, options?: { sshKey?: string }): Promise<string> {
async clone(url: string): Promise<string> {
const path = await this.resolvePath(url);
if (existsSync(path)) return path;
await this.runCommand('clone', [url, path], { ...options });
if (this._token)
url = url.replace('https://github.com/', `https://oauth2:${this._token}@github.com/`);
await this.runCommand('clone', [url, path]);
return path;
}

private async runCommand(
command: string,
params: string[],
options?: { path?: string; sshKey?: string },
) {
let sshPath: string | undefined;
if (options?.sshKey) {
sshPath = await this.createSshKeyFile(options.sshKey);
}

const cwd = resolve(this._rootPath, options?.path ?? '');
const sshEnv = sshPath ? { GIT_SSH_COMMAND: `ssh -i ${sshPath}` } : {};

try {
await $`git ${command} ${params}`.cwd(cwd).env({ ...process.env, ...sshEnv });
} finally {
if (sshPath) await this.deleteSshKeyFile(sshPath);
}
async push() {
await this.runCommand('add', ['--all'], { path: this._path });
const res = await this.runCommand('status', [], { path: this._path });
if (res.stdout.includes('nothing to commit')) return;
await this.runCommand('commit', ['-m', `nanoforged at ${new Date().toISOString()}`], {
path: this._path,
});
await this.runCommand('push', ['-u', 'origin', 'main'], { path: this._path });
}

private async createSshKeyFile(sshKey: string): Promise<string> {
const path = `/tmp/nanoforge/${generateKey()}`;
await Bun.file(path).write(sshKey);
return path;
}
private runCommand(command: string, params: string[], options?: { path?: string }) {
const cwd = resolve(this._rootPath, options?.path ?? '');

private async deleteSshKeyFile(path: string): Promise<void> {
await Bun.file(path).delete();
return $`git ${command} ${params}`.cwd(cwd).env({ ...process.env });
}

private async resolvePath(url: string) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/server/session/project/project.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ export interface SessionProject {
path: string;
gateway?: {
id: string;
sshKey: string;
token: string;
};
}
2 changes: 1 addition & 1 deletion src/lib/server/utils/request-handler/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class Handler<Body = any> {
}

get git(): Git {
if (!this._gitCache) this._gitCache = new Git();
if (!this._gitCache) this._gitCache = new Git(this._context);
return this._gitCache;
}

Expand Down
6 changes: 5 additions & 1 deletion src/routes/actions/project/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { completeProjectAction } from '$lib/server/actions/project/complete.action';
import { getGatewayProjectsAction } from '$lib/server/actions/project/gateway.action';
import {
getGatewayProjectsAction,
syncGatewayProjectAction,
} from '$lib/server/actions/project/gateway.action';
import {
getInfoProjectAction,
setInfoProjectAction,
Expand All @@ -14,4 +17,5 @@ export const actions = {
'get-info': getInfoProjectAction,
'set-info': setInfoProjectAction,
'get-gateway-projects': getGatewayProjectsAction,
'sync-gateway-project': syncGatewayProjectAction,
};
Loading