@lacrous/omnicloud 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +158 -15
- package/package.json +4 -4
- package/server/index.js +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
|
-
import {
|
|
5
|
+
import { randomBytes } from "crypto";
|
|
6
|
+
import { chmodSync, existsSync, readFileSync, realpathSync, writeFileSync } from "fs";
|
|
6
7
|
import { createRequire } from "module";
|
|
7
8
|
import { dirname, join, resolve } from "path";
|
|
9
|
+
import { createInterface } from "readline/promises";
|
|
8
10
|
import { fileURLToPath } from "url";
|
|
9
11
|
var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
12
|
var serverDir = join(packageRoot, "server");
|
|
@@ -13,30 +15,36 @@ var pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
|
13
15
|
var USAGE = `omnicloud ${pkg.version}
|
|
14
16
|
|
|
15
17
|
Usage:
|
|
16
|
-
omnicloud
|
|
18
|
+
omnicloud Start OmniCloud and open it in your browser
|
|
19
|
+
omnicloud start [--no-open] [--env <file>] Start the server (opens the browser unless --no-open)
|
|
17
20
|
omnicloud migrate [--env <file>] Apply database migrations
|
|
18
21
|
omnicloud version Print the version
|
|
19
22
|
omnicloud help Show this message
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
environment are never overwritten.
|
|
24
|
+
Migrations run automatically before the server starts. Configuration is read from
|
|
25
|
+
environment variables, then from a .env file in the current directory (or the file
|
|
26
|
+
given with --env). Variables already set in the environment are never overwritten.
|
|
27
|
+
See https://github.com/Lacrous/OmniCloud#configuration.
|
|
24
28
|
`;
|
|
25
29
|
function parseArgs(argv) {
|
|
26
|
-
const [
|
|
30
|
+
const [first, ...rest] = argv;
|
|
31
|
+
const command = first === void 0 || first.startsWith("-") ? "start" : first;
|
|
32
|
+
const options = first === void 0 || first.startsWith("-") ? argv : rest;
|
|
27
33
|
let migrate2 = false;
|
|
28
34
|
let envFile = null;
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
let open = true;
|
|
36
|
+
for (let i = 0; i < options.length; i++) {
|
|
37
|
+
const arg = options[i];
|
|
31
38
|
if (arg === "--migrate") migrate2 = true;
|
|
39
|
+
else if (arg === "--no-open") open = false;
|
|
32
40
|
else if (arg === "--env") {
|
|
33
|
-
const value =
|
|
41
|
+
const value = options[++i];
|
|
34
42
|
if (!value) throw new Error("--env requires a file path");
|
|
35
43
|
envFile = value;
|
|
36
44
|
} else if (arg?.startsWith("--env=")) envFile = arg.slice("--env=".length);
|
|
37
45
|
else throw new Error(`Unknown option: ${arg}`);
|
|
38
46
|
}
|
|
39
|
-
return { command, migrate: migrate2, envFile };
|
|
47
|
+
return { command, migrate: migrate2, envFile, open };
|
|
40
48
|
}
|
|
41
49
|
function parseEnvFile(content) {
|
|
42
50
|
const values = {};
|
|
@@ -90,16 +98,72 @@ async function generate(env) {
|
|
|
90
98
|
const code = await run(process.execPath, [prismaCli(), "generate", "--schema", schemaPath], env);
|
|
91
99
|
if (code !== 0) throw new Error(`Prisma client generation failed (exit ${code}).`);
|
|
92
100
|
}
|
|
101
|
+
function openUrl(url) {
|
|
102
|
+
const opener = process.platform === "darwin" ? { command: "open", args: [url] } : process.platform === "win32" ? { command: "cmd", args: ["/c", "start", "", url] } : { command: "xdg-open", args: [url] };
|
|
103
|
+
const child = spawn(opener.command, opener.args, { stdio: "ignore", detached: true });
|
|
104
|
+
child.on("error", () => {
|
|
105
|
+
process.stdout.write(`Open ${url} in your browser.
|
|
106
|
+
`);
|
|
107
|
+
});
|
|
108
|
+
child.unref();
|
|
109
|
+
}
|
|
110
|
+
async function waitForServer(url, timeoutMs) {
|
|
111
|
+
const deadline = Date.now() + timeoutMs;
|
|
112
|
+
while (Date.now() < deadline) {
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(url);
|
|
115
|
+
if (response.ok) return true;
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
await new Promise((done) => setTimeout(done, 250));
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function localUrl(env) {
|
|
123
|
+
const port = env.PORT && env.PORT.trim() !== "" ? env.PORT.trim() : "4000";
|
|
124
|
+
const host = env.HOST && env.HOST.trim() !== "" && env.HOST.trim() !== "0.0.0.0" ? env.HOST.trim() : "127.0.0.1";
|
|
125
|
+
return `http://${host}:${port}`;
|
|
126
|
+
}
|
|
93
127
|
async function start(args, env) {
|
|
94
128
|
if (!existsSync(join(serverDir, "index.js"))) {
|
|
95
129
|
throw new Error("The server bundle is missing. Reinstall @lacrous/omnicloud.");
|
|
96
130
|
}
|
|
97
131
|
await generate(env);
|
|
98
|
-
|
|
132
|
+
await migrate(env);
|
|
99
133
|
env.WEB_DIST_DIR ??= join(serverDir, "web");
|
|
100
134
|
env.NODE_ENV ??= "production";
|
|
101
|
-
const
|
|
102
|
-
|
|
135
|
+
const url = localUrl(env);
|
|
136
|
+
const server = spawn(process.execPath, [join(serverDir, "index.js")], { stdio: "inherit", env });
|
|
137
|
+
let exited = false;
|
|
138
|
+
server.on("exit", () => {
|
|
139
|
+
exited = true;
|
|
140
|
+
});
|
|
141
|
+
const forward = (signal) => () => {
|
|
142
|
+
if (!exited) server.kill(signal);
|
|
143
|
+
};
|
|
144
|
+
process.on("SIGINT", forward("SIGINT"));
|
|
145
|
+
process.on("SIGTERM", forward("SIGTERM"));
|
|
146
|
+
if (args.open) {
|
|
147
|
+
const ready = await waitForServer(`${url}/api/health`, 6e4);
|
|
148
|
+
if (ready) {
|
|
149
|
+
process.stdout.write(`
|
|
150
|
+
OmniCloud is running at ${url}
|
|
151
|
+
`);
|
|
152
|
+
openUrl(url);
|
|
153
|
+
} else if (!exited) {
|
|
154
|
+
process.stdout.write(`
|
|
155
|
+
OmniCloud did not answer at ${url} yet. Check the log above.
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
await new Promise((resolveExit, rejectExit) => {
|
|
160
|
+
server.on("exit", (code, signal) => {
|
|
161
|
+
if (signal) return resolveExit();
|
|
162
|
+
if (code === 0 || code === null) return resolveExit();
|
|
163
|
+
rejectExit(new Error(`Server exited with code ${code}.`));
|
|
164
|
+
});
|
|
165
|
+
server.on("error", rejectExit);
|
|
166
|
+
});
|
|
103
167
|
}
|
|
104
168
|
async function main(argv, env = process.env) {
|
|
105
169
|
let args;
|
|
@@ -120,7 +184,19 @@ ${USAGE}`);
|
|
|
120
184
|
`);
|
|
121
185
|
return 0;
|
|
122
186
|
}
|
|
123
|
-
|
|
187
|
+
const envPath = resolve(args.envFile ?? ".env");
|
|
188
|
+
if (args.command === "start" && !existsSync(envPath) && !env.DATABASE_URL) {
|
|
189
|
+
const outcome = await firstRunSetup(envPath);
|
|
190
|
+
if (outcome === "not-interactive") {
|
|
191
|
+
process.stderr.write(
|
|
192
|
+
`omnicloud: no configuration found. Create ${envPath} (see https://github.com/Lacrous/OmniCloud#configuration) or run omnicloud in a terminal.
|
|
193
|
+
`
|
|
194
|
+
);
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
if (outcome !== "saved") return 1;
|
|
198
|
+
}
|
|
199
|
+
loadEnvFile(envPath, env);
|
|
124
200
|
try {
|
|
125
201
|
if (args.command === "migrate") {
|
|
126
202
|
await migrate(env);
|
|
@@ -141,6 +217,71 @@ ${USAGE}`);
|
|
|
141
217
|
${USAGE}`);
|
|
142
218
|
return 2;
|
|
143
219
|
}
|
|
220
|
+
function buildEnvFile(values) {
|
|
221
|
+
return [
|
|
222
|
+
"# Written by `omnicloud` on first run. Keep this file private.",
|
|
223
|
+
`DATABASE_URL=${values.databaseUrl}`,
|
|
224
|
+
`TELEGRAM_API_ID=${values.apiId}`,
|
|
225
|
+
`TELEGRAM_API_HASH=${values.apiHash}`,
|
|
226
|
+
`OMNICLOUD_ENCRYPTION_KEY=${values.encryptionKey}`,
|
|
227
|
+
""
|
|
228
|
+
].join("\n");
|
|
229
|
+
}
|
|
230
|
+
function validateSetup(values) {
|
|
231
|
+
if (!/^postgres(ql)?:\/\/.+@.+\/.+$/.test(values.databaseUrl)) {
|
|
232
|
+
return "DATABASE_URL must look like postgresql://user:password@host:5432/database";
|
|
233
|
+
}
|
|
234
|
+
if (!/^[1-9][0-9]*$/.test(values.apiId)) {
|
|
235
|
+
return "TELEGRAM_API_ID must be the number shown at my.telegram.org";
|
|
236
|
+
}
|
|
237
|
+
if (!/^[0-9a-f]{32}$/i.test(values.apiHash)) {
|
|
238
|
+
return "TELEGRAM_API_HASH must be the 32-character value shown at my.telegram.org";
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
async function firstRunSetup(envPath) {
|
|
243
|
+
if (!process.stdin.isTTY) return "not-interactive";
|
|
244
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
245
|
+
try {
|
|
246
|
+
process.stdout.write(
|
|
247
|
+
"\nWelcome to OmniCloud. No .env file was found, so let's create one.\n\n1. PostgreSQL database\n Format: postgresql://USER:PASSWORD@HOST:PORT/DATABASE\n The default docker setup uses postgresql://omnicloud:omnicloud@127.0.0.1:5432/omnicloud\n\n2. Telegram API credentials\n Open https://my.telegram.org, log in, choose 'API development tools',\n and create an application. Copy the api_id and api_hash.\n\n"
|
|
248
|
+
);
|
|
249
|
+
const databaseUrl = (await rl.question("DATABASE_URL: ")).trim();
|
|
250
|
+
const apiId = (await rl.question("TELEGRAM_API_ID: ")).trim();
|
|
251
|
+
const apiHash = (await rl.question("TELEGRAM_API_HASH: ")).trim();
|
|
252
|
+
const problem = validateSetup({ databaseUrl, apiId, apiHash });
|
|
253
|
+
if (problem) {
|
|
254
|
+
process.stderr.write(`
|
|
255
|
+
omnicloud: ${problem}
|
|
256
|
+
Run omnicloud again to retry.
|
|
257
|
+
`);
|
|
258
|
+
return "invalid";
|
|
259
|
+
}
|
|
260
|
+
const answer = (await rl.question(`
|
|
261
|
+
Save these settings to ${envPath}? [Y/n] `)).trim().toLowerCase();
|
|
262
|
+
if (answer === "n" || answer === "no") {
|
|
263
|
+
process.stdout.write("Nothing was saved.\n");
|
|
264
|
+
return "declined";
|
|
265
|
+
}
|
|
266
|
+
writeFileSync(
|
|
267
|
+
envPath,
|
|
268
|
+
buildEnvFile({
|
|
269
|
+
databaseUrl,
|
|
270
|
+
apiId,
|
|
271
|
+
apiHash,
|
|
272
|
+
encryptionKey: randomBytes(32).toString("hex")
|
|
273
|
+
}),
|
|
274
|
+
{ mode: 384 }
|
|
275
|
+
);
|
|
276
|
+
chmodSync(envPath, 384);
|
|
277
|
+
process.stdout.write(`Saved ${envPath}. A private encryption key was generated for you.
|
|
278
|
+
|
|
279
|
+
`);
|
|
280
|
+
return "saved";
|
|
281
|
+
} finally {
|
|
282
|
+
rl.close();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
144
285
|
function isEntryPoint() {
|
|
145
286
|
const entry = process.argv[1];
|
|
146
287
|
if (!entry) return false;
|
|
@@ -154,8 +295,10 @@ if (isEntryPoint()) {
|
|
|
154
295
|
main(process.argv.slice(2)).then((code) => process.exit(code));
|
|
155
296
|
}
|
|
156
297
|
export {
|
|
298
|
+
buildEnvFile,
|
|
157
299
|
loadEnvFile,
|
|
158
300
|
main,
|
|
159
301
|
parseArgs,
|
|
160
|
-
parseEnvFile
|
|
302
|
+
parseEnvFile,
|
|
303
|
+
validateSetup
|
|
161
304
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lacrous/omnicloud",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "SDK and core building blocks for OmniCloud — self-hosted cloud storage powered by your Telegram account.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"prisma": "^6.2.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@omnicloud/core": "0.2.
|
|
52
|
-
"@omnicloud/shared": "0.2.
|
|
53
|
-
"@omnicloud/telegram": "0.2.
|
|
51
|
+
"@omnicloud/core": "0.2.7",
|
|
52
|
+
"@omnicloud/shared": "0.2.7",
|
|
53
|
+
"@omnicloud/telegram": "0.2.7",
|
|
54
54
|
"@types/node": "^22.10.0",
|
|
55
55
|
"esbuild": "0.25.12",
|
|
56
56
|
"tsup": "^8.3.0",
|
package/server/index.js
CHANGED
|
@@ -4685,7 +4685,7 @@ function encryptionCheck(configured) {
|
|
|
4685
4685
|
}
|
|
4686
4686
|
|
|
4687
4687
|
// ../../apps/api/src/routes/storage.ts
|
|
4688
|
-
var VERSION = "0.2.
|
|
4688
|
+
var VERSION = "0.2.7";
|
|
4689
4689
|
function registerStorageRoutes(app2, container2) {
|
|
4690
4690
|
app2.get("/api/health", async () => {
|
|
4691
4691
|
const database = await probeDatabase(container2);
|