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;