holesail-file-system
Version:
A P2P based node package to expose your files access on the Holepunch protocol
403 lines (379 loc) • 15.1 kB
JavaScript
const DHT = require('holesail-server') //require module to start server on local port
const goodbye = require('graceful-goodbye')
const HyperDHT = require('hyperdht')
const argv = require('minimist')(process.argv.slice(2)) //required to parse cli arguments
const {
createHash
} = require('crypto'); //for connectors
//setting up the command hierarchy
if (argv.help) {
const help = require('./includes/help.js');
help.printHelp(help.helpMessage);
process.exit(-1)
}
const localServer = new DHT();
if (argv.live) {
const http = require('http');
const fs = require('fs');
const path = require('path');
const qs = require('querystring');
const server = http.createServer((req, res) => {
const basePath = '.'; // Base directory to list directories
const urlPath = decodeURIComponent(req.url);
const fullPath = path.join(basePath, urlPath);
if (req.method === 'GET') {
// Handle GET requests
fs.stat(fullPath, (err, stats) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
return;
}
if (stats.isDirectory()) {
fs.readdir(fullPath, { withFileTypes: true }, (err, files) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
return;
}
const directoryList = files
.map(file => {
const filePath = path.join(urlPath, file.name);
const downloadButton = `<a href="${filePath}" download>Download</a>`;
const deleteButton = `<button class="btn btn-danger" onclick="deleteFile('${file.name}')">Delete</button>`;
return `<tr><td><a href="${filePath}">${file.name}</a></td><td>${downloadButton} ${deleteButton}</td></tr>`;
})
.join('');
const htmlResponse = `
<!DOCTYPE html>
<html>
<head>
<title>Directory Listing: ${urlPath}</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
h1 {
color: #333;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
}
tr:hover {
background-color: #f5f5f5;
}
.btn {
background-color: #4CAF50;
border: none;
color: white;
padding: 6px 12px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 2px 0;
cursor: pointer;
border-radius: 4px;
}
.btn-danger {
background-color: #f44336;
}
.btn:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<h1>Directory Listing: ${urlPath}</h1>
<table>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
${directoryList}
</table>
<form method="POST" action="${urlPath}">
<label for="item_type">New Item Type:</label>
<select name="item_type" id="item_type">
<option value="folder">Folder</option>
<option value="file">File</option>
</select>
<label for="name">Name:</label>
<input type="text" name="name" id="name" required>
<label for="directory">Select Directory:</label>
<select name="directory" id="directory">
${getDirectoryOptions(basePath)}
<option value="." selected>Current Directory</option>
</select>
<button class="btn" type="submit">Create</button>
</form>
<script>
function deleteFile(fileName) {
if (confirm("Are you sure you want to delete '" + fileName + "'?")) {
window.location.href = "${urlPath}?delete_file=" + encodeURIComponent(fileName);
}
}
</script>
</body>
</html>
`;
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(htmlResponse);
});
} else if (stats.isFile()) {
const extension = path.extname(fullPath).toLowerCase();
const contentType = getContentType(extension);
if (contentType) {
fs.readFile(fullPath, (err, data) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error reading file.');
return;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
});
} else {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Unsupported file type.');
}
}
});
} else if (req.method === 'POST') {
// Handle POST requests for creating items and deleting files
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
const formData = qs.parse(body);
const item_type = formData['item_type'];
const name = formData['name'];
const directory = formData['directory'];
const newFullPath = path.join(basePath, directory, name);
if (item_type === 'folder') {
fs.mkdir(newFullPath, err => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error creating folder.');
return;
}
res.writeHead(302, { 'Location': urlPath });
res.end();
});
} else if (item_type === 'file') {
fs.writeFile(newFullPath, '', err => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error creating file.');
return;
}
res.writeHead(302, { 'Location': urlPath });
res.end();
});
} else if (req.url.includes('?delete_file=')) {
const deleteFileName = qs.parse(req.url.split('?')[1])['delete_file'];
if (!deleteFileName) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Invalid delete request: File name is missing.');
return;
}
const deleteFilePath = path.join(basePath, deleteFileName);
fs.unlink(deleteFilePath, err => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error deleting file.');
return;
}
res.writeHead(302, { 'Location': urlPath });
res.end();
});
}
});
}
});
function getContentType(extension) {
// Content type mapping
switch (extension) {
case '.html':
return 'text/html';
case '.css':
return 'text/css';
case '.js':
return 'application/javascript';
case '.json':
case '.map':
return 'application/json';
case '.xml':
return 'application/xml';
case '.txt':
case '.log':
return 'text/plain';
case '.md':
return 'text/markdown';
case '.py':
return 'text/x-python';
case '.c':
return 'text/x-c';
case '.cpp':
return 'text/x-c++src';
case '.java':
return 'text/x-java';
case '.rb':
return 'text/x-ruby';
case '.php':
return 'text/x-php';
case '.pl':
return 'text/x-perl';
case '.sh':
return 'text/x-shellscript';
case '.yaml':
case '.yml':
return 'text/yaml';
case '.csv':
return 'text/csv';
case '.pptx':
return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
case '.tsv':
return 'text/tab-separated-values';
case '.pdf':
return 'application/pdf';
case '.doc':
return 'application/msword';
case '.docx':
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
case '.xls':
return 'application/vnd.ms-excel';
case '.xlsx':
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case '.ppt':
return 'application/vnd.ms-powerpoint';
case '.pptx':
return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
case '.jpg':
case '.jpeg':
return 'image/jpeg';
case '.png':
return 'image/png';
case '.gif':
return 'image/gif';
case '.bmp':
return 'image/bmp';
case '.tiff':
case '.tif':
return 'image/tiff';
case '.svg':
return 'image/svg+xml';
case '.mp3':
return 'audio/mpeg';
case '.ogg':
return 'audio/ogg';
case '.wav':
return 'audio/wav';
case '.aac':
return 'audio/aac';
case '.flac':
return 'audio/flac';
case '.mp4':
return 'video/mp4';
case '.webm':
return 'video/webm';
case '.avi':
return 'video/x-msvideo';
case '.wmv':
return 'video/x-ms-wmv';
case '.mov':
return 'video/quicktime';
case '.mkv':
return 'video/x-matroska';
default:
return 'text/plain';
}
}
function getDirectoryOptions(basePath) {
let options = '';
const directories = fs.readdirSync(basePath, { withFileTypes: true }).filter(dirent => dirent.isDirectory());
for (const dir of directories) {
options += `<option value="${dir.name}">${dir.name}</option>`;
}
return options;
}
const PORT = argv.live;
server.listen(PORT, () => {
console.log(`File manager is running on port ${PORT}`);
});
// --host
if (argv.host) {
host = argv.host
} else {
host = '127.0.0.1'
}
//to preserve seed
if (argv.connector) {
if (argv.connector.length === 64) {
connector = argv.connector
} else {
connector = createHash('sha256').update(argv.connector.toString()).digest('hex');
}
} else {
connector = null
}
localServer.serve(argv.live, host, () => {
console.log(`Server started, Now listening on ${host}:` + argv.live);
console.log(`Your connector is: ${argv.connector}`);
console.log('Server public key:', localServer.getPublicKey());
}, connector);
} else if (argv.connect) {
//give priority to connector instead of connection seed
if (argv.connector) {
if (argv.connector.length === 64) {
connector = argv.connector
} else {
connector = createHash('sha256').update(argv.connector.toString()).digest('hex');
const seed = Buffer.from(connector, 'hex');
//the keypair here is not a reference to the function above
connector = HyperDHT.keyPair(seed).publicKey.toString('hex');
}
} else {
connector = argv.connect
}
if (!argv.port) {
port = 8989
} else {
port = argv.port
}
//--host
if (argv.host) {
host = argv.host
} else {
host = '127.0.0.1'
}
const holesailClient = require('holesail-client')
const pubClient = new holesailClient(connector)
pubClient.connect(port, host, () => {
console.log(`Client setup, access on ${host}:${port}`);
console.log(`Your connector is: ${argv.connector}`);
console.log('Connected to public key:', connector);
}
)
} else {
console.log(helpMessage);
process.exit(-1)
}
goodbye(async () => {
await localServer.destroy()
})