Introduction
In this second part of our tutorial series, we will significantly enhance our decentralized file storage platform, which we built on the Celo blockchain in the first part. By adding a comprehensive file-sharing feature, we’ll transform our platform into a robust, user-friendly decentralized file sharing platform. We will not only be extending the smart contract but also greatly improving the user interface, adding various new features to create an engaging, easy-to-use application. Let’s get started!
Prerequisites
Before we dive into this tutorial, ensure you have completed the first part of the series - Implementing Celo-based Decentralized Storage. We will be extending the code written in that tutorial. You should also be comfortable with blockchain concepts, Solidity for contract development, JavaScript for scripting, and React for frontend development.
Requirements
Ensure these software are installed on your system:
- Node.js: To run our scripts.
- Metamask: A digital wallet used to interact with the Celo blockchain.
- Truffle: A development environment, testing framework, and asset pipeline for Ethereum Virtual Machine (EVM) compatible blockchains.
Building the File Sharing Platform
Updating the Smart Contract
Let’s start with the contract. We’ll be adding new methods to enable file sharing and retrieval. The new methods are shareFile
, which allows a user to share a file with others, and getSharedFiles
, which retrieves a list of files shared with a user.
// ... existing code ...
mapping (address => string[]) sharedFileHashes;
function shareFile(string memory _fileHash, address _recipient) public {
require(bytes(fileHashes[msg.sender]).length > 0, "File not found");
sharedFileHashes[_recipient].push(_fileHash);
}
function getSharedFiles() public view returns (string[] memory) {
return sharedFileHashes[msg.sender];
}
Considering the functionalities of our existing contract, and the desired enhancement to include file sharing functionality, the updated FileStorage.sol
will look as follows:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract FileStorage {
struct File {
string ipfsHash;
address owner;
bool isPublic;
mapping(address => bool) authorizedUsers;
}
mapping(bytes32 => File) private files;
mapping(address => string[]) private sharedFileHashes;
event FileAdded(bytes32 indexed fileId, string ipfsHash, address indexed owner, bool isPublic);
event FileAccessGranted(bytes32 indexed fileId, address indexed user);
event FileAccessRevoked(bytes32 indexed fileId, address indexed user);
event FileShared(bytes32 indexed fileId, address indexed recipient);
function addFile(string memory ipfsHash, bool isPublic) public {
bytes32 fileId = keccak256(abi.encodePacked(ipfsHash));
require(files[fileId].owner == address(0), "File already exists");
files[fileId].ipfsHash = ipfsHash;
files[fileId].owner = msg.sender;
files[fileId].isPublic = isPublic;
emit FileAdded(fileId, ipfsHash, msg.sender, isPublic);
}
function getFile(string memory ipfsHash) public view returns (string memory, bool) {
bytes32 fileId = keccak256(abi.encodePacked(ipfsHash));
File storage file = files[fileId];
require(file.owner != address(0), "File does not exist");
require(file.isPublic || file.owner == msg.sender || file.authorizedUsers[msg.sender], "Access denied");
return (file.ipfsHash, file.isPublic);
}
function grantAccess(string memory ipfsHash, address user) public {
bytes32 fileId = keccak256(abi.encodePacked(ipfsHash));
File storage file = files[fileId];
require(file.owner == msg.sender, "Only the file owner can grant access");
file.authorizedUsers[user] = true;
emit FileAccessGranted(fileId, user);
}
function revokeAccess(string memory ipfsHash, address user) public {
bytes32 fileId = keccak256(abi.encodePacked(ipfsHash));
File storage file = files[fileId];
require(file.owner == msg.sender, "Only the file owner can revoke access");
file.authorizedUsers[user] = false;
emit FileAccessRevoked(fileId, user);
}
function shareFile(string memory ipfsHash, address recipient) public {
bytes32 fileId = keccak256(abi.encodePacked(ipfsHash));
File storage file = files[fileId];
require(file.owner == msg.sender, "Only the file owner can share the file");
file.authorizedUsers[recipient] = true;
sharedFileHashes[recipient].push(ipfsHash);
emit FileShared(fileId, recipient);
}
function getSharedFiles() public view returns (string[] memory) {
return sharedFileHashes[msg.sender];
}
}
This updated contract now includes a mapping sharedFileHashes
to keep track of the files that each user has received. The shareFile
function ensures only the file owner can share their files. After successfully sharing, the function stores the IPFS hash of the shared file in the recipient’s array of shared files.
Also, the getSharedFiles
function allows a user to retrieve all the file hashes that have been shared with them. Please note that it returns only the IPFS hashes of the files. Users will still need to use the getFile
function to access the shared files.
Compiling and Deploying the Smart Contract
We would then update and redeploy our update contract for the changes to take effect.
- Run the following command to compile the smart contract:
truffle compile
- Run the following command to deploy the
FileStorage
smart contract to the Celo network:
truffle migrate --network alfajores
You should note the contract address for later use in our code.
Enhancing the Front End
Next, we will update our frontend application to utilize these new contract methods. Let’s break down the features we want to implement:
Sharing Files
First, we want users to be able to share their files. For this, we will create a new ShareFile
component in React.
1. ShareFile Component.
Create a ShareFile.js file and add this code:
import { Card, TextField, Button, Typography, Grid } from "@material-ui/core";
export default function ShareFile() {
const [fileHash, setFileHash] = useState('');
const [recipient, setRecipient] = useState('');
const shareFile = async () => {
const result = await contract.methods.shareFile(fileHash, recipient).send({ from: account });
console.log(result);
}
return (
<Card style={{ margin: "20px", padding: "20px" }}>
<Typography variant="h5">Share File</Typography>
<Grid container spacing={3} alignItems="center">
<Grid item xs={12}>
<TextField
fullWidth
label="File Hash"
variant="outlined"
value={fileHash}
onChange={e => setFileHash(e.target.value)}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Recipient Address"
variant="outlined"
value={recipient}
onChange={e => setRecipient(e.target.value)}
/>
</Grid>
<Grid item xs={12}>
<Button variant="contained" color="primary" onClick={shareFile}>Share File</Button>
</Grid>
</Grid>
</Card>
);
}
2. ViewSharedFiles Component:
After a user shares a file, we want the recipient to view the shared files. For this, we’ll create a ViewSharedFiles
component.
import { Card, Typography } from "@material-ui/core";
export default function ViewSharedFiles() {
const [sharedFiles, setSharedFiles] = useState([]);
useEffect(() => {
const fetchFiles = async () => {
const files = await contract.methods.getSharedFiles().call({ from: account });
setSharedFiles(files);
}
fetchFiles();
}, []);
return (
<Card style={{ margin: "20px", padding: "20px" }}>
<Typography variant="h5">Shared Files</Typography>
{sharedFiles.map(file => (
<Typography key={file}>{file}</Typography>
))}
</Card>
);
}
3. FileSection Component:
Our current design displays all files in a simple list. However, as users add more files, this might become unmanageable. Therefore, we will introduce a file organization system.
We can categorize files based on the type (e.g., Images, Documents, Others) and display them in separate sections on the user interface. Let’s create a FileSection
component for this.
import { Card, Typography } from "@material-ui/core";
export default function FileSection({ title, files }) {
return (
<Card style={{ margin: "20px", padding: "20px" }}>
<Typography variant="h5">{title}</Typography>
{files.map(file => (
<Typography key={file.hash}>{file.name}</Typography>
))}
</Card>
);
}
We can use this component in our main app and pass the appropriate files to each section.
// ... imports ...
export default function App() {
// ... existing code ...
const images = files.filter(file => file.type.startsWith('image/'));
const documents = files.filter(file => file.type.startsWith('text/') || file.type === 'application/pdf');
const others = files.filter(file => !images.includes(file) && !documents.includes(file));
return (
<div>
// ... existing code ...
<FileSection title="Images" files={images} />
<FileSection title="Documents" files={documents} />
<FileSection title="Others" files={others} />
</div>
);
}
Finally, In App.js
, import the new components and incorporate them into the existing code:
import React, { useState, useEffect } from 'react';
import { Contract } from 'ethers';
import { Web3Provider } from '@ethersproject/providers';
import { create as ipfsClient } from 'ipfs-http-client';
import FileStorage from './FileStorage.json';
import { Buffer } from 'buffer';
import { Container, Grid, Paper, Typography, Button, TextField, Checkbox, FormControlLabel, makeStyles } from '@material-ui/core';
import FileViewer from 'react-file-viewer';
import './App.css';
import ShareFile from './ShareFile'; // New import
import ViewSharedFiles from './ViewSharedFiles'; // New import
import FileSection from './FileSection'; // New import
// ... existing code ...
function App() {
// ... existing code ...
const images = files.filter(file => file.type.startsWith('image/'));
const documents = files.filter(file => file.type.startsWith('text/') || file.type === 'application/pdf');
const others = files.filter(file => !images.includes(file) && !documents.includes(file));
return (
<Container>
<Typography variant="h4" component="h1">Celo-based Decentralized Storage</Typography>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<Paper className={classes.paper}>
// ... existing code ...
</Paper>
</Grid>
<Grid item xs={12} sm={6}>
<Paper className={classes.paper}>
// ... existing code ...
</Paper>
</Grid>
<Grid item xs={12} sm={6}>
<Paper className={classes.paper}>
<ShareFile contract={fileStorage} account={/* Your account */} />
</Paper>
</Grid>
<Grid item xs={12} sm={6}>
<Paper className={classes.paper}>
<ViewSharedFiles contract={fileStorage} account={/* Your account */} />
</Paper>
</Grid>
</Grid>
<div>
<h2>File Preview</h2>
{selectedFile && (
<FileViewer
fileType={selectedFile.type.split('/')[1]}
filePath={URL.createObjectURL(selectedFile)}
/>
)}
</div>
<FileSection title="Images" files={images} />
<FileSection title="Documents" files={documents} />
<FileSection title="Others" files={others} />
</Container>
);
}
export default App;
This updated code now includes the functionality to share files, view shared files, and also categorizes files based on type (images, documents, and others).
Remember to install Metamask in your browser and connect to it in order for your dApp to interact with the blockchain.
Testing the Application
After making these changes, you can start your application by running npm start
in the terminal. Make sure your Metamask wallet is connected to the Celo test network.
You should now see the improved user interface. Try uploading different types of files and verify that they appear in the correct sections. Also, try the file-sharing feature by sharing a file with another account and checking that account’s shared files.
On running the command :
npm start
You should see this browser window with the application.
1. Uploading the File:
- Click on choose File and select a file from your local storage
As you can see, we are able to see the preview of the file irrespective of its format.
- Click on Upload File
- Confirm the Transaction
The File is uploaded, the ipfs hash
is recorded and tthis action is also registered on the celo blockchain.
2. Sharing the File:
-
Input the File Hash and the Recipient Address you would like to share the file with
-
Click on Share File
-
Confirm the transaction in the MetaMask pop-out window.
The action is registered on the Celo Blockchain and the file is shared with the recipient account.
On switching to the receiving the address and refreshing the page, we can see the ipfs hash
of the shared file, which can then be used for retrieval.
Conclusion
Congratulations! We have transformed our decentralized storage platform into a full-fledged file-sharing platform on the Celo blockchain. By adding new contract methods and creating a richer, more interactive user interface, we have developed an application that’s not just technically sound, but also user-friendly. This allows for secure p2p file sharing.
Next Steps
You have a strong foundation now, and there’s so much more you can do to enhance this application. Here are a few ideas:
- Implement file permissions: Allow the file owner to control who can view or download their files.
- Improve the file viewer: Add support for previewing more file types, like PDFs and videos.
- Enhance the user experience: Add user profiles, implement a search feature, or even enable users to comment on or like files.
Remember, learning is a journey. Keep experimenting, keep building, and most importantly, keep sharing your knowledge!
About the Author
Oluwalana is a blockchain developer and technical writer, experienced in creating decentralized applications on Ethereum and Celo platforms. With a passion for knowledge-sharing, Oluwalana has authored various tutorials and articles on blockchain and other emerging technologies. Follow me on Twitter for insights on blockchain and emerging tech. For professional inquiries, kindly connect witth me on LinkedIn and explore my work on GitHub.