├── .gitignore ├── docs └── images │ ├── filezap-logo.png │ └── filezap-logo.svg ├── bin ├── cpd.js └── filezap.js ├── .npmignore ├── package.json ├── commander ├── utils │ ├── generateKey.js │ ├── listSharedFiles.js │ ├── copyCmd.js │ └── shellIntegration.js └── commands.js ├── utils ├── logger.js ├── fallbackTunnel.js ├── tunnelManager.js ├── nonInteractiveSsh.js ├── ngrokDiagnostics.js └── tunnelProviders.js ├── setup.js ├── src ├── client.js └── server.js └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | cpd-1.0.0.tgz 3 | update.txt 4 | youtube-script.md -------------------------------------------------------------------------------- /docs/images/filezap-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanshcodeworks/filezap/HEAD/docs/images/filezap-logo.png -------------------------------------------------------------------------------- /bin/cpd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Import the new filezap.js instead of commands.js directly 4 | import '../bin/filezap.js'; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Development files 2 | .git 3 | .github 4 | .editorconfig 5 | .eslintrc 6 | .prettierrc 7 | .idea 8 | .vscode 9 | *.log 10 | *.swp 11 | *~ 12 | 13 | .node_modules 14 | node_modules 15 | # Source control 16 | .gitignore 17 | .gitattributes 18 | 19 | # Build scripts 20 | webpack.config.js 21 | rollup.config.js 22 | babel.config.js 23 | 24 | # Test files 25 | test 26 | tests 27 | __tests__ 28 | coverage 29 | .nyc_output 30 | 31 | # Documentation 32 | docs 33 | 34 | .env -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filezap", 3 | "version": "2.0.4", 4 | "description": "Fast and secure file sharing across devices and networks", 5 | "type": "module", 6 | "main": "./bin/filezap.js", 7 | "bin": { 8 | "filezap": "./bin/filezap.js" 9 | }, 10 | "scripts": { 11 | "start": "node ./bin/filezap.js" 12 | }, 13 | "dependencies": { 14 | "axios": "^1.6.0", 15 | "boxen": "^7.1.1", 16 | "chalk": "^5.3.0", 17 | "commander": "^11.0.0", 18 | "crypto-js": "^4.1.1", 19 | "fs-extra": "^11.1.1", 20 | "get-port": "^7.0.0", 21 | "gradient-string": "^2.0.2", 22 | "ip": "^1.1.8", 23 | "ngrok": "^5.0.0-beta.2", 24 | "ora": "^7.0.1", 25 | "qrcode-terminal": "^0.12.0", 26 | "ws": "^8.14.1" 27 | }, 28 | "license": "MIT" 29 | } 30 | -------------------------------------------------------------------------------- /commander/utils/generateKey.js: -------------------------------------------------------------------------------- 1 | import ora from 'ora'; 2 | import os from 'os' 3 | import fs from 'fs-extra' 4 | import CryptoJS from 'crypto-js'; 5 | import path from 'path'; 6 | 7 | const CONFIG_DIR = path.join(os.homedir(), '.filezap'); 8 | const KEY_FILE = path.join(CONFIG_DIR, 'keys.json'); 9 | 10 | fs.ensureDirSync(CONFIG_DIR); 11 | if (!fs.existsSync(KEY_FILE)) { 12 | fs.writeJSONSync(KEY_FILE, {}, { spaces: 2 }); 13 | } 14 | 15 | export function generateKey(){ 16 | const spinner = ora("Generating secure Key").start(); 17 | try { 18 | const newKey = CryptoJS.lib.WordArray.random(4).toString(); 19 | const username = os.userInfo().username; 20 | 21 | const keys = fs.readJSONSync(KEY_FILE); 22 | 23 | keys[username] = newKey; 24 | fs.writeJSONSync(KEY_FILE, keys, { spaces: 2 }); 25 | 26 | spinner.succeed(`Key generated successfully for ${username}`); 27 | console.log(`Your key: ${newKey}`); 28 | console.log(`Store this safely. Others will need it to copy files to you.`); 29 | 30 | } catch (error) { 31 | spinner.fail('failed to generate key'); 32 | console.error(error); 33 | process.exit(1); 34 | } 35 | } -------------------------------------------------------------------------------- /commander/utils/listSharedFiles.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import ora from 'ora'; 5 | 6 | // Fix the inconsistent directory path (.filezap to .filezap) 7 | const CONFIG_DIR = path.join(os.homedir(), '.filezap'); 8 | 9 | export function listSharedFiles() { 10 | const spinner = ora('Looking for shared files...').start(); 11 | 12 | try { 13 | const username = os.userInfo().username; 14 | const userDir = path.join(CONFIG_DIR, 'shared', username); 15 | 16 | // Create directory if it doesn't exist 17 | fs.ensureDirSync(userDir); 18 | 19 | // Read the files in the directory 20 | const files = fs.readdirSync(userDir); 21 | 22 | spinner.succeed('Found shared files:'); 23 | 24 | if (files.length === 0) { 25 | console.log('No files have been shared with you yet.'); 26 | } else { 27 | console.log(`Files shared with you (${files.length}):\n`); 28 | 29 | files.forEach((file, index) => { 30 | const filePath = path.join(userDir, file); 31 | const stats = fs.statSync(filePath); 32 | const fileSizeInKB = (stats.size / 1024).toFixed(2); 33 | 34 | console.log(`${index + 1}. ${file} (${fileSizeInKB} KB)`); 35 | console.log(` Location: ${filePath}`); 36 | }); 37 | } 38 | } catch (error) { 39 | spinner.fail('Failed to list shared files'); 40 | console.error(error); 41 | } 42 | } -------------------------------------------------------------------------------- /utils/logger.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | 5 | // Fix the inconsistent directory path (.filezap to .filezap) 6 | const LOG_DIR = path.join(os.homedir(), '.filezap', 'logs'); 7 | const DEBUG_LOG_PATH = path.join(LOG_DIR, 'debug.log'); 8 | let isDebugMode = false; 9 | 10 | /** 11 | * Initialize debug logging 12 | * @param {boolean} debugMode - Whether to enable debug mode 13 | */ 14 | export function initDebugLogging(debugMode) { 15 | isDebugMode = debugMode; 16 | 17 | if (isDebugMode) { 18 | // Create logs directory if it doesn't exist 19 | fs.ensureDirSync(LOG_DIR); 20 | 21 | // Add timestamp to log entries 22 | const now = new Date(); 23 | const timestamp = `${now.toISOString()}\n${'='.repeat(50)}\n`; 24 | 25 | // Append to log file 26 | fs.appendFileSync(DEBUG_LOG_PATH, `\n\nSession started: ${timestamp}`); 27 | } 28 | } 29 | 30 | /** 31 | * Log debug information 32 | * @param {...any} args - Information to log 33 | */ 34 | export function logDebug(...args) { 35 | if (isDebugMode) { 36 | const timestamp = new Date().toISOString(); 37 | const logMessage = args.map(arg => 38 | typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) 39 | ).join(' '); 40 | 41 | // Write to log file 42 | fs.appendFileSync(DEBUG_LOG_PATH, `[${timestamp}] ${logMessage}\n`); 43 | 44 | // Also output to console with timestamp 45 | console.error(`[DEBUG] ${logMessage}`); 46 | } 47 | } 48 | 49 | /** 50 | * Get the path to the debug log file 51 | * @returns {string} Path to debug log file 52 | */ 53 | export function getDebugLogPath() { 54 | return DEBUG_LOG_PATH; 55 | } 56 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { execSync } from 'child_process'; 3 | import ora from 'ora'; 4 | import chalk from 'chalk'; 5 | import os from 'os'; 6 | 7 | const spinner = ora('Setting up FileZap globally...').start(); 8 | 9 | try { 10 | // Run npm link to make filezap globally available 11 | spinner.text = 'Creating global symlink...'; 12 | execSync('npm link', { stdio: 'pipe' }); 13 | 14 | spinner.succeed('FileZap is now globally available!'); 15 | 16 | console.log('\n' + chalk.green.bold('✓') + ' You can now run FileZap from anywhere using the ' + chalk.cyan('filezap') + ' command.'); 17 | console.log('\nFor example:'); 18 | console.log(' ' + chalk.yellow('filezap send myfile.txt')); 19 | console.log(' ' + chalk.yellow('filezap list')); 20 | 21 | // Suggest shell integration 22 | console.log('\nWould you like to install right-click menu integration?'); 23 | console.log('Run: ' + chalk.cyan('filezap integrate')); 24 | 25 | // Show current system info 26 | console.log('\nSystem Information:'); 27 | console.log(' OS: ' + os.type() + ' (' + os.platform() + ')'); 28 | console.log(' Node: ' + process.version); 29 | 30 | // Show how to uninstall if needed 31 | console.log('\nTo uninstall FileZap from the global commands:'); 32 | console.log(' ' + chalk.yellow('npm unlink -g filezap') + ' or ' + chalk.yellow('npm run unlink')); 33 | 34 | } catch (error) { 35 | spinner.fail('Failed to set up FileZap globally'); 36 | console.error('\n' + chalk.red('Error: ') + error.message); 37 | 38 | // Provide helpful error messages for common issues 39 | if (error.message.includes('permission')) { 40 | console.log('\nTry running with administrator/sudo privileges:'); 41 | if (os.platform() === 'win32') { 42 | console.log(' ' + chalk.cyan('Right-click on Command Prompt/PowerShell and select "Run as administrator"')); 43 | } else { 44 | console.log(' ' + chalk.cyan('sudo npm link')); 45 | } 46 | } 47 | 48 | process.exit(1); 49 | } 50 | -------------------------------------------------------------------------------- /utils/fallbackTunnel.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import { logDebug } from './logger.js'; 3 | import axios from 'axios'; 4 | 5 | /** 6 | * Create a fallback tunnel using publicly available services that don't require SSH 7 | * @param {number} port - Local port to expose 8 | * @returns {Promise} - Public URL 9 | */ 10 | export async function createFallbackTunnel(port) { 11 | // Try these services in order 12 | const services = [ 13 | tryPlayground, 14 | tryPagekite, 15 | tryTunnelto 16 | ]; 17 | 18 | for (const service of services) { 19 | try { 20 | const url = await service(port); 21 | if (url) return url; 22 | } catch (error) { 23 | logDebug(`Fallback tunnel provider error: ${error.message}`); 24 | } 25 | } 26 | 27 | throw new Error('All fallback tunnel providers failed'); 28 | } 29 | 30 | /** 31 | * Try to create a tunnel using js.org playground 32 | * @param {number} port - Local port to expose 33 | * @returns {Promise} - Public URL 34 | */ 35 | async function tryPlayground(port) { 36 | try { 37 | logDebug('Trying js.org playground tunnel'); 38 | 39 | // This service uses a simple HTTP request to establish a tunnel 40 | const response = await axios.post('https://playground.js.org/tunnel', { 41 | port: port, 42 | }, { 43 | timeout: 10000 44 | }); 45 | 46 | if (response.data && response.data.url) { 47 | return response.data.url; 48 | } 49 | throw new Error('Invalid response from playground.js.org'); 50 | } catch (error) { 51 | logDebug(`Playground tunnel error: ${error.message}`); 52 | throw error; 53 | } 54 | } 55 | 56 | /** 57 | * Try to create a tunnel using tunnelto.dev 58 | * @param {number} port - Local port to expose 59 | * @returns {Promise} - Public URL 60 | */ 61 | async function tryTunnelto(port) { 62 | // Implementation similar to above for tunnelto.dev 63 | // Note: This is a placeholder - tunnelto requires a binary/client 64 | throw new Error('tunnelto not implemented'); 65 | } 66 | 67 | /** 68 | * Try to create a tunnel using pagekite 69 | * @param {number} port - Local port to expose 70 | * @returns {Promise} - Public URL 71 | */ 72 | async function tryPagekite(port) { 73 | // Implementation similar to above for pagekite 74 | // Note: This is a placeholder - pagekite requires a binary/client 75 | throw new Error('pagekite not implemented'); 76 | } 77 | -------------------------------------------------------------------------------- /commander/utils/copyCmd.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import ora from 'ora'; 3 | import path from 'path'; 4 | import os from 'os'; 5 | 6 | // Common constants (should match generateKey.js) 7 | const CONFIG_DIR = path.join(os.homedir(), '.filezap'); 8 | const KEY_FILE = path.join(CONFIG_DIR, 'keys.json'); 9 | 10 | export function copyCmd(filepath, userKey) { 11 | const spinner = ora(`Copying ${filepath} to user with key ${userKey}`).start(); 12 | 13 | try { 14 | // Check if file exists 15 | if (!fs.existsSync(filepath)) { 16 | spinner.fail(`File not found: ${filepath}`); 17 | return; 18 | } 19 | 20 | // Normalize filepath to handle different path formats 21 | filepath = path.normalize(filepath); 22 | 23 | // Extract username and key 24 | let username, key; 25 | 26 | if (userKey.includes(':')) { 27 | [username, key] = userKey.split(':'); 28 | } else { 29 | key = userKey; 30 | // Try to find username for this key 31 | const keys = fs.readJSONSync(KEY_FILE, { throws: false }) || {}; 32 | const entry = Object.entries(keys).find(([_, val]) => val === key); 33 | 34 | if (!entry) { 35 | spinner.fail('Invalid key - no matching user found'); 36 | return; 37 | } 38 | username = entry[0]; 39 | } 40 | 41 | if (!username || !key) { 42 | spinner.fail('Invalid key format'); 43 | return; 44 | } 45 | 46 | // Load keys for validation 47 | const keys = fs.readJSONSync(KEY_FILE, { throws: false }) || {}; 48 | 49 | // Validate the key 50 | if (keys[username] !== key) { 51 | spinner.fail('Invalid key for specified user'); 52 | return; 53 | } 54 | 55 | // Create the destination directory 56 | const userDir = path.join(CONFIG_DIR, 'shared', username); 57 | fs.ensureDirSync(userDir); 58 | 59 | // Get the filename from the path 60 | const filename = path.basename(filepath); 61 | const destPath = path.join(userDir, filename); 62 | 63 | // Copy the file 64 | fs.copySync(filepath, destPath); 65 | 66 | spinner.succeed(`File copied successfully to ${username}'s shared folder`); 67 | console.log(`Location: ${destPath}`); 68 | } catch (error) { 69 | spinner.fail("Unable to copy the file"); 70 | console.error("Error:", error.message); 71 | } 72 | } -------------------------------------------------------------------------------- /docs/images/filezap-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 41 | 43 | 45 | 48 | 50 | 52 | 55 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /bin/filezap.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs-extra'; 4 | import path from 'path'; 5 | import readline from 'readline'; 6 | import chalk from 'chalk'; 7 | import { exec } from 'child_process'; 8 | import { promisify } from 'util'; 9 | import { startFileServer } from '../src/server.js'; 10 | 11 | // Import commander for CLI commands 12 | import '../commander/commands.js'; 13 | 14 | // Return true if running in an interactive terminal 15 | function isInteractive() { 16 | return process.stdin.isTTY && process.stdout.isTTY; 17 | } 18 | 19 | // Handle keyboard shortcuts only in interactive mode 20 | if (isInteractive()) { 21 | // Only show shortcuts tip in normal mode, not for share-ui command 22 | if (!process.argv.includes('share-ui')) { 23 | console.log(chalk.cyan('💡 Tip: Press Alt+S or Ctrl+S for quick file sharing')); 24 | } 25 | 26 | // Set up keyboard event handling 27 | readline.emitKeypressEvents(process.stdin); 28 | 29 | // Try to set raw mode - this is required for keyboard shortcuts 30 | try { 31 | if (process.stdin.setRawMode) { 32 | process.stdin.setRawMode(true); 33 | 34 | // Listen for keyboard shortcuts 35 | process.stdin.on('keypress', async (str, key) => { 36 | // Check for Alt+S or Ctrl+S (Alt key is represented as 'meta' in some terminals) 37 | if ((key && key.ctrl && key.name === 's') || 38 | (key && key.meta && key.name === 's')) { 39 | console.log(chalk.green('\n🔍 Quick Share activated. Opening file selector...')); 40 | 41 | try { 42 | // Different file picker approach based on platform 43 | let filePath; 44 | const execAsync = promisify(exec); 45 | 46 | if (process.platform === 'win32') { 47 | // For Windows - use a more reliable PowerShell script 48 | const script = ` 49 | Add-Type -AssemblyName System.Windows.Forms 50 | $openFileDialog = New-Object System.Windows.Forms.OpenFileDialog 51 | $openFileDialog.Title = "FileZap - Select a file to share" 52 | $openFileDialog.Filter = "All files (*.*)|*.*" 53 | if($openFileDialog.ShowDialog() -eq 'OK') { 54 | $openFileDialog.FileName 55 | } 56 | `; 57 | 58 | const { stdout } = await execAsync(`powershell -Command "${script}"`); 59 | filePath = stdout.trim(); 60 | } 61 | else if (process.platform === 'darwin') { 62 | // For macOS 63 | const { stdout } = await execAsync('osascript -e \'tell application "System Events" to POSIX path of (choose file)\''); 64 | filePath = stdout.trim(); 65 | } 66 | else { 67 | // For Linux - try zenity first 68 | try { 69 | const { stdout } = await execAsync('zenity --file-selection --title="FileZap - Select a file to share"'); 70 | filePath = stdout.trim(); 71 | } catch (e) { 72 | // Fallback to manual input if zenity not available 73 | console.log(chalk.yellow('File selector not available. Please enter a path manually:')); 74 | const rl = readline.createInterface({ 75 | input: process.stdin, 76 | output: process.stdout 77 | }); 78 | 79 | filePath = await new Promise((resolve) => { 80 | rl.question('Enter file path: ', (answer) => { 81 | rl.close(); 82 | resolve(answer); 83 | }); 84 | }); 85 | } 86 | } 87 | 88 | if (filePath && fs.existsSync(filePath)) { 89 | console.log(`\nSharing file: ${filePath}`); 90 | 91 | // Start sharing with browser UI 92 | await startFileServer(filePath, { 93 | webOnly: false, 94 | openBrowser: true 95 | }); 96 | } else { 97 | console.log(chalk.yellow('No file selected or file not found. Cancelling quick share.')); 98 | } 99 | } catch (error) { 100 | console.error(chalk.red(`Error: ${error.message}`)); 101 | } 102 | } 103 | 104 | // Ctrl+C to exit 105 | if (key && key.ctrl && key.name === 'c') { 106 | process.exit(0); 107 | } 108 | }); 109 | } else { 110 | console.log(chalk.yellow('Keyboard shortcuts not available in this environment.')); 111 | } 112 | } catch (error) { 113 | console.log(chalk.yellow(`Keyboard shortcuts disabled: ${error.message}`)); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /utils/tunnelManager.js: -------------------------------------------------------------------------------- 1 | import ngrok from 'ngrok'; 2 | import fs from 'fs-extra'; 3 | import path from 'path'; 4 | import os from 'os'; 5 | import chalk from 'chalk'; 6 | import { logDebug } from './logger.js'; 7 | 8 | // Directory to store tunnel info 9 | const TUNNEL_DIR = path.join(os.homedir(), '.filezap', 'tunnels'); 10 | const TUNNEL_INFO_PATH = path.join(TUNNEL_DIR, 'active_tunnels.json'); 11 | 12 | /** 13 | * Initialize the tunnel manager 14 | */ 15 | export function initTunnelManager() { 16 | try { 17 | fs.ensureDirSync(TUNNEL_DIR); 18 | 19 | // Create tunnel file if it doesn't exist 20 | if (!fs.existsSync(TUNNEL_INFO_PATH)) { 21 | saveTunnelInfo([]); 22 | } 23 | 24 | // Validate the JSON format 25 | try { 26 | const data = fs.readJSONSync(TUNNEL_INFO_PATH); 27 | if (!data || !Array.isArray(data.tunnels)) { 28 | // Fix corrupted data 29 | saveTunnelInfo([]); 30 | } 31 | } catch (parseError) { 32 | logDebug(`Tunnel info file corrupted, recreating: ${parseError.message}`); 33 | saveTunnelInfo([]); 34 | } 35 | } catch (error) { 36 | logDebug(`Error initializing tunnel manager: ${error.message}`); 37 | // Try to proceed even with error 38 | } 39 | } 40 | 41 | /** 42 | * Save tunnel information to disk 43 | * @param {Array} tunnels - List of active tunnel URLs 44 | */ 45 | function saveTunnelInfo(tunnels) { 46 | try { 47 | fs.writeJSONSync(TUNNEL_INFO_PATH, { 48 | tunnels, 49 | lastUpdated: new Date().toISOString() 50 | }, { spaces: 2 }); 51 | } catch (error) { 52 | logDebug(`Error saving tunnel info: ${error.message}`); 53 | } 54 | } 55 | 56 | /** 57 | * Load tunnel information from disk 58 | * @returns {Array} List of active tunnel URLs 59 | */ 60 | function loadTunnelInfo() { 61 | try { 62 | if (fs.existsSync(TUNNEL_INFO_PATH)) { 63 | const data = fs.readJSONSync(TUNNEL_INFO_PATH); 64 | return data.tunnels || []; 65 | } 66 | } catch (error) { 67 | logDebug(`Error loading tunnel info: ${error.message}`); 68 | } 69 | return []; 70 | } 71 | 72 | /** 73 | * Register a new tunnel 74 | * @param {string} tunnelUrl - The ngrok tunnel URL 75 | */ 76 | export function registerTunnel(tunnelUrl) { 77 | try { 78 | const tunnels = loadTunnelInfo(); 79 | if (!tunnels.includes(tunnelUrl)) { 80 | tunnels.push(tunnelUrl); 81 | saveTunnelInfo(tunnels); 82 | } 83 | } catch (error) { 84 | logDebug(`Error registering tunnel: ${error.message}`); 85 | } 86 | } 87 | 88 | /** 89 | * Unregister a tunnel 90 | * @param {string} tunnelUrl - The ngrok tunnel URL 91 | */ 92 | export function unregisterTunnel(tunnelUrl) { 93 | try { 94 | const tunnels = loadTunnelInfo(); 95 | const updatedTunnels = tunnels.filter(url => url !== tunnelUrl); 96 | saveTunnelInfo(updatedTunnels); 97 | } catch (error) { 98 | logDebug(`Error unregistering tunnel: ${error.message}`); 99 | } 100 | } 101 | 102 | /** 103 | * List all active tunnels 104 | * @returns {Promise} Object containing active tunnels info 105 | */ 106 | export async function listActiveTunnels() { 107 | const result = { 108 | fromNgrok: [], 109 | fromRegistry: loadTunnelInfo(), 110 | active: false, 111 | ngrokVersion: null, 112 | error: null 113 | }; 114 | 115 | try { 116 | // Try to get ngrok version 117 | try { 118 | result.ngrokVersion = ngrok.getVersion ? await ngrok.getVersion() : 'unknown'; 119 | } catch (versionError) { 120 | result.error = `Version error: ${versionError.message}`; 121 | } 122 | 123 | // Try to get running tunnels directly from ngrok 124 | try { 125 | await ngrok.connect({ addr: 9999, name: 'test-connection' }); 126 | result.active = true; 127 | 128 | const tunnelList = await ngrok.listTunnels(); 129 | if (tunnelList && tunnelList.tunnels) { 130 | result.fromNgrok = tunnelList.tunnels.map(t => t.public_url); 131 | } 132 | 133 | // Close the test tunnel 134 | await ngrok.disconnect('test-connection'); 135 | } catch (tunnelError) { 136 | if (!result.error) { 137 | result.error = tunnelError.message; 138 | } 139 | } 140 | 141 | return result; 142 | } catch (error) { 143 | result.error = `General error: ${error.message}`; 144 | return result; 145 | } 146 | } 147 | 148 | /** 149 | * Close all tunnels 150 | * @returns {Promise} Results of the operation 151 | */ 152 | export async function closeAllTunnels() { 153 | const result = { 154 | closedFromNgrok: 0, 155 | closedFromRegistry: 0, 156 | errors: [] 157 | }; 158 | 159 | // Kill all ngrok processes 160 | try { 161 | await ngrok.kill(); 162 | result.closedFromNgrok = 1; 163 | } catch (killError) { 164 | result.errors.push(`Kill error: ${killError.message}`); 165 | } 166 | 167 | // Clear the registry 168 | try { 169 | const registeredTunnels = loadTunnelInfo(); 170 | result.closedFromRegistry = registeredTunnels.length; 171 | saveTunnelInfo([]); 172 | } catch (registryError) { 173 | result.errors.push(`Registry error: ${registryError.message}`); 174 | } 175 | 176 | return result; 177 | } 178 | -------------------------------------------------------------------------------- /utils/nonInteractiveSsh.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import { spawn } from 'child_process'; 5 | import { exec } from 'child_process'; 6 | import { promisify } from 'util'; 7 | import { logDebug } from './logger.js'; 8 | 9 | const execAsync = promisify(exec); 10 | const SSH_DIR = path.join(os.homedir(), '.ssh'); 11 | 12 | /** 13 | * Configure SSH to accept keys non-interactively for certain hosts 14 | * @param {string} host - The hostname to configure 15 | * @returns {Promise} - Success status 16 | */ 17 | export async function configureNonInteractiveSsh(host) { 18 | try { 19 | const configFile = path.join(SSH_DIR, 'config'); 20 | fs.ensureDirSync(SSH_DIR); 21 | 22 | // Check if config already exists for this host 23 | let configContent = ''; 24 | if (fs.existsSync(configFile)) { 25 | configContent = fs.readFileSync(configFile, 'utf8'); 26 | } 27 | 28 | if (!configContent.includes(`Host ${host}`)) { 29 | const hostConfig = ` 30 | # Added by FileZap for non-interactive tunnels 31 | Host ${host} 32 | StrictHostKeyChecking no 33 | UserKnownHostsFile /dev/null 34 | BatchMode yes 35 | `; 36 | fs.appendFileSync(configFile, hostConfig); 37 | logDebug(`Added ${host} configuration to SSH config`); 38 | } 39 | 40 | return true; 41 | } catch (error) { 42 | logDebug(`Failed to configure SSH for ${host}: ${error.message}`); 43 | return false; 44 | } 45 | } 46 | 47 | /** 48 | * Create an SSH tunnel with proper non-interactive settings 49 | * @param {number} localPort - The local port to tunnel 50 | * @param {string} host - The SSH host 51 | * @param {string} user - The SSH user 52 | * @param {number} remotePort - The remote port to use (default: 80) 53 | * @param {number} timeoutMs - Timeout in milliseconds 54 | * @returns {Promise<{url: string, process: object}>} - The tunnel URL and process 55 | */ 56 | export function createSshTunnel(localPort, host, user, remotePort = 80, timeoutMs = 15000) { 57 | return new Promise(async (resolve, reject) => { 58 | try { 59 | // Configure SSH to be non-interactive for this host 60 | await configureNonInteractiveSsh(host); 61 | 62 | // Command for creating reverse tunnel 63 | const sshArgs = [ 64 | '-o', 'StrictHostKeyChecking=no', 65 | '-o', 'UserKnownHostsFile=/dev/null', 66 | '-o', 'BatchMode=yes', 67 | '-R', `${remotePort}:localhost:${localPort}`, 68 | `${user}@${host}` 69 | ]; 70 | 71 | // Set timeout to kill the process if it takes too long 72 | const tunnelTimeout = setTimeout(() => { 73 | sshProcess.kill(); 74 | reject(new Error(`SSH tunnel creation timed out after ${timeoutMs}ms`)); 75 | }, timeoutMs); 76 | 77 | // Spawn SSH process 78 | const sshProcess = spawn('ssh', sshArgs, { 79 | stdio: ['ignore', 'pipe', 'pipe'] 80 | }); 81 | 82 | let output = ''; 83 | let errorOutput = ''; 84 | 85 | sshProcess.stdout.on('data', (data) => { 86 | const chunk = data.toString(); 87 | output += chunk; 88 | logDebug(`SSH stdout: ${chunk}`); 89 | }); 90 | 91 | sshProcess.stderr.on('data', (data) => { 92 | const chunk = data.toString(); 93 | errorOutput += chunk; 94 | logDebug(`SSH stderr: ${chunk}`); 95 | }); 96 | 97 | // Monitor for URLs in both stdout and stderr 98 | const checkForUrl = (text, regexPattern) => { 99 | const match = text.match(regexPattern); 100 | if (match && match[0]) { 101 | clearTimeout(tunnelTimeout); 102 | resolve({ 103 | url: match[0], 104 | process: sshProcess 105 | }); 106 | return true; 107 | } 108 | return false; 109 | }; 110 | 111 | // Function to check both streams periodically 112 | const urlCheckInterval = setInterval(() => { 113 | // Different hosts might output URLs in different formats 114 | if (host === 'localhost.run') { 115 | if (checkForUrl(output, /https?:\/\/[a-zA-Z0-9\-]+\.localhost\.run/i)) { 116 | clearInterval(urlCheckInterval); 117 | } 118 | } else if (host === 'serveo.net') { 119 | if (checkForUrl(output, /https?:\/\/[a-zA-Z0-9\-]+\.serveo\.net/i) || 120 | checkForUrl(errorOutput, /https?:\/\/[a-zA-Z0-9\-]+\.serveo\.net/i)) { 121 | clearInterval(urlCheckInterval); 122 | } 123 | } 124 | }, 500); 125 | 126 | // Handle process exit 127 | sshProcess.on('exit', (code) => { 128 | clearTimeout(tunnelTimeout); 129 | clearInterval(urlCheckInterval); 130 | 131 | if (code !== 0) { 132 | reject(new Error(`SSH process exited with code ${code}: ${errorOutput}`)); 133 | } else if (!output.includes(host)) { 134 | // If the process exited normally but we never got a URL 135 | reject(new Error(`SSH process completed but no tunnel URL found: ${output}`)); 136 | } 137 | }); 138 | 139 | sshProcess.on('error', (err) => { 140 | clearTimeout(tunnelTimeout); 141 | clearInterval(urlCheckInterval); 142 | reject(new Error(`SSH process error: ${err.message}`)); 143 | }); 144 | } catch (error) { 145 | reject(error); 146 | } 147 | }); 148 | } 149 | 150 | /** 151 | * Check if SSH is available on the system 152 | * @returns {Promise} - True if SSH is available 153 | */ 154 | export async function isSshAvailable() { 155 | try { 156 | await execAsync('ssh -V'); 157 | return true; 158 | } catch (error) { 159 | logDebug(`SSH not available: ${error.message}`); 160 | return false; 161 | } 162 | } 163 | 164 | /** 165 | * Kill any SSH processes matching a specific pattern 166 | * @param {string} pattern - Pattern to match in process listing 167 | * @returns {Promise} - Success status 168 | */ 169 | export async function killSshProcesses(pattern) { 170 | try { 171 | if (os.platform() === 'win32') { 172 | await execAsync(`taskkill /F /FI "WINDOWTITLE eq *${pattern}*" /IM ssh.exe`, { timeout: 5000 }); 173 | } else { 174 | await execAsync(`pkill -f "ssh.*${pattern}"`, { timeout: 5000 }); 175 | } 176 | return true; 177 | } catch (error) { 178 | // Error might just mean no processes found 179 | return true; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /utils/ngrokDiagnostics.js: -------------------------------------------------------------------------------- 1 | import ngrok from 'ngrok'; 2 | import os from 'os'; 3 | import fs from 'fs-extra'; 4 | import path from 'path'; 5 | import ora from 'ora'; 6 | import chalk from 'chalk'; 7 | import { exec } from 'child_process'; 8 | import { promisify } from 'util'; 9 | import axios from 'axios'; 10 | 11 | const execPromise = promisify(exec); 12 | 13 | const NGROK_BIN_PATH = ngrok.getPath ? ngrok.getPath() : null; 14 | const NGROK_CONFIG_PATH = path.join(os.homedir(), '.ngrok2', 'ngrok.yml'); 15 | const REPORT_PATH = path.join(os.homedir(), '.filezap', 'logs', 'ngrok_diagnostics.json'); 16 | 17 | /** 18 | * Run diagnostics on ngrok 19 | * @returns {Promise} Diagnostic results 20 | */ 21 | export async function diagnoseNgrok() { 22 | const spinner = ora('Running ngrok diagnostics...').start(); 23 | const results = { 24 | timestamp: new Date().toISOString(), 25 | os: { 26 | platform: os.platform(), 27 | release: os.release(), 28 | type: os.type() 29 | }, 30 | ngrok: { 31 | installed: false, 32 | version: null, 33 | binary: null, 34 | config: null, 35 | authtoken: false, 36 | connection: false, 37 | errors: [] 38 | }, 39 | network: { 40 | internet: false, 41 | canReachNgrokApi: false, 42 | ports: {} 43 | } 44 | }; 45 | 46 | try { 47 | // 1. Check if ngrok binary exists 48 | if (NGROK_BIN_PATH && fs.existsSync(NGROK_BIN_PATH)) { 49 | results.ngrok.installed = true; 50 | results.ngrok.binary = NGROK_BIN_PATH; 51 | } else { 52 | results.ngrok.errors.push('Ngrok binary not found'); 53 | } 54 | 55 | // 2. Check ngrok config 56 | try { 57 | if (fs.existsSync(NGROK_CONFIG_PATH)) { 58 | const configContent = fs.readFileSync(NGROK_CONFIG_PATH, 'utf8'); 59 | results.ngrok.config = NGROK_CONFIG_PATH; 60 | // Check if auth token exists in config (don't extract it for security) 61 | if (configContent.includes('authtoken:')) { 62 | results.ngrok.authtoken = true; 63 | } 64 | } 65 | } catch (configError) { 66 | results.ngrok.errors.push(`Config error: ${configError.message}`); 67 | } 68 | 69 | // 3. Check internet connectivity 70 | try { 71 | await axios.get('https://api.ngrok.com', { timeout: 5000 }); 72 | results.network.internet = true; 73 | results.network.canReachNgrokApi = true; 74 | } catch (netError) { 75 | if (netError.response) { 76 | // Got a response, so internet works but might not have API access 77 | results.network.internet = true; 78 | } 79 | results.ngrok.errors.push(`API connectivity error: ${netError.message}`); 80 | } 81 | 82 | // 4. Try to get ngrok version 83 | if (results.ngrok.installed) { 84 | try { 85 | const { stdout } = await execPromise(`"${NGROK_BIN_PATH}" --version`); 86 | results.ngrok.version = stdout.trim(); 87 | } catch (versionError) { 88 | results.ngrok.errors.push(`Version check error: ${versionError.message}`); 89 | } 90 | } 91 | 92 | // 5. Test ngrok connection briefly 93 | try { 94 | spinner.text = 'Testing ngrok connection...'; 95 | const url = await ngrok.connect({ addr: 9999, onStatusChange: status => { 96 | if (status === 'connected') { 97 | results.ngrok.connection = true; 98 | } 99 | }}); 100 | 101 | if (url) { 102 | results.ngrok.connection = true; 103 | // Disconnect immediately after test 104 | await ngrok.disconnect(url); 105 | } 106 | } catch (connError) { 107 | results.ngrok.errors.push(`Connection test error: ${connError.message}`); 108 | } 109 | 110 | // Save diagnostic results 111 | fs.ensureDirSync(path.dirname(REPORT_PATH)); 112 | fs.writeJSONSync(REPORT_PATH, results, { spaces: 2 }); 113 | 114 | spinner.succeed('Ngrok diagnostics completed'); 115 | return results; 116 | } catch (error) { 117 | spinner.fail(`Diagnostics failed: ${error.message}`); 118 | results.ngrok.errors.push(`General error: ${error.message}`); 119 | return results; 120 | } 121 | } 122 | 123 | /** 124 | * Fix common ngrok issues 125 | * @returns {Promise} Results of the fix attempts 126 | */ 127 | export async function fixNgrokIssues() { 128 | const spinner = ora('Attempting to fix ngrok issues...').start(); 129 | const results = { 130 | actions: [], 131 | fixed: false, 132 | errors: [] 133 | }; 134 | 135 | try { 136 | // 1. Check for existing processes and kill them 137 | spinner.text = 'Checking for orphaned ngrok processes...'; 138 | 139 | try { 140 | if (os.platform() === 'win32') { 141 | await execPromise('taskkill /f /im ngrok.exe', { timeout: 5000 }); 142 | results.actions.push('Terminated existing ngrok processes'); 143 | } else { 144 | await execPromise('pkill -f ngrok', { timeout: 5000 }); 145 | results.actions.push('Terminated existing ngrok processes'); 146 | } 147 | } catch (killError) { 148 | // Ignore errors here, likely means no processes were found 149 | } 150 | 151 | // 2. Try to reinstall ngrok if possible 152 | spinner.text = 'Reinitializing ngrok...'; 153 | try { 154 | await ngrok.kill(); 155 | results.actions.push('Killed any running ngrok processes'); 156 | } catch (killError) { 157 | results.errors.push(`Kill error: ${killError.message}`); 158 | } 159 | 160 | // 3. Test if we can connect now 161 | spinner.text = 'Testing connection after fixes...'; 162 | try { 163 | const url = await ngrok.connect({ addr: 9999 }); 164 | if (url) { 165 | await ngrok.disconnect(url); 166 | results.fixed = true; 167 | results.actions.push('Successfully established test connection'); 168 | } 169 | } catch (testError) { 170 | results.errors.push(`Test connection failed: ${testError.message}`); 171 | } 172 | 173 | spinner.succeed(results.fixed ? 'Successfully fixed ngrok issues' : 'Attempted fixes but issues may remain'); 174 | return results; 175 | } catch (error) { 176 | spinner.fail(`Fix attempt failed: ${error.message}`); 177 | results.errors.push(`General error: ${error.message}`); 178 | return results; 179 | } 180 | } 181 | 182 | /** 183 | * Display a human-readable report of ngrok status 184 | * @param {Object} diagnosticResults - Results from diagnoseNgrok 185 | */ 186 | export function displayNgrokReport(diagnosticResults) { 187 | console.log(chalk.cyan('\n====== NGROK DIAGNOSTIC REPORT ======')); 188 | 189 | // System info 190 | console.log(chalk.yellow('\n▶ System Information:')); 191 | console.log(`OS: ${diagnosticResults.os.type} (${diagnosticResults.os.platform} ${diagnosticResults.os.release})`); 192 | 193 | // Ngrok status 194 | console.log(chalk.yellow('\n▶ Ngrok Status:')); 195 | console.log(`Installation: ${diagnosticResults.ngrok.installed ? chalk.green('✓ Installed') : chalk.red('✗ Not found')}`); 196 | if (diagnosticResults.ngrok.version) { 197 | console.log(`Version: ${diagnosticResults.ngrok.version}`); 198 | } 199 | console.log(`Auth Token: ${diagnosticResults.ngrok.authtoken ? chalk.green('✓ Found') : chalk.red('✗ Missing')}`); 200 | console.log(`Test Connection: ${diagnosticResults.ngrok.connection ? chalk.green('✓ Working') : chalk.red('✗ Failed')}`); 201 | 202 | // Network status 203 | console.log(chalk.yellow('\n▶ Network Status:')); 204 | console.log(`Internet Connectivity: ${diagnosticResults.network.internet ? chalk.green('✓ Connected') : chalk.red('✗ Not connected')}`); 205 | console.log(`Ngrok API Access: ${diagnosticResults.network.canReachNgrokApi ? chalk.green('✓ Accessible') : chalk.red('✗ Not accessible')}`); 206 | 207 | // Errors 208 | if (diagnosticResults.ngrok.errors.length > 0) { 209 | console.log(chalk.yellow('\n▶ Detected Issues:')); 210 | diagnosticResults.ngrok.errors.forEach((error, i) => { 211 | console.log(`${i+1}. ${chalk.red(error)}`); 212 | }); 213 | 214 | // Recommendations 215 | console.log(chalk.yellow('\n▶ Recommendations:')); 216 | if (!diagnosticResults.ngrok.installed) { 217 | console.log('• Run npm reinstall ngrok to reinstall the binary'); 218 | } 219 | if (!diagnosticResults.ngrok.authtoken) { 220 | console.log('• Set up an ngrok auth token with: npx ngrok authtoken YOUR_TOKEN'); 221 | console.log(' (Get a token at https://dashboard.ngrok.com/get-started/your-authtoken)'); 222 | } 223 | if (!diagnosticResults.network.internet) { 224 | console.log('• Check your internet connection'); 225 | } 226 | if (diagnosticResults.ngrok.errors.some(e => e.includes('already in use'))) { 227 | console.log('• Kill existing ngrok processes with: npx ngrok kill'); 228 | } 229 | } else if (diagnosticResults.ngrok.connection) { 230 | console.log(chalk.green('\n✓ Ngrok appears to be working correctly!')); 231 | } 232 | 233 | console.log(chalk.cyan('\n====================================')); 234 | } 235 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws'; 2 | import fs from 'fs-extra'; 3 | import path from 'path'; 4 | import os from 'os'; 5 | import ora from 'ora'; 6 | import chalk from 'chalk'; 7 | import axios from 'axios'; 8 | import readline from 'readline'; 9 | 10 | // Fix the inconsistent directory path (.cpd to .filezap) 11 | const CONFIG_DIR = path.join(os.homedir(), '.filezap'); 12 | 13 | // Function to resolve shortened URLs 14 | async function resolveUrl(url) { 15 | try { 16 | const response = await axios.get(url, { 17 | maxRedirects: 0, 18 | validateStatus: status => status >= 200 && status < 400 19 | }); 20 | return url; // URL is not redirecting, it's the final URL 21 | } catch (error) { 22 | if (error.response && error.response.headers.location) { 23 | return error.response.headers.location; 24 | } 25 | return url; // If can't resolve, return original 26 | } 27 | } 28 | 29 | // Function to prompt for password if needed 30 | async function promptForPassword() { 31 | const rl = readline.createInterface({ 32 | input: process.stdin, 33 | output: process.stdout 34 | }); 35 | 36 | return new Promise((resolve) => { 37 | rl.question('Enter password to access file: ', (password) => { 38 | rl.close(); 39 | resolve(password); 40 | }); 41 | }); 42 | } 43 | 44 | export async function receiveFile(serverIp, serverPort, fileName, password = null) { 45 | const spinner = ora(`Connecting to file server at ${serverIp}:${serverPort}...`).start(); 46 | 47 | try { 48 | // Create user directory if it doesn't exist 49 | const username = os.userInfo().username; 50 | const userDir = path.join(CONFIG_DIR, 'shared', username); 51 | fs.ensureDirSync(userDir); 52 | 53 | const filePath = path.join(userDir, fileName); 54 | 55 | // If file already exists, create a unique name 56 | let finalFilePath = filePath; 57 | let counter = 1; 58 | while (fs.existsSync(finalFilePath)) { 59 | const ext = path.extname(filePath); 60 | const baseName = path.basename(filePath, ext); 61 | finalFilePath = path.join(userDir, `${baseName}_${counter}${ext}`); 62 | counter++; 63 | } 64 | 65 | // If URL is shortened, try to resolve it 66 | if (serverIp.startsWith('http://tinyurl.com/') || 67 | serverIp.startsWith('https://tinyurl.com/') || 68 | serverIp.includes('bit.ly')) { 69 | spinner.text = 'Resolving shortened URL...'; 70 | serverIp = await resolveUrl(serverIp); 71 | } 72 | 73 | // Figure out if we're connecting to a local or ngrok URL 74 | let wsUrl; 75 | if (serverIp.startsWith('http://') || serverIp.startsWith('https://')) { 76 | // Convert HTTP URL to WebSocket URL 77 | const url = new URL(serverIp); 78 | wsUrl = `ws://${url.hostname}:${serverPort}`; 79 | spinner.text = `Connecting to remote server via tunnel...`; 80 | } else { 81 | // Standard connection 82 | wsUrl = `ws://${serverIp}:${serverPort}`; 83 | } 84 | 85 | // Connect to WebSocket server 86 | const ws = new WebSocket(wsUrl); 87 | 88 | // Set a connection timeout 89 | const connectionTimeout = setTimeout(() => { 90 | spinner.fail('Connection timed out. Server may be unreachable.'); 91 | ws.terminate(); 92 | process.exit(1); 93 | }, 10000); 94 | 95 | ws.on('open', () => { 96 | clearTimeout(connectionTimeout); 97 | spinner.text = 'Connected to server. Waiting for file...'; 98 | 99 | // After connection, send ready message with password if provided 100 | ws.send(JSON.stringify({ 101 | type: 'ready', 102 | clientName: os.hostname(), 103 | password: password // Include password if provided 104 | })); 105 | }); 106 | 107 | // Track download progress 108 | let totalSize = 0; 109 | let receivedSize = 0; 110 | let fileStartTime = 0; 111 | 112 | ws.on('message', async (data) => { 113 | // Check if this is a metadata message 114 | try { 115 | const message = JSON.parse(data.toString()); 116 | 117 | if (message.type === 'metadata') { 118 | totalSize = message.fileSize; 119 | fileStartTime = Date.now(); 120 | spinner.text = `Receiving: ${message.fileName} (${(message.fileSize / 1024).toFixed(2)} KB)`; 121 | return; 122 | } 123 | 124 | // Handle error messages (like password failures) 125 | if (message.type === 'error') { 126 | spinner.fail(`Error: ${message.message}`); 127 | 128 | // If it's a password error, prompt for password 129 | if (message.message === 'Invalid password') { 130 | console.log(chalk.yellow('\nThe file is password protected.')); 131 | 132 | // Prompt for password 133 | const enteredPassword = await promptForPassword(); 134 | 135 | // Try reconnecting with the password 136 | console.log('Reconnecting with password...'); 137 | ws.close(); 138 | setTimeout(() => { 139 | receiveFile(serverIp, serverPort, fileName, enteredPassword); 140 | }, 1000); 141 | } else { 142 | process.exit(1); 143 | } 144 | return; 145 | } 146 | 147 | // Handle ping messages to keep connection alive 148 | if (message.type === 'ping') { 149 | ws.send(JSON.stringify({ type: 'pong' })); 150 | return; 151 | } 152 | } catch (e) { 153 | // Not JSON, so it's file content 154 | receivedSize = data.length; 155 | 156 | // Calculate transfer speed 157 | const elapsedSeconds = (Date.now() - fileStartTime) / 1000; 158 | const speedKBps = ((receivedSize / 1024) / elapsedSeconds).toFixed(2); 159 | 160 | // Calculate percentage and display progress 161 | const percent = Math.floor((receivedSize / totalSize) * 100); 162 | spinner.text = `Receiving: ${fileName} | ${percent}% complete | ${speedKBps} KB/s`; 163 | 164 | // Write file with error handling 165 | try { 166 | // Make sure the directory exists 167 | const fileDir = path.dirname(finalFilePath); 168 | fs.ensureDirSync(fileDir); 169 | 170 | // Write the file 171 | fs.writeFileSync(finalFilePath, data); 172 | spinner.succeed(`File received and saved to: ${finalFilePath}`); 173 | 174 | // Acknowledge receipt 175 | ws.send(JSON.stringify({ 176 | type: 'received', 177 | clientName: os.hostname(), 178 | savePath: finalFilePath 179 | })); 180 | 181 | console.log('\n' + chalk.green('✓') + ' Transfer successful!'); 182 | console.log(chalk.cyan('File saved to:') + ' ' + finalFilePath); 183 | 184 | // Open file option based on platform 185 | if (os.platform() === 'win32') { 186 | console.log('\nTo open the file: ' + chalk.yellow(`start "${finalFilePath}"`)); 187 | } else if (os.platform() === 'darwin') { 188 | console.log('\nTo open the file: ' + chalk.yellow(`open "${finalFilePath}"`)); 189 | } else { 190 | console.log('\nTo open the file: ' + chalk.yellow(`xdg-open "${finalFilePath}"`)); 191 | } 192 | } catch (writeError) { 193 | spinner.fail(`Failed to write file: ${writeError.message}`); 194 | console.log(chalk.red(`Error: ${writeError.message}`)); 195 | 196 | // Try with a different name if there's a permission issue 197 | if (writeError.code === 'EACCES' || writeError.code === 'EPERM') { 198 | const altPath = path.join(os.tmpdir(), fileName); 199 | try { 200 | fs.writeFileSync(altPath, data); 201 | spinner.succeed(`File saved to alternate location: ${altPath}`); 202 | console.log('\n' + chalk.green('✓') + ' Transfer saved to alternate location due to permissions.'); 203 | console.log(chalk.cyan('File saved to:') + ' ' + altPath); 204 | } catch (altError) { 205 | spinner.fail(`Could not save file to alternate location: ${altError.message}`); 206 | } 207 | } 208 | } 209 | 210 | setTimeout(() => { 211 | ws.close(); 212 | process.exit(0); 213 | }, 1000); 214 | } 215 | }); 216 | 217 | ws.on('error', (error) => { 218 | clearTimeout(connectionTimeout); 219 | spinner.fail(`Connection error: ${error.message}`); 220 | console.log(chalk.yellow('\nTips:')); 221 | console.log('1. Make sure both devices are on the same network'); 222 | console.log('2. Try another IP address if multiple were provided'); 223 | console.log('3. Check if firewalls are blocking the connection'); 224 | console.log('4. If using a shortened URL, it might have expired'); 225 | console.log('5. Try using the full ngrok URL if available'); 226 | process.exit(1); 227 | }); 228 | 229 | } catch (error) { 230 | spinner.fail(`Failed to receive file: ${error.message}`); 231 | process.exit(1); 232 | } 233 | } -------------------------------------------------------------------------------- /commander/commands.js: -------------------------------------------------------------------------------- 1 | import {program} from 'commander'; 2 | import { generateKey } from '../commander/utils/generateKey.js'; 3 | import { copyCmd } from '../commander/utils/copyCmd.js'; 4 | import { listSharedFiles } from '../commander/utils/listSharedFiles.js'; 5 | import { startFileServer } from '../src/server.js'; 6 | import { receiveFile } from '../src/client.js'; 7 | import { installShellIntegration, uninstallShellIntegration } from '../commander/utils/shellIntegration.js'; 8 | import { listActiveTunnels, closeAllTunnels } from '../utils/tunnelManager.js'; 9 | import { diagnoseNgrok, fixNgrokIssues, displayNgrokReport } from '../utils/ngrokDiagnostics.js'; 10 | import { tunnelManager } from '../utils/tunnelProviders.js'; 11 | import os from 'os'; 12 | import path from 'path'; 13 | import fs from 'fs-extra'; 14 | import readline from 'readline'; 15 | import chalk from 'chalk'; 16 | import ora from 'ora'; 17 | 18 | program 19 | .version('1.0.0') 20 | .description('FileZap - Cross-platform command-line file sharing tool\n' + 21 | 'Keyboard shortcuts:\n' + 22 | ' Ctrl+S: Quick share (prompts for file)\n' + 23 | ' Ctrl+C: Exit program'); 24 | 25 | program 26 | .command('key') 27 | .alias('-key') 28 | .description('Generate a new key') 29 | .action(() => {generateKey()}); 30 | 31 | program 32 | .command('copy ') 33 | .description("Copy file to the user (key)") 34 | .action((filepath, userKey) => { 35 | copyCmd(filepath, userKey); 36 | }); 37 | 38 | program 39 | .command('list') 40 | .description("List all files shared with you") 41 | .action(() => { 42 | listSharedFiles(); 43 | }); 44 | 45 | // New WebSocket commands 46 | program 47 | .command('send ') 48 | .description("Start a file sharing server to send a file over network") 49 | .option('-p, --password ', 'Set a password for file protection') 50 | .option('-s, --secure', 'Enable password protection with auto-generated password') 51 | .option('-d, --debug', 'Enable debug mode with detailed logging') 52 | .option('-t, --tunnel', 'Force enable tunneling (even if it failed before)') 53 | .action((filepath, options) => { 54 | try { 55 | // Resolve and normalize the filepath 56 | let normalizedPath = filepath; 57 | 58 | // Remove outer quotes that might be present 59 | if ((normalizedPath.startsWith('"') && normalizedPath.endsWith('"')) || 60 | (normalizedPath.startsWith("'") && normalizedPath.endsWith("'"))) { 61 | normalizedPath = normalizedPath.substring(1, normalizedPath.length - 1); 62 | } 63 | 64 | // Handle duplicated paths (Windows context menu issue) 65 | const match = normalizedPath.match(/^([A-Z]:\\.*?)\\?"[A-Z]:\\/i); 66 | if (match) { 67 | // The file path was duplicated, extract the part after the quotes 68 | const pathAfterQuotes = normalizedPath.match(/"([A-Z]:\\.*?)"/i); 69 | if (pathAfterQuotes && pathAfterQuotes[1]) { 70 | normalizedPath = pathAfterQuotes[1]; 71 | } 72 | } 73 | 74 | // Resolve to absolute path if relative 75 | if (!path.isAbsolute(normalizedPath)) { 76 | normalizedPath = path.resolve(process.cwd(), normalizedPath); 77 | } 78 | 79 | console.log(`File to share: ${normalizedPath}`); 80 | 81 | // Check if file exists before proceeding 82 | if (!fs.existsSync(normalizedPath)) { 83 | console.error(chalk.red(`Error: File not found: ${normalizedPath}`)); 84 | process.exit(1); 85 | } 86 | 87 | // Handle password options 88 | const passwordProtect = options.password || options.secure || false; 89 | const password = options.password || null; 90 | const debug = options.debug || false; 91 | const forceTunnel = options.tunnel || false; 92 | 93 | startFileServer(normalizedPath, { 94 | passwordProtect, 95 | password, 96 | debug, 97 | forceTunnel 98 | }); 99 | } catch (error) { 100 | console.error(chalk.red(`Error: ${error.message}`)); 101 | process.exit(1); 102 | } 103 | }); 104 | 105 | program 106 | .command('receive ') 107 | .description("Connect to a file sharing server and receive a file") 108 | .option('-p, --password ', 'Password for protected file') 109 | .option('-d, --debug', 'Enable debug mode with detailed logging') 110 | .action((serverIp, serverPort, fileName, options) => { 111 | receiveFile(serverIp, serverPort, fileName, options.password, options.debug); 112 | }); 113 | 114 | // Add new commands for shell integration 115 | program 116 | .command('integrate') 117 | .description("Add CPD to your system's right-click menu") 118 | .action(() => { 119 | installShellIntegration(); 120 | }); 121 | 122 | program 123 | .command('unintegrate') 124 | .description("Remove CPD from your system's right-click menu") 125 | .action(() => { 126 | uninstallShellIntegration(); 127 | }); 128 | 129 | // Add new command for easier ngrok URL handling 130 | program 131 | .command('get ') 132 | .description("Receive a file from a global sharing URL") 133 | .option('-p, --password ', 'Password for protected file') 134 | .action((url, fileName, options) => { 135 | // Extract hostname from URL and use default WebSocket port 136 | try { 137 | const parsedUrl = new URL(url); 138 | receiveFile(url, 80, fileName, options.password); 139 | } catch (e) { 140 | console.error('Invalid URL format. Please provide a valid http:// or https:// URL'); 141 | process.exit(1); 142 | } 143 | }); 144 | 145 | // Add tunnel management commands 146 | program 147 | .command('tunnels') 148 | .description("List active tunnels and connection status") 149 | .action(async () => { 150 | const spinner = ora('Checking active tunnels...').start(); 151 | 152 | try { 153 | const tunnelInfo = await listActiveTunnels(); 154 | spinner.stop(); 155 | 156 | console.log(chalk.cyan('\n📡 TUNNEL STATUS')); 157 | 158 | if (tunnelInfo.active) { 159 | console.log(chalk.green('✓ Ngrok service is active')); 160 | if (tunnelInfo.ngrokVersion) { 161 | console.log(chalk.gray(` Version: ${tunnelInfo.ngrokVersion}`)); 162 | } 163 | } else { 164 | console.log(chalk.yellow('⚠️ Could not connect to ngrok service')); 165 | if (tunnelInfo.error) { 166 | console.log(chalk.gray(` Error: ${tunnelInfo.error}`)); 167 | } 168 | } 169 | 170 | console.log(chalk.cyan('\nActive Tunnels:')); 171 | 172 | if (tunnelInfo.fromRegistry.length > 0) { 173 | console.log(chalk.yellow(`${tunnelInfo.fromRegistry.length} tunnels in local registry:`)); 174 | tunnelInfo.fromRegistry.forEach((url, i) => { 175 | console.log(` ${i+1}. ${url}`); 176 | }); 177 | } else { 178 | console.log(chalk.gray(' No active tunnels in registry')); 179 | } 180 | 181 | console.log(chalk.cyan('\nTo close all tunnels: ') + chalk.yellow('cpd tunnels-close')); 182 | } catch (error) { 183 | spinner.fail(`Error checking tunnels: ${error.message}`); 184 | } 185 | }); 186 | 187 | program 188 | .command('tunnels-close') 189 | .description("Close all active tunnels") 190 | .action(async () => { 191 | const spinner = ora('Closing all active tunnels...').start(); 192 | 193 | try { 194 | const result = await closeAllTunnels(); 195 | 196 | if (result.closedFromNgrok > 0 || result.closedFromRegistry > 0) { 197 | spinner.succeed(`Closed ${result.closedFromNgrok + result.closedFromRegistry} tunnels successfully`); 198 | } else if (result.errors.length > 0) { 199 | spinner.warn('Attempted to close tunnels with some errors'); 200 | result.errors.forEach(err => console.log(chalk.yellow(` - ${err}`))); 201 | } else { 202 | spinner.info('No active tunnels found to close'); 203 | } 204 | } catch (error) { 205 | spinner.fail(`Error closing tunnels: ${error.message}`); 206 | } 207 | }); 208 | 209 | // Add specific ngrok diagnostics command 210 | program 211 | .command('ngrok-diagnose') 212 | .description("Diagnose ngrok issues and show a detailed report") 213 | .action(async () => { 214 | const spinner = ora('Running ngrok diagnostics...').start(); 215 | try { 216 | const results = await diagnoseNgrok(); 217 | spinner.stop(); 218 | displayNgrokReport(results); 219 | } catch (error) { 220 | spinner.fail(`Diagnostics failed: ${error.message}`); 221 | } 222 | }); 223 | 224 | program 225 | .command('ngrok-fix') 226 | .description("Attempt to fix common ngrok issues") 227 | .action(async () => { 228 | try { 229 | await fixNgrokIssues(); 230 | } catch (error) { 231 | console.error(chalk.red(`Error: ${error.message}`)); 232 | } 233 | }); 234 | 235 | // Add new command for web UI sharing from context menu 236 | program 237 | .command('share-ui ') 238 | .description("Start a file sharing server with web UI") 239 | .action(async (filepath) => { 240 | try { 241 | // Get available port for the status UI 242 | const getPort = (await import('get-port')).default; 243 | const port = await getPort(); 244 | 245 | // Normalize filepath 246 | let normalizedPath = filepath; 247 | 248 | // Remove quotes if present 249 | if ((normalizedPath.startsWith('"') && normalizedPath.endsWith('"')) || 250 | (normalizedPath.startsWith("'") && normalizedPath.endsWith("'"))) { 251 | normalizedPath = normalizedPath.substring(1, normalizedPath.length - 1); 252 | } 253 | 254 | // Handle duplicated paths (Windows context menu issue) 255 | const match = normalizedPath.match(/^([A-Z]:\\.*?)\\?"[A-Z]:\\/i); 256 | if (match) { 257 | const pathAfterQuotes = normalizedPath.match(/"([A-Z]:\\.*?)"/i); 258 | if (pathAfterQuotes && pathAfterQuotes[1]) { 259 | normalizedPath = pathAfterQuotes[1]; 260 | } 261 | } 262 | 263 | // Resolve to absolute path if relative 264 | if (!path.isAbsolute(normalizedPath)) { 265 | normalizedPath = path.resolve(process.cwd(), normalizedPath); 266 | } 267 | 268 | // Check if file exists 269 | if (!fs.existsSync(normalizedPath)) { 270 | console.error(chalk.red(`Error: File not found: ${normalizedPath}`)); 271 | process.exit(1); 272 | } 273 | 274 | // Check if path is a directory - prevent directory sharing 275 | const stats = fs.statSync(normalizedPath); 276 | if (stats.isDirectory()) { 277 | console.error(chalk.red(`Error: ${normalizedPath} is a directory. FileZap only supports sharing individual files.`)); 278 | console.error(chalk.yellow(`Tip: You can compress the directory into a zip file first and then share it.`)); 279 | process.exit(1); 280 | } 281 | 282 | // Start the server in UI mode - hide terminal output but show browser 283 | const serverOptions = { 284 | webOnly: true, // Hide terminal output 285 | openBrowser: true, // But show browser 286 | httpPort: port 287 | }; 288 | 289 | // Start sharing 290 | startFileServer(normalizedPath, serverOptions); 291 | } catch (error) { 292 | console.error(chalk.red(`Error: ${error.message}`)); 293 | process.exit(1); 294 | } 295 | }); 296 | 297 | program.parse(process.argv); 298 | 299 | // Handle command execution 300 | if (!process.argv.slice(2).length) { 301 | const commandName = path.basename(process.argv[1]).replace(/\.js$/, ''); 302 | 303 | // Show system info on empty command 304 | console.log(`${commandName} - File sharing tool`); 305 | console.log(`Running on: ${os.type()} (${os.platform()}) ${os.release()}`); 306 | console.log(`Hostname: ${os.hostname()}`); 307 | console.log(`Username: ${os.userInfo().username}`); 308 | console.log(`Network interfaces: ${Object.keys(os.networkInterfaces()).join(', ')}`); 309 | console.log(`\nType '${commandName} --help' for available commands`); 310 | console.log(chalk.cyan(`\nKeyboard shortcuts:`)); 311 | console.log(chalk.yellow(` Ctrl+S: Quick share (prompts for file)`)); 312 | console.log(chalk.yellow(` Ctrl+C: Exit program`)); 313 | } -------------------------------------------------------------------------------- /utils/tunnelProviders.js: -------------------------------------------------------------------------------- 1 | import { exec, spawn } from 'child_process'; 2 | import { promisify } from 'util'; 3 | import fs from 'fs-extra'; 4 | import path from 'path'; 5 | import os from 'os'; 6 | import { logDebug } from './logger.js'; 7 | import axios from 'axios'; 8 | 9 | const execAsync = promisify(exec); 10 | const TUNNEL_DIR = path.join(os.homedir(), '.filezap', 'tunnels'); 11 | const TUNNEL_INFO_PATH = path.join(TUNNEL_DIR, 'active_tunnels.json'); 12 | 13 | /** 14 | * Initialize the tunnel manager 15 | */ 16 | export function initTunnelManager() { 17 | try { 18 | fs.ensureDirSync(TUNNEL_DIR); 19 | if (!fs.existsSync(TUNNEL_INFO_PATH)) { 20 | saveTunnelInfo([]); 21 | } 22 | } catch (error) { 23 | logDebug(`Error initializing tunnel manager: ${error.message}`); 24 | } 25 | } 26 | 27 | /** 28 | * Save tunnel information to disk 29 | * @param {Array} tunnels - List of active tunnel URLs 30 | */ 31 | function saveTunnelInfo(tunnels) { 32 | try { 33 | fs.writeJSONSync(TUNNEL_INFO_PATH, { 34 | tunnels, 35 | lastUpdated: new Date().toISOString() 36 | }, { spaces: 2 }); 37 | } catch (error) { 38 | logDebug(`Error saving tunnel info: ${error.message}`); 39 | } 40 | } 41 | 42 | /** 43 | * Load tunnel information from disk 44 | * @returns {Array} List of active tunnel URLs 45 | */ 46 | function loadTunnelInfo() { 47 | try { 48 | if (fs.existsSync(TUNNEL_INFO_PATH)) { 49 | const data = fs.readJSONSync(TUNNEL_INFO_PATH); 50 | return data.tunnels || []; 51 | } 52 | } catch (error) { 53 | logDebug(`Error loading tunnel info: ${error.message}`); 54 | } 55 | return []; 56 | } 57 | 58 | /** 59 | * Class to manage SSH-based tunnels with Serveo 60 | */ 61 | class ServeoTunnelManager { 62 | constructor() { 63 | this.activeTunnels = new Map(); 64 | this.sshProcesses = new Map(); 65 | } 66 | 67 | /** 68 | * Create a tunnel using Serveo 69 | * @param {number} port - Port to tunnel 70 | * @returns {Promise} - Tunnel details 71 | */ 72 | async createTunnel(port) { 73 | logDebug(`Creating Serveo tunnel for port ${port}`); 74 | 75 | try { 76 | // Create a unique subdomain for this tunnel 77 | const subdomain = `cpd-${Date.now().toString(36).substring(2, 8)}`; 78 | logDebug(`Using subdomain: ${subdomain}`); 79 | 80 | return await this.createServeoTunnel(port, subdomain); 81 | } catch (error) { 82 | logDebug(`Serveo tunnel failed: ${error.message}`); 83 | 84 | // If first attempt fails, try without a subdomain 85 | try { 86 | logDebug('Retrying without specific subdomain...'); 87 | return await this.createServeoTunnel(port); 88 | } catch (retryError) { 89 | logDebug(`Serveo retry failed: ${retryError.message}`); 90 | return { 91 | success: false, 92 | error: retryError.message 93 | }; 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Create a Serveo tunnel with specific options 100 | * @param {number} port - Port to tunnel 101 | * @param {string} subdomain - Optional subdomain 102 | * @returns {Promise} - Tunnel details 103 | */ 104 | createServeoTunnel(port, subdomain = null) { 105 | return new Promise((resolve, reject) => { 106 | try { 107 | // Prepare SSH args 108 | const args = ['-oStrictHostKeyChecking=accept-new']; 109 | 110 | // Add forwarding options 111 | if (subdomain) { 112 | args.push(`-R${subdomain}:80:localhost:${port}`); 113 | } else { 114 | args.push(`-R80:localhost:${port}`); 115 | } 116 | 117 | // Add server address 118 | args.push('serveo.net'); 119 | 120 | logDebug(`Running SSH with args: ${args.join(' ')}`); 121 | 122 | // Spawn SSH process 123 | const sshProcess = spawn('ssh', args, { 124 | stdio: ['ignore', 'pipe', 'pipe'] 125 | }); 126 | 127 | let stdoutData = ''; 128 | let stderrData = ''; 129 | let resolved = false; 130 | 131 | // Set timeout for tunnel creation 132 | const timeout = setTimeout(() => { 133 | if (!resolved) { 134 | logDebug('Serveo tunnel creation timed out'); 135 | sshProcess.kill(); 136 | reject(new Error('Serveo tunnel creation timed out after 20 seconds')); 137 | } 138 | }, 20000); 139 | 140 | // Listen for stdout to extract tunnel URL 141 | sshProcess.stdout.on('data', (data) => { 142 | const output = data.toString(); 143 | stdoutData += output; 144 | logDebug(`SSH stdout: ${output}`); 145 | 146 | // Extract URL from serveo output - use more precise regex 147 | const urlMatch = output.match(/https?:\/\/[a-zA-Z0-9]+\.serveo\.net/); 148 | if (urlMatch && urlMatch[0] && !resolved) { 149 | const url = urlMatch[0]; 150 | logDebug(`Found URL in stdout: ${url}`); 151 | 152 | clearTimeout(timeout); 153 | resolved = true; 154 | 155 | // Save process for later cleanup 156 | this.sshProcesses.set(url, sshProcess); 157 | 158 | // Register tunnel 159 | this.activeTunnels.set(url, { 160 | provider: 'serveo', 161 | port, 162 | created: new Date().toISOString() 163 | }); 164 | 165 | // Save to registry 166 | const tunnels = loadTunnelInfo(); 167 | if (!tunnels.includes(url)) { 168 | tunnels.push(url); 169 | saveTunnelInfo(tunnels); 170 | } 171 | 172 | // Create shortened URL 173 | this.shortenUrl(url).then(shortenedUrl => { 174 | resolve({ 175 | url, 176 | shortenedUrl, 177 | provider: 'serveo', 178 | success: true 179 | }); 180 | }).catch(() => { 181 | // If URL shortening fails, just use the original URL 182 | resolve({ 183 | url, 184 | shortenedUrl: url, 185 | provider: 'serveo', 186 | success: true 187 | }); 188 | }); 189 | } 190 | }); 191 | 192 | // Listen for stderr to extract URL (serveo sometimes outputs to stderr) 193 | sshProcess.stderr.on('data', (data) => { 194 | const output = data.toString(); 195 | stderrData += output; 196 | logDebug(`SSH stderr: ${output}`); 197 | 198 | // Extract URL from stderr output with improved pattern 199 | const urlMatch = output.match(/https?:\/\/[a-zA-Z0-9]+\.serveo\.net/); 200 | if (urlMatch && urlMatch[0] && !resolved) { 201 | const url = urlMatch[0]; 202 | logDebug(`Found URL in stderr: ${url}`); 203 | 204 | clearTimeout(timeout); 205 | resolved = true; 206 | 207 | // Save process for later cleanup 208 | this.sshProcesses.set(url, sshProcess); 209 | 210 | // Register tunnel 211 | this.activeTunnels.set(url, { 212 | provider: 'serveo', 213 | port, 214 | created: new Date().toISOString() 215 | }); 216 | 217 | // Save to registry 218 | const tunnels = loadTunnelInfo(); 219 | if (!tunnels.includes(url)) { 220 | tunnels.push(url); 221 | saveTunnelInfo(tunnels); 222 | } 223 | 224 | // Create shortened URL 225 | this.shortenUrl(url).then(shortenedUrl => { 226 | resolve({ 227 | url, 228 | shortenedUrl, 229 | provider: 'serveo', 230 | success: true 231 | }); 232 | }).catch(() => { 233 | // If URL shortening fails, just use the original URL 234 | resolve({ 235 | url, 236 | shortenedUrl: url, 237 | provider: 'serveo', 238 | success: true 239 | }); 240 | }); 241 | } 242 | }); 243 | 244 | // Handle process exit 245 | sshProcess.on('close', (code) => { 246 | if (!resolved) { 247 | clearTimeout(timeout); 248 | reject(new Error(`SSH process exited with code ${code}, stdout: ${stdoutData}, stderr: ${stderrData}`)); 249 | } 250 | }); 251 | 252 | // Handle process error 253 | sshProcess.on('error', (error) => { 254 | if (!resolved) { 255 | clearTimeout(timeout); 256 | reject(new Error(`SSH process error: ${error.message}`)); 257 | } 258 | }); 259 | } catch (error) { 260 | reject(error); 261 | } 262 | }); 263 | } 264 | 265 | /** 266 | * Shorten URL using TinyURL 267 | * @param {string} url - URL to shorten 268 | * @returns {Promise} - Shortened URL 269 | */ 270 | async shortenUrl(url) { 271 | try { 272 | const response = await axios.get(`https://tinyurl.com/api-create.php?url=${encodeURIComponent(url)}`); 273 | return response.data; 274 | } catch (error) { 275 | logDebug(`URL shortening failed: ${error.message}`); 276 | return url; 277 | } 278 | } 279 | 280 | /** 281 | * Close a specific tunnel 282 | * @param {string} url - The tunnel URL to close 283 | * @returns {Promise} - Success status 284 | */ 285 | async closeTunnel(url) { 286 | logDebug(`Closing tunnel: ${url}`); 287 | 288 | try { 289 | // Kill the SSH process if we have it 290 | if (this.sshProcesses.has(url)) { 291 | const process = this.sshProcesses.get(url); 292 | process.kill(); 293 | this.sshProcesses.delete(url); 294 | logDebug(`Killed SSH process for ${url}`); 295 | } 296 | 297 | // Remove from active tunnels 298 | this.activeTunnels.delete(url); 299 | 300 | // Remove from registry 301 | const tunnels = loadTunnelInfo(); 302 | const updatedTunnels = tunnels.filter(t => t !== url); 303 | saveTunnelInfo(updatedTunnels); 304 | 305 | return true; 306 | } catch (error) { 307 | logDebug(`Error closing tunnel: ${error.message}`); 308 | return false; 309 | } 310 | } 311 | 312 | /** 313 | * Close all tunnels 314 | * @returns {Promise} - Results of the operation 315 | */ 316 | async closeAllTunnels() { 317 | logDebug('Closing all tunnels'); 318 | 319 | const results = { 320 | closed: 0, 321 | failed: 0, 322 | errors: [] 323 | }; 324 | 325 | // Close each active tunnel 326 | for (const [url] of this.activeTunnels.entries()) { 327 | try { 328 | const success = await this.closeTunnel(url); 329 | if (success) { 330 | results.closed++; 331 | } else { 332 | results.failed++; 333 | } 334 | } catch (error) { 335 | results.failed++; 336 | results.errors.push(`Error closing ${url}: ${error.message}`); 337 | } 338 | } 339 | 340 | // Kill any remaining SSH processes to serveo.net 341 | try { 342 | if (os.platform() === 'win32') { 343 | await execAsync('taskkill /F /IM ssh.exe /FI "WINDOWTITLE eq *serveo.net*"', { timeout: 5000 }); 344 | } else { 345 | await execAsync('pkill -f "ssh.*serveo.net"', { timeout: 5000 }); 346 | } 347 | } catch (error) { 348 | // Ignore errors - likely means no processes were found 349 | } 350 | 351 | // Clear registry 352 | saveTunnelInfo([]); 353 | 354 | return results; 355 | } 356 | } 357 | 358 | // Export a singleton instance 359 | export const tunnelManager = new ServeoTunnelManager(); 360 | 361 | // Export utility functions for CLI tools 362 | export async function listActiveTunnels() { 363 | return { 364 | active: true, 365 | fromRegistry: loadTunnelInfo() 366 | }; 367 | } 368 | 369 | export async function closeAllTunnels() { 370 | const result = await tunnelManager.closeAllTunnels(); 371 | return { 372 | closedFromRegistry: result.closed, 373 | errors: result.errors 374 | }; 375 | } 376 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # FileZap 2 | 3 | 4 | ``` 5 | ______ _ _ _____ 6 | | ____(_) | |___ | 7 | | |__ _| | ___ / / __ _ _ __ 8 | | __| | | |/ _ \ / / / _` | '_ \ 9 | | | | | | __// /__| (_| | |_) | 10 | |_| |_|_|\___/_____|__,_| .__/ 11 | | | 12 | |_| 13 | ``` 14 | 15 |

16 | Blazingly Fast Cross-Platform File Sharing Tool 17 |

18 | 19 |

20 | Installation • 21 | Quick Start • 22 | Features • 23 | Architecture • 24 | Troubleshooting 25 |

26 | 27 | --- 28 | 29 | ## Overview 30 | 31 | FileZap is a lightning-fast, secure file sharing tool designed to simplify sharing files across devices and networks. Whether you're transferring files between machines on a local network or sharing with colleagues across the globe, FileZap provides an intuitive CLI and context-menu integration for seamless transfers. 32 | 33 | ## Features 34 | 35 | - **🚀 Blazingly Fast**: Direct WebSocket transfers at maximum network speeds 36 | - **🔒 Secure Sharing**: Optional password protection for sensitive files 37 | - **🌐 Global Access**: Automatic tunnel creation for sharing beyond local networks 38 | - **📱 Mobile Friendly**: QR codes for easy mobile access 39 | - **🖥️ Context Menu Integration**: Right-click to share from Windows Explorer, macOS Finder, or Linux file managers 40 | - **📋 Command-line Interface**: Powerful CLI with intuitive commands 41 | - **🔄 No Registration Required**: Instant sharing without accounts or sign-ups 42 | - **📊 Status Dashboard**: Real-time transfer monitoring via web interface 43 | 44 | --- 45 | 46 | ## Installation 47 | 48 | FileZap can be installed on Windows, macOS, and Linux platforms using multiple methods. 49 | 50 | ### Prerequisites 51 | 52 | - Node.js v14.0.0 or later 53 | - npm v6.0.0 or later 54 | 55 | ### Method 1: Global NPM Installation 56 | 57 | The recommended method for most users is to install FileZap globally via npm: 58 | 59 | ```bash 60 | npm install -g filezap 61 | ``` 62 | 63 | Verify installation worked by running: 64 | 65 | ```bash 66 | filezap --version 67 | ``` 68 | 69 | ### Method 2: Local Installation 70 | 71 | If you prefer to install FileZap as a project dependency: 72 | 73 | ```bash 74 | # Create a new directory 75 | mkdir my-filezap 76 | cd my-filezap 77 | 78 | # Initialize npm project 79 | npm init -y 80 | 81 | # Install FileZap locally 82 | npm install filezap 83 | 84 | # Run using npx 85 | npx filezap 86 | ``` 87 | 88 | ### Method 3: Manual Installation from Source 89 | 90 | For developers or users who want the latest features: 91 | 92 | ```bash 93 | # Clone the repository 94 | git clone https://github.com/yourname/filezap.git 95 | cd filezap 96 | 97 | # Install dependencies 98 | npm install 99 | 100 | # Link the package globally 101 | npm link 102 | 103 | # Now you can use FileZap from anywhere 104 | filezap --version 105 | ``` 106 | 107 | ### Platform Specific Setup 108 | 109 | #### Windows Setup 110 | 111 | 1. Launch command prompt or PowerShell as Administrator 112 | 2. Install FileZap globally: 113 | ```powershell 114 | npm install -g filezap 115 | ``` 116 | 3. Enable context menu integration: 117 | ```powershell 118 | filezap integrate 119 | ``` 120 | 4. If you encounter any permission errors, ensure you're running as Administrator 121 | 122 | #### macOS Setup 123 | 124 | 1. Open Terminal 125 | 2. Install FileZap globally: 126 | ```bash 127 | npm install -g filezap 128 | ``` 129 | 3. You may need to provide additional permissions: 130 | ```bash 131 | sudo chmod -R 755 $(npm root -g)/filezap 132 | ``` 133 | 4. Enable Finder integration: 134 | ```bash 135 | filezap integrate 136 | ``` 137 | 5. For macOS Catalina and later, you may need to grant full disk access to Terminal 138 | 139 | #### Linux Setup 140 | 141 | 1. Open your terminal 142 | 2. Install FileZap globally: 143 | ```bash 144 | npm install -g filezap 145 | ``` 146 | 3. Enable file manager integration: 147 | ```bash 148 | filezap integrate 149 | ``` 150 | 4. For Debian/Ubuntu systems, you may need: 151 | ```bash 152 | sudo apt-get install xdg-utils 153 | ``` 154 | 155 | ### Setting Up Global Sharing (Optional) 156 | 157 | For sharing outside your local network, FileZap uses tunneling services: 158 | 159 | 1. Check tunnel status: 160 | ```bash 161 | filezap tunnels 162 | ``` 163 | 2. If you encounter issues: 164 | ```bash 165 | filezap ngrok-diagnose 166 | filezap ngrok-fix 167 | ``` 168 | 169 | ### Verifying Installation 170 | 171 | After installation, run a simple diagnostic check: 172 | 173 | ```bash 174 | filezap 175 | ``` 176 | 177 | You should see information about your system and available commands. 178 | 179 | --- 180 | 181 | ## Quick Start 182 | 183 | ### Sharing a File 184 | 185 | **Method 1: Command Line** 186 | ```bash 187 | filezap send /path/to/yourfile.pdf 188 | ``` 189 | 190 | **Method 2: Context Menu** 191 | Right-click any file → Select "Share via FileZap" 192 | 193 | **Method 3: Keyboard Shortcut** 194 | In terminal, press `Ctrl+S` or `Alt+S` and select a file 195 | 196 | ### Receiving a File 197 | 198 | **Method 1: Use the provided URL** 199 | Open the URL provided by the sender in your browser 200 | 201 | **Method 2: Command Line (Local Network)** 202 | ```bash 203 | filezap receive 192.168.1.100 55555 filename.pdf 204 | ``` 205 | 206 | **Method 3: Command Line (Global URL)** 207 | ```bash 208 | filezap get https://shortened-url.com filename.pdf 209 | ``` 210 | 211 | --- 212 | 213 | ## Architecture 214 | 215 | FileZap employs a sophisticated architecture designed for speed, security, and ease of use. This section details the technical underpinnings of the application. 216 | 217 | ### System Architecture Overview 218 | 219 | FileZap operates on a hybrid peer-to-peer model with the following components: 220 | 221 | ``` 222 | ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 223 | │ │ │ │ │ │ 224 | │ Sender Device │◄────┤ Tunnel Server │────►│ Receiver Device │ 225 | │ │ │ (Optional) │ │ │ 226 | └────────┬────────┘ └─────────────────┘ └────────┬────────┘ 227 | │ │ 228 | │ │ 229 | ▼ ▼ 230 | ┌─────────────────┐ ┌─────────────────┐ 231 | │ Local Server │ │ Web Browser/ │ 232 | │ (WebSockets) │◄───────Direct Connection───►│ CLI Client │ 233 | └─────────────────┘ └─────────────────┘ 234 | ``` 235 | 236 | ### Key Components 237 | 238 | 1. **Command-Line Interface (CLI)** 239 | - Built with Commander.js 240 | - Manages user interactions and command processing 241 | - Implements keyboard shortcuts through readline module 242 | 243 | 2. **File Server** 244 | - Implemented using native Node.js HTTP server and WebSocket server 245 | - Serves both web interface and handles WebSocket connections 246 | - Processes file transfers with binary WebSocket messages 247 | 248 | 3. **Tunnel Management** 249 | - Integrates with tunneling services (ngrok/Serveo) 250 | - Handles URL shortening via TinyURL API 251 | - Manages tunnel lifecycle and error recovery 252 | 253 | 4. **Shell Integration** 254 | - Platform-specific implementations for Windows/macOS/Linux 255 | - Windows: Registry modifications for context menu 256 | - macOS: Automator workflow creation 257 | - Linux: Nautilus scripts and desktop entries 258 | 259 | 5. **Web Interface** 260 | - Responsive HTML/CSS design 261 | - QR code generation for mobile access 262 | - Real-time status updates via polling 263 | 264 | ### Data Flow 265 | 266 | 1. **Initialization Phase** 267 | - Application starts, detects platform, and configures environment 268 | - Command parser interprets user intent 269 | - Server component initializes on available ports 270 | 271 | 2. **Connection Phase** 272 | - For local sharing: WebSocket server starts on random available port 273 | - For global sharing: Tunnel established to local port 274 | - Authentication layer activated if password protection enabled 275 | 276 | 3. **Transfer Phase** 277 | - Binary data transmitted via WebSocket protocol 278 | - Progress monitoring and speed calculation 279 | - Integrity verification upon completion 280 | 281 | 4. **Termination Phase** 282 | - Resources cleaned up (tunnels closed, ports released) 283 | - Transfer statistics logged 284 | - Success/failure notification delivered 285 | 286 | ### Security Model 287 | 288 | FileZap implements several security measures: 289 | 290 | 1. **Password Protection** 291 | - Optional per-file password protection 292 | - Password never stored in logs or transmitted in clear text 293 | - Auto-generation of strong passwords when using secure mode 294 | 295 | 2. **Temporary Links** 296 | - All sharing sessions have a default 30-minute expiration 297 | - One-time tunnels created per session 298 | - Server automatically shuts down after timeout 299 | 300 | 3. **Local Storage** 301 | - Downloaded files stored in user-specific directories 302 | - Path traversal protection implemented 303 | - Automatic file renaming to prevent overwrites 304 | 305 | ### Network Architecture 306 | 307 | FileZap optimizes network connectivity through: 308 | 309 | 1. **IP Address Prioritization** 310 | - Smart ranking of network interfaces to determine primary IP 311 | - Deprioritization of virtual adapters and Docker interfaces 312 | - Fallback mechanisms when preferred interfaces unavailable 313 | 314 | 2. **Port Management** 315 | - Dynamic port allocation to avoid conflicts 316 | - Separate ports for WebSocket and HTTP servers 317 | - Port availability checking before server initialization 318 | 319 | 3. **Browser Integration** 320 | - Platform-specific browser launching techniques 321 | - Multiple fallback mechanisms for browser opening 322 | - Multiple connection methods (direct URL, QR code, command line) 323 | 324 | ### File System Integration 325 | 326 | The application interacts with the host file system through: 327 | 328 | 1. **Path Normalization** 329 | - Cross-platform path handling 330 | - Support for spaces, special characters, and Unicode in filenames 331 | - Handling of Windows-specific path issues (quotes, UNC paths) 332 | 333 | 2. **File Discovery** 334 | - File dialog integration for interactive selection 335 | - Drag-and-drop support via command-line argument processing 336 | - Context menu integration for right-click sharing 337 | 338 | 3. **File Storage** 339 | - Configurable download location (defaults to ~/.filezap/shared/username) 340 | - Automatic directory creation if not present 341 | - Collision detection and unique filename generation 342 | 343 | ### Troubleshooting & Diagnostics 344 | 345 | FileZap includes comprehensive diagnostic capabilities: 346 | 347 | 1. **Logging System** 348 | - Debug logs with verbose option 349 | - Log rotation and management 350 | - Error categorization and reporting 351 | 352 | 2. **Tunnel Diagnostics** 353 | - Tunnel connection testing 354 | - Auto-repair for common tunnel issues 355 | - Alternative tunnel providers when primary fails 356 | 357 | 3. **Network Diagnostics** 358 | - IP detection verification 359 | - Connectivity testing 360 | - Port availability checking 361 | 362 | --- 363 | 364 | ## Troubleshooting 365 | 366 | ### Common Issues 367 | 368 | **Issue: Browser doesn't open automatically** 369 | - **Solution**: Copy the URL shown in the terminal and paste it manually into your browser 370 | - **Fix**: Ensure your system allows applications to open browsers 371 | 372 | **Issue: "Failed to install shell integration"** 373 | - **Solution**: Run the application with administrator/root privileges 374 | - **Fix**: `sudo filezap integrate` on macOS/Linux or run as Administrator on Windows 375 | 376 | **Issue: "Cannot establish tunnel"** 377 | - **Solution**: Check your internet connection and firewall settings 378 | - **Diagnosis**: Run `filezap ngrok-diagnose` to identify issues 379 | - **Fix**: Run `filezap ngrok-fix` to attempt automatic repair 380 | 381 | **Issue: "File not found" when the file exists** 382 | - **Solution**: Use simple paths without special characters 383 | - **Fix**: Move file to a directory with a simpler path and try again 384 | 385 | ### Diagnostics 386 | 387 | For more detailed diagnostics: 388 | 389 | ```bash 390 | # Check tunnel status 391 | filezap tunnels 392 | 393 | # Run ngrok diagnostics 394 | filezap ngrok-diagnose 395 | 396 | # Fix common issues 397 | filezap ngrok-fix 398 | 399 | # See active connections 400 | filezap list 401 | ``` 402 | 403 | ## Project Structure 404 | 405 | If contributing to FileZap, you should be familiar with the project structure: 406 | 407 | ``` 408 | filezap/ 409 | ├── bin/ # Executable entry points 410 | │ ├── cpd.js # Legacy entry point 411 | │ └── filezap.js # Main entry point with keyboard shortcuts 412 | ├── commander/ # Command modules 413 | │ ├── commands.js # Command definitions 414 | │ └── utils/ # Command utilities 415 | ├── src/ # Core functionality 416 | │ ├── client.js # File receiver 417 | │ └── server.js # File sharing server 418 | ├── utils/ # Shared utilities 419 | │ ├── logger.js # Logging functionality 420 | │ ├── ngrokDiagnostics.js # Tunnel diagnostics 421 | │ └── tunnelProviders.js # Tunnel management 422 | └── README.md # This documentation 423 | ``` 424 | 425 | --- 426 | 427 | ## Contributing 428 | 429 | Contributions are welcome! To contribute: 430 | 431 | 1. Fork the repository 432 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 433 | 3. Make your changes 434 | 4. Commit your changes (`git commit -m 'Add some amazing feature'`) 435 | 5. Push to the branch (`git push origin feature/amazing-feature`) 436 | 6. Open a Pull Request 437 | 438 | ## License 439 | 440 | [MIT License](LICENSE) 441 | 442 | --- 443 | 444 |

445 | Made with ❤️ by the FileZap Team 446 |

-------------------------------------------------------------------------------- /commander/utils/shellIntegration.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import child_process from 'child_process'; 5 | import { execSync } from 'child_process'; 6 | import ora from 'ora'; 7 | import chalk from 'chalk'; 8 | 9 | // Improved path to the installed FileZap binary 10 | const getExecutablePath = () => { 11 | // On Windows, use the installed location or the current file location 12 | if (os.platform() === 'win32') { 13 | try { 14 | // First check if we need to ensure Node.js runs the command 15 | const isJsFile = true; // Assume it's a JS file by default 16 | 17 | // First check global npm installs 18 | const npmGlobal = execSync('npm root -g').toString().trim(); 19 | const possiblePath = path.join(npmGlobal, '..', 'filezap.cmd'); 20 | if (fs.existsSync(possiblePath)) { 21 | return { path: possiblePath, isJsFile: false }; // .cmd file is executable directly 22 | } 23 | 24 | // Also check for cpd.cmd 25 | const cpdPath = path.join(npmGlobal, '..', 'cpd.cmd'); 26 | if (fs.existsSync(cpdPath)) { 27 | return { path: cpdPath, isJsFile: false }; // .cmd file is executable directly 28 | } 29 | 30 | // Check for node_modules filezap.js 31 | const jsPath = path.join(npmGlobal, 'filezap', 'bin', 'filezap.js'); 32 | if (fs.existsSync(jsPath)) { 33 | return { path: jsPath, isJsFile: true }; // .js file needs node 34 | } 35 | } catch (e) { 36 | // Ignore error if npm not found 37 | } 38 | 39 | // Fallback to current directory executable 40 | const currentDir = path.resolve(process.cwd()); 41 | return { path: path.join(currentDir, 'bin', 'filezap.js'), isJsFile: true }; 42 | } 43 | 44 | // On macOS, be more specific about where to find the executable 45 | if (os.platform() === 'darwin') { 46 | try { 47 | // Check for global npm installs 48 | const npmGlobal = execSync('npm root -g').toString().trim(); 49 | const possiblePath = path.join(npmGlobal, '..', 'bin', 'filezap'); 50 | if (fs.existsSync(possiblePath)) { 51 | return possiblePath; 52 | } 53 | } catch (e) { 54 | // Ignore error if npm not found 55 | } 56 | 57 | // Try checking in standard paths 58 | const standardPaths = [ 59 | '/usr/local/bin/filezap', 60 | '/usr/bin/filezap', 61 | path.join(os.homedir(), '.npm-global', 'bin', 'filezap') 62 | ]; 63 | 64 | for (const stdPath of standardPaths) { 65 | if (fs.existsSync(stdPath)) { 66 | return stdPath; 67 | } 68 | } 69 | 70 | // Last resort: use which command 71 | try { 72 | const whichPath = execSync('which filezap').toString().trim(); 73 | if (whichPath) { 74 | return whichPath; 75 | } 76 | } catch (e) { 77 | // Command not found 78 | } 79 | } 80 | 81 | // On Linux or as a fallback, we expect it to be in the PATH 82 | return 'filezap'; 83 | }; 84 | 85 | // Windows implementation using registry 86 | const windowsIntegration = { 87 | install: async () => { 88 | const spinner = ora('Adding Windows Explorer context menu integration...').start(); 89 | 90 | try { 91 | // Get the path and whether it's a JS file 92 | const { path: filezapPath, isJsFile } = getExecutablePath(); 93 | 94 | spinner.text = `Using FileZap at: ${filezapPath}`; 95 | 96 | // Find Node.js executable path if needed 97 | let nodePath = ''; 98 | if (isJsFile) { 99 | try { 100 | nodePath = execSync('where node').toString().split('\n')[0].trim(); 101 | spinner.text = `Using Node.js at: ${nodePath}`; 102 | } catch (e) { 103 | spinner.warn('Could not find Node.js. Will try to use the system default.'); 104 | nodePath = 'node'; // Hope it's in PATH 105 | } 106 | } 107 | 108 | // Create a batch script to handle the execution properly 109 | const batchDir = path.join(os.tmpdir(), 'filezap-launcher'); 110 | fs.ensureDirSync(batchDir); 111 | 112 | const batchPath = path.join(batchDir, 'filezap-launcher.bat'); 113 | 114 | // The batch file will handle proper execution with error handling 115 | const batchContent = isJsFile ? 116 | `@echo off 117 | rem FileZap Launcher Script 118 | if not exist "${filezapPath.replace(/\\/g, '\\\\')}" ( 119 | echo FileZap executable not found: ${filezapPath.replace(/\\/g, '\\\\')} 120 | echo Please reinstall FileZap or run 'npm install -g filezap' 121 | pause 122 | exit /b 1 123 | ) 124 | 125 | "${nodePath}" "${filezapPath.replace(/\\/g, '\\\\')}" share-ui %1 126 | if errorlevel 1 ( 127 | echo Error launching FileZap. Please check your installation. 128 | pause 129 | ) 130 | ` : 131 | `@echo off 132 | rem FileZap Launcher Script 133 | if not exist "${filezapPath.replace(/\\/g, '\\\\')}" ( 134 | echo FileZap executable not found: ${filezapPath.replace(/\\/g, '\\\\')} 135 | echo Please reinstall FileZap or run 'npm install -g filezap' 136 | pause 137 | exit /b 1 138 | ) 139 | 140 | "${filezapPath.replace(/\\/g, '\\\\')}" share-ui %1 141 | if errorlevel 1 ( 142 | echo Error launching FileZap. Please check your installation. 143 | pause 144 | ) 145 | `; 146 | 147 | fs.writeFileSync(batchPath, batchContent); 148 | 149 | // Set the batch file to be executable 150 | fs.chmodSync(batchPath, 0o755); 151 | 152 | // Use a simpler, more reliable registry entry that uses the batch file 153 | const registryContent = `Windows Registry Editor Version 5.00 154 | 155 | ; FileZap Share menu for files 156 | [HKEY_CURRENT_USER\\Software\\Classes\\*\\shell\\FileZapShare] 157 | @="Share via FileZap" 158 | "Icon"="%SystemRoot%\\System32\\shell32.dll,133" 159 | 160 | [HKEY_CURRENT_USER\\Software\\Classes\\*\\shell\\FileZapShare\\command] 161 | @="\\"${batchPath.replace(/\\/g, '\\\\')}\\\" \\"%1\\"" 162 | 163 | ; FileZap Share menu for folders 164 | [HKEY_CURRENT_USER\\Software\\Classes\\Directory\\shell\\FileZapShare] 165 | @="Share via FileZap" 166 | "Icon"="%SystemRoot%\\System32\\shell32.dll,133" 167 | 168 | [HKEY_CURRENT_USER\\Software\\Classes\\Directory\\shell\\FileZapShare\\command] 169 | @="\\"${batchPath.replace(/\\/g, '\\\\')}\\\" \\"%1\\"" 170 | `; 171 | 172 | // Write registry file 173 | const regFilePath = path.join(os.tmpdir(), 'filezap_shell_integration.reg'); 174 | fs.writeFileSync(regFilePath, registryContent); 175 | 176 | // Execute the registry file 177 | execSync(`regedit /s "${regFilePath}"`); 178 | spinner.succeed('Windows Explorer integration installed successfully!'); 179 | console.log(chalk.cyan('\nYou can now right-click on any file or folder and select "Share via FileZap"')); 180 | 181 | return true; 182 | } catch (error) { 183 | spinner.fail(`Failed to install Windows Explorer integration: ${error.message}`); 184 | console.log(chalk.red('\nYou may need to run this command with administrator privileges')); 185 | return false; 186 | } 187 | }, 188 | 189 | uninstall: async () => { 190 | const spinner = ora('Removing Windows Explorer context menu integration...').start(); 191 | 192 | try { 193 | const registryContent = `Windows Registry Editor Version 5.00 194 | 195 | ; Remove FileZap Share menu for files 196 | [-HKEY_CURRENT_USER\\Software\\Classes\\*\\shell\\FileZapShare] 197 | 198 | ; Remove FileZap Share menu for folders 199 | [-HKEY_CURRENT_USER\\Software\\Classes\\Directory\\shell\\FileZapShare] 200 | `; 201 | 202 | // Write registry file 203 | const regFilePath = path.join(os.tmpdir(), 'filezap_shell_uninstall.reg'); 204 | fs.writeFileSync(regFilePath, registryContent); 205 | 206 | // Execute the registry file 207 | execSync(`regedit /s "${regFilePath}"`); 208 | spinner.succeed('Windows Explorer integration removed successfully!'); 209 | return true; 210 | } catch (error) { 211 | spinner.fail(`Failed to remove Windows Explorer integration: ${error.message}`); 212 | console.log(chalk.red('\nYou may need to run this command with administrator privileges')); 213 | return false; 214 | } 215 | } 216 | }; 217 | 218 | // Completely rewritten macOS implementation using Automator Service 219 | const macosIntegration = { 220 | install: async () => { 221 | const spinner = ora('Adding macOS Finder context menu integration...').start(); 222 | 223 | try { 224 | // Get the path to the executable with better error handling 225 | let filezapPath = getExecutablePath(); 226 | spinner.text = `Using FileZap at: ${filezapPath}`; 227 | 228 | // Check if the path exists and is executable 229 | try { 230 | const stats = fs.statSync(filezapPath); 231 | if (!stats.isFile()) { 232 | spinner.warn(`FileZap not found at ${filezapPath}, using PATH reference`); 233 | filezapPath = 'filezap'; // Fallback to PATH 234 | } 235 | } catch (e) { 236 | spinner.warn(`FileZap not found at ${filezapPath}, using PATH reference`); 237 | filezapPath = 'filezap'; // Fallback to PATH 238 | } 239 | 240 | const servicesDir = path.join(os.homedir(), 'Library', 'Services'); 241 | fs.ensureDirSync(servicesDir); 242 | 243 | // Create the workflow directory 244 | const workflowName = 'Share via FileZap.workflow'; 245 | const workflowDir = path.join(servicesDir, workflowName); 246 | 247 | // Remove any existing damaged workflow 248 | if (fs.existsSync(workflowDir)) { 249 | spinner.text = 'Removing existing workflow...'; 250 | fs.removeSync(workflowDir); 251 | } 252 | 253 | const contentsDir = path.join(workflowDir, 'Contents'); 254 | fs.ensureDirSync(workflowDir); 255 | fs.ensureDirSync(contentsDir); 256 | 257 | // Paths to required files 258 | const infoPath = path.join(contentsDir, 'Info.plist'); 259 | const documentPath = path.join(contentsDir, 'document.wflow'); 260 | 261 | // Create Info.plist with more precise configuration 262 | spinner.text = 'Creating Info.plist...'; 263 | const infoPlist = ` 264 | 265 | 266 | 267 | AMApplicationBuild 268 | 521.1 269 | AMApplicationVersion 270 | 2.10 271 | AMDocumentVersion 272 | 2 273 | NSServices 274 | 275 | 276 | NSMenuItem 277 | 278 | default 279 | Share via FileZap 280 | 281 | NSMessage 282 | runWorkflowAsService 283 | NSRequiredContext 284 | 285 | NSApplicationIdentifier 286 | com.apple.finder 287 | 288 | NSSendFileTypes 289 | 290 | public.item 291 | 292 | 293 | 294 | 295 | `; 296 | 297 | // Create a simpler and more robust document.wflow file 298 | spinner.text = 'Creating workflow...'; 299 | const documentWflow = ` 300 | 301 | 302 | 303 | AMApplicationBuild 304 | 521.1 305 | AMApplicationVersion 306 | 2.10 307 | AMDocumentVersion 308 | 2 309 | actions 310 | 311 | 312 | action 313 | 314 | AMAccepts 315 | 316 | Container 317 | List 318 | Optional 319 | 320 | Types 321 | 322 | com.apple.cocoa.path 323 | 324 | 325 | AMActionVersion 326 | 2.0.3 327 | AMApplication 328 | 329 | Automator 330 | 331 | AMParameterProperties 332 | 333 | COMMAND_STRING 334 | 335 | CheckedForUserDefaultShell 336 | 337 | inputMethod 338 | 339 | shell 340 | 341 | source 342 | 343 | 344 | AMProvides 345 | 346 | Container 347 | List 348 | Types 349 | 350 | com.apple.cocoa.string 351 | 352 | 353 | ActionBundlePath 354 | /System/Library/Automator/Run Shell Script.action 355 | ActionName 356 | Run Shell Script 357 | ActionParameters 358 | 359 | COMMAND_STRING 360 | #!/bin/bash 361 | # Simple error handler 362 | function handle_error() { 363 | osascript -e 'display dialog "Error sharing file with FileZap: $1" buttons {"OK"} default button "OK" with icon caution' 364 | exit 1 365 | } 366 | 367 | # Ensure we have at least one file 368 | if [ $# -eq 0 ]; then 369 | handle_error "No files selected" 370 | fi 371 | 372 | # Process the first file (Automator passes all files as arguments) 373 | FILE="$1" 374 | 375 | # Check if file exists 376 | if [ ! -e "$FILE" ]; then 377 | handle_error "File not found: $FILE" 378 | fi 379 | 380 | # Try to find FileZap executable 381 | FILEZAP="${filezapPath}" 382 | 383 | # Check if executable exists 384 | if ! command -v "$FILEZAP" &> /dev/null; then 385 | # Try to locate using which 386 | FILEZAP=$(which filezap 2>/dev/null || echo "") 387 | 388 | if [ -z "$FILEZAP" ]; then 389 | handle_error "FileZap executable not found" 390 | fi 391 | fi 392 | 393 | # Launch FileZap in the background 394 | "$FILEZAP" share-ui "$FILE" & 395 | 396 | # Exit successfully 397 | exit 0 398 | 399 | CheckedForUserDefaultShell 400 | 401 | inputMethod 402 | 0 403 | shell 404 | /bin/bash 405 | source 406 | 407 | 408 | BundleIdentifier 409 | com.apple.RunShellScript 410 | CFBundleVersion 411 | 2.0.3 412 | CanShowSelectedItemsWhenRun 413 | 414 | CanShowWhenRun 415 | 416 | Category 417 | 418 | AMCategoryUtilities 419 | 420 | Class Name 421 | RunShellScriptAction 422 | InputUUID 423 | 72AE4080-2A1E-4A33-8AE9-4911427DFC2E 424 | Keywords 425 | 426 | Shell 427 | Script 428 | Command 429 | Run 430 | Unix 431 | 432 | OutputUUID 433 | 7F88CACD-F784-4551-9A42-51B732C2BDA9 434 | UUID 435 | 3D637299-A1D0-4A81-A9BC-929C2DAEF400 436 | UnlocalizedApplications 437 | 438 | Automator 439 | 440 | 441 | isViewVisible 442 | 1 443 | 444 | 445 | connectors 446 | 447 | workflowMetaData 448 | 449 | applicationBundleIDsByPath 450 | 451 | applicationPaths 452 | 453 | inputTypeIdentifier 454 | com.apple.Automator.fileSystemObject 455 | outputTypeIdentifier 456 | com.apple.Automator.nothing 457 | presentationMode 458 | 15 459 | processesInput 460 | 0 461 | serviceInputTypeIdentifier 462 | com.apple.Automator.fileSystemObject 463 | serviceOutputTypeIdentifier 464 | com.apple.Automator.nothing 465 | serviceProcessesInput 466 | 0 467 | systemImageName 468 | NSActionTemplate 469 | useAutomaticInputType 470 | 0 471 | workflowTypeIdentifier 472 | com.apple.Automator.servicesMenu 473 | 474 | 475 | `; 476 | 477 | // Write the files 478 | fs.writeFileSync(infoPath, infoPlist); 479 | fs.writeFileSync(documentPath, documentWflow); 480 | 481 | // Set proper permissions 482 | spinner.text = 'Setting proper permissions...'; 483 | try { 484 | fs.chmodSync(workflowDir, 0o755); 485 | fs.chmodSync(contentsDir, 0o755); 486 | fs.chmodSync(infoPath, 0o644); 487 | fs.chmodSync(documentPath, 0o644); 488 | } catch (permError) { 489 | spinner.warn('Could not set permissions properly. Integration may not work correctly.'); 490 | console.log(chalk.yellow(`Permission error: ${permError.message}`)); 491 | } 492 | 493 | // Refresh macOS services cache 494 | spinner.text = 'Refreshing macOS services...'; 495 | try { 496 | execSync('/System/Library/CoreServices/pbs -flush'); 497 | } catch (e) { 498 | // Older macOS versions may not have the pbs command 499 | try { 500 | execSync('killall Finder'); 501 | } catch (err) { 502 | // Ignore if it fails 503 | } 504 | } 505 | 506 | spinner.succeed('macOS Finder integration installed successfully!'); 507 | console.log(chalk.cyan('\nYou can now right-click on any file and select:')); 508 | console.log(chalk.cyan(' Services → Share via FileZap')); 509 | console.log(chalk.gray('\nYou may need to log out and log back in, or restart your Mac for changes to take effect.')); 510 | console.log(chalk.gray('If service does not appear, try running:')); 511 | console.log(chalk.gray(' killall -KILL Finder && /System/Library/CoreServices/pbs -flush\n')); 512 | 513 | return true; 514 | } catch (error) { 515 | spinner.fail(`Failed to install macOS Finder integration: ${error.message}`); 516 | console.log(chalk.red('\nYou may need to check permissions or try again with admin privileges')); 517 | console.log(chalk.yellow('\nTroubleshooting steps:')); 518 | console.log('1. Make sure FileZap is properly installed: npm list -g filezap'); 519 | console.log('2. Try installing manually:'); 520 | console.log(' - Open Automator.app'); 521 | console.log(' - Create a new Quick Action'); 522 | console.log(' - Set "Workflow receives" to "files or folders" in "Finder"'); 523 | console.log(' - Add a "Run Shell Script" action'); 524 | console.log(` - Add command: filezap share-ui "$1" &`); 525 | console.log(' - Save as "Share via FileZap"'); 526 | return false; 527 | } 528 | }, 529 | 530 | uninstall: async () => { 531 | const spinner = ora('Removing macOS Finder context menu integration...').start(); 532 | 533 | try { 534 | const workflowPath = path.join(os.homedir(), 'Library', 'Services', 'Share via FileZap.workflow'); 535 | 536 | if (fs.existsSync(workflowPath)) { 537 | fs.removeSync(workflowPath); 538 | } 539 | 540 | // Refresh macOS services cache 541 | try { 542 | execSync('/System/Library/CoreServices/pbs -flush'); 543 | } catch (e) { 544 | // Older macOS versions may not have the pbs command 545 | try { 546 | execSync('killall Finder'); 547 | } catch (err) { 548 | // Ignore if it fails 549 | } 550 | } 551 | 552 | spinner.succeed('macOS Finder integration removed successfully!'); 553 | return true; 554 | } catch (error) { 555 | spinner.fail(`Failed to remove macOS Finder integration: ${error.message}`); 556 | return false; 557 | } 558 | } 559 | }; 560 | 561 | // Update Linux integration to open a browser window 562 | const linuxIntegration = { 563 | install: async () => { 564 | const spinner = ora('Adding Linux file manager context menu integration...').start(); 565 | 566 | try { 567 | const filezapPath = getExecutablePath(); 568 | const localShareDir = path.join(os.homedir(), '.local', 'share'); 569 | 570 | // Create Nautilus scripts directory (GNOME) 571 | const nautilusDir = path.join(localShareDir, 'nautilus', 'scripts'); 572 | fs.ensureDirSync(nautilusDir); 573 | 574 | const scriptPath = path.join(nautilusDir, 'Share via FileZap'); 575 | const scriptContent = `#!/bin/bash 576 | # Nautilus script to share files using FileZap 577 | 578 | for f in "$@"; do 579 | # Open browser directly instead of terminal 580 | ${filezapPath} share-ui "$f" & 581 | done 582 | `; 583 | 584 | fs.writeFileSync(scriptPath, scriptContent); 585 | fs.chmodSync(scriptPath, 0o755); // Make executable 586 | 587 | // Create desktop service file (for KDE and other environments) 588 | const applicationsDir = path.join(localShareDir, 'applications'); 589 | fs.ensureDirSync(applicationsDir); 590 | 591 | const desktopFilePath = path.join(applicationsDir, 'filezap-share.desktop'); 592 | const desktopFileContent = `[Desktop Entry] 593 | Version=1.0 594 | Type=Service 595 | Name=Share via FileZap 596 | Exec=bash -c "${filezapPath} send %F --debug; echo 'Press Enter to close'; read" 597 | Terminal=true 598 | Icon=network-transmit 599 | MimeType=all/all; 600 | X-KDE-Priority=TopLevel 601 | `; 602 | 603 | fs.writeFileSync(desktopFilePath, desktopFileContent); 604 | fs.chmodSync(desktopFilePath, 0o755); // Make executable 605 | 606 | // Update desktop database 607 | try { 608 | execSync('update-desktop-database ~/.local/share/applications'); 609 | } catch (e) { 610 | // Command might not exist, ignore 611 | } 612 | 613 | spinner.succeed('Linux file manager integration installed successfully!'); 614 | console.log(chalk.cyan('\nFor GNOME/Nautilus: Right-click > Scripts > Share via FileZap')); 615 | console.log(chalk.cyan('For other file managers: Look for "Share via FileZap" in the context menu')); 616 | console.log(chalk.gray('You may need to restart your file manager for changes to take effect')); 617 | 618 | return true; 619 | } catch (error) { 620 | spinner.fail(`Failed to install Linux file manager integration: ${error.message}`); 621 | return false; 622 | } 623 | }, 624 | 625 | uninstall: async () => { 626 | const spinner = ora('Removing Linux file manager context menu integration...').start(); 627 | 628 | try { 629 | // Remove Nautilus script 630 | const nautilusScriptPath = path.join(os.homedir(), '.local', 'share', 'nautilus', 'scripts', 'Share via FileZap'); 631 | if (fs.existsSync(nautilusScriptPath)) { 632 | fs.unlinkSync(nautilusScriptPath); 633 | } 634 | 635 | // Remove desktop service file 636 | const desktopFilePath = path.join(os.homedir(), '.local', 'share', 'applications', 'filezap-share.desktop'); 637 | if (fs.existsSync(desktopFilePath)) { 638 | fs.unlinkSync(desktopFilePath); 639 | } 640 | 641 | // Update desktop database 642 | try { 643 | execSync('update-desktop-database ~/.local/share/applications'); 644 | } catch (e) { 645 | // Command might not exist, ignore 646 | } 647 | 648 | spinner.succeed('Linux file manager integration removed successfully!'); 649 | return true; 650 | } catch (error) { 651 | spinner.fail(`Failed to remove Linux file manager integration: ${error.message}`); 652 | return false; 653 | } 654 | } 655 | }; 656 | 657 | // Main function to install shell integration based on platform 658 | export async function installShellIntegration() { 659 | const platform = os.platform(); 660 | 661 | if (platform === 'win32') { 662 | return windowsIntegration.install(); 663 | } else if (platform === 'darwin') { 664 | return macosIntegration.install(); 665 | } else if (platform === 'linux') { 666 | return linuxIntegration.install(); 667 | } else { 668 | console.log(chalk.yellow(`Shell integration is not supported on ${platform}`)); 669 | return false; 670 | } 671 | } 672 | 673 | // Main function to uninstall shell integration based on platform 674 | export async function uninstallShellIntegration() { 675 | const platform = os.platform(); 676 | 677 | if (platform === 'win32') { 678 | return windowsIntegration.uninstall(); 679 | } else if (platform === 'darwin') { 680 | return macosIntegration.uninstall(); 681 | } else if (platform === 'linux') { 682 | return linuxIntegration.uninstall(); 683 | } else { 684 | console.log(chalk.yellow(`Shell integration is not supported on ${platform}`)); 685 | return false; 686 | } 687 | } 688 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import WebSocket, { WebSocketServer } from 'ws'; 2 | import http from 'http'; 3 | import fs from 'fs-extra'; 4 | import path from 'path'; 5 | import getPort from 'get-port'; 6 | import ip from 'ip'; 7 | import ora from 'ora'; 8 | import os from 'os'; 9 | import chalk from 'chalk'; 10 | import qrcode from 'qrcode-terminal'; 11 | import ngrok from 'ngrok'; 12 | import axios from 'axios'; 13 | import boxen from 'boxen'; 14 | import gradient from 'gradient-string'; 15 | import crypto from 'crypto'; 16 | import readline from 'readline'; 17 | import { logDebug, initDebugLogging } from '../utils/logger.js'; 18 | import { tunnelManager } from '../utils/tunnelProviders.js'; 19 | import { exec } from 'child_process'; 20 | 21 | // Base configuration 22 | const CHUNK_SIZE = 1024 * 1024; // 1MB chunks 23 | const DEBUG_LOG_PATH = path.join(os.homedir(), '.filezap', 'logs', 'debug.log'); 24 | 25 | // Function to generate a random password 26 | function generateRandomPassword(length = 6) { 27 | return Math.random().toString(36).substring(2, 2 + length).toUpperCase(); 28 | } 29 | 30 | // Function to shorten URL using TinyURL API 31 | async function shortenUrl(url) { 32 | try { 33 | const response = await axios.get(`https://tinyurl.com/api-create.php?url=${encodeURIComponent(url)}`); 34 | return response.data; 35 | } catch (error) { 36 | console.error('URL shortening failed. Using original URL.'); 37 | return url; 38 | } 39 | } 40 | 41 | // Check for existing tunnels and close them 42 | async function closeExistingTunnels() { 43 | try { 44 | logDebug('Closing any existing tunnels...'); 45 | const result = await tunnelManager.closeAllTunnels(); 46 | 47 | if (result.closed > 0) { 48 | logDebug(`Closed ${result.closed} tunnels`); 49 | } 50 | 51 | if (result.failed > 0) { 52 | logDebug(`Failed to close ${result.failed} tunnels`); 53 | logDebug('Errors:', result.errors); 54 | } 55 | 56 | return true; 57 | } catch (error) { 58 | logDebug(`Error during tunnel cleanup: ${error.message}`); 59 | return false; 60 | } 61 | } 62 | 63 | export async function startFileServer(filePath, options = {}) { 64 | // Initialize debug logging and tunnel manager 65 | const isDebug = options.debug === true; 66 | const webOnly = options.webOnly === true; 67 | const customHttpPort = options.httpPort || null; 68 | const openBrowser = options.openBrowser || false; 69 | 70 | initDebugLogging(isDebug); 71 | 72 | logDebug('Starting file server with options:', options); 73 | 74 | const spinner = ora('Starting file sharing server...').start(); 75 | let ngrokUrl = null; 76 | let ngrokTunnel = null; 77 | let shortenedUrl = null; 78 | 79 | // Set up password protection (default to random password if not provided but protection enabled) 80 | const usePassword = options.passwordProtect === true; 81 | let password = usePassword ? (options.password || generateRandomPassword()) : null; 82 | 83 | // Create a function to clean up resources for better error handling 84 | let server = null; 85 | let wss = null; 86 | let serverTimeout = null; 87 | 88 | // Add enhanced path normalization 89 | try { 90 | // Normalize file path to handle different formats 91 | filePath = path.normalize(filePath); 92 | 93 | // Handle Windows paths that may have quotes or escape characters 94 | if ((filePath.startsWith('"') && filePath.endsWith('"')) || 95 | (filePath.startsWith("'") && filePath.endsWith("'"))) { 96 | filePath = filePath.substring(1, filePath.length - 1); 97 | } 98 | 99 | // Handle duplicated paths like E:\path\"E:\path\file.txt" 100 | const match = filePath.match(/^([A-Z]:\\.*?)\\?"[A-Z]:\\/i); 101 | if (match && match[1]) { 102 | // The file path was duplicated, extract the part after the quotes 103 | const pathAfterQuotes = filePath.match(/"([A-Z]:\\.*?)"/i); 104 | if (pathAfterQuotes && pathAfterQuotes[1]) { 105 | filePath = pathAfterQuotes[1]; 106 | } 107 | } 108 | 109 | logDebug(`Normalized file path: ${filePath}`); 110 | } catch (pathError) { 111 | logDebug(`Error normalizing path: ${pathError.message}`); 112 | // Continue with the original path 113 | } 114 | 115 | async function cleanupAndExit(code = 0) { 116 | logDebug('Cleaning up resources...'); 117 | try { 118 | if (ngrokUrl) { 119 | spinner.text = 'Closing global tunnel...'; 120 | spinner.start(); 121 | try { 122 | await tunnelManager.closeTunnel(ngrokUrl); 123 | spinner.succeed('Global tunnel closed'); 124 | logDebug('Tunnel closed successfully'); 125 | } catch (e) { 126 | logDebug(`Error closing tunnel: ${e.message}`); 127 | spinner.fail(`Failed to close tunnel: ${e.message}`); 128 | } 129 | } 130 | 131 | if (wss) { 132 | logDebug('Closing WebSocket server'); 133 | wss.close(); 134 | } 135 | 136 | if (server) { 137 | logDebug('Closing HTTP server'); 138 | server.close(); 139 | } 140 | 141 | if (serverTimeout) { 142 | clearTimeout(serverTimeout); 143 | } 144 | 145 | console.log(chalk.green('File sharing ended.')); 146 | 147 | if (code !== 0 && isDebug) { 148 | console.log(chalk.yellow(`\nFor more details, check the debug log at: ${DEBUG_LOG_PATH}`)); 149 | } 150 | 151 | logDebug('Cleanup complete, exiting'); 152 | 153 | if (!isDebug || code === 0) { 154 | process.exit(code); 155 | } else { 156 | // In debug mode with errors, keep the console open 157 | console.log(chalk.yellow('\nPress Enter to exit...')); 158 | process.stdin.once('data', () => process.exit(code)); 159 | } 160 | } catch (error) { 161 | console.error('Error during cleanup:', error); 162 | process.exit(1); 163 | } 164 | } 165 | 166 | try { 167 | // Make sure any existing tunnels are closed 168 | logDebug('Checking for existing ngrok tunnels...'); 169 | await closeExistingTunnels(); 170 | 171 | // Check if file exists with improved error handling 172 | if (!fs.existsSync(filePath)) { 173 | spinner.fail(`File not found: ${filePath}`); 174 | logDebug(`File not found: ${filePath}`); 175 | 176 | // Try to provide more helpful error information 177 | console.log(chalk.yellow('\nThe file could not be found. This could be due to:')); 178 | console.log('1. The file path contains special characters that are not handled correctly'); 179 | console.log('2. The application does not have permission to access this location'); 180 | console.log('3. The file was moved or deleted since you selected it'); 181 | console.log('\nTry the following:'); 182 | console.log('1. Copy the file to a simple path (like your desktop)'); 183 | console.log('2. Try drag-and-drop the file onto the command window and use "cpd send" command'); 184 | console.log('3. Run the application as administrator if accessing protected locations'); 185 | 186 | return await cleanupAndExit(1); 187 | } 188 | 189 | // Get file details 190 | const fileName = path.basename(filePath); 191 | const fileSize = fs.statSync(filePath).size; 192 | 193 | // Get an available port for WebSocket 194 | const wsPort = await getPort(); 195 | // Get another port for HTTP server 196 | const httpPort = await getPort({port: wsPort + 1}); 197 | 198 | // Get all network interfaces 199 | const networkInterfaces = os.networkInterfaces(); 200 | const localIps = []; 201 | 202 | // Helper function to rank IP addresses by likelihood of being the main connection 203 | function rankIpAddress(ip) { 204 | // Deprioritize virtual adapters 205 | if (ip.startsWith('192.168.56.')) return 10; // VirtualBox 206 | if (ip.startsWith('172.16.')) return 5; // Docker/VM common 207 | if (ip.startsWith('10.')) return 3; // Common subnet but sometimes internal 208 | 209 | // Prioritize common home/office networks 210 | if (ip.startsWith('192.168.1.') || 211 | ip.startsWith('192.168.0.') || 212 | ip.startsWith('192.168.2.') || 213 | ip.startsWith('192.168.100.')) return 0; 214 | 215 | return 2; // Default priority 216 | } 217 | 218 | // Find all IPv4 addresses 219 | let foundIps = []; 220 | Object.keys(networkInterfaces).forEach(ifaceName => { 221 | networkInterfaces[ifaceName].forEach(iface => { 222 | if (iface.family === 'IPv4' && !iface.internal) { 223 | foundIps.push({ 224 | address: iface.address, 225 | priority: rankIpAddress(iface.address) 226 | }); 227 | } 228 | }); 229 | }); 230 | 231 | // Sort IPs by priority (lowest first) 232 | foundIps.sort((a, b) => a.priority - b.priority); 233 | 234 | // Add the sorted IPs to localIps 235 | foundIps.forEach(ip => localIps.push(ip.address)); 236 | 237 | // Fallback to the ip package if no address found 238 | if (localIps.length === 0) { 239 | localIps.push(ip.address()); 240 | } 241 | 242 | // Get the primary IP (first prioritized non-internal IPv4 address) 243 | const primaryIp = localIps[0]; 244 | // Create WebSocket server 245 | wss = new WebSocketServer({ port: wsPort }); 246 | logDebug(`WebSocket server started on port ${wsPort}`); 247 | 248 | // Create HTTP server for web interface with password protection 249 | server = http.createServer((req, res) => { 250 | // Parse URL and query parameters 251 | const url = new URL(req.url, `http://${req.headers.host}`); 252 | const params = url.searchParams; 253 | const enteredPassword = params.get('password'); 254 | const isAuthenticated = !usePassword || enteredPassword === password; 255 | 256 | if (url.pathname === '/') { 257 | res.writeHead(200, {'Content-Type': 'text/html'}); 258 | 259 | if (usePassword && !isAuthenticated) { 260 | // Show improved password form with better mobile support 261 | res.write(` 262 | 263 | 264 | 265 | 266 | 267 | FileZap - Password Required 268 | 359 | 360 | 361 |
362 |
363 | 371 |

Password Protected File

372 |
373 |

${fileName}

374 |

Size: ${(fileSize / 1024).toFixed(2)} KB

375 |
376 |

This file is password protected. Please enter the password to continue.

377 |
378 |
379 | 380 |
381 | 382 |
383 | 386 |
387 |
388 | 389 | 390 | `); 391 | } else { 392 | // Show improved download page with better mobile support 393 | res.write(` 394 | 395 | 396 | 397 | 398 | 399 | FileZap - Download ${fileName} 400 | 520 | 521 | 522 |
523 |
524 |
525 | 532 |

FileZap Transfer${usePassword ? ' SECURED' : ''}

533 |
534 | 535 |
536 |
${fileName}
537 |
Size: ${(fileSize / 1024).toFixed(2)} KB
538 |
539 | 540 | ${shortenedUrl ? ` 541 | 545 | ` : ''} 546 | 547 |

Choose your download method:

548 | Download File 549 | 550 |
551 |

Or using the command line:

552 |
filezap receive ${primaryIp} ${wsPort} "${fileName}"${usePassword ? ` --password "${password}"` : ''}
553 |
554 | 555 | 558 |
559 |
560 | 561 | 562 | `); 563 | } 564 | res.end(); 565 | } else if (url.pathname === '/status') { 566 | // New status page for context menu sharing and Alt+S shortcut 567 | res.writeHead(200, {'Content-Type': 'text/html'}); 568 | res.write(` 569 | 570 | 571 | 572 | 573 | 574 | FileZap - Sharing ${fileName} 575 | 774 | 775 | 776 |
777 |
778 | 783 |

FileZap

784 |
785 | 786 |
787 |
788 |
789 |
File is being shared
790 |
791 | 792 |
793 |
${fileName}
794 |
Size: ${(fileSize / 1024).toFixed(2)} KB
795 |
796 | 797 | ${usePassword ? ` 798 |
799 | Password Protection: 800 |

This file is protected with password: ${password}

801 |
802 | ` : ''} 803 |
804 | 805 | ${ngrokUrl ? ` 806 | 842 | ` : ''} 843 | 844 | 880 | 881 | 885 |
886 | 887 | 897 | 898 | 899 | `); 900 | res.end(); 901 | } else if (url.pathname === '/ping') { 902 | // Simple endpoint to check if server is still running 903 | res.writeHead(200, {'Content-Type': 'application/json'}); 904 | res.end(JSON.stringify({status: 'ok'})); 905 | } else if (url.pathname === '/download') { 906 | // Check password for download 907 | if (usePassword && !isAuthenticated) { 908 | res.writeHead(403, {'Content-Type': 'text/html'}); 909 | res.write(` 910 | 911 |

Access Denied

912 |

Invalid password. Please go back and enter the correct password.

913 |

Go back

914 | 915 | `); 916 | res.end(); 917 | } else { 918 | // Check if the path is a directory before attempting to read it 919 | try { 920 | const stats = fs.statSync(filePath); 921 | 922 | if (stats.isDirectory()) { 923 | // Handle directory download request - return an error 924 | res.writeHead(400, {'Content-Type': 'text/html'}); 925 | res.write(` 926 | 927 |

Cannot Download Directory

928 |

The selected path is a directory, not a file. Directories cannot be downloaded directly.

929 |

Go back

930 | 931 | `); 932 | res.end(); 933 | return; 934 | } 935 | 936 | // Only proceed if it's actually a file 937 | res.writeHead(200, { 938 | 'Content-Disposition': `attachment; filename="${fileName}"`, 939 | 'Content-Type': 'application/octet-stream', 940 | 'Content-Length': fileSize 941 | }); 942 | const fileStream = fs.createReadStream(filePath); 943 | fileStream.pipe(res); 944 | } catch (error) { 945 | // Handle any file system errors 946 | logDebug(`Error accessing file for download: ${error.message}`); 947 | res.writeHead(500, {'Content-Type': 'text/html'}); 948 | res.write(` 949 | 950 |

Error Accessing File

951 |

There was a problem accessing the requested file: ${error.message}

952 |

Go back

953 | 954 | `); 955 | res.end(); 956 | } 957 | } 958 | } else { 959 | res.writeHead(404); 960 | res.end('Not found'); 961 | } 962 | }); 963 | 964 | server.listen(httpPort); 965 | logDebug(`HTTP server started on port ${httpPort}`); 966 | 967 | // Start tunnel with a timeout and better error handling 968 | spinner.text = 'Creating secure tunnel with Serveo...'; 969 | try { 970 | logDebug('Starting Serveo tunnel...'); 971 | 972 | // Create the tunnel with timeout 973 | const timeoutPromise = new Promise((_, reject) => { 974 | setTimeout(() => reject(new Error('Tunnel creation timed out after 30 seconds')), 30000); 975 | }); 976 | 977 | const tunnelPromise = tunnelManager.createTunnel(httpPort); 978 | const tunnelResult = await Promise.race([tunnelPromise, timeoutPromise]); 979 | 980 | if (tunnelResult && tunnelResult.success && tunnelResult.url) { 981 | ngrokUrl = tunnelResult.url; 982 | ngrokTunnel = tunnelResult; 983 | logDebug(`Serveo tunnel established: ${ngrokUrl}`); 984 | 985 | // Use the shortened URL if available, otherwise use the original URL 986 | shortenedUrl = tunnelResult.shortenedUrl || tunnelResult.url; 987 | logDebug(`URL to share: ${shortenedUrl}`); 988 | 989 | spinner.succeed(`Global file sharing ready! (via Serveo)`); 990 | } else { 991 | throw new Error(tunnelResult?.error || 'Failed to create tunnel'); 992 | } 993 | } catch (tunnelError) { 994 | logDebug(`Tunnel connection error: ${tunnelError.message}`); 995 | console.log(chalk.yellow(`\n⚠️ Couldn't establish global tunnel: ${tunnelError.message}`)); 996 | console.log(chalk.gray('Falling back to local network sharing only')); 997 | 998 | if (options.forceTunnel) { 999 | console.log(chalk.red(`\nForced tunnel mode was enabled but tunnel creation failed.`)); 1000 | console.log(chalk.red(`Check your internet connection and SSH setup.`)); 1001 | } 1002 | } 1003 | 1004 | // Skip terminal output in web-only mode 1005 | if (webOnly) { 1006 | // Skip the terminal UI display 1007 | logDebug('Running in web-only mode, skipping terminal UI'); 1008 | } else { 1009 | // Show all possible connection addresses with beautified output 1010 | console.log('\n' + chalk.bgGreen.black(' READY TO SHARE ')); 1011 | console.log(chalk.cyan('\n📁 FILE INFORMATION:')); 1012 | console.log(`Name: ${fileName}`); 1013 | console.log(`Size: ${(fileSize / 1024).toFixed(2)} KB`); 1014 | 1015 | // If password is enabled, show it prominently 1016 | if (usePassword) { 1017 | console.log(chalk.magenta('\n🔐 PASSWORD PROTECTION:')); 1018 | console.log(`Password: ${chalk.bold.yellow(password)}`); 1019 | console.log(chalk.gray('Recipients will need this password to download the file.')); 1020 | } 1021 | 1022 | // If a tunnel is available, create a beautiful box with sharing info 1023 | if (ngrokUrl) { 1024 | const tunnelInfo = ngrokTunnel?.provider ? ` via ${ngrokTunnel.provider}` : ''; 1025 | const shareMessage = ` 1026 | 🌍 SHARE GLOBALLY${tunnelInfo} 1027 | 1028 | ${gradient.rainbow('Easy Share Link:')} 1029 | ${chalk.bold.green(shortenedUrl || ngrokUrl)} 1030 | ${usePassword ? `\n ${gradient.passion('Password: ' + password)}` : ''} 1031 | 1032 | ${gradient.pastel('Scan QR code to download:')}`; 1033 | 1034 | console.log(boxen(shareMessage, { 1035 | padding: 1, 1036 | margin: 1, 1037 | borderStyle: 'round', 1038 | borderColor: 'cyan', 1039 | backgroundColor: '#222' 1040 | })); 1041 | 1042 | // Create a QR code for the shortened URL or ngrok URL 1043 | qrcode.generate(shortenedUrl || ngrokUrl, {small: true}); 1044 | 1045 | // Show command for the shortened URL 1046 | if (shortenedUrl) { 1047 | console.log(chalk.yellow('\n◉ Command line shortcut:')); 1048 | console.log(`filezap get "${shortenedUrl}" "${fileName}"${usePassword ? ` --password "${password}"` : ''}`); 1049 | } 1050 | } 1051 | 1052 | // Display local network options in a nice box 1053 | const localShareMessage = ` 1054 | 🏠 LOCAL NETWORK SHARING 1055 | 1056 | ${gradient.fruit('Local Network Link:')} 1057 | ${chalk.bold.blue(`http://${primaryIp}:${httpPort}`)} 1058 | ${usePassword ? `\n ${gradient.fruit('Password: ' + password)}` : ''} 1059 | 1060 | ${gradient.cristal('Command Line:')} 1061 | ${chalk.bold.yellow(`filezap receive ${primaryIp} ${wsPort} ${fileName}${usePassword ? ` --password "${password}"` : ''}`)}`; 1062 | 1063 | console.log(boxen(localShareMessage, { 1064 | padding: 1, 1065 | margin: 1, 1066 | borderStyle: 'round', 1067 | borderColor: 'blue', 1068 | backgroundColor: '#222' 1069 | })); 1070 | 1071 | // Create a QR code for the primary IP 1072 | qrcode.generate(`http://${primaryIp}:${httpPort}${usePassword ? `?password=${password}` : ''}`, {small: true}); 1073 | 1074 | // If multiple IPs detected, show alternatives in a compact way 1075 | if (localIps.length > 1) { 1076 | console.log(chalk.yellow('\n📡 ALTERNATIVE IP ADDRESSES:')); 1077 | for (let i = 1; i < localIps.length; i++) { 1078 | console.log(`${i+1}. ${chalk.cyan(`http://${localIps[i]}:${httpPort}`)}`); 1079 | } 1080 | } 1081 | 1082 | console.log(gradient.rainbow('\n✨ Server running and ready to accept connections ✨')); 1083 | console.log(chalk.gray('Press Ctrl+C to stop sharing')); 1084 | } 1085 | 1086 | // If browser opening is requested, do it for all modes 1087 | if (openBrowser) { 1088 | try { 1089 | const statusUrl = `http://localhost:${httpPort}/status`; 1090 | logDebug(`Opening browser to ${statusUrl}`); 1091 | 1092 | // Define a function to try multiple browser opening methods 1093 | const tryOpenBrowser = async () => { 1094 | return new Promise((resolve) => { 1095 | let success = false; 1096 | 1097 | if (process.platform === 'win32') { 1098 | // Method 1: Use start command (most reliable on Windows) 1099 | exec(`cmd.exe /c start "" "${statusUrl}"`, (error) => { 1100 | if (!error) { 1101 | success = true; 1102 | resolve(true); 1103 | } else { 1104 | logDebug(`Failed to open with start command: ${error.message}`); 1105 | 1106 | // Method 2: Try PowerShell if cmd fails 1107 | exec(`powershell.exe -Command "Start-Process '${statusUrl}'"`, (psError) => { 1108 | if (!psError) { 1109 | success = true; 1110 | resolve(true); 1111 | } else { 1112 | logDebug(`Failed to open with PowerShell: ${psError.message}`); 1113 | 1114 | // Method 3: Try explorer.exe as a last resort 1115 | exec(`explorer.exe "${statusUrl}"`, (explorerError) => { 1116 | success = !explorerError; 1117 | resolve(!explorerError); 1118 | if (explorerError) { 1119 | logDebug(`Failed to open with explorer: ${explorerError.message}`); 1120 | } 1121 | }); 1122 | } 1123 | }); 1124 | } 1125 | }); 1126 | } else if (process.platform === 'darwin') { 1127 | // macOS methods 1128 | exec(`open "${statusUrl}"`, (error) => { 1129 | if (!error) { 1130 | success = true; 1131 | resolve(true); 1132 | } else { 1133 | logDebug(`Failed to open with 'open' command: ${error.message}`); 1134 | resolve(false); 1135 | } 1136 | }); 1137 | } else { 1138 | // Linux methods - try multiple commands with fallbacks 1139 | exec(`xdg-open "${statusUrl}"`, (error) => { 1140 | if (!error) { 1141 | success = true; 1142 | resolve(true); 1143 | } else { 1144 | logDebug(`Failed to open with xdg-open: ${error.message}`); 1145 | 1146 | // Try other browsers 1147 | const browsers = ['sensible-browser', 'x-www-browser', 'gnome-open', 'firefox', 'google-chrome', 'chromium-browser']; 1148 | let attemptCount = 0; 1149 | 1150 | const tryNextBrowser = (index) => { 1151 | if (index >= browsers.length) { 1152 | resolve(false); 1153 | return; 1154 | } 1155 | 1156 | exec(`${browsers[index]} "${statusUrl}"`, (browserError) => { 1157 | if (!browserError) { 1158 | success = true; 1159 | resolve(true); 1160 | } else { 1161 | logDebug(`Failed to open with ${browsers[index]}: ${browserError.message}`); 1162 | tryNextBrowser(index + 1); 1163 | } 1164 | }); 1165 | }; 1166 | 1167 | tryNextBrowser(0); 1168 | } 1169 | }); 1170 | } 1171 | 1172 | // Safety timeout 1173 | setTimeout(() => { 1174 | if (!success) { 1175 | resolve(false); 1176 | } 1177 | }, 5000); 1178 | }); 1179 | }; 1180 | 1181 | // Try to open the browser 1182 | tryOpenBrowser().then((opened) => { 1183 | if (opened) { 1184 | if (!webOnly) { 1185 | console.log(chalk.green(`\n🌐 Browser window opened with sharing details`)); 1186 | } 1187 | } else { 1188 | console.log(chalk.yellow('\n⚠️ Failed to open browser window automatically.')); 1189 | console.log(chalk.yellow(`Please open ${chalk.bold(`http://localhost:${httpPort}/status`)} in your browser.`)); 1190 | 1191 | // Provide specific instructions based on platform 1192 | if (process.platform === 'win32') { 1193 | console.log(chalk.cyan('\nTip: Copy the URL above, then press Win+R and paste it.')); 1194 | } else if (process.platform === 'darwin') { 1195 | console.log(chalk.cyan('\nTip: Copy the URL above, then press Cmd+Space, type "Safari" and paste the URL.')); 1196 | } else { 1197 | console.log(chalk.cyan('\nTip: Copy the URL above and paste it in your browser\'s address bar.')); 1198 | } 1199 | } 1200 | }).catch(() => { 1201 | console.log(chalk.yellow('\n⚠️ Failed to open browser window automatically.')); 1202 | console.log(chalk.yellow(`Please open ${chalk.bold(`http://localhost:${httpPort}/status`)} in your browser.`)); 1203 | }); 1204 | } catch (e) { 1205 | logDebug(`Exception in browser opening logic: ${e.message}`); 1206 | console.log(chalk.yellow('\n⚠️ Failed to open browser window automatically.')); 1207 | console.log(chalk.yellow(`Please open ${chalk.bold(`http://localhost:${httpPort}/status`)} in your browser.`)); 1208 | } 1209 | } 1210 | 1211 | // Add a timeout to close the server if no connections happen 1212 | serverTimeout = setTimeout(async () => { 1213 | logDebug('Sharing timeout reached. Closing server...'); 1214 | console.log(chalk.yellow('\nInactivity timeout reached. Closing server...')); 1215 | await cleanupAndExit(); 1216 | }, 30 * 60 * 1000); // 30 minutes timeout 1217 | 1218 | // Set up cleanup on process exit 1219 | process.on('SIGINT', async () => { 1220 | logDebug('SIGINT received, stopping file sharing...'); 1221 | console.log('\nStopping file sharing...'); 1222 | clearTimeout(serverTimeout); 1223 | await cleanupAndExit(); 1224 | }); 1225 | 1226 | // Handle server errors 1227 | server.on('error', (error) => { 1228 | logDebug(`HTTP server error: ${error.message}`); 1229 | spinner.fail(`Server error: ${error.message}`); 1230 | cleanupAndExit(1); 1231 | }); 1232 | 1233 | // Handle WebSocket connections 1234 | wss.on('connection', (ws) => { 1235 | spinner.text = 'Client connected. Preparing to send file...'; 1236 | spinner.start(); 1237 | 1238 | let clientName = "Unknown client"; 1239 | let transferComplete = false; 1240 | 1241 | logDebug('New client connected'); 1242 | 1243 | ws.on('message', (message) => { 1244 | try { 1245 | const data = JSON.parse(message.toString()); 1246 | 1247 | // Check password if needed 1248 | if (usePassword && data.type === 'ready') { 1249 | if (data.password !== password) { 1250 | ws.send(JSON.stringify({ 1251 | type: 'error', 1252 | message: 'Invalid password' 1253 | })); 1254 | setTimeout(() => ws.close(), 1000); 1255 | return; 1256 | } 1257 | } 1258 | 1259 | // Handle client ready message 1260 | if (data.type === 'ready') { 1261 | clientName = data.clientName || "Unknown client"; 1262 | spinner.text = `Sending file to ${clientName}`; 1263 | 1264 | // Send file metadata 1265 | ws.send(JSON.stringify({ 1266 | type: 'metadata', 1267 | fileName, 1268 | fileSize 1269 | })); 1270 | 1271 | // Send file data after a short delay 1272 | setTimeout(() => { 1273 | const fileContent = fs.readFileSync(filePath); 1274 | ws.send(fileContent); 1275 | }, 500); 1276 | } 1277 | 1278 | // Handle successful receipt 1279 | if (data.type === 'received') { 1280 | transferComplete = true; 1281 | spinner.succeed(`File sent successfully to ${data.clientName || clientName}!`); 1282 | console.log(chalk.green('\n✓ Transfer complete!')); 1283 | 1284 | // Ask if user wants to continue sharing or exit 1285 | console.log(chalk.yellow('\nPress Ctrl+C to stop sharing or wait for more connections.')); 1286 | console.log(chalk.gray('Server will automatically close after 30 minutes of inactivity.')); 1287 | 1288 | // Reset the server timeout after successful transfer 1289 | clearTimeout(serverTimeout); 1290 | serverTimeout = setTimeout(async () => { 1291 | console.log(chalk.yellow('\nInactivity timeout reached. Closing server...')); 1292 | await cleanupAndExit(); 1293 | }, 30 * 60 * 1000); // 30 minutes timeout 1294 | 1295 | spinner.text = 'Waiting for more connections...'; 1296 | spinner.start(); 1297 | } 1298 | 1299 | // Handle pong (keep-alive response) 1300 | if (data.type === 'pong') { 1301 | // Connection is still alive 1302 | } 1303 | } catch (e) { 1304 | // Invalid JSON, ignore 1305 | } 1306 | }); 1307 | 1308 | ws.on('error', (error) => { 1309 | logDebug(`WebSocket error: ${error.message}`); 1310 | spinner.fail(`Connection error: ${error.message}`); 1311 | // Keep server running for other connections 1312 | spinner.text = 'Waiting for connections...'; 1313 | spinner.start(); 1314 | }); 1315 | 1316 | ws.on('close', () => { 1317 | // Connection closed, wait for more 1318 | spinner.text = 'Waiting for connections...'; 1319 | spinner.start(); 1320 | }); 1321 | 1322 | // Send keep-alive pings every 30 seconds 1323 | const keepAliveInterval = setInterval(() => { 1324 | if (ws.readyState === WebSocket.OPEN) { 1325 | ws.send(JSON.stringify({ type: 'ping' })); 1326 | } else { 1327 | clearInterval(keepAliveInterval); 1328 | } 1329 | }, 30000); 1330 | }); 1331 | 1332 | wss.on('error', (error) => { 1333 | logDebug(`WebSocket server error: ${error.message}`); 1334 | spinner.fail(`Server error: ${error.message}`); 1335 | cleanupAndExit(1); 1336 | }); 1337 | 1338 | } catch (error) { 1339 | logDebug(`Fatal error: ${error.message}`); 1340 | spinner.fail(`Failed to start file server: ${error.message}`); 1341 | // Clean up ngrok if there was an error 1342 | if (ngrokUrl) { 1343 | try { 1344 | await tunnelManager.closeTunnel(ngrokUrl); 1345 | } catch (e) { 1346 | logDebug(`Failed to close tunnel: ${e.message}`); 1347 | } 1348 | } 1349 | 1350 | if (isDebug) { 1351 | console.log(chalk.red('\nError details:')); 1352 | console.error(error); 1353 | console.log(chalk.yellow(`\nFor more information, check the debug log at: ${DEBUG_LOG_PATH}`)); 1354 | console.log(chalk.yellow('\nPress Enter to exit...')); 1355 | process.stdin.once('data', () => process.exit(1)); 1356 | } else { 1357 | process.exit(1); 1358 | } 1359 | } 1360 | } --------------------------------------------------------------------------------