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
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>