feat: ziplinectl

This commit is contained in:
diced
2024-02-29 21:04:40 -08:00
parent bc3baa0a27
commit 90a6a6329a
9 changed files with 230 additions and 0 deletions

View File

@@ -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
View File

@@ -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'}

View 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.');
}

View 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));
}

View 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));
}

View 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
View 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();

View File

@@ -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
View 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,
},
]);