UNPKG

net-files

Version:

Share files in browser in p2p manner. The file is not stored on the server.

306 lines (304 loc) 9.7 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Net Files</title> <style> .inputs > div { margin: 0.5rem; } .file-size::before { content: '('; } .file-size::after { content: ')'; } .file { margin-bottom: 1rem; } h1 sub { font-size: 1rem; } </style> </head> <body> <h1> net-files <sub> <a href="https://github.com/beenotung/net-files" target="_blank"> git </a> </sub> </h1> <p>P2P file sharing.</p> <p> Remark: the files are not stored on the server. The browser should be kept open when seeding files. </p> <div class="inputs"> <div> <label> slug: <input type="text" id="slugInput" /> </label> </div> <div> <input type="file" id="fileInput" multiple onchange="updateFileList()" /> </div> <div> <button onclick="share()">share</button> <button onclick="retrieve()">retrieve</button> </div> </div> <h2>File List</h2> <div id="fileList"> <div class="file"> <span class="file-name">sample.zip</span> <span class="file-size">1.5MB</span> <span class="seeding">(seeding)</span> <button class="download-button">download</button> <button class="remove-button">remove</button> <div class="remove-menu"> <button class="remove-from-me-button">remove from me</button> <button class="remove-from-network-button"> remove from network </button> <button class="cancel-remove-button">cancel removal</button> </div> <div class="progress-container"> <div> <span class="received-size"></span>/<span class="total-size"></span> </div> <progress></progress> </div> <button class="save-button">save</button> </div> </div> <script src="/socket.io/socket.io.js"></script> <script> let socket = io() socket.connect() function autoSync() { if (slugInput.value) { if (fileInput.files.length > 0) { share() } else { retrieve() } } } slugInput.value = location.hash.replace('#', '') || slugInput.value slugInput.addEventListener('change', () => { location.hash = slugInput.value autoSync() }) window.addEventListener('hashchange', () => { slugInput.value = location.hash.replace('#', '') autoSync() }) autoSync() function updateFileList() { for (let node of fileList.children) { // check for receiving files if (node.buffer) continue // check for received files if (node.file) continue node.remove() } share() } function share() { let slug = slugInput.value if (!slug) { alert('please select a slug to serve as share key') return } socket.emit('join', slug) for (let file of fileInput.files) { seedFile(file) } for (let node of fileList.children) { let file = node.file if (file) { seedFile(file) } } } function seedFile(file) { let slug = slugInput.value console.log('seeding:', { slug, file }) socket.emit('has', { slug, name: file.name, size: file.size, type: file.type, lastModified: file.lastModified, }) file.getContent = () => { return new Promise(resolve => { let reader = new FileReader() reader.onload = () => { let content = reader.result resolve(content) file.getContent = () => content } reader.readAsArrayBuffer(file) }) } } function retrieve() { let slug = slugInput.value if (!slug) { alert('please select a slug to serve as share key') return } socket.emit('join', slug) } let fileTemplate = fileList.children[0] fileTemplate.remove() socket.on('has', ({ slug, name, size, type, lastModified }) => { if (slug != slugInput.value) { socket.emit('leave', slug) return } let node = findFileNode({ name, size }) if (node) { return } node = fileTemplate.cloneNode(true) node.dataset.name = name node.dataset.size = size node.querySelector('.file-name').textContent = name node.querySelector('.file-size').textContent = formatSize(size) let downloadButton = node.querySelector('.download-button') let removeButton = node.querySelector('.remove-button') let removeMenu = node.querySelector('.remove-menu') node.querySelector('.remove-from-me-button').onclick = () => { node.remove() } node.querySelector('.remove-from-network-button').onclick = () => { socket.emit('remove', { slug, name }) node.remove() } removeMenu.hidden = true removeButton.onclick = () => { removeButton.hidden = true removeMenu.hidden = false } node.querySelector('.cancel-remove-button').onclick = () => { removeButton.hidden = false removeMenu.hidden = true } let saveButton = node.querySelector('.save-button') let progressContainer = node.querySelector('.progress-container') let receivedSizeSpan = node.querySelector('.received-size') let progressBar = node.querySelector('progress') node.querySelector('.total-size').textContent = formatSize(size) progressBar.max = size let seedingSpan = node.querySelector('.seeding') let file = findFile({ name, size }) node.buffer = [] progressContainer.hidden = true downloadButton.hidden = !!file seedingSpan.hidden = !file saveButton.hidden = true downloadButton.onclick = () => { progressContainer.hidden = false saveButton.hidden = false saveButton.disabled = true let offset = 0 socket.emit('want', { slug, name, size, offset }) receivedSizeSpan.textContent = formatSize(offset) } node.onContent = ({ offset, buffer }) => { let src = new Uint8Array(buffer) let dest = node.buffer for (let i = 0; i < src.length; i++) { dest[offset + i] = src[i] } let receivedSize = offset + src.length progressBar.value = receivedSize receivedSizeSpan.textContent = formatSize(receivedSize) if (src.length > 0) { offset += src.length socket.emit('want', { slug, name, size, offset }) return } console.log('downloaded:', { slug, name, size }) saveButton.disabled = false let arrayBuffer = new Uint8Array(node.buffer) let file = new File([arrayBuffer], name, { type, lastModified }) node.buffer = [] node.file = file seedFile(file) downloadButton.hidden = true seedingSpan.hidden = false saveButton.onclick = () => { let url = URL.createObjectURL(file) let a = document.createElement('a') a.download = name a.href = url document.body.appendChild(a) a.click() setTimeout(() => { a.remove() URL.revokeObjectURL(url) }) } } fileList.appendChild(node) }) socket.on('want', async ({ slug, name, size, offset }) => { if (slug != slugInput.value) { socket.emit('leave', slug) return } let file = findFile({ name, size }) if (!file) return let arrayBuffer = await file.getContent() let bufferSize = 200e3 let buffer = new Uint8Array( arrayBuffer.slice(offset, offset + bufferSize), ) socket.emit('content', { slug, name, size, offset, buffer }) }) socket.on('content', ({ slug, name, size, offset, buffer }) => { if (slug != slugInput.value) { socket.emit('leave', slug) return } let file = findFile({ name, size }) if (file) return let node = findFileNode({ name, size }) if (!node) return node.onContent({ offset, buffer }) }) function findFile({ name, size }) { for (let file of fileInput.files) { if (file.name == name && file.size == size) return file } return findFileNode({ name, size })?.file } function findFileNode({ name, size }) { for (let node of fileList.children) { if (node.dataset.name == name && node.dataset.size == size) { return node } } } function formatSize(size) { if (size < 1e3) return size + 'B' if (size < 1e6) return (size / 1e3).toFixed(2) + 'KB' if (size < 1e9) return (size / 1e6).toFixed(2) + 'MB' return (size / 1e9).toFixed(2) + 'GB' } </script> </body> </html>