mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 07:40:45 -08:00
feat: ziplinectl
This commit is contained in:
@@ -11,8 +11,10 @@
|
||||
"dev": "pnpm run build:server && pnpm run dev:server",
|
||||
"dev:server": "NODE_ENV=development DEBUG=zipline node --require dotenv/config --enable-source-maps ./build/server.js",
|
||||
"dev:inspector": "NODE_ENV=development DEBUG=zipline node --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./build/server.js",
|
||||
"dev:ctl": "tsup --config tsup.ctl.config.ts --watch",
|
||||
"start": "NODE_ENV=production node --require dotenv/config --enable-source-maps ./build/server.js",
|
||||
"start:inspector": "NODE_ENV=production node --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./build/server.js",
|
||||
"ctl": "NODE_ENV=production node --require dotenv/config --enable-source-maps ./build/ctl.js",
|
||||
"validate": "pnpm run \"/^validate:.*/\"",
|
||||
"validate:lint": "eslint --cache --ignore-path .gitignore --fix .",
|
||||
"validate:format": "prettier --write --ignore-path .gitignore .",
|
||||
@@ -38,6 +40,7 @@
|
||||
"bytes": "^3.1.2",
|
||||
"clsx": "^2.0.0",
|
||||
"colorette": "^2.0.20",
|
||||
"commander": "^12.0.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -62,6 +62,9 @@ dependencies:
|
||||
colorette:
|
||||
specifier: ^2.0.20
|
||||
version: 2.0.20
|
||||
commander:
|
||||
specifier: ^12.0.0
|
||||
version: 12.0.0
|
||||
dayjs:
|
||||
specifier: ^1.11.10
|
||||
version: 1.11.10
|
||||
@@ -2815,6 +2818,11 @@ packages:
|
||||
engines: {node: '>=16'}
|
||||
dev: false
|
||||
|
||||
/commander@12.0.0:
|
||||
resolution: {integrity: sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==}
|
||||
engines: {node: '>=18'}
|
||||
dev: false
|
||||
|
||||
/commander@4.1.1:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
71
src/ctl/commands/import-dir.ts
Normal file
71
src/ctl/commands/import-dir.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { guess } from '@/lib/mimes';
|
||||
import { statSync } from 'fs';
|
||||
import { readFile, readdir } from 'fs/promises';
|
||||
import { join, parse, resolve } from 'path';
|
||||
|
||||
export async function importDir(directory: string, { id, folder }: { id?: string; folder?: string }) {
|
||||
const fullPath = resolve(directory);
|
||||
if (!statSync(fullPath).isDirectory()) return console.error('Not a directory:', directory);
|
||||
|
||||
const { prisma } = await import('@/lib/db');
|
||||
let userId: string;
|
||||
|
||||
if (id) {
|
||||
userId = id;
|
||||
} else {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { username: 'administrator', role: 'SUPERADMIN' },
|
||||
});
|
||||
|
||||
if (!user)
|
||||
return console.error(
|
||||
'There was no user with the username "administrator" and role "SUPERADMIN" found. Please provide a user id to continue.',
|
||||
);
|
||||
|
||||
userId = user.id;
|
||||
}
|
||||
|
||||
if (folder) {
|
||||
const exists = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: folder,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!exists) return console.error('Folder not found:', folder);
|
||||
}
|
||||
|
||||
const files = await readdir(fullPath);
|
||||
const data = [];
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
const info = parse(files[i]);
|
||||
const mime = await guess(info.ext.replace('.', ''));
|
||||
const { size } = statSync(join(fullPath, files[i]));
|
||||
|
||||
data[i] = {
|
||||
name: info.base,
|
||||
type: mime,
|
||||
size,
|
||||
userId,
|
||||
...(folder ? { folderId: folder } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const res = await prisma.file.createMany({
|
||||
data,
|
||||
});
|
||||
|
||||
console.log('Imported', res.count, 'files');
|
||||
|
||||
const { datasource } = await import('@/lib/datasource');
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
const buff = await readFile(join(fullPath, files[i]));
|
||||
|
||||
await datasource.put(data[i].name, buff);
|
||||
console.log('Uploaded', data[i].name);
|
||||
}
|
||||
|
||||
console.log('Done importing files.');
|
||||
}
|
||||
30
src/ctl/commands/list-users.ts
Normal file
30
src/ctl/commands/list-users.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { userSelect } from '@/lib/db/models/user';
|
||||
|
||||
export async function listUsers({ extra, format, id }: { extra?: string[]; format?: boolean; id?: string }) {
|
||||
if (extra?.includes('list')) {
|
||||
console.log('Listing possible keys:\n' + Object.keys(userSelect).join('\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
const select: Record<string, boolean> = {
|
||||
id: true,
|
||||
username: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
};
|
||||
|
||||
for (const key of extra || []) {
|
||||
if (key in userSelect) {
|
||||
select[key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const { prisma } = await import('@/lib/db');
|
||||
const users = await prisma.user.findMany({
|
||||
where: id ? { id } : undefined,
|
||||
select,
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(users, null, format ? 2 : 0));
|
||||
}
|
||||
5
src/ctl/commands/read-config.ts
Normal file
5
src/ctl/commands/read-config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export async function readConfig({ format }: { format: boolean }) {
|
||||
const { config } = await import('@/lib/config');
|
||||
|
||||
console.log(JSON.stringify(config, null, format ? 2 : 0));
|
||||
}
|
||||
37
src/ctl/commands/set-user.ts
Normal file
37
src/ctl/commands/set-user.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { hashPassword } from '@/lib/crypto';
|
||||
const SUPPORTED_FIELDS = ['username', 'password', 'role', 'avatar', 'token', 'totpSecret'];
|
||||
|
||||
export async function setUser(property: string, value: string, { id }: { id: string }) {
|
||||
if (!SUPPORTED_FIELDS.includes(property)) return console.error('Unsupported field:', property);
|
||||
|
||||
const { prisma } = await import('@/lib/db');
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!user) return console.error('User not found');
|
||||
|
||||
let parsed;
|
||||
|
||||
if (value === 'true') parsed = true;
|
||||
else if (value === 'false') parsed = false;
|
||||
|
||||
if (property === 'password') {
|
||||
parsed = await hashPassword(value);
|
||||
} else if (property === 'role') {
|
||||
const valid = ['USER', 'ADMIN', 'SUPERADMIN'];
|
||||
if (!valid.includes(value.toUpperCase())) return console.error('Invalid role:', value);
|
||||
parsed = value.toUpperCase();
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
[property]: parsed,
|
||||
},
|
||||
});
|
||||
|
||||
if (property === 'password') parsed = '*********';
|
||||
|
||||
console.log(`updated user(${id}) -> ${property} = ${parsed || value}`);
|
||||
}
|
||||
46
src/ctl/index.ts
Normal file
46
src/ctl/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Command } from 'commander';
|
||||
import { version } from '../../package.json';
|
||||
import { listUsers } from './commands/list-users';
|
||||
import { readConfig } from './commands/read-config';
|
||||
import { setUser } from './commands/set-user';
|
||||
import { importDir } from './commands/import-dir';
|
||||
|
||||
const cli = new Command();
|
||||
|
||||
cli.name('ziplinectl').version(version).description('controll utility for zipline');
|
||||
|
||||
cli
|
||||
.command('read-config')
|
||||
.option('-f, --format', 'whether or not to format the json')
|
||||
.summary('output the configuration as json, exactly how Zipline sees it')
|
||||
.action(readConfig);
|
||||
|
||||
cli
|
||||
.command('list-users')
|
||||
.option('-f, --format', 'whether or not to format the json')
|
||||
.option(
|
||||
'-e, --extra [extra...]',
|
||||
'extra properties to include in the output, "list" is used to list all possible keys',
|
||||
)
|
||||
.option('-i, --id [user_id]', 'list a specific user by their id')
|
||||
.summary('list all users')
|
||||
.action(listUsers);
|
||||
|
||||
cli
|
||||
.command('set-user')
|
||||
.option('-i, --id <user_id>', 'the id of the user to set')
|
||||
.argument('<property>', 'the property to set')
|
||||
.argument('<value>', 'the value to set')
|
||||
.action(setUser);
|
||||
|
||||
cli
|
||||
.command('import-dir')
|
||||
.option(
|
||||
'-i, --id [user_id]',
|
||||
'the id that imported files should belong to. if unspecificed the user with the "administrator" username as well as the "SUPERADMIN" role will be used',
|
||||
)
|
||||
.option('-f, --folder [folder_id]', 'an optional folder to add the files to')
|
||||
.argument('<directory>', 'the directory to import into Zipline')
|
||||
.action(importDir);
|
||||
|
||||
cli.parse();
|
||||
@@ -12,6 +12,19 @@ export default defineConfig([
|
||||
},
|
||||
outDir: 'build',
|
||||
},
|
||||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeshake: true,
|
||||
clean: false,
|
||||
sourcemap: true,
|
||||
entryPoints: {
|
||||
ctl: 'src/ctl/index.ts',
|
||||
},
|
||||
outDir: 'build',
|
||||
bundle: true,
|
||||
minify: true,
|
||||
},
|
||||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
|
||||
17
tsup.ctl.config.ts
Normal file
17
tsup.ctl.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeshake: true,
|
||||
clean: false,
|
||||
sourcemap: true,
|
||||
entryPoints: {
|
||||
ctl: 'src/ctl/index.ts',
|
||||
},
|
||||
outDir: 'build',
|
||||
bundle: true,
|
||||
minify: true,
|
||||
},
|
||||
]);
|
||||
Reference in New Issue
Block a user