From 21cf9b27733d1edd0f426f85c740a905a7a63066 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Fri, 30 Dec 2022 16:20:43 -0800 Subject: [PATCH] FTP support --- packages/common/src/Logger.ts | 16 +- packages/common/src/PromiseHelper.ts | 50 --- packages/common/src/index.ts | 2 +- .../pocketbase-client-helpers/RpcHelper.ts | 10 +- .../pocketbase-client-helpers/WatchHelper.ts | 12 +- packages/common/src/safeCatch.ts | 42 +++ packages/daemon/package.json | 1 + packages/daemon/src/constants.ts | 5 + packages/daemon/src/db/InstanceMIxin.ts | 5 + packages/daemon/src/db/PbClient.ts | 27 +- packages/daemon/src/server.ts | 41 +-- packages/daemon/src/services/FtpService.ts | 312 ++++++++++++++++++ .../daemon/src/services/PocketBaseService.ts | 8 +- packages/daemon/src/services/ProxyService.ts | 12 +- packages/daemon/src/util/promiseHelper.ts | 5 +- packages/pockethost.io/.env-template | 6 - packages/pockethost.io/src/env.ts | 5 +- .../src/pocketbase/PocketbaseClient.ts | 15 +- .../pockethost.io/src/pocketbase/index.ts | 13 +- .../app/instances/[instanceId]/+page.svelte | 5 +- .../app/instances/[instanceId]/Code.svelte | 4 +- .../instances/[instanceId]/Overview.svelte | 4 +- .../src/routes/app/new/+page.svelte | 4 +- .../src/routes/dashboard/+page.svelte | 4 +- .../src/routes/signup/+page.svelte | 2 +- patches/ftp-srv+4.6.2.patch | 46 +++ yarn.lock | 170 +++++++++- 27 files changed, 684 insertions(+), 142 deletions(-) delete mode 100644 packages/common/src/PromiseHelper.ts create mode 100644 packages/common/src/safeCatch.ts create mode 100644 packages/daemon/src/services/FtpService.ts delete mode 100644 packages/pockethost.io/.env-template create mode 100644 patches/ftp-srv+4.6.2.patch diff --git a/packages/common/src/Logger.ts b/packages/common/src/Logger.ts index 1d0968f0..5ebdc261 100644 --- a/packages/common/src/Logger.ts +++ b/packages/common/src/Logger.ts @@ -3,6 +3,7 @@ import { mkSingleton } from './mkSingleton' export type Config = { raw?: boolean debug: boolean + errorTrace?: boolean pfx?: string[] } @@ -16,10 +17,11 @@ export const createLogger = (config: Partial) => { const _config: Required = { raw: false, debug: true, + errorTrace: true, pfx: [''], ...config, } - const { pfx } = _config + const { pfx, errorTrace } = _config const _pfx = (s: string) => [s, ...pfx] @@ -54,8 +56,13 @@ export const createLogger = (config: Partial) => { _log(true, _pfx(`INFO`), ...args) } + const trace = (...args: any[]) => { + _log(true, _pfx(`TRACE`), ...args) + } + const error = (...args: any[]) => { _log(true, _pfx(`ERROR`), ...args) + if (!errorTrace) return console.error(`========================`) ;[..._buf.slice(_curIdx, MAX_BUF), ..._buf.slice(0, _curIdx)].forEach( (args) => { @@ -65,13 +72,16 @@ export const createLogger = (config: Partial) => { console.error(`========================`) } - const create = (s: string) => + const create = (s: string, configOverride?: Partial) => createLogger({ ..._config, + ...configOverride, pfx: [..._config.pfx, s], }) - return { raw, dbg, warn, info, error, create } + const child = (extra: any) => create(JSON.stringify(extra)) + + return { raw, dbg, warn, info, error, create, child, trace, debug: dbg } } export const logger = mkSingleton((config: Config) => createLogger(config)) diff --git a/packages/common/src/PromiseHelper.ts b/packages/common/src/PromiseHelper.ts deleted file mode 100644 index cb38255e..00000000 --- a/packages/common/src/PromiseHelper.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ClientResponseError } from 'pocketbase' -import { logger } from './Logger' - -export type PromiseHelperConfig = {} - -export type PromiseHelper = ReturnType - -export const createPromiseHelper = (config: PromiseHelperConfig) => { - let c = 0 - const safeCatch = ( - name: string, - cb: (...args: TIn) => Promise - ) => { - return (...args: TIn) => { - const _c = c++ - const uuid = `${name}:${_c}` - const pfx = `safeCatch:${uuid}` - const { raw, error, warn } = logger().create(pfx) - raw(`args`, args) - const tid = setTimeout(() => { - warn(`timeout waiting for ${pfx}`) - }, 100) - - return cb(...args) - .then((res) => { - raw(`finished`) - clearTimeout(tid) - return res - }) - .catch((e: any) => { - if (e instanceof ClientResponseError) { - if (e.status === 400) { - error( - `PocketBase API error: It looks like you don't have permission to make this request.` - ) - } else if (e.status === 0) { - warn(`Client request aborted (duplicate)`) - } else { - error(`Unknown PocketBase API error`, JSON.stringify(e)) - } - } else { - error(JSON.stringify(e, null, 2)) - } - throw e - }) - } - } - - return { safeCatch } -} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index f5a8a07a..7a72a9fb 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -3,6 +3,6 @@ export * from './CleanupManager' export * from './Logger' export * from './mkSingleton' export * from './pocketbase-client-helpers' -export * from './PromiseHelper' +export * from './safeCatch' export * from './schema' export * from './TimerManager' diff --git a/packages/common/src/pocketbase-client-helpers/RpcHelper.ts b/packages/common/src/pocketbase-client-helpers/RpcHelper.ts index 5f3e3a16..178e5683 100644 --- a/packages/common/src/pocketbase-client-helpers/RpcHelper.ts +++ b/packages/common/src/pocketbase-client-helpers/RpcHelper.ts @@ -6,8 +6,8 @@ import { UnsubscribeFunc, } from 'pocketbase' import type { JsonObject } from 'type-fest' -import { Logger } from '../Logger' -import { PromiseHelper } from '../PromiseHelper' +import { logger } from '../Logger' +import { safeCatch } from '../safeCatch' import { BaseFields, RpcCommands, UserId } from '../schema' import type { WatchHelper } from './WatchHelper' @@ -17,8 +17,6 @@ export const newId = () => nanoid(15) export type RpcHelperConfig = { client: pocketbaseEs watchHelper: WatchHelper - promiseHelper: PromiseHelper - logger: Logger } export type RpcHelper = ReturnType @@ -57,8 +55,6 @@ export const createRpcHelper = (config: RpcHelperConfig) => { const { client, watchHelper: { watchById }, - promiseHelper: { safeCatch }, - logger: { dbg }, } = config const mkRpc = ( @@ -66,6 +62,8 @@ export const createRpcHelper = (config: RpcHelperConfig) => { ) => { type ConcreteRpcRecord = RpcFields + const { dbg } = logger().create('mkRpc') + return safeCatch( cmd, async ( diff --git a/packages/common/src/pocketbase-client-helpers/WatchHelper.ts b/packages/common/src/pocketbase-client-helpers/WatchHelper.ts index 2fb5d331..126bf790 100644 --- a/packages/common/src/pocketbase-client-helpers/WatchHelper.ts +++ b/packages/common/src/pocketbase-client-helpers/WatchHelper.ts @@ -1,24 +1,18 @@ import type { BaseFields, RecordId } from '@pockethost/common' +import { logger, safeCatch } from '@pockethost/common' import type pocketbaseEs from 'pocketbase' import type { RecordSubscription, UnsubscribeFunc } from 'pocketbase' -import { Logger } from '../Logger' -import { PromiseHelper } from '../PromiseHelper' export type WatchHelperConfig = { client: pocketbaseEs - promiseHelper: PromiseHelper - logger: Logger } export type WatchHelper = ReturnType export const createWatchHelper = (config: WatchHelperConfig) => { - const { - client, - promiseHelper: { safeCatch }, - logger: { dbg }, - } = config + const { client } = config + const { dbg } = logger().create(`watchHelper`) const watchById = safeCatch( `watchById`, async ( diff --git a/packages/common/src/safeCatch.ts b/packages/common/src/safeCatch.ts new file mode 100644 index 00000000..fde7c6fe --- /dev/null +++ b/packages/common/src/safeCatch.ts @@ -0,0 +1,42 @@ +import { ClientResponseError } from 'pocketbase' +import { logger } from './Logger' + +let c = 0 +export const safeCatch = ( + name: string, + cb: (...args: TIn) => Promise +) => { + return (...args: TIn) => { + const _c = c++ + const uuid = `${name}:${_c}` + const pfx = `safeCatch:${uuid}` + const { raw, error, warn } = logger().create(pfx) + raw(`args`, args) + const tid = setTimeout(() => { + warn(`timeout waiting for ${pfx}`) + }, 100) + + return cb(...args) + .then((res) => { + raw(`finished`) + clearTimeout(tid) + return res + }) + .catch((e: any) => { + if (e instanceof ClientResponseError) { + if (e.status === 400) { + warn( + `PocketBase API error: It looks like you don't have permission to make this request.` + ) + } else if (e.status === 0) { + warn(`Client request aborted (duplicate)`) + } else { + warn(`Unknown PocketBase API error`, JSON.stringify(e)) + } + } else { + warn(JSON.stringify(e, null, 2)) + } + throw e + }) + } +} diff --git a/packages/daemon/package.json b/packages/daemon/package.json index e410b43f..603592e2 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -23,6 +23,7 @@ "date-fns": "^2.29.3", "event-source-polyfill": "^1.0.31", "eventsource": "^2.0.2", + "ftp-srv": "^4.6.2", "get-port": "^6.1.2", "http-proxy": "^1.18.1", "knex": "^2.3.0", diff --git a/packages/daemon/src/constants.ts b/packages/daemon/src/constants.ts index cf3b9e78..f4131543 100644 --- a/packages/daemon/src/constants.ts +++ b/packages/daemon/src/constants.ts @@ -51,3 +51,8 @@ export const PH_BIN_CACHE = env( `PH_BIN_CACHE`, join(__dirname, `../../../.pbincache`) ) + +export const PH_FTP_PORT = envi('PH_FTP_PORT', 21) + +export const SSL_KEY = env('SSL_KEY') +export const SSL_CERT = env('SSL_CERT') diff --git a/packages/daemon/src/db/InstanceMIxin.ts b/packages/daemon/src/db/InstanceMIxin.ts index 3deeccb3..1f1c8901 100644 --- a/packages/daemon/src/db/InstanceMIxin.ts +++ b/packages/daemon/src/db/InstanceMIxin.ts @@ -65,6 +65,10 @@ export const createInstanceMixin = (context: MixinContext) => { } ) + const getInstances = safeCatch(`getInstances`, async () => { + return client.collection('instances').getFullList() + }) + const updateInstances = safeCatch( 'updateInstances', async (cb: (rec: InstanceFields) => Partial) => { @@ -110,6 +114,7 @@ export const createInstanceMixin = (context: MixinContext) => { ) return { + getInstances, updateInstance, updateInstanceStatus, getInstanceBySubdomain, diff --git a/packages/daemon/src/db/PbClient.ts b/packages/daemon/src/db/PbClient.ts index 87395894..585701f9 100644 --- a/packages/daemon/src/db/PbClient.ts +++ b/packages/daemon/src/db/PbClient.ts @@ -1,11 +1,18 @@ -import { logger } from '@pockethost/common' +import { logger, mkSingleton } from '@pockethost/common' import { Knex } from 'knex' import { Collection, default as PocketBase, default as pocketbaseEs, } from 'pocketbase' -import { DAEMON_PB_DATA_DIR, PUBLIC_PB_SUBDOMAIN } from '../constants' +import { + DAEMON_PB_DATA_DIR, + DAEMON_PB_PASSWORD, + DAEMON_PB_USERNAME, + PUBLIC_PB_DOMAIN, + PUBLIC_PB_PROTOCOL, + PUBLIC_PB_SUBDOMAIN, +} from '../constants' import { Collection_Serialized } from '../migrate/schema' import { safeCatch } from '../util/promiseHelper' import { createBackupMixin } from './BackupMixin' @@ -49,6 +56,7 @@ export const createPbClient = (url: string) => { const api = { client, + url, knex: rawDb, adminAuthViaEmail, applySchema, @@ -60,3 +68,18 @@ export const createPbClient = (url: string) => { return api } + +export const clientService = mkSingleton(async (url: string) => { + const { dbg, error } = logger().create(`client singleton`) + const client = createPbClient(url) + try { + await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD) + dbg(`Logged in`) + } catch (e) { + error( + `***WARNING*** CANNOT AUTHENTICATE TO ${PUBLIC_PB_PROTOCOL}://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}/_/` + ) + error(`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`) + } + return client +}) diff --git a/packages/daemon/src/server.ts b/packages/daemon/src/server.ts index 64079e69..34bc8e0f 100644 --- a/packages/daemon/src/server.ts +++ b/packages/daemon/src/server.ts @@ -1,15 +1,8 @@ import { logger } from '@pockethost/common' -import { - DAEMON_PB_PASSWORD, - DAEMON_PB_USERNAME, - DEBUG, - PH_BIN_CACHE, - PUBLIC_PB_DOMAIN, - PUBLIC_PB_PROTOCOL, - PUBLIC_PB_SUBDOMAIN, -} from './constants' -import { createPbClient } from './db/PbClient' +import { DEBUG, PH_BIN_CACHE, PUBLIC_PB_SUBDOMAIN } from './constants' +import { clientService } from './db/PbClient' import { createBackupService } from './services/BackupService' +import { ftpService } from './services/FtpService' import { createInstanceService } from './services/InstanceService' import { pocketbase } from './services/PocketBaseService' import { createProxyService } from './services/ProxyService' @@ -21,6 +14,8 @@ logger({ debug: DEBUG }) global.EventSource = require('eventsource') ;(async () => { const { dbg, error, info } = logger().create(`server.ts`) + info(`Starting`) + const pbService = await pocketbase({ cachePath: PH_BIN_CACHE, checkIntervalMs: 1000 * 5 * 60, @@ -37,17 +32,9 @@ global.EventSource = require('eventsource') /** * Launch services */ - const client = createPbClient(url) - try { - await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD) - dbg(`Logged in`) - } catch (e) { - error( - `***WARNING*** CANNOT AUTHENTICATE TO ${PUBLIC_PB_PROTOCOL}://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}/_/` - ) - error(`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`) - } + const client = await clientService(url) + ftpService({}) const rpcService = await createRpcService({ client }) const instanceService = await createInstanceService({ client, rpcService }) const proxyService = await createProxyService({ @@ -56,12 +43,20 @@ global.EventSource = require('eventsource') }) const backupService = await createBackupService(client, rpcService) - process.once('SIGUSR2', async () => { - info(`SIGUSR2 detected`) + info(`Hooking into process exit event`) + + const shutdown = (signal: NodeJS.Signals) => { + info(`Got signal ${signal}`) + info(`Shutting down`) + ftpService().shutdown() proxyService.shutdown() instanceService.shutdown() rpcService.shutdown() backupService.shutdown() pbService.shutdown() - }) + } + + process.on('SIGTERM', shutdown) + process.on('SIGINT', shutdown) + process.on('SIGHUP', shutdown) })() diff --git a/packages/daemon/src/services/FtpService.ts b/packages/daemon/src/services/FtpService.ts new file mode 100644 index 00000000..ab8f3dc2 --- /dev/null +++ b/packages/daemon/src/services/FtpService.ts @@ -0,0 +1,312 @@ +import { Logger, logger, mkSingleton } from '@pockethost/common' +import { existsSync, mkdirSync, readFileSync } from 'fs' +import { FileStat, FileSystem, FtpConnection, FtpSrv } from 'ftp-srv' +import { join } from 'path' +import { Readable } from 'stream' +import { + DAEMON_PB_DATA_DIR, + PH_FTP_PORT, + SSL_CERT, + SSL_KEY, +} from '../constants' +import { + clientService, + createPbClient, + PocketbaseClientApi, +} from '../db/PbClient' + +export type FtpConfig = {} + +export enum FolderNames { + PbData = 'pb_data', + PbStatic = 'pb_static', + PbWorkers = 'workers', + PbBackup = 'backup', +} + +const README_CONTENTS: { [_ in FolderNames]: string } = { + [FolderNames.PbBackup]: `This directory contains tgz backups of your data. PocketHost creates these automatically, or you can create them manually. For more information, see https://pockethost.io/docs/backups`, + [FolderNames.PbData]: `This directory contains your PocketBase data. For more information, see https://pockethost.io/docs/data`, + [FolderNames.PbStatic]: `This directory contains static files such as your web frontend. PocketHost will serve these when your instance URL receives a request. For more information, see https://pockethost.io/docs/static `, + [FolderNames.PbWorkers]: `This directory contains your Deno workers. For more information, see https://pockethost.io/docs/workers`, +} +const README_NAME = 'readme.md' + +const FOLDER_NAMES: FolderNames[] = [ + FolderNames.PbBackup, + FolderNames.PbData, + FolderNames.PbStatic, + FolderNames.PbWorkers, +] + +function isFolder(name: string): name is FolderNames { + return FOLDER_NAMES.includes(name as FolderNames) +} + +class PhFs extends FileSystem { + private log: Logger + private client: PocketbaseClientApi + + constructor(connection: FtpConnection, client: PocketbaseClientApi) { + super(connection, { root: '/', cwd: '/' }) + this.client = client + this.log = logger().create(`PhFs`) + } + + async chdir(path?: string | undefined): Promise { + this.log.dbg(`chdir`, path) + if (!path) { + throw new Error(`Expected path`) + } + const _path = path.startsWith('/') ? path : join(this.cwd, path) + const [empty, subdomain] = _path.split('/') + this.log.dbg({ _path, subdomain }) + + if (subdomain) { + const instance = await this.client.getInstanceBySubdomain(subdomain) + if (!instance) { + throw new Error(`Subdomain not found`) + } + } + this.cwd = _path + return path + } + + async list(path?: string | undefined): Promise { + this.log.dbg(`list ${path}`, this.cwd) + if (!path) { + throw new Error(`Expected path`) + } + const _path = path.startsWith('/') ? path : join(this.cwd, path) + const [empty, subdomain, folderName] = _path.split('/') + this.log.dbg({ _path, subdomain, folderName }) + + if (subdomain === '') { + const instances = await this.client.getInstances() + return instances.map((i) => { + return { + isDirectory: () => true, + mode: 0o755, + size: 0, + mtime: Date.parse(i.updated), + name: i.subdomain, + } + }) + } + if (subdomain) { + const [instance, user] = await this.client.getInstanceBySubdomain( + subdomain + ) + if (!instance) { + throw new Error(`Expected instance here`) + } + if (!folderName) { + return FOLDER_NAMES.map((name) => ({ + isDirectory: () => true, + mode: 0o755, + size: 0, + mtime: Date.parse(instance.updated), + name: name, + })) + } + if (isFolder(folderName)) { + const dir = join(DAEMON_PB_DATA_DIR, instance.id, folderName) + this.log.dbg({ dir, exists: existsSync(dir) }) + return [ + { + isDirectory: () => false, + mode: 0o444, + size: README_CONTENTS[folderName].length, + mtime: Date.parse(instance.updated), + name: README_NAME, + }, + ...(existsSync(dir) + ? await super.list( + join(DAEMON_PB_DATA_DIR, instance.id, folderName) + ) + : []), + ] + } + } + throw new Error(`Error parsing ${_path}`) + } + + async get(fileName: string): Promise { + const _path = fileName.startsWith('/') ? fileName : join(this.cwd, fileName) + const [empty, subdomain, folderName, ...fNames] = _path.split('/') + const fName = fNames.join('/') + this.log.dbg(`get`, { _path, subdomain, folderName, fName, fileName }) + + if (!subdomain) { + return { + isDirectory: () => true, + mode: 0o755, + size: 0, + mtime: +new Date(), + name: '/', + } + } + const [instance, user] = await this.client.getInstanceBySubdomain(subdomain) + if (!instance) { + throw new Error(`Expected instance here`) + } + if (!folderName) { + return { + isDirectory: () => true, + mode: 0o755, + size: 0, + mtime: Date.parse(instance.updated), + name: subdomain, + } + } + if (!isFolder(folderName)) { + throw new Error(`Invalid folder name`) + } + if (!fName) { + return { + isDirectory: () => true, + mode: 0o755, + size: 0, + mtime: Date.parse(instance.updated), + name: folderName, + } + } + return super.get(_path) + } + + async write( + fileName: string, + options?: { append?: boolean | undefined; start?: any } | undefined + ) { + const _path = fileName.startsWith('/') ? fileName : join(this.cwd, fileName) + const [empty, subdomain, folderName, ...fNames] = _path.split('/') + const fName = fNames.join('/') + this.log.dbg(`read`, { subdomain, folderName, fName }) + + if (!subdomain) { + throw new Error(`Subdomain not found`) + } + if (!folderName) { + throw new Error(`Folder name expected here`) + } + const [instance, user] = await this.client.getInstanceBySubdomain(subdomain) + if (!instance) { + throw new Error(`Expected instance here`) + } + if (!isFolder(folderName)) { + throw new Error(`Invalid folder name ${folderName}`) + } + + if (fName === README_NAME) { + throw new Error(`Can't overwrite ${fName}`) + } + const dir = join(DAEMON_PB_DATA_DIR, instance.id, folderName) + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }) + } + return super.write(join(dir, fName), options) + } + + async read( + fileName: string, + options: { start?: any } | undefined + ): Promise { + const _path = fileName.startsWith('/') ? fileName : join(this.cwd, fileName) + const [empty, subdomain, folderName, ...fNames] = _path.split('/') + const fName = fNames.join('/') + this.log.dbg(`read`, { subdomain, folderName, fName }) + + if (!subdomain) { + throw new Error(`Subdomain not found`) + } + if (!folderName) { + throw new Error(`Folder name expected here`) + } + const [instance, user] = await this.client.getInstanceBySubdomain(subdomain) + if (!instance) { + throw new Error(`Expected instance here`) + } + if (!isFolder(folderName)) { + throw new Error(`Invalid folder name ${folderName}`) + } + + if (fName === README_NAME) { + return Readable.from([README_CONTENTS[folderName]]) + } + const realPath = join( + `/`, + DAEMON_PB_DATA_DIR, + instance.id, + folderName, + fName + ) + this.log.dbg({ realPath }) + return super.read(realPath, options) + } + + async delete(path: string): Promise { + throw new Error(`delete ${path}`) + } + + async mkdir(path: string): Promise { + throw new Error(`mkdir ${path}`) + } + + async rename(from: string, to: string): Promise { + throw new Error(`rename ${from} ${to}`) + } + + async chmod(path: string, mode: string): Promise { + throw new Error(`chmod ${path} ${mode}`) + } + + getUniqueName(fileName: string): string { + throw new Error(`getUniqueName ${fileName}`) + } +} + +const tls = { + key: readFileSync(SSL_KEY || ''), + cert: readFileSync(SSL_CERT || ''), +} + +export const ftpService = mkSingleton((config: FtpConfig) => { + const log = logger().create('FtpService') + const { dbg, info } = log + + const ftpServer = new FtpSrv({ + url: 'ftp://0.0.0.0:' + PH_FTP_PORT, + anonymous: false, + log: log.create(`ftpServer`, { errorTrace: false }), + tls, + }) + + ftpServer.on( + 'login', + async ({ connection, username, password }, resolve, reject) => { + const url = (await clientService()).url + const client = createPbClient(url) + try { + await client.client + .collection('users') + .authWithPassword(username, password) + dbg(`Logged in`) + const fs = new PhFs(connection, client) + resolve({ fs }) + } catch (e) { + reject(new Error(`Invalid username or password`)) + } + } + ) + + ftpServer.listen().then(() => { + info('Ftp server is starting...') + }) + + const shutdown = () => { + info(`Closing FTP server`) + ftpServer.close() + } + + return { shutdown } +}) diff --git a/packages/daemon/src/services/PocketBaseService.ts b/packages/daemon/src/services/PocketBaseService.ts index 2f7094f2..f94af2f6 100644 --- a/packages/daemon/src/services/PocketBaseService.ts +++ b/packages/daemon/src/services/PocketBaseService.ts @@ -143,7 +143,13 @@ export const createPocketbaseService = async ( ) } - const args = [command, `--dir`, `${DAEMON_PB_DATA_DIR}/${slug}/pb_data`] + const args = [ + command, + `--dir`, + `${DAEMON_PB_DATA_DIR}/${slug}/pb_data`, + `--publicDir`, + `${DAEMON_PB_DATA_DIR}/${slug}/pb_static`, + ] if (command === 'serve') { args.push(`--http`) args.push(mkInternalAddress(port)) diff --git a/packages/daemon/src/services/ProxyService.ts b/packages/daemon/src/services/ProxyService.ts index 0ff89172..e6204d08 100644 --- a/packages/daemon/src/services/ProxyService.ts +++ b/packages/daemon/src/services/ProxyService.ts @@ -75,10 +75,16 @@ export const createProxyService = async (config: ProxyServiceConfig) => { info('daemon on port 3000') server.listen(3000) - const shutdown = () => { + const shutdown = async () => { info(`Shutting down proxy server`) - server.close() - instanceManager.shutdown() + return new Promise((resolve) => { + server.close((err) => { + if (err) error(err) + resolve() + }) + server.closeAllConnections() + instanceManager.shutdown() + }) } return { shutdown } diff --git a/packages/daemon/src/util/promiseHelper.ts b/packages/daemon/src/util/promiseHelper.ts index 5e5ec368..613181e3 100644 --- a/packages/daemon/src/util/promiseHelper.ts +++ b/packages/daemon/src/util/promiseHelper.ts @@ -1,4 +1 @@ -import { createPromiseHelper } from '@pockethost/common' - -export const promiseHelper = createPromiseHelper({}) -export const { safeCatch } = promiseHelper +export { safeCatch } from '@pockethost/common' diff --git a/packages/pockethost.io/.env-template b/packages/pockethost.io/.env-template deleted file mode 100644 index 8e8cb206..00000000 --- a/packages/pockethost.io/.env-template +++ /dev/null @@ -1,6 +0,0 @@ -PUBLIC_APP_PROTOCOL = http -PUBLIC_APP_DOMAIN = localhost -PUBLIC_PB_PROTOCOL=https -PUBLIC_PB_SUBDOMAIN = pockethost-central -PUBLIC_PB_DOMAIN = pockethost.io -PUBLIC_POCKETHOST_VERSION=0.5.0 \ No newline at end of file diff --git a/packages/pockethost.io/src/env.ts b/packages/pockethost.io/src/env.ts index 32ae82ca..33317502 100644 --- a/packages/pockethost.io/src/env.ts +++ b/packages/pockethost.io/src/env.ts @@ -13,9 +13,8 @@ export const envi = (name: string, _default: number) => parseInt(env(name, _defa export const envb = (name: string, _default: boolean) => boolean(env(name, _default.toString())) -export const PUBLIC_PB_DOMAIN = env('PUBLIC_PB_DOMAIN', 'pockethost.io') -export const PUBLIC_PB_SUBDOMAIN = env('PUBLIC_PB_SUBDOMAIN', 'pockethost-central') -export const PUBLIC_APP_DOMAIN = env('PUBLIC_PB_SUBDOMAIN', 'localhost') +export const PUBLIC_APP_DB = env('PUBLIC_APP_DB', 'pockethost-central') +export const PUBLIC_APP_DOMAIN = env('PUBLIC_APP_DOMAIN', 'pockethost.io') export const PUBLIC_APP_PROTOCOL = env('PUBLIC_APP_PROTOCOL', 'https') export const PUBLIC_PB_PROTOCOL = env( 'PUBLIC_PB_PROTOCOL', diff --git a/packages/pockethost.io/src/pocketbase/PocketbaseClient.ts b/packages/pockethost.io/src/pocketbase/PocketbaseClient.ts index 9a9ca3e3..a6a08a7d 100644 --- a/packages/pockethost.io/src/pocketbase/PocketbaseClient.ts +++ b/packages/pockethost.io/src/pocketbase/PocketbaseClient.ts @@ -3,7 +3,9 @@ import { assertExists, createRpcHelper, createWatchHelper, + logger, RpcCommands, + safeCatch, type BackupFields, type BackupInstancePayload, type BackupInstanceResult, @@ -11,8 +13,6 @@ import { type CreateInstanceResult, type InstanceFields, type InstanceId, - type Logger, - type PromiseHelper, type RestoreInstancePayload, type RestoreInstanceResult, type UserFields @@ -38,15 +38,12 @@ export type AuthStoreProps = { export type PocketbaseClientConfig = { url: string - logger: Logger - promiseHelper: PromiseHelper } export type PocketbaseClient = ReturnType export const createPocketbaseClient = (config: PocketbaseClientConfig) => { - const { url, logger, promiseHelper } = config - const { dbg, error } = logger - const { safeCatch } = promiseHelper + const { url } = config + const { dbg, error } = logger() const client = new PocketBase(url) @@ -109,9 +106,9 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => { client.collection('users').authRefresh() ) - const watchHelper = createWatchHelper({ client, promiseHelper, logger }) + const watchHelper = createWatchHelper({ client }) const { watchById, watchAllById } = watchHelper - const rpcMixin = createRpcHelper({ client, watchHelper, promiseHelper, logger }) + const rpcMixin = createRpcHelper({ client, watchHelper }) const { mkRpc } = rpcMixin const createInstance = mkRpc( diff --git a/packages/pockethost.io/src/pocketbase/index.ts b/packages/pockethost.io/src/pocketbase/index.ts index 38465eb1..f1eb8b03 100644 --- a/packages/pockethost.io/src/pocketbase/index.ts +++ b/packages/pockethost.io/src/pocketbase/index.ts @@ -1,6 +1,6 @@ import { browser, dev } from '$app/environment' -import { PUBLIC_PB_DOMAIN, PUBLIC_PB_SUBDOMAIN } from '$src/env' -import { createLogger, createPromiseHelper } from '@pockethost/common' +import { PUBLIC_APP_DB, PUBLIC_APP_DOMAIN } from '$src/env' +import { logger } from '@pockethost/common' import { createPocketbaseClient, type PocketbaseClient } from './PocketbaseClient' export const client = (() => { @@ -8,11 +8,10 @@ export const client = (() => { return () => { if (!browser) throw new Error(`PocketBase client not supported in SSR`) if (clientInstance) return clientInstance - const logger = createLogger({ debug: dev }) - logger.info(`Initializing pocketbase client`) - const url = `https://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}` - const promiseHelper = createPromiseHelper({ logger }) - clientInstance = createPocketbaseClient({ url, logger, promiseHelper }) + const { info } = logger({ debug: dev }) + info(`Initializing pocketbase client`) + const url = `https://${PUBLIC_APP_DB}.${PUBLIC_APP_DOMAIN}` + clientInstance = createPocketbaseClient({ url }) return clientInstance } })() diff --git a/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte b/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte index a9b4ddc5..cbca027e 100644 --- a/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte +++ b/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte @@ -1,5 +1,6 @@ diff --git a/packages/pockethost.io/src/routes/app/instances/[instanceId]/Code.svelte b/packages/pockethost.io/src/routes/app/instances/[instanceId]/Code.svelte index aa0bebf0..f5f342f1 100644 --- a/packages/pockethost.io/src/routes/app/instances/[instanceId]/Code.svelte +++ b/packages/pockethost.io/src/routes/app/instances/[instanceId]/Code.svelte @@ -1,12 +1,12 @@ diff --git a/packages/pockethost.io/src/routes/app/instances/[instanceId]/Overview.svelte b/packages/pockethost.io/src/routes/app/instances/[instanceId]/Overview.svelte index b7b74f84..5e3b2c69 100644 --- a/packages/pockethost.io/src/routes/app/instances/[instanceId]/Overview.svelte +++ b/packages/pockethost.io/src/routes/app/instances/[instanceId]/Overview.svelte @@ -1,12 +1,12 @@
diff --git a/packages/pockethost.io/src/routes/app/new/+page.svelte b/packages/pockethost.io/src/routes/app/new/+page.svelte index 595cbbe5..57a32f50 100644 --- a/packages/pockethost.io/src/routes/app/new/+page.svelte +++ b/packages/pockethost.io/src/routes/app/new/+page.svelte @@ -1,6 +1,6 @@