Skip to main content
Glama
qnap-client.js13.6 kB
import fetch from 'node-fetch'; import FormData from 'form-data'; import fs from 'fs'; import path from 'path'; /** * QNAP NAS File Station API v5 Client * Provides programmatic access to QNAP NAS file operations */ export class QNAPClient { constructor(nasIP, username, password, useHTTPS = false) { this.nasIP = nasIP; this.username = username; this.password = password; this.useHTTPS = useHTTPS; this.port = useHTTPS ? 8081 : 8080; this.sid = null; this.baseURL = `${useHTTPS ? 'https' : 'http'}://${nasIP}:${this.port}`; } /** * QNAP's ezEncode function for password encoding * Based on QNAP's official get_sid.js implementation * This is essentially Base64 encoding with proper UTF-8 conversion */ utf16to8(str) { let out = ''; for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i); if (c >= 0x0001 && c <= 0x007F) { out += str.charAt(i); } else if (c > 0x07FF) { out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F)); out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F)); out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); } else { out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F)); out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); } } return out; } ezEncode(str) { const ezEncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; let out = ''; let i = 0; const length = str.length; while (i < length) { const c1 = str.charCodeAt(i++) & 0xff; if (i === length) { out += ezEncodeChars[c1 >> 2]; out += ezEncodeChars[(c1 & 0x3) << 4]; out += '=='; break; } const c2 = str.charCodeAt(i++); if (i === length) { out += ezEncodeChars[c1 >> 2]; out += ezEncodeChars[((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)]; out += ezEncodeChars[(c2 & 0xF) << 2]; out += '='; break; } const c3 = str.charCodeAt(i++); out += ezEncodeChars[c1 >> 2]; out += ezEncodeChars[((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)]; out += ezEncodeChars[((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)]; out += ezEncodeChars[c3 & 0x3F]; } return out; } /** * Properly encode password using QNAP's method */ encodePassword(password) { return this.ezEncode(this.utf16to8(password)); } /** * Authenticate with QNAP NAS and get session ID */ async login() { try { const encodedPassword = this.encodePassword(this.password); const url = `${this.baseURL}/cgi-bin/authLogin.cgi?user=${encodeURIComponent(this.username)}&pwd=${encodeURIComponent(encodedPassword)}`; const response = await fetch(url); const xml = await response.text(); // Parse session ID from XML response const sidMatch = xml.match(/<authSid><!\[CDATA\[(.*?)\]\]><\/authSid>/); if (sidMatch && sidMatch[1]) { this.sid = sidMatch[1]; console.log('✅ QNAP NAS authentication successful'); return true; } else { console.error('❌ QNAP authentication failed:', xml); return false; } } catch (error) { console.error('❌ QNAP login error:', error.message); return false; } } /** * Logout and invalidate session */ async logout() { if (!this.sid) return true; try { const params = new URLSearchParams({ func: 'logout', sid: this.sid }); const url = `${this.baseURL}/cgi-bin/filemanager/utilRequest.cgi?${params}`; await fetch(url); this.sid = null; console.log('✅ QNAP logout successful'); return true; } catch (error) { console.error('❌ QNAP logout error:', error.message); return false; } } /** * Ensure authenticated session */ async ensureAuthenticated() { if (!this.sid) { return await this.login(); } return true; } /** * Get file and directory listing */ async listFiles(dirPath = '/') { if (!await this.ensureAuthenticated()) { throw new Error('Authentication failed'); } try { const params = new URLSearchParams({ func: 'get_tree', sid: this.sid, node: dirPath, hidden_file: '0', security_scan: '0' }); const url = `${this.baseURL}/cgi-bin/filemanager/utilRequest.cgi?${params}`; const response = await fetch(url); const data = await response.json(); return { success: true, path: dirPath, files: data }; } catch (error) { return { success: false, error: error.message }; } } /** * Create a new directory */ async createDirectory(parentPath, folderName) { if (!await this.ensureAuthenticated()) { throw new Error('Authentication failed'); } try { const params = new URLSearchParams({ func: 'createdir', sid: this.sid, dest_path: parentPath, dest_folder: folderName }); const url = `${this.baseURL}/cgi-bin/filemanager/utilRequest.cgi?${params}`; const response = await fetch(url); const data = await response.json(); return { success: data.status === 1, message: data.status === 1 ? `Directory "${folderName}" created successfully` : data.error_msg, path: path.join(parentPath, folderName) }; } catch (error) { return { success: false, error: error.message }; } } /** * Upload a file to QNAP NAS */ async uploadFile(localFilePath, remotePath, fileName = null) { if (!await this.ensureAuthenticated()) { throw new Error('Authentication failed'); } try { if (!fs.existsSync(localFilePath)) { throw new Error(`Local file not found: ${localFilePath}`); } const actualFileName = fileName || path.basename(localFilePath); const form = new FormData(); // Add file to form form.append('file', fs.createReadStream(localFilePath), { filename: actualFileName }); const params = new URLSearchParams({ func: 'upload', type: 'standard', sid: this.sid, dest_path: remotePath, overwrite: '1', progress: `-${remotePath.replace('/', '')}-${actualFileName}` }); const url = `${this.baseURL}/cgi-bin/filemanager/utilRequest.cgi?${params}`; const response = await fetch(url, { method: 'POST', body: form }); const data = await response.json(); return { success: data.status === 1, message: data.status === 1 ? `File "${actualFileName}" uploaded successfully` : data.error_msg, remotePath: path.join(remotePath, actualFileName) }; } catch (error) { return { success: false, error: error.message }; } } /** * Download a file from QNAP NAS */ async downloadFile(remoteFilePath, localPath = null) { if (!await this.ensureAuthenticated()) { throw new Error('Authentication failed'); } try { const params = new URLSearchParams({ func: 'download', sid: this.sid, path: remoteFilePath }); const url = `${this.baseURL}/cgi-bin/filemanager/utilRequest.cgi?${params}`; const response = await fetch(url); if (!response.ok) { throw new Error(`Download failed: ${response.statusText}`); } const fileName = path.basename(remoteFilePath); const outputPath = localPath || `./${fileName}`; // Write file to local system const buffer = await response.buffer(); fs.writeFileSync(outputPath, buffer); return { success: true, message: `File downloaded successfully`, localPath: outputPath, size: buffer.length }; } catch (error) { return { success: false, error: error.message }; } } /** * Delete a file or directory */ async deleteItem(itemPath, fileName) { if (!await this.ensureAuthenticated()) { throw new Error('Authentication failed'); } try { const params = new URLSearchParams({ func: 'delete', sid: this.sid, path: itemPath, file_name: fileName, file_total: '1' }); const url = `${this.baseURL}/cgi-bin/filemanager/utilRequest.cgi?${params}`; const response = await fetch(url); const data = await response.json(); return { success: data.status === 1, message: data.status === 1 ? `"${fileName}" deleted successfully` : data.error_msg }; } catch (error) { return { success: false, error: error.message }; } } /** * Copy or move files */ async copyOrMoveFile(sourcePath, sourceFile, destPath, operation = 'copy') { if (!await this.ensureAuthenticated()) { throw new Error('Authentication failed'); } try { const params = new URLSearchParams({ func: operation, // 'copy' or 'move' sid: this.sid, source_path: sourcePath, source_file: sourceFile, source_total: '1', dest_path: destPath, mode: '1' }); const url = `${this.baseURL}/cgi-bin/filemanager/utilRequest.cgi?${params}`; const response = await fetch(url); const data = await response.json(); return { success: data.status === 1, message: data.status === 1 ? `File ${operation === 'copy' ? 'copied' : 'moved'} successfully` : data.error_msg, operation: operation }; } catch (error) { return { success: false, error: error.message }; } } /** * Create n8n blueprints directory structure */ async createN8nBlueprintsStructure() { try { // Create main n8n-blueprints directory await this.createDirectory('/Public', 'n8n-blueprints'); // Create subdirectories for organization const subdirs = ['workflows', 'community-nodes', 'credentials', 'backups']; for (const subdir of subdirs) { await this.createDirectory('/Public/n8n-blueprints', subdir); } return { success: true, message: 'n8n blueprints directory structure created successfully', directories: subdirs.map(dir => `/Public/n8n-blueprints/${dir}`) }; } catch (error) { return { success: false, error: error.message }; } } /** * Upload n8n workflow blueprint */ async uploadN8nBlueprint(localBlueprintPath, blueprintType = 'workflows') { try { const blueprintName = path.basename(localBlueprintPath); const remotePath = `/Public/n8n-blueprints/${blueprintType}`; const result = await this.uploadFile(localBlueprintPath, remotePath, blueprintName); return { ...result, blueprintType, remotePath: result.success ? `${remotePath}/${blueprintName}` : null }; } catch (error) { return { success: false, error: error.message }; } } } export default QNAPClient;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bermingham85/mcp-puppet-pipeline'

If you have feedback or need assistance with the MCP directory API, please join our Discord server