Ever wanted to share files from your iPhone to your Windows PC, or from your laptop to another laptop without going through the hard way? In this article, Iโm going to walk you through how I built a file-sharing web app with vanilla javascript, Nodejs, express, and the socket io library.
So first open your terminal. We are going to create the project folder. This folder is going to contain the client and the server-side code.
Letโs first create the project folder.
mkdir file-share-app
cd file-share-app
mkdir public
Next, we initialize our nodejs project by installing the required modules project by running:
npm init -y
npm install express socket.io
Now we are ready to get into the code. In the public folder, create files index.html & client.js then add this boilerplate code in the index.html:
<!DOCTYPE html>
<html>
<head>
<title>Socket io File sharing</title>
<style>
* {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
}
#file-input {
border: 2px solid #00000026;
padding: 10px;
border-radius: 10px;
}
#file-input:hover {
background-color: #f1f1f1;
cursor: pointer;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100vh;
/* background: #00e5ff; */
}
button {
padding: 13px;
background: black;
border: none;
width: 140px;
border-radius: 10px;
margin-top: 30px;
color: #fff;
font-weight: bold;
cursor: pointer;
transition: .3s linear;
}
button:hover {
opacity: 0.5;
}
</style>
</head>
<body>
<div class="wrapper">
<h1>File share ๐ฆ</h1><br><br>
<input type="file" id="file-input">
<button id="share-btn">Share this file ๐</button>
<div class="dynamic-content"></div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="client.js"></script>
</body>
</html>
You should see something like this when you run node index
from the terminal.
In the client.js file, we will get all the required html elements and also initialize our socket io connection by adding const socket = io()
const fileInputElement = document.getElementById('file-input')
const shareButton = document.getElementById('share-btn')
const dynamicContent = document.querySelector('.dynamic-content')
const socket = io()
window.addEventListener('load', () => {
// run on page load
})
function downloadFile(blob, name = 'shared.txt') {
// force download received file
}
shareButton.addEventListener('click', async () => {
// handle share button press
})
Open index.js from the root directory and add this code to create our web server:
const path = require("path")
const http = require("http")
const express = require('express')
const app = express()
const server = http.createServer(app)
const port = process.env.PORT || 3000
const publicDirPath = path.join(__dirname, "/public")
app.use(express.static(publicDirPath))
server.listen(port, () => {
console.log(`server running on port ${port}! ๐ฆ`)
})
This will display the index.html file when you navigate to localhost://3000
from your browser.
Letโs initialize socket io from the server. So in the index.js, add these lines:
const socketio = require('socket.io')
const io = socketio(server)
Now letโs handle socket events when a user connects or disconnects from the server. Still in the index.js:
io.on("connection", (socket) => {
console.log('client connected ๐', socket.id)
socket.on('disconnect', () => {
// execute callback when client disconnects from server
console.log('client left the socket ๐ข', socket.id)
})
})
Our index.js should now look like this:
const path = require("path");
const http = require("http");
const express = require('express');
const socketio = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketio(server);
const port = process.env.PORT || 3000;
const publicDirPath = path.join(__dirname, "/public");
app.use(express.static(publicDirPath));
io.on("connection", (socket) => {
console.log('client connected ๐', socket.id);
socket.on('disconnect', () => {
console.log('client left the socket ๐ข', socket.id);
})
})
server.listen(port, () => {
console.log(`Server is up on port ${port}!`);
})
Now letโs start sharing files by handling the file input.
shareButton.addEventListener('click', async () => {
if (fileInputElement.files.length === 0) {
alert('Choose the file you want to send ๐')
return;
}
let file = fileInputElement.files[0]
let reader = new FileReader()
reader.onload = () => {
// reader is loaded and ready
}
reader.readAsArrayBuffer(file)
})
The new FileReader()
object allows our app to asynchronously read the contents of the file selected from the HTML input element. The `reader.readArrayAsArrayBuffer(file) returns partial Blob data representing the number of bytes currently loaded as a fraction of the total.
For the app to work, we need to wait for the FileReader object to load. This is why we added reader.onload
function. In the reader.onload
, we call a function to start sharing the file with the socket.
reader.onload = () => {
let buffer = new Uint8Array(reader.result)
initFileShare({ filename: file.name, bufferSize: buffer.length }, buffer)10
}
The initFileShare
method accepts two arguments; the metadata
and the buffer
of the current chunk. The metadata
object takes the filename
and the bufferSize
. We pass the bufferSize so we can check the complete size of the file and also track whether or not the file has been completely received. You can track the progress of the file share process, but itโs beyond the scope of this article.
In the initFileShare
function, we do what I normally call โchunkingโ where we break the file into smaller 1Mb sized raw binary data chunks. Why do we perform chunking? Socket io and nodejs by default depend on memory to run asynchronous processes. And if the overall memory is used up the whole app crashes. So if we send the whole file in its raw large state, the server will overload and crash.
You can decide not to break the file into smaller buffers if only you are sharing files below 1Mb
In the initFileShare
function:
function initFileShare(metadata, buffer) {
socket.emit('file-metadata', metadata)
let chunkSize = 1024
let initialChunk = 0
while (initialChunk < metadata.bufferSize) {
let filePiece = buffer.slice(0, chunkSize)
console.log(metadata.bufferSize, filePiece.length)
socket.emit('file-chunk', filePiece)
initialChunk++;
}
}
The socket.emit('file-metadata', metadata)
line emits the metadata of the file to the WebSocket. We use a for loop to emit the file-chunk
event for every received chunk. The chunks will then be compiled and converted back into the complete file when received.
Open the index.js file:
io.on("connection", (socket) => {
console.log('client connected ๐', socket.id);
socket.on('file-metadata', metadata => {
socket.broadcast.emit('file-metadata', metadata)
})
socket.on('file-chunk', chunk => {
socket.broadcast.emit('file-chunk', chunk)
})
socket.on('disconnect', () => {
console.log('client left the socket ๐ข', socket.id);
})
})
Here we are listening for the file-metadata
& file-chunk
events from the client. When the server receives such events, we use the socket.broadcast.emit
method to broadcast the data to all connected clients except the sender. At this point, the server is done. So letโs go back to the client.js.
We listen for server-side events when the window is loaded window.addEventListener('load', () => {})
because socket io only needs to connect to the server once. Add this code to listen for server socket events:
window.addEventListener('load', () => {
let newFile = {
buffer: [],
metadata: null
}
socket.on('file-metadata', metadata => {
// received metadata โก๏ธ
})
socket.on('file-chunk', chunk => {
// received chunk โก๏ธ
})
})
socket.on()
takes in a custom event name and a callback function which sometimes contains data from the server. In our case, the file-metadata
event contains the metadata (filename, bufferSize) and the file-chunk
event contains the chunk. Now letโs listen for the file metadata and insert it into the newFile
object.
socket.on('file-metadata', metadata => {
// received metadata โก๏ธ
newFile.metadata = metadata
newFile.buffer = []
console.log('received metadata โก๏ธ')
})
When we receive a chunk:
socket.on('file-chunk', chunk => {
/** Use the dynamicContent.innerHTML to show an HTML element to the user when a chunk is received.
You can track, calculate and display progress
dynamicContent.innerHTML = `<b></b>`
**/
newFile.buffer.push(chunk)
if (newFile.buffer.length === newFile.metadata.bufferSize) {
// complete file has been received
let receivedFile = new Blob(newFile.buffer)
downloadFile(receivedFile, newFile.metadata.filename);
newFile = {}
alert('Yayy! File received ๐')
}
})
When a chunk is received, the newFile.buffer.push(chunk)
adds the new chunk to the newFile
โs buffer array. We do this so that we can rebuild the complete file with everything in place.
new Blob(newFile.buffer)
creates a new Blob
from the array of buffers so we can download later.
The downloadFile()
function takes the Blob and the file name. At this point, the complete file has been received and is ready to be downloaded. So letโs add the code which downloads the file:
function downloadFile(blob, name = 'shared.txt') {
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = blobUrl;
link.download = name;
document.body.appendChild(link);
link.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
})
);
document.body.removeChild(link);
}
The above function creates a DOMString containing a URL representing the file Blob object given in the parameter. An invisible anchor tag containing the Blob of the received file is created. We then force-click the anchor tag with the MouseEvent
โs click event. The anchor tag is removed afterward. So when the whole file is received, it is automatically downloaded with the filename.
Final words
You can host this project on Heroku or use the localtunnel tool to get a temporal web URL for the project. You can add some cool features like joining rooms or showing a progress indicator when sending or receiving files.
Open localhost://3000
in two tabs and try sending a file from one ๐ฆ๐.
Summary
Have fun sharing your files. I hope you liked this article ๐ฅ๐ฅ๐ฅ.
Follow me on
Twitter ๐๐ผ @langford_dev
YouTube channel ๐๐ผ LangfordDev