#!/usr/bin/env bun import { promises as fs } from 'fs'; import * as os from 'os'; import * as path from 'path'; // --- Constants --- const CONFIG_FILE = path.join(os.homedir(), '.note.conf'); // --- Types --- interface Config { TOKEN?: string; REPO?: string; URL?: string; } export const defaultConfigSF: Config = { TOKEN: process.env.SF_TOKEN, REPO: process.env.SF_REPO, URL: process.env.SF_URL, } // --- Config Management --- // async function createDefaultConfig(): Promise { // const defaultConfig = `TOKEN=fa49bf1774cad2ec89d2882ae2c6ac1f5d7df445 // REPO=repos/e23626dc-cc18-4bb8-8fbc-d103b7d33bc8 // URL=https://cld-dkr-makuro-seafile.wibudev.com/api2 // `; // await fs.writeFile(CONFIG_FILE, defaultConfig, 'utf8'); // } // async function editConfig(): Promise { // if (!(await fs.stat(CONFIG_FILE)).isFile()) { // createDefaultConfig(); // } // const editor = process.env.EDITOR || 'vim'; // try { // execSync(`${editor} "${CONFIG_FILE}"`, { stdio: 'inherit' }); // } catch { // console.error('❌ Failed to open editor'); // process.exit(1); // } // } export async function loadConfig(): Promise { if (!(await fs.stat(CONFIG_FILE)).isFile()) { console.error(`⚠️ Config file not found at ${CONFIG_FILE}`); console.error('Run: bun note.ts config to create/edit it.'); process.exit(1); } const configContent = await fs.readFile(CONFIG_FILE, 'utf8'); const config: Config = {}; configContent.split('\n').forEach((line) => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) return; const [key, ...valueParts] = trimmed.split('='); if (key && valueParts.length > 0) { let value = valueParts.join('=').trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } config[key as keyof Config] = value; } }); if (!config.TOKEN || !config.REPO || !config.URL) { console.error(`❌ Config invalid. Please set TOKEN, REPO, and URL inside ${CONFIG_FILE}`); process.exit(1); } return config; } // --- HTTP Helpers --- export async function fetchWithAuth(config: Config, url: string, options: RequestInit = {}): Promise { const headers = { Authorization: `Token ${config.TOKEN}`, ...options.headers, }; const response = await fetch(url, { ...options, headers }); if (!response.ok) { console.error(`❌ Request failed: ${response.status} ${response.statusText}`); console.error(`🔍 URL: ${url}`); console.error(`🔍 Headers:`, headers); try { const errorText = await response.text(); console.error(`🔍 Response body: ${errorText}`); } catch { console.error('🔍 Could not read response body'); } } return response; } // --- Commands --- export async function testConnection(config: Config): Promise { try { const response = await fetchWithAuth(config, `${config.URL}/ping/`); return `✅ API connection successful: ${await response.text()}` } catch { // return '⚠️ API ping failed, trying repo access...' try { await fetchWithAuth(config, `${config.URL}/${config.REPO}/`); return `✅ Repo access successful` } catch { return '❌ Both API ping and repo access failed' } } } export async function listFiles(config: Config): Promise<{ name: string }[]> { const url = `${config.URL}/${config.REPO}/dir/?p=/`; const response = await fetchWithAuth(config, url); try { const files = (await response.json()) as { name: string }[]; return files } catch { console.error('❌ Failed to parse response'); process.exit(1); } } export async function catFile(config: Config, folder: string, fileName: string): Promise { const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${folder}/${fileName}`); const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); // Download file sebagai binary, BUKAN text const fileResponse = await fetchWithAuth(config, downloadUrl); const buffer = await fileResponse.arrayBuffer(); return buffer; } export async function uploadFile(config: Config, file: File, folder: string): Promise { const remoteName = path.basename(file.name); // 1. Dapatkan upload link (pakai Authorization) const uploadUrlResponse = await fetchWithAuth( config, `${config.URL}/${config.REPO}/upload-link/` ); const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, ""); // 2. Siapkan form-data const formData = new FormData(); formData.append("parent_dir", "/"); formData.append("relative_path", folder); // tanpa slash di akhir formData.append("file", file, remoteName); // file langsung, jangan pakai Blob // 3. Upload file TANPA Authorization header, token di query param const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, { method: "POST", body: formData, }); const text = await res.text(); if (!res.ok) return 'gagal' return `✅ Uploaded ${file.name} successfully`; } export async function uploadFileBase64(config: Config, base64File: { name: string; data: string; }): Promise { const remoteName = path.basename(base64File.name); // 1. Dapatkan upload link (pakai Authorization) const uploadUrlResponse = await fetchWithAuth( config, `${config.URL}/${config.REPO}/upload-link/` ); const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, ""); // 2. Konversi base64 ke Blob const binary = Buffer.from(base64File.data, "base64"); const blob = new Blob([binary]); // 3. Siapkan form-data const formData = new FormData(); formData.append("parent_dir", "/"); formData.append("relative_path", "syarat-dokumen"); // tanpa slash di akhir formData.append("file", blob, remoteName); // 4. Upload file TANPA Authorization header, token di query param const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, { method: "POST", body: formData, }); const text = await res.text(); if (!res.ok) throw new Error(`Upload failed: ${text}`); return `✅ Uploaded ${base64File.name} successfully`; } export async function uploadFileToFolder(config: Config, base64File: { name: string; data: string; }, folder: 'syarat-dokumen' | 'pengaduan'): Promise { const remoteName = path.basename(base64File.name); // 1. Dapatkan upload link (pakai Authorization) const uploadUrlResponse = await fetchWithAuth( config, `${config.URL}/${config.REPO}/upload-link/` ); const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, ""); // 2. Konversi base64 ke Blob const binary = Buffer.from(base64File.data, "base64"); const blob = new Blob([binary]); // 3. Siapkan form-data const formData = new FormData(); formData.append("parent_dir", "/"); formData.append("relative_path", folder); // tanpa slash di akhir formData.append("file", blob, remoteName); // 4. Upload file TANPA Authorization header, token di query param const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, { method: "POST", body: formData, }); const text = await res.text(); if (!res.ok) throw new Error(`Upload failed: ${text}`); return `✅ Uploaded ${base64File.name} successfully`; } export async function removeFile(config: Config, fileName: string, folder: string): Promise { const res = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${folder}/${fileName}`, { method: 'DELETE' }); if (!res.ok) return 'gagal menghapus file'; return `🗑️ Removed ${fileName}` } export async function moveFile(config: Config, oldName: string, newName: string): Promise { const url = `${config.URL}/${config.REPO}/file/?p=/${oldName}`; const formData = new FormData(); formData.append('operation', 'rename'); formData.append('newname', newName); await fetchWithAuth(config, url, { method: 'POST', body: formData }); return `✏️ Renamed ${oldName} → ${newName}` } export async function downloadFile(config: Config, fileName: string, folder: string, localFile?: string): Promise { const localName = localFile || fileName; // 🔹 gabungkan path folder + file const filePath = `/${folder}/${fileName}`.replace(/\/+/g, "/"); // 🔹 encode path agar aman (spasi, dll) const params = new URLSearchParams({ p: filePath, }); const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?${params.toString()}`); if(!downloadUrlResponse.ok) return 'gagal' const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); const buffer = Buffer.from(await (await fetchWithAuth(config, downloadUrl)).arrayBuffer()); await fs.writeFile(localName, buffer); return `⬇️ Downloaded ${fileName} → ${localName}` } export async function getFileLink(config: Config, fileName: string): Promise { const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`); return `🔗 Link for ${fileName}:\n${(await downloadUrlResponse.text()).replace(/"/g, '')}` }