276 lines
7.4 KiB
TypeScript
Executable File
276 lines
7.4 KiB
TypeScript
Executable File
#!/usr/bin/env bun
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import { execSync } from 'child_process';
|
|
|
|
const CONFIG_FILE = path.join(os.homedir(), '.note.conf');
|
|
|
|
interface Config {
|
|
TOKEN?: string;
|
|
REPO?: string;
|
|
URL?: string;
|
|
}
|
|
|
|
// --- Check dependencies ---
|
|
function checkDependency(cmd: string): boolean {
|
|
try {
|
|
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function checkDependencies(): void {
|
|
const deps = ['curl', 'jq'];
|
|
for (const cmd of deps) {
|
|
if (!checkDependency(cmd)) {
|
|
console.error(`❌ Missing dependency: ${cmd} (please install it)`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Config management ---
|
|
function createDefaultConfig(): void {
|
|
const defaultConfig = `TOKEN=
|
|
REPO=
|
|
URL=https://cld-dkr-makuro-seafile.wibudev.com/api2
|
|
`;
|
|
fs.writeFileSync(CONFIG_FILE, defaultConfig);
|
|
}
|
|
|
|
function editConfig(): void {
|
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
createDefaultConfig();
|
|
}
|
|
|
|
const editor = process.env.EDITOR || 'vim';
|
|
try {
|
|
execSync(`${editor} "${CONFIG_FILE}"`, { stdio: 'inherit' });
|
|
} catch (error) {
|
|
console.error('❌ Failed to open editor');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function loadConfig(): Config {
|
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
console.error(`⚠️ Config file not found at ${CONFIG_FILE}`);
|
|
console.error('Run: node note.ts config to create/edit it.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const configContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
const config: Config = {};
|
|
|
|
for (const line of configContent.split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (trimmed && !trimmed.startsWith('#')) {
|
|
const [key, ...valueParts] = trimmed.split('=');
|
|
if (key && valueParts.length > 0) {
|
|
const value = valueParts.join('=');
|
|
config[key as keyof Config] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!config.TOKEN || !config.REPO) {
|
|
console.error(`❌ Config invalid. Please set TOKEN=... and REPO=... inside ${CONFIG_FILE}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Set default URL if not provided
|
|
if (!config.URL) {
|
|
console.error(`❌ Config invalid. Please set URL=... inside ${CONFIG_FILE}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
// --- HTTP helpers ---
|
|
function curlCommand(config: Config, options: string): string {
|
|
try {
|
|
return execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" ${options}`,
|
|
{ encoding: 'utf8' });
|
|
} catch (error) {
|
|
console.error('❌ Request failed');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function curlCommandSilent(config: Config, options: string): void {
|
|
try {
|
|
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" ${options} >/dev/null`,
|
|
{ stdio: 'ignore' });
|
|
} catch (error) {
|
|
console.error('❌ Request failed');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// --- Commands ---
|
|
function listFiles(config: Config): void {
|
|
const response = curlCommand(config, `"${config.URL}/${config.REPO}/dir/?p=/"`);
|
|
try {
|
|
const files = JSON.parse(response);
|
|
for (const file of files) {
|
|
console.log(file.name);
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Failed to parse response');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function catFile(config: Config, fileName: string): void {
|
|
const downloadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/file/?p=/${fileName}"`);
|
|
const downloadUrl = downloadUrlResponse.replace(/"/g, '');
|
|
const content = curlCommand(config, `"${downloadUrl}"`);
|
|
console.log(content);
|
|
}
|
|
|
|
function uploadFile(config: Config, localFile: string, remoteFile?: string): void {
|
|
if (!fs.existsSync(localFile)) {
|
|
console.error(`❌ File not found: ${localFile}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const remoteName = remoteFile || path.basename(localFile);
|
|
const uploadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/upload-link/?p=/"`);
|
|
const uploadUrl = uploadUrlResponse.replace(/"/g, '');
|
|
|
|
try {
|
|
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" \
|
|
-F "file=@${localFile}" \
|
|
-F "filename=${remoteName}" \
|
|
-F "parent_dir=/" \
|
|
"${uploadUrl}" >/dev/null`, { stdio: 'ignore' });
|
|
console.log(`✅ Uploaded ${localFile} → ${remoteName}`);
|
|
} catch (error) {
|
|
console.error('❌ Upload failed');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function removeFile(config: Config, fileName: string): void {
|
|
curlCommandSilent(config, `-X DELETE "${config.URL}/${config.REPO}/file/?p=/${fileName}"`);
|
|
console.log(`🗑️ Removed ${fileName}`);
|
|
}
|
|
|
|
function moveFile(config: Config, oldName: string, newName: string): void {
|
|
try {
|
|
execSync(`curl -s -X POST -H "Authorization: Token ${config.TOKEN}" \
|
|
-d "operation=rename" \
|
|
-d "newname=${newName}" \
|
|
"${config.URL}/${config.REPO}/file/?p=/${oldName}" >/dev/null`, { stdio: 'ignore' });
|
|
console.log(`✏️ Renamed ${oldName} → ${newName}`);
|
|
} catch (error) {
|
|
console.error('❌ Rename failed');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function downloadFile(config: Config, remoteFile: string, localFile?: string): void {
|
|
const localName = localFile || remoteFile;
|
|
const downloadUrlResponse = curlCommand(config, `"${config.URL}/${config.REPO}/file/?p=/${remoteFile}"`);
|
|
const downloadUrl = downloadUrlResponse.replace(/"/g, '');
|
|
|
|
try {
|
|
execSync(`curl -s -H "Authorization: Token ${config.TOKEN}" "${downloadUrl}" -o "${localName}"`,
|
|
{ stdio: 'ignore' });
|
|
console.log(`⬇️ Downloaded ${remoteFile} → ${localName}`);
|
|
} catch (error) {
|
|
console.error('❌ Download failed');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function showHelp(): void {
|
|
console.log(`note - simple CLI for Seafile notes
|
|
Usage:
|
|
node note.ts ls List files
|
|
node note.ts cat <file> Show file content
|
|
node note.ts cp <local> [remote] Upload file
|
|
node note.ts rm <remote> Remove file
|
|
node note.ts mv <old> <new> Rename/move file
|
|
node note.ts get <remote> [local] Download file
|
|
node note.ts config Edit config (~/.note.conf)
|
|
|
|
Config (~/.note.conf):
|
|
TOKEN=your_seafile_token
|
|
REPO=repos/<repo-id>
|
|
URL=your_seafile_url/api2`);
|
|
}
|
|
|
|
// --- Main ---
|
|
function not3(): void {
|
|
const args = process.argv.slice(2);
|
|
const cmd = args[0] || 'help';
|
|
|
|
// Handle config command without dependency check
|
|
if (cmd === 'config') {
|
|
editConfig();
|
|
return;
|
|
}
|
|
|
|
// Check dependencies for other commands
|
|
checkDependencies();
|
|
|
|
// Load config for other commands
|
|
const config = loadConfig();
|
|
|
|
switch (cmd) {
|
|
case 'ls':
|
|
listFiles(config);
|
|
break;
|
|
|
|
case 'cat':
|
|
if (!args[1]) {
|
|
console.error('Usage: node note.ts cat <file>');
|
|
process.exit(1);
|
|
}
|
|
catFile(config, args[1]);
|
|
break;
|
|
|
|
case 'cp':
|
|
if (!args[1]) {
|
|
console.error('Usage: node note.ts cp <local_file> [remote_file]');
|
|
process.exit(1);
|
|
}
|
|
uploadFile(config, args[1], args[2]);
|
|
break;
|
|
|
|
case 'rm':
|
|
if (!args[1]) {
|
|
console.error('Usage: node note.ts rm <remote_file>');
|
|
process.exit(1);
|
|
}
|
|
removeFile(config, args[1]);
|
|
break;
|
|
|
|
case 'mv':
|
|
if (!args[1] || !args[2]) {
|
|
console.error('Usage: node note.ts mv <old_name> <new_name>');
|
|
process.exit(1);
|
|
}
|
|
moveFile(config, args[1], args[2]);
|
|
break;
|
|
|
|
case 'get':
|
|
if (!args[1]) {
|
|
console.error('Usage: node note.ts get <remote_file> [local_file]');
|
|
process.exit(1);
|
|
}
|
|
downloadFile(config, args[1], args[2]);
|
|
break;
|
|
|
|
case 'help':
|
|
default:
|
|
showHelp();
|
|
break;
|
|
}
|
|
}
|